Return multiple responses and decode them in your smart contract

In the Using Imports with Functions tutorial, we explored the fundamentals of module imports. This tutorial will teach you how to use the Ethers library encode function to perform ABI encoding of several responses. Then, you will use the ABI specifications in Solidity to decode the responses in your smart contract.

Prerequisites

This tutorial assumes you have completed the Using Imports with Functions tutorial. Also, check your subscription details (including the balance in LINK) in the Chainlink Functions Subscription Manager. If your subscription runs out of LINK, follow the Fund a Subscription guide.

In this tutorial, you will use a different Chainlink Functions consumer contract, which shows how to use ABI decoding to decode the response received from Chainlink Functions:

  1. Open the FunctionsConsumerDecoder.sol contract in Remix.

  2. Compile the contract.

  3. Open MetaMask and select the Ethereum Sepolia network.

  4. In Remix under the Deploy & Run Transactions tab, select Injected Provider - MetaMask in the Environment list. Remix will use the MetaMask wallet to communicate with Ethereum Sepolia.

  5. Under the Deploy section, fill in the router address for your specific blockchain. You can find this address on the Supported Networks page. For Ethereum Sepolia, the router address is 0xb83E47C2bC239B3bf370bc41e1459A34b41238D0.

  6. Click the Deploy button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to Ethereum Sepolia.

  7. After you confirm the transaction, the contract address appears in the Deployed Contracts list. Copy the contract address.

  8. Add your consumer contract address to your subscription on Ethereum Sepolia.

Tutorial

This tutorial demonstrates using the ethers library to interact with smart contract functions through a JSON RPC provider. It involves calling the latestRoundData, decimals, and description functions of a price feed contract based on the AggregatorV3Interface. After retrieving the necessary data, the guide shows how to use ABI encoding to encode these responses into a single hexadecimal string and then convert this string to a Uint8Array. This step ensures compliance with the Chainlink Functions API requirements, which specify that the source code must return a Uint8Array representing the bytes for on-chain use.

You can locate the scripts used in this tutorial in the examples/12-abi-encoding directory.

To run the example:

  1. Make sure you have correctly set up your environment first. If you haven't already, follow the Set up your environment section of the Using Imports with Functions tutorial.

  2. Open the file request.js, located in the 12-abi-encoding folder.

  3. Replace the consumer contract address and the subscription ID with your own values.

    const consumerAddress = "0x5fC6e53646CC53f0C3575fd2c71b5056c4823f5c" // REPLACE this with your Functions consumer address
    const subscriptionId = 139 // REPLACE this with your subscription ID
    
  4. Make a request:

    node examples/12-abi-encoding/request.js
    

    The script runs your function in a sandbox environment before making an onchain transaction:

    $ node examples/12-abi-encoding/request.js
    secp256k1 unavailable, reverting to browser version
    Start simulation...
    Simulation result {
      capturedTerminalOutput: 'Fetched BTC / USD price: dataFeedResponse.answer\n' +
        'Updated at: 1712941559\n' +
        'Decimals: 8\n' +
        'Description: BTC / USD\n',
      responseBytesHexstring: '0x0000000000000000000000000000000000000000000000000000063c3570cc8400000000000000000000000000000000000000000000000000000000661969f7000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000'
    }
    āœ… Decoded response to bytes:  0x0000000000000000000000000000000000000000000000000000063c3570cc8400000000000000000000000000000000000000000000000000000000661969f7000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000
    
    Estimate request costs...
    Fulfillment cost estimated to 1.007671833192655 LINK
    
    Make request...
    
    āœ… Functions request sent! Transaction hash 0x5618089ec9b5e662ec72c81241d78cb6daa135ecc3fa3a33032d910e3b47c2b1. Waiting for a response...
    See your request in the explorer https://sepolia.etherscan.io/tx/0x5618089ec9b5e662ec72c81241d78cb6daa135ecc3fa3a33032d910e3b47c2b1
    
    āœ… Request 0xdf22fa28c81a3ea78f356334b6d28d969e953009fae8ece4fe544f2eb466419b successfully fulfilled. Cost is 0.282344694329387405 LINK.Complete reponse:  {
      requestId: '0xdf22fa28c81a3ea78f356334b6d28d969e953009fae8ece4fe544f2eb466419b',
      subscriptionId: 2303,
      totalCostInJuels: 282344694329387405n,
      responseBytesHexstring: '0x0000000000000000000000000000000000000000000000000000063c3570cc8400000000000000000000000000000000000000000000000000000000661969f7000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000',
      errorString: '',
      returnDataBytesHexstring: '0x',
      fulfillmentCode: 0
    }
    
    āœ… Raw response:  0x0000000000000000000000000000000000000000000000000000063c3570cc8400000000000000000000000000000000000000000000000000000000661969f7000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000
    
    āœ… Fetched BTC / USD price: 6855664389252 (updatedAt: 1712941559) (decimals: 8) (description: BTC / USD)
    

    The output of the example gives you the following information:

    • Your request is first run on a sandbox environment to ensure it is correctly configured.

    • The fulfillment costs are estimated before making the request.

    • Your request was successfully sent to Chainlink Functions. The transaction in this example is 0x5618089ec9b5e662ec72c81241d78cb6daa135ecc3fa3a33032d910e3b47c2b1, and the request ID is 0xdf22fa28c81a3ea78f356334b6d28d969e953009fae8ece4fe544f2eb466419b.

    • The DON successfully fulfilled your request. The total cost was: 0.282344694329387405 LINK.

    • The consumer contract received a response in hexadecimal string with a value of 0x0000000000000000000000000000000000000000000000000000063c3570cc8400000000000000000000000000000000000000000000000000000000661969f7000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000. This value is the ABI encoded response of the latestRoundData, decimals, and description of the BTC / USD price feed. This value is then decoded and stored in the consumer contract.

    • The script calls the consumer contract to fetch the decoded values and then logs them to the console. The output is Fetched BTC / USD price: 6855664389252 (updatedAt: 1712941559) (decimals: 8) (description: BTC / USD).

