// Package erc721 is an idiomatic gno.land port of the Solidity ERC-721 // non-fungible token standard. Each token has a unique integer id owned by // exactly one address; ids are minted sequentially. Ownership, per-owner // balances and single-token approvals are kept in ordered avl trees so that // Render can iterate deterministically. package erc721 import ( "strconv" "chain" "chain/runtime/unsafe" "gno.land/p/moul/kit/store/v0" "gno.land/p/nt/avl/v0" ) const ( name = "Gno NFT" symbol = "GNFT" ) var ( // owners assigns the token ids. v0 kept its own nextID plus a key() that // zero-padded to width 12; the store keys by seqid, so the iteration order // Render depends on is numeric for every uint64 rather than up to 10^12. owners = store.Named("erc721: token") // tokenID -> address balances avl.Tree // owner address (string) -> uint64 approvals avl.Tree // tokenID (store key) -> approved address ) // Mint creates the next token id and assigns it to `to`. Only sequential // minting is supported (id is returned via the Mint event). func Mint(cur realm, to address) int64 { if !to.IsValid() { panic("erc721: mint to invalid address") } id := int64(owners.Add(to)) balances.Set(to.String(), balanceOf(to)+1) chain.Emit("Mint", "to", to.String(), "tokenID", strconv.FormatInt(id, 10)) return id } // Transfer moves token `id` from the caller to `to`. The caller must own the // token (approvals are cleared on transfer). func Transfer(cur realm, to address, id int64) { if !to.IsValid() { panic("erc721: transfer to invalid address") } caller := unsafe.PreviousRealm().Address() from := ownerOf(id) // panics if the token does not exist if caller != from { // allow the single-token approved operator too if approvedOf(id) != caller { panic("erc721: caller is neither owner nor approved") } } if from == to { panic("erc721: transfer to current owner") } owners.Set(store.ID(id), to) balances.Set(from.String(), balanceOf(from)-1) balances.Set(to.String(), balanceOf(to)+1) approvals.Remove(store.ID(id).Key()) // clear approval on transfer chain.Emit("Transfer", "from", from.String(), "to", to.String(), "tokenID", strconv.FormatInt(id, 10)) } // Approve grants `spender` the right to transfer token `id`. Only the current // owner may approve. func Approve(cur realm, spender address, id int64) { caller := unsafe.PreviousRealm().Address() owner := ownerOf(id) if caller != owner { panic("erc721: approve caller is not owner") } approvals.Set(store.ID(id).Key(), spender) chain.Emit("Approval", "owner", owner.String(), "spender", spender.String(), "tokenID", strconv.FormatInt(id, 10)) } // --- read-only helpers (safe to call from tests and Render) --- // ownerOf returns the owner of token `id`, panicking if it does not exist. // The store's label puts the id in the message, which v0's fixed string // ("erc721: query for nonexistent token") left out. func ownerOf(id int64) address { return owners.MustGet(store.ID(id)).(address) } // approvedOf returns the approved address for token `id`, or the zero address. func approvedOf(id int64) address { v := approvals.Get(store.ID(id).Key()) if v == nil { return address("") } return v.(address) } // balanceOf returns how many tokens `owner` holds. func balanceOf(owner address) uint64 { v := balances.Get(owner.String()) if v == nil { return 0 } return v.(uint64) } // exists reports whether token `id` has been minted (and not since moved away). func exists(id int64) bool { return owners.Has(store.ID(id)) } // totalSupply returns the number of tokens in circulation. There is no burn, // so the highest id ever assigned is the count. func totalSupply() int64 { return int64(owners.LastID()) } // OwnerOf is the exported read-only accessor for ownerOf. func OwnerOf(id int64) address { return ownerOf(id) } // BalanceOf is the exported read-only accessor for balanceOf. func BalanceOf(owner address) uint64 { return balanceOf(owner) } // TotalSupply is the exported read-only accessor for totalSupply. func TotalSupply() int64 { return totalSupply() } // Render displays collection metadata and a token -> owner table. func Render(path string) string { out := "# " + name + " (" + symbol + ")\n\n" out += "**Total supply:** " + strconv.FormatInt(totalSupply(), 10) + "\n\n" if owners.Len() == 0 { out += "_No tokens minted yet._\n" return out } out += "| Token ID | Owner | Approved |\n" out += "|---------:|-------|----------|\n" owners.Each(func(id store.ID, v any) { owner := v.(address) appr := approvedOf(int64(id)) apprStr := "—" if appr != address("") { apprStr = appr.String() } out += "| " + id.String() + " | " + owner.String() + " | " + apprStr + " |\n" }) return out }