Create a Dynamic NFT

This guide will walk you through creating a dynamic NFT with Space and Time and Chainlink Functions

Introduction

This guide will walk you through creating a dynamic NFT using Space and Time, and Chainlink Functions. We will create a SwordNFT that's rating changes based on how the sword is being used in-game. Gaming telemetry in Space and Time is updated and fed to the NFT contract using Chainlink Functions.

Here is an example of what the NFT will look like when we're done.

Before we get started, we should answer a couple of important questions.

What is a dynamic NFT (dNFT)?

From a simple view, according to ERC721 (and ERC1155) non-fungible tokens should have a tokenURI function that stores a URL which will return a JSON blob of metadata for a given NFT. That blob usually contains things like a pointer to an image file, NFT name, description, etc. A dNFT is simply an NFT where the metadata for the NFT is designed to change.

But I thought NFTs weren't supposed to change!

There is nothing in ERC721/ERC1155 that says an NFT's metadata cannot change. The idea that an NFT's metadata shouldn't change likely comes from one specific use case involving permanent digital collectibles. While a full discussion on the topic is beyond the scope of this guide, it's important to understand that NFTs can take on many forms, and there is an increasing demand for NFTs that can evolve and/or be leveled up. Also, keep in mind that it's important to think about who (or what) can make changes to an NFT, how those changes can be made, and how long it's possible to make changes.

Overview

For the guide we will go through the following high-level steps:

  1. Base Setup & Config
    • Prerequisites
    • Download & install repo
    • Setup env-enc
  2. Space and Time Setup
    • Create Table
    • Insert Data
  3. Connect SxT to Mumbai via Chainlink Functions
  4. Level Up Your SwordNFT

1. Base Setup & Config

Space and Time and Chainlink Functions are both in beta. It's probable that updates will be made to one or both that change how steps in this guide work. You can find us on Discord with questions.

Prerequisites

  1. You will need beta access to Space and Time and Chainlink Functions.

  2. If you're new to SxT, it's recommended that you start first with our SxT Getting Started Guide. If you're new to Chainlink Functions, we recommend you start with their Getting Started Guide. Having a basic understanding of how to connect to SxT and how Chainlink Functions work will set you up for success with this guide.

  3. You will obviously need a wallet address on Polygon Mumbai testnet, with token balances of 2 LINK and 0.2 MATIC.

Setup

Open up a terminal / command window, navigate to a folder where you'd like to run this demo from, and enter the following commands into the terminal window:

  1. Clone the demo repo from github, which includes all solidity and js files:
    git clone https://github.com/SxT-Community/SxT-dNFT.git && cd SxT-DNFT
    If you don't have git installed, this page has install instructions..

  2. Install all node.js dependancies by running: npm install

  3. One of the packages you install is a handy tool created by Chainlink Labs for encrypting your local environment variables. Please have a look at npx env-enc help to see all available commands.

  • Enter npx env-enc set-pw to set your root password (or unlock your encrypted envar file)

  • Enter env-enc set to set the following environment variables, which will be used by the above demo. The commandline app will ask for a variable NAME, then VALUE. For each, enter:

    • POLYGON_MUMBAI_RPC_URL - RPC_URL for Mumbai network (Infura, Alchemy, etc) used by CL Functions.
      For example, check out the first two sections of Infura's 'Getting Started' docs. The RPC_URL you'll need will look something like: https://polygon-mumbai.infura.io/v3/your_api_key
    • MUMBAI_RPC_URL - RPC_URL used by Hardhat - same URL as above. As of this writing, Hardhat and Chainlink have different environment variables for the RPC endpoint. The value should be 100% identical.
    • PRIVATE_KEY - Private key for your wallet address (with aforementioned 2 LINK and 0.2 MATIC), and should look something like 4bb23044e2b4e1b8e3793d6686306915adcc25e35211f08f91c462b36250ac99
    • POLYGONSCAN_API_KEY - API Key for Polyscan - optional, but recommended to verify contracts.
      The API key should look something like: G9JRV6CBJWYMPUGQ7382RKUBNINI44QRUF
    • API_KEY - API Key for Space and Time - required to execute off-chain queries.
      The API key should look something like: sxt_dMFtbHgkW5_59sND2XMSXuEeOtXzNalSKbvZ
    • API_URL - Space and Time API URL, which contains the gateway / secrets proxy endpoint, where the query is sent. The URL should look something like: https://proxy.api.spaceandtime.app/v1/sql

