Chainlink Functions - Tutorial 2 Explained

Devyush RaturiDevyush Raturi
10 min read

Call an API with HTTP Query Parameters

Chainlink has released the Chainlink Functions and its tutorials. Today, I will be explaining every code file used in the 2nd tutorial.

You can refer to tutorial 2 by clicking HERE.

This tutorial shows you how to send a request to a Decentralized Oracle Network to call the Cryptocompare GET /data/pricemultifull API. And I will be explaining the different code files used in the tutorial line by line.

Functions-request-config.js

const fs = require("fs")

// Loads environment variables from .env.enc file (if it exists)
require("@chainlink/env-enc").config()

const Location = {
  Inline: 0,
  Remote: 1,
}

const CodeLanguage = {
  JavaScript: 0,
}

const ReturnType = {
  uint: "uint256",
  uint256: "uint256",
  int: "int256",
  int256: "int256",
  string: "string",
  bytes: "Buffer",
  Buffer: "Buffer",
}

// Configure the request by setting the fields below
const requestConfig = {
  // location of source code (only Inline is curently supported)
  codeLocation: Location.Inline,
  // code language (only JavaScript is currently supported)
  codeLanguage: CodeLanguage.JavaScript,
  // string containing the source code to be executed
  source: fs.readFileSync("./Functions-request-source.js").toString(),
  // args can be accessed within the source code with `args[index]` (ie: args[0])
  args: ["ETH", "USD"],
  // expected type of the returned value
  expectedReturnType: ReturnType.uint256,
}

module.exports = requestConfig

The code above was taken from HERE. It is provided in the tutorial. It exports a configuration object requestConfig that specifies the parameters for the request to execute the Javascript Source Code used for getting the ETH/USD price.

Here is a breakdown of the code:

  1. Importing Required Modules:

     const fs = require("fs");
    

    This line imports the fs (file system) module, which allows interaction with the file system.

  2. Loading Environment Variables:

     require("@chainlink/env-enc").config();
    

    This line loads environment variables from an encrypted .env.enc file using the @chainlink/env-enc library, if the file exists.

  3. Defining Enumerations:

     const Location = {
       Inline: 0,
       Remote: 1,
     };
    
     const CodeLanguage = {
       JavaScript: 0,
     };
    
     const ReturnType = {
       uint: "uint256",
       uint256: "uint256",
       int: "int256",
       int256: "int256",
       string: "string",
       bytes: "Buffer",
       Buffer: "Buffer",
     };
    
    • These constants define enumerations for code location, code language, and return types.

    • Location can be Inline (code is provided directly) or Remote (code is fetched from a remote source).

    • CodeLanguage currently only supports JavaScript.

    • ReturnType lists different types of return values that the executed code can produce.

  4. Configuring the Request:

     const requestConfig = {
       // location of source code (only Inline is curently supported)
       codeLocation: Location.Inline,
       // code language (only JavaScript is currently supported)
       codeLanguage: CodeLanguage.JavaScript,
       // string containing the source code to be executed
       source: fs.readFileSync("./Functions-request-source.js").toString(),
       // args can be accessed within the source code with `args[index]` (ie: args[0])
       args: ["ETH", "USD"],
       // expected type of the returned value
       expectedReturnType: ReturnType.uint256,
     };
    

    requestConfig is an object that holds the configuration for the request:

    • codeLocation specifies that the source code is provided inline.

    • codeLanguage specifies that the code language is JavaScript.

    • source reads the content of the Functions-request-source.js file and converts it to a string.

    • args is an array of arguments that will be passed to the source code (in this case, "ETH" and "USD").

    • expectedReturnType specifies that the expected return type of the executed code is uint256.

  5. Exporting the Configuration:

     module.exports = requestConfig;
    

    This line exports the requestConfig object so that it can be imported and used in other parts of the tutorial.

Finally, the requestConfig object is exported so that it can be used by the Functions-request-source.js in the tutorial.

Note that the source code to be executed is read from the file Functions-request-source.js. The args property is an array of two arguments that can be accessed within the source code. The expectedReturnType property specifies the expected type of the returned value.

Functions-request-source.js

// This example shows how to make a call to an open API (no authentication required)
// to retrieve asset price from a symbol(e.g., ETH) to another symbol (e.g., USD)

// CryptoCompare API https://min-api.cryptocompare.com/documentation?key=Price&cat=multipleSymbolsFullPriceEndpoint

// Refer to https://github.com/smartcontractkit/functions-hardhat-starter-kit#javascript-code

// Arguments can be provided when a request is initated on-chain and used in the request source code as shown below
const fromSymbol = args[0]
const toSymbol = args[1]

// make HTTP request
const url = `https://min-api.cryptocompare.com/data/pricemultifull`
console.log(`HTTP GET Request to ${url}?fsyms=${fromSymbol}&tsyms=${toSymbol}`)

