erc721.gno
4.63 Kb · 168 lines
1// Package erc721 is an idiomatic gno.land port of the Solidity ERC-721
2// non-fungible token standard. Each token has a unique integer id owned by
3// exactly one address; ids are minted sequentially. Ownership, per-owner
4// balances and single-token approvals are kept in ordered avl trees so that
5// Render can iterate deterministically.
6package erc721
7
8import (
9 "strconv"
10
11 "chain"
12 "chain/runtime/unsafe"
13
14 "gno.land/p/nt/avl/v0"
15)
16
17const (
18 name = "Gno NFT"
19 symbol = "GNFT"
20)
21
22var (
23 owners avl.Tree // tokenID (zero-padded string) -> address
24 balances avl.Tree // owner address (string) -> uint64
25 approvals avl.Tree // tokenID (zero-padded string) -> approved address
26 nextID int64 = 1
27 minted int64 // total tokens ever minted (== live supply, no burn)
28)
29
30// key formats a token id into a zero-padded, lexicographically-sortable key.
31func key(id int64) string {
32 s := strconv.FormatInt(id, 10)
33 for len(s) < 12 {
34 s = "0" + s
35 }
36 return s
37}
38
39// Mint creates the next token id and assigns it to `to`. Only sequential
40// minting is supported (id is returned via the Mint event).
41func Mint(cur realm, to address) int64 {
42 if !to.IsValid() {
43 panic("erc721: mint to invalid address")
44 }
45 id := nextID
46 nextID++
47 minted++
48
49 owners.Set(key(id), to)
50 balances.Set(to.String(), balanceOf(to)+1)
51
52 chain.Emit("Mint", "to", to.String(), "tokenID", strconv.FormatInt(id, 10))
53 return id
54}
55
56// Transfer moves token `id` from the caller to `to`. The caller must own the
57// token (approvals are cleared on transfer).
58func Transfer(cur realm, to address, id int64) {
59 if !to.IsValid() {
60 panic("erc721: transfer to invalid address")
61 }
62 caller := unsafe.PreviousRealm().Address()
63 from := ownerOf(id) // panics if the token does not exist
64
65 if caller != from {
66 // allow the single-token approved operator too
67 if approvedOf(id) != caller {
68 panic("erc721: caller is neither owner nor approved")
69 }
70 }
71 if from == to {
72 panic("erc721: transfer to current owner")
73 }
74
75 owners.Set(key(id), to)
76 balances.Set(from.String(), balanceOf(from)-1)
77 balances.Set(to.String(), balanceOf(to)+1)
78 approvals.Remove(key(id)) // clear approval on transfer
79
80 chain.Emit("Transfer", "from", from.String(), "to", to.String(),
81 "tokenID", strconv.FormatInt(id, 10))
82}
83
84// Approve grants `spender` the right to transfer token `id`. Only the current
85// owner may approve.
86func Approve(cur realm, spender address, id int64) {
87 caller := unsafe.PreviousRealm().Address()
88 owner := ownerOf(id)
89 if caller != owner {
90 panic("erc721: approve caller is not owner")
91 }
92 approvals.Set(key(id), spender)
93 chain.Emit("Approval", "owner", owner.String(), "spender", spender.String(),
94 "tokenID", strconv.FormatInt(id, 10))
95}
96
97// --- read-only helpers (safe to call from tests and Render) ---
98
99// ownerOf returns the owner of token `id`, panicking if it does not exist.
100func ownerOf(id int64) address {
101 v := owners.Get(key(id))
102 if v == nil {
103 panic("erc721: query for nonexistent token")
104 }
105 return v.(address)
106}
107
108// approvedOf returns the approved address for token `id`, or the zero address.
109func approvedOf(id int64) address {
110 v := approvals.Get(key(id))
111 if v == nil {
112 return address("")
113 }
114 return v.(address)
115}
116
117// balanceOf returns how many tokens `owner` holds.
118func balanceOf(owner address) uint64 {
119 v := balances.Get(owner.String())
120 if v == nil {
121 return 0
122 }
123 return v.(uint64)
124}
125
126// exists reports whether token `id` has been minted (and not since moved away).
127func exists(id int64) bool {
128 return owners.Has(key(id))
129}
130
131// totalSupply returns the number of tokens in circulation.
132func totalSupply() int64 { return minted }
133
134// OwnerOf is the exported read-only accessor for ownerOf.
135func OwnerOf(id int64) address { return ownerOf(id) }
136
137// BalanceOf is the exported read-only accessor for balanceOf.
138func BalanceOf(owner address) uint64 { return balanceOf(owner) }
139
140// TotalSupply is the exported read-only accessor for totalSupply.
141func TotalSupply() int64 { return totalSupply() }
142
143// Render displays collection metadata and a token -> owner table.
144func Render(path string) string {
145 out := "# " + name + " (" + symbol + ")\n\n"
146 out += "**Total supply:** " + strconv.FormatInt(totalSupply(), 10) + "\n\n"
147
148 if owners.Size() == 0 {
149 out += "_No tokens minted yet._\n"
150 return out
151 }
152
153 out += "| Token ID | Owner | Approved |\n"
154 out += "|---------:|-------|----------|\n"
155 owners.Iterate("", "", func(k string, v interface{}) bool {
156 owner := v.(address)
157 id, _ := strconv.ParseInt(k, 10, 64)
158 appr := approvedOf(id)
159 apprStr := "—"
160 if appr != address("") {
161 apprStr = appr.String()
162 }
163 out += "| " + strconv.FormatInt(id, 10) + " | " +
164 owner.String() + " | " + apprStr + " |\n"
165 return false
166 })
167 return out
168}