Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

10.16 Kb · 300 lines
  1package transfer
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7	"time"
  8
  9	"gno.land/p/aib/jsonpage"
 10	"gno.land/p/moul/txlink"
 11	"gno.land/p/nt/mux/v0"
 12	"gno.land/p/nt/ufmt/v0"
 13	"gno.land/p/onbloc/json"
 14	"gno.land/r/aib/ibc/core"
 15)
 16
 17// transferRealmPath is the package path of this realm; used to compute
 18// per-voucher GRC20 registry keys. Hardcoded because we cannot read it from
 19// runtime.CurrentRealm() in v2 without the unsafe package, and Render needs
 20// it from a non-crossing render handler.
 21const transferRealmPath = "gno.land/r/aib/ibc/apps/transfer"
 22
 23func Render(path string) string {
 24	router := mux.NewRouter()
 25	router.HandleFunc("", renderHome)
 26	router.HandleFunc("denoms", renderDenoms)
 27	router.HandleFunc("denoms/ibc/{hash}", renderDenom)
 28	router.HandleFunc("total_escrow/{denom}", renderTotalEscrowForDenom)
 29	router.HandleFunc("vouchers", renderVouchers)
 30	router.HandleFunc("voucher/ibc/{hash}", renderVoucher)
 31	router.HandleFunc("voucher/ibc/{hash}/balance/{addr}", renderVoucherBalance)
 32	return router.Render(path)
 33}
 34
 35func renderHome(w *mux.ResponseWriter, r *mux.Request) {
 36	var out strings.Builder
 37	out.WriteString("# IBC transfer\n\n")
 38	out.WriteString("ICS-20 style transfer state and voucher token queries.\n\n")
 39	out.WriteString(renderTransferLinks())
 40	out.WriteString(ufmt.Sprintf("## Vouchers (%d)\n\n", denoms.Size()))
 41	if denoms.Size() > 0 {
 42		out.WriteString("| Denom | Base | Path | Supply | GRC20 |\n")
 43		out.WriteString("|-------|------|------|--------|-------|\n")
 44		denoms.IterateByOffset(0, denoms.Size(), func(_ string, v any) bool {
 45			d := v.(Denom)
 46			ibcDenom := d.IBCDenom()
 47			supply := ""
 48			grc20link := ""
 49			if inst := getVoucher(ibcDenom); inst != nil {
 50				supply = ufmt.Sprintf("[%d](/r/aib/ibc/apps/transfer:voucher/%s)", inst.token.TotalSupply(), ibcDenom)
 51				key := grc20regKey(ibcDenom)
 52				// Shorten the hash suffix: "gno.land/r/.../transfer.CAEF9C..." → keep prefix + first 4 … last 4 of hash
 53				dotIdx := strings.LastIndex(key, ".")
 54				shortKey := key
 55				if dotIdx != -1 {
 56					hash := key[dotIdx+1:]
 57					if len(hash) > 10 {
 58						shortKey = key[:dotIdx+1] + hash[:4] + "…" + hash[len(hash)-4:]
 59					}
 60				}
 61				const grc20regPath = "gno.land/r/demo/defi/grc20reg"
 62				grc20link = ufmt.Sprintf("[%s](%s:%s)", shortKey, stripDomain(grc20regPath), key)
 63			}
 64			out.WriteString(ufmt.Sprintf(
 65				"| [`%s`](/r/aib/ibc/apps/transfer:denoms/%s) | %s | %s | %s | %s |\n",
 66				shortDenom(ibcDenom), ibcDenom, d.Base, d.Path(), supply, grc20link,
 67			))
 68			return false
 69		})
 70		out.WriteString("\n")
 71	} else {
 72		out.WriteString("No vouchers yet.\n\n")
 73	}
 74	out.WriteString(ufmt.Sprintf("## Escrow (%d)\n\n", totalEscrow.Size()))
 75	if totalEscrow.Size() > 0 {
 76		out.WriteString("| Denom | Amount |\n")
 77		out.WriteString("|-------|--------|\n")
 78		totalEscrow.IterateByOffset(0, totalEscrow.Size(), func(_ string, v any) bool {
 79			c := v.(chain.Coin)
 80			out.WriteString(ufmt.Sprintf(
 81				"| [`%s`](/r/aib/ibc/apps/transfer:total_escrow/%s) | %d |\n",
 82				c.Denom, c.Denom, c.Amount,
 83			))
 84			return false
 85		})
 86		out.WriteString("\n")
 87	} else {
 88		out.WriteString("No escrow yet.\n\n")
 89	}
 90	out.WriteString("## JSON endpoints\n\n")
 91	out.WriteString("- [`denoms`](/r/aib/ibc/apps/transfer:denoms): list known IBC denoms (`?page`, `?limit`)\n")
 92	out.WriteString("- `denoms/ibc/{hash}`: get metadata for an IBC denom\n")
 93	out.WriteString("- `total_escrow/{denom}`: get total escrow tracked for a base denom\n")
 94	out.WriteString("- [`vouchers`](/r/aib/ibc/apps/transfer:vouchers): list voucher tokens (`?page`, `?limit`)\n")
 95	out.WriteString("- `voucher/ibc/{hash}`: get voucher token metadata\n")
 96	out.WriteString("- `voucher/ibc/{hash}/balance/{addr}`: get a voucher balance for an address\n\n")
 97	w.Write(out.String())
 98}
 99
