1package grc72123import (4 "chain"5 "math/overflow"6 "strconv"78 "gno.land/p/nt/avl/v0"9 "gno.land/p/nt/ufmt/v0"10)1112type basicNFT struct {13 name string14 symbol string15 owners avl.Tree // tokenId -> OwnerAddress16 balances avl.Tree // OwnerAddress -> TokenCount17 tokenApprovals avl.Tree // TokenId -> ApprovedAddress18 tokenURIs avl.Tree // TokenId -> URIs19 operatorApprovals avl.Tree // "OwnerAddress:OperatorAddress" -> bool20}2122// Returns new basic NFT23func NewBasicNFT(name string, symbol string) *basicNFT {24 return &basicNFT{25 name: name,26 symbol: symbol,2728 owners: avl.Tree{},29 balances: avl.Tree{},30 tokenApprovals: avl.Tree{},31 tokenURIs: avl.Tree{},32 operatorApprovals: avl.Tree{},33 }34}3536func (s *basicNFT) Name() string { return s.name }37func (s *basicNFT) Symbol() string { return s.symbol }38func (s *basicNFT) TokenCount() int64 { return int64(s.owners.Size()) }3940// BalanceOf returns balance of input address41func (s *basicNFT) BalanceOf(addr address) (int64, error) {42 if err := isValidAddress(addr); err != nil {43 return 0, err44 }4546 balance := s.balances.Get(addr.String())47 if balance == nil {48 return 0, nil49 }5051 return balance.(int64), nil52}5354// OwnerOf returns owner of input token id55func (s *basicNFT) OwnerOf(tid TokenID) (address, error) {56 owner := s.owners.Get(string(tid))57 if owner == nil {58 return "", ErrInvalidTokenId59 }6061 return owner.(address), nil62}6364// TokenURI returns the URI of input token id65func (s *basicNFT) TokenURI(tid TokenID) (string, error) {66 uri := s.tokenURIs.Get(tid.String())67 if uri == nil {68 return "", ErrInvalidTokenId69 }7071 return uri.(string), nil72}7374// SetTokenURI sets the URI of a token. caller must equal the token's75// owner. The owning realm's public wrapper is responsible for deriving76// caller from rlm.Previous().Address() under an rlm.IsCurrent() guard77// before invoking this method; this method trusts the supplied caller.78func (s *basicNFT) SetTokenURI(caller address, tid TokenID, tURI TokenURI) (bool, error) {79 // check for invalid TokenID80 if !s.exists(tid) {81 return false, ErrInvalidTokenId82 }8384 // check for the right owner85 owner, err := s.OwnerOf(tid)86 if err != nil {87 return false, err88 }89 if caller != owner {90 return false, ErrCallerIsNotOwner91 }92 s.tokenURIs.Set(tid.String(), tURI.String())93 return true, nil94}9596// IsApprovedForAll returns true if operator is approved for all by the owner.97// Otherwise, returns false98func (s *basicNFT) IsApprovedForAll(owner, operator address) bool {99 key := owner.String() + ":" + operator.String()100 approved := s.operatorApprovals.Get(key)101 if approved == nil {102 return false103 }104105 return approved.(bool)106}107108// Approve approves the input address for a particular token. caller109// must be the owner OR an operator approved for all on owner's behalf.110// The owning realm's wrapper validates IsCurrent and derives caller111// from rlm.Previous().Address() before calling.112func (s *basicNFT) Approve(caller, to address, tid TokenID) error {113 if err := isValidAddress(to); err != nil {114 return err115 }116117 owner, err := s.OwnerOf(tid)118 if err != nil {119 return err120 }121 if owner == to {122 return ErrApprovalToCurrentOwner123 }124125 if caller != owner && !s.IsApprovedForAll(owner, caller) {126 return ErrCallerIsNotOwnerOrApproved127 }128129 tidStr := tid.String()130 s.tokenApprovals.Set(tidStr, to)131132 chain.Emit(133 ApprovalEvent,134 "slug", s.symbol,135 "owner", owner.String(),136 "to", to.String(),137 "tokenId", tidStr,138 )139140 return nil141}142143// GetApproved return the approved address for token144func (s *basicNFT) GetApproved(tid TokenID) (address, error) {145 addr := s.tokenApprovals.Get(tid.String())146 if addr == nil {147 return zeroAddress, ErrTokenIdNotHasApproved148 }149150 return addr.(address), nil151}152153// SetApprovalForAll grants/revokes operator permission across all of154// the caller's tokens. caller is the owner whose approvals are mutated;155// the owning realm's wrapper derives it from rlm.Previous().Address()156// under an IsCurrent() guard.157func (s *basicNFT) SetApprovalForAll(caller, operator address, approved bool) error {158 if err := isValidAddress(operator); err != nil {159 return ErrInvalidAddress160 }161 return s.setApprovalForAll(caller, operator, approved)162}163164// SafeTransferFrom transfers a token from `from` to `to`, checking that165// contract recipients are aware of the GRC721 protocol to prevent166// tokens from being forever locked. caller must be the owner or an167// approved operator. The owning realm's wrapper derives caller from168// rlm.Previous().Address() under an IsCurrent() guard.169func (s *basicNFT) SafeTransferFrom(caller, from, to address, tid TokenID) error {170 if !s.isApprovedOrOwner(caller, tid) {171 return ErrCallerIsNotOwnerOrApproved172 }173174 err := s.transfer(from, to, tid)175 if err != nil {176 return err177 }178179 if !s.checkOnGRC721Received(from, to, tid) {180 return ErrTransferToNonGRC721Receiver181 }182183 return nil184}185186// TransferFrom transfers a token from `from` to `to`. Same caller187// contract as SafeTransferFrom.188func (s *basicNFT) TransferFrom(caller, from, to address, tid TokenID) error {189 if !s.isApprovedOrOwner(caller, tid) {190 return ErrCallerIsNotOwnerOrApproved191 }192193 err := s.transfer(from, to, tid)194 if err != nil {195 return err196 }197198 return nil199}200201// Mints `tokenId` and transfers it to `to`.202func (s *basicNFT) Mint(to address, tid TokenID) error {203 return s.mint(to, tid)204}205206// Mints `tokenId` and transfers it to `to`. Also checks that207// contract recipients are using GRC721 protocol208func (s *basicNFT) SafeMint(to address, tid TokenID) error {209 err := s.mint(to, tid)210 if err != nil {211 return err212 }213214 if !s.checkOnGRC721Received(zeroAddress, to, tid) {215 return ErrTransferToNonGRC721Receiver216 }217218 return nil219}220221func (s *basicNFT) Burn(tid TokenID) error {222 owner, err := s.OwnerOf(tid)223 if err != nil {224 return err225 }226227 s.beforeTokenTransfer(owner, zeroAddress, tid, 1)228229 tidStr := tid.String()230 s.tokenApprovals.Remove(tidStr)231 balance, err := s.BalanceOf(owner)232 if err != nil {233 return err234 }235 balance = overflow.Sub64p(balance, 1)236237 ownerStr := owner.String()238 s.balances.Set(ownerStr, balance)239 s.owners.Remove(tidStr)240241 chain.Emit(242 BurnEvent,243 "slug", s.symbol,244 "from", ownerStr,245 "tokenId", tidStr,246 )247248 s.afterTokenTransfer(owner, zeroAddress, tid, 1)249250 return nil251}252253/* Helper methods */254255// Helper for SetApprovalForAll()256func (s *basicNFT) setApprovalForAll(owner, operator address, approved bool) error {257 if owner == operator {258 return ErrApprovalToCurrentOwner259 }260261 key := owner.String() + ":" + operator.String()262 s.operatorApprovals.Set(key, approved)263264 chain.Emit(265 ApprovalForAllEvent,266 "slug", s.symbol,267 "owner", owner.String(),268 "to", operator.String(),269 "approved", strconv.FormatBool(approved),270 )271272 return nil273}274275// Helper for TransferFrom() and SafeTransferFrom()276func (s *basicNFT) transfer(from, to address, tid TokenID) error {277 if err := isValidAddress(from); err != nil {278 return ErrInvalidAddress279 }280 if err := isValidAddress(to); err != nil {281 return ErrInvalidAddress282 }283284 if from == to {285 return ErrCannotTransferToSelf286 }287288 owner, err := s.OwnerOf(tid)289 if err != nil {290 return err291 }292 if owner != from {293 return ErrTransferFromIncorrectOwner294 }295296 s.beforeTokenTransfer(from, to, tid, 1)297298 // Check that tokenId was not transferred by `beforeTokenTransfer`299 owner, err = s.OwnerOf(tid)300 if err != nil {301 return err302 }303 if owner != from {304 return ErrTransferFromIncorrectOwner305 }306307 tidStr := tid.String()308 s.tokenApprovals.Remove(tidStr)309 fromBalance, err := s.BalanceOf(from)310 if err != nil {311 return err312 }313 toBalance, err := s.BalanceOf(to)314 if err != nil {315 return err316 }317 fromBalance = overflow.Sub64p(fromBalance, 1)318 toBalance = overflow.Add64p(toBalance, 1)319320 fromStr := from.String()321 toStr := to.String()322323 s.balances.Set(fromStr, fromBalance)324 s.balances.Set(toStr, toBalance)325 s.owners.Set(tidStr, to)326327 chain.Emit(328 TransferEvent,329 "slug", s.symbol,330 "from", fromStr,331 "to", toStr,332 "tokenId", tidStr,333 )334335 s.afterTokenTransfer(from, to, tid, 1)336337 return nil338}339340// Helper for Mint() and SafeMint()341func (s *basicNFT) mint(to address, tid TokenID) error {342 if err := isValidAddress(to); err != nil {343 return err344 }345346 if s.exists(tid) {347 return ErrTokenIdAlreadyExists348 }349350 s.beforeTokenTransfer(zeroAddress, to, tid, 1)351352 // Check that tokenId was not minted by `beforeTokenTransfer`353 if s.exists(tid) {354 return ErrTokenIdAlreadyExists355 }356357 toBalance, err := s.BalanceOf(to)358 if err != nil {359 return err360 }361 toBalance = overflow.Add64p(toBalance, 1)362 toStr := to.String()363 tidStr := tid.String()364 s.balances.Set(toStr, toBalance)365 s.owners.Set(tidStr, to)366367 chain.Emit(368 MintEvent,369 "slug", s.symbol,370 "to", toStr,371 "tokenId", tidStr,372 )373374 s.afterTokenTransfer(zeroAddress, to, tid, 1)375376 return nil377}378379func (s *basicNFT) isApprovedOrOwner(addr address, tid TokenID) bool {380 owner := s.owners.Get(tid.String())381 if owner == nil {382 return false383 }384385 ownerAddr := owner.(address)386 if addr == ownerAddr || s.IsApprovedForAll(ownerAddr, addr) {387 return true388 }389390 approved, err := s.GetApproved(tid)391 if err != nil {392 return false393 }394395 return approved == addr396}397398// Checks if token id already exists399func (s *basicNFT) exists(tid TokenID) bool {400 return s.owners.Has(tid.String())401}402403func (s *basicNFT) beforeTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {404 // TODO: Implementation405}406407func (s *basicNFT) afterTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {408 // TODO: Implementation409}410411func (s *basicNFT) checkOnGRC721Received(from, to address, tid TokenID) bool {412 // TODO: Implementation413 return true414}415416func (s *basicNFT) RenderHome() (str string) {417 str += ufmt.Sprintf("# %s ($%s)\n\n", s.name, s.symbol)418 str += ufmt.Sprintf("* **Total supply**: %d\n", s.TokenCount())419 str += ufmt.Sprintf("* **Known accounts**: %d\n", s.balances.Size())420421 return422}423424// Getter returns an NFTGetter that yields a reader-only view of the NFT.425// Safe to register with cross-realm aggregators like tokenhub — readers426// only, no rlm-typed methods, so no cur can be captured via this surface.427func (n *basicNFT) Getter() NFTGetter {428 return func() IGRC721Reader {429 return n430 }431}432Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.