Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}