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.71 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/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/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}