grc20reg.gno
5.41 Kb · 172 lines
1package grc20reg
2
3import (
4 "chain"
5 "strings"
6
7 "gno.land/p/demo/tokens/grc20"
8 "gno.land/p/moul/md"
9 "gno.land/p/nt/avl/v0"
10 "gno.land/p/nt/avl/v0/rotree"
11 "gno.land/p/nt/fqname/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// Transfer moves tokens owned by the immediate caller. A direct user call
77// spends the user's balance. A realm cross-call spends the calling realm's
78// balance.
79//
80// This differs from calling `Get(tokenKey).CallerTeller().Transfer` within that
81// realm, which spends the balance of the realm's previous caller. To act as
82// your own realm explicitly, use `Get(tokenKey).RealmTeller(0, cur)`.
83func Transfer(cur realm, tokenKey string, to address, amount int64) {
84 checkErr(MustGet(tokenKey).CallerTeller().Transfer(0, cur, to, amount))
85}
86
87// Approve sets an allowance owned by the immediate caller. A direct user call
88// updates the user's allowance. A realm cross-call updates the calling realm's
89// allowance.
90//
91// This differs from calling `Get(tokenKey).CallerTeller().Approve` within that
92// realm, which updates the allowance of the realm's previous caller. To act as
93// your own realm explicitly, use `Get(tokenKey).RealmTeller(0, cur)`.
94func Approve(cur realm, tokenKey string, spender address, amount int64) {
95 checkErr(MustGet(tokenKey).CallerTeller().Approve(0, cur, spender, amount))
96}
97
98// TransferFrom spends an allowance as the immediate caller. A direct user call
99// uses the user as the spender. A realm cross-call uses the calling realm as
100// the spender.
101//
102// This differs from calling `Get(tokenKey).CallerTeller().TransferFrom` within
103// that realm, which uses the realm's previous caller as the spender. To act as
104// your own realm explicitly, use `Get(tokenKey).RealmTeller(0, cur)`.
105func TransferFrom(cur realm, tokenKey string, from, to address, amount int64) {
106 checkErr(MustGet(tokenKey).CallerTeller().TransferFrom(0, cur, from, to, amount))
107}
108
109func Render(path string) string {
110 switch {
111 case path == "": // home
112 // TODO: add pagination
113 s := ""
114 count := 0
115 registry.Iterate("", "", func(key string, tokenI any) bool {
116 count++
117 token := tokenI.(*grc20.Token)
118 rlmPath, tokenID := fqname.Parse(key)
119 rlmLink := fqname.RenderLink(rlmPath, tokenID)
120 infoLink := "/r/demo/grc20reg:" + key
121 s += "- " + md.Bold(md.EscapeText(token.GetName())) + " - " + rlmLink + " - " + md.Link("info", infoLink) + "\n"
122 return false
123 })
124 if count == 0 {
125 return "No registered token."
126 }
127 return s
128 default: // specific token
129 key := path
130 token := MustGet(key)
131 rlmPath, tokenID := fqname.Parse(key)
132 rlmLink := fqname.RenderLink(rlmPath, tokenID)
133 s := ufmt.Sprintf("# %s\n", md.EscapeText(token.GetName()))
134 s += "- symbol: " + md.Bold(md.EscapeText(token.GetSymbol())) + "\n"
135 s += ufmt.Sprintf("- realm: %s\n", rlmLink)
136 s += ufmt.Sprintf("- decimals: %d\n", token.GetDecimals())
137 s += ufmt.Sprintf("- total supply: %d\n", token.TotalSupply())
138 return s
139 }
140}
141
142const (
143 registerEvent = "register"
144 maxSlugLen = 128
145)
146
147func GetRegistry() *rotree.ReadOnlyTree {
148 return rotree.Wrap(registry, nil)
149}
150
151func checkErr(err error) {
152 if err != nil {
153 panic(err)
154 }
155}
156
157// validateSlug panics if the slug is too long or contains non-alphanumeric characters.
158// Only letters, digits, dashes, and underscores are allowed.
159func validateSlug(slug string) {
160 if len(slug) > maxSlugLen {
161 panic("grc20reg: slug too long")
162 }
163 for _, c := range slug {
164 if !isAlphanumeric(c) && c != '_' && c != '-' {
165 panic("grc20reg: invalid slug character: " + string(c))
166 }
167 }
168}
169
170func isAlphanumeric(c rune) bool {
171 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
172}