Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

token.gno

9.66 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, _, _ := 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}