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.