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}