2. Space and Time Setup

Setup Data in Space and Time

📘

There are many ways to connect to Space and Time, however the easiest and fastest is via Space and Time Studio.

Here's a look at the gaming telemetry table SxT we're going to use:

Table: SXTNFT.GAME_TELEMETRY_ARTHUR

IDGamerIdActionTypeAchievementIdcollectableIdLevelItemIdpoints
11Game Started1SwordNFT3
SwordNFT1Attack1SwordNFT3
SwordNFT1Defense1SwordNFT2
41KillKing1SwordNFT100
51Attack1SwordNFT3
61CollectPotionA2SwordNFT100
71CollectPotionB2SwordNFT150
81Attack3SwordNFT9

  1. The table above is Public_Write meaning anyone can read and modify data. This is ideal for a public demo since it means you can use this table for your demo as-is. However, others can also, and you may have others working on the demo simultaneously, leading to unexpected results. If you want to create your version of the table, use this recipe for creating tables with this SQL:
CREATE TABLE <SomeOtherSchema>.GAME_TELEMETRY_ARTHUR (
    ID INTEGER,
    GamerId INTEGER,
    ActionType VARCHAR,
    AchievementId VARCHAR,
    collectableId VARCHAR,
    Level_ INTEGER,
    ItemId VARCHAR,
    Points INTEGER,
    PRIMARY KEY (ID)
) WITH "public_key=<your_biscuit_public_key>,
       access_type=public_write, 
       template=PARTITIONED, 
       atomicity=transactional"
  1. Delete any records that may be in the table already using the DML endpoint:
Delete From SXTNFT.GAME_TELEMETRY_ARTHUR
  1. INSERT data - This is where a game would be inserting its telemetry into SxT:
INSERT INTO SXTNFT.GAME_TELEMETRY_ARTHUR (ID, GamerId, ActionType, AchievementId, collectableId, Level_, ItemId, Points)
VALUES 
    (1, 1, 'Game Started', '', '', 1, 'SwordNFT', 3),
    (2, 1, 'Attack', '', '', 1, 'SwordNFT', 3),
    (3, 1, 'Defense', '', '', 1, 'SwordNFT', 2),
    (4, 1, 'Kill', 'King', '', 1, 'SwordNFT', 100),
    (5, 1, 'Attack', '', '', 1, 'SwordNFT', 3);
  1. View data that is loaded:
SELECT * from SXTNFT.GAME_TELEMETRY_ARTHUR

Also, we can run logic to determine the SwordNFT's current level:

SELECT /*! USE ROWS */
    ItemId, 
    SUM(Points),
    CASE 
        WHEN SUM(Points) BETWEEN 100 AND 150 THEN 1
        WHEN SUM(Points) BETWEEN 151 AND 300 THEN 2
        WHEN SUM(Points) > 300 THEN 3 
        ELSE ''
    END AS SWORD  
FROM SXTNFT.GAME_TELEMETRY_ARTHUR
GROUP BY ItemId;

Should return:

ITEMID  |SUM(POINTS)|SWORD|
--------+-----------+-----+
SwordNFT|111        |1    |

3. Connect SxT to Mumbai via Chainlink Functions

Now that we have our gaming telemetry table in SxT, we're going to connect everything up. The following steps were adapted from the Chainlink Functions repo and might be useful as a resource if you run into any issues getting Chainlink Functions setup.

The first thing we're going to do is simulate the full interaction. This is helpful because it allows us to identify potential issues before we deploy our dNFT contract.