Examine the code

FunctionsConsumerDecoder.sol

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

import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.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 FunctionsConsumerDecoder is FunctionsClient, ConfirmedOwner {
    using FunctionsRequest for FunctionsRequest.Request;

    bytes32 public s_lastRequestId;
    bytes public s_lastResponse;
    bytes public s_lastError;

    uint256 public s_answer;
    uint256 public s_updatedAt;
    uint8 public s_decimals;
    string public s_description;

    error UnexpectedRequestID(bytes32 requestId);

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

    event DecodedResponse(
        bytes32 indexed requestId,
        uint256 answer,
        uint256 updatedAt,
        uint8 decimals,
        string description
    );

    constructor(
        address router
    ) FunctionsClient(router) ConfirmedOwner(msg.sender) {}

    /**
     * @notice Send a simple request
     * @param source JavaScript source code
     * @param encryptedSecretsUrls Encrypted URLs where to fetch user secrets
     * @param donHostedSecretsSlotID Don hosted secrets slotId
     * @param donHostedSecretsVersion Don hosted secrets version
     * @param args List of arguments accessible from within the source code
     * @param bytesArgs Array of bytes arguments, represented as hex strings
     * @param subscriptionId Billing ID
     */
    function sendRequest(
        string memory source,
        bytes memory encryptedSecretsUrls,
        uint8 donHostedSecretsSlotID,
        uint64 donHostedSecretsVersion,
        string[] memory args,
        bytes[] memory bytesArgs,
        uint64 subscriptionId,
        uint32 gasLimit,
        bytes32 donID
    ) external onlyOwner returns (bytes32 requestId) {
        FunctionsRequest.Request memory req;
        req.initializeRequestForInlineJavaScript(source);
        if (encryptedSecretsUrls.length > 0)
            req.addSecretsReference(encryptedSecretsUrls);
        else if (donHostedSecretsVersion > 0) {
            req.addDONHostedSecrets(
                donHostedSecretsSlotID,
                donHostedSecretsVersion
            );
        }
        if (args.length > 0) req.setArgs(args);
        if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs);
        s_lastRequestId = _sendRequest(
            req.encodeCBOR(),
            subscriptionId,
            gasLimit,
            donID
        );
        return s_lastRequestId;
    }

    /**
     * @notice Send a pre-encoded CBOR request
     * @param request CBOR-encoded request data
     * @param subscriptionId Billing ID
     * @param gasLimit The maximum amount of gas the request can consume
     * @param donID ID of the job to be invoked
     * @return requestId The ID of the sent request
     */
    function sendRequestCBOR(
        bytes memory request,
        uint64 subscriptionId,
        uint32 gasLimit,
        bytes32 donID
    ) external onlyOwner returns (bytes32 requestId) {
        s_lastRequestId = _sendRequest(
            request,
            subscriptionId,
            gasLimit,
            donID
        );
        return s_lastRequestId;
    }

    /**
     * @dev Internal function to process the outcome of a data request. It stores the latest response or error and updates the contract state accordingly. This function is designed to handle only one of `response` or `err` at a time, not both. It decodes the response if present and emits events to log both raw and decoded data.
     *
     * @param requestId The unique identifier of the request, originally returned by `sendRequest`. Used to match responses with requests.
     * @param response The raw aggregated response data from the external source. This data is ABI-encoded and is expected to contain specific information (e.g., answer, updatedAt) if no error occurred. The function attempts to decode this data if `response` is not empty.
     * @param err The raw aggregated error information, indicating an issue either from the user's code or within the execution of the user Chainlink Function.
     *
     * Emits a `DecodedResponse` event if the `response` is successfully decoded, providing detailed information about the data received.
     * Emits a `Response` event for every call to log the raw response and error data.
     *
     * Requirements:
     * - The `requestId` must match the last stored request ID to ensure the response corresponds to the latest request sent.
     * - Only one of `response` or `err` should contain data for a given call; the other should be empty.
     */
    function fulfillRequest(
        bytes32 requestId,
        bytes memory response,
        bytes memory err
    ) internal override {
        if (s_lastRequestId != requestId) {
            revert UnexpectedRequestID(requestId);
        }

        s_lastError = err;
        s_lastResponse = response;

        if (response.length > 0) {
            (
                uint256 answer,
                uint256 updatedAt,
                uint8 decimals,
                string memory description
            ) = abi.decode(response, (uint256, uint256, uint8, string));

            s_answer = answer;
            s_updatedAt = updatedAt;
            s_decimals = decimals;
            s_description = description;

            emit DecodedResponse(
                requestId,
                answer,
                updatedAt,
                decimals,
                description
            );
        }

        emit Response(requestId, response, err);
    }
}