100func renderDenoms(w *mux.ResponseWriter, r *mux.Request) {
101	renderNode(w, jsonpage.Render(denoms, r, nil))
102}
103
104func renderDenom(w *mux.ResponseWriter, r *mux.Request) {
105	denom := "ibc/" + r.GetVar("hash")
106	d, ok := denoms.Get(denom)
107	if !ok {
108		renderNode(w, nodeError(ufmt.Sprintf("denom %s not found", denom)))
109		return
110	}
111	renderNode(w, d.(Denom).RenderJSON())
112}
113
114func renderTotalEscrowForDenom(w *mux.ResponseWriter, r *mux.Request) {
115	denom := r.GetVar("denom")
116	var amt int64
117	x, found := totalEscrow.Get(denom)
118	if found {
119		amt = x.(chain.Coin).Amount
120	}
121	renderNode(w, json.ObjectNode("", map[string]*json.Node{
122		"denom":  json.StringNode("", denom),
123		"amount": json.NumberNode("", float64(amt)),
124	}))
125}
126
127func renderVouchers(w *mux.ResponseWriter, r *mux.Request) {
128	renderNode(w, jsonpage.Render(voucherTokens, r, func(key string, v any) *json.Node {
129		inst := v.(*voucher)
130		return json.ObjectNode("", map[string]*json.Node{
131			"denom":        json.StringNode("", key),
132			"grc20reg_key": json.StringNode("", grc20regKey(key)),
133			"name":         json.StringNode("", inst.token.GetName()),
134			"symbol":       json.StringNode("", inst.token.GetSymbol()),
135			"decimals":     json.NumberNode("", float64(inst.token.GetDecimals())),
136			"total_supply": json.NumberNode("", float64(inst.token.TotalSupply())),
137		})
138	}))
139}
140
141func renderVoucher(w *mux.ResponseWriter, r *mux.Request) {
142	ibcDenom := "ibc/" + r.GetVar("hash")
143	inst := getVoucher(ibcDenom)
144	if inst == nil {
145		renderNode(w, nodeError("voucher token %s not found", ibcDenom))
146		return
147	}
148	renderNode(w, json.ObjectNode("", map[string]*json.Node{
149		"denom":        json.StringNode("", ibcDenom),
150		"grc20reg_key": json.StringNode("", grc20regKey(ibcDenom)),
151		"name":         json.StringNode("", inst.token.GetName()),
152		"symbol":       json.StringNode("", inst.token.GetSymbol()),
153		"decimals":     json.NumberNode("", float64(inst.token.GetDecimals())),
154		"total_supply": json.NumberNode("", float64(inst.token.TotalSupply())),
155	}))
156}
157
158func renderVoucherBalance(w *mux.ResponseWriter, r *mux.Request) {
159	ibcDenom := "ibc/" + r.GetVar("hash")
160	addr := r.GetVar("addr")
161	inst := getVoucher(ibcDenom)
162	if inst == nil {
163		renderNode(w, nodeError("voucher token %s not found", ibcDenom))
164		return
165	}
166	balance := inst.token.BalanceOf(address(addr))
167	renderNode(w, json.ObjectNode("", map[string]*json.Node{
168		"denom":   json.StringNode("", ibcDenom),
169		"address": json.StringNode("", addr),
170		"balance": json.NumberNode("", float64(balance)),
171	}))
172}
173
174// renderTransferLinks returns the "## Transfer" section: a set of txlinks
175// that wallets can turn into MsgCalls to Transfer. Each link leaves at least
176// one argument empty so the user fills it in their wallet.
177func renderTransferLinks() string {
178	var out strings.Builder
179	out.WriteString("## Transfer\n\n")
180
181	clientIDs := core.ClientIDs()
182	if len(clientIDs) == 0 {
183		out.WriteString("No IBC client registered yet — create one in [`/r/aib/ibc/core`](/r/aib/ibc/core) before transferring.\n\n")
184		return out.String()
185	}
186
187	out.WriteString("Trigger an IBC transfer from your wallet. Pick the client of the destination chain:\n\n")
188	for _, clientID := range clientIDs {
189		out.WriteString(ufmt.Sprintf(
190			"- [send via `%s`](%s) ([client details](/r/aib/ibc/core:clients/%s))\n",
191			clientID, newTransferLink(clientID, "", "").URL(), clientID,
192		))
193	}
194
195	// Per-voucher links: send back via IBC, plus local GRC20 send/approve.
196	// All pre-fill the voucher denom; the user only fills the counterparty
197	// and amount (and timeout for the IBC send) in their wallet.
198	if denoms.Size() > 0 {
199		out.WriteString("\n### Per-voucher actions\n\n")
200		denoms.IterateByOffset(0, denoms.Size(), func(_ string, v any) bool {
201			d := v.(Denom)
202			if d.IsNative() {
203				return false
204			}
205			sourceClient := d.Trace[0].ClientId
206			ibcDenom := d.IBCDenom()
207			out.WriteString(ufmt.Sprintf(
208				"- `%s` (%s): [send back](%s) via `%s` — [send](%s) — [approve](%s)\n",
209				shortDenom(ibcDenom), d.Base,
210				newTransferLink(sourceClient, "", ibcDenom).URL(),
211				sourceClient,
212				newVoucherSendLink(ibcDenom).URL(),
213				newVoucherApproveLink(ibcDenom).URL(),
214			))
215			return false
216		})
217	}
218	out.WriteString("\n")
219	return out.String()
220}
221
222// newVoucherSendLink builds a txlink to VoucherSend with the ibc denom
223// pre-filled. The user fills `to` and `amount` in their wallet.
224func newVoucherSendLink(ibcDenom string) *txlink.TxBuilder {
225	return txlink.NewLink("VoucherSend").AddArgs(
226		"ibcDenom", ibcDenom,
227		"to", "",
228		"amount", "",
229	)
230}
231
232// newVoucherApproveLink builds a txlink to VoucherApprove with the ibc denom
233// pre-filled. The user fills `spender` and `amount` in their wallet.
234func newVoucherApproveLink(ibcDenom string) *txlink.TxBuilder {
235	return txlink.NewLink("VoucherApprove").AddArgs(
236		"ibcDenom", ibcDenom,
237		"spender", "",
238		"amount", "",
239	)
240}
241
242// newTransferLink builds a txlink to the Transfer function with the given
243// pre-filled arguments. Empty values are left wallet-settable.
244// timeoutTimestamp defaults to one hour from now (in unix seconds), matching
245// what the e2e tests use; the user can still override it in their wallet.
246func newTransferLink(clientID, receiver, denom string) *txlink.TxBuilder {
247	timeoutTimestamp := strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10)
248	return txlink.NewLink("Transfer").AddArgs(
249		"clientID", clientID,
250		"receiver", receiver,
251		"denom", denom,
252		"amount", "",
253		"timeoutTimestamp", timeoutTimestamp,
254		"memo", "",
255	)
256}
257
258// stripDomain removes the domain from a gno pkg path for use in URLs.
259// "gno.land/r/demo/foo" → "/r/demo/foo"
260func stripDomain(path string) string {
261	i := strings.Index(path, "/")
262	if i != -1 {
263		return path[i:]
264	}
265	return path
266}
267
268// shortDenom shortens an IBC denom hash for display.
269// "ibc/CAEF9CABC…9D0F" (ibc/ + first 4 + … + last 4)
270func shortDenom(ibcDenom string) string {
271	const prefix = "ibc/"
272	if !strings.HasPrefix(ibcDenom, prefix) {
273		return ibcDenom
274	}
275	hash := ibcDenom[len(prefix):]
276	if len(hash) > 10 {
277		return prefix + hash[:4] + "…" + hash[len(hash)-4:]
278	}
279	return ibcDenom
280}
281
282func renderNode(w *mux.ResponseWriter, n *json.Node) {
283	bz, err := json.Marshal(n)
284	if err != nil {
285		panic(err)
286	}
287	w.Write(string(bz))
288}
289
290// grc20regKey returns the grc20reg key for a voucher ibc denom.
291// e.g. "ibc/CAEF9C..." → "gno.land/r/aib/ibc/apps/transfer.CAEF9C..."
292func grc20regKey(ibcDenom string) string {
293	return transferRealmPath + "." + ibcDenom[len("ibc/"):]
294}
295
296func nodeError(msg string, args ...any) *json.Node {
297	return json.ObjectNode("", map[string]*json.Node{
298		"error": json.StringNode("", ufmt.Sprintf(msg, args...)),
299	})
300}