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.

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}