NEW

Join us at SmartCon 2023—where Web3 gets real. Get your ticket.

Using 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 it off-chain with a Decentralized Oracle Network (DON).

The Chainlink Functions starter kit creates gists containing encrypted secrets on your behalf. Then, it shares the gist URL with the DON. This method comes with a security benefit:

  • The encrypted secrets are never stored on-chain. The secrets are encrypted with the DON's public key so that only an oracle node in the DON can decrypt them using the DON's private key. After the DON fulfills a request, the starter kit deletes the gist. This revokes the credentials after the DON has fulfilled the request.

Before you begin

  1. 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.

  2. Make sure your subscription has enough LINK to pay for your requests. Read Get Subscription details to learn how to check your subscription balance. If your subscription runs out of LINK, follow the Fund a Subscription guide.

  3. Check out the tutorials branch of the Chainlink Functions Starter Kit. You can locate this tutorial in the /tutorials/5-use-secrets directory.

    git checkout tutorials
  4. Get a free API key from CoinMarketCap and note your API key.

  5. The starter kit store encrypted secrets as gists to share them off-chain with the Decentralized Oracle Network. To allow the starter kit to write gists on your behalf, create a github fine-grained personal access token.

    1. Visit Github tokens settings page.
    2. Click on Generate new token.
    3. Provide a name to your token and define the expiration date.
    4. Under Account permissions, enable Read and write for Gists. Note: Do not enable additional settings.
    5. Click on Generate token and copy your fine-grained personal access token.
  6. Run npx env-enc set to add an encrypted GITHUB_API_TOKEN and COINMARKETCAP_API_KEY to your .env.enc file.

    npx env-enc set

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 Explanation section.

  • Open config.js. The args value is ["1", "USD"], which fetches the current BTC/USD price. The value of "1" is the BTC CoinMarketCap ID. You can change args to fetch other asset prices. See the CoinMarketCap API documentation to learn about the available values. Read the request config section for more details about the request config file.
  • Open source.js to analyze the JavaScript source code. Read the source code section for more details about the source code file.

Simulation

The Chainlink Functions Hardhat Starter Kit includes a simulator to test Functions code on your local machine. The functions-simulate command executes your code in a local runtime environment and simulate an end-to-end fulfillment. This helps you fix issues before you submit functions to a Decentralized Oracle Network.

Run the functions-simulate task to run the source code locally and make sure config.js and source.js are correctly written:

npx hardhat functions-simulate --configpath REPLACE_CONFIG_PATH

Example:

$ npx hardhat functions-simulate --configpath tutorials/5-use-secrets/config.js
secp256k1 unavailable, reverting to browser version

__Compiling Contracts__
Nothing to compile
Duplicate definition of Transfer (Transfer(address,address,uint256,bytes), Transfer(address,address,uint256))

Executing JavaScript request source code locally...

__Console log messages from sandboxed code__
Price: 28272.77 USD

__Output from sandboxed source code__
Output represented as a hex string: 0x00000000000000000000000000000000000000000000000000000000002b240d
Decoded as a uint256: 2827277

__Simulated On-Chain Response__
Response returned to client contract represented as a hex string: 0x00000000000000000000000000000000000000000000000000000000002b240d
Decoded as a uint256: 2827277

Gas used by sendRequest: 355760
Gas used by client callback function: 75029

Reading the output of the example above, you can note that the BTC/USD price is 28272.77 USD. Because Solidity does not support decimals, move the decimal point so that the value looks like an integer 2827277 before returning the bytes encoded value 0x00000000000000000000000000000000000000000000000000000000002b240d in the callback. Read the source code section for a more details.

Request

Send a request to the Decentralized Oracle Network to fetch the asset price. Run the functions-request task with the subid (subscription ID) and contract parameters. This task passes the functions JavaScript source code, arguments, and secrets to the executeRequest function in your deployed FunctionsConsumer contract. Read the functionsConsumer section for more details about the consumer contract.

npx hardhat functions-request --subid REPLACE_SUBSCRIPTION_ID --contract REPLACE_CONSUMER_CONTRACT_ADDRESS --network REPLACE_NETWORK --configpath REPLACE_CONFIG_PATH

Example:

$ npx hardhat functions-request --subid 443 --contract 0x4B4BA2Fd6b93aDF8d6b6002E10540E58394388Ea --network polygonMumbai --configpath tutorials/5-use-secrets/config.js
secp256k1 unavailable, reverting to browser version
Estimating cost if the current gas price remains the same...

The transaction to initiate this request will charge the wallet (0x9d087fC03ae39b088326b67fA3C788236645b717):
0.00049769700497697 MATIC, which (using mainnet value) is $0.0005508427833382278

If the request's callback uses all 100,000 gas, this request will charge the subscription:
0.200148068209448658 LINK

Continue? Enter (y) Yes / (n) No
y
Simulating Functions request locally...

