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