token.gno
9.67 Kb · 406 lines
1package grc721
2
3import (
4 "chain"
5 "math/overflow"
6 "strconv"
7
8 "gno.land/p/nt/seqid/v0"
9 "gno.land/p/nt/ufmt/v0"
10)
11
12// NewToken creates a core token and its ledger.
13// rlm must be the caller's own captured cur (IsCurrent); its PkgPath becomes the unforgeable origRealm.
14//
15// Every successful call emits a NewToken event carrying the resulting Token.ID().
16// Because Token's fields are unexported, NewToken is the only way a Token can come
17// into existence, so this event makes token creation fully observable: an indexer
18// that sees the same Token.ID() announced twice knows the realm built two
19// independent ledgers behind one identifier, and that every later
20// Mint/Burn/Transfer/Approval carrying that id is ambiguous. Such a realm is
21// emitting untrustworthy events and should be flagged or ignored wholesale.
22func NewToken(name, symbol string, id seqid.ID, rlm realm) (*Token, *PrivateLedger) {
23 if !rlm.IsCurrent() {
24 panic(ErrSpoofedRealm)
25 }
26
27 pkgPath := rlm.PkgPath()
28 if pkgPath == "" {
29 panic(ErrNotRealm)
30 }
31
32 if !validName(name) {
33 panic(ErrInvalidName)
34 }
35
36 if !validSymbol(symbol) {
37 panic(ErrInvalidSymbol)
38 }
39
40 // origRealm is the host path, with any ":subpath" synthesized by realm.Sub
41 // stripped. guardHome strips the invoking path the same way, so a token
42 // created from a sub frame is not locked out of its own host realm, and a
43 // host's sub-realms are not falsely rejected. Token.ID() keeps the raw
44 // pkgPath, so identities and the NewToken event are unaffected.
45 origRealm, _, _ := chain.SplitPkgSubPath(pkgPath)
46
47 ledger := &PrivateLedger{}
48 token := &Token{
49 id: pkgPath + "." + symbol + "." + id.String(),
50 name: name,
51 symbol: symbol,
52 ledger: ledger,
53 origRealm: origRealm,
54 }
55 ledger.token = token
56
57 chain.Emit(
58 NewTokenEvent,
59 "token", token.id,
60 "name", name,
61 "symbol", symbol,
62 )
63
64 return token, ledger
65}
66
67func validName(name string) bool {
68 if name == "" || len(name) > MaxNameLen {
69 return false
70 }
71
72 for _, c := range name {
73 if c < 0x20 || c == 0x7f {
74 return false
75 }
76 }
77
78 return true
79}
80
81func validSymbol(s string) bool {
82 if s == "" || len(s) > MaxSymbolLen {
83 return false
84 }
85
86 for _, c := range s {
87 if !isAlnum(c) && c != '_' && c != '-' {
88 return false
89 }
90 }
91
92 return true
93}
94
95func isAlnum(c rune) bool {
96 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
97}
98
99func (tok *Token) GetName() string { return tok.name }
100
101func (tok *Token) GetSymbol() string { return tok.symbol }
102
103func (tok *Token) ID() string { return tok.id }
104
105func (tok *Token) TotalSupply() int64 { return tok.ledger.totalSupply }
106
107func (tok *Token) KnownAccounts() int { return tok.ledger.balances.Size() }
108
109// EIP-721: balanceOf throws for queries about the zero address,
110// since NFTs assigned to it are considered invalid.
111func (tok *Token) BalanceOf(addr address) (int64, error) {
112 if !addr.IsValid() {
113 return 0, ErrInvalidAddress
114 }
115
116 return tok.ledger.balanceOf(addr), nil
117}
118
119func (tok *Token) OwnerOf(tid TokenID) (address, error) { return tok.ledger.ownerOf(tid) }
120
121// EIP-721: getApproved throws for a token that is not a valid NFT, so a missing
122// token is ErrInvalidTokenId and a live but unapproved one is ErrTokenIdNotApproved.
123func (tok *Token) GetApproved(tid TokenID) (address, error) {
124 if !tok.ledger.exists(tid) {
125 return zeroAddress, ErrInvalidTokenId
126 }
127
128 addr := tok.ledger.tokenApprovals.Get(tid.String())
129 if addr == nil {
130 return zeroAddress, ErrTokenIdNotApproved
131 }
132
133 return addr.(address), nil
134}
135
136func (tok *Token) IsApprovedForAll(owner, operator address) bool {
137 return tok.ledger.isApprovedForAll(owner, operator)
138}
139
140func (tok *Token) RenderHome() string {
141 str := ""
142 str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol)
143 str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply)
144 str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts())
145
146 return str
147}
148
149// RegisterExtension attaches an issuer-supplied hook set. Issuer-only (holds the
150// PrivateLedger), which keeps the surface from being attacker-attachable — but the
151// attachment is still a privilege: per the Extension doc, an attached hook can veto
152// transfers (a panic aborts the transaction before the Transfer event is emitted) and
153// adds gas to every movement.
154// One extension per kind; a second registration of an attached kind panics.
155func (led *PrivateLedger) RegisterExtension(ext Extension) {
156 if ext == nil {
157 panic("grc721: nil extension")
158 }
159
160 kind := ext.ExtensionKind()
161 for _, e := range led.extensions {
162 if e.ExtensionKind() == kind {
163 panic("grc721: extension kind already registered: " + kind)
164 }
165 }
166
167 led.extensions = append(led.extensions, ext)
168}
169
170func (led *PrivateLedger) ReadToken() *Token { return led.token }
171
172func (led *PrivateLedger) Mint(to address, tid TokenID) error {
173 if !to.IsValid() {
174 return ErrInvalidAddress
175 }
176
177 if tid == "" {
178 return ErrInvalidTokenId
179 }
180
181 if led.exists(tid) {
182 return ErrTokenIdAlreadyExists
183 }
184
185 led.balances.Set(to.String(), overflow.Add64p(led.balanceOf(to), 1))
186 led.owners.Set(tid.String(), to)
187 led.totalSupply = overflow.Add64p(led.totalSupply, 1)
188
189 for _, ext := range led.extensions {
190 ext.OnMint(to, tid)
191 }
192
193 // Mint emits Transfer from the empty address (EIP-721); indexers reconstruct ownership from Transfer alone.
194 chain.Emit(
195 TransferEvent,
196 "token", led.token.id,
197 "from", "",
198 "to", to.String(),
199 "tokenId", tid.String(),
200 )
201
202 return nil
203}
204
205// Fans OnBurn out to every extension so per-token state cannot resurface if tid is re-minted.
206func (led *PrivateLedger) Burn(tid TokenID) error {
207 owner, err := led.ownerOf(tid)
208 if err != nil {
209 return err
210 }
211
212 tidStr := tid.String()
213 led.tokenApprovals.Remove(tidStr)
214 led.setBalance(owner, overflow.Sub64p(led.balanceOf(owner), 1))
215 led.owners.Remove(tidStr)
216 led.totalSupply = overflow.Sub64p(led.totalSupply, 1)
217
218 for _, ext := range led.extensions {
219 ext.OnBurn(tid)
220 }
221
222 // Burn emits Transfer to the empty address (EIP-721).
223 chain.Emit(
224 TransferEvent,
225 "token", led.token.id,
226 "from", owner.String(),
227 "to", "",
228 "tokenId", tidStr,
229 )
230
231 return nil
232}
233
234// spender must own or be approved for tid; callers derive it from a trusted per-frame identity.
235func (led *PrivateLedger) TransferFrom(spender, from, to address, tid TokenID) error {
236 if !led.isApprovedOrOwner(spender, tid) {
237 return ErrCallerIsNotOwnerOrApproved
238 }
239
240 return led.transfer(from, to, tid)
241}
242
243// caller must own tid or be an approved operator of the owner.
244// EIP-721: approving the zero address clears (revokes) any single-token approval.
245func (led *PrivateLedger) Approve(caller, to address, tid TokenID) error {
246 revoke := to == zeroAddress
247 if !revoke && !to.IsValid() {
248 return ErrInvalidAddress
249 }
250
251 owner, err := led.ownerOf(tid)
252 if err != nil {
253 return err
254 }
255
256 if owner == to {
257 return ErrApprovalToCurrentOwner
258 }
259
260 if caller != owner && !led.isApprovedForAll(owner, caller) {
261 return ErrCallerIsNotOwnerOrApproved
262 }
263
264 if revoke {
265 led.tokenApprovals.Remove(tid.String())
266 } else {
267 led.tokenApprovals.Set(tid.String(), to)
268 }
269
270 chain.Emit(
271 ApprovalEvent,
272 "token", led.token.id,
273 "owner", owner.String(),
274 "to", to.String(), // empty for a revoke
275 "tokenId", tid.String(),
276 )
277
278 return nil
279}
280
281func (led *PrivateLedger) SetApprovalForAll(owner, operator address, approved bool) error {
282 if !owner.IsValid() || !operator.IsValid() {
283 return ErrInvalidAddress
284 }
285
286 if owner == operator {
287 return ErrApprovalToCurrentOwner
288 }
289
290 key := operatorKey(owner, operator)
291 if approved {
292 led.operatorApprovals.Set(key, approved)
293 } else {
294 led.operatorApprovals.Remove(key)
295 }
296
297 chain.Emit(
298 ApprovalForAllEvent,
299 "token", led.token.id,
300 "owner", owner.String(),
301 "to", operator.String(),
302 "approved", strconv.FormatBool(approved),
303 )
304
305 return nil
306}
307
308func (led *PrivateLedger) transfer(from, to address, tid TokenID) error {
309 if !from.IsValid() || !to.IsValid() {
310 return ErrInvalidAddress
311 }
312
313 if from == to {
314 return ErrCannotTransferToSelf
315 }
316
317 owner, err := led.ownerOf(tid)
318 if err != nil {
319 return err
320 }
321
322 if owner != from {
323 return ErrTransferFromIncorrectOwner
324 }
325
326 tidStr := tid.String()
327 led.tokenApprovals.Remove(tidStr)
328 led.setBalance(from, overflow.Sub64p(led.balanceOf(from), 1))
329 led.setBalance(to, overflow.Add64p(led.balanceOf(to), 1))
330 led.owners.Set(tidStr, to)
331
332 for _, ext := range led.extensions {
333 ext.OnTransfer(from, to, tid)
334 }
335
336 chain.Emit(
337 TransferEvent,
338 "token", led.token.id,
339 "from", from.String(),
340 "to", to.String(),
341 "tokenId", tidStr,
342 )
343
344 return nil
345}
346
347func (led *PrivateLedger) ownerOf(tid TokenID) (address, error) {
348 owner := led.owners.Get(tid.String())
349 if owner == nil {
350 return zeroAddress, ErrInvalidTokenId
351 }
352
353 return owner.(address), nil
354}
355
356func (led *PrivateLedger) balanceOf(addr address) int64 {
357 balance := led.balances.Get(addr.String())
358 if balance == nil {
359 return 0
360 }
361
362 return balance.(int64)
363}
364
365func (led *PrivateLedger) setBalance(addr address, balance int64) {
366 if balance == 0 {
367 led.balances.Remove(addr.String())
368
369 return
370 }
371
372 led.balances.Set(addr.String(), balance)
373}
374
375func (led *PrivateLedger) isApprovedForAll(owner, operator address) bool {
376 approved := led.operatorApprovals.Get(operatorKey(owner, operator))
377 if approved == nil {
378 return false
379 }
380
381 return approved.(bool)
382}
383
384func (led *PrivateLedger) isApprovedOrOwner(addr address, tid TokenID) bool {
385 owner := led.owners.Get(tid.String())
386 if owner == nil {
387 return false
388 }
389
390 ownerAddr := owner.(address)
391 if addr == ownerAddr || led.isApprovedForAll(ownerAddr, addr) {
392 return true
393 }
394
395 approved := led.tokenApprovals.Get(tid.String())
396
397 return approved != nil && approved.(address) == addr
398}
399
400func (led *PrivateLedger) exists(tid TokenID) bool {
401 return led.owners.Has(tid.String())
402}
403
404func operatorKey(owner, operator address) string {
405 return owner.String() + ":" + operator.String()
406}