__Console log messages from sandboxed code__
Price: 28264.75 USD

__Output from sandboxed source code__
Output represented as a hex string: 0x00000000000000000000000000000000000000000000000000000000002b20eb
Decoded as a uint256: 2826475

Successfully created encrypted secrets Gist: https://gist.github.com/aelmanaa/677f0b039b5036d9ab9f09caa7d3ecd5

⣾ Request 0x958c7b88f2edb0d827e9ca8b5d5e63ee373527a409099983e18a69c16ebc4a24 has been initiated. Waiting for fulfillment from the Decentralized Oracle Network...
ℹ Transaction confirmed, see https://mumbai.polygonscan.com/tx/0x1cd9edfb6ee3dc6c45038d93a2b146217fd104eb92fe82a1c9203d4f83ced5d6 for more details.

✔ Request 0x958c7b88f2edb0d827e9ca8b5d5e63ee373527a409099983e18a69c16ebc4a24 fulfilled! Data has been written on-chain.

Response returned to client contract represented as a hex string: 0x00000000000000000000000000000000000000000000000000000000002b20eb
Decoded as a uint256: 2826475

Actual amount billed to subscription #443:
┌──────────────────────┬─────────────────────────────┐
│         Type         │           Amount            │
├──────────────────────┼─────────────────────────────┤
│  Transmission cost:  │  0.000066649973244399 LINK  │
│      Base fee:       │          0.2 LINK           │
│                      │                             │
│     Total cost:      │  0.200066649973244399 LINK  │
└──────────────────────┴─────────────────────────────┘


Off-chain secrets Gist https://gist.github.com/aelmanaa/677f0b039b5036d9ab9f09caa7d3ecd5 deleted successfully

The example output tells you the following information:

  • The executeRequest function was successfully called in the FunctionsConsumer contract. The transaction in this example is 0x1cd9edfb6ee3dc6c45038d93a2b146217fd104eb92fe82a1c9203d4f83ced5d6.
  • The request ID is 0x958c7b88f2edb0d827e9ca8b5d5e63ee373527a409099983e18a69c16ebc4a24.
  • The DON successfully fulfilled your request. The total cost was: 0.200066649973244399 LINK.
  • The consumer contract received a response in bytes with a value of 0x00000000000000000000000000000000000000000000000000000000002b20eb. Decoding it off-chain to uint256 give you a result: 2826475.
  • The starter kit created a gist https://gist.github.com/aelmanaa/677f0b039b5036d9ab9f09caa7d3ecd5 containing the encrypted secrets. This gist is shared with the DON when making the request.
  • After request fulfillment, the starter kit deleted the gist https://gist.github.com/aelmanaa/677f0b039b5036d9ab9f09caa7d3ecd5.

At any time, you can run the functions-read task with the contract parameter to read the latest received response.

npx hardhat functions-read  --contract REPLACE_CONSUMER_CONTRACT_ADDRESS --network REPLACE_NETWORK --configpath REPLACE_CONFIG_PATH

Example:

$ npx hardhat functions-read  --contract 0x4B4BA2Fd6b93aDF8d6b6002E10540E58394388Ea  --network polygonMumbai --configpath tutorials/5-use-secrets/config.js
secp256k1 unavailable, reverting to browser version
Reading data from Functions client contract 0x4B4BA2Fd6b93aDF8d6b6002E10540E58394388Ea on network mumbai

On-chain response represented as a hex string: 0x00000000000000000000000000000000000000000000000000000000002b20eb
Decoded as a uint256: 2826475

Explanation

