render.gno
15.36 Kb · 443 lines
1package core
2
3import (
4 "encoding/base64"
5 "strconv"
6 "strings"
7 "time"
8
9 "gno.land/p/aib/ibc/lightclient"
10 "gno.land/p/aib/ibc/lightclient/tendermint"
11 "gno.land/p/aib/ibc/types"
12 "gno.land/p/aib/jsonpage"
13 "gno.land/p/nt/bptree/v0"
14 "gno.land/p/nt/mux/v0"
15 "gno.land/p/nt/seqid/v0"
16 "gno.land/p/nt/ufmt/v0"
17 "gno.land/p/onbloc/json"
18)
19
20func Render(path string) string {
21 router := mux.NewRouter()
22 router.HandleFunc("", renderHome)
23 router.HandleFunc("admin", renderAdmin)
24 router.HandleFunc("apps", renderApps)
25 router.HandleFunc("clients", renderClients)
26 router.HandleFunc("clients/{id}", renderClient)
27 router.HandleFunc("clients/{id}/status", renderClientStatus)
28 router.HandleFunc("clients/{id}/consensus_states", renderClientConsensusStates)
29 router.HandleFunc("clients/{id}/consensus_states/{revision_number}/{revision_height}", renderClientConsensusState)
30 router.HandleFunc("clients/{id}/next_sequence_send", renderClientNextSequenceSend)
31 router.HandleFunc("clients/{id}/packet_commitments", renderClientPacketCommitments)
32 router.HandleFunc("clients/{id}/packet_commitments/{sequence}", renderClientPacketCommitment)
33 router.HandleFunc("clients/{id}/packet_receipts", renderClientPacketReceipts)
34 router.HandleFunc("clients/{id}/packet_receipts/{sequence}", renderClientPacketReceipt)
35 router.HandleFunc("clients/{id}/packet_acknowledgements", renderClientPacketAcknowledgements)
36 router.HandleFunc("clients/{id}/packet_acknowledgements/{sequence}", renderClientPacketAcknowledgement)
37 router.HandleFunc("clients/{id}/unreceived_packets", renderClientUnreceivedPackets)
38 return router.Render(path)
39}
40
41func renderHome(w *mux.ResponseWriter, r *mux.Request) {
42 var out strings.Builder
43 out.WriteString("# IBC core\n\n")
44 out.WriteString("IBC v2 core state and query endpoints.\n\n")
45 out.WriteString(ufmt.Sprintf("## Clients (%d)\n\n", store.clientByID.Size()))
46 if store.clientByID.Size() > 0 {
47 out.WriteString("| Client | St. | Creator | Cpty | Height | CS | Seq | Cmts | Rcpts | Acks |\n")
48 out.WriteString("|--------|-----|---------|------|--------|----|-----|------|-------|------|\n")
49 store.clientByID.IterateByOffset(0, store.clientByID.Size(), func(_ string, v any) bool {
50 c := v.(*client)
51 var nConsStates int
52 switch c.typ {
53 case lightclient.Tendermint:
54 lc := c.lightClient.(*tendermint.TMLightClient)
55 nConsStates = lc.ConsensusStateByHeight.Size()
56 }
57 out.WriteString(ufmt.Sprintf(
58 "| [`%s`](/r/aib/ibc/core:clients/%s) | [%s](/r/aib/ibc/core:clients/%s/status) | %s | %s | [%s](/r/aib/ibc/core:clients/%s/consensus_states/%d/%d) | [%d](/r/aib/ibc/core:clients/%s/consensus_states) | [%d](/r/aib/ibc/core:clients/%s/next_sequence_send) | [%d](/r/aib/ibc/core:clients/%s/packet_commitments) | [%d](/r/aib/ibc/core:clients/%s/packet_receipts) | [%d](/r/aib/ibc/core:clients/%s/packet_acknowledgements) |\n",
59 c.id, c.id,
60 c.lightClient.Status(), c.id,
61 renderAddr(c.creator),
62 c.counterpartyClientID,
63 c.lightClient.LatestHeight().String(), c.id, c.lightClient.LatestHeight().RevisionNumber, c.lightClient.LatestHeight().RevisionHeight,
64 nConsStates, c.id,
65 int64(c.sendSeq)+1, c.id,
66 c.packetCommitmentsBySeq.Size(), c.id,
67 c.packetReceiptsBySeq.Size(), c.id,
68 c.packetAcknowledgementsBySeq.Size(), c.id,
69 ))
70 return false
71 })
72 out.WriteString("\n")
73 } else {
74 out.WriteString("No clients yet.\n\n")
75 }
76 out.WriteString(ufmt.Sprintf("## Apps (%d)\n\n", len(store.routes)))
77 if len(store.routes) > 0 {
78 out.WriteString("| Port | Pkg | Addr |\n")
79 out.WriteString("|------|-----|------|\n")
80 for port, app := range store.routes {
81 out.WriteString(ufmt.Sprintf("| %s | %s | %s |\n", port, renderPkgPath(app.pkgPath), renderAddr(app.address)))
82 }
83 out.WriteString("\n")
84 } else {
85 out.WriteString("No apps registered yet.\n\n")
86 }
87 out.WriteString("## JSON endpoints\n\n")
88 out.WriteString("- [`admin`](/r/aib/ibc/core:admin): admin and relayers\n")
89 out.WriteString("- [`apps`](/r/aib/ibc/core:apps): list registered IBC applications\n")
90 out.WriteString("- [`clients`](/r/aib/ibc/core:clients): list clients (`?page`, `?limit`)\n")
91 out.WriteString("- `clients/{id}`: get client details\n")
92 out.WriteString("- `clients/{id}/status`: get client status\n")
93 out.WriteString("- `clients/{id}/consensus_states`: list client consensus states (`?page`, `?limit`)\n")
94 out.WriteString("- `clients/{id}/consensus_states/{revision_number}/{revision_height}`: get a consensus state by height\n")
95 out.WriteString("- `clients/{id}/next_sequence_send`: get the next packet sequence to send\n")
96 out.WriteString("- `clients/{id}/packet_commitments`: list packet commitments (`?page`, `?limit`)\n")
97 out.WriteString("- `clients/{id}/packet_commitments/{sequence}`: get a packet commitment by sequence\n")
98 out.WriteString("- `clients/{id}/packet_receipts`: list packet receipts (`?page`, `?limit`)\n")
99 out.WriteString("- `clients/{id}/packet_receipts/{sequence}`: get a packet receipt by sequence\n")
100 out.WriteString("- `clients/{id}/packet_acknowledgements`: list packet acknowledgements (`?page`, `?limit`)\n")
101 out.WriteString("- `clients/{id}/packet_acknowledgements/{sequence}`: get a packet acknowledgement by sequence\n")
102 out.WriteString("- `clients/{id}/unreceived_packets?sequences=1,2,3`: return sequences without a local packet receipt\n\n")
103 w.Write(out.String())
104}
105
106func renderApps(w *mux.ResponseWriter, r *mux.Request) {
107 var nodes []*json.Node
108 for port, app := range store.routes {
109 nodes = append(nodes, json.ObjectNode("", map[string]*json.Node{
110 "port_id": json.StringNode("", port),
111 "pkg_path": json.StringNode("", app.pkgPath),
112 "address": json.StringNode("", app.address.String()),
113 }))
114 }
115 renderNode(w, json.ArrayNode("", nodes))
116}
117
118func renderClients(w *mux.ResponseWriter, r *mux.Request) {
119 renderNode(w, jsonpage.Render(store.clientByID, r, nil))
120}
121
122func renderClient(w *mux.ResponseWriter, r *mux.Request) {
123 id := r.GetVar("id")
124 c := store.getClient(id)
125 if c == nil {
126 renderNode(w, nodeErrorClientNotFound(id))
127 return
128 }
129 renderNode(w, c.RenderJSON())
130}
131
132func renderClientStatus(w *mux.ResponseWriter, r *mux.Request) {
133 id := r.GetVar("id")
134 c := store.getClient(id)
135 if c == nil {
136 renderNode(w, nodeErrorClientNotFound(id))
137 return
138 }
139 renderNode(w, json.ObjectNode("", map[string]*json.Node{
140 "status": json.StringNode("", c.lightClient.Status()),
141 }))
142}
143
144func renderClientConsensusStates(w *mux.ResponseWriter, r *mux.Request) {
145 id := r.GetVar("id")
146 c := store.getClient(id)
147 if c == nil {
148 renderNode(w, nodeErrorClientNotFound(id))
149 return
150 }
151 renderNode(w, c.renderConsensusStates(r))
152}
153
154func renderClientConsensusState(w *mux.ResponseWriter, r *mux.Request) {
155 var (
156 id = r.GetVar("id")
157 revisionNumber = r.GetVar("revision_number")
158 revisionHeight = r.GetVar("revision_height")
159 heightStr = revisionNumber + "/" + revisionHeight
160 )
161 height, err := types.ParseHeight(heightStr)
162 if err != nil {
163 renderNode(w, nodeError("cant parse height %s: %v", heightStr, err))
164 return
165 }
166 c := store.getClient(id)
167 if c == nil {
168 renderNode(w, nodeErrorClientNotFound(id))
169 return
170 }
171 switch c.typ {
172 case lightclient.Tendermint:
173 lc := c.lightClient.(*tendermint.TMLightClient)
174 cs, found := lc.GetConsensusState(height)
175 if !found {
176 renderNode(w, nodeError("consensus state not found for height %s", height))
177 return
178 }
179 renderNode(w, renderConsensusState(height, cs))
180 }
181}
182
183func renderClientNextSequenceSend(w *mux.ResponseWriter, r *mux.Request) {
184 id := r.GetVar("id")
185 c := store.getClient(id)
186 if c == nil {
187 renderNode(w, nodeErrorClientNotFound(id))
188 return
189 }
190 w.Write(ufmt.Sprintf(`{"next_sequence_send": %d}"`, int64(c.sendSeq)+1))
191}
192
193func renderClientPacketCommitments(w *mux.ResponseWriter, r *mux.Request) {
194 id := r.GetVar("id")
195 c := store.getClient(id)
196 if c == nil {
197 renderNode(w, nodeErrorClientNotFound(id))
198 return
199 }
200 renderCommitments(w, r, c.packetCommitmentsBySeq)
201}
202
203func renderClientPacketCommitment(w *mux.ResponseWriter, r *mux.Request) {
204 id := r.GetVar("id")
205 c := store.getClient(id)
206 if c == nil {
207 renderNode(w, nodeErrorClientNotFound(id))
208 return
209 }
210 renderCommitment(w, r, c.packetCommitmentsBySeq)
211}
212
213func renderClientPacketReceipts(w *mux.ResponseWriter, r *mux.Request) {
214 id := r.GetVar("id")
215 c := store.getClient(id)
216 if c == nil {
217 renderNode(w, nodeErrorClientNotFound(id))
218 return
219 }
220 renderCommitments(w, r, c.packetReceiptsBySeq)
221}
222
223func renderClientPacketReceipt(w *mux.ResponseWriter, r *mux.Request) {
224 id := r.GetVar("id")
225 c := store.getClient(id)
226 if c == nil {
227 renderNode(w, nodeErrorClientNotFound(id))
228 return
229 }
230 renderCommitment(w, r, c.packetReceiptsBySeq)
231}
232
233func renderClientPacketAcknowledgements(w *mux.ResponseWriter, r *mux.Request) {
234 id := r.GetVar("id")
235 c := store.getClient(id)
236 if c == nil {
237 renderNode(w, nodeErrorClientNotFound(id))
238 return
239 }
240 renderCommitments(w, r, c.packetAcknowledgementsBySeq)
241}
242
243func renderClientPacketAcknowledgement(w *mux.ResponseWriter, r *mux.Request) {
244 id := r.GetVar("id")
245 c := store.getClient(id)
246 if c == nil {
247 renderNode(w, nodeErrorClientNotFound(id))
248 return
249 }
250 renderCommitment(w, r, c.packetAcknowledgementsBySeq)
251}
252
253func renderCommitment(w *mux.ResponseWriter, r *mux.Request, tree *bptree.BPTree) {
254 seqStr := r.GetVar("sequence")
255 seq, err := strconv.ParseUint(seqStr, 10, 64)
256 if err != nil {
257 renderNode(w, nodeError("invalid sequence %q: %v", seqStr, err))
258 return
259 }
260 v, found := tree.Get(seqid.ID(seq).Binary())
261 if !found {
262 renderNode(w, nodeError("sequence %s not found", seqStr))
263 return
264 }
265 renderNode(w, commitmentNode(seq, v.([]byte)))
266}
267
268func renderCommitments(w *mux.ResponseWriter, r *mux.Request, tree *bptree.BPTree) {
269 renderNode(w, jsonpage.Render(tree, r, func(k string, v any) *json.Node {
270 id, _ := seqid.FromBinary(k)
271 return commitmentNode(uint64(id), v.([]byte))
272 }))
273}
274
275func commitmentNode(sequence uint64, data []byte) *json.Node {
276 return json.ObjectNode("", map[string]*json.Node{
277 "sequence": json.StringNode("", strconv.FormatUint(sequence, 10)),
278 "data": json.StringNode("", base64.StdEncoding.EncodeToString(data)),
279 })
280}
281
282// renderClientUnreceivedPackets returns, given a list of sequences, the
283// sequences for which no counterparty packet commitments have been received.
284// This is done by checking if a receipt exists on this chain for the packet
285// sequence.
286func renderClientUnreceivedPackets(w *mux.ResponseWriter, r *mux.Request) {
287 id := r.GetVar("id")
288 c := store.getClient(id)
289 if c == nil {
290 renderNode(w, nodeErrorClientNotFound(id))
291 return
292 }
293 seqs := strings.Split(strings.TrimSpace(r.Query.Get("sequences")), ",")
294 var unreceivedSeqs []*json.Node
295 for _, seq := range seqs {
296 seqi, err := strconv.ParseUint(seq, 10, 64)
297 if err != nil {
298 renderNode(w, nodeError(ufmt.Sprintf("invalid sequence %q: %v", seq, err)))
299 return
300 }
301 if !c.hasPacketReceipt(seqi) {
302 unreceivedSeqs = append(unreceivedSeqs, json.StringNode("", seq))
303 }
304 }
305 renderNode(w, json.ObjectNode("", map[string]*json.Node{
306 "height": renderHeight(types.GetSelfHeight()),
307 "unreceived_sequences": json.ArrayNode("", unreceivedSeqs),
308 }))
309}
310
311// Implements jsonpage.JSONRenderer
312func (c *client) RenderJSON() *json.Node {
313 m := map[string]*json.Node{
314 "id": json.StringNode("", c.id),
315 "type": json.StringNode("", c.typ),
316 "creator": json.StringNode("", c.creator.String()),
317 "status": json.StringNode("", c.lightClient.Status()),
318 "counterparty_client_id": json.StringNode("", c.counterpartyClientID),
319 }
320 var prefixes []*json.Node
321 for _, m := range c.counterpartyMerklePrefix {
322 prefixes = append(prefixes, json.StringNode("", string(m)))
323 }
324 m["counterparty_merke_prefix"] = json.ArrayNode("", prefixes)
325 switch c.typ {
326 case lightclient.Tendermint:
327 lc := c.lightClient.(*tendermint.TMLightClient)
328 m["client_state"] = json.ObjectNode("", map[string]*json.Node{
329 "chain_id": json.StringNode("", lc.ClientState.ChainID),
330 "latest_height": renderHeight(lc.ClientState.LatestHeight),
331 "frozen_height": renderHeight(lc.ClientState.FrozenHeight),
332 "trust_level": renderFraction(lc.ClientState.TrustLevel),
333 "trusting_period": renderDuration(lc.ClientState.TrustingPeriod),
334 "unbonding_period": renderDuration(lc.ClientState.UnbondingPeriod),
335 "max_clock_drift": renderDuration(lc.ClientState.MaxClockDrift),
336 "upgrade_path": renderStrings(lc.ClientState.UpgradePath),
337 })
338 lastConsState, found := lc.GetConsensusState(lc.LatestHeight())
339 if found {
340 m["last_consensus_state"] = renderConsensusState(lc.LatestHeight(), lastConsState)
341 }
342 }
343 return json.ObjectNode("", m)
344}
345
346func (c *client) renderConsensusStates(r *mux.Request) *json.Node {
347 switch c.typ {
348 case lightclient.Tendermint:
349 lc := c.lightClient.(*tendermint.TMLightClient)
350 return jsonpage.Render(lc.ConsensusStateByHeight, r, func(k string, v any) *json.Node {
351 cs := v.(*tendermint.ConsensusState)
352 height := types.ParseHeightNatSort(k)
353 return renderConsensusState(height, cs)
354 })
355 }
356 return nodeError("unhandled client type %s", c.typ)
357}
358
359func renderConsensusState(height types.Height, cs *tendermint.ConsensusState) *json.Node {
360 return json.ObjectNode("", map[string]*json.Node{
361 "height": renderHeight(height),
362 "timestamp": json.NumberNode("", float64(cs.Timestamp.Unix())),
363 "root": json.StringNode("", base64.StdEncoding.EncodeToString(cs.Root.Hash)),
364 "next_validators_hash": json.StringNode("", base64.StdEncoding.EncodeToString(cs.NextValidatorsHash)),
365 })
366}
367
368func renderPkgPath(path string) string {
369 const prefix = "gno.land"
370 if strings.HasPrefix(path, prefix) {
371 return ufmt.Sprintf("[`%s`](%s)", path, path[len(prefix):])
372 }
373 return "`" + path + "`"
374}
375
376func renderAddr(addr address) string {
377 s := addr.String()
378 short := s
379 if len(s) > 10 {
380 short = s[:6] + "…" + s[len(s)-4:]
381 }
382 return ufmt.Sprintf("[%s](/u/%s)", short, s)
383}
384
385func renderHeight(h types.Height) *json.Node {
386 return json.ObjectNode("", map[string]*json.Node{
387 "revision_number": json.NumberNode("", float64(h.RevisionNumber)),
388 "revision_height": json.NumberNode("", float64(h.RevisionHeight)),
389 })
390}
391
392func renderFraction(f tendermint.Fraction) *json.Node {
393 return json.ObjectNode("", map[string]*json.Node{
394 "numerator": json.NumberNode("", float64(f.Numerator)),
395 "denominator": json.NumberNode("", float64(f.Denominator)),
396 })
397}
398
399func renderDuration(d time.Duration) *json.Node {
400 return json.NumberNode("", d.Seconds())
401}
402
403func renderStrings(s []string) *json.Node {
404 var nodes []*json.Node
405 for _, s := range s {
406 nodes = append(nodes, json.StringNode("", s))
407 }
408 return json.ArrayNode("", nodes)
409}
410
411func renderNode(w *mux.ResponseWriter, n *json.Node) {
412 bz, err := json.Marshal(n)
413 if err != nil {
414 panic(err)
415 }
416 w.Write(string(bz))
417}
418
419func nodeErrorClientNotFound(id string) *json.Node {
420 return nodeError("client %s not found", id)
421}
422
423func nodeError(msg string, args ...any) *json.Node {
424 return json.ObjectNode("", map[string]*json.Node{
425 "error": json.StringNode("", ufmt.Sprintf(msg, args...)),
426 })
427}
428
429func renderAdmin(w *mux.ResponseWriter, r *mux.Request) {
430 var out strings.Builder
431 out.WriteString("## Admin & Relayers\n\n")
432 out.WriteString(ufmt.Sprintf("- **Admin**: %s\n", renderAddr(admin)))
433 if relayers.Size() == 0 {
434 out.WriteString("- **Relayers**: any (whitelist empty)\n")
435 } else {
436 out.WriteString("- **Relayers**:\n")
437 relayers.Iterate("", "", func(k string, v any) bool {
438 out.WriteString(ufmt.Sprintf(" - %s\n", renderAddr(address(k))))
439 return false
440 })
441 }
442 w.Write(out.String())
443}