In your terminal window (same one where you set your environment variables above via env-enc set) enter the following steps:

  1. Test/Simulate

    npx hardhat functions-simulate-script

  2. Deploy our contract to Mumbai:

    npx hardhat functions-deploy-consumer --network polygonMumbai --verify true

  3. Get the contract address from the result of the previous step and set a temporary envar:

    export CONTRACT_ADDRESS=<your_contract_address>

  4. Create and fund a new Functions billing subscription using the Chainlink Functions UI and add the deployed consumer contract as an authorized consumer to your subscription. You can also do this programmatically with:

    npx hardhat functions-sub-create --network polygonMumbai --amount 2 --contract $CONTRACT_ADDRESS

    Get the subscription id and set a shell envar for:
    export SUB_ID=<CL_FUNCTIONS_SUB_ID>

  5. Prompt the NFT to call Chainlink Functions request to query Space and Time:

npx hardhat functions-request --network polygonMumbai --contract $CONTRACT_ADDRESS --subid $SUB_ID

  1. Now is an excellent time to pull up your dNFT contract on Rarible!
    • Enter into the terminal:open https://testnet.rarible.com/token/polygon/$CONTRACT_ADDRESS:0
    • or go to the URL directly, replacing $CONTRACT_ADDRESS with the contract address from step #3
  • NOTE: If you get an error about your contract not being added to the subscription, you can add it as a Consumer to your Subscription via the web UI or the CLI like this:

npx hardhat functions-sub-add --network polygonMumbai --contract $CONTRACT_ADDRESS --subid $SUB_ID

4. Level Up Your dNFT Sword

Now it's time to level up your swordNFT based on new gaming telemetry loaded into Space and Time.

Add more game telemetry to SxT (sword 2)

INSERT INTO SXTNFT.GAME_TELEMETRY_ARTHUR (ID, GamerId, ActionType, AchievementId, collectableId, Level_, ItemId, Points)
VALUES (6, 1, 'Collect', '', 'PotionA', 2, 'SwordNFT', 100);
SELECT /*! USE ROWS */
    ItemId,
    SUM(Points),
    CASE 
        WHEN SUM(Points) BETWEEN 0 AND 150 THEN 1
        WHEN SUM(Points) BETWEEN 151 AND 300 THEN 2
        WHEN SUM(Points) > 300 THEN 3 
        ELSE ''
    END AS SWORD  
FROM SXTNFT.GAME_TELEMETRY_ARTHUR
GROUP BY ItemId;

Game prompts NFT to re-query Space and Time

  1. Test/Simulate
    npx hardhat functions-simulate-script

  2. Prompt the NFT to call Chainlink Functions request to query Space and Time:

    npx hardhat functions-request --network polygonMumbai --contract $CONTRACT_ADDRESS --subid $SUB_ID

  3. Let's look at the NFT again, to see it change! Just enter into the terminal:
    open https://testnet.rarible.com/token/polygon/$CONTRACT_ADDRESS:0 or go to the URL directly, replacing $CONTRACT_ADDRESS with the contract address from step #3

Add More Game Telemetry to SxT (sword 3)

As the game is played, more telemetry is loaded into Space and Time!

INSERT INTO SXTNFT.GAME_TELEMETRY_ARTHUR(ID, GamerId, ActionType, AchievementId, collectableId, Level_, ItemId, Points)
VALUES 
    (7, 1, 'Collect', '', 'PotionB', 2, 'SwordNFT', 150),
    (8, 1, 'Attack', '', '', 3, 'SwordNFT', 9);

View the insert with the following:

SELECT * from SXTNFT.GAME_TELEMETRY_ARTHUR

Or:

SELECT /*! USE ROWS */ 
    ItemId,
    SUM(Points),
    CASE 
        WHEN SUM(Points) BETWEEN 100 AND 150 THEN 1
        WHEN SUM(POINTS) BETWEEN 151 AND 300 THEN 2
        WHEN SUM(POINTS) > 300 THEN 3 
        ELSE 1 
    END AS SWORD  
FROM SXTNFT.GAME_TELEMETRY_ARTHUR
GROUP BY ItemId;

Push New Telemetry to Mumbai

  1. Test/Simulate
    npx hardhat functions-simulate-script

  2. Prompt the NFT to call Chainlink Functions request to query Space and Time:

    npx hardhat functions-request --network polygonMumbai --contract $CONTRACT_ADDRESS --subid $SUB_ID

Head back to Rarible to see your level three swordNFT!