1package grc20reg23import (4 "chain"5 "strings"67 "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)1415var registry = avl.NewTree() // rlmPath.symbol -> *Token1617// Construction lives in grc20.NewToken — it takes rlm realm last18// and binds origRealm from rlm.PkgPath() under an IsCurrent assertion.19// The registry key is the canonical fqname rlmPath.symbol (one token per20// realm+symbol), independent of Token.ID()'s trailing sequence id, so21// callers can look a token up from the (realm, symbol) pair they already22// know:23//24// Token, ledger := grc20.NewToken(name, symbol, decimals, id, cur)25// key := grc20reg.Register(cross(cur), Token, "")2627// Register records token under its rlmPath.symbol key and returns that key.28// Token.ID() carries a trailing sequence id (rlmPath.symbol.<id>) that keeps29// token identities/events unique, but the registry deliberately keys by30// rlmPath.symbol so lookups don't need to know the id, and so a realm cannot31// 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 the42 // 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 key58}5960func Get(key string) *grc20.Token {61 token := registry.Get(key)62 if token == nil {63 return nil64 }65 return token.(*grc20.Token)66}6768func MustGet(key string) *grc20.Token {69 token := Get(key)70 if token == nil {71 panic("unknown token: " + key)72 }73 return token74}7576// Write wrappers: a registered token can be moved through the registry without77// importing the token's realm, which is the point of a registry. What makes78// that safe is the calling convention, so it is worth stating once here rather79// than three times below.80//81// These are NOT crossing functions. `_ int, rlm realm` is the only shape that82// gives a non-crossing realm parameter — a realm parameter in first position83// must be named `cur`, which makes the function crossing — and the distinction84// is load-bearing, not stylistic:85//86// - Crossing (`func Transfer(cur realm, …)`) mints a fresh `cur` for THIS87// realm. RealmTeller would then bind the actor to the registry's own88// address and the registry would spend its own balance. Useless at best.89// - Non-crossing (`func Transfer(_ int, rlm realm, …)`) declaring-borrows to90// the registry without a realm-context change, so `rlm` is still the91// caller's own live token and the actor is the caller.92//93// The safety comes from RealmTeller's IsCurrent() assertion. The actor is94// rlm.Address() on a token that must be the live crossing frame, so it is95// provably the immediate caller: a stale or foreign token is refused with96// ErrSpoofedRealm. Debiting anyone else would mean holding their live `cur`,97// which means executing inside their frame — authority they handed over98// deliberately, and the same trust model RealmTeller already carries.99//100// This is deliberately not grc20.CallerTeller. "Act as whoever called me" is101// the confused deputy: the debited account ends up chosen by whoever the hub102// can be induced to serve, and an intermediate realm frame silently changes who103// pays. CallerTeller is confined to the token's own realm for that reason and104// is not reachable from a *Token. "Act as the realm that called me, verified105// current" has nothing to induce — the caller cannot name a victim, only106// itself.107//108// Realm-only by construction: MsgCall cannot build a realm argument109// (convertArgToGno rejects non-primitive parameter types), so a signing user110// 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 points112// (wugnot.Transfer, foo20.Transfer, …).113114// 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}122123// Approve sets an allowance owned by the CALLING REALM, letting `spender` draw124// on the calling realm's balance. It does not touch the signing user's125// 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}129130// TransferFrom spends an allowance with the CALLING REALM as the spender.131//132// Note the allowance direction this implies: `from` must have approved the133// calling realm, not the signing user. That is the supported way for a realm to134// move a user's funds — the user grants the realm an allowance, and the realm135// draws on it as itself, so every debit is one the owner authorized against136// 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}140141func checkErr(err error) {142 if err != nil {143 panic(err)144 }145}146147func Render(path string) string {148 switch {149 case path == "": // home150 // TODO: add pagination151 s := ""152 count := 0153 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:" + key159 s += "- " + md.Bold(md.EscapeText(token.GetName())) + " - " + rlmLink + " - " + md.Link("info", infoLink) + "\n"160 return false161 })162 if count == 0 {163 return "No registered token."164 }165 return s166 default: // specific token167 key := path168 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 s177 }178}179180const (181 registerEvent = "register"182 maxSlugLen = 128183)184185func GetRegistry() *rotree.ReadOnlyTree {186 return rotree.Wrap(registry, nil)187}188189// 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}201202func isAlphanumeric(c rune) bool {203 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')204}205Approve(int, rlm interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tokenKey string, spender string, amount int64)
Get(key string) *gno.land/p/nt/grc20/v0.Token
GetRegistry() *gno.land/p/nt/avl/rotree/v0.ReadOnlyTree
MustGet(key string) *gno.land/p/nt/grc20/v0.Token
Register(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, token *gno.land/p/nt/grc20/v0.Token, slug string) string
Render(path string) string
Transfer(int, rlm interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tokenKey string, to string, amount int64)
TransferFrom(int, rlm interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tokenKey string, from string, to string, amount int64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.