grc20reg.gno
7.25 Kb · 204 lines
1package grc20reg
2
3import (
4 "chain"
5 "strings"
6
7 "gno.land/p/moul/md/v0"
8 "gno.land/p/nt/avl/rotree/v0"
9 "gno.land/p/nt/avl/v0"
10 "gno.land/p/nt/fqname/v0"
11 "gno.land/p/nt/grc20/v0"
12 "gno.land/p/nt/ufmt/v0"
13)
14
15var registry = avl.NewTree() // rlmPath.symbol -> *Token
16
17// Construction lives in grc20.NewToken — it takes rlm realm last
18// and binds origRealm from rlm.PkgPath() under an IsCurrent assertion.
19// The registry key is the canonical fqname rlmPath.symbol (one token per
20// realm+symbol), independent of Token.ID()'s trailing sequence id, so
21// callers can look a token up from the (realm, symbol) pair they already
22// know:
23//
24// Token, ledger := grc20.NewToken(name, symbol, decimals, id, cur)
25// key := grc20reg.Register(cross(cur), Token, "")
26
27// Register records token under its rlmPath.symbol key and returns that key.
28// Token.ID() carries a trailing sequence id (rlmPath.symbol.<id>) that keeps
29// token identities/events unique, but the registry deliberately keys by
30// rlmPath.symbol so lookups don't need to know the id, and so a realm cannot
31// register two tokens under the same symbol (overwrite/alias guard).
32func Register(cur realm, token *grc20.Token, slug string) string {
33 if token == nil {
34 panic("grc20reg: nil token")
35 }
36 if slug != "" {
37 validateSlug(slug)
38 }
39 rlmPath := cur.Previous().PkgPath()
40 key := fqname.Construct(rlmPath, token.GetSymbol())
41 // Token.ID() == key + "." + <id>; verify the token originates from the
42 // registering realm and symbol.
43 if !strings.HasPrefix(token.ID(), key+".") {
44 panic("grc20reg: token must be registered from its own realm")
45 }
46 if registry.Has(key) {
47 panic("grc20reg: token already registered")
48 }
49 registry.Set(key, token)
50 chain.Emit(
51 registerEvent,
52 "token_path", key,
53 "pkgpath", rlmPath,
54 "slug", slug,
55 "symbol", token.GetSymbol(),
56 )
57 return key
58}
59
60func Get(key string) *grc20.Token {
61 token := registry.Get(key)
62 if token == nil {
63 return nil
64 }
65 return token.(*grc20.Token)
66}
67
68func MustGet(key string) *grc20.Token {
69 token := Get(key)
70 if token == nil {
71 panic("unknown token: " + key)
72 }
73 return token
74}
75
76// Write wrappers: a registered token can be moved through the registry without
77// importing the token's realm, which is the point of a registry. What makes
78// that safe is the calling convention, so it is worth stating once here rather
79// than three times below.
80//
81// These are NOT crossing functions. `_ int, rlm realm` is the only shape that
82// gives a non-crossing realm parameter — a realm parameter in first position
83// must be named `cur`, which makes the function crossing — and the distinction
84// is load-bearing, not stylistic:
85//
86// - Crossing (`func Transfer(cur realm, …)`) mints a fresh `cur` for THIS
87// realm. RealmTeller would then bind the actor to the registry's own
88// address and the registry would spend its own balance. Useless at best.
89// - Non-crossing (`func Transfer(_ int, rlm realm, …)`) declaring-borrows to
90// the registry without a realm-context change, so `rlm` is still the
91// caller's own live token and the actor is the caller.
92//
93// The safety comes from RealmTeller's IsCurrent() assertion. The actor is
94// rlm.Address() on a token that must be the live crossing frame, so it is
95// provably the immediate caller: a stale or foreign token is refused with
96// ErrSpoofedRealm. Debiting anyone else would mean holding their live `cur`,
97// which means executing inside their frame — authority they handed over
98// deliberately, and the same trust model RealmTeller already carries.
99//
100// This is deliberately not grc20.CallerTeller. "Act as whoever called me" is
101// the confused deputy: the debited account ends up chosen by whoever the hub
102// can be induced to serve, and an intermediate realm frame silently changes who
103// pays. CallerTeller is confined to the token's own realm for that reason and
104// is not reachable from a *Token. "Act as the realm that called me, verified
105// current" has nothing to induce — the caller cannot name a victim, only
106// itself.
107//
108// Realm-only by construction: MsgCall cannot build a realm argument
109// (convertArgToGno rejects non-primitive parameter types), so a signing user
110// cannot reach these at all and there is no in-band case to guard against.
111// Users move their own tokens through the token realm's own entry points
112// (wugnot.Transfer, foo20.Transfer, …).
113
114// Transfer moves `amount` out of the CALLING REALM's own balance.
115//
116// Call it non-crossing, forwarding your own `cur`:
117//
118// grc20reg.Transfer(0, cur, "gno.land/r/demo/defi/foo20.FOO", to, 100)
119func Transfer(_ int, rlm realm, tokenKey string, to address, amount int64) {
120 checkErr(MustGet(tokenKey).RealmTeller(0, rlm).Transfer(0, rlm, to, amount))
121}
122
123// Approve sets an allowance owned by the CALLING REALM, letting `spender` draw
124// on the calling realm's balance. It does not touch the signing user's
125// allowances.
126func Approve(_ int, rlm realm, tokenKey string, spender address, amount int64) {
127 checkErr(MustGet(tokenKey).RealmTeller(0, rlm).Approve(0, rlm, spender, amount))
128}
129
130// TransferFrom spends an allowance with the CALLING REALM as the spender.
131//
132// Note the allowance direction this implies: `from` must have approved the
133// calling realm, not the signing user. That is the supported way for a realm to
134// move a user's funds — the user grants the realm an allowance, and the realm
135// draws on it as itself, so every debit is one the owner authorized against
136// that specific realm.
137func TransferFrom(_ int, rlm realm, tokenKey string, from, to address, amount int64) {
138 checkErr(MustGet(tokenKey).RealmTeller(0, rlm).TransferFrom(0, rlm, from, to, amount))
139}
140
141func checkErr(err error) {
142 if err != nil {
143 panic(err)
144 }
145}
146
147func Render(path string) string {
148 switch {
149 case path == "": // home
150 // TODO: add pagination
151 s := ""
152 count := 0
153 registry.Iterate("", "", func(key string, tokenI any) bool {
154 count++
155 token := tokenI.(*grc20.Token)
156 rlmPath, tokenID := fqname.Parse(key)
157 rlmLink := fqname.RenderLink(rlmPath, tokenID)
158 infoLink := "/r/nt/grc20reg/v0:" + key
159 s += "- " + md.Bold(md.EscapeText(token.GetName())) + " - " + rlmLink + " - " + md.Link("info", infoLink) + "\n"
160 return false
161 })
162 if count == 0 {
163 return "No registered token."
164 }
165 return s
166 default: // specific token
167 key := path
168 token := MustGet(key)
169 rlmPath, tokenID := fqname.Parse(key)
170 rlmLink := fqname.RenderLink(rlmPath, tokenID)
171 s := ufmt.Sprintf("# %s\n", md.EscapeText(token.GetName()))
172 s += "- symbol: " + md.Bold(md.EscapeText(token.GetSymbol())) + "\n"
173 s += ufmt.Sprintf("- realm: %s\n", rlmLink)
174 s += ufmt.Sprintf("- decimals: %d\n", token.GetDecimals())
175 s += ufmt.Sprintf("- total supply: %d\n", token.TotalSupply())
176 return s
177 }
178}
179
180const (
181 registerEvent = "register"
182 maxSlugLen = 128
183)
184
185func GetRegistry() *rotree.ReadOnlyTree {
186 return rotree.Wrap(registry, nil)
187}
188
189// validateSlug panics if the slug is too long or contains non-alphanumeric characters.
190// Only letters, digits, dashes, and underscores are allowed.
191func validateSlug(slug string) {
192 if len(slug) > maxSlugLen {
193 panic("grc20reg: slug too long")
194 }
195 for _, c := range slug {
196 if !isAlphanumeric(c) && c != '_' && c != '-' {
197 panic("grc20reg: invalid slug character: " + string(c))
198 }
199 }
200}
201
202func isAlphanumeric(c rune) bool {
203 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
204}