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

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}