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:
-
Compile the contract.
-
Open MetaMask and select the Ethereum Sepolia network.
-
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.
-
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. -
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.
-
After you confirm the transaction, the contract address appears in the Deployed Contracts list. Copy the contract address.
-
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:
-
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.
-
Open the file
request.js, located in the12-abi-encodingfolder. -
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 -
Make a request:
node examples/12-abi-encoding/request.jsThe 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 response: { 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 is0xdf22fa28c81a3ea78f356334b6d28d969e953009fae8ece4fe544f2eb466419b. -
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 thelatestRoundData,decimals, anddescriptionof 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
undefined
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.decodeto decode theresponseto retrieve theanswer,updatedAt,decimals, anddescription.( 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:
-
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 usefs.readFileSyncto readsource.jsand then calltoString()to get the content as astringobject.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
signerandproviderobjects. The signer is used to make transactions on the blockchain, and the provider reads data from the blockchain.
-
Simulating your request in a local sandbox environment:
- Use
simulateScriptfrom the Chainlink Functions NPM package. - Read the
responseof the simulation. If successful, use the Functions NPM packagedecodeResultfunction andReturnTypeenum to decode the response to the expected returned type (ReturnType.bytesin this example).
- Use
-
Estimating the costs:
- Initialize a
SubscriptionManagerfrom the Functions NPM package, then call theestimateFunctionsRequestCost. - The response is returned in Juels (1 LINK = 10**18 Juels). Use the
ethers.utils.formatEtherutility function to convert the output to LINK.
- Initialize a
-
Making a Chainlink Functions request:
- Initialize your functions consumer contract using the contract address, abi, and ethers signer.
- Call the
sendRequestfunction of your consumer contract.
-
Waiting for the response:
- Initialize a
ResponseListenerfrom the Functions NPM package and then call thelistenForResponseFromTransactionfunction to wait for a response. By default, this function waits for five minutes. - Upon reception of the response, use the Functions NPM package
decodeResultfunction andReturnTypeenum to decode the response to the expected returned type (ReturnType.bytesin this example).
- Initialize a
-
Read the decoded response:
- Call the
s_answer,s_updatedAt,s_decimals, ands_descriptionfunctions of your consumer contract to fetch the decoded values. - Log the decoded values to the console.
- Call the
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
}
}