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

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}