This Solidity contract is similar to the FunctionsConsumer.sol contract used in the Using Imports with Functions tutorial. The main difference is the processing of the response in the fulfillRequest function:

  • It uses Solidity abi.decode to decode the response to retrieve the answer, updatedAt, decimals, and description.

    (
      uint256 answer,
      uint256 updatedAt,
      uint8 decimals,
      string memory description
    ) = abi.decode(response, (uint256, uint256, uint8, string));
    
  • Then stores the decoded values in the contract state.

    s_answer = answer;
    s_updatedAt = updatedAt;
    s_decimals = decimals;
    s_description = description;
    

JavaScript example

source.js

The Decentralized Oracle Network will run the JavaScript code. The code is self-explanatory and has comments to help you understand all the steps.

The example source.js file is similar to the one used in the Using Imports with Functions tutorial. It uses a JSON RPC call to the latestRoundData, decimals, and description functions of a Chainlink Data Feed. It then uses the ethers library to encode the response of these functions into a single hexadecimal string.

const encoded = ethers.AbiCoder.defaultAbiCoder().encode(
  ["uint256", "uint256", "uint8", "string"],
  [dataFeedResponse.answer, dataFeedResponse.updatedAt, decimals, description]
)

Finally, it uses the ethers library getBytes to convert the hexadecimal string to a Uint8Array:

return ethers.getBytes(encoded)

request.js

This explanation focuses on the request.js script and shows how to use the Chainlink Functions NPM package in your own JavaScript/TypeScript project to send requests to a DON. The code is self-explanatory and has comments to help you understand all the steps.

The script imports:

  • path and fs: Used to read the source file.
  • ethers: Ethers.js library, enables the script to interact with the blockchain.
  • @chainlink/functions-toolkit: Chainlink Functions NPM package. All its utilities are documented in the NPM README.
  • @chainlink/env-enc: A tool for loading and storing encrypted environment variables. Read the official documentation to learn more.
  • ../abi/functionsDecoder.json: The abi of the contract your script will interact with. Note: The script was tested with this FunctionsConsumerDecoder contract.

The script has two hardcoded values that you have to change using your own Functions consumer contract and subscription ID:

const consumerAddress = "0x5fC6e53646CC53f0C3575fd2c71b5056c4823f5c" // REPLACE this with your Functions consumer address
const subscriptionId = 139 // REPLACE this with your subscription ID