FunctionsConsumer.sol

  • To write a Chainlink Functions consumer contract, your contract must import FunctionsClient.sol. You can read the API reference: FunctionsClient.

    This contract is not available in an NPM package, so you must download and import it from within your project.

    import {Functions, FunctionsClient} from "./dev/functions/FunctionsClient.sol";
  • Use the Functions.sol library to get all the functions needed for building a Chainlink Functions request. You can read the API reference: Functions.

    using Functions for Functions.Request;
    
  • The latest request id, latest received response, and latest received error (if any) are defined as state variables. Note latestResponse and latestError are encoded as dynamically sized byte array bytes, so you will still need to decode them to read the response or error:

    bytes32 public latestRequestId;
    bytes public latestResponse;
    bytes public latestError;
  • We define the OCRResponse event that your smart contract will emit during the callback

    event OCRResponse(bytes32 indexed requestId, bytes result, bytes err);
  • Pass the oracle address for your network when you deploy the contract:

    constructor(address oracle) FunctionsClient(oracle)
  • At any time, you can change the oracle address by calling the updateOracleAddress function.

  • The two remaining functions are:

    • executeRequest for sending a request. It receives the JavaScript source code, encrypted secrets, list of arguments to pass to the source code, subscription id, and callback gas limit as parameters. Then:

      • It uses the Functionslibrary to initialize the request and add any passed encrypted secrets or arguments. You can read the API Reference for Initializing a request, adding secrets, and adding arguments.

        Functions.Request memory req;
        req.initializeRequest(Functions.Location.Inline, Functions.CodeLanguage.JavaScript, source);
        if (secrets.length > 0) {
          req.addRemoteSecrets(secrets);
        }
        if (args.length > 0) req.addArgs(args);
      • It sends the request to the oracle by calling the FunctionsClient sendRequest function. You can read the API reference for sending a request. Finally, it stores the request id in latestRequestId.

        bytes32 assignedReqID = sendRequest(req, subscriptionId, gasLimit);
        latestRequestId = assignedReqID;
    • fulfillRequest to be invoked during the callback. This function is defined in FunctionsClient as virtual (read fulfillRequest 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 in latestResponse and latestError before emitting the OCRResponse event.

      latestResponse = response;
      latestError = err;
      emit OCRResponse(requestId, response, err);

config.js

Read the Request Configuration section for a detailed description of each setting. In this example, the settings are the following:

  • codeLocation: Location.Inline: The JavaScript code is provided within the request.
  • codeLanguage: CodeLanguage.JavaScript: The source code is developed in the JavaScript language.
  • source: fs.readFileSync(path.resolve(__dirname, "source.js")).toString(): The source code must be a script object. This example uses fs.readFileSync to read source.js and calls toString() to get the content as a string object.
  • secrets: { apiKey: process.env.COINMARKETCAP_API_KEY }: JavaScript object which contains secret values. These secrets are encrypted using the DON public key. The process.env.COINMARKETCAP_API_KEY setting means COINMARKETCAP_API_KEY is fetched from the environment variables. Note: secrets is limited to a key-value map that can only contain strings. It cannot include any other types or nested parameters.
  • walletPrivateKey: process.env["PRIVATE_KEY"]: This is your EVM account private key. It is used to generate a signature for the encrypted secrets such that an unauthorized third party cannot reuse them.
  • args: ["1", "USD"]: These arguments are passed to the source code. In this example, request the BTC/USD price. The value of "1" is the BTC ID for CoinMarketCap. You can adapt args to fetch other asset prices. Read the CoinMarketCap API documentation to see what options are available.
  • expectedReturnType: ReturnType.uint256: The response received by the DON is encoded in bytes. Because the asset price is a uint256, define ReturnType.uint256 to inform users how to decode the response received by the DON.

source.js

To check the expected API response, run the curl command in your terminal:

curl -X 'GET' \
  'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?id=1&convert=USD' \
  -H 'accept: application/json' \
  -H 'X-CMC_PRO_API_KEY: REPLACE_WITH_YOUR_API_KEY'

The response should be similar to the following example:

{
  ...,
  "data": {
    "1": {
      "id": 1,
      "name": "Bitcoin",
      "symbol": "BTC",
      "slug": "bitcoin",
      ...,
      "quote": {
        "USD": {
          "price": 23036.068560170934,
          "volume_24h": 33185308895.694683,
          "volume_change_24h": 24.8581,
          "percent_change_1h": 0.07027098,
          "percent_change_24h": 1.79073805,
          "percent_change_7d": 10.29859656,
          "percent_change_30d": 38.10735851,
          "percent_change_60d": 39.26624921,
          "percent_change_90d": 11.59835416,
          "market_cap": 443982488416.99316,
          "market_cap_dominance": 42.385,
          "fully_diluted_market_cap": 483757439763.59,
          "tvl": null,
          "last_updated": "2023-01-26T18:27:00.000Z"
        }
      }
    }
  }
}

The price is located at data,1,quote,USD,price.

Read the JavaScript code section for a detailed explanation of how to write a compatible JavaScript source code. This JavaScript source code uses Functions.makeHttpRequest to make HTTP requests. If you read the Functions.makeHttpRequest documentation, you can see the following required parameters:

  • url: https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest

  • headers: This is an HTTP headers object set to { "X-CMC_PRO_API_KEY": secrets.apiKey }. The apiKey is defined in the request config file.

  • params: The query parameters object:

    {
      convert: currencyCode,
      id: coinMarketCapCoinId
    }

Note currencyCode and coinMarketCapCoinId are fetched from args (see request config).

The code is self-explanatory and has comments to help you understand all the steps. The main steps are:

  • Construct the HTTP object coinMarketCapRequest using Functions.makeHttpRequest.
  • Make the HTTP call.
  • Read the asset price from the response.
  • Return the result as a buffer using the helper function: Functions.encodeUint256. Note: Because solidity doesn't support decimals, we multiply the result by 100 and round the result to the nearest integer. Note: Read this article if you are new to Javascript Buffers and want to understand why they are important.

Stay updated on the latest Chainlink news