From f61dd7069f216ebadef4f524d3854515ace1444c Mon Sep 17 00:00:00 2001 From: aelmanaa Date: Mon, 5 Feb 2024 20:33:26 +0100 Subject: [PATCH] Show abi.encoding/decoding example --- .../FunctionsConsumerDecoder.sol | 156 ++++++++++++ src/config/sidebar.ts | 4 + .../tutorials/abi-decoding.mdx | 225 ++++++++++++++++++ .../tutorials/importing-packages.mdx | 5 - .../chainlink-functions/tutorials/index.mdx | 1 + 5 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 public/samples/ChainlinkFunctions/FunctionsConsumerDecoder.sol create mode 100644 src/content/chainlink-functions/tutorials/abi-decoding.mdx diff --git a/public/samples/ChainlinkFunctions/FunctionsConsumerDecoder.sol b/public/samples/ChainlinkFunctions/FunctionsConsumerDecoder.sol new file mode 100644 index 00000000000..9e99ce2984a --- /dev/null +++ b/public/samples/ChainlinkFunctions/FunctionsConsumerDecoder.sol @@ -0,0 +1,156 @@ +// 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 FunctionsConsumerDecoder is FunctionsClient, ConfirmedOwner { + using FunctionsRequest for FunctionsRequest.Request; + + bytes32 public s_lastRequestId; + bytes public s_lastResponse; + bytes public s_lastError; + + uint256 public s_answer; + uint256 public s_updatedAt; + uint8 public s_decimals; + string public s_description; + + error UnexpectedRequestID(bytes32 requestId); + + event Response(bytes32 indexed requestId, bytes response, bytes err); + + event DecodedResponse( + bytes32 indexed requestId, + uint256 answer, + uint256 updatedAt, + uint8 decimals, + string description + ); + + 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 donID + ) 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, + donID + ); + 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 donID 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 donID + ) external onlyOwner returns (bytes32 requestId) { + s_lastRequestId = _sendRequest( + request, + subscriptionId, + gasLimit, + donID + ); + return s_lastRequestId; + } + + /** + * @dev Internal function to process the outcome of a data request. It stores the latest response or error and updates the contract state accordingly. This function is designed to handle only one of `response` or `err` at a time, not both. It decodes the response if present and emits events to log both raw and decoded data. + * + * @param requestId The unique identifier of the request, originally returned by `sendRequest`. Used to match responses with requests. + * @param response The raw aggregated response data from the external source. This data is ABI-encoded and is expected to contain specific information (e.g., answer, updatedAt) if no error occurred. The function attempts to decode this data if `response` is not empty. + * @param err The raw aggregated error information, indicating an issue either from the user's code or within the execution of the user Chainlink Function. + * + * Emits a `DecodedResponse` event if the `response` is successfully decoded, providing detailed information about the data received. + * Emits a `Response` event for every call to log the raw response and error data. + * + * Requirements: + * - The `requestId` must match the last stored request ID to ensure the response corresponds to the latest request sent. + * - Only one of `response` or `err` should contain data for a given call; the other should be empty. + */ + function fulfillRequest( + bytes32 requestId, + bytes memory response, + bytes memory err + ) internal override { + if (s_lastRequestId != requestId) { + revert UnexpectedRequestID(requestId); + } + + s_lastError = err; + s_lastResponse = response; + + if (response.length > 0) { + ( + uint256 answer, + uint256 updatedAt, + uint8 decimals, + string memory description + ) = abi.decode(response, (uint256, uint256, uint8, string)); + + s_answer = answer; + s_updatedAt = updatedAt; + s_decimals = decimals; + s_description = description; + + emit DecodedResponse( + requestId, + answer, + updatedAt, + decimals, + description + ); + } + + emit Response(requestId, response, err); + } +} diff --git a/src/config/sidebar.ts b/src/config/sidebar.ts index 8bd01c2a327..5c396633bee 100644 --- a/src/config/sidebar.ts +++ b/src/config/sidebar.ts @@ -546,6 +546,10 @@ export const SIDEBAR: Partial> = { title: "Using Imports with Functions", url: "chainlink-functions/tutorials/importing-packages", }, + { + title: "Returning multiple responses and decoding them in your smart contract", + url: "chainlink-functions/tutorials/abi-decoding", + }, ], }, { diff --git a/src/content/chainlink-functions/tutorials/abi-decoding.mdx b/src/content/chainlink-functions/tutorials/abi-decoding.mdx new file mode 100644 index 00000000000..7cf35659c22 --- /dev/null +++ b/src/content/chainlink-functions/tutorials/abi-decoding.mdx @@ -0,0 +1,225 @@ +--- +section: chainlinkFunctions +date: Last Modified +title: "Returning multiple responses and decoding them in your smart contract" +metadata: + linkToWallet: true +whatsnext: + { + "Try out the Chainlink Functions Tutorials": "/chainlink-functions/tutorials", + "Read the Architecture to understand how Chainlink Functions operates": "/chainlink-functions/resources/architecture", + } +--- + +import { Aside, CopyText, CodeSample } from "@components" +import { Tabs } from "@components/Tabs" +import ChainlinkFunctions from "@features/chainlink-functions/common/ChainlinkFunctions.astro" + +In the [Using Imports with Functions](/chainlink-functions/tutorials/importing-packages) tutorial, we explored the fundamentals of module imports. This tutorial will teach you how to use the Ethers library [encode](https://docs.ethers.org/v6/api/abi/abi-coder/#AbiCoder-encode) function to perform ABI encoding of several responses. Then, you will use the [ABI specifications](https://docs.soliditylang.org/en/v0.8.24/abi-spec.html) in Solidity to decode the responses in your smart contract. + + + +## Prerequisites + +This tutorial assumes you have completed the [Using Imports with Functions](/chainlink-functions/tutorials/importing-packages) tutorial. Also, +check your subscription details (including the balance in LINK) in the [Chainlink Functions Subscription +Manager](/chainlink-functions/resources/subscriptions). If your subscription runs out of LINK, follow the [Fund a +Subscription](/chainlink-functions/resources/subscriptions#fund-a-subscription) guide. + +In this tutorial, you will use a different Chainlink Functions consumer contract, which shows how to use ABI decoding to decode the response received from Chainlink Functions: + +1. [Open the FunctionsConsumerDecoder.sol contract](https://remix.ethereum.org/#url=https://docs.chain.link/samples/ChainlinkFunctions/FunctionsConsumerDecoder.sol) in Remix. + + + +1. Compile the contract. +1. Open MetaMask and select the _Polygon Mumbai_ network. +1. In Remix under the **Deploy & Run Transactions** tab, select _Injected Provider - MetaMask_ in the **Environment** list. Remix will use the MetaMask wallet to communicate with _Polygon Mumbai_. +1. Under the **Deploy** section, fill in the router address for your specific blockchain. You can find both of these addresses on the [Supported Networks](/chainlink-functions/supported-networks) page. For _Polygon Mumbai_, the router address is . +1. Click the **Deploy** button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to _Polygon Mumbai_. +1. After you confirm the transaction, the contract address appears in the **Deployed Contracts** list. Copy the contract address. +1. Add your consumer contract address to your subscription on _Polygon Mumbai_. + +## Tutorial + +This tutorial demonstrates using the [ethers](https://www.npmjs.com/package/ethers) library to interact with smart contract functions through a JSON RPC provider. It involves calling the [`latestRoundData`](/data-feeds/api-reference#latestrounddata), [`decimals`](/data-feeds/api-reference#decimals), and [`description`](/data-feeds/api-reference#description) functions of a price feed contract based on the from the `AggregatorV3Interface`. +After retrieving the necessary data, the guide shows how to use ABI encoding to encode these responses into a single hexadecimal string and then convert this string to a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array). This step ensures compliance with the [Chainlink functions' API requirements](/chainlink-functions/api-reference/javascript-source#data-encoding-functions), which specify that the source code must return a Uint8Array representing the bytes for on-chain use. + +You can locate the scripts used in this tutorial in the [_examples/12-abi-encoding_ directory](https://github.com/smartcontractkit/smart-contract-examples/tree/main/functions-examples/examples/12-abi-encoding). + +To run the example: + +1. Open the file `request.js`, which is located in the `12-abi-encoding` folder. +1. Replace the consumer contract address and the subscription ID with your own values. + + ```javascript + const consumerAddress = "0x5fC6e53646CC53f0C3575fd2c71b5056c4823f5c" // REPLACE this with your Functions consumer address + const subscriptionId = 139 // REPLACE this with your subscription ID + ``` + +1. Make a request: + + ```shell + node examples/12-abi-encoding/request.js + ``` + + The script runs your function in a sandbox environment before making an onchain transaction: + + ```text + $ node examples/12-abi-encoding/request.js + secp256k1 unavailable, reverting to browser version + Start simulation... + Simulation result { + capturedTerminalOutput: 'Fetched BTC / USD price: dataFeedResponse.answer\n' + + 'Updated at: 1707155795\n' + + 'Decimals: 8\n' + + 'Description: BTC / USD\n', + responseBytesHexstring: '0x000000000000000000000000000000000000000000000000000003e1b870b6320000000000000000000000000000000000000000000000000000000065c12153000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000' + } + ✅ Decoded response to bytes: 0x000000000000000000000000000000000000000000000000000003e1b870b6320000000000000000000000000000000000000000000000000000000065c12153000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000 + + Estimate request costs... + Fulfillment cost estimated to 0.20180840394238 LINK + + Make request... + + ✅ Functions request sent! Transaction hash 0x660bc9bd4c85645209295c957d0764823b94a1d351a5ead380113e5352e01fb5. Waiting for a response... + See your request in the explorer https://mumbai.polygonscan.com/tx/0x660bc9bd4c85645209295c957d0764823b94a1d351a5ead380113e5352e01fb5 + + ✅ Request 0xa790aaaca3fdcdd0ca0b0e68bf0c699d84da8d59f8b6dd95aa3d4046a3801c27 successfully fulfilled. Cost is 0.200304341399547731 LINK.Complete reponse: { + requestId: '0xa790aaaca3fdcdd0ca0b0e68bf0c699d84da8d59f8b6dd95aa3d4046a3801c27', + subscriptionId: 139, + totalCostInJuels: 200304341399547731n, + responseBytesHexstring: '0x000000000000000000000000000000000000000000000000000003e1b870b6320000000000000000000000000000000000000000000000000000000065c12153000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000', + errorString: '', + returnDataBytesHexstring: '0x', + fulfillmentCode: 0 + } + + ✅ Raw response: 0x000000000000000000000000000000000000000000000000000003e1b870b6320000000000000000000000000000000000000000000000000000000065c12153000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000 + + ✅ Fetched BTC / USD price: 4267996919346 (updatedAt: 1707155795) (decimals: 8) (description: BTC / USD) + ``` + + 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. + - Your request was successfully sent to Chainlink Functions. The transaction in this example is `0x660bc9bd4c85645209295c957d0764823b94a1d351a5ead380113e5352e01fb5` and the request ID is `0xa790aaaca3fdcdd0ca0b0e68bf0c699d84da8d59f8b6dd95aa3d4046a3801c27`. + + - The DON successfully fulfilled your request. The total cost was: `0.20180840394238 LINK`. + - The consumer contract received a response in hexadecimal string with a value of `0x000000000000000000000000000000000000000000000000000003e1b870b6320000000000000000000000000000000000000000000000000000000065c12153000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009425443202f205553440000000000000000000000000000000000000000000000`. This value is the ABI encoded response of the `latestRoundData`, `decimals`, and `description` of the _BTC / USD_ price feed. This value is then decoded and stored in the consumer contract. + - The script calls the consumer contract to fetch the decoded values then log them to the console. The output is `Fetched BTC / USD price: 4267996919346 (updatedAt: 1707155795) (decimals: 8) (description: BTC / USD)`. + +## Examine the code + +### FunctionsConsumerDecoder.sol + + + +This Solidity contract is very similar to the [FunctionsConsumer.sol](https://remix.ethereum.org/#url=https://docs.chain.link/samples/ChainlinkFunctions/FunctionsConsumer.sol) contract used in the [Using Imports with Functions](/chainlink-functions/tutorials/importing-packages) tutorial. The main difference is the processing of the response in the `fulfillRequest`function: + +- It uses Solidity `abi.decode` to decode the `response` to retrieve the `answer`, `updatedAt`, `decimals`, and `description`. + + ```solidity + ( + uint256 answer, + uint256 updatedAt, + uint8 decimals, + string memory description + ) = abi.decode(response, (uint256, uint256, uint8, string)); + ``` + +- Then stored the decoded values in the contract state. + + ```solidity + s_answer = answer; + s_updatedAt = updatedAt; + s_decimals = decimals; + s_description = description; + ``` + +### JavaScript example + +#### source.js + +The Decentralized Oracle Network will run the [JavaScript code](https://github.com/smartcontractkit/smart-contract-examples/blob/main/functions-examples/examples/12-abi-encoding/source.js). The code is self-explanatory and has comments to help you understand all the steps. + + + +The example `source.js` file is similar to the one used in the [Using Imports with Functions](/chainlink-functions/tutorials/importing-packages#sourcejs) tutorial. It uses a JSON RPC call to the [`latestRoundData`](/data-feeds/api-reference#latestrounddata), [`decimals`](/data-feeds/api-reference#decimals), and [`description`](/data-feeds/api-reference#description) functions of a [Chainlink Data Feed](/data-feeds). It then uses the `ethers` library to encode the response of these functions into a single hexadecimal string. + +```javascript +const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["uint256", "uint256", "uint8", "string"], + [dataFeedResponse.answer, dataFeedResponse.updatedAt, decimals, description] +) +``` + +Finally, it uses the `ethers` library [`getBytes`](https://docs.ethers.org/v6/api/utils/#getBytes) to convert the hexadecimal string to a `Uint8Array`: + +```javascript +return ethers.getBytes(encoded) +``` + +#### request.js + +This explanation focuses on the [request.js](https://github.com/smartcontractkit/smart-contract-examples/blob/main/functions-examples/examples/12-abi-encoding/request.js) script and shows how to use the [Chainlink Functions NPM package](https://github.com/smartcontractkit/functions-toolkit) 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](https://nodejs.org/docs/latest/api/path.html) and [fs](https://nodejs.org/api/fs.html) : Used to read the [source file](https://github.com/smartcontractkit/smart-contract-examples/blob/main/functions-examples/examples/12-abi-encoding/source.js). +- [ethers](https://docs.ethers.org/v5/): 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](https://github.com/smartcontractkit/functions-toolkit/blob/main/README.md). +- `@chainlink/env-enc`: A tool for loading and storing encrypted environment variables. Read the [official documentation](https://www.npmjs.com/package/@chainlink/env-enc) to learn more. +- `../abi/functionsDecoder.json`: The abi of the contract your script will interact with. **Note**: The script was tested with this [FunctionsConsumerDecoder contract](https://remix.ethereum.org/#url=https://docs.chain.link/samples/ChainlinkFunctions/FunctionsConsumerDecoder.sol). + +The script has two hardcoded values that you have to change using your own Functions consumer contract and subscription ID: + +```javascript +const consumerAddress = "0x5fC6e53646CC53f0C3575fd2c71b5056c4823f5c" // REPLACE this with your Functions consumer address +const subscriptionId = 139 // REPLACE this with your subscription ID +``` + +The primary function that the script executes is `makeRequestMumbai`. This function can be broken into five main parts: + +1. 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 use `fs.readFileSync` to read `source.js` and then call `toString()` to get the content as a `string` object. + - `args`: During the execution of your function, These arguments are passed to the source code. + - `gasLimit`: Maximum gas that Chainlink Functions can use when transmitting the response to your contract. + - Initialization of ethers `signer` and `provider` objects. The signer is used to make transactions on the blockchain, and the provider reads data from the blockchain. + +1. 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 package `decodeResult` function and `ReturnType` enum to decode the response to the expected returned type (`ReturnType.bytes` in this example). + +1. Estimating the costs: + + - Initialize a `SubscriptionManager` from the Functions NPM package, then call the `estimateFunctionsRequestCost`. + - The response is returned in Juels (1 LINK = 10\*\*18 Juels). Use the `ethers.utils.formatEther` utility function to convert the output to LINK. + +1. Making a Chainlink Functions request: + + - Initialize your functions consumer contract using the contract address, abi, and ethers signer. + - Call the `sendRequest` function of your consumer contract. + +1. Waiting for the response: + + - Initialize a `ResponseListener` from the Functions NPM package and then call the `listenForResponseFromTransaction` 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 and `ReturnType` enum to decode the response to the expected returned type (`ReturnType.bytes` in this example). + +1. Read the decoded response: + + - Call the `s_answer`, `s_updatedAt`, `s_decimals`, and `s_description` functions of your consumer contract to fetch the decoded values. + - Log the decoded values to the console. diff --git a/src/content/chainlink-functions/tutorials/importing-packages.mdx b/src/content/chainlink-functions/tutorials/importing-packages.mdx index e0beee82bb0..8cd7accacf0 100644 --- a/src/content/chainlink-functions/tutorials/importing-packages.mdx +++ b/src/content/chainlink-functions/tutorials/importing-packages.mdx @@ -4,11 +4,6 @@ date: Last Modified title: "Using Imports with Functions" metadata: linkToWallet: true -whatsnext: - { - "Try out the Chainlink Functions Tutorials": "/chainlink-functions/tutorials", - "Read the Architecture to understand how Chainlink Functions operates": "/chainlink-functions/resources/architecture", - } --- import { Aside, CopyText } from "@components" diff --git a/src/content/chainlink-functions/tutorials/index.mdx b/src/content/chainlink-functions/tutorials/index.mdx index 0b5e635115c..56155cdb6b3 100644 --- a/src/content/chainlink-functions/tutorials/index.mdx +++ b/src/content/chainlink-functions/tutorials/index.mdx @@ -27,3 +27,4 @@ import { Aside } from "@components" - [Automate your Functions (Time-based Automation)](/chainlink-functions/tutorials/automate-functions) - [Automate your Functions (Custom Logic Automation)](/chainlink-functions/tutorials/automate-functions-custom-logic) - [Using Imports with Functions](/chainlink-functions/tutorials/importing-packages) +- [Use ABI encoding and decoding](/chainlink-functions/tutorials/abi-decoding)