package chaincode import ( "encoding/base64" "encoding/json" "errors" "fmt" "github.com/hyperledger/fabric-contract-api-go/v2/contractapi" ) // Define objectType names for prefix const balancePrefix = "balance" const nftPrefix = "nft" const approvalPrefix = "approval" // Define key names for options const nameKey = "name" const symbolKey = "symbol" // TokenERC721Contract contract for managing CRUD operations type TokenERC721Contract struct { contractapi.Contract } func _readNFT(ctx contractapi.TransactionContextInterface, tokenId string) (*Nft, error) { nftKey, err := ctx.GetStub().CreateCompositeKey(nftPrefix, []string{tokenId}) if err != nil { return nil, fmt.Errorf("failed to CreateCompositeKey %s: %v", tokenId, err) } nftBytes, err := ctx.GetStub().GetState(nftKey) if err != nil { return nil, fmt.Errorf("failed to GetState %s: %v", tokenId, err) } nft := new(Nft) err = json.Unmarshal(nftBytes, nft) if err != nil { return nil, fmt.Errorf("failed to Unmarshal nftBytes: %v", err) } return nft, nil } func _nftExists(ctx contractapi.TransactionContextInterface, tokenId string) bool { nftKey, err := ctx.GetStub().CreateCompositeKey(nftPrefix, []string{tokenId}) if err != nil { panic("error creating CreateCompositeKey:" + err.Error()) } nftBytes, err := ctx.GetStub().GetState(nftKey) if err != nil { panic("error GetState nftBytes:" + err.Error()) } return len(nftBytes) > 0 } // BalanceOf counts all non-fungible tokens assigned to an owner // param owner {String} An owner for whom to query the balance // returns {int} The number of non-fungible tokens owned by the owner, possibly zero func (c *TokenERC721Contract) BalanceOf(ctx contractapi.TransactionContextInterface, owner string) int { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { panic("failed to check if contract is already initialized:" + err.Error()) } if !initialized { panic("Contract options need to be set before calling any function, call Initialize() to initialize contract") } // There is a key record for every non-fungible token in the format of balancePrefix.owner.tokenId. // BalanceOf() queries for and counts all records matching balancePrefix.owner.* iterator, err := ctx.GetStub().GetStateByPartialCompositeKey(balancePrefix, []string{owner}) if err != nil { panic("Error creating asset chaincode:" + err.Error()) } // Count the number of returned composite keys balance := 0 for iterator.HasNext() { _, err := iterator.Next() if err != nil { return 0 } balance++ } return balance } // OwnerOf finds the owner of a non-fungible token // param {String} tokenId The identifier for a non-fungible token // returns {String} Return the owner of the non-fungible token func (c *TokenERC721Contract) OwnerOf(ctx contractapi.TransactionContextInterface, tokenId string) (string, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return "", fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return "", errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } nft, err := _readNFT(ctx, tokenId) if err != nil { return "", fmt.Errorf("could not process OwnerOf for tokenId: %w", err) } return nft.Owner, nil } // Approve changes or reaffirms the approved client for a non-fungible token // param {String} operator The new approved client // param {String} tokenId the non-fungible token to approve // returns {Boolean} Return whether the approval was successful or not func (c *TokenERC721Contract) Approve(ctx contractapi.TransactionContextInterface, operator string, tokenId string) (bool, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return false, fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return false, errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } sender64, err := ctx.GetClientIdentity().GetID() if err != nil { return false, fmt.Errorf("failed to GetClientIdentity: %v", err) } senderBytes, err := base64.StdEncoding.DecodeString(sender64) if err != nil { return false, fmt.Errorf("failed to DecodeString senderBytes: %v", err) } sender := string(senderBytes) nft, err := _readNFT(ctx, tokenId) if err != nil { return false, fmt.Errorf("failed to _readNFT: %v", err) } // Check if the sender is the current owner of the non-fungible token // or an authorized operator of the current owner owner := nft.Owner operatorApproval, err := c.IsApprovedForAll(ctx, owner, sender) if err != nil { return false, fmt.Errorf("failed to get IsApprovedForAll: %v", err) } if owner != sender && !operatorApproval { return false, errors.New("the sender is not the current owner nor an authorized operator") } // Update the approved operator of the non-fungible token nft.Approved = operator nftKey, err := ctx.GetStub().CreateCompositeKey(nftPrefix, []string{tokenId}) if err != nil { return false, fmt.Errorf("failed to CreateCompositeKey %s: %v", nftKey, err) } nftBytes, err := json.Marshal(nft) if err != nil { return false, fmt.Errorf("failed to marshal nftBytes: %v", err) } err = ctx.GetStub().PutState(nftKey, nftBytes) if err != nil { return false, fmt.Errorf("failed to PutState for nftKey: %v", err) } return true, nil } // SetApprovalForAll enables or disables approval for a third party ("operator") // to manage all the message sender's assets // param {String} operator A client to add to the set of authorized operators // param {Boolean} approved True if the operator is approved, false to revoke approval // returns {Boolean} Return whether the approval was successful or not func (c *TokenERC721Contract) SetApprovalForAll(ctx contractapi.TransactionContextInterface, operator string, approved bool) (bool, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return false, fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return false, errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } sender64, err := ctx.GetClientIdentity().GetID() if err != nil { return false, fmt.Errorf("failed to GetClientIdentity: %v", err) } senderBytes, err := base64.StdEncoding.DecodeString(sender64) if err != nil { return false, fmt.Errorf("failed to DecodeString sender: %v", err) } sender := string(senderBytes) nftApproval := new(Approval) nftApproval.Owner = sender nftApproval.Operator = operator nftApproval.Approved = approved approvalKey, err := ctx.GetStub().CreateCompositeKey(approvalPrefix, []string{sender, operator}) if err != nil { return false, fmt.Errorf("failed to CreateCompositeKey: %v", err) } approvalBytes, err := json.Marshal(nftApproval) if err != nil { return false, fmt.Errorf("failed to marshal approvalBytes: %v", err) } err = ctx.GetStub().PutState(approvalKey, approvalBytes) if err != nil { return false, fmt.Errorf("failed to PutState approvalBytes: %v", err) } // Emit the ApprovalForAll event err = ctx.GetStub().SetEvent("ApprovalForAll", approvalBytes) if err != nil { return false, fmt.Errorf("failed to SetEvent ApprovalForAll: %v", err) } return true, nil } // IsApprovedForAll returns if a client is an authorized operator for another client // param {String} owner The client that owns the non-fungible tokens // param {String} operator The client that acts on behalf of the owner // returns {Boolean} Return true if the operator is an approved operator for the owner, false otherwise func (c *TokenERC721Contract) IsApprovedForAll(ctx contractapi.TransactionContextInterface, owner string, operator string) (bool, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return false, fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return false, errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } approvalKey, err := ctx.GetStub().CreateCompositeKey(approvalPrefix, []string{owner, operator}) if err != nil { return false, fmt.Errorf("failed to CreateCompositeKey: %v", err) } approvalBytes, err := ctx.GetStub().GetState(approvalKey) if err != nil { return false, fmt.Errorf("failed to GetState approvalBytes %s: %v", approvalBytes, err) } if len(approvalBytes) < 1 { return false, nil } approval := new(Approval) err = json.Unmarshal(approvalBytes, approval) if err != nil { return false, fmt.Errorf("failed to Unmarshal: %v, string %s", err, string(approvalBytes)) } return approval.Approved, nil } // GetApproved returns the approved client for a single non-fungible token // param {String} tokenId the non-fungible token to find the approved client for // returns {Object} Return the approved client for this non-fungible token, or null if there is none func (c *TokenERC721Contract) GetApproved(ctx contractapi.TransactionContextInterface, tokenId string) (string, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return "false", fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return "false", errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } nft, err := _readNFT(ctx, tokenId) if err != nil { return "false", fmt.Errorf("failed GetApproved for tokenId : %v", err) } return nft.Approved, nil } // TransferFrom transfers the ownership of a non-fungible token // from one owner to another owner // param {String} from The current owner of the non-fungible token // param {String} to The new owner // param {String} tokenId the non-fungible token to transfer // returns {Boolean} Return whether the transfer was successful or not func (c *TokenERC721Contract) TransferFrom(ctx contractapi.TransactionContextInterface, from string, to string, tokenId string) (bool, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return false, fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return false, errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } // Get ID of submitting client identity sender64, err := ctx.GetClientIdentity().GetID() if err != nil { return false, fmt.Errorf("failed to GetClientIdentity: %v", err) } senderBytes, err := base64.StdEncoding.DecodeString(sender64) if err != nil { return false, fmt.Errorf("failed to DecodeString sender: %v", err) } sender := string(senderBytes) nft, err := _readNFT(ctx, tokenId) if err != nil { return false, fmt.Errorf("failed to _readNFT : %v", err) } owner := nft.Owner operator := nft.Approved operatorApproval, err := c.IsApprovedForAll(ctx, owner, sender) if err != nil { return false, fmt.Errorf("failed to get IsApprovedForAll : %v", err) } if owner != sender && operator != sender && !operatorApproval { return false, errors.New("the sender is not the current owner nor an authorized operator") } // Check if `from` is the current owner if owner != from { return false, errors.New("the from is not the current owner") } // Clear the approved client for this non-fungible token nft.Approved = "" // Overwrite a non-fungible token to assign a new owner. nft.Owner = to nftKey, err := ctx.GetStub().CreateCompositeKey(nftPrefix, []string{tokenId}) if err != nil { return false, fmt.Errorf("failed to CreateCompositeKey: %v", err) } nftBytes, err := json.Marshal(nft) if err != nil { return false, fmt.Errorf("failed to marshal approval: %v", err) } err = ctx.GetStub().PutState(nftKey, nftBytes) if err != nil { return false, fmt.Errorf("failed to PutState nftBytes %s: %v", nftBytes, err) } // Remove a composite key from the balance of the current owner balanceKeyFrom, err := ctx.GetStub().CreateCompositeKey(balancePrefix, []string{from, tokenId}) if err != nil { return false, fmt.Errorf("failed to CreateCompositeKey from: %v", err) } err = ctx.GetStub().DelState(balanceKeyFrom) if err != nil { return false, fmt.Errorf("failed to DelState balanceKeyFrom %s: %v", nftBytes, err) } // Save a composite key to count the balance of a new owner balanceKeyTo, err := ctx.GetStub().CreateCompositeKey(balancePrefix, []string{to, tokenId}) if err != nil { return false, fmt.Errorf("failed to CreateCompositeKey to: %v", err) } err = ctx.GetStub().PutState(balanceKeyTo, []byte{0}) if err != nil { return false, fmt.Errorf("failed to PutState balanceKeyTo %s: %v", balanceKeyTo, err) } // Emit the Transfer event transferEvent := new(Transfer) transferEvent.From = from transferEvent.To = to transferEvent.TokenId = tokenId transferEventBytes, err := json.Marshal(transferEvent) if err != nil { return false, fmt.Errorf("failed to marshal transferEventBytes: %v", err) } err = ctx.GetStub().SetEvent("Transfer", transferEventBytes) if err != nil { return false, fmt.Errorf("failed to SetEvent transferEventBytes %s: %v", transferEventBytes, err) } return true, nil } // ============== ERC721 metadata extension =============== // Name returns a descriptive name for a collection of non-fungible tokens in this contract // returns {String} Returns the name of the token func (c *TokenERC721Contract) Name(ctx contractapi.TransactionContextInterface) (string, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return "", fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return "", errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } bytes, err := ctx.GetStub().GetState(nameKey) if err != nil { return "", fmt.Errorf("failed to get Name bytes: %s", err) } return string(bytes), nil } // Symbol returns an abbreviated name for non-fungible tokens in this contract. // returns {String} Returns the symbol of the token func (c *TokenERC721Contract) Symbol(ctx contractapi.TransactionContextInterface) (string, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return "", fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return "", errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } bytes, err := ctx.GetStub().GetState(symbolKey) if err != nil { return "", fmt.Errorf("failed to get Symbol: %v", err) } return string(bytes), nil } // TokenURI returns a distinct Uniform Resource Identifier (URI) for a given token. // param {string} tokenId The identifier for a non-fungible token // returns {String} Returns the URI of the token func (c *TokenERC721Contract) TokenURI(ctx contractapi.TransactionContextInterface, tokenId string) (string, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return "", fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return "", errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } nft, err := _readNFT(ctx, tokenId) if err != nil { return "", fmt.Errorf("failed to get TokenURI: %v", err) } return nft.TokenURI, nil } // ============== ERC721 enumeration extension =============== // TotalSupply counts non-fungible tokens tracked by this contract. // // @param {Context} ctx the transaction context // @returns {Number} Returns a count of valid non-fungible tokens tracked by this contract, // where each one of them has an assigned and queryable owner. func (c *TokenERC721Contract) TotalSupply(ctx contractapi.TransactionContextInterface) int { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { panic("failed to check if contract is already initialized:" + err.Error()) } if !initialized { panic("Contract options need to be set before calling any function, call Initialize() to initialize contract") } // There is a key record for every non-fungible token in the format of nftPrefix.tokenId. // TotalSupply() queries for and counts all records matching nftPrefix.* iterator, err := ctx.GetStub().GetStateByPartialCompositeKey(nftPrefix, []string{}) if err != nil { panic("Error creating GetStateByPartialCompositeKey:" + err.Error()) } // Count the number of returned composite keys totalSupply := 0 for iterator.HasNext() { _, err := iterator.Next() if err != nil { return 0 } totalSupply++ } return totalSupply } // ============== ERC721 enumeration extension =============== // Set information for a token and intialize contract. // param {String} name The name of the token // param {String} symbol The symbol of the token func (c *TokenERC721Contract) Initialize(ctx contractapi.TransactionContextInterface, name string, symbol string) (bool, error) { // Check minter authorization - this sample assumes Org1 is the issuer with privilege to set the name and symbol clientMSPID, err := ctx.GetClientIdentity().GetMSPID() if err != nil { return false, fmt.Errorf("failed to get clientMSPID: %v", err) } if clientMSPID != "Org1MSP" { return false, errors.New("client is not authorized to set the name and symbol of the token") } bytes, err := ctx.GetStub().GetState(nameKey) if err != nil { return false, fmt.Errorf("failed to get Name: %v", err) } if bytes != nil { return false, errors.New("contract options are already set, client is not authorized to change them") } err = ctx.GetStub().PutState(nameKey, []byte(name)) if err != nil { return false, fmt.Errorf("failed to PutState nameKey %s: %v", nameKey, err) } err = ctx.GetStub().PutState(symbolKey, []byte(symbol)) if err != nil { return false, fmt.Errorf("failed to PutState symbolKey %s: %v", symbolKey, err) } return true, nil } // Mint a new non-fungible token // param {String} tokenId Unique ID of the non-fungible token to be minted // param {String} tokenURI URI containing metadata of the minted non-fungible token // returns {Object} Return the non-fungible token object func (c *TokenERC721Contract) MintWithTokenURI(ctx contractapi.TransactionContextInterface, tokenId string, tokenURI string) (*Nft, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return nil, fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return nil, errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } // Check minter authorization - this sample assumes Org1 is the issuer with privilege to mint a new token clientMSPID, err := ctx.GetClientIdentity().GetMSPID() if err != nil { return nil, fmt.Errorf("failed to get clientMSPID: %v", err) } if clientMSPID != "Org1MSP" { return nil, errors.New("client is not authorized to set the name and symbol of the token") } // Get ID of submitting client identity minter64, err := ctx.GetClientIdentity().GetID() if err != nil { return nil, fmt.Errorf("failed to get minter id: %v", err) } minterBytes, err := base64.StdEncoding.DecodeString(minter64) if err != nil { return nil, fmt.Errorf("failed to DecodeString minter64: %v", err) } minter := string(minterBytes) // Check if the token to be minted does not exist exists := _nftExists(ctx, tokenId) if exists { return nil, fmt.Errorf("the token %s is already minted.: %v", tokenId, err) } // Add a non-fungible token nft := new(Nft) nft.TokenId = tokenId nft.Owner = minter nft.TokenURI = tokenURI nftKey, err := ctx.GetStub().CreateCompositeKey(nftPrefix, []string{tokenId}) if err != nil { return nil, fmt.Errorf("failed to CreateCompositeKey to nftKey: %v", err) } nftBytes, err := json.Marshal(nft) if err != nil { return nil, fmt.Errorf("failed to marshal nft: %v", err) } err = ctx.GetStub().PutState(nftKey, nftBytes) if err != nil { return nil, fmt.Errorf("failed to PutState nftBytes %s: %v", nftBytes, err) } // A composite key would be balancePrefix.owner.tokenId, which enables partial // composite key query to find and count all records matching balance.owner.* // An empty value would represent a delete, so we simply insert the null character. balanceKey, err := ctx.GetStub().CreateCompositeKey(balancePrefix, []string{minter, tokenId}) if err != nil { return nil, fmt.Errorf("failed to CreateCompositeKey to balanceKey: %v", err) } err = ctx.GetStub().PutState(balanceKey, []byte{'\u0000'}) if err != nil { return nil, fmt.Errorf("failed to PutState balanceKey %s: %v", nftBytes, err) } // Emit the Transfer event transferEvent := new(Transfer) transferEvent.From = "0x0" transferEvent.To = minter transferEvent.TokenId = tokenId transferEventBytes, err := json.Marshal(transferEvent) if err != nil { return nil, fmt.Errorf("failed to marshal transferEventBytes: %v", err) } err = ctx.GetStub().SetEvent("Transfer", transferEventBytes) if err != nil { return nil, fmt.Errorf("failed to SetEvent transferEventBytes %s: %v", transferEventBytes, err) } return nft, nil } // Burn a non-fungible token // param {String} tokenId Unique ID of a non-fungible token // returns {Boolean} Return whether the burn was successful or not func (c *TokenERC721Contract) Burn(ctx contractapi.TransactionContextInterface, tokenId string) (bool, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return false, fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return false, errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } owner64, err := ctx.GetClientIdentity().GetID() if err != nil { return false, fmt.Errorf("failed to GetClientIdentity owner64: %v", err) } ownerBytes, err := base64.StdEncoding.DecodeString(owner64) if err != nil { return false, fmt.Errorf("failed to DecodeString owner64: %v", err) } owner := string(ownerBytes) // Check if a caller is the owner of the non-fungible token nft, err := _readNFT(ctx, tokenId) if err != nil { return false, fmt.Errorf("failed to _readNFT nft : %v", err) } if nft.Owner != owner { return false, fmt.Errorf("non-fungible token %s is not owned by %s", tokenId, owner) } // Delete the token nftKey, err := ctx.GetStub().CreateCompositeKey(nftPrefix, []string{tokenId}) if err != nil { return false, fmt.Errorf("failed to CreateCompositeKey tokenId: %v", err) } err = ctx.GetStub().DelState(nftKey) if err != nil { return false, fmt.Errorf("failed to DelState nftKey: %v", err) } // Remove a composite key from the balance of the owner balanceKey, err := ctx.GetStub().CreateCompositeKey(balancePrefix, []string{owner, tokenId}) if err != nil { return false, fmt.Errorf("failed to CreateCompositeKey balanceKey %s: %v", balanceKey, err) } err = ctx.GetStub().DelState(balanceKey) if err != nil { return false, fmt.Errorf("failed to DelState balanceKey %s: %v", balanceKey, err) } // Emit the Transfer event transferEvent := new(Transfer) transferEvent.From = owner transferEvent.To = "0x0" transferEvent.TokenId = tokenId transferEventBytes, err := json.Marshal(transferEvent) if err != nil { return false, fmt.Errorf("failed to marshal transferEventBytes: %v", err) } err = ctx.GetStub().SetEvent("Transfer", transferEventBytes) if err != nil { return false, fmt.Errorf("failed to SetEvent transferEventBytes: %v", err) } return true, nil } // ClientAccountBalance returns the balance of the requesting client's account. // returns {Number} Returns the account balance func (c *TokenERC721Contract) ClientAccountBalance(ctx contractapi.TransactionContextInterface) (int, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return 0, fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return 0, errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } // Get ID of submitting client identity clientAccountID64, err := ctx.GetClientIdentity().GetID() if err != nil { return 0, fmt.Errorf("failed to GetClientIdentity minter: %v", err) } clientAccountIDBytes, err := base64.StdEncoding.DecodeString(clientAccountID64) if err != nil { return 0, fmt.Errorf("failed to DecodeString sender: %v", err) } clientAccountID := string(clientAccountIDBytes) return c.BalanceOf(ctx, clientAccountID), nil } // ClientAccountID returns the id of the requesting client's account. // In this implementation, the client account ID is the clientId itself. // Users can use this function to get their own account id, which they can then give to others as the payment address func (c *TokenERC721Contract) ClientAccountID(ctx contractapi.TransactionContextInterface) (string, error) { // Check if contract has been intilized first initialized, err := checkInitialized(ctx) if err != nil { return "", fmt.Errorf("failed to check if contract is already initialized: %v", err) } if !initialized { return "", errors.New("contract options need to be set before calling any function, call Initialize() to initialize contract") } // Get ID of submitting client identity clientAccountID64, err := ctx.GetClientIdentity().GetID() if err != nil { return "", fmt.Errorf("failed to GetClientIdentity minter: %v", err) } clientAccountBytes, err := base64.StdEncoding.DecodeString(clientAccountID64) if err != nil { return "", fmt.Errorf("failed to DecodeString clientAccount64: %v", err) } clientAccount := string(clientAccountBytes) return clientAccount, nil } // Checks that contract options have been already initialized func checkInitialized(ctx contractapi.TransactionContextInterface) (bool, error) { tokenName, err := ctx.GetStub().GetState(nameKey) if err != nil { return false, fmt.Errorf("failed to get token name: %v", err) } if tokenName == nil { return false, nil } return true, nil }