Developing Dynamic NFTs

Ever wanted to create an NFT that reacts to real-world events and data? Now you can, with Chain Link Oracles. Let’s check out how this can be done!

Jeffer1981
Better Programming

--

Photo by Choong Deng Xiang on Unsplash

Remark: unfortunately Chain Link Oracle is only set up on the Kovan testnet, while the OpenSea testnet is on Rinkeby. So developing and testing everything was done on over two networks, which was not ideal.

What’s a Chain Link Oracle?

The first generation of NFTs, like BAYC, CryptoPunks,… are in essence static. Their image and metadata are usually just stored in static files on eg IPFS or a (cloud) server. Some level of change in the NFT was sometimes developed by the ability to switch the NFT from one set of static files to another set.

Chain Link Oracles provide a utility where you can interface from your Ethereum smart contract with any API. That’s very cool, it means you can get any data that are being offered through an API to your smart contract, and you can find APIs on just about anything: stock prices, sports data, weather,…

On-chain metadata

The second change we need to make is that our NFTs metadata can be dynamic. So instead of storing the metadata in an off-chain JSON file, we will now generate the metadata onchain.

Step 1: using the Chain Link Oracle

In this example, we will be retrieving the temperature of a city through an API from WeatherAPI.com.

Let’s see some code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import '@chainlink/contracts/src/v0.8/ChainlinkClient.sol';
import '@chainlink/contracts/src/v0.8/ConfirmedOwner.sol';
import "./Base64.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract DynamicNFT is ERC721, ChainlinkClient, Ownable {
using Chainlink for Chainlink.Request;
using Counters for Counters.Counter;
uint256 public temperature;
bytes32 private jobId;
uint256 private fee;
Counters.Counter private _tokenIdCounter;
bool public paused = false;
event RequestTemperature(bytes32 indexed requestId, uint256 temperature);/**
* @notice Initialize the link token and target oracle
*
* Kovan Testnet details:
* Link Token: 0xa36085F69e2889c224210F603D836748e7dC0088
* Oracle: 0x74EcC8Bdeb76F2C6760eD2dc8A46ca5e581fA656 (Chainlink DevRel)
* jobId: ca98366cc7314957b8c012c72f05aeeb
*
*/
constructor() ERC721("Dynamic NFT", "DNFT") {
setChainlinkToken(0xa36085F69e2889c224210F603D836748e7dC0088);
setChainlinkOracle(0x74EcC8Bdeb76F2C6760eD2dc8A46ca5e581fA656);
jobId = 'ca98366cc7314957b8c012c72f05aeeb';
fee = (1 * LINK_DIVISIBILITY) / 10; // 0,1 * 10**18 (Varies by network and job)
temperature=0;
}
function buildMetadata(uint256 _tokenId)
private
view
returns (string memory)
{
return
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"Dynamic NFT", "description":"Dynamic NFT Test","image": "https://gateway.pinata.cloud/ipfs/QmeAKDXvQyGUdvwRSvazCyj4CYeN6qrcpQr4Lmgf7Cc2UC", "attributes": ',
"[",
'{"trait_type": "Temperature",',
'"value":"',
Strings.toString(temperature),
'"}',
"]",
"}"
)
)
)
)
);
}
function requestTempData() public returns (bytes32 requestId) {
Chainlink.Request memory req = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
// Set the URL to perform the GET request on
req.add('get', 'http://api.weatherapi.com/v1/current.json?q=Aartselaar&Key=30e737e440484fd18a5134039221006');
req.add('path', 'current,temp_c');
// Multiply the result by 1000000000000000000 to remove decimals
int256 timesAmount = 10;
req.addInt('times', timesAmount);
// Sends the request
return sendChainlinkRequest(req, fee);
}
/**
* Receive the response in the form of uint256
*/
function fulfill(bytes32 _requestId, uint256 _temperature) public recordChainlinkFulfillment(_requestId) {
emit RequestTemperature(_requestId, _temperature);
temperature = _temperature;
}
/**
* Allow withdraw of Link tokens from the contract
*/
function withdrawLink() public onlyOwner {
LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());
require(link.transfer(msg.sender, link.balanceOf(address(this))), 'Unable to transfer');
}
//ERC721 functions
function safeMint(uint minttimes) external payable {

require(!paused);

for(uint i=0;i<minttimes;i++){
_safeMint(msg.sender, _tokenIdCounter.current());
_tokenIdCounter.increment();

}


}
function tokenURI(uint256 tokenId)
public
view
virtual
override(ERC721)
returns (string memory)
{
require(
_exists(tokenId),
"ERC721Metadata: URI query for nonexistent token"
);

return buildMetadata(tokenId);
}