The primary function that the script executes is makeRequestSepolia. This function consists of five main parts:

  1. Definition of necessary identifiers:

    • routerAddress: Chainlink Functions router address on Sepolia.
    • donId: Identifier of the DON that will fulfill your requests on Sepolia.
    • explorerUrl: Block explorer URL of the Sepolia testnet.
    • source: The source code must be a string object. That's why we use fs.readFileSync to read source.js and then call toString() to get the content as a string object.
    • args: During the execution of your function, These arguments are passed to the source code.
    • gasLimit: Maximum gas that Chainlink Functions can use when transmitting the response to your contract.
    • Initialization of ethers signer and provider objects. The signer is used to make transactions on the blockchain, and the provider reads data from the blockchain.
  2. Simulating your request in a local sandbox environment:

    • Use simulateScript from the Chainlink Functions NPM package.
    • Read the response of the simulation. If successful, use the Functions NPM package decodeResult function and ReturnType enum to decode the response to the expected returned type (ReturnType.bytes in this example).
  3. Estimating the costs:

    • Initialize a SubscriptionManager from the Functions NPM package, then call the estimateFunctionsRequestCost.
    • The response is returned in Juels (1 LINK = 10**18 Juels). Use the ethers.utils.formatEther utility function to convert the output to LINK.
  4. Making a Chainlink Functions request:

    • Initialize your functions consumer contract using the contract address, abi, and ethers signer.
    • Call the sendRequest function of your consumer contract.
  5. Waiting for the response:

    • Initialize a ResponseListener from the Functions NPM package and then call the listenForResponseFromTransaction function to wait for a response. By default, this function waits for five minutes.
    • Upon reception of the response, use the Functions NPM package decodeResult function and ReturnType enum to decode the response to the expected returned type (ReturnType.bytes in this example).
  6. Read the decoded response:

    • Call the s_answer, s_updatedAt, s_decimals, and s_description functions of your consumer contract to fetch the decoded values.
    • Log the decoded values to the console.

Handling complex data types with ABI Encoding and Decoding

This section details the process of encoding complex data types into Uint8Array typed arrays to fulfill the Ethereum Virtual Machine (EVM) data handling requirements for transactions and smart contract interactions. It will then outline the steps for decoding these byte arrays to align with corresponding structures defined in Solidity.

Consider a scenario where a contract needs to interact with a data structure that encapsulates multiple properties, including nested objects:

{
  "id": 1,
  "metadata": {
    "description": "Decentralized Oracle Network",
    "awesome": true
  }
}

Transferring and storing this kind of structured data requires encoding it into a format (array of 8-bit unsigned integers) that smart contracts can accept and process.

Encoding in JavaScript

Because Chainlink Functions supports important external modules, you can import a web3 library such as ethers.js and perform encoding. To encode complex data structures, you can use the defaultAbiCoder.encode function from the ethers.js library. The function takes two arguments:

  • An array of Solidity data types.
  • The corresponding data in JavaScript format.

and returns the encoded data as a hexadecimal string.

Here's how you can encode the aforementioned complex data:

const { ethers } = await import("npm:ethers@6.10.0") // Import ethers.js v6.10.0

const abiCoder = ethers.AbiCoder.defaultAbiCoder()

// Define the data structure
const complexData = {
  id: 1,
  metadata: {
    description: "Decentralized Oracle Network",
    awesome: true,
  },
}

// Define the Solidity types for encoding
const types = ["tuple(uint256 id, tuple(string description, bool awesome) metadata)"]

// Encoding the data
const encodedData = abiCoder.encode(types, [complexData])

After encoding the data, it's necessary to format it as a Uint8Array array for smart contract interactions and blockchain transactions. In Solidity, the data type for byte arrays data is bytes. However, when working in a JavaScript environment, such as when using the ethers.js library, the equivalent data structure is a Uint8Array.

The ethers.js library provides the getBytes function to convert encoded hexadecimal strings into a Uint8Array:

return ethers.getBytes(encodedData) // Return the encoded data converted into a Uint8Array

Decoding in Solidity

The encoded data can be decoded using the abi.decode function. To decode the data, you'll need to handle the decoding in your fulfillRequest function:

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

contract DataDecoder {
  // Example of a structure to hold the complex data
  struct Metadata {
    string description;
    bool awesome;
  }

  struct ComplexData {
    uint256 id;
    Metadata metadata;
  }

  // ... other contract functions (including the send request function)

  // Fulfill function (callback function)
  function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override {
    // Decode the response
    ComplexData memory metadata = abi.decode(response, (ComplexData));
    // ... rest of the function
  }
}

What's next

Get the latest Chainlink content straight to your inbox.