NEW

CCIP is now available on testnet for all developers. Get started today.

Return Custom Data Types

In this tutorial, you send a request to a Decentralized Oracle Network to call the Cryptocompare GET /data/pricemultifull API. After OCR completes off-chain computation and aggregation, it returns several responses to your smart contract. The response includes an asset price, daily volume, and market name to your smart contract. The example uses query parameters to specify the ETH/USD asset pair, but you can configure HTTP query parameters to make requests for different assets.

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/3-custom-response directory.

    git checkout tutorials

Tutorial

This tutorial is configured to get the ETH/USD, daily volume, and market in a single request. For a detailed explanation of the code example, read the Explanation section.

  • Open config.js. Note the args value is ["ETH", "USD"]: We want to fetch the current ETH/USD price and daily volume. You can adapt args to get the list of supported symbols. Read the API docs to learn more about these configuration variables. For a more detailed explanation about the configuration in this example, read the request config explanation section.
  • Open source.js to analyze the JavaScript source code. Read the source code explanation section for a more detailed explanation of how the source file is written.

Simulation

The Chainlink Functions hardhat starter kit includes a simulator to test your Functions code on your local machine. The functions-simulate command will execute your code in a local runtime environment and simulate an end-to-end fulfillment. Simulation can help you to fix any issues before you submit your requests to the Decentralized Oracle Network.

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

npx hardhat functions-simulate --configpath REPLACE_CONFIG_PATH

Example:

$ npx hardhat functions-simulate --configpath tutorials/3-custom-response/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__
HTTP GET Request to https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD
ETH price is: 1810.29 USD. 24h Volume is 113255.87 USD. Market: Coinbase

__Output from sandboxed source code__
Output represented as a hex string: 0x7b227072696365223a22313831302e3239222c22766f6c756d65223a223131333235352e3837222c226c6173744d61726b6574223a22436f696e62617365227d
Decoded as a string: {"price":"1810.29","volume":"113255.87","lastMarket":"Coinbase"}

__Simulated On-Chain Response__
Response returned to client contract represented as a hex string: 0x7b227072696365223a22313831302e3239222c22766f6c756d65223a223131333235352e3837222c226c6173744d61726b6574223a22436f696e62617365227d
Decoded as a string: {"price":"1810.29","volume":"113255.87","lastMarket":"Coinbase"}

Gas used by sendRequest: 376604
Gas used by client callback function: 98391

Reading the output of the example above, you can see that the ETH/USD price is 1810.29 USD, volume is 113255.87, and the market is Coinbase. Because the final result is a JSON object, the example converts it to a string and returns the bytes encoded value 0x7b227072696365223a22313831302e3239222c22766f6c756d65223a223131333235352e3837222c226c6173744d61726b6574223a22436f696e62617365227d in the callback. Read the source code explanation for a more detailed explanation.

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 JavaScript source code, arguments, and secrets when you call the executeRequest function in your deployed FunctionsConsumer contract. Read the functionsConsumer section for a more detailed explanation.

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/3-custom-response/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.00050942400509424 MATIC, which (using mainnet value) is $0.0005628062918760629

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

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

__Console log messages from sandboxed code__
HTTP GET Request to https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD
ETH price is: 1811.08 USD. 24h Volume is 113909.01 USD. Market: Bitfinex

__Output from sandboxed source code__
Output represented as a hex string: 0x7b227072696365223a22313831312e3038222c22766f6c756d65223a223131333930392e3031222c226c6173744d61726b6574223a2242697466696e6578227d
Decoded as a string: {"price":"1811.08","volume":"113909.01","lastMarket":"Bitfinex"}


⣾ Request 0x51d3aa3be08c71c49a37d586de07eb85e87a403e35a73521c5c914c430e2de2d has been initiated. Waiting for fulfillment from the Decentralized Oracle Network...

ℹ Transaction confirmed, see https://mumbai.polygonscan.com/tx/0xd0140c40e65fdf51ea1b4396258915438693b61cfacb6d28a368bdd4f94b0443 for more details.

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

