Encode request data off-chain
This tutorial shows you how make multiple API calls from your smart contract to a Decentralized Oracle Network. After OCR completes off-chain computation and aggregation, the DON returns the asset price to your smart contract. This example returns the BTC/USD price.
This guide assumes that you know how to build HTTP requests and how to use secrets. Read the API query parameters and API use secrets guides before you follow the example in this document. To build a decentralized asset price, send a request to the DON to fetch the price from many different API providers. Then, calculate the median price. The API providers in this example are:
Read the Call Multiple Data Sources tutorial before you follow the steps in this example. This tutorial uses the same example but with a slightly different process:
- Instead of sending the request data (source code, encrypted secrets reference, and arguments) in the request, you will first encode it off-chain and then send the encoded request. Encoding the request off-chain (e.g., from your front end or a server) rather than on-chain (from your smart contract) helps save gas.
Before you begin
-
Complete the setup steps in the Getting Started guide: The Getting Started Guide shows you how to set up your environment with the necessary tools for these tutorials. You can re-use the same consumer contract for each of these tutorials.
-
Make sure your subscription has enough LINK to pay for your requests. Also, you must maintain a minimum balance to upload encrypted secrets to the DON (Read the minimum balance for uploading encrypted secrets section to learn more). You can check your subscription details (including the balance in LINK) in the Chainlink Functions frontend. If your subscription runs out of LINK, follow the Fund a Subscription guide. This guide recommends maintaining at least 2 LINK within your subscription.
-
You can locate the scripts used in this tutorial in the examples/9-send-cbor directory.
-
Get a free API key from CoinMarketCap and note your API key.
-
Run
npx env-enc setto add an encryptedCOINMARKETCAP_API_KEYto your.env.encfile.npx env-enc set
Tutorial
This tutorial is configured to get the median BTC/USD price from multiple data sources. For a detailed explanation of the code example, read the Examine the code section.
To run the example:
-
Open the file
request.js, which is located in the9-send-cborfolder. -
Replace the consumer contract address and the subscription ID with your own values.
const consumerAddress = "0x8dFf78B7EE3128D00E90611FBeD20A71397064D9" // REPLACE this with your Functions consumer address const subscriptionId = 3 // REPLACE this with your subscription ID -
Make a request:
node examples/9-send-cbor/request.jsThe script runs your function in a sandbox environment before making an on-chain transaction:
$ node examples/9-send-cbor/request.js secp256k1 unavailable, reverting to browser version Start simulation... Performing simulation with the following versions: deno 1.36.3 (release, aarch64-apple-darwin) v8 11.6.189.12 typescript 5.1.6 Simulation result { capturedTerminalOutput: 'Median Bitcoin price: 25713.36\n', responseBytesHexstring: '0x0000000000000000000000000000000000000000000000000000000000273c48' } ✅ Decoded response to uint256: 2571336n Estimate request costs... Duplicate definition of Transfer (Transfer(address,address,uint256,bytes), Transfer(address,address,uint256)) Fulfillment cost estimated to 0.000000000000215 LINK Make request... Upload encrypted secret to gateways https://01.functions-gateway.testnet.chain.link/user. StorageSlotId 0. Expiration in minutes: 15 ✅ Secrets uploaded properly to gateways https://01.functions-gateway.testnet.chain.link/user! Gateways response: { version: 1693899379, success: true } ✅ Functions request sent! Transaction hash 0x310c93ba1c9515a8e1dc4308e198c36f5f915410fc67c13f80ffb29a43dcbe60 - Request id is 0x74b5b88db49dad155d1cd9b0da47cb140caf1f824cf09a88bc46132c21d41d20. Waiting for a response... See your request in the explorer https://mumbai.polygonscan.com/tx/0x310c93ba1c9515a8e1dc4308e198c36f5f915410fc67c13f80ffb29a43dcbe60 ✅ Request 0x74b5b88db49dad155d1cd9b0da47cb140caf1f824cf09a88bc46132c21d41d20 fulfilled with code: 0. Cost is 0.000039224977086446 LINK. Complete response: { requestId: '0x74b5b88db49dad155d1cd9b0da47cb140caf1f824cf09a88bc46132c21d41d20', subscriptionId: 3, totalCostInJuels: 39224977086446n, responseBytesHexstring: '0x0000000000000000000000000000000000000000000000000000000000273c48', errorString: '', returnDataBytesHexstring: '0x', fulfillmentCode: 0 } ✅ Decoded response to uint256: 2571336nThe 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.
-
The encrypted secrets were uploaded to the secrets endpoint
https://01.functions-gateway.testnet.chain.link/user. -
Your request was successfully sent to Chainlink Functions. The transaction in this example is 0x310c93ba1c9515a8e1dc4308e198c36f5f915410fc67c13f80ffb29a43dcbe60 and the request ID is
0x74b5b88db49dad155d1cd9b0da47cb140caf1f824cf09a88bc46132c21d41d20. -
The DON successfully fulfilled your request. The total cost was:
0.000039224977086446 LINK. -
The consumer contract received a response in
byteswith a value of0x0000000000000000000000000000000000000000000000000000000000273c48. Decoding it off-chain touint256gives you a result:2571336. The median BTC price is 25713.36 USD.
-
Examine the code
FunctionsConsumerExample.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/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/dev/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 FunctionsConsumerExample is FunctionsClient, ConfirmedOwner {
using FunctionsRequest for FunctionsRequest.Request;
bytes32 public s_lastRequestId;
bytes public s_lastResponse;
bytes public s_lastError;
error UnexpectedRequestID(bytes32 requestId);
event Response(bytes32 indexed requestId, bytes response, bytes err);
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 jobId
) 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,
jobId
);
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 jobId 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 jobId
) external onlyOwner returns (bytes32 requestId) {
s_lastRequestId = _sendRequest(
request,
subscriptionId,
gasLimit,
jobId
);
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);
}
}
-
To write a Chainlink Functions consumer contract, your contract must import FunctionsClient.sol and FunctionsRequest.sol. You can read the API references: FunctionsClient and FunctionsRequest.
These contracts are available in an NPM package, so you can import them from within your project.
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol"; import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol"; -
Use the FunctionsRequest.sol library to get all the functions needed for building a Chainlink Functions request.
using FunctionsRequest for FunctionsRequest.Request; -
The latest request id, latest received response, and latest received error (if any) are defined as state variables:
bytes32 public s_lastRequestId; bytes public s_lastResponse; bytes public s_lastError; -
We define the
Responseevent that your smart contract will emit during the callbackevent Response(bytes32 indexed requestId, bytes response, bytes err); -
Pass the router address for your network when you deploy the contract:
constructor(address router) FunctionsClient(router) -
The three remaining functions are:
-
sendRequestfor sending a request. It receives the JavaScript source code, encrypted secretsUrls (in case the encrypted secrets are hosted by the user), DON hosted secrets slot id and version (in case the encrypted secrets are hosted by the DON), list of arguments to pass to the source code, subscription id, and callback gas limit as parameters. Then:-
It uses the
FunctionsRequestlibrary to initialize the request and add any passed encrypted secrets reference or arguments. You can read the API Reference for Initializing a request, adding user hosted secrets, adding DON hosted secrets, adding arguments, and adding bytes arguments.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); -
It sends the request to the router by calling the
FunctionsClientsendRequestfunction. You can read the API reference for sending a request. Finally, it stores the request id ins_lastRequestIdthen return it.s_lastRequestId = _sendRequest( req.encodeCBOR(), subscriptionId, gasLimit, jobId ); return s_lastRequestId;Note:
_sendRequestaccepts requests encoded inbytes. Therefore, you must encode it using encodeCBOR.
-
-
sendRequestCBORfor sending a request already encoded inbytes. It receives the request object encoded inbytes, subscription id, and callback gas limit as parameters. Then, it sends the request to the router by calling theFunctionsClientsendRequestfunction. Note: This function is helpful if you want to encode a request off-chain before sending it, saving gas when submitting the request.
-
-
fulfillRequestto be invoked during the callback. This function is defined inFunctionsClientasvirtual(readfulfillRequestAPI reference). So, your smart contract must override the function to implement the callback. The implementation of the callback is straightforward: the contract stores the latest response and error ins_lastResponseands_lastErrorbefore emitting theResponseevent.s_lastResponse = response; s_lastError = err; emit Response(requestId, s_lastResponse, s_lastError);
JavaScript example
source.js
The JavaScript code is similar to the Call Multiple Data Sources tutorial.
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/functionsClient.json: The abi of the contract your script will interact with. Note: The script was tested with this FunctionsConsumerExample contract.
The script has two hardcoded values that you have to change using your own Functions consumer contract and subscription ID:
const consumerAddress = "0x8dFf78B7EE3128D00E90611FBeD20A71397064D9" // REPLACE this with your Functions consumer address
const subscriptionId = 3 // REPLACE this with your subscription ID
The primary function that the script executes is makeRequestMumbai. This function can be broken into seven main parts:
-
Definition of necessary identifiers:
routerAddress: Chainlink Functions router address on Polygon Mumbai.donId: Identifier of the DON that will fulfill your requests on Polygon Mumbai.gatewayUrls: The secrets endpoint URL to which you will upload the encrypted secrets.explorerUrl: Block explorer url of Polygon Mumbai.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. Theargsvalue is["1", "bitcoin", "btc-bitcoin"]. These arguments are BTC IDs at CoinMarketCap, CoinGecko, and Coinpaprika. You can adapt args to fetch other asset prices.secrets: The secrets object that will be encrypted.slotIdNumber: Slot ID at the DON where to upload the encrypted secrets.expirationTimeMinutes: Expiration time in minutes of the encrypted secrets.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.uint256in this example).
- Use
-
Estimating the costs:
- Initialize a
SubscriptionManagerfrom the Functions NPM package, then call theestimateFunctionsRequestCostfunction. - 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
-
Encrypt the secrets, upload the encrypted secrets to the DON, and then encode the reference to the DON-hosted encrypted secrets. This is done in three steps:
- Initialize a
SecretsManagerinstance from the Functions NPM package, then call theencryptSecretsfunction. - Call the
uploadEncryptedSecretsToDONfunction of theSecretsManagerinstance. This function returns an object containing asuccessboolean as long asversion, the secret version on the DON storage. - Call the
buildDONHostedEncryptedSecretsReferencefunction of theSecretsManagerinstance and use the slot ID and version to encode the DON-hosted encrypted secrets reference.
- Initialize a
-
Encode the request data off-chain using the
buildRequestCBORfunction fomr the Functions NPM package. -
Making a Chainlink Functions request:
- Initialize your functions consumer contract using the contract address, abi, and ethers signer.
- Make a static call to the
sendRequestCBORfunction of your consumer contract to return the request ID that Chainlink Functions will generate. - Call the
sendRequestCBORfunction of your consumer contract. Note: The encoded data that was generated bybuildRequestCBORis passed in the request.
-
Waiting for the response:
- Initialize a
ResponseListenerfrom the Functions NPM package and then call thelistenForResponsefunction 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.uint256in this example).
- Initialize a