//Return current counter value
function getCounter()

external
view
returns (uint256)
{
return _tokenIdCounter.current();
}
function setTemp(uint256 newTemp) external{
temperature = newTemp;
}
}

First of all, we need to import the Chain Link client library and create our contract using this library.

Let’s look at the constructor:

constructor() ERC721(“Dynamic NFT”, “DNFT”) {setChainlinkToken(0xa36085F69e2889c224210F603D836748e7dC0088);setChainlinkOracle(0x74EcC8Bdeb76F2C6760eD2dc8A46ca5e581fA656);jobId = ‘ca98366cc7314957b8c012c72f05aeeb’;fee = (1 * LINK_DIVISIBILITY) / 10; // 0,1 * 10**18 (Varies by network and job)}

Setting these variables (setChainlinkToken, setChainlinkOracle and jobid) links our contract with the Chain Link Oracle on Kovan.

The fee for using Oracle is 0.1 LINK per API call. So you’ll need to make sure your contract is always funded with enough LINK to make the expected number of API calls!

Two functions are defined to use the Oracle: requestTempData and fulfill.

function requestTempData() public returns (bytes32 requestId) {Chainlink.Request memory req = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);// Set the URL to perform the GET request onreq.add(‘get’, ‘http://api.weatherapi.com/v1/current.json?q=Aartselaar&Key=30e737e440484fd18a5134039221006');req.add(‘path’, ‘current,temp_c’);// Multiply the result by 1000000000000000000 to remove decimalsint256 timesAmount = 10;req.addInt(‘times’, timesAmount);// Sends the requestreturn sendChainlinkRequest(req, fee);}

requestTempData builds the API request (get) and specifies which data from the JSON answer it will fetch (path). As we’re expecting an integer and the temperature is retrieved with one decimal, we multiply the answer by 10. Finally, the API call is made while paying the LINK fee.

Fulfill retrieves the API answer and stores it in the local temperature variable:

function fulfill(bytes32 _requestId, uint256 _temperature) public recordChainlinkFulfillment(_requestId) {emit RequestTemperature(_requestId, _temperature);temperature = _temperature;}

Let’s try it out on Kovan:

Before the API call:

After executing the API call:

It actually is about 21C here:

Step 2: NFT with on chain metadata

Making the NFT dynamic means we have to build the metadata on chain with the variables we get from the API. We do this by calling the following function in the tokenURI function:

function buildMetadata(uint256 _tokenId)

function buildMetadata(uint256 _tokenId)
private
view
returns (string memory)
{
return
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"Dynamic NFT", "description":"Dynamic NFT Test","image": "https://gateway.pinata.cloud/ipfs/QmeAKDXvQyGUdvwRSvazCyj4CYeN6qrcpQr4Lmgf7Cc2UC", "attributes": ',
"[",
'{"trait_type": "Temperature",',
'"value":"',
Strings.toString(temperature),
'"}',
"]",
"}"
)
)
)
)
);
}

So let’s test this on Rinkeby:

After minting, before the API call, the value is initialized to 0. Now we run the API call and check the value again.

Value is updated to the local temperature! Nice!

Conclusion

As shown, you can make your NFT dynamic by using the Chain Link Oracle external API call to get data and by generating your metadata on the chain.

Drawbacks are that every API call costs LINK and thus money and that dynamic contracts are more complex and thus cost more gas. No free lunch sadly. Really annoying that the Chain Link tools are available on Kovan for testing, but Opensea is on Rinkeby.

Chain Link also offers smart contract automation tools, so you could automate the API calls as well.

I hope you enjoyed this article!

--

--

Project Manager for ERP software | Tech enthousiast in python, AI | Loves sports and martial arts | Living in Belgium