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

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}