Skip to main content

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:

  1. Creating a contract that implements INFT
  2. Deploying the contract
  3. 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:


_15
public 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:


_69
public 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:


_10
public 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.


_10
public 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.


_38
import SDKExampleNFT from 0xf8d6e0586b0a20c7
_38
import NonFungibleToken from 0xf8d6e0586b0a20c7
_38
_38
transaction(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.


_41
public 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:


_31
import SDKExampleNFT from 0xf8d6e0586b0a20c7
_31
_31
access(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.