store.gno
6.61 Kb · 194 lines
1package transfer
2
3import (
4 "chain"
5 "strings"
6
7 "gno.land/p/demo/tokens/grc20"
8 "gno.land/p/nt/bptree/v0"
9 "gno.land/p/nt/seqid/v0"
10 "gno.land/p/nt/ufmt/v0"
11 "gno.land/r/demo/defi/grc20reg"
12)
13
14var (
15 denoms *bptree.BPTree // ibcDenom:Denom
16 totalEscrow *bptree.BPTree // clientID:*bptree.BPTree(denom:chain.Coin)
17 voucherTokens *bptree.BPTree // ibcDenom:*voucher
18
19 // nextVoucherID allocates a unique grc20 token id for each voucher. Voucher
20 // symbols are truncated IBC-hash prefixes that can collide, so a shared
21 // monotonic id keeps every token's grc20 identifier distinct.
22 nextVoucherID seqid.ID
23)
24
25// voucher holds a voucher token and its private ledger for mint/burn.
26type voucher struct {
27 token *grc20.Token
28 ledger *grc20.PrivateLedger
29}
30
31func init() {
32 denoms = bptree.NewBPTree32()
33 totalEscrow = bptree.NewBPTree32()
34 voucherTokens = bptree.NewBPTree32()
35}
36
37// escrowForClient returns the per-client denom->coin tree, creating it on first
38// access. Escrow is accounted per (client, denom) so a malicious client cannot
39// release funds escrowed under a different client (see shared-escrow drain).
40func escrowForClient(clientID string) *bptree.BPTree {
41 t := getEscrowForClient(clientID)
42 if t == nil {
43 t = bptree.NewBPTree32()
44 totalEscrow.Set(clientID, t)
45 }
46 return t
47}
48
49// getEscrowForClient returns the per-client tree without creating it. Returns
50// nil if clientID has never escrowed anything. Used by read-only paths
51// (checkEscrowForClient, rendering) so a query for an unknown client doesn't
52// allocate an empty entry in totalEscrow.
53func getEscrowForClient(clientID string) *bptree.BPTree {
54 v := totalEscrow.Get(clientID)
55 if v == nil {
56 return nil
57 }
58 return v.(*bptree.BPTree)
59}
60
61// addEscrowForClient records coins escrowed under clientID. Used when this chain
62// sends tokens over clientID (OnSendPacket); the matching release can only come
63// back through the same client.
64func addEscrowForClient(clientID string, c chain.Coin) {
65 t := escrowForClient(clientID)
66 if x := t.Get(c.Denom); x != nil {
67 c = x.(chain.Coin).Add(c)
68 }
69 t.Set(c.Denom, c)
70}
71
72// checkEscrowForClient verifies clientID locked enough of c.Denom, without
73// mutating the accounting. Read-only so callers can move tokens between the
74// check and the debit (see unescrowGRC20/unescrowNative).
75func checkEscrowForClient(clientID string, c chain.Coin) error {
76 t := getEscrowForClient(clientID)
77 if t == nil {
78 return ufmt.Errorf("insufficient escrow for client %s denom %s: have 0, want %d", clientID, c.Denom, c.Amount)
79 }
80 x := t.Get(c.Denom)
81 if x == nil {
82 return ufmt.Errorf("insufficient escrow for client %s denom %s: have 0, want %d", clientID, c.Denom, c.Amount)
83 }
84 have := x.(chain.Coin)
85 if have.Amount < c.Amount {
86 return ufmt.Errorf("insufficient escrow for client %s denom %s: have %d, want %d", clientID, c.Denom, have.Amount, c.Amount)
87 }
88 return nil
89}
90
91// debitEscrowForClient subtracts c from clientID's balance. Assumes
92// checkEscrowForClient already passed.
93func debitEscrowForClient(clientID string, c chain.Coin) {
94 t := escrowForClient(clientID)
95 x := t.Get(c.Denom)
96 have := x.(chain.Coin)
97 t.Set(c.Denom, chain.NewCoin(c.Denom, have.Amount-c.Amount))
98}
99
100func hasDenom(voucherDenom string) bool {
101 return denoms.Has(voucherDenom)
102}
103
104func setDenom(denom Denom) {
105 denoms.Set(denom.IBCDenom(), denom)
106}
107
108func getDenom(ibcDenom string) (Denom, bool) {
109 d := denoms.Get(ibcDenom)
110 if d == nil {
111 return Denom{}, false
112 }
113 return d.(Denom), true
114}
115
116// VoucherSymbol returns the grc20 token symbol for an IBC voucher denom: the IBC
117// hash truncated to grc20's MaxSymbolLen. The full voucherDenom ("ibc/<64-hex-
118// hash>") can't be a symbol because v2 grc20 enforces MaxSymbolLen and
119// [A-Za-z0-9_-] only. Since grc20reg keys tokens by rlmpath.symbol, this is also
120// the symbol component of a voucher's registry key, so callers locate a voucher
121// via grc20reg.Get(<this realm's pkgpath> + "." + VoucherSymbol(ibcDenom)).
122func VoucherSymbol(ibcDenom string) string {
123 symbol := strings.TrimPrefix(ibcDenom, "ibc/")
124 if len(symbol) > grc20.MaxSymbolLen {
125 symbol = symbol[:grc20.MaxSymbolLen]
126 }
127 return symbol
128}
129
130// getOrCreateVoucher returns the existing voucher instance for the given IBC
131// denom, or creates a new one and registers it in grc20reg for DeFi
132// discoverability. Non-crossing helper; rlm is needed to issue the cross
133// call into grc20reg.Register from this realm's frame.
134//
135// The token symbol is a truncated IBC hash (see VoucherSymbol); its name is the
136// base denom, and nextVoucherID keeps each token's grc20 id distinct. grc20reg
137// keys the token by rlmpath.symbol (not the slug), so the full-hash slug is
138// retained only as event metadata. Because the key is the truncated symbol,
139// two vouchers whose hashes share an 11-char prefix would collide on the
140// registry key (astronomically unlikely, but no longer guarded by the slug).
141func getOrCreateVoucher(_ int, rlm realm, baseDenom, voucherDenom string) *voucher {
142 if v := voucherTokens.Get(voucherDenom); v != nil {
143 return v.(*voucher)
144 }
145 token, ledger := grc20.NewToken(baseDenom, VoucherSymbol(voucherDenom), 0, nextVoucherID.Next(), rlm)
146 inst := &voucher{token: token, ledger: ledger}
147 voucherTokens.Set(voucherDenom, inst)
148 grc20reg.Register(cross(rlm), token, voucherDenom[len("ibc/"):])
149 return inst
150}
151
152// getVoucher returns the voucher instance for the given IBC denom, or nil if not
153// found.
154func getVoucher(ibcDenom string) *voucher {
155 v := voucherTokens.Get(ibcDenom)
156 if v == nil {
157 return nil
158 }
159 return v.(*voucher)
160}
161
162// VoucherBalanceOf returns the balance of a voucher token for a given address.
163// Returns 0 if the token does not exist.
164func VoucherBalanceOf(ibcDenom string, addr address) int64 {
165 inst := getVoucher(ibcDenom)
166 if inst == nil {
167 return 0
168 }
169 return inst.token.BalanceOf(addr)
170}
171
172// GRC20Alias returns a slash-free alias for a GRC20 denom (grc20reg key)
173// by replacing "/" with ":". This is safe because grc20reg slugs are
174// alphanumeric (enforced by grc20reg.Register), so colons never appear in
175// grc20reg keys, making the replacement reversible.
176// Examples:
177//
178// "gno.land/r/demo/foo" → "gno.land:r:demo:foo"
179// "gno.land/r/demo/foo.FOO" → "gno.land:r:demo:foo.FOO"
180func GRC20Alias(grc20regKey string) string {
181 return strings.ReplaceAll(grc20regKey, "/", ":")
182}
183
184// resolveGRC20Alias converts a GRC20 alias back to the grc20reg key
185// by replacing ":" with "/".
186func resolveGRC20Alias(alias string) string {
187 return strings.ReplaceAll(alias, ":", "/")
188}
189
190// isGRC20Alias returns true if the denom is a GRC20 alias.
191// GRC20 aliases always start with "gno.land:" (the colon form of "gno.land/").
192func isGRC20Alias(denom string) bool {
193 return strings.HasPrefix(denom, "gno.land:")
194}