LinkLabs (ElizaOS+Functions)
  • Chainlink Workshop: Use AI agent to mint an NFT gift
    • Use an AI agent to interact with Blockchains
  • 1. Create table in Supabase
  • 2. Deploy the GetGift.sol
  • 3 Register A CL Functions Subscription
  • 4. Prepare DON hosted secrets
  • 5. Setup and start Eliza agent
  • 6. Use twitter client
Powered by GitBook
On this page
  • Where are we?
  • What is Functions consumer smart contract?
  • Compile and deploy the GetGift.sol as functions consumer
  • Examine the GetGift code
Export as PDF

2. Deploy the GetGift.sol

Previous1. Create table in SupabaseNext3 Register A CL Functions Subscription

Last updated 3 days ago

Where are we?

What is Functions consumer smart contract?

Requests to Chainlink Functions are constructed and sent from on-chain smart contracts, so we need to build a consumer smart contract for Chainlink Functions.

Compile and deploy the GetGift.sol as functions consumer

  1. ATTENTION: In the pasted code, look for the variable SOURCE. This contains the JavaScript program that Chainlink Functions will execute. Notice that there is a reference to the Supabase database connection URL. Update it, but make sure you leave the beginning and ending "\" symbols. The URL will be a REST API URL that also refers to your supabase project's ID : For example it would look like https://<<Your Project ID>>.supabase.co/rest/v1/Gifts?select=gift_name,gift_code

Note: The table name "Gifts" in the URL above is case-sensitive. Make sure it matches the casing in your table name in Supabase. The safest way to do this is copy the Supabase project ID only and paste it in. Replace only that bit and be careful not to touch any other part of the SOURCE javascript.

  1. Compile the smart contract.

  2. Switch the network in metamask to Avalanche Fuji.

  3. In the tab deploy, select "Injected Provider" under "Environment" to make sure you are using the Avalanche Fuji network in your metamask, the Custom (43113) network should show under Injected Provider.

  4. Deploy GetGift.sol

  5. Save the address of the deployed contract (we will use it on the next page).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol";
