Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}