Simple NFT demo
This tutorial will show you how to create, mint and list a simple NFT. It follows the Non Fungible Token standard (https://github.com/onflow/flow-nft/blob/master/contracts/NonFungibleToken.cdc), but does not implement the MetadataViews interface. If you would like to make your NFT compatible with marketplaces, look at implementing MetadataViews (https://github.com/onflow/flow-nft/blob/master/contracts/MetadataViews.cdc)
The following are the main points of this tutorial:
- Creating a contract that implements INFT
- Deploying the contract
- Listing, minting and storing NFTs defined by the contract via a transaction
Getting started
Load the Samples/Flow SDK/x.x.x/Example NFT/Scenes/NFTExampleScene scene. Press play and approve the transactions that come up (only on first time run) Click Authenticate and choose the emulator_service_account. Click Mint Fill in the Text and URL fields and click Mint Approve the transaction Click List to refresh the NFT display panel and show your newly minted NFT Repeat Mint and List as desired to make your list grow
Now we'll show you how this works.
Creating an NFT contract
When creating an NFT it is recommended (but not required) to implement the NonFungibleToken.INFT interface. We will be doing so in this case.
At its simplest, an NFT on Flow is a resource with a unique id. A Collection is a resource that will allow you to store, list, deposit, and withdraw NFTs of a specific type.
We recommend reading through the NFT tutorial to understand what is happening, as well as reviewing the contents of Cadence/Contracts/SDKExampleNFT.cdc
The SDKExampleNFT minter allows for anyone to mint an SDKExampleNFT. Typically you would restrict minting to an authorized account.
This tutorial will not delve deeply into the NFT contract or Cadence, instead focusing on interacting with them using the functionality the Unity SDK provides.
Deploying the contracts
Open up Example.cs to follow along.
Our Start function looks like this:
_15public void Start()_15{_15    //Initialize the FlowSDK, connecting to an emulator using HTTP_15    FlowSDK.Init(new FlowConfig_15    {_15        NetworkUrl = FlowControl.Data.EmulatorSettings.emulatorEndpoint,_15        Protocol = FlowConfig.NetworkProtocol.HTTP_15    });_15_15    //Register the DevWallet provider that we will be using_15    FlowSDK.RegisterWalletProvider(new DevWalletProvider());_15    _15    //Deploy the NonFungibleToken and SDKExampleNFT contracts if they are not already deployed_15    StartCoroutine(DeployContracts());_15}
This initializes the FlowSDK to connect to the emulator, creates and registers a DevWalletProvioder, then starts a coroutine to deploy our contract if needed.
Contracts can be deployed via the FlowControl Tools window, but we will deploy them via code for ease of use.
The DeployContracts coroutine:
_69public IEnumerator DeployContracts()_69{_69    statusText.text = "Verifying contracts";_69    //Wait 1 second to ensure emulator has started up and service account information has been captured._69    yield return new WaitForSeconds(1.0f);_69_69    //Get the address of the emulator_service_account, then get an account object for that account. _69    Task<FlowAccount> accountTask = Accounts.GetByAddress(FlowControl.Data.Accounts.Find(acct => acct.Name == "emulator_service_account").AccountConfig["Address"]);_69    //Wait until the account fetch is complete_69    yield return new WaitUntil(() => accountTask.IsCompleted);_69_69    //Check for errors._69    if (accountTask.Result.Error != null)_69    {_69        Debug.LogError(accountTask.Result.Error.Message);_69        Debug.LogError(accountTask.Result.Error.StackTrace);_69    }_69_69    //We now have an Account object, which contains the contracts deployed to that account.  Check if the NonFungileToken and SDKExampleNFT contracts are deployed_69    if (!accountTask.Result.Contracts.Exists(x => x.Name == "SDKExampleNFT") || !accountTask.Result.Contracts.Exists(x => x.Name == "NonFungibleToken"))_69    {_69        statusText.text = "Deploying contracts,\napprove transactions";_69_69        //First authenticate as the emulator_service_account using DevWallet_69        FlowSDK.GetWalletProvider().Authenticate("emulator_service_account", null, null);_69_69        //Ensure that we authenticated properly_69        if (FlowSDK.GetWalletProvider().GetAuthenticatedAccount() == null)_69        {_69            Debug.LogError("No authenticated account.");_69            yield break;_69        }_69_69        //Deploy the NonFungibleToken contract_69        Task<FlowTransactionResponse> txResponse = CommonTransactions.DeployContract("NonFungibleToken", NonFungibleTokenContract.text);_69        yield return new WaitUntil(() => txResponse.IsCompleted);_69        if (txResponse.Result.Error != null)_69        {_69            Debug.LogError(txResponse.Result.Error.Message);_69            Debug.LogError(txResponse.Result.Error.StackTrace);_69            yield break;_69        }_69_69        //Wait until the transaction finishes executing_69        Task<FlowTransactionResult> txResult = Transactions.GetResult(txResponse.Result.Id);_69        yield return new WaitUntil(() => txResult.IsCompleted);_69        _69        //Deploy the SDKExampleNFT contract_69        txResponse = CommonTransactions.DeployContract("SDKExampleNFT", SDKExampleNFTContract.text);_69        yield return new WaitUntil(() => txResponse.IsCompleted);_69        if (txResponse.Result.Error != null)_69        {_69            Debug.LogError(txResponse.Result.Error.Message);_69            Debug.LogError(txResponse.Result.Error.StackTrace);_69            yield break;_69        }_69_69        //Wait until the transaction finishes executing_69        txResult = Transactions.GetResult(txResponse.Result.Id);_69        yield return new WaitUntil(() => txResult.IsCompleted);_69_69        //Unauthenticate as the emulator_service_account_69        FlowSDK.GetWalletProvider().Unauthenticate();_69    }_69_69    //Enable the Authenticate button._69    authenticateButton.interactable = true;_69    statusText.text = "";_69}
We start by waiting one second. This ensures that the emulator has finished initializing and the required service account has been populated.
Next we fetch the emulator_service_account Account. This Account object will contain the contracts that are deployed to the account. We check if both the required contracts are deployed, and if they are not, we deploy them.
Upon first running the scene, you will be presented with two popups by DevWallet. This authorizes the transactions that will deploy the contracts. You will not see these popups during subsequent runs because the contracts will already be present on the account. If you purge the emulator data, you will see the popups again the next time you play the scene.
When using Testnet or Mainnet, the NonFungibleToken contract will already be deployed at a known location. Launching the emulator with the --contracts flag will also deploy this contract. I this case we are running without --contracts, so we will deploy the NonFungibleToken contract ourselves.
Listing, minting, and storing NFTs
Now that the contracts are in place, the Authenticate button will be clickable. This uses the registered wallet provider (DevWalletProvider) to authenticate. Unless you create another account using the FlowControl Tools panel, only emulator_service_account will be available.
After clicking Authenticate, it will prompt you to select an account to authenticate as. Choose emulator_service_account. This is done with the following functions:
_22    public void Authenticate()_22    {_22        FlowSDK.GetWalletProvider().Authenticate("", OnAuthSuccess, OnAuthFailed);_22    }_22    _22    private void OnAuthFailed()_22    {_22        Debug.LogError("Authentication failed!");_22        accountText.text = $"Account:  {FlowSDK.GetWalletProvider().GetAuthenticatedAccount()?.Address??"None"}";_22        if (FlowSDK.GetWalletProvider().GetAuthenticatedAccount() == null)_22        {_22            mintPanelButton.interactable = false;_22            listButton.interactable = false;_22        }_22    }_22_22    private void OnAuthSuccess(string obj)_22    {_22        accountText.text = $"Account:  {FlowSDK.GetWalletProvider().GetAuthenticatedAccount().Address}";_22        mintPanelButton.interactable = true;_22        listButton.interactable = true;_22    }
If authentication succeeds, a coroutine is started that will make the Mint button available.
Clicking on the Mint button displays the Minting panel that will allow you to customize the NFT that will be minted:
_10public void ShowMintPanel()_10    {_10        textInputField.text = "";_10        URLInputField.text = "";_10        mintPanel.SetActive(true);_10    }
Minting
Clicking Mint in the Mint panel will trigger the creation of the NFT with the supplied text.
_10public void MintNFT()_10    {_10        if(FlowSDK.GetWalletProvider() != null && FlowSDK.GetWalletProvider().IsAuthenticated())_10        {_10            StartCoroutine(MintNFTCoroutine());_10        }_10        _10        mintPanel.SetActive(false);_10    }
_42    public IEnumerator MintNFTCoroutine()_42    {_42        statusText.text = "Minting...";_42        List<CadenceBase> args = new List<CadenceBase>_42        {_42            Convert.ToCadence(new Dictionary<string, string>_42            {_42                ["Text"] = textInputField.text,_42                ["URL"] = URLInputField.text_42            }, "{String:String}")_42        };_42_42        Task<FlowTransactionResponse> txResponse = Transactions.Submit(mintTransaction.text, args);_42        _42        while(!txResponse.IsCompleted)_42        {_42            yield return null;_42        }_42_42        if (txResponse.Result.Error != null)_42        {_42            statusText.text = "Error, see log";_42            Debug.LogError(txResponse.Result.Error.Message);_42            yield break;_42        }_42_42        Task<FlowTransactionResult> txResult = Transactions.GetResult(txResponse.Result.Id);_42_42        while (!txResult.IsCompleted)_42        {_42            yield return null;_42        }_42_42        if (txResult.Result.Error != null)_42        {_42            statusText.text = "Error, see log";_42            Debug.LogError(txResult.Result.Error.Message);_42            yield break;_42        }_42        _42        statusText.text = "";_42    }
Because transactions can take a while, they are done in coroutines to prevent the interface from locking up.
First we construct a list of arguments we are going to pass to the transaction in MintAndSave.cdc.  This
list consists of a single Dictionary containing the "Text" and "URL" keys and String values from the Mint
panel.  We use Cadence.Convert to convert from a Dictionary<string, string> into a Cadence {String:String}
for the argument.
The MintAndSave.cdc file contains the transaction that will be executed.
_38import SDKExampleNFT from 0xf8d6e0586b0a20c7_38import NonFungibleToken from 0xf8d6e0586b0a20c7_38_38transaction(md: {String:String}) {_38    let acct : auth(Storage, Capabilities) &Account_38    _38    prepare(signer: auth(Storage, Capabilities) &Account) {_38        self.acct = signer_38    }_38    _38    execute {_38        // Create collection if it doesn't exist_38        if self.acct.storage.borrow<&SDKExampleNFT.Collection>(from: SDKExampleNFT.CollectionStoragePath) == nil_38        {_38            // Create a new empty collection_38            let collection <- SDKExampleNFT.createEmptyCollection()_38            // save it to the account_38            self.acct.save(<-collection, to: SDKExampleNFT.CollectionStoragePath)_38            let newCap = self.acct.capabilities.storage.issue<&{SDKExampleNFT.CollectionPublic, NonFungibleToken.CollectionPublic}>(_38                SDKExampleNFT.CollectionStoragePath_38            )_38            self.acct.capabilities.publish(newCap, to: SDKExampleNFT.CollectionPublicPath)_38        }_38        _38        //Get a reference to the minter_38        let minter = getAccount(0xf8d6e0586b0a20c7)_38            .capabilities.get(SDKExampleNFT.MinterPublicPath)_38            .borrow<&{SDKExampleNFT.PublicMinter}>()_38        _38        _38        //Get a CollectionPublic reference to the collection_38        let collection = self.acct.capabilities.get(SDKExampleNFT.CollectionPublicPath)_38            .borrow<&{NonFungibleToken.CollectionPublic}>()_38              _38        //Mint a new NFT and deposit into the authorizers account_38        minter?.mintNFT(recipient: collection!, metadata: md)_38    }_38}
This transaction checks to see if an SDKExampleNFT collection exists on the account, creating/saving/linking it if it does not. Then it calls the contract to mint a new NFT with the desired metadata and saves it to the collection.
Listing NFTs
The List button calls the UpdateNFTPanelCoroutine function that is responsible for populating the panel with information about the SDKExampleNFT resources in the account you are authenticated as.
_41public IEnumerator UpdateNFTPanelCoroutine()_41{_41    //Create the script request.  We use the text in the GetNFTsOnAccount.cdc file and pass the address of the_41    //authenticated account as the address of the account we want to query._41    FlowScriptRequest scriptRequest = new FlowScriptRequest_41    {_41        Script = listScript.text,_41        Arguments = new List<CadenceBase>_41        {_41            new CadenceAddress(FlowSDK.GetWalletProvider().GetAuthenticatedAccount().Address)_41        }_41    };_41_41    //Execute the script and wait until it is completed._41    Task<FlowScriptResponse> scriptResponse = Scripts.ExecuteAtLatestBlock(scriptRequest);_41    yield return new WaitUntil(() => scriptResponse.IsCompleted);_41_41    //Destroy existing NFT display prefabs_41    foreach (TMP_Text child in NFTContentPanel.GetComponentsInChildren<TMP_Text>())_41    {_41        Destroy(child.transform.parent.gameObject);_41    }_41    _41    //Iterate over the returned dictionary_41    Dictionary<ulong, Dictionary<string, string>> results = Convert.FromCadence<Dictionary<UInt64, Dictionary<string, string>>>(scriptResponse.Result.Value);_41    //Iterate over the returned dictionary_41    foreach (KeyValuePair<ulong, Dictionary<string, string>> nft in results)_41    {_41        //Create a prefab for the NFT_41        GameObject prefab = Instantiate(NFTPrefab, NFTContentPanel.transform);_41        _41        //Set the text_41        string text = $"ID:  {nft.Key}\n";_41        foreach (KeyValuePair<string,string> pair in nft.Value)_41        {_41            text += $"    {pair.Key}: {pair.Value}\n";_41        }_41        _41        prefab.GetComponentInChildren<TMP_Text>().text = text;_41    }_41}
When running a script, you can query any account. In this case we will only query the account that is authenticated with the wallet provider.
It executes the script defined in GetNFTsOnAccount.cdc:
_31import SDKExampleNFT from 0xf8d6e0586b0a20c7_31_31access(all) fun main(addr:Address): {UInt64:{String:String}} {_31_31    //Get a capability to the SDKExampleNFT collection if it exists.  Return an empty dictionary if it does not_31    let collectionCap = getAccount(addr).capabilities.get<&{SDKExampleNFT.CollectionPublic}>(SDKExampleNFT.CollectionPublicPath)_31    if(!collectionCap.check())_31    {_31        return {}_31    }_31    _31    //Borrow a reference to the capability, returning an empty dictionary if it can not borrow_31    let collection = collectionCap.borrow()_31    if(collection == nil)_31    {_31        return {}_31    }_31_31    //Create a variable to store the information we extract from the NFTs_31    var output : {UInt64:{String:String}} = {}_31    _31    //Iterate through the NFTs, extracting id and metadata from each._31    for id in collection?.getIDs()! {_31        log(collection!.borrowSDKExampleNFT(id:id))_31        log(collection!.borrowSDKExampleNFT(id:id)!.metadata)_31        output[id] = collection!.borrowSDKExampleNFT(id:id)!.metadata;_31    }_31    _31    //Return the constructed data_31    return output_31}
This ensures that an SDKExampleNFT.Collection resource exists at the proper path, then creates and returns
a {UInt64:{String:String}} containing the information of all SDKExampleNFTs in the collection.  We use
Cadence.Convert to convert this into a C# Dictionary<UInt64, Dictionary<string,string>>
After that we Instantiate prefabs to display the data of each of the returned NFTs.