Transaction
827B561A29E722…190A7C7AF977
Block 352,828 · index 0 · indexed
Summary
- Hash
- 827B561A29E72286FA0C94A36655A9460B1625CB1E65B03F7422190A7C7AF977
- Block
- 352,828
- Size
- 17678 bytes
- Gas used
- 24,335,397 / 31,636,068
- Fee
- 31637 ugnot
- Status
- success
Messages
- Attached funds
- 7000000 ugnot
Arguments · 17
- #1grc721v2
- #2basic_nft.gno
- #3package grc721v2 import ( "chain" unsaferealm "chain/runtime/unsafe" "math/overflow" "strconv" "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/avl" "gno.land/p/nt/ufmt/v0" ) type BasicNFT struct { name string symbol string origRealm string // owning realm's package path owners avl.Tree // tokenId -> OwnerAddress balances avl.Tree // OwnerAddress -> TokenCount tokenApprovals avl.Tree // TokenId -> ApprovedAddress tokenURIs avl.Tree // TokenId -> URIs operatorApprovals avl.Tree // "OwnerAddress:OperatorAddress" -> bool } func NewBasicNFT(_ int, _ realm, name, symbol string) *BasicNFT { pkgPath := unsaferealm.CurrentRealm().PkgPath() if pkgPath == "" { panic(ErrNotRealm) } if !validName(name) { panic(ErrInvalidName) } if !validSymbol(symbol) { panic(ErrInvalidSymbol) } return &BasicNFT{ name: name, symbol: symbol, origRealm: pkgPath, owners: avl.Tree{}, balances: avl.Tree{}, tokenApprovals: avl.Tree{}, tokenURIs: avl.Tree{}, operatorApprovals: avl.Tree{}, } } func (s *BasicNFT) Name() string { return s.name } func (s *BasicNFT) Symbol() string { return s.symbol } func (s *BasicNFT) TokenCount() int64 { return int64(s.owners.Size()) } func (s *BasicNFT) ID() string { return s.origRealm + "." + s.symbol } func (s *BasicNFT) BalanceOf(addr address) (int64, error) { if err := isValidAddress(addr); err != nil { return 0, err } balance := s.balances.Get(addr.String()) if balance == nil { return 0, nil } return balance.(int64), nil } func (s *BasicNFT) OwnerOf(tid TokenID) (address, error) { owner := s.owners.Get(string(tid)) if owner == nil { return "", ErrInvalidTokenId } return owner.(address), nil } func (s *BasicNFT) TokenURI(tid TokenID) (string, error) { uri := s.tokenURIs.Get(tid.String()) if uri == nil { return "", ErrInvalidTokenId } return uri.(string), nil } func (s *BasicNFT) SetTokenURI(caller address, tid TokenID, tURI TokenURI) (bool, error) { if !s.exists(tid) { return false, ErrInvalidTokenId } owner, err := s.OwnerOf(tid) if err != nil { return false, err } if caller != owner { return false, ErrCallerIsNotOwner } s.tokenURIs.Set(tid.String(), tURI.String()) chain.Emit( TokenURIUpdateEvent, "token", s.ID(), "tokenId", tid.String(), ) return true, nil } func (s *BasicNFT) IsApprovedForAll(owner, operator address) bool { key := owner.String() + ":" + operator.String() approved := s.operatorApprovals.Get(key) if approved == nil { return false } return approved.(bool) } func (s *BasicNFT) Approve(caller, to address, tid TokenID) error { if err := isValidAddress(to); err != nil { return err } owner, err := s.OwnerOf(tid) if err != nil { return err } if owner == to { return ErrApprovalToCurrentOwner } if caller != owner && !s.IsApprovedForAll(owner, caller) { return ErrCallerIsNotOwnerOrApproved } tidStr := tid.String() s.tokenApprovals.Set(tidStr, to) chain.Emit( ApprovalEvent, "token", s.ID(), "owner", owner.String(), "to", to.String(), "tokenId", tidStr, ) return nil } func (s *BasicNFT) GetApproved(tid TokenID) (address, error) { addr := s.tokenApprovals.Get(tid.String()) if addr == nil { return zeroAddress, ErrTokenIdNotHasApproved } return addr.(address), nil } func (s *BasicNFT) SetApprovalForAll(caller, operator address, approved bool) error { if err := isValidAddress(operator); err != nil { return ErrInvalidAddress } return s.setApprovalForAll(caller, operator, approved) } func (s *BasicNFT) SafeTransferFrom(caller, from, to address, tid TokenID) error { if !s.isApprovedOrOwner(caller, tid) { return ErrCallerIsNotOwnerOrApproved } err := s.transfer(from, to, tid) if err != nil { return err } if !s.checkOnGRC721Received(from, to, tid) { return ErrTransferToNonGRC721Receiver } return nil } func (s *BasicNFT) TransferFrom(caller, from, to address, tid TokenID) error { if !s.isApprovedOrOwner(caller, tid) { return ErrCallerIsNotOwnerOrApproved } err := s.transfer(from, to, tid) if err != nil { return err } return nil } func (s *BasicNFT) Mint(to address, tid TokenID) error { return s.mint(to, tid) } func (s *BasicNFT) SafeMint(to address, tid TokenID) error { err := s.mint(to, tid) if err != nil { return err } if !s.checkOnGRC721Received(zeroAddress, to, tid) { return ErrTransferToNonGRC721Receiver } return nil } func (s *BasicNFT) Burn(tid TokenID) error { owner, err := s.OwnerOf(tid) if err != nil { return err } s.beforeTokenTransfer(owner, zeroAddress, tid, 1) tidStr := tid.String() s.tokenApprovals.Remove(tidStr) balance, err := s.BalanceOf(owner) if err != nil { return err } balance = overflow.Sub64p(balance, 1) ownerStr := owner.String() s.balances.Set(ownerStr, balance) s.owners.Remove(tidStr) chain.Emit( BurnEvent, "token", s.ID(), "from", ownerStr, "tokenId", tidStr, ) s.afterTokenTransfer(owner, zeroAddress, tid, 1) return nil } func (s *BasicNFT) setApprovalForAll(owner, operator address, approved bool) error { if owner == operator { return ErrApprovalToCurrentOwner } key := owner.String() + ":" + operator.String() s.operatorApprovals.Set(key, approved) chain.Emit( ApprovalForAllEvent, "token", s.ID(), "owner", owner.String(), "to", operator.String(), "approved", strconv.FormatBool(approved), ) return nil } func (s *BasicNFT) transfer(from, to address, tid TokenID) error { if err := isValidAddress(from); err != nil { return ErrInvalidAddress } if err := isValidAddress(to); err != nil { return ErrInvalidAddress } if from == to { return ErrCannotTransferToSelf } owner, err := s.OwnerOf(tid) if err != nil { return err } if owner != from { return ErrTransferFromIncorrectOwner } s.beforeTokenTransfer(from, to, tid, 1) owner, err = s.OwnerOf(tid) if err != nil { return err } if owner != from { return ErrTransferFromIncorrectOwner } tidStr := tid.String() s.tokenApprovals.Remove(tidStr) fromBalance, err := s.BalanceOf(from) if err != nil { return err } toBalance, err := s.BalanceOf(to) if err != nil { return err } fromBalance = overflow.Sub64p(fromBalance, 1) toBalance = overflow.Add64p(toBalance, 1) fromStr := from.String() toStr := to.String() s.balances.Set(fromStr, fromBalance) s.balances.Set(toStr, toBalance) s.owners.Set(tidStr, to) chain.Emit( TransferEvent, "token", s.ID(), "from", fromStr, "to", toStr, "tokenId", tidStr, ) s.afterTokenTransfer(from, to, tid, 1) return nil } func (s *BasicNFT) mint(to address, tid TokenID) error { if err := isValidAddress(to); err != nil { return err } if s.exists(tid) { return ErrTokenIdAlreadyExists } s.beforeTokenTransfer(zeroAddress, to, tid, 1) if s.exists(tid) { return ErrTokenIdAlreadyExists } toBalance, err := s.BalanceOf(to) if err != nil { return err } toBalance = overflow.Add64p(toBalance, 1) toStr := to.String() tidStr := tid.String() s.balances.Set(toStr, toBalance) s.owners.Set(tidStr, to) chain.Emit( MintEvent, "token", s.ID(), "to", toStr, "tokenId", tidStr, ) s.afterTokenTransfer(zeroAddress, to, tid, 1) return nil } func (s *BasicNFT) isApprovedOrOwner(addr address, tid TokenID) bool { owner := s.owners.Get(tid.String()) if owner == nil { return false } ownerAddr := owner.(address) if addr == ownerAddr || s.IsApprovedForAll(ownerAddr, addr) { return true } approved, err := s.GetApproved(tid) if err != nil { return false } return approved == addr } func (s *BasicNFT) exists(tid TokenID) bool { return s.owners.Has(tid.String()) } func (s *BasicNFT) beforeTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) { } func (s *BasicNFT) afterTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) { } func (s *BasicNFT) checkOnGRC721Received(from, to address, tid TokenID) bool { return true } func (s *BasicNFT) RenderHome() (str string) { str += ufmt.Sprintf("# %s ($%s)\n\n", s.name, s.symbol) str += ufmt.Sprintf("* **Total supply**: %d\n", s.TokenCount()) str += ufmt.Sprintf("* **Known accounts**: %d\n", s.balances.Size()) return } func (n *BasicNFT) Getter() NFTGetter { return func() IGRC721Reader { return n } }
- #4errors.gno
- #5package grc721v2 import "errors" var ( ErrInvalidTokenId = errors.New("invalid token id") ErrInvalidAddress = errors.New("invalid address") ErrTokenIdNotHasApproved = errors.New("token id not approved for anyone") ErrApprovalToCurrentOwner = errors.New("approval to current owner") ErrCallerIsNotOwner = errors.New("caller is not token owner") ErrCallerNotApprovedForAll = errors.New("caller is not approved for all") ErrCannotTransferToSelf = errors.New("cannot send transfer to self") ErrTransferFromIncorrectOwner = errors.New("transfer from incorrect owner") ErrTransferToNonGRC721Receiver = errors.New("transfer to non GRC721Receiver implementer") ErrCallerIsNotOwnerOrApproved = errors.New("caller is not token owner or approved") ErrTokenIdAlreadyExists = errors.New("token id already exists") ErrNotRealm = errors.New("rlm must be a realm (got EOA/origin)") ErrInvalidName = errors.New("invalid token name (empty, too long, or contains control chars)") ErrInvalidSymbol = errors.New("invalid token symbol (empty, too long, or contains chars outside [A-Za-z0-9_-])") )
- #6gnomod.toml
- #7module = "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/grc721v2" gno = "0.9"
- #8grc721_metadata.gno
- #9package grc721v2 import ( "chain" "encoding/base64" "strings" "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/avl" ) type MetadataNFT struct { *BasicNFT extensions *avl.Tree // AVL tree for storing metadata extensions } var _ IGRC721MetadataOnchain = (*MetadataNFT)(nil) func NewNFTWithMetadata(_ int, rlm realm, name, symbol string) *MetadataNFT { return &MetadataNFT{ BasicNFT: NewBasicNFT(0, rlm, name, symbol), extensions: avl.NewTree(), } } func (s *MetadataNFT) SetTokenMetadata(caller address, tid TokenID, metadata Metadata) error { owner, err := s.OwnerOf(tid) if err != nil { return err } if caller != owner { return ErrCallerIsNotOwner } s.extensions.Set(tid.String(), metadata) chain.Emit( MetadataUpdateEvent, "token", s.ID(), "tokenId", tid.String(), ) return nil } func (s *MetadataNFT) TokenMetadata(tid TokenID) (Metadata, error) { metadata := s.extensions.Get(tid.String()) if metadata == nil { return Metadata{}, ErrInvalidTokenId } return metadata.(Metadata), nil } func (s *MetadataNFT) Name() string { return s.BasicNFT.Name() } func (s *MetadataNFT) Symbol() string { return s.BasicNFT.Symbol() } func (s *MetadataNFT) TokenCount() int64 { return s.BasicNFT.TokenCount() } func (s *MetadataNFT) BalanceOf(addr address) (int64, error) { return s.BasicNFT.BalanceOf(addr) } func (s *MetadataNFT) OwnerOf(tid TokenID) (address, error) { return s.BasicNFT.OwnerOf(tid) } func (s *MetadataNFT) TokenURI(tid TokenID) (string, error) { metadata, err := s.TokenMetadata(tid) if err != nil { return "", err } return metadataDataURI(metadata), nil } func jsonEscape(s string) string { var b strings.Builder for _, r := range s { switch r { case '\\': b.WriteString(`\\`) case '"': b.WriteString(`\"`) case '\n': b.WriteString(`\n`) case '\r': b.WriteString(`\r`) case '\t': b.WriteString(`\t`) default: b.WriteRune(r) } } return b.String() } func metadataDataURI(m Metadata) string { image := m.Image if image == "" { image = m.ImageData } var b strings.Builder b.WriteString(`{"name":"`) b.WriteString(jsonEscape(m.Name)) b.WriteString(`","description":"`) b.WriteString(jsonEscape(m.Description)) b.WriteString(`","image":"`) b.WriteString(jsonEscape(image)) b.WriteString(`"`) if m.ExternalURL != "" { b.WriteString(`,"external_url":"`) b.WriteString(jsonEscape(m.ExternalURL)) b.WriteString(`"`) } if m.BackgroundColor != "" { b.WriteString(`,"background_color":"`) b.WriteString(jsonEscape(m.BackgroundColor)) b.WriteString(`"`) } if m.AnimationURL != "" { b.WriteString(`,"animation_url":"`) b.WriteString(jsonEscape(m.AnimationURL)) b.WriteString(`"`) } if m.YoutubeURL != "" { b.WriteString(`,"youtube_url":"`) b.WriteString(jsonEscape(m.YoutubeURL)) b.WriteString(`"`) } b.WriteString(`,"attributes":[`) for i, t := range m.Attributes { if i > 0 { b.WriteString(",") } b.WriteString(`{"trait_type":"`) b.WriteString(jsonEscape(t.TraitType)) b.WriteString(`","value":"`) b.WriteString(jsonEscape(t.Value)) b.WriteString(`"}`) } b.WriteString("]}") return "data:application/json;base64," + base64.StdEncoding.EncodeToString([]byte(b.String())) } func (s *MetadataNFT) SetTokenURI(caller address, tid TokenID, tURI TokenURI) (bool, error) { return s.BasicNFT.SetTokenURI(caller, tid, tURI) } func (s *MetadataNFT) IsApprovedForAll(owner, operator address) bool { return s.BasicNFT.IsApprovedForAll(owner, operator) } func (s *MetadataNFT) Approve(caller, to address, tid TokenID) error { return s.BasicNFT.Approve(caller, to, tid) } func (s *MetadataNFT) GetApproved(tid TokenID) (address, error) { return s.BasicNFT.GetApproved(tid) } func (s *MetadataNFT) SetApprovalForAll(caller, operator address, approved bool) error { return s.BasicNFT.SetApprovalForAll(caller, operator, approved) } func (s *MetadataNFT) SafeTransferFrom(caller, from, to address, tid TokenID) error { return s.BasicNFT.SafeTransferFrom(caller, from, to, tid) } func (s *MetadataNFT) TransferFrom(caller, from, to address, tid TokenID) error { return s.BasicNFT.TransferFrom(caller, from, to, tid) } func (s *MetadataNFT) Mint(to address, tid TokenID) error { return s.BasicNFT.Mint(to, tid) } func (s *MetadataNFT) SafeMint(to address, tid TokenID) error { return s.BasicNFT.SafeMint(to, tid) } func (s *MetadataNFT) Burn(tid TokenID) error { if err := s.BasicNFT.Burn(tid); err != nil { return err } s.extensions.Remove(tid.String()) return nil } func (s *MetadataNFT) RenderHome() string { return s.BasicNFT.RenderHome() }
- #10igrc721.gno
- #11package grc721v2 type IGRC721Reader interface { Name() string Symbol() string TokenCount() int64 BalanceOf(owner address) (int64, error) OwnerOf(tid TokenID) (address, error) GetApproved(tid TokenID) (address, error) IsApprovedForAll(owner, operator address) bool } type ( TokenID string TokenURI string ) func (t TokenID) String() string { return string(t) } func (t TokenURI) String() string { return string(t) } const ( MintEvent = "Mint" BurnEvent = "Burn" TransferEvent = "Transfer" ApprovalEvent = "Approval" ApprovalForAllEvent = "ApprovalForAll" TokenURIUpdateEvent = "TokenUriUpdate" MetadataUpdateEvent = "MetadataUpdate" ) type NFTGetter func() IGRC721Reader
- #12igrc721_metadata.gno
- #13package grc721v2 type IGRC721CollectionMetadata interface { Name() string // Name returns the name of the collection. Symbol() string // Symbol returns the symbol of the collection. } type IGRC721Metadata interface { IGRC721CollectionMetadata TokenURI(tid TokenID) (string, error) // TokenURI returns the URI of a specific token. } type IGRC721MetadataOnchain interface { IGRC721CollectionMetadata TokenMetadata(tid TokenID) (Metadata, error) } type Trait struct { DisplayType string TraitType string Value string } type Metadata struct { Image string // URL, IPFS/Arweave path, or (commonly here) a data: URI holding the image itself. ImageData string // Raw SVG image data, if generating images on the fly. Only used if Image is empty. ExternalURL string // URL shown alongside the asset on marketplaces, linking back to the collection's own site. Description string // Human-readable description of the item. Markdown is supported. Name string // Name of the item. Attributes []Trait // Attributes for the item, shown on marketplace pages for the item. BackgroundColor string // Background color of the item on marketplaces. Six-character hex, no leading #. AnimationURL string // URL/data URI to a multimedia attachment for the item. YoutubeURL string // URL to a YouTube video (only used if AnimationURL is not provided). }
- #14util.gno
- #15package grc721v2 const ( MaxNameLen = 64 MaxSymbolLen = 11 ) var zeroAddress = address("") func isValidAddress(addr address) error { if !addr.IsValid() { return ErrInvalidAddress } return nil } func validName(name string) bool { if name == "" || len(name) > MaxNameLen { return false } for _, c := range name { if c < 0x20 || c == 0x7f { return false } } return true } func validSymbol(symbol string) bool { if symbol == "" || len(symbol) > MaxSymbolLen { return false } for _, c := range symbol { if !isAlnum(c) && c != '_' && c != '-' { return false } } return true } func isAlnum(c rune) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]