// construct the HTTP Request object. See: https://github.com/smartcontractkit/functions-hardhat-starter-kit#javascript-code
// params used for URL query parameters
// Example of query: https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD
const cryptoCompareRequest = Functions.makeHttpRequest({
  url: url,
  params: {
    fsyms: fromSymbol,
    tsyms: toSymbol,
  },
})

// Execute the API request (Promise)
const cryptoCompareResponse = await cryptoCompareRequest
if (cryptoCompareResponse.error) {
  console.error(cryptoCompareResponse.error)
  throw Error("Request failed")
}

const data = cryptoCompareResponse["data"]
if (data.Response === "Error") {
  console.error(data.Message)
  throw Error(`Functional error. Read message: ${data.Message}`)
}

// extract the price
const price = data["RAW"][fromSymbol][toSymbol]["PRICE"]
console.log(`${fromSymbol} price is: ${price.toFixed(2)} ${toSymbol}`)

// Solidity doesn't support decimals so multiply by 100 and round to the nearest integer
// Use Functions.encodeUint256 to encode an unsigned integer to a Buffer
return Functions.encodeUint256(Math.round(price * 100))

The code above was taken from HERE. It is provided in the tutorial. This code provides a complete example of how to make an HTTP request to retrieve cryptocurrency prices and prepare the data for use in a smart contract by encoding it in a format that Solidity can handle.

Here's a detailed explanation of the code:

  1. Comments and Documentation:

    • The initial comments explain the purpose of the script: to retrieve the price of an asset from one symbol to another using an open API from CryptoCompare.

    • It also references the CryptoCompare API documentation and the GitHub repository for additional context and examples.

  2. Extracting Arguments:

     const fromSymbol = args[0];
     const toSymbol = args[1];
    
    • args is an array of arguments provided when the request is initiated on-chain. The first argument (args[0]) is the source symbol (e.g., "ETH") and the second argument (args[1]) is the target symbol (e.g., "USD").
  3. Constructing the URL:

     const url = `https://min-api.cryptocompare.com/data/pricemultifull`;
     console.log(`HTTP GET Request to ${url}?fsyms=${fromSymbol}&tsyms=${toSymbol}`);
    
    • This constructs the base URL for the CryptoCompare API and logs the full URL with query parameters for debugging purposes.
  4. Making the HTTP Request:

     const cryptoCompareRequest = Functions.makeHttpRequest({
       url: url,
       params: {
         fsyms: fromSymbol,
         tsyms: toSymbol,
       },
     });
    
    • Functions.makeHttpRequest is used to create an HTTP request object. The params object specifies the query parameters (fsyms and tsyms), which are the symbols for the from and to currencies, respectively.
  5. Executing the API Request:

     const cryptoCompareResponse = await cryptoCompareRequest;
     if (cryptoCompareResponse.error) {
       console.error(cryptoCompareResponse.error);
       throw Error("Request failed");
     }
    
    • The request is executed, and the response is awaited. If there's an error in the response, it's logged to the console, and an error is thrown to stop further execution.
  6. Handling the Response:

     const data = cryptoCompareResponse["data"];
     if (data.Response === "Error") {
       console.error(data.Message);
       throw Error(`Functional error. Read message: ${data.Message}`);
     }
    
    • The response data is extracted. If the API response indicates an error (e.g., invalid symbols), the error message is logged, and an error is thrown.
  7. Extracting the Price:

     const price = data["RAW"][fromSymbol][toSymbol]["PRICE"];
     console.log(`${fromSymbol} price is: ${price.toFixed(2)} ${toSymbol}`);
    
    • The price of the cryptocurrency is extracted from the response data and logged to the console. The price is formatted to two decimal places.
  8. Encoding the Price for Solidity:

     return Functions.encodeUint256(Math.round(price * 100));
    
    • Since Solidity does not support decimal numbers, the price is multiplied by 100 and rounded to the nearest integer. The Functions.encodeUint256 method is then used to encode this integer into a Buffer for use in smart contracts.

FunctionsConsumer.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import {Functions, FunctionsClient} from "./dev/functions/FunctionsClient.sol";
// import "@chainlink/contracts/src/v0.8/dev/functions/FunctionsClient.sol"; // Once published
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";

/**
 * @title Functions Consumer contract
 * @notice This contract is a demonstration of using Functions.
 * @notice NOT FOR PRODUCTION USE
 */