import {ERC721} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract GetGift is FunctionsClient, ERC721URIStorage {
    using FunctionsRequest for FunctionsRequest.Request;

    bytes32 public s_lastRequestId;
    bytes public s_lastResponse;
    bytes public s_lastError;
    string public result;
    mapping(address => bool) private allowList;
    mapping(string => bool) private giftCodeRedeemed;
    mapping(bytes32 => address) private reqIdToAddr;
    mapping(bytes32 => string) private reqIdToGiftCode;
    uint256 public tokenId;

    error UnexpectedRequestID(bytes32 requestId);

    event Response(bytes32 indexed requestId, bytes response, bytes err);

    // Gift codes and NFT metadata (saved on IPFS)
    mapping(bytes => string) giftToTokenUri;
    bytes ITEM_1 = bytes("100 discount");
    bytes ITEM_2 = bytes("50 discount");
    bytes ITEM_3 = bytes("1-month premium");

    string constant ITEM_1_METADATA =
        "ipfs://QmaGqBNqHazCjSMNMuDk6VrgjNLMQKNZqaab1vfMHAwkoj";
    string constant ITEM_2_METADATA =
        "ipfs://QmfNhhpUezQLcyqXBGL4ehPwo7Gfbwk9yy3YcJqGgr9dPb";
    string constant ITEM_3_METADATA =
        "ipfs://QmNxq7GqehZf9SpCEFK7C4moxZTZPNwCer5yCAqCBNdk2a";

    // Hardcode for Avalanche Fuji testnet
    address public constant ROUTER_ADDR =
        0xA9d587a00A31A52Ed70D6026794a8FC5E2F5dCb0;
    bytes32 public constant DON_ID =
        0x66756e2d6176616c616e6368652d66756a692d31000000000000000000000000;
    uint32 public constant CALLBACK_GAS_LIMIT = 300_000;

    // Hardcode javascript code that is to sent to DON
    // REPLACE THE SUPBASE PROJECT NAME in js code below:
    // "url: `https://<SUPBASE_PROJECT_NAME>.supabase.co/rest/v1/<TABLE_NAME>?select=<COLUMN_NAME1>,<COLUMN_NAME2>`,"
    // TABLE_NAME is the name of table created in step 1.
    // COLUMN_NAMES are names of columns to be search, in the case, they are gift_code and gift_name.
    string public constant SOURCE =
        "const giftCode = args[0];"
        'if(!secrets.apikey) { throw Error("Error: Supabase API Key is not set!") };'
        "const apikey = secrets.apikey;"
        "const apiResponse = await Functions.makeHttpRequest({"
        'url: "https://nwkmcizenqgokebiuass.supabase.co/rest/v1/Gifts?select=gift_name,gift_code",'
        'method: "GET",'
        'headers: { "apikey": apikey}'
        "});"
        "if (apiResponse.error) {"
        "console.error(apiResponse.error);"
        'throw Error("Request failed: " + apiResponse.message);'
        "};"
        "const { data } = apiResponse;"
        "const item = data.find(item => item.gift_code == giftCode);"
        'if(item == undefined) {return Functions.encodeString("not found")};'
        "return Functions.encodeString(item.gift_name);";

    constructor() FunctionsClient(ROUTER_ADDR) ERC721("Gift", "GT") {
        allowList[msg.sender] = true;
        giftToTokenUri[ITEM_1] = ITEM_1_METADATA;
        giftToTokenUri[ITEM_2] = ITEM_2_METADATA;
        giftToTokenUri[ITEM_3] = ITEM_3_METADATA;
    }

    /**
     * @notice Send a simple request
     * @param subscriptionId Billing ID
     */
    function sendRequest(
        uint8 donHostedSecretsSlotID,
        uint64 donHostedSecretsVersion,
        string[] memory args,
        uint64 subscriptionId,
        address userAddr
    ) external onlyAllowList returns (bytes32 requestId) {
        // make sure the code is redeemable
        string memory giftCode = args[0];
        require(!giftCodeRedeemed[giftCode], "the code is redeemed");

        // send the Chainlink Functions request with DON hosted secret
        FunctionsRequest.Request memory req;
        req.initializeRequestForInlineJavaScript(SOURCE);
        if (donHostedSecretsVersion > 0)
            req.addDONHostedSecrets(
                donHostedSecretsSlotID,
                donHostedSecretsVersion
            );
        if (args.length > 0) req.setArgs(args);
        s_lastRequestId = _sendRequest(
            req.encodeCBOR(),
            subscriptionId,
            CALLBACK_GAS_LIMIT,
            DON_ID
        );

        reqIdToAddr[s_lastRequestId] = userAddr;
        reqIdToGiftCode[s_lastRequestId] = giftCode;
        return s_lastRequestId;
    }

    /**
     * @notice Store latest result/error
     * @param requestId The request ID, returned by sendRequest()
     * @param response Aggregated response from the user code
     * @param err Aggregated error from the user code or from the execution pipeline
     * Either response or error parameter will be set, but never both
     */
    function fulfillRequest(
        bytes32 requestId,
        bytes memory response,
        bytes memory err
    ) internal override {
        if (s_lastRequestId != requestId) {
            revert UnexpectedRequestID(requestId);
        }
        s_lastResponse = response;
        s_lastError = err;

        emit Response(requestId, s_lastResponse, s_lastError);

        // check if the code is valid, incorrected code returns empty string
        if (keccak256(response) == keccak256(bytes("not found"))) return;

        // If no error, mint the NFT
        if (err.length == 0) {
            // response is not empty, giftCode is valid
            address userAddr = reqIdToAddr[requestId];
            string memory tokenUri = giftToTokenUri[response];
            safeMint(userAddr, tokenUri);

            // mark gift code is redeemed
            // please be noticed that gift can only be redeemed once
            string memory giftCode = reqIdToGiftCode[requestId];
            giftCodeRedeemed[giftCode] = true;
        }
    }

    function safeMint(address to, string memory uri) internal {
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
        tokenId++;
    }

    function addGift(string memory giftName, string memory _tokenUri)
        public
        onlyAllowList
    {
        giftToTokenUri[bytes(giftName)] = _tokenUri;
    }

    function addToAllowList(address addrToAdd) external onlyAllowList {
        allowList[addrToAdd] = true;
    }

    function removeFromAllowList() external onlyAllowList {
        allowList[msg.sender] = false;
    }

    modifier onlyAllowList() {
        require(
            allowList[msg.sender],
            "you do not have permission to call the function"
        );
        _;
    }
}

