nativereg.gno
6.40 Kb · 196 lines
1// Package nativereg is the metadata a native coin does not have.
2//
3// A realm-issued coin is a string and a balance. `/gno.land/r/moul/x/moultest/v0:moultest`
4// is the entire object: no name, no symbol, no decimals, no icon, no link to
5// whoever issues it. The bank knows the number and nothing else, so a wallet or
6// an explorer showing you a balance has only that path to print.
7//
8// This realm is where a denom says what it is. It is the mirror image of
9// `r/nt/grc20reg`, and the two solve opposite problems: a GRC20 is an object
10// that carries its own name and is hard to FIND, so the registry maps a key to
11// the object; a native denom is trivial to find and carries no name, so the
12// registry maps the denom to what it means.
13//
14// # Registration is proved, not claimed
15//
16// A denom embeds the package path of the realm allowed to issue it, verbatim:
17// `"/" + pkgPath + ":" + baseName` (`chain.CoinDenom`). So the issuer is
18// readable from the denom, and [Register] simply requires the caller to be it.
19// A realm registers its own coins and nobody else's, with no allowlist, no
20// owner and no signature scheme.
21//
22// A user account cannot register anything, not even a denom it holds all of:
23// there is no package path in a user call to compare against. That is the
24// intended shape, since the thing being described is the issuance, not the
25// holding.
26//
27// # What is a hint and what is a fact
28//
29// `Denom` and `Issuer` are facts: the chain enforces the relation between them.
30// Everything else is whatever the issuing realm chose to say, and `Decimals` in
31// particular is a DISPLAY hint with nothing behind it. The bank has no notion of
32// divisibility; a coin wrapped out of a 6-decimal GRC20 is still counted in
33// whole units by every balance query. Rendering 1000000 as 1.000000 is a
34// convention between this realm and whoever reads it.
35//
36// So trusting an entry means trusting the realm that wrote it, exactly as much
37// as holding its coin already means trusting it not to call `RemoveCoin` on you.
38package nativereg
39
40import (
41 "chain/runtime"
42 "strings"
43
44 "gno.land/p/nt/avl/v0"
45 "gno.land/p/nt/ufmt/v0"
46)
47
48// Entry is what one denom says about itself.
49type Entry struct {
50 // Denom is the full chain denom, the key of this registry.
51 Denom string
52 // Issuer is the package path embedded in Denom: the only realm that can
53 // issue, remove or re-describe this coin.
54 Issuer string
55 // Name, Symbol and Decimals are display metadata, chosen by the issuer.
56 // Decimals is a hint; see the package doc.
57 Name string
58 Symbol string
59 Decimals int
60 // Doc is one line about what the coin is for.
61 Doc string
62 // Height is when the entry was last written.
63 Height int64
64 // Revisions counts how many times the issuer has rewritten it.
65 Revisions int
66}
67
68const (
69 // MaxName, MaxSymbol and MaxDoc bound what an issuer can store here. The
70 // registry pays the storage for every byte a caller hands it, so the
71 // limits are the realm's own cost control, not a style rule.
72 MaxName = 40
73 MaxSymbol = 12
74 MaxDoc = 200
75 // MaxDecimals matches the GRC20 ceiling, which is where a wrapped coin's
76 // hint comes from in practice.
77 MaxDecimals = 18
78)
79
80var (
81 byDenom = avl.NewTree() // denom -> *Entry
82 denoms []string // registration order, for a stable Render
83)
84
85// Register records or rewrites what a denom means. The caller must be the realm
86// the denom names, which is the entire authorization.
87//
88// Calling it again overwrites the entry and bumps [Entry.Revisions]. An issuer
89// can always re-describe its own coin, and can never describe anybody else's.
90func Register(cur realm, denom, name, symbol string, decimals int, doc string) {
91 issuer := issuerOf(denom)
92 prev := cur.Previous()
93 if prev.PkgPath() == "" {
94 panic("only the issuing realm can register a denom; a user account has no package path to prove")
95 }
96 if prev.PkgPath() != issuer {
97 panic(ufmt.Sprintf("%s is issued by %s, not by %s", denom, issuer, prev.PkgPath()))
98 }
99 assertLen("name", name, MaxName)
100 assertLen("symbol", symbol, MaxSymbol)
101 assertLen("doc", doc, MaxDoc)
102 if decimals < 0 || decimals > MaxDecimals {
103 panic(ufmt.Sprintf("decimals is %d, must be 0 to %d", decimals, MaxDecimals))
104 }
105
106 e := &Entry{
107 Denom: denom,
108 Issuer: issuer,
109 Name: name,
110 Symbol: symbol,
111 Decimals: decimals,
112 Doc: doc,
113 Height: runtime.ChainHeight(),
114 }
115 if old := byDenom.Get(denom); old != nil {
116 e.Revisions = old.(*Entry).Revisions + 1
117 } else {
118 denoms = append(denoms, denom)
119 }
120 byDenom.Set(denom, e)
121}
122
123// Get returns the entry for a denom, and whether it has one.
124func Get(denom string) (Entry, bool) {
125 v := byDenom.Get(denom)
126 if v == nil {
127 return Entry{}, false
128 }
129 return *v.(*Entry), true
130}
131
132// MustGet is [Get] for a caller that treats an unregistered denom as a bug.
133func MustGet(denom string) Entry {
134 e, ok := Get(denom)
135 if !ok {
136 panic("no entry for " + denom)
137 }
138 return e
139}
140
141// IsRegistered reports whether a denom has said anything about itself.
142func IsRegistered(denom string) bool {
143 return byDenom.Get(denom) != nil
144}
145
146// List returns every entry, in registration order.
147func List() []Entry {
148 out := make([]Entry, 0, len(denoms))
149 for _, d := range denoms {
150 out = append(out, MustGet(d))
151 }
152 return out
153}
154
155// ByIssuer returns every entry issued by one realm, in registration order.
156func ByIssuer(pkgPath string) []Entry {
157 out := []Entry{}
158 for _, d := range denoms {
159 if e := MustGet(d); e.Issuer == pkgPath {
160 out = append(out, e)
161 }
162 }
163 return out
164}
165
166// Count returns how many denoms are registered.
167func Count() int { return byDenom.Size() }
168
169// IssuerOf returns the package path embedded in a denom, without consulting the
170// registry: the relation is in the string itself. It aborts on anything that is
171// not a realm denom, which includes `ugnot`.
172//
173// It is exported because it is useful to anybody handling a denom, registered or
174// not: given a balance, this is how you find the code that can take it away.
175func IssuerOf(denom string) string { return issuerOf(denom) }
176
177func issuerOf(denom string) string {
178 if !strings.HasPrefix(denom, "/") {
179 panic("not a realm denom (it must start with /): " + denom)
180 }
181 i := strings.LastIndex(denom, ":")
182 if i < 0 {
183 panic("not a realm denom (it must contain :): " + denom)
184 }
185 issuer := denom[1:i]
186 if issuer == "" || denom[i+1:] == "" {
187 panic("malformed denom: " + denom)
188 }
189 return issuer
190}
191
192func assertLen(what, s string, max int) {
193 if len(s) > max {
194 panic(ufmt.Sprintf("%s is %d bytes, max %d", what, len(s), max))
195 }
196}