Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}