Using User-hosted Secrets in Requests
This tutorial shows you how to send a request to a Decentralized Oracle Network to call the Coinmarketcap API. After OCR completes off-chain computation and aggregation, it returns the BTC/USD
asset price to your smart contract. Because the API requires you to provide an API key, this guide will also show you how to encrypt, sign your API key, and share the encrypted secret off-chain with a Decentralized Oracle Network (DON).
The encrypted secrets are never stored on-chain. This tutorial uses the threshold decryption feature. This tutorial shows you how to share encrypted secrets off-chain with a Decentralized Oracle Network (DON) using a storage platform such as AWS S3, Google Drive, IPFS, or any other service where the DON can fetch secrets via HTTP. Read the Secrets Management page to learn more.
Read the Using User-hosted (gist) Secrets in Requests tutorial before you follow the steps in this example. This tutorial uses the same example but with a slightly different process:
- Instead of relying on storing the encrypted secrets on gist, you will host your encrypted secrets on AWS S3.
- Include the encrypted secrets in an
offchain-secrets.json
file. - Host the secrets file off-chain (AWS S3).
- Encrypt the S3 HTTPs URL .
- Include the encrypted URL in your Chainlink Functions request.
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. 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.
-
You can locate the scripts used in this tutorial in the examples/7-use-secrets-url directory.
-
Get a free API key from CoinMarketCap.
-
Run
npx env-enc set
to add an encryptedCOINMARKETCAP_API_KEY
to your.env.enc
file.npx env-enc set
-
Prepare the store for your encrypted secrets file.
- Create a AWS free tier account.
- Follow these steps to create a AWS S3 bucket. Choose a name for your bucket, set ACLs enabled, and turn off Block all public access.
Tutorial
This tutorial is configured to get the BTC/USD
price with a request that requires API keys. For a detailed explanation of the code example, read the Examine the code section.
Build Off-chain Secrets
Before you make a request, prepare the secrets file and host it off-chain:
-
Encrypt the secrets and store them in the
offchain-secrets.json
file using thegen-offchain-secrets
script of the7-use-secrets-url
folder.node examples/7-use-secrets-url/gen-offchain-secrets.js
Example:
$ node examples/7-use-secrets-url/gen-offchain-secrets.js secp256k1 unavailable, reverting to browser version Encrypted secrets object written to /functions-examples/offchain-secrets.json
-
Follow these steps to upload the file
offchain-secrets.json
to your AWS S3 bucket. -
To make the file publically accessible without authentication:
- Find the file in the bucket list, and click on it to open the object overview.
- Click on the Permissions tab to display the Access control list (ACL).
- Click on Edit.
- Set Everyone (public access) Objects read, then confirm. This action makes the object readable by anyone on the internet.
- Note the object URL.
- To verify that the URL is publicly readable without authentication, open a new browser tab and copy/paste the object URL in the browser location bar. After you hit Enter , the browser will display the content of your encrypted secrets file.
-
Note the URL. You will need it in the following section. For example:
https://clfunctions.s3.eu-north-1.amazonaws.com/offchain-secrets.json
.
Send a Request
To run the example:
-
Open the file
request.js
, which is located in the7-use-secrets-url
folder. -
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
-
Replace the
secretsUrls
with your AWS S3 URL:const secretsUrls = ["https://clfunctions.s3.eu-north-1.amazonaws.com/offchain-secrets.json"] // REPLACE WITH YOUR VALUES after running gen-offchain-secrets.js and uploading offchain-secrets.json to a public URL
-
Make a request:
node examples/7-use-secrets-url/request.js
The script runs your function in a sandbox environment before making an on-chain transaction:
$ node examples/7-use-secrets-url/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: 'Price: 25834.51 USD\n', responseBytesHexstring: '0x0000000000000000000000000000000000000000000000000000000000276b9b' } ✅ Decoded response to uint256: 2583451n 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... Encrypt the URLs.. ✅ Functions request sent! Transaction hash 0x1af53189bc379740632dd5ff9fc295d46d8d41e901b34b787ccfdb7d0692547a - Request id is 0xd62d6a69f11c40c088492fce7b1ff6cabf94683f8dc1313e048d9729812d27b6. Waiting for a response... See your request in the explorer https://mumbai.polygonscan.com/tx/0x1af53189bc379740632dd5ff9fc295d46d8d41e901b34b787ccfdb7d0692547a ✅ Request 0xd62d6a69f11c40c088492fce7b1ff6cabf94683f8dc1313e048d9729812d27b6 fulfilled with code: 0. Cost is 0.000038203918015388 LINK. Complete response: { requestId: '0xd62d6a69f11c40c088492fce7b1ff6cabf94683f8dc1313e048d9729812d27b6', subscriptionId: 3, totalCostInJuels: 38203918015388n, responseBytesHexstring: '0x0000000000000000000000000000000000000000000000000000000000276b9b', errorString: '', returnDataBytesHexstring: '0x', fulfillmentCode: 0 } ✅ Decoded response to uint256: 2583451n
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.
-
The AWS S3 URL is encrypted before sending it in the request.
-
Your request was successfully sent to Chainlink Functions. The transaction in this example is 0x1af53189bc379740632dd5ff9fc295d46d8d41e901b34b787ccfdb7d0692547a and the request ID is
0xd62d6a69f11c40c088492fce7b1ff6cabf94683f8dc1313e048d9729812d27b6
. -
The DON successfully fulfilled your request. The total cost was:
0.000038203918015388 LINK
. -
The consumer contract received a response in
bytes
with a value of0x0000000000000000000000000000000000000000000000000000000000276b9b
. Decoding it off-chain touint256
gives you a result:2583451
.
-
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
Response
event 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:
-
sendRequest
for 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
FunctionsRequest
library 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
FunctionsClient
sendRequest
function. You can read the API reference for sending a request. Finally, it stores the request id ins_lastRequestId
then return it.s_lastRequestId = _sendRequest( req.encodeCBOR(), subscriptionId, gasLimit, jobId ); return s_lastRequestId;
Note:
_sendRequest
accepts requests encoded inbytes
. Therefore, you must encode it using encodeCBOR.
-
-
sendRequestCBOR
for 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 theFunctionsClient
sendRequest
function. Note: This function is helpful if you want to encode a request off-chain before sending it, saving gas when submitting the request.
-
-
fulfillRequest
to be invoked during the callback. This function is defined inFunctionsClient
asvirtual
(readfulfillRequest
API 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_lastResponse
ands_lastError
before emitting theResponse
event.s_lastResponse = response; s_lastError = err; emit Response(requestId, s_lastResponse, s_lastError);
JavaScript example
source.js
The JavaScript code is similar to the Using Secrets in Requests tutorial.
gen-offchain-secrets.js
This explanation focuses on the gen-offchain-secrets.js script and shows how to use the Chainlink Functions NPM package in your own JavaScript/TypeScript project to encrypts your secrets. After encryption, the script saves the encrypted secrets on a local file, offchain-secrets.json
. You can then upload the file to your storage of choice (AWS S3 in this example).
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.
The primary function that the script executes is generateOffchainSecretsFile
. This function can be broken into three 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.secrets
: The secrets object.- Initialization of ethers
signer
andprovider
objects. The Chainlink NPM package uses the signer to sign the encrypted secrets with your private key.
-
Encrypt the secrets:
- Initialize a
SecretsManager
instance from the Chainlink Functions NPM package. - Call the
encryptSecrets
function from the created instance to encrypt the secrets.
- Initialize a
-
Use the
fs
library to store the encrypted secrets on a local file,offchain-secrets.json
.
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 six 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.explorerUrl
: Block explorer url of Polygon Mumbai.source
: The source code must be a string object. That's why we usefs.readFileSync
to readsource.js
and then calltoString()
to get the content as astring
object.args
: During the execution of your function, These arguments are passed to the source code. Theargs
value is["1", "USD"]
, which fetches the BTC/USD price.secrets
: The secrets object. Note: Because we are sharing the URL of the encrypted secrets with the DON, thesecrets
object is only used during simulation.secretsUrls
: The URL of the encrypted secrets object.gasLimit
: Maximum gas that Chainlink Functions can use when transmitting the response to your contract.- Initialization of ethers
signer
andprovider
objects. 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
simulateScript
from the Chainlink Functions NPM package. - Read the
response
of the simulation. If successful, use the Functions NPM packagedecodeResult
function andReturnType
enum to decode the response to the expected returned type (ReturnType.uint256
in this example).
- Use
-
Estimating the costs:
- Initialize a
SubscriptionManager
from the Functions NPM package, then call theestimateFunctionsRequestCost
function. - The response is returned in Juels (1 LINK = 10**18 Juels). Use the
ethers.utils.formatEther
utility function to convert the output to LINK.
- Initialize a
-
Encrypt the secrets, then create a gist containing the encrypted secrets object. This is done in two steps:
- Initialize a
SecretsManager
instance from the Functions NPM package, then call theencryptSecrets
function. - Call the
encryptedSecretsUrls
function of theSecretsManager
instance. This function encrypts the secrets URL. Note: The encrypted URL will be sent to the DON when making a request.
- Initialize a
-
Making a Chainlink Functions request:
- Initialize your functions consumer contract using the contract address, abi, and ethers signer.
- Make a static call to the
sendRequest
function of your consumer contract to return the request ID that Chainlink Functions will generate. - Call the
sendRequest
function of your consumer contract.
-
Waiting for the response:
- Initialize a
ResponseListener
from the Functions NPM package and then call thelistenForResponse
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 andReturnType
enum to decode the response to the expected returned type (ReturnType.uint256
in this example).
- Initialize a