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.67 Kb · 155 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/moul/kit/store/v0"
 15	"gno.land/p/nt/avl/v0"
 16)
 17
 18const (
 19	name   = "Gno NFT"
 20	symbol = "GNFT"
 21)
 22
 23var (
 24	// owners assigns the token ids. v0 kept its own nextID plus a key() that
 25	// zero-padded to width 12; the store keys by seqid, so the iteration order
 26	// Render depends on is numeric for every uint64 rather than up to 10^12.
 27	owners = store.Named("erc721: token") // tokenID -> address
 28
 29	balances  avl.Tree // owner address (string) -> uint64
 30	approvals avl.Tree // tokenID (store key) -> approved address
 31)
 32
 33// Mint creates the next token id and assigns it to `to`. Only sequential
 34// minting is supported (id is returned via the Mint event).
 35func Mint(cur realm, to address) int64 {
 36	if !to.IsValid() {
 37		panic("erc721: mint to invalid address")
 38	}
 39	id := int64(owners.Add(to))
 40	balances.Set(to.String(), balanceOf(to)+1)
 41
 42	chain.Emit("Mint", "to", to.String(), "tokenID", strconv.FormatInt(id, 10))
 43	return id
 44}
 45
 46// Transfer moves token `id` from the caller to `to`. The caller must own the
 47// token (approvals are cleared on transfer).
 48func Transfer(cur realm, to address, id int64) {
 49	if !to.IsValid() {
 50		panic("erc721: transfer to invalid address")
 51	}
 52	caller := unsafe.PreviousRealm().Address()
 53	from := ownerOf(id) // panics if the token does not exist
 54
 55	if caller != from {
 56		// allow the single-token approved operator too
 57		if approvedOf(id) != caller {
 58			panic("erc721: caller is neither owner nor approved")
 59		}
 60	}
 61	if from == to {
 62		panic("erc721: transfer to current owner")
 63	}
 64
 65	owners.Set(store.ID(id), to)
 66	balances.Set(from.String(), balanceOf(from)-1)
 67	balances.Set(to.String(), balanceOf(to)+1)
 68	approvals.Remove(store.ID(id).Key()) // clear approval on transfer
 69
 70	chain.Emit("Transfer", "from", from.String(), "to", to.String(),
 71		"tokenID", strconv.FormatInt(id, 10))
 72}
 73
 74// Approve grants `spender` the right to transfer token `id`. Only the current
 75// owner may approve.
 76func Approve(cur realm, spender address, id int64) {
 77	caller := unsafe.PreviousRealm().Address()
 78	owner := ownerOf(id)
 79	if caller != owner {
 80		panic("erc721: approve caller is not owner")
 81	}
 82	approvals.Set(store.ID(id).Key(), spender)
 83	chain.Emit("Approval", "owner", owner.String(), "spender", spender.String(),
 84		"tokenID", strconv.FormatInt(id, 10))
 85}
 86
 87// --- read-only helpers (safe to call from tests and Render) ---
 88
 89// ownerOf returns the owner of token `id`, panicking if it does not exist.
 90// The store's label puts the id in the message, which v0's fixed string
 91// ("erc721: query for nonexistent token") left out.
 92func ownerOf(id int64) address {
 93	return owners.MustGet(store.ID(id)).(address)
 94}
 95
 96// approvedOf returns the approved address for token `id`, or the zero address.
 97func approvedOf(id int64) address {
 98	v := approvals.Get(store.ID(id).Key())
 99	if v == nil {
100		return address("")
101	}
102	return v.(address)
103}
104
105// balanceOf returns how many tokens `owner` holds.
106func balanceOf(owner address) uint64 {
107	v := balances.Get(owner.String())
108	if v == nil {
109		return 0
110	}
111	return v.(uint64)
112}
113
114// exists reports whether token `id` has been minted (and not since moved away).
115func exists(id int64) bool {
116	return owners.Has(store.ID(id))
117}
118
119// totalSupply returns the number of tokens in circulation. There is no burn,
120// so the highest id ever assigned is the count.
121func totalSupply() int64 { return int64(owners.LastID()) }
122
123// OwnerOf is the exported read-only accessor for ownerOf.
124func OwnerOf(id int64) address { return ownerOf(id) }
125
126// BalanceOf is the exported read-only accessor for balanceOf.
127func BalanceOf(owner address) uint64 { return balanceOf(owner) }
128
129// TotalSupply is the exported read-only accessor for totalSupply.
130func TotalSupply() int64 { return totalSupply() }
131
132// Render displays collection metadata and a token -> owner table.
133func Render(path string) string {
134	out := "# " + name + " (" + symbol + ")\n\n"
135	out += "**Total supply:** " + strconv.FormatInt(totalSupply(), 10) + "\n\n"
136
137	if owners.Len() == 0 {
138		out += "_No tokens minted yet._\n"
139		return out
140	}
141
142	out += "| Token ID | Owner | Approved |\n"
143	out += "|---------:|-------|----------|\n"
144	owners.Each(func(id store.ID, v any) {
145		owner := v.(address)
146		appr := approvedOf(int64(id))
147		apprStr := "—"
148		if appr != address("") {
149			apprStr = appr.String()
150		}
151		out += "| " + id.String() + " | " +
152			owner.String() + " | " + apprStr + " |\n"
153	})
154	return out
155}