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

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}