Response returned to client contract represented as a hex string: 0x7b227072696365223a22313831312e3139222c22766f6c756d65223a223131333931302e3933222c226c6173744d61726b6574223a2242697466696e6578227d
Decoded as a string: {"price":"1811.19","volume":"113910.93","lastMarket":"Bitfinex"}

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

The output of the example above gives you the following information:

  • The executeRequest function was successfully called in the FunctionsConsumer contract. The transaction in this example is 0xd0140c40e65fdf51ea1b4396258915438693b61cfacb6d28a368bdd4f94b0443.
  • The request ID is 0x51d3aa3be08c71c49a37d586de07eb85e87a403e35a73521c5c914c430e2de2d.
  • The DON successfully fulfilled your request. The total cost was: 0.200071836060128269 LINK.
  • The consumer contract received a response in bytes with a value of 0x7b227072696365223a22313831312e3038222c22766f6c756d65223a223131333930392e3031222c226c6173744d61726b6574223a2242697466696e6578227d. Decoding it off-chain to string gives you the following result: {"price":"1811.19","volume":"113910.93","lastMarket":"Bitfinex"}.

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/3-custom-response/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: 0x7b227072696365223a22313831312e3139222c22766f6c756d65223a223131333931302e3933222c226c6173744d61726b6574223a2242697466696e6578227d
Decoded as a string: {"price":"1811.19","volume":"113910.93","lastMarket":"Bitfinex"}

Decoding 0x7b227072696365223a22313831312e3139222c22766f6c756d65223a223131333931302e3933222c226c6173744d61726b6574223a2242697466696e6578227d from bytes to string gives you {"price":"1811.19","volume":"113910.93","lastMarket":"Bitfinex"}. Off-chain, you can use JSON.parse() to convert the JSON string to a JSON object.

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. This example uses the following settings:

  • 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.
  • args: ["ETH", "USD"]: These arguments are passed to the source code. This example requests the ETH/USD price and daily volume.
  • expectedReturnType: ReturnType.string: The response received by the DON is encoded in bytes. Because the price, daily volume and market are put in a JSON string, define ReturnType.string to inform users how to decode the response received by the DON. Read source code explanation for more information.

source.js

You can check the expected API response. Directly paste the following URL in your browser https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD or run a curl command in your terminal:

curl -X 'GET' \
  'https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD' \
  -H 'accept: application/json'

The response should be similar to the following example:

{
  "RAW": {
    "ETH": {
      "USD": {
        "TYPE": "5",
        "MARKET": "CCCAGG",
        "FROMSYMBOL": "ETH",
        "TOSYMBOL": "USD",
        "FLAGS": "2049",
        "PRICE": 2867.04,
        "LASTUPDATE": 1650896942,
        "MEDIAN": 2866.2,
        "LASTVOLUME": 0.16533939,
        "LASTVOLUMETO": 474.375243849,
        "LASTTRADEID": "1072154517",
        "VOLUMEDAY": 195241.78281014622,
        "VOLUMEDAYTO": 556240560.4621655,
        "VOLUME24HOUR": 236248.94641103,
        ...
}

The price is located at RAW,ETH,USD,PRICE, the volume is at RAW,ETH,USD,VOLUME24HOUR, and the market is at RAW,ETH,USD,LASTMARKET.

Read the JavaScript code section for a detailed explanation of how to write compatible JavaScript source code. This JavaScript source code uses Functions.makeHttpRequest to make HTTP requests. To request the ETH/USD price, the source code calls this URL: https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD If you read the documentation for Functions.makeHttpRequest, you see you must provide the following parameters:

  • url: https://min-api.cryptocompare.com/data/pricemultifull

  • params: The query parameters object:

    {
      fsyms: fromSymbol,
      tsyms: toSymbol
    }

Note fromSymbol and toSymbol 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 cryptoCompareRequest using Functions.makeHttpRequest.

  • Run the HTTP request.

  • Read the asset price, daily volume, and market from the response.

  • Construct a JSON object.

    const result = {
      price: price.toFixed(2),
      volume: volume.toFixed(2),
      lastMarket,
    }
  • Convert the JSON object to a JSON string using JSON.stringify(result). This step is mandatory before encoding string to bytes.

  • Return the result as a buffer using the Functions.string helper function. 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