contract FunctionsConsumer is FunctionsClient, ConfirmedOwner {
  using Functions for Functions.Request;

  bytes32 public latestRequestId;
  bytes public latestResponse;
  bytes public latestError;

  event OCRResponse(bytes32 indexed requestId, bytes result, bytes err);

  /**
   * @notice Executes once when a contract is created to initialize state variables
   *
   * @param oracle - The FunctionsOracle contract
   */
  // https://github.com/protofire/solhint/issues/242
  // solhint-disable-next-line no-empty-blocks
  constructor(address oracle) FunctionsClient(oracle) ConfirmedOwner(msg.sender) {}

  /**
   * @notice Send a simple request
   *
   * @param source JavaScript source code
   * @param secrets Encrypted secrets payload
   * @param args List of arguments accessible from within the source code
   * @param subscriptionId Funtions billing subscription ID
   * @param gasLimit Maximum amount of gas used to call the client contract's `handleOracleFulfillment` function
   * @return Functions request ID
   */
  function executeRequest(
    string calldata source,
    bytes calldata secrets,
    string[] calldata args,
    uint64 subscriptionId,
    uint32 gasLimit
  ) public onlyOwner returns (bytes32) {
    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);

    bytes32 assignedReqID = sendRequest(req, subscriptionId, gasLimit);
    latestRequestId = assignedReqID;
    return assignedReqID;
  }

  /**
   * @notice Callback that is invoked once the DON has resolved the request or hit an 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 {
    latestResponse = response;
    latestError = err;
    emit OCRResponse(requestId, response, err);
  }

  /**
   * @notice Allows the Functions oracle address to be updated
   *
   * @param oracle New oracle address
   */
  function updateOracleAddress(address oracle) public onlyOwner {
    setOracle(oracle);
  }

  function addSimulatedRequestId(address oracleAddress, bytes32 requestId) public onlyOwner {
    addExternalRequest(oracleAddress, requestId);
  }
}

NOTE: This contract is labeled as a demonstration and is not intended for production use.

The code above was taken from HERE. It is provided in the tutorial. This Solidity code defines the smart contract named FunctionsConsumer that demonstrates how to interact with Chainlink Functions for making off-chain computation requests. Here’s a detailed explanation of each part of the code:

  1. Pragma Directive:

     // SPDX-License-Identifier: MIT
     pragma solidity ^0.8.7;
    
    • This specifies the version of Solidity to be used for compiling the contract and includes an SPDX license identifier.
  2. Import Statements:

     import {Functions, FunctionsClient} from "./dev/functions/FunctionsClient.sol";
     import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";
    
    • These import necessary libraries and contracts. Functions and FunctionsClient are used for interacting with Chainlink Functions, while ConfirmedOwner is a utility contract that provides ownership control.
  3. Contract Definition:

     contract FunctionsConsumer is FunctionsClient, ConfirmedOwner {
    
    • This contract inherits from FunctionsClient and ConfirmedOwner, enabling it to interact with Chainlink Functions and utilize ownership control features.
  4. State Variables:

     bytes32 public latestRequestId;
     bytes public latestResponse;
     bytes public latestError;
    
     event OCRResponse(bytes32 indexed requestId, bytes result, bytes err);
    
    • latestRequestId, latestResponse, and latestError store the most recent request ID, response, and error, respectively.

    • OCRResponse is an event emitted when a request is fulfilled, logging the request ID, response, and error.

  5. Constructor:

     constructor(address oracle) FunctionsClient(oracle) ConfirmedOwner(msg.sender) {}
    
    • The constructor initializes the contract with the address of the Functions oracle and sets the contract deployer as the owner.
  6. Request Execution Function:

     function executeRequest(
       string calldata source,
       bytes calldata secrets,
       string[] calldata args,
       uint64 subscriptionId,
       uint32 gasLimit
     ) public onlyOwner returns (bytes32) {
       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);
    
       bytes32 assignedReqID = sendRequest(req, subscriptionId, gasLimit);
       latestRequestId = assignedReqID;
       return assignedReqID;
     }
    
    • This function allows the contract owner to send a request to the Functions oracle.

    • source: The JavaScript source code to be executed.

    • secrets: Encrypted secrets used in the source code.

    • args: Arguments accessible from within the source code.

    • subscriptionId: The billing subscription ID for Functions.

    • gasLimit: The maximum amount of gas used to call the handleOracleFulfillment function.

    • It initializes a request, adds secrets and arguments if provided, sends the request, stores the request ID, and returns the request ID.

  7. Fulfillment Callback:

     function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override {
       latestResponse = response;
       latestError = err;
       emit OCRResponse(requestId, response, err);
     }
    
    • This function is called once the request is resolved or an error occurs. It updates the state variables with the response or error and emits the OCRResponse event.
  8. Oracle Address Update Function:

     function updateOracleAddress(address oracle) public onlyOwner {
       setOracle(oracle);
     }
    
    • This function allows the contract owner to update the Functions oracle address.
  9. Simulated Request ID Addition:

     function addSimulatedRequestId(address oracleAddress, bytes32 requestId) public onlyOwner {
       addExternalRequest(oracleAddress, requestId);
     }
    
    • This function allows the contract owner to add a simulated request ID, useful for testing or other purposes.

Summary:

  • FunctionsConsumer contract interacts with Chainlink Functions for off-chain computation.

  • executeRequest sends a request to the Functions oracle.

  • fulfillRequest handles the response or error from the oracle.

  • updateOracleAddress and addSimulatedRequestId provide additional control and testing functionalities.

  • Uses Chainlink's FunctionsClient and ConfirmedOwner contracts for core functionality and ownership control.

0
Subscribe to my newsletter

Read articles from Devyush Raturi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Devyush Raturi
Devyush Raturi