Examine the GetGift code

Because the gifts are represented by NFTs, the contract needs to have NFT minting functionality. This contract also acts as the client for Chainlink Functions requests. Therefore GetGift inherits from the following two contracts:

  1. ERC721URIStorage: this makes GetGift an ERC-721(the NFT standard) compliant smart contract, so that users NFTs can be minted and transferred under the contract.

  2. FunctionsClient: This smart contract provides helper methods and structs to construct and send Chainlink Functions requests and receive the fulfillments from off-chain DON.

ERC-721 related attributes and functions

  1. giftToTokenUri: the mapping is to save the gift name as key and the corresponding NFT metadata as value.

For example, ITEM_1_METADATA saved at the URL ipfs://QmaGqBNqHazCjSMNMuDk6VrgjNLMQKNZqaab1vfMHAwkoj is as below:

{
   "description":"100 dollar discount",
   "external_url":"https://openseacreatures.io/3",
   "image":"https://ipfs.io/ipfs/QmU3aiUjBP66jSAHTJJP5kzAaiwvdsn43yoP1WTZCUsFrn",
   "name":"100 dollar discount",
   "attributes":[
      {
         "trait_type":"value",
         "value":"100"
      }
} 

In the metadata JSON file, imageis the URL of the saved image (https://ipfs.io/ipfs/QmU3aiUjBP66jSAHTJJP5kzAaiwvdsn43yoP1WTZCUsFrn), and it is the image for "100 discount":

  1. addToAllowList: This is a user defined function to add access control for NFT related functions. The function creates an allowlist where users can manage giftToTokenUri and call function sendRequest.

Chainlink Function related attributes and functions

  1. SOURCE: The JavaScript code you supply that that is executed by Chainlink Functions on the DON. In the Chainlink Functions requests, this source code is uploaded and executed in DON environment. This code fetches the data in the Supabase database, and returns the correct gift name corresponding to the gift code provided by the user.

  2. sendRequest: we use this to send Chainlink Functions requests. In this case, the function wraps SOURCE with all necessary data as a Request object and sends it to the DON.

Open and create a new file with name GetGift.sol.

Paste the code below into your new file (the file is also ).

ITEM_1, ITEM_2, ITEM_3 are gifts' names in bytes, ITEM_1_METADATA, ITEM_2_METADATA, ITEM_3_METADATA are URLs that hold JSON metadata for the 3 items. The Metadata is formatted to comply with the .

safeMint: Function to mint a new NFT under this collection. Token ID for all minted NFTs are incremented and input metadata is added to the minted token with function setTokenUri. Find more details about ERC-721 functions .

ROUTER_ADDR: Hardcoded Functions Router address on Avalanche Fuji testnet. The address is used to construct Chainlink Functions requests. Functions Router contracts addresses vary across different blockchain networks. Configurations for router addresses can be found on page.

DON_ID: Hardcoded ID for the DON (decentralized oracle network) employed by Chainlink Functions on Avalanche Fuji. Similar as Functions router addresses, DON ID varies in different blockchain network. Please check the DON ID configurations.

CALLBACK_GAS_LIMIT: Hardcoded maximum of gas limit that Chainlink Functions can spend to make callback call. Check the for more details.

fulfillRequest: this is a method that must be implemented in any consumer contract for Chainlink Functions to work. It is used as the "callback" function that the DON sends responses to, after the computation in SOURCEis completed . In our case, the DON returns gift names to the GetGiftconsumer contract, and fulfillRequest then calls the internal function safeMint if the returned gift name is valid.You can find more details about the function in .

remix
here
Opensea metadata standard
here
Supported Networks
Supported Network
official docs
official document
Image for the gift 100 discount