├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── main.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .solhint.json ├── .solhintignore ├── API-request-example.js ├── Functions-request-config.js ├── LICENSE ├── README.md ├── calculation-example.js ├── contracts ├── AutomatedFunctionsConsumer.sol ├── FunctionsConsumer.sol └── test │ ├── LinkToken.sol │ └── MockV3Aggregator.sol ├── env.enc.example ├── hardhat.config.js ├── networks.js ├── package-lock.json ├── package.json ├── scripts ├── listen.js └── startLocalFunctionsTestnet.js ├── tasks ├── Functions-billing │ ├── accept.js │ ├── add.js │ ├── cancel.js │ ├── create.js │ ├── fund.js │ ├── index.js │ ├── info.js │ ├── remove.js │ ├── timeoutRequests.js │ └── transfer.js ├── Functions-consumer │ ├── buildOffchainSecrets.js │ ├── checkUpkeep.js │ ├── deployAutoConsumer.js │ ├── deployConsumer.js │ ├── index.js │ ├── listDonSecrets.js │ ├── performManualUpkeep.js │ ├── readResultAndError.js │ ├── request.js │ ├── setAutoRequest.js │ ├── setDonId.js │ └── uploadSecretsToDon.js ├── balance.js ├── block-number.js ├── index.js ├── simulateScript.js └── utils │ ├── index.js │ ├── logger.js │ ├── network.js │ ├── price.js │ ├── prompt.js │ └── spin.js └── test └── unit └── FunctionsConsumer.spec.js /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Runs basic commands to install dependencies 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: actions/setup-node@v3 7 | with: 8 | node-version: 18 9 | - name: npm install 10 | run: npm install 11 | shell: bash 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 15 | - name: Use Node.js 16 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 17 | with: 18 | node-version: "18" 19 | cache: "npm" 20 | - run: npm install --frozen-lockfile 21 | - run: npm run lint 22 | 23 | unit-tests: 24 | name: Unit Tests 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 28 | - name: Use Node.js 29 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 30 | with: 31 | node-version: "18" 32 | cache: "npm" 33 | - run: npm install --frozen-lockfile 34 | - run: npm run test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # hardhat 5 | artifacts 6 | cache 7 | deployments 8 | node_modules 9 | coverage 10 | coverage.json 11 | typechain 12 | 13 | # don't push the environment vars! 14 | *.env 15 | 16 | # These environment variables are encrypted, but should not be pushed unless a secure password is used 17 | offchain-encrypted-secrets.json 18 | .env.enc 19 | offchain-encrypted-secrets.json 20 | 21 | # Built application files 22 | .DS* 23 | *.apk 24 | *.ap_ 25 | *.aab 26 | 27 | # Files for the ART/Dalvik VM 28 | *.dex 29 | 30 | # Java class files 31 | *.class 32 | 33 | # Generated files 34 | bin/ 35 | gen/ 36 | out/ 37 | # Uncomment the following line in case you need and you don't have the release build type files in your app 38 | # release/ 39 | 40 | # Gradle files 41 | .gradle/ 42 | build/ 43 | 44 | # Local configuration file (sdk path, etc) 45 | local.properties 46 | 47 | # Proguard folder generated by Eclipse 48 | proguard/ 49 | 50 | # Log Files 51 | *.log 52 | 53 | # Android Studio Navigation editor temp files 54 | .navigation/ 55 | 56 | # Android Studio captures folder 57 | captures/ 58 | 59 | # IntelliJ 60 | *.iml 61 | .idea/workspace.xml 62 | .idea/tasks.xml 63 | .idea/gradle.xml 64 | .idea/assetWizardSettings.xml 65 | .idea/dictionaries 66 | .idea/libraries 67 | # Android Studio 3 in .gitignore file. 68 | .idea/caches 69 | .idea/modules.xml 70 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 71 | .idea/navEditor.xml 72 | 73 | # Keystore files 74 | # Uncomment the following lines if you do not want to check your keystore files in. 75 | #*.jks 76 | #*.keystore 77 | 78 | # External native build folder generated in Android Studio 2.2 and later 79 | .externalNativeBuild 80 | 81 | # Google Services (e.g. APIs or Firebase) 82 | # google-services.json 83 | 84 | # Freeline 85 | freeline.py 86 | freeline/ 87 | freeline_project_description.json 88 | 89 | # fastlane 90 | fastlane/report.xml 91 | fastlane/Preview.html 92 | fastlane/screenshots 93 | fastlane/test_output 94 | fastlane/readme.md 95 | 96 | # Version control 97 | vcs.xml 98 | 99 | # lint 100 | lint/intermediates/ 101 | lint/generated/ 102 | lint/outputs/ 103 | lint/tmp/ 104 | # lint/reports/ 105 | 106 | gas-report.txt 107 | 108 | contracts/test/fuzzing/crytic-export 109 | 110 | Functions-request.json 111 | offchain-secrets.json 112 | 113 | allowlist.csv 114 | invalidUsers.csv 115 | updatedAllowlist.csv 116 | 117 | # OpenZeppelin Upgrades local network artifacts 118 | .openzeppelin 119 | 120 | # Chainlink Functions request artifacts 121 | .chainlink_functions -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | hardhat.config.js 2 | scripts 3 | test 4 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["warn", "^0.8.19"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | contracts/test 3 | -------------------------------------------------------------------------------- /API-request-example.js: -------------------------------------------------------------------------------- 1 | // This example shows how to make a decentralized price feed using multiple APIs 2 | 3 | // Arguments can be provided when a request is initated on-chain and used in the request source code as shown below 4 | const coinMarketCapCoinId = args[0] 5 | const coinGeckoCoinId = args[1] 6 | const coinPaprikaCoinId = args[2] 7 | const badApiCoinId = args[3] 8 | 9 | if ( 10 | secrets.apiKey == "" || 11 | secrets.apiKey === "Your coinmarketcap API key (get a free one: https://coinmarketcap.com/api/)" 12 | ) { 13 | throw Error( 14 | "COINMARKETCAP_API_KEY environment variable not set for CoinMarketCap API. Get a free key from https://coinmarketcap.com/api/" 15 | ) 16 | } 17 | 18 | // To make an HTTP request, use the Functions.makeHttpRequest function 19 | // Functions.makeHttpRequest function parameters: 20 | // - url 21 | // - method (optional, defaults to 'GET') 22 | // - headers: headers supplied as an object (optional) 23 | // - params: URL query parameters supplied as an object (optional) 24 | // - data: request body supplied as an object (optional) 25 | // - timeout: maximum request duration in ms (optional, defaults to 10000ms) 26 | // - responseType: expected response type (optional, defaults to 'json') 27 | 28 | // Use multiple APIs & aggregate the results to enhance decentralization 29 | const coinMarketCapRequest = Functions.makeHttpRequest({ 30 | url: `https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?convert=USD&id=${coinMarketCapCoinId}`, 31 | // Get a free API key from https://coinmarketcap.com/api/ 32 | headers: { "X-CMC_PRO_API_KEY": secrets.apiKey }, 33 | }) 34 | const coinGeckoRequest = Functions.makeHttpRequest({ 35 | url: `https://api.coingecko.com/api/v3/simple/price?ids=${coinGeckoCoinId}&vs_currencies=usd`, 36 | }) 37 | const coinPaprikaRequest = Functions.makeHttpRequest({ 38 | url: `https://api.coinpaprika.com/v1/tickers/${coinPaprikaCoinId}`, 39 | }) 40 | // This dummy request simulates a failed API request 41 | const badApiRequest = Functions.makeHttpRequest({ 42 | url: `https://badapi.com/price/symbol/${badApiCoinId}`, 43 | }) 44 | 45 | // First, execute all the API requests are executed concurrently, then wait for the responses 46 | const [coinMarketCapResponse, coinGeckoResponse, coinPaprikaResponse, badApiResponse] = await Promise.all([ 47 | coinMarketCapRequest, 48 | coinGeckoRequest, 49 | coinPaprikaRequest, 50 | badApiRequest, 51 | ]) 52 | 53 | const prices = [] 54 | 55 | if (!coinMarketCapResponse.error) { 56 | prices.push(coinMarketCapResponse.data.data[coinMarketCapCoinId].quote.USD.price) 57 | } else { 58 | console.log("CoinMarketCap Error") 59 | } 60 | if (!coinGeckoResponse.error) { 61 | prices.push(coinGeckoResponse.data[coinGeckoCoinId].usd) 62 | } else { 63 | console.log("CoinGecko Error") 64 | } 65 | if (!coinPaprikaResponse.error) { 66 | prices.push(coinPaprikaResponse.data.quotes.USD.price) 67 | } else { 68 | console.log("CoinPaprika Error") 69 | } 70 | // A single failed API request does not cause the whole request to fail 71 | if (!badApiResponse.error) { 72 | prices.push(httpResponses[3].data.price.usd) 73 | } else { 74 | console.log( 75 | "Bad API request failed. (This message is expected to demonstrate using console.log for debugging locally with the simulator)" 76 | ) 77 | } 78 | 79 | // At least 3 out of 4 prices are needed to aggregate the median price 80 | if (prices.length < 3) { 81 | // If an error is thrown, it will be returned back to the smart contract 82 | throw Error("More than 1 API failed") 83 | } 84 | 85 | const medianPrice = prices.sort((a, b) => a - b)[Math.round(prices.length / 2)] 86 | console.log(`Median Bitcoin price: $${medianPrice.toFixed(2)}`) 87 | 88 | // The source code MUST return a Buffer or the request will return an error message 89 | // Use one of the following functions to convert to a Buffer representing the response bytes that are returned to the consumer smart contract: 90 | // - Functions.encodeUint256 91 | // - Functions.encodeInt256 92 | // - Functions.encodeString 93 | // Or return a custom Buffer for a custom byte encoding 94 | return Functions.encodeUint256(Math.round(medianPrice * 100)) 95 | -------------------------------------------------------------------------------- /Functions-request-config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const { Location, ReturnType, CodeLanguage } = require("@chainlink/functions-toolkit") 3 | 4 | // Configure the request by setting the fields below 5 | const requestConfig = { 6 | // String containing the source code to be executed 7 | source: fs.readFileSync("./calculation-example.js").toString(), 8 | //source: fs.readFileSync("./API-request-example.js").toString(), 9 | // Location of source code (only Inline is currently supported) 10 | codeLocation: Location.Inline, 11 | // Optional. Secrets can be accessed within the source code with `secrets.varName` (ie: secrets.apiKey). The secrets object can only contain string values. 12 | secrets: { apiKey: process.env.COINMARKETCAP_API_KEY ?? "" }, 13 | // Optional if secrets are expected in the sourceLocation of secrets (only Remote or DONHosted is supported) 14 | secretsLocation: Location.DONHosted, 15 | // Args (string only array) can be accessed within the source code with `args[index]` (ie: args[0]). 16 | args: ["1", "bitcoin", "btc-bitcoin", "btc", "1000000", "450"], 17 | // Code language (only JavaScript is currently supported) 18 | codeLanguage: CodeLanguage.JavaScript, 19 | // Expected type of the returned value 20 | expectedReturnType: ReturnType.uint256, 21 | } 22 | 23 | module.exports = requestConfig 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SmartContract Chainlink Limited SEZC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chainlink Functions Starter Kit 2 | 3 | - [Chainlink Functions Starter Kit](#chainlink-functions-starter-kit) 4 | - [Overview](#overview) 5 | - [Motivation](#motivation) 6 | - [Supported Networks](#supported-networks) 7 | - [Mainnets](#mainnets) 8 | - [Testnets](#testnets) 9 | - [For Beginners](#for-beginners) 10 | - [Tutorials \& examples](#tutorials--examples) 11 | - [Quickstart](#quickstart) 12 | - [Requirements](#requirements) 13 | - [Steps on Live (Public) Testnets](#steps-on-live-public-testnets) 14 | - [Steps on local testnet](#steps-on-local-testnet) 15 | - [Environment Variable Management](#environment-variable-management) 16 | - [Using Remote Secrets (e.g. Github Gists)](#using-remote-secrets-eg-github-gists) 17 | - [Environment Variable Management Commands](#environment-variable-management-commands) 18 | - [Functions Command Glossary](#functions-command-glossary) 19 | - [Functions Commands](#functions-commands) 20 | - [Functions Subscription Management Commands](#functions-subscription-management-commands) 21 | - [Request Configuration](#request-configuration) 22 | - [JavaScript Code](#javascript-code) 23 | - [Functions Library](#functions-library) 24 | - [Importing Dependencies](#importing-dependencies) 25 | - [Modifying Contracts](#modifying-contracts) 26 | - [Local Simulations with the `localFunctionsTestnet`](#local-simulations-with-the-localfunctionstestnet) 27 | - [Managing Secrets](#managing-secrets) 28 | - [Automation Integration](#automation-integration) 29 | - [Gas Spikes](#gas-spikes) 30 | - [Troubleshooting](#troubleshooting) 31 | 32 | ## Overview 33 | 34 |

Chainlink Functions allows users to request data from HTTP(s) APIs and perform custom computation using JavaScript. 35 | It works by executing the request on a decentralized oracle network (DON). 36 | When a request is initiated, each node in the DON executes the user-provided JavaScript code simultaneously. Then, nodes use the Chainlink OCR protocol to come to consensus on the results. Finally, the median result is returned to the requesting contract via a callback function. 37 |

Chainlink Functions also enables users to securely share secrets with the DON, allowing users to access APIs that require authentication without exposing their API keys. Secrets are encrypted with threshold public key cryptography, requiring multiple nodes to participate in a decentralized decryption process such that no node can decrypt secrets without consensus from the rest of the DON.

38 | 39 | Nodes are compensated in LINK via a subscription billing model. You can see billing details [here](https://docs.chain.link/chainlink-functions/resources/subscriptions) and pricing for each network [here](https://docs.chain.link/chainlink-functions/supported-networks). 40 | 41 |

Working with Chainlink Functions requires accepting the terms of service before you are able to create a subscription. Please visit chain.link/functions.

42 | 43 | ## Motivation 44 | 45 | This repo provides developers with a "works out of the box" experience as it comes preconfigured with dependencies and popular tooling like [Hardhat](https://hardhat.org). This is not a tutorial for the Hardhat toolchain. It assumes basic familiarity with Hardhat and the command line. We use HardHat CLI scripts to run Chainlink Functions commands and operations. 46 | 47 | In order to set up your own project which uses Chainlink Functions, please refer to the [Functions Toolkit NPM package](https://www.npmjs.com/package/@chainlink/functions-toolkit). 48 | 49 | ## Supported Networks 50 | 51 | > ⚠️⚠️⚠️ 52 | > As at 13 April 2024, Mumbai (anchored to Goerli) stopped producing blocks. Mumbai's deprecation had been announced in favour of a new Amoy testnet, anchored to Sepolia. 53 | 54 | ### Mainnets 55 | 56 | - Ethereum : `ETHEREUM_RPC_URL`, `--network ethereum`, `ETHERSCAN_API_KEY` 57 | - Polygon : `POLYGON_RPC_URL`, `--network polygon`, `POLYGONSCAN_API_KEY` 58 | - Avalanche : `AVALANCHE_RPC_URL`, `--network avalanche`, `SNOWTRACE_API_KEY` 59 | - Arbitrum : `ARBITRUM_RPC_URL`, `--network arbitrum`, `ARBISCAN_API_KEY` 60 | - Base : `BASE_RPC_URL`, `--network base`, `BASESCAN_API_KEY` 61 | - Celo : `CELO_RPC_URL`, `--network celo`, `CELOSCAN_API_KEY` 62 | 63 | ### Testnets 64 | 65 | - Ethereum Sepolia: `ETHEREUM_SEPOLIA_RPC_URL`, `--network ethereumSepolia`, `ETHERSCAN_API_KEY` 66 | - Polygon Amoy: `POLYGON_AMOY_RPC_URL`, `--network polygonAmoy`, `POLYGONSCAN_API_KEY` 67 | - Avalanche Fuji: `AVALANCHE_FUJI_RPC_URL`, `--network avalancheFuji`, `SNOWTRACE_API_KEY` 68 | - Arbitrum Sepolia: `ARBITRUM_SEPOLIA_RPC_URL`, `--network arbitrumSepolia`, `ARBISCAN_API_KEY` 69 | - Base Sepolia: `BASE_SEPOLIA_RPC_URL`, `--network baseSepolia`, `BASESCAN_API_KEY` 70 | - Optimism Sepolia: `OPTIMISM_SEPOLIA_RPC_URL`, `--network optimismSepolia`, `OP_ETHERSCAN_API_KEY` 71 | - Celo Alfajores: `CELO_ALFAJORES_RPC_URL`, `--network celoAlfajores`, `CELOSCAN_API_KEY` 72 | 73 | ## For Beginners 74 | 75 | If you're new to web3, it is recommended starting with the [Functions - Getting Started](https://docs.chain.link/chainlink-functions/getting-started/) guide before diving into the code. 76 | 77 | The above document will help you: 78 | 79 | - Set up a wallet 80 | - Get funds 81 | - Provides more detailed step-by-step instructions and further information 82 | 83 | ### Tutorials & examples 84 | 85 | For other detailed tutorials and examples, check out the [Chainlink Functions Tutorials](https://docs.chain.link/chainlink-functions/tutorials/) to get started. 86 | 87 | ## Quickstart 88 | 89 | ### Requirements 90 | 91 | Install **both** of the following: 92 | 93 | - Node.js version [20](https://nodejs.org/en/download/) 94 | - Deno version [1.36](https://deno.land/manual@v1.36.4/getting_started/installation) (or the latest release of Deno v1 if a later one is available) 95 | 96 | ## Steps on Live (Public) Testnets 97 | 98 | 1. Clone this repository to your local machine

. Also ensure that the testnet your wanting to deploy on is [supported](https://docs.chain.link/chainlink-functions/supported-networks) by Chainlink Functions. 99 | 2. Open this directory in your command line/terminal app, then run `npm install` to install all dependencies.

100 | 3. Obtain the values for following environment variables (examples only - please see `./env.enc.example` for env vars you may need): 101 | - `PRIVATE_KEY` for your development wallet - `POLYGON_AMOY_RPC_URL`, `ETHEREUM_SEPOLIA_RPC_URL`, or `AVALANCHE_FUJI_RPC_URL` 102 | - `POLYGONSCAN_API_KEY`, `ETHERSCAN_API_KEY`, or `FUJI_SNOWTRACE_API_KEY` blockchain explore API keys depending on which network you're using 103 | - `COINMARKETCAP_API_KEY` (from [here](https://pro.coinmarketcap.com/)) 104 |

105 | 4. Set the required environment variables (see `./env.enc.example` for the correctly capitalized names of environment variables used in this repo). For improved security, Chainlink provides the NPM package [@chainlink/env-enc](https://www.npmjs.com/package/@chainlink/env-enc) which can be used to keep environment variables in a password encrypted `.env.enc` file instead of a plaintext `.env` for additional security. More detail on environment variable management and the tooling is provided in the [Environment Variable Management](#environment-variable-management) section. 106 | 1. Set an encryption password for your environment variables to a secure password by running `npx env-enc set-pw`. This password needs to be set each time you create or restart a terminal shell session.
107 | 2. Use the command `npx env-enc set` to set the required environment variables. 108 | 3. Set any other values you intend to pass into the _secrets_ object in _Functions-request-config.js_ .

109 | 5. There are four files to notice that the default example will use: 110 | - `Functions-request-config.js` which contains the `request` object that has all the data necessary to trigger a Functions request. This config file also specifies which `source` code to pass to Functions. More information on request configuration is in the [Request Configuration section](#request-configuration). 111 | - `contracts/FunctionsConsumer.sol` is the consumer smart contract that will receive the Functions-related data from the request config, and trigger the functions request. 112 | - `calculation-example.js` contains example JavaScript code that will be executed by each node of the DON. This example performs complex calculations but no API requests. 113 | - `API-request-example.js` contains example JavaScript code which fetches data from APIs before processing the data

114 | 6. Locally simulate the execution of your JavaScript source by running `npx hardhat functions-simulate-script` 115 | 116 | 7. Deploy and verify the consumer contract to an actual blockchain network by running `npx hardhat functions-deploy-consumer --network network_name_here --verify true`
**Note**: Make sure `_API_KEY` is set if using `--verify true` depending on which network is used.

117 | 8. Create and fund a new Functions billing subscription using the [Chainlink Functions UI](https://functions.chain.link) and add the deployed consumer contract as an authorized consumer to your subscription. You can also do this programmatically with `npx hardhat functions-sub-create --network network_name_here --amount LINK_funding_amount_here --contract 0x_deployed_client_contract_address_here`
**Note**: Ensure your wallet has a sufficient LINK balance before running this command. Testnet LINK can be obtained at faucets.chain.link. Also make a note of your subscription Id as you will need it for most commands.
118 | 119 | 9. Make an on-chain request by running:
`npx hardhat functions-request --network network_name_here --contract 0xDeployed_client_contract_address_here --subid subscription_id_number_here`. You will see a confirmation request, so hit `Y` and press enter. Once the request is fulfilled the console will show the response (decoded into the relevant return type) from the execution of your custom JS script. 120 | 121 | 10. You can also query the response that was stored in your Functions Consumer contract by runnning `npx hardhat functions-read --contract 0xConsumer_contract_address --network your_network_name` 122 | 123 | ### Steps on local testnet 124 | 125 | 1. To do an end-to-end simulation using a local testnet you can first open a new terminal window and run `npm run startLocalFunctionsTestnet`. This will spin up a local blockchain testnet (the `localFunctionsTestnet`), on which you can simulate an end-to-end Functions request. 126 | 127 | 2. Follow the workflow steps above, including subscription creation, funding, deploying your Functions Consumer etc. but omit the `--network network_name_here` flag in your CLI commands as the default network will be the `localFunctionsTestnet`. 128 | 129 | 3. Running this end-to-end simulation will surface most errors in your smart contract and/or JavaScript source code and configuration.

130 | 131 | ## Environment Variable Management 132 | 133 | This repo uses the NPM package `@chainlink/env-enc` for keeping environment variables such as wallet private keys, RPC URLs, and other secrets encrypted at rest. This reduces the risk of credential exposure by ensuring credentials are not visible in plaintext as they are with [.env files](https://www.npmjs.com/package/dotenv). 134 | 135 | By default, all encrypted environment variables will be stored in a file named `.env.enc` in the root directory of this repo. This file is `.gitignore`'d. 136 | 137 | For a full list of the Env Var names (keys) that this repo uses and has defined please look at `./env.enc.example`. 138 | 139 | First, set the encryption password by running the command `npx env-enc set-pw`. 140 | 141 | > **NOTE:** On Windows, this command may show a security confirmation. 142 | 143 | The password must be set at the beginning of each new session. 144 | If this password is lost, there will be no way to recover the encrypted environment variables. 145 | 146 | Run the command `npx env-enc set` to set and save environment variables. 147 | These variables will be loaded into your environment when the `config()` method is called at the top of `networks.js`. 148 | 149 | Use `npx env-enc view` to view all currently saved environment variables. 150 | When pressing _ENTER_, the terminal will be cleared to prevent these values from remaining visible. 151 | 152 | Running `npx env-enc remove VAR_NAME_HERE` deletes the specified environment variable. 153 | 154 | The command `npx env-enc remove-all` deletes the entire saved environment variable file. 155 | 156 | When running this command on a Windows machine, you may receive a security confirmation prompt. Enter `r` to proceed. 157 | 158 | > **NOTE:** When you finish each work session, close down your terminal to prevent your encryption password from becoming exposes if your machine is compromised. You will need to set the same password on future session to decrypt the `.env.enc` file. 159 | 160 | ### Using Remote Secrets (e.g. Github Gists) 161 | 162 | To upload and delete secrets gists that will remotely store your encrypted secrets, you need to first acquire a Github personal access token which allows reading and writing Gists. 163 | 164 | 1. Visit [https://github.com/settings/tokens?type=beta](https://github.com/settings/tokens?type=beta) and click "Generate new token" 165 | 2. Name the token and enable read & write access for Gists from the "Account permissions" drop-down menu. Do not enable any additional permissions. 166 | 3. Click "Generate token" and copy the resulting personal access token for step 4. 167 | 4. set the `GITHUB_API_TOKEN` environment variable using `npx env-enc set` 168 | 5. Specify `Location.Remote` for the `secretLocation` in _Functions-request-config.js_ 169 | 170 | ### Environment Variable Management Commands 171 | 172 | The following commands accept an optional `--path` flag followed by a path to the desired encrypted environment variable file. 173 | If one does not exist, it will be created automatically by the `npx env-enc set` command. 174 | 175 | The `--path` flag has no effect on the `npx env-enc set-pw` command as the password is stored as an ephemeral environment variable for the current terminal session. 176 | 177 | | Command | Description | Parameters | 178 | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | 179 | | `npx env-enc set-pw` | Sets the password to encrypt and decrypt the environment variable file **NOTE:** On Windows, this command may show a security confirmation prompt | | 180 | | `npx env-enc set` | Sets and saves variables to the encrypted environment variable file | | 181 | | `npx env-enc view` | Shows all currently saved variables in the encrypted environment variable file | | 182 | | `npx env-enc remove ` | Removes a variable from the encrypted environment variable file | `name`: Variable name | 183 | | `npx env-enc remove-all` | Deletes the encrypted environment variable file | | 184 | 185 | ## Functions Command Glossary 186 | 187 | [Functions Commands](#functions-commands) and [Subscription Management Commands](#functions-subscription-management-commands) commands can be executed in the following format: 188 | `npx hardhat command_here --parameter1 parameter_1_value_here --parameter2 parameter_2_value_here` 189 | 190 | Example: `npx hardhat functions-read --network polygonMumbai --contract 0x787Fe00416140b37B026f3605c6C72d096110Bb8` 191 | 192 | ### Functions Commands 193 | 194 | | Command | Description | Parameters | 195 | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 196 | | `compile` | Compiles all smart contracts | | 197 | | `functions-simulate-script` | Executes the JavaScript source code locally | `network`: Name of blockchain network, `configpath` (optional): Path to request config file (defaults to `./Functions-request-config.js`) | 198 | | `functions-deploy-consumer` | Deploys the `FunctionsConsumer` contract | `network`: Name of blockchain network, `verify` (optional): Set to `true` to verify the deployed `FunctionsConsumer` contract (defaults to `false`) | 199 | | `functions-request` | Initiates a request from a `FunctionsConsumer` contract using data from the Functions request config file | `network`: Name of blockchain network, `contract`: Address of the consumer contract to call, `subid`: Billing subscription ID used to pay for the request, `callbackgaslimit` (optional): Maximum amount of gas that can be used to call `fulfillRequest` in the consumer contract (defaults to 100,000 & must be less than 300,000), `slotid` (optional): Slot ID to use for uploading DON hosted secrets. If the slot is already in use, the existing encrypted secrets will be overwritten. (defaults to 0), `simulate` (optional, default true): Flag indicating if simulation should be run before making an on-chain request, `requestgaslimit` (optional): Gas limit for calling the sendRequest function (defaults to 1,500,000) `configpath` (optional): Path to request config file (defaults to `./Functions-request-config.js`) | 200 | | `functions-read` | Reads the latest response (or error) returned to a `FunctionsConsumer` or `AutomatedFunctionsConsumer` contract | `network`: Name of blockchain network, `contract`: Address of the consumer contract to read, `configpath` (optional): Path to request config file (defaults to `./Functions-request-config.js`) | 201 | | `functions-deploy-auto-consumer` | Deploys the `AutomatedFunctionsConsumer` contract and sets the Functions request using data from the Functions request config file | `network`: Name of blockchain network, `subid`: Billing subscription ID used to pay for Functions requests, `verify` (optional, default false): Set to `true` to verify the deployed `AutomatedFunctionsConsumer` contract, `configpath` (optional): Path to request config file (defaults to `./Functions-request-config.js`) | 202 | | `functions-set-auto-request` | Updates the Functions request in deployed `AutomatedFunctionsConsumer` contract using data from the Functions request config file | `network`: Name of blockchain network, `contract`: Address of the contract to update, `subid`: Billing subscription ID used to pay for Functions requests, `interval` (optional): Update interval in seconds for Chainlink Automation to call `performUpkeep` (defaults to 300), `slotid` (optional) 0 or higher integer denoting the storage slot for DON-hosted secrets, `ttl` (optional) the minutes after which DON hosted secrets must be expired, `gaslimit` (optional): Maximum amount of gas that can be used to call `fulfillRequest` in the consumer contract (defaults to 250,000), `simulate` (optional, default true): Flag indicating if simulation should be run before making an on-chain request, `configpath` (optional): Path to request config file (defaults to `./Functions-request-config.js`) | 203 | | `functions-check-upkeep` | Checks if `checkUpkeep` returns true for an Automation compatible contract | `network`: Name of blockchain network, `contract`: Address of the contract to check, `data` (optional): Hex string representing bytes that are passed to the `checkUpkeep` function (defaults to empty bytes) | 204 | | `functions-perform-upkeep` | Manually call `performUpkeep` in an Automation compatible contract | `network`: Name of blockchain network, `contract`: Address of the contract to call, `data` (optional): Hex string representing bytes that are passed to the `performUpkeep` function (defaults to empty bytes) | 205 | | `functions-set-donid` | Updates the DON ID for a consumer contract using the `donId` address from `networks.js` | `network`: Name of blockchain network, `contract`: Address of the consumer contract to update | 206 | | `functions-build-request` | Creates a JSON file with Functions request parameters including encrypted secrets, using data from the Functions request config file | `network`: Name of blockchain network, `output` (optional): Output JSON file name (defaults to _Functions-request.json_), `simulate` (optional, default true): Flag indicating if simulation should be run before building the request JSON file, `configpath` (optional): Path to request config file (defaults to `./Functions-request-config.js`) | 207 | | `functions-build-offchain-secrets` | Builds an off-chain secrets object that can be uploaded and referenced via URL | `network`: Name of blockchain network, `output` (optional): Output JSON file name (defaults to `offchain-encrypted-secrets.json`), `configpath` (optional): Path to request config file (defaults to `./Functions-request-config.js`) | 208 | | `functions-upload-secrets-don` | Encrypts secrets and uploads them to the DON | `network`: Name of blockchain network, `configpath` (optional): Path to request config file (defaults to `./Functions-request-config.js`), `slotid` Storage slot number 0 or higher - if the slotid is already in use, the existing secrets for that slotid will be overwritten, `ttl` (optional): Time to live - minutes until the secrets hosted on the DON expire (defaults to 10, and must be at least 5) | 209 | | `functions-list-don-secrets` | Displays encrypted secrets hosted on the DON | `network`: Name of blockchain network | 210 | 211 | ### Functions Subscription Management Commands 212 | 213 | | Command | Description | Parameters | 214 | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 215 | | `functions-sub-create` | Creates a new Functions billing subscription for Functions consumer contracts | `network`: Name of blockchain network, `amount` (optional): Initial amount used to fund the subscription in LINK (decimals are accepted), `contract` (optional): Address of the consumer contract to add to the subscription | 216 | | `functions-sub-info` | Gets the Functions billing subscription balance, owner, and list of authorized consumer contract addresses | `network`: Name of blockchain network, `subid`: Subscription ID | 217 | | `functions-sub-fund` | Funds a Functions billing subscription with LINK | `network`: Name of blockchain network, `subid`: Subscription ID, `amount`: Amount to fund subscription in LINK (decimals are accepted) | 218 | | `functions-sub-cancel` | Cancels a Functions billing subscription and refunds the unused balance. Cancellation is only possible if there are no pending requests. | `network`: Name of blockchain network, `subid`: Subscription ID, `refundaddress` (optional): Address where the remaining subscription balance is sent (defaults to caller's address) | 219 | | `functions-sub-add` | Authorizes a consumer contract to use the Functions billing subscription | `network`: Name of blockchain network, `subid`: Subscription ID, `contract`: Address of the consumer contract to authorize for billing | 220 | | `functions-sub-remove` | Removes a consumer contract from a Functions billing subscription | `network`: Name of blockchain network, `subid`: Subscription ID, `contract`: Address of the consumer contract to remove from billing subscription | 221 | | `functions-sub-transfer` | Request ownership of a Functions subscription be transferred to a new address | `network`: Name of blockchain network, `subid`: Subscription ID, `newowner`: Address of the new owner | 222 | | `functions-sub-accept` | Accepts ownership of a Functions subscription after a transfer is requested | `network`: Name of blockchain network, `subid`: Subscription ID | 223 | | `functions-timeout-requests` | Times out expired Functions requests which have not been fulfilled within 5 minutes | `network`: Name of blockchain network, `requestids`: 1 or more request IDs to timeout separated by commas, `toblock` (optional): Ending search block number (defaults to latest block), `pastblockstosearch` (optional): Number of past blocks to search (defaults to 1,000) | 224 | 225 | ## Request Configuration 226 | 227 | Chainlink Functions requests can be configured by modifying values in the `requestConfig` object found in the _Functions-request-config.js_ file located in the root of this repository. 228 | 229 | | Setting Name | Description | 230 | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 231 | | `codeLocation` | This specifies where the JavaScript code for a request is located. Currently, only the `Location.Inline` option is supported (represented by the value `0`). This means the JavaScript string is provided directly in the on-chain request instead of being referenced via a URL. | 232 | | `codeLanguage` | This specifies the language of the source code which is executed in a request. Currently, only `JavaScript` is supported (represented by the value `0`). | 233 | | `source` | This is a string containing the source code which is executed in a request. This must be valid JavaScript code that returns a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array). See the [JavaScript Code](#javascript-code) section for more details. | 234 | | `secrets` | This is an (optional) object which contains secret values that are injected into the JavaScript source code and can be accessed using the name `secrets`. This object can only contain string values. This object will be automatically encrypted by the tooling using the threshold public key before making request. | 235 | | `secretsLocation` | This (optional) value must be present if `secrets` are present. Values must be one of either `DONhosted` or `Remote`. This refers to the location of the Secrets - which can be User-hosted (Remote) at a URL or DON-hosted. | 236 | | `args` | This is an array of strings which contains values that are injected into the JavaScript source code and can be accessed using the name `args`. This provides a convenient way to set modifiable parameters within a request. If no arguments, then an empty array is passed. | 237 | | `expectedReturnType` | This specifies the expected return type of a request. It has no on-chain impact, but is used by the CLI to decode the response bytes into the specified type. The options are `uint256`, `int256`, `string`, or `bytes`. | 238 | 239 | ### JavaScript Code 240 | 241 | The JavaScript source code for a Functions request can use any valid [Deno](https://deno.land/manual@v1.36.4/introduction) JavaScript, but _cannot_ use any imported modules. 242 | 243 | The code must return a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) which represents the response bytes that are sent back to the requesting contract. 244 | Encoding functions are provided in the [Functions library](#functions-library). 245 | Additionally, any external APIs to which requests are made must script must respond in **less than 9 seconds** and the JavaScript Code as a whole must return in **less than 10 seconds** or it will be terminated and send back an error (in bytes) to the requesting contract. 246 | 247 | In order to make HTTP requests, the source code must use the `Functions.makeHttpRequest` function from the exposed [Functions library](#functions-library). 248 | Asynchronous code with top-level `await` statements is supported, as shown in the file _API-request-example.js_. 249 | 250 | #### Functions Library 251 | 252 | The `Functions` library is injected into the JavaScript source code and can be accessed using the name `Functions`. 253 | 254 | In order to make HTTP requests, use the `Functions.makeHttpRequest` method which takes an object as an argument with the following parameters. 255 | 256 | ``` 257 | { 258 | url: String with the URL to which the request is sent, 259 | method (optional): String specifying the HTTP method to use which can be either 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', or 'OPTIONS' (defaults to 'GET'), 260 | headers (optional): Object with headers to use in the request, 261 | params (optional): Object with URL query parameters, 262 | data (optional): Object or other value which represents the body sent with the request, 263 | timeout (optional): Number with the maximum request duration in ms (defaults to 3000 ms), 264 | responseType (optional): String specifying the expected response type which can be either 'json', 'arraybuffer', 'document', 'text' or 'stream' (defaults to 'json'), 265 | } 266 | ``` 267 | 268 | The function returns a promise that resolves to either a success response object or an error response object. 269 | 270 | A success response object will have the following parameters. 271 | 272 | ``` 273 | { 274 | error: false, 275 | data: Response data sent by the server, 276 | status: Number representing the response status, 277 | statusText: String representing the response status, 278 | headers: Object with response headers sent by the server, 279 | } 280 | ``` 281 | 282 | An error response object will have the following parameters. 283 | 284 | ``` 285 | { 286 | error: true, 287 | message (may be undefined): String containing error message, 288 | code (may be undefined): String containing an error code, 289 | response (may be undefined): Object containing response sent from the server, 290 | } 291 | ``` 292 | 293 | This library also exposes functions for encoding JavaScript values into Uint8Arrays which represent the bytes that a returned on-chain. 294 | 295 | - `Functions.encodeUint256` takes a positive JavaScript integer number and returns a Uint8Array of 32 bytes representing a `uint256` type in Solidity. 296 | - `Functions.encodeInt256` takes a JavaScript integer number and returns a Uint8Array of 32 bytes representing a `int256` type in Solidity. 297 | - `Functions.encodeString` takes a JavaScript string and returns a Uint8Array representing a `string` type in Solidity. 298 | 299 | Remember, it is not required to use these encoding functions. The JavaScript code must only return a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) which represents the `bytes` that are returned on-chain. 300 | 301 | #### Importing Dependencies 302 | 303 | To import and use libraries in your Functions request JavaScript source code, you must use the async `import` function. Since this is an async function, you must remember to use the `await` keyword to wait for the dependency to be imported before it can be used as shown in the examples below. 304 | 305 | ``` 306 | const lodash = await import("http://cdn.skypack.dev/lodash"); 307 | const result = lodash.concat([1], 2); 308 | return Functions.encodeString(JSON.stringify(result)); 309 | ``` 310 | 311 | ``` 312 | const { ethers } = await import("npm:ethers@6.9.0"); 313 | const myNumber = ethers.AbiCoder.defaultAbiCoder().decode( 314 | ["uint256"], 315 | "0x000000000000000000000000000000000000000000000000000000000000002a" 316 | ); 317 | return Functions.encodeUint256(BigInt(myNumber.toString())); 318 | ``` 319 | 320 | > ⚠️ **Users are fully responsible for any dependencies their JavaScript source code imports. Chainlink is not responsible for any imported dependencies and provides no guarantees of the validity, availability or security of any libraries a user chooses to import or the repositories from which these dependencies are downloaded. Developers are advised to fully vet any imported dependencies or avoid dependencies altogether to avoid any risks associated with a compromised library or a compromised repository from which the dependency is downloaded.** 321 | 322 | Chainlink Functions supports importing ESM-compatible modules with are supported by Deno within the JavaScript source code. It also supports importing some NPM packages [via the `npm:` specifier](https://docs.deno.com/runtime/manual/node/npm_specifiers) and some standard Node.js modules [via the `node:` specifier](https://docs.deno.com/runtime/manual/node/node_specifiers). Check out the [Deno documentation on importing modules](https://docs.deno.com/runtime/manual/basics/modules/) for more information or visit [deno.land/x](https://deno.land/x) to find 3rd party modules which have been built for Deno. 323 | 324 | The total number of imports and the size of each import are restricted: 325 | 326 | - You can import a maximum of 100 dependencies. Sub-dependencies required by the target library also count toward this limit. 327 | - The total size of each imported dependency cannot be larger than 10 MB. This 10 MB size limit includes any sub-dependencies required by the target library. 328 | 329 | All other [service limits](https://docs.chain.link/chainlink-functions/resources/service-limits) still apply to imported dependencies. This means the dependencies will not have access to the file system, environment variables or any other Deno permissions. If an imported library requires restricted permissions, importing the library may result in an error. Furthermore, dependencies are downloaded at runtime, meaning the time required to download a dependency is counted toward the total JavaScript source code execution time limit. 330 | 331 | Sometimes imported dependencies use additional fetch requests to load additional code or resources. These fetch requests count toward the total number of HTTP requests that the JavaScript source code is allowed to perform. If the imported dependencies exceed this total number of allowed fetch requests, the import attempt will fail with an error. 332 | 333 | ### Modifying Contracts 334 | 335 | Consumer contracts which initiate a request and receive a fulfillment can be modified for specific use cases. The only requirements are that the contract successfully calls `sendRequest` in the `FunctionsRouter`, 336 | and that it correctly implements the `fulfillRequest` function which is called by `handleOracleFulfillment` in the inherited `FunctionsClient` contract (See _FunctionsClient.sol_ for details).
337 | At this time, the maximum amount of gas that _handleOracleFulfillment_ can use is 300,000 (please contact Chainlink Labs if you require a higher callback gas limit). 338 | 339 | ### Local Simulations with the `localFunctionsTestnet` 340 | 341 | The Functions Toolkit NPM package provides the ability to create a local testnet blockchain on your machine which allows you to make simulated requests to debug your JavaScript code and smart contracts. 342 | For more details, please see the [Functions Toolkit NPM package documentation](https://github.com/smartcontractkit/functions-toolkit/blob/main/README.md#local-functions-testnet). 343 | 344 | In order to launch the `localFunctionsTestnet` in this project, open a new terminal window and run the command `npm run startLocalFunctionsTestnet`. 345 | Then, you can interact with this local testnet blockchain as you would with a live testnet. 346 | 347 | By default, all the `npx hardhat` commands in this project are configured to use this local testnet running on port `8545`, so you can omit the `--network` CLI argument (just don't forget to start the testnet first). 348 | 349 | ### Managing Secrets 350 | 351 | Please refer to the [Functions Toolkit NPM package documentation](https://github.com/smartcontractkit/functions-toolkit/blob/main/README.md#functions-secrets-manager) for more details. 352 | 353 | Secrets can be managed in either of two ways: user-hosted (`Location.Remote`) or DON hosted (`Location.DONHosted`). 354 | 355 | This project uses DONHosted secrets by default, which means secrets from the `Functions-request-config.js` file are encrypted and then uploaded to the DON and automatically. 356 | 357 | The CLI command to upload secrets to the DON is `npx hardhat functions-upload-secrets-don --slotid _0_or_higher --network network_name --ttl minutes_until_expired`. 358 | 359 | ## Automation Integration 360 | 361 | Chainlink Functions can be used with [Chainlink Automation](https://docs.chain.link/chainlink-automation/introduction) in order to automatically trigger a Functions as specified intervals. 362 | 363 | 1. Create & fund a new Functions billing subscription by running `npx hardhat functions-sub-create --network network_name_here --amount LINK_funding_amount_here`
**Note**: Ensure your wallet has a sufficient LINK balance before running this command.

364 | 365 | 2. Deploy the `AutomationFunctionsConsumer` contract by running `npx hardhat functions-deploy-auto-consumer --subid subscription_id_number_here --verify true --network network_name_here`
**Note**: Make sure `_API_KEY` environment variable is set when using `--verify true`. 366 | 367 | - This step will automatically add your consumer contract as an authorized user of your subscription. You can verify by running `npm functions-sub-info --network network_name_here --subid subscription_id_number_here`. 368 | 369 | 3. Encode the request parameters into CBOR and store it on chain with `npx hardhat functions-set-auto-request --network network_name_here --subid subscription_id_number_here --interval automation-call-interval --slotid don_hosted_secret_slotId --ttl minutes_until_secrets_expiry --contract 0x_contract_address`
370 | 371 | > DON-Hosted secrets and expire after the specified `ttl` (which defaults to 10 minutes if no `ttl` is specified). If a request is sent after the `ttl` has expired, you will see error bytes returned to your consumer contract. 372 | 373 | 1. Register the `AutomationFunctionsConsumer` contract for upkeep via the Chainlink Automation web app here: [https://automation.chain.link/](https://automation.chain.link/). This example uses a "Custom Logic" Automation. 374 | - Be sure to set the `Gas limit` for the `performUpkeep` function to a high enough value. The recommended value is 1,000,000. 375 | - Once created, ensure the Automation upkeep has sufficient funds. You can add funds, pause or cancel the upkeep in the web app. 376 | - Find further documentation for working with Chainlink Automation here: [https://docs.chain.link/chainlink-automation/introduction](https://docs.chain.link/chainlink-automation/introduction) 377 | 378 | Once the contract is registered for upkeep, check the latest response or error with the commands `npx hardhat functions-read --network network_name_here --contract 0x_contract_address`. 379 | 380 | 1. For debugging on your machine, use the command `npx hardhat functions-check-upkeep --network network_name_here --contract contract_address_here` to see if Automation needs to call `performUpkeep`. If this call returns `false` then the upkeep interval has not yet passed and `performUpkeep` will not execute. In order to test that `performUpkeep` will run correctly before registering the Automation upkeep, you can also trigger a request manually using the command `npx hardhat functions-perform-upkeep --network network_name_here --contract contract_address_here` 381 | 382 | You can also attach a listener to a Subscription ID by updating the `subId` variable in `/scripts/listen.js`, and then running `npm run listen --network your_network_name` from the repo root in a new terminal so that it can keep listening as you develop. This script uses nodemon which restarts the script when you save files or when the listener returns a result. 383 | 384 | ## Gas Spikes 385 | 386 | When on-chain traffic is high, transaction gas prices can spike unexpectedly. This may decrease the accuracy of the estimated requests costs or cause transactions to fail. 387 | In order to mitigate these problems, ensure your Functions subscription balance has a sufficient buffer of two or more times the expected request cost in LINK. 388 | Additionally, you can manually set a hardcoded transaction gas price in the HardHat tooling by modifying the `gasPrice` parameter in the _networks.js_ config file for a particular network. 389 | 390 | ## Troubleshooting 391 | 392 | 1. If you get strange (and scary large) error output in your terminal because a transaction failed, it is super helpful to use [tenderly.co](https://tenderly.co). Once you create an account, and a project look for "Transactions" in the tab list on the left, and past in your Transaction Hash. Tenderly will look across various networks for it. It will then show you the causes for the error especially if the contract has been verified. Here is a useful video on how to debug transactions with Tenderly: 393 | 394 | 395 | 2. When running Chainlink Functions make sure your subscription ID has your `FunctionsConsumer` contract added as an authorized consumer. Also make sure that your subscription has enough LINK balance. You do this by calling `npx hardhat functions-sub-info --network network_name_here --subid subscription_id_here` to see your subscription details. If the Functions Router calculates that your subscription's balance is insufficient it will revert with a `InsufficientBalance` custom Solidity error. 396 | 397 | 3. When running Chainlink Functions with Automation you also need to ensure the Chainlink Automation upkeeps are funded to run the automation calls. The fastest way to maintain your Automation LINK subscription balance is through the Chainlink Automation web app here: [https://automation.chain.link/](https://automation.chain.link/) 398 | 399 | 4. If you get a transaction failure when calling `npx hardhat functions-request` and its an out of gas error (you can tell from the block explorer or from [Tenderly](https://tenderly.co)) then you may need to add the optional `---requestgaslimit` flag with a value higher than than the default which is 1_500_000. For example: `npx hardhat functions-request --requestgaslimit 1750000`. Note that `./tasks/Functions-consumer/request.js` already has some logic around this that applies to some networks that require higher gas. 400 | 401 | 5. BASE Sepolia / Optimism Sepolia: if you see an error like `ProviderError: transaction underpriced: tip needed 50, tip permitted 0` then wait a few seconds and re-try. This can happen due to network spikes. Also double check the `./networks.js` file configs to make sure that `gasPrice` is set to `1000_000` as these networks can require higher request gas. 402 | -------------------------------------------------------------------------------- /calculation-example.js: -------------------------------------------------------------------------------- 1 | // This example shows how to calculate a continuously compounding interested rate. 2 | // This calculation would require significant on-chain gas, but is easy for a decentralized oracle network. 3 | 4 | // Arguments can be provided when a request is initated on-chain and used in the request source code as shown below 5 | const principalAmount = parseInt(args[4]) 6 | const APYTimes100 = parseInt(args[5]) 7 | const APYAsDecimalPercentage = APYTimes100 / 100 / 100 8 | 9 | const timeInYears = 1 / 12 // represents 1 month 10 | const eulersNumber = 2.7183 11 | 12 | // Continuously-compounding interest formula: A = Pe^(rt) 13 | const totalAmountAfterInterest = principalAmount * eulersNumber ** (APYAsDecimalPercentage * timeInYears) 14 | 15 | // The source code MUST return a Buffer or the request will return an error message 16 | // Use one of the following functions to convert to a Buffer representing the response bytes that are returned to the consumer smart contract: 17 | // - Functions.encodeUint256 18 | // - Functions.encodeInt256 19 | // - Functions.encodeString 20 | // Or return a custom Buffer for a custom byte encoding 21 | return Functions.encodeUint256(Math.round(totalAmountAfterInterest)) 22 | -------------------------------------------------------------------------------- /contracts/AutomatedFunctionsConsumer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol"; 5 | 6 | import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; 7 | import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol"; 8 | import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/AutomationCompatible.sol"; 9 | 10 | /** 11 | * @title Automated Functions Consumer contract using Chainlink Automations 12 | * @notice This contract is for demonstration not production use. 13 | */ 14 | contract AutomatedFunctionsConsumer is FunctionsClient, ConfirmedOwner, AutomationCompatibleInterface { 15 | using FunctionsRequest for FunctionsRequest.Request; 16 | 17 | // State variables for Chainlink Functions 18 | bytes32 public donId; 19 | bytes public s_requestCBOR; 20 | uint64 public s_subscriptionId; 21 | uint32 public s_fulfillGasLimit; 22 | bytes32 public s_lastRequestId; 23 | bytes public s_lastResponse; 24 | bytes public s_lastError; 25 | 26 | // State variables for Chainlink Automation 27 | uint256 public s_updateInterval; 28 | uint256 public s_lastUpkeepTimeStamp; 29 | uint256 public s_upkeepCounter; 30 | uint256 public s_requestCounter; 31 | uint256 public s_responseCounter; 32 | 33 | event OCRResponse(bytes32 indexed requestId, bytes result, bytes err); 34 | event RequestRevertedWithErrorMsg(string reason); 35 | event RequestRevertedWithoutErrorMsg(bytes data); 36 | 37 | /** 38 | * @notice Executes once when a contract is created to initialize state variables 39 | * 40 | * @param router The Functions Router contract for the network 41 | * @param _donId The DON Id for the DON that will execute the Function 42 | */ 43 | constructor(address router, bytes32 _donId) FunctionsClient(router) ConfirmedOwner(msg.sender) { 44 | donId = _donId; 45 | s_lastUpkeepTimeStamp = 0; 46 | } 47 | 48 | /** 49 | * @notice Sets the bytes representing the CBOR-encoded FunctionsRequest.Request that is sent when performUpkeep is called 50 | 51 | * @param _subscriptionId The Functions billing subscription ID used to pay for Functions requests 52 | * @param _fulfillGasLimit Maximum amount of gas used to call the client contract's `handleOracleFulfillment` function 53 | * @param _updateInterval Time interval at which Chainlink Automation should call performUpkeep 54 | * @param requestCBOR Bytes representing the CBOR-encoded FunctionsRequest.Request 55 | */ 56 | function setRequest( 57 | uint64 _subscriptionId, 58 | uint32 _fulfillGasLimit, 59 | uint256 _updateInterval, 60 | bytes calldata requestCBOR 61 | ) external onlyOwner { 62 | s_updateInterval = _updateInterval; 63 | s_subscriptionId = _subscriptionId; 64 | s_fulfillGasLimit = _fulfillGasLimit; 65 | s_requestCBOR = requestCBOR; 66 | } 67 | 68 | /** 69 | * @notice Used by Automation to check if performUpkeep should be called. 70 | * 71 | * The function's argument is unused in this example, but there is an option to have Automation pass custom data 72 | * that can be used by the checkUpkeep function. 73 | * 74 | * Returns a tuple where the first element is a boolean which determines if upkeep is needed and the 75 | * second element contains custom bytes data which is passed to performUpkeep when it is called by Automation. 76 | */ 77 | function checkUpkeep(bytes memory) public view override returns (bool upkeepNeeded, bytes memory) { 78 | upkeepNeeded = (block.timestamp - s_lastUpkeepTimeStamp) > s_updateInterval; 79 | } 80 | 81 | /** 82 | * @notice Called by Automation to trigger a Functions request 83 | * 84 | * The function's argument is unused in this example, but there is an option to have Automation pass custom data 85 | * returned by checkUpkeep (See Chainlink Automation documentation) 86 | */ 87 | function performUpkeep(bytes calldata) external override { 88 | (bool upkeepNeeded, ) = checkUpkeep(""); 89 | require(upkeepNeeded, "Time interval not met"); 90 | s_lastUpkeepTimeStamp = block.timestamp; 91 | s_upkeepCounter = s_upkeepCounter + 1; 92 | 93 | try 94 | i_router.sendRequest( 95 | s_subscriptionId, 96 | s_requestCBOR, 97 | FunctionsRequest.REQUEST_DATA_VERSION, 98 | s_fulfillGasLimit, 99 | donId 100 | ) 101 | returns (bytes32 requestId) { 102 | s_requestCounter = s_requestCounter + 1; 103 | s_lastRequestId = requestId; 104 | emit RequestSent(requestId); 105 | } catch Error(string memory reason) { 106 | emit RequestRevertedWithErrorMsg(reason); 107 | } catch (bytes memory data) { 108 | emit RequestRevertedWithoutErrorMsg(data); 109 | } 110 | } 111 | 112 | /** 113 | * @notice Callback that is invoked once the DON has resolved the request or hit an error 114 | * 115 | * @param requestId The request ID, returned by sendRequest() 116 | * @param response Aggregated response from the user code 117 | * @param err Aggregated error from the user code or from the execution pipeline 118 | * Either response or error parameter will be set, but never both 119 | */ 120 | function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { 121 | s_lastResponse = response; 122 | s_lastError = err; 123 | s_responseCounter = s_responseCounter + 1; 124 | emit OCRResponse(requestId, response, err); 125 | } 126 | 127 | /** 128 | * @notice Set the DON ID 129 | * @param newDonId New DON ID 130 | */ 131 | function setDonId(bytes32 newDonId) external onlyOwner { 132 | donId = newDonId; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /contracts/FunctionsConsumer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol"; 5 | import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; 6 | import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol"; 7 | 8 | /** 9 | * @title Chainlink Functions example on-demand consumer contract example 10 | */ 11 | contract FunctionsConsumer is FunctionsClient, ConfirmedOwner { 12 | using FunctionsRequest for FunctionsRequest.Request; 13 | 14 | bytes32 public donId; // DON ID for the Functions DON to which the requests are sent 15 | 16 | bytes32 public s_lastRequestId; 17 | bytes public s_lastResponse; 18 | bytes public s_lastError; 19 | 20 | constructor(address router, bytes32 _donId) FunctionsClient(router) ConfirmedOwner(msg.sender) { 21 | donId = _donId; 22 | } 23 | 24 | /** 25 | * @notice Set the DON ID 26 | * @param newDonId New DON ID 27 | */ 28 | function setDonId(bytes32 newDonId) external onlyOwner { 29 | donId = newDonId; 30 | } 31 | 32 | /** 33 | * @notice Triggers an on-demand Functions request using remote encrypted secrets 34 | * @param source JavaScript source code 35 | * @param secretsLocation Location of secrets (only Location.Remote & Location.DONHosted are supported) 36 | * @param encryptedSecretsReference Reference pointing to encrypted secrets 37 | * @param args String arguments passed into the source code and accessible via the global variable `args` 38 | * @param bytesArgs Bytes arguments passed into the source code and accessible via the global variable `bytesArgs` as hex strings 39 | * @param subscriptionId Subscription ID used to pay for request (FunctionsConsumer contract address must first be added to the subscription) 40 | * @param callbackGasLimit Maximum amount of gas used to call the inherited `handleOracleFulfillment` method 41 | */ 42 | function sendRequest( 43 | string calldata source, 44 | FunctionsRequest.Location secretsLocation, 45 | bytes calldata encryptedSecretsReference, 46 | string[] calldata args, 47 | bytes[] calldata bytesArgs, 48 | uint64 subscriptionId, 49 | uint32 callbackGasLimit 50 | ) external onlyOwner { 51 | FunctionsRequest.Request memory req; 52 | req.initializeRequest(FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, source); 53 | req.secretsLocation = secretsLocation; 54 | req.encryptedSecretsReference = encryptedSecretsReference; 55 | if (args.length > 0) { 56 | req.setArgs(args); 57 | } 58 | if (bytesArgs.length > 0) { 59 | req.setBytesArgs(bytesArgs); 60 | } 61 | s_lastRequestId = _sendRequest(req.encodeCBOR(), subscriptionId, callbackGasLimit, donId); 62 | } 63 | 64 | /** 65 | * @notice Store latest result/error 66 | * @param requestId The request ID, returned by sendRequest() 67 | * @param response Aggregated response from the user code 68 | * @param err Aggregated error from the user code or from the execution pipeline 69 | * Either response or error parameter will be set, but never both 70 | */ 71 | function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { 72 | s_lastResponse = response; 73 | s_lastError = err; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contracts/test/LinkToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@chainlink/contracts/src/v0.8/shared/token/ERC677/LinkToken.sol"; 5 | -------------------------------------------------------------------------------- /contracts/test/MockV3Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@chainlink/contracts/src/v0.8/shared/mocks/MockV3Aggregator.sol"; 5 | -------------------------------------------------------------------------------- /env.enc.example: -------------------------------------------------------------------------------- 1 | // The following environment variables are used in this example repo. When setting 2 | // them using the npm env-enc package, please make sure you use the same names and casing for the 3 | // keys, as this codebase uses them as follows. 4 | // Reference: https://github.com/smartcontractkit/functions-hardhat-starter-kit?tab=readme-ov-file#environment-variable-management 5 | 6 | GITHUB_API_TOKEN 7 | PRIVATE_KEY 8 | SECOND_PRIVATE_KEY 9 | ETHEREUM_SEPOLIA_RPC_URL 10 | ETHERSCAN_API_KEY 11 | POLYGONSCAN_API_KEY 12 | POLYGON_MUMBAI_RPC_URL 13 | AVALANCHE_FUJI_RPC_URL 14 | BASESCAN_API_KEY 15 | ARBITRUM_RPC_URL 16 | ARBISCAN_API_KEY 17 | OPTIMISM_API_KEY 18 | OPTIMISM_SEPOLIA_RPC_URL 19 | BASE_SEPOLIA_RPC_URL 20 | COINMARKETCAP_API_KEY -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox") 2 | require("hardhat-contract-sizer") 3 | require("./tasks") 4 | const { networks } = require("./networks") 5 | 6 | // Enable gas reporting (optional) 7 | const REPORT_GAS = process.env.REPORT_GAS?.toLowerCase() === "true" ? true : false 8 | 9 | const SOLC_SETTINGS = { 10 | optimizer: { 11 | enabled: true, 12 | runs: 1_000, 13 | }, 14 | } 15 | 16 | /** @type import('hardhat/config').HardhatUserConfig */ 17 | module.exports = { 18 | defaultNetwork: "localFunctionsTestnet", 19 | solidity: { 20 | compilers: [ 21 | { 22 | version: "0.8.19", 23 | settings: SOLC_SETTINGS, 24 | }, 25 | { 26 | version: "0.8.7", 27 | settings: SOLC_SETTINGS, 28 | }, 29 | { 30 | version: "0.7.0", 31 | settings: SOLC_SETTINGS, 32 | }, 33 | { 34 | version: "0.6.6", 35 | settings: SOLC_SETTINGS, 36 | }, 37 | { 38 | version: "0.4.24", 39 | settings: SOLC_SETTINGS, 40 | }, 41 | ], 42 | }, 43 | networks: { 44 | ...networks, 45 | }, 46 | etherscan: { 47 | apiKey: { 48 | mainnet: networks.ethereum.verifyApiKey, 49 | avalanche: networks.avalanche.verifyApiKey, 50 | polygon: networks.polygon.verifyApiKey, 51 | sepolia: networks.ethereumSepolia.verifyApiKey, 52 | polygonAmoy: networks.polygonAmoy.verifyApiKey, 53 | avalancheFujiTestnet: networks.avalancheFuji.verifyApiKey, 54 | arbitrum: networks.arbitrum.verifyApiKey, 55 | arbitrumSepolia: networks.arbitrumSepolia.verifyApiKey, 56 | baseSepolia: networks.baseSepolia.verifyApiKey, 57 | optimismSepolia: networks.optimismSepolia.verifyApiKey, 58 | base: networks.base.verifyApiKey, 59 | optimism: networks.optimism.verifyApiKey, 60 | celoAlfajores: networks.celoAlfajores.verifyApiKey, 61 | celo: networks.celo.verifyApiKey, 62 | }, 63 | customChains: [ 64 | { 65 | network: "arbitrumSepolia", 66 | chainId: 421614, 67 | urls: { 68 | apiURL: "https://api-sepolia.arbiscan.io/api", 69 | browserURL: "https://sepolia.arbiscan.io/", 70 | }, 71 | }, 72 | { 73 | network: "baseSepolia", 74 | chainId: 84532, 75 | urls: { 76 | apiURL: "https://api-sepolia.basescan.org/api", 77 | browserURL: "https://sepolia-explorer.base.org", 78 | }, 79 | }, 80 | { 81 | network: "optimismSepolia", 82 | chainId: 11155420, 83 | urls: { 84 | apiURL: "https://api-sepolia-optimistic.etherscan.io/api", // https://docs.optimism.etherscan.io/v/optimism-sepolia-etherscan 85 | browserURL: "https://sepolia-optimistic.etherscan.io/", 86 | }, 87 | }, 88 | { 89 | network: "polygonAmoy", 90 | chainId: 80002, 91 | urls: { 92 | apiURL: "https://api-amoy.polygonscan.com/api", 93 | browserURL: "https://amoy.polygonscan.com", 94 | }, 95 | }, 96 | { 97 | network: "base", 98 | chainId: 8453, 99 | urls: { 100 | apiURL: "https://api.basescan.org/api", 101 | browserURL: "https://basescan.org", 102 | }, 103 | }, 104 | { 105 | network: "optimism", 106 | chainId: 10, 107 | urls: { 108 | apiUrl: "https://api-optimistic.etherscan.io/api", 109 | browserURL: "https://optimistic.etherscan.io/", 110 | }, 111 | }, 112 | { 113 | celoAlfajores: "celoAlfajores", 114 | chainId: 44787, 115 | urls: { 116 | apiURL: "https://alfajores.celoscan.io/api", 117 | browserURL: "https://alfajores.celoscan.io", 118 | }, 119 | }, 120 | { 121 | celoAlfajores: "celo", 122 | chainId: 42220, 123 | urls: { 124 | apiURL: "https://celoscan.io/api", 125 | browserURL: "https://celoscan.io", 126 | }, 127 | }, 128 | ], 129 | }, 130 | gasReporter: { 131 | enabled: REPORT_GAS, 132 | currency: "USD", 133 | outputFile: "gas-report.txt", 134 | noColors: true, 135 | }, 136 | contractSizer: { 137 | runOnCompile: false, 138 | only: ["FunctionsConsumer", "AutomatedFunctionsConsumer", "FunctionsBillingRegistry"], 139 | }, 140 | paths: { 141 | sources: "./contracts", 142 | tests: "./test", 143 | cache: "./build/cache", 144 | artifacts: "./build/artifacts", 145 | }, 146 | mocha: { 147 | timeout: 200000, // 200 seconds max for running tests 148 | }, 149 | } 150 | -------------------------------------------------------------------------------- /networks.js: -------------------------------------------------------------------------------- 1 | // All supported networks and related contract addresses are defined here. 2 | // 3 | // LINK token addresses: https://docs.chain.link/resources/link-token-contracts/ 4 | // Price feeds addresses: https://docs.chain.link/data-feeds/price-feeds/addresses 5 | // Chain IDs: https://chainlist.org/?testnets=true 6 | 7 | // Loads environment variables from .env.enc file (if it exists) 8 | require("@chainlink/env-enc").config() 9 | 10 | const DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS = 2 11 | 12 | const npmCommand = process.env.npm_lifecycle_event 13 | const isTestEnvironment = npmCommand == "test" || npmCommand == "test:unit" 14 | 15 | const isSimulation = process.argv.length === 3 && process.argv[2] === "functions-simulate-script" ? true : false 16 | 17 | // Set EVM private keys (required) 18 | const PRIVATE_KEY = process.env.PRIVATE_KEY 19 | 20 | // TODO @dev - set this to run the accept.js task. 21 | const SECOND_PRIVATE_KEY = process.env.SECOND_PRIVATE_KEY 22 | 23 | if (!isTestEnvironment && !isSimulation && !PRIVATE_KEY) { 24 | throw Error("Set the PRIVATE_KEY environment variable with your EVM wallet private key") 25 | } 26 | 27 | const accounts = [] 28 | if (PRIVATE_KEY) { 29 | accounts.push(PRIVATE_KEY) 30 | } 31 | if (SECOND_PRIVATE_KEY) { 32 | accounts.push(SECOND_PRIVATE_KEY) 33 | } 34 | 35 | const networks = { 36 | ethereum: { 37 | url: process.env.ETHEREUM_RPC_URL || "UNSET", 38 | gasPrice: undefined, // gas price for the functions request - default's to auto as per HH https://hardhat.org/hardhat-network/docs/reference#eth_gasprice 39 | nonce: undefined, 40 | accounts, 41 | verifyApiKey: process.env.ETHERSCAN_API_KEY || "UNSET", 42 | chainId: 1, 43 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 44 | nativeCurrencySymbol: "ETH", 45 | linkToken: "0x514910771AF9Ca656af840dff83E8264EcF986CA", 46 | linkPriceFeed: "0xDC530D9457755926550b59e8ECcdaE7624181557", // LINK/ETH 47 | functionsRouter: "0x65Dcc24F8ff9e51F10DCc7Ed1e4e2A61e6E14bd6", 48 | donId: "fun-ethereum-mainnet-1", 49 | gatewayUrls: ["https://01.functions-gateway.chain.link/", "https://02.functions-gateway.chain.link/"], 50 | }, 51 | avalanche: { 52 | url: process.env.AVALANCHE_RPC_URL || "UNSET", 53 | gasPrice: undefined, 54 | nonce: undefined, 55 | accounts, 56 | verifyApiKey: process.env.SNOWTRACE_API_KEY || "UNSET", 57 | chainId: 43114, 58 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 59 | nativeCurrencySymbol: "AVAX", 60 | linkToken: "0x5947BB275c521040051D82396192181b413227A3", 61 | linkPriceFeed: "0x1b8a25F73c9420dD507406C3A3816A276b62f56a", // LINK/AVAX 62 | functionsRouter: "0x9f82a6A0758517FD0AfA463820F586999AF314a0", 63 | donId: "fun-avalanche-mainnet-1", 64 | gatewayUrls: ["https://01.functions-gateway.chain.link/", "https://02.functions-gateway.chain.link/"], 65 | }, 66 | polygon: { 67 | url: process.env.POLYGON_RPC_URL || "UNSET", 68 | gasPrice: undefined, 69 | nonce: undefined, 70 | accounts, 71 | verifyApiKey: process.env.POLYGONSCAN_API_KEY || "UNSET", 72 | chainId: 137, 73 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 74 | nativeCurrencySymbol: "ETH", 75 | linkToken: "0xb0897686c545045aFc77CF20eC7A532E3120E0F1", 76 | linkPriceFeed: "0x5787BefDc0ECd210Dfa948264631CD53E68F7802", // LINK/MATIC 77 | functionsRouter: "0xdc2AAF042Aeff2E68B3e8E33F19e4B9fA7C73F10", 78 | donId: "fun-polygon-mainnet-1", 79 | gatewayUrls: ["https://01.functions-gateway.chain.link/", "https://02.functions-gateway.chain.link/"], 80 | }, 81 | ethereumSepolia: { 82 | url: process.env.ETHEREUM_SEPOLIA_RPC_URL || "UNSET", 83 | gasPrice: undefined, 84 | nonce: undefined, 85 | accounts, 86 | verifyApiKey: process.env.ETHERSCAN_API_KEY || "UNSET", 87 | chainId: 11155111, 88 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 89 | nativeCurrencySymbol: "ETH", 90 | linkToken: "0x779877A7B0D9E8603169DdbD7836e478b4624789", 91 | linkPriceFeed: "0x42585eD362B3f1BCa95c640FdFf35Ef899212734", // LINK/ETH 92 | functionsRouter: "0xb83E47C2bC239B3bf370bc41e1459A34b41238D0", 93 | donId: "fun-ethereum-sepolia-1", 94 | gatewayUrls: [ 95 | "https://01.functions-gateway.testnet.chain.link/", 96 | "https://02.functions-gateway.testnet.chain.link/", 97 | ], 98 | }, 99 | avalancheFuji: { 100 | url: process.env.AVALANCHE_FUJI_RPC_URL || "UNSET", 101 | gasPrice: undefined, 102 | nonce: undefined, 103 | accounts, 104 | verifyApiKey: process.env.SNOWTRACE_API_KEY || "UNSET", 105 | chainId: 43113, 106 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 107 | nativeCurrencySymbol: "AVAX", 108 | linkToken: "0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846", 109 | linkPriceFeed: "0x79c91fd4F8b3DaBEe17d286EB11cEE4D83521775", // LINK/AVAX 110 | functionsRouter: "0xA9d587a00A31A52Ed70D6026794a8FC5E2F5dCb0", 111 | donId: "fun-avalanche-fuji-1", 112 | gatewayUrls: [ 113 | "https://01.functions-gateway.testnet.chain.link/", 114 | "https://02.functions-gateway.testnet.chain.link/", 115 | ], 116 | }, 117 | arbitrum: { 118 | url: process.env.ARBITRUM_RPC_URL || "UNSET", 119 | gasPrice: undefined, 120 | nonce: undefined, 121 | accounts, 122 | verifyApiKey: process.env.ARBISCAN_API_KEY || "UNSET", 123 | chainId: 42161, 124 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 125 | nativeCurrencySymbol: "ETH", 126 | linkToken: "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", 127 | linkPriceFeed: "0xb7c8Fb1dB45007F98A68Da0588e1AA524C317f27", // LINK/ETH 128 | functionsRouter: "0x97083E831F8F0638855e2A515c90EdCF158DF238", 129 | donId: "fun-arbitrum-mainnet-1", 130 | gatewayUrls: ["https://01.functions-gateway.chain.link/", "https://02.functions-gateway.chain.link/"], 131 | }, 132 | arbitrumSepolia: { 133 | url: process.env.ARBITRUM_SEPOLIA_RPC_URL || "UNSET", 134 | gasPrice: undefined, 135 | nonce: undefined, 136 | accounts, 137 | verifyApiKey: process.env.ARBISCAN_API_KEY || "UNSET", 138 | url: process.env.ARBITRUM_SEPOLIA_RPC_URL || "UNSET", 139 | accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [], 140 | chainId: 421614, 141 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 142 | nativeCurrencySymbol: "ETH", 143 | linkToken: "0xb1D4538B4571d411F07960EF2838Ce337FE1E80E", 144 | linkPriceFeed: "0x3ec8593F930EA45ea58c968260e6e9FF53FC934f", // LINK/ETH 145 | functionsRouter: "0x234a5fb5Bd614a7AA2FfAB244D603abFA0Ac5C5C", 146 | donId: "fun-arbitrum-sepolia-1", 147 | gatewayUrls: [ 148 | "https://01.functions-gateway.testnet.chain.link/", 149 | "https://02.functions-gateway.testnet.chain.link/", 150 | ], 151 | }, 152 | baseSepolia: { 153 | url: process.env.BASE_SEPOLIA_RPC_URL || "UNSET", // https://docs.basescan.org/v/sepolia-basescan/ 154 | gasPrice: undefined, 155 | nonce: undefined, 156 | accounts, 157 | verifyApiKey: process.env.BASESCAN_API_KEY || "UNSET", 158 | chainId: 84532, 159 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 160 | nativeCurrencySymbol: "ETH", 161 | linkToken: "0xE4aB69C077896252FAFBD49EFD26B5D171A32410", 162 | linkPriceFeed: "0x56a43EB56Da12C0dc1D972ACb089c06a5dEF8e69", // https://docs.chain.link/data-feeds/price-feeds/addresses?network=base&page=1 163 | functionsRouter: "0xf9B8fc078197181C841c296C876945aaa425B278", 164 | donId: "fun-base-sepolia-1", 165 | gatewayUrls: [ 166 | "https://01.functions-gateway.testnet.chain.link/", 167 | "https://02.functions-gateway.testnet.chain.link/", 168 | ], 169 | }, 170 | optimismSepolia: { 171 | url: process.env.OPTIMISM_SEPOLIA_RPC_URL || "UNSET", // https://docs.optimism.io/chain/networks#op-sepolia 172 | gasPrice: undefined, 173 | nonce: undefined, 174 | accounts, 175 | verifyApiKey: process.env.OPTIMISM_API_KEY || "UNSET", 176 | chainId: 11155420, 177 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 178 | nativeCurrencySymbol: "ETH", 179 | linkToken: "0xE4aB69C077896252FAFBD49EFD26B5D171A32410", 180 | linkPriceFeed: "0x98EeB02BC20c5e7079983e8F0D0D839dFc8F74fA", //https://docs.chain.link/data-feeds/price-feeds/addresses?network=optimism&page=1#optimism-sepolia 181 | functionsRouter: "0xC17094E3A1348E5C7544D4fF8A36c28f2C6AAE28", 182 | donId: "fun-optimism-sepolia-1", 183 | gatewayUrls: [ 184 | "https://01.functions-gateway.testnet.chain.link/", 185 | "https://02.functions-gateway.testnet.chain.link/", 186 | ], 187 | }, 188 | polygonAmoy: { 189 | url: process.env.AMOY_RPC_URL || "UNSET", // h 190 | gasPrice: undefined, 191 | nonce: undefined, 192 | accounts, 193 | verifyApiKey: process.env.POLYGONSCAN_API_KEY || "UNSET", 194 | chainId: 80002, 195 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 196 | nativeCurrencySymbol: "MATIC", 197 | linkToken: "0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904", 198 | linkPriceFeed: "0x408D97c89c141e60872C0835e18Dd1E670CD8781", // LINK/MATIC 199 | 200 | functionsRouter: "0xC22a79eBA640940ABB6dF0f7982cc119578E11De", 201 | donId: "fun-polygon-amoy-1", 202 | gatewayUrls: [ 203 | "https://01.functions-gateway.testnet.chain.link/", 204 | "https://02.functions-gateway.testnet.chain.link/", 205 | ], 206 | }, 207 | base: { 208 | url: process.env.BASE_RPC_URL || "UNSET", 209 | gasPrice: undefined, 210 | nonce: undefined, 211 | accounts, 212 | verifyApiKey: process.env.BASESCAN_API_KEY || "UNSET", 213 | chainId: 8453, 214 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 215 | nativeCurrencySymbol: "ETH", 216 | linkToken: "0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196", 217 | linkPriceFeed: "0xc5E65227fe3385B88468F9A01600017cDC9F3A12", // LINK/ETH 218 | functionsRouter: "0xf9b8fc078197181c841c296c876945aaa425b278", 219 | donId: "fun-base-mainnet-1", 220 | gatewayUrls: ["https://01.functions-gateway.chain.link/", "https://02.functions-gateway.chain.link/"], 221 | }, 222 | optimism: { 223 | url: process.env.OPTIMISM_RPC_URL || "UNSET", 224 | gasPrice: undefined, 225 | nonce: undefined, 226 | accounts, 227 | verifyApiKey: process.env.OP_ETHERSCAN_API_KEY || "UNSET", 228 | chainId: 10, 229 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 230 | nativeCurrencySymbol: "ETH", 231 | linkToken: "0x350a791Bfc2C21F9Ed5d10980Dad2e2638ffa7f6", 232 | linkPriceFeed: "0x464A1515ADc20de946f8d0DEB99cead8CEAE310d", // LINK/ETH 233 | functionsRouter: "0xaA8AaA682C9eF150C0C8E96a8D60945BCB21faad", 234 | donId: "fun-optimism-mainnet-1", 235 | gatewayUrls: ["https://01.functions-gateway.chain.link/", "https://02.functions-gateway.chain.link/"], 236 | }, 237 | celoAlfajores: { 238 | url: process.env.CELO_ALFAJORES_RPC_URL || "UNSET", 239 | gasPrice: undefined, 240 | nonce: undefined, 241 | accounts, 242 | verifyApiKey: process.env.CELOSCAN_API_KEY || "UNSET", 243 | chainId: 44787, 244 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 245 | nativeCurrencySymbol: "ETH", 246 | linkToken: "0x32e08557b14fad8908025619797221281d439071", 247 | linkPriceFeed: "0xeA11fA4307B8BD6D6074Cf1c0caAc9790cf96857", // LINK/CELO 248 | functionsRouter: "0x53BA5D8E5aab0cf9589aCE139666Be2b9Fd268e2", 249 | donId: "fun-celo-alfajores-1", 250 | gatewayUrls: [ 251 | "https://01.functions-gateway.testnet.chain.link/", 252 | "https://02.functions-gateway.testnet.chain.link/", 253 | ], 254 | }, 255 | celo: { 256 | url: process.env.CELO_RPC_URL || "UNSET", 257 | gasPrice: undefined, 258 | nonce: undefined, 259 | accounts, 260 | verifyApiKey: process.env.CELOSCAN_API_KEY || "UNSET", 261 | chainId: 42220, 262 | confirmations: DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS, 263 | nativeCurrencySymbol: "ETH", 264 | linkToken: "0xd07294e6E917e07dfDcee882dd1e2565085C2ae0", 265 | linkPriceFeed: "0xBa45f0a1a2fa3FB62a4D6dC135741E2aeb1b14e7", // LINK/CELO 266 | functionsRouter: "0xd74646C75163f9dA0F3666C3BE8A9C42F4b3b261", 267 | donId: "fun-celo-mainnet-1", 268 | gatewayUrls: ["https://01.functions-gateway.chain.link/", "https://02.functions-gateway.chain.link/"], 269 | }, 270 | // localFunctionsTestnet is updated dynamically by scripts/startLocalFunctionsTestnet.js so it should not be modified here 271 | localFunctionsTestnet: { 272 | url: "http://localhost:8545/", 273 | accounts, 274 | confirmations: 1, 275 | nativeCurrencySymbol: "ETH", 276 | linkToken: "0x94d3C68A91C972388d7863D25EDD2Be7e2F21F21", 277 | functionsRouter: "0xCbfD616baE0F13EFE0528c446184C9C0EAa8040e", 278 | donId: "local-functions-testnet", 279 | }, 280 | } 281 | 282 | module.exports = { 283 | networks, 284 | } 285 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions-hardhat-starter-kit", 3 | "license": "MIT", 4 | "version": "0.2.1", 5 | "description": "Tooling for interacting with Chainlink Functions", 6 | "scripts": { 7 | "prepare": "husky install", 8 | "compile": "hardhat compile", 9 | "test": "npm run test:unit", 10 | "test:unit": "hardhat test test/unit/*.spec.js", 11 | "startLocalFunctionsTestnet": "node scripts/startLocalFunctionsTestnet.js", 12 | "listen": "nodemon scripts/listen.js", 13 | "lint": "npm run lint:contracts && npm run format:check", 14 | "lint:fix": "solhint 'contracts/**/*.sol' --fix", 15 | "lint:contracts": "solhint 'contracts/*.sol'", 16 | "lint:contracts:fix": "solhint 'contracts/**/*.sol' --fix", 17 | "format:check": "prettier --check .", 18 | "format:fix": "prettier --write ." 19 | }, 20 | "dependencies": { 21 | "@chainlink/contracts": "^1.4.0", 22 | "@chainlink/env-enc": "^1.0.5", 23 | "@chainlink/functions-toolkit": "^0.2.8", 24 | "@ethersproject/abi": "^5.7.0", 25 | "@ethersproject/providers": "^5.7.1", 26 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.3", 27 | "@nomicfoundation/hardhat-network-helpers": "^1.0.6", 28 | "@nomicfoundation/hardhat-toolbox": "^2.0.0", 29 | "@nomiclabs/hardhat-ethers": "^2.2.2", 30 | "@openzeppelin/contracts-upgradeable": "^4.9.3", 31 | "@typechain/ethers-v5": "^10.1.0", 32 | "@typechain/hardhat": "^6.1.3", 33 | "axios": "^1.1.3", 34 | "chai": "^4.3.6", 35 | "eth-crypto": "^2.4.0", 36 | "ethers": "^5.7.2", 37 | "hardhat": "^2.17.3", 38 | "hardhat-contract-sizer": "^2.6.1", 39 | "hardhat-gas-reporter": "^1.0.9", 40 | "husky": "^8.0.1", 41 | "lint-staged": "^13.0.3", 42 | "nodemon": "^3.0.1", 43 | "ora": "5.4.1", 44 | "prettier": "^2.7.1", 45 | "prettier-plugin-solidity": "^1.0.0-beta.24", 46 | "readline": "^1.3.0", 47 | "solhint": "^3.3.7", 48 | "solhint-plugin-prettier": "^0.0.5", 49 | "solidity-coverage": "^0.8.2", 50 | "typechain": "^8.1.0" 51 | }, 52 | "lint-staged": { 53 | "*.{js,json,yml,yaml}": [ 54 | "prettier --write" 55 | ], 56 | "*.sol": [ 57 | "prettier --write", 58 | "solhint" 59 | ] 60 | }, 61 | "prettier": { 62 | "trailingComma": "es5", 63 | "tabWidth": 2, 64 | "semi": false, 65 | "singleQuote": false, 66 | "printWidth": 120 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /scripts/listen.js: -------------------------------------------------------------------------------- 1 | // Loads environment variables from .env.enc file (if it exists) 2 | require("@chainlink/env-enc").config("../.env.enc") 3 | 4 | const { networks } = require("../networks") 5 | 6 | const { ResponseListener, decodeResult, ReturnType } = require("@chainlink/functions-toolkit") 7 | const { providers } = require("ethers") 8 | 9 | const subscriptionId = "TODO" // TODO @dev update this to show your subscription Id 10 | 11 | if (!subscriptionId || isNaN(subscriptionId)) { 12 | throw Error("Please update the subId variable in scripts/listen.js to your subscription ID.") 13 | } 14 | 15 | const networkName = "polygonMumbai" // TODO @dev update this to your network name 16 | 17 | // Mount Response Listener 18 | const provider = new providers.JsonRpcProvider(networks[networkName].url) 19 | const functionsRouterAddress = networks[networkName]["functionsRouter"] 20 | 21 | const responseListener = new ResponseListener({ provider, functionsRouterAddress }) 22 | // Remove existing listeners 23 | console.log("\nRemoving existing listeners...") 24 | responseListener.stopListeningForResponses() 25 | 26 | console.log(`\nListening for Functions Responses for subscriptionId ${subscriptionId} on network ${networkName}...`) 27 | // Listen for response 28 | responseListener.listenForResponses(subscriptionId, (response) => { 29 | console.log(`\n✅ Request ${response.requestId} fulfilled. Functions Status Code: ${response.fulfillmentCode}`) 30 | if (!response.errorString) { 31 | console.log( 32 | "\nFunctions response received!\nData written on chain:", 33 | response.responseBytesHexstring, 34 | "\n and that decodes to an int256 value of: ", 35 | decodeResult(response.responseBytesHexstring, ReturnType.int256).toString(), 36 | "\n" 37 | ) 38 | } else { 39 | console.log("\n❌ Error during the execution: ", response.errorString, "\n") 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /scripts/startLocalFunctionsTestnet.js: -------------------------------------------------------------------------------- 1 | const process = require("process") 2 | const path = require("path") 3 | const fs = require("fs") 4 | const { startLocalFunctionsTestnet } = require("@chainlink/functions-toolkit") 5 | const { utils, Wallet } = require("ethers") 6 | // Loads environment variables from .env.enc file (if it exists) 7 | require("@chainlink/env-enc").config("../.env.enc") 8 | ;(async () => { 9 | const requestConfigPath = path.join(process.cwd(), "Functions-request-config.js") // @dev Update this to point to your desired request config file 10 | console.log(`Using Functions request config file ${requestConfigPath}\n`) 11 | 12 | const localFunctionsTestnetInfo = await startLocalFunctionsTestnet( 13 | requestConfigPath, 14 | { 15 | logging: { 16 | debug: false, 17 | verbose: false, 18 | quiet: true, // Set this to `false` to see logs from the local testnet 19 | }, 20 | } // Ganache server options (optional) 21 | ) 22 | 23 | console.table({ 24 | "FunctionsRouter Contract Address": localFunctionsTestnetInfo.functionsRouterContract.address, 25 | "DON ID": localFunctionsTestnetInfo.donId, 26 | "Mock LINK Token Contract Address": localFunctionsTestnetInfo.linkTokenContract.address, 27 | }) 28 | 29 | // Fund wallets with ETH and LINK 30 | const addressToFund = new Wallet(process.env["PRIVATE_KEY"]).address 31 | await localFunctionsTestnetInfo.getFunds(addressToFund, { 32 | weiAmount: utils.parseEther("100").toString(), // 100 ETH 33 | juelsAmount: utils.parseEther("100").toString(), // 100 LINK 34 | }) 35 | if (process.env["SECOND_PRIVATE_KEY"]) { 36 | const secondAddressToFund = new Wallet(process.env["SECOND_PRIVATE_KEY"]).address 37 | await localFunctionsTestnetInfo.getFunds(secondAddressToFund, { 38 | weiAmount: utils.parseEther("100").toString(), // 100 ETH 39 | juelsAmount: utils.parseEther("100").toString(), // 100 LINK 40 | }) 41 | } 42 | 43 | // Update values in networks.js 44 | let networksConfig = fs.readFileSync(path.join(process.cwd(), "networks.js")).toString() 45 | const regex = /localFunctionsTestnet:\s*{\s*([^{}]*)\s*}/s 46 | const newContent = `localFunctionsTestnet: { 47 | url: "http://localhost:8545/", 48 | accounts, 49 | confirmations: 1, 50 | nativeCurrencySymbol: "ETH", 51 | linkToken: "${localFunctionsTestnetInfo.linkTokenContract.address}", 52 | functionsRouter: "${localFunctionsTestnetInfo.functionsRouterContract.address}", 53 | donId: "${localFunctionsTestnetInfo.donId}", 54 | }` 55 | networksConfig = networksConfig.replace(regex, newContent) 56 | fs.writeFileSync(path.join(process.cwd(), "networks.js"), networksConfig) 57 | })() 58 | -------------------------------------------------------------------------------- /tasks/Functions-billing/accept.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | 3 | const { networks } = require("../../networks") 4 | 5 | task("functions-sub-accept", "Accepts ownership of an Functions subscription after a transfer is requested") 6 | .addParam("subid", "Subscription ID") 7 | .setAction(async (taskArgs) => { 8 | const accounts = await ethers.getSigners() 9 | if (accounts.length < 2) { 10 | throw Error("This command requires a second wallet's private key to be made available in networks.js") 11 | } 12 | const accepter = accounts[1] // Second wallet. 13 | 14 | const subscriptionId = parseInt(taskArgs.subid) 15 | const confirmations = networks[network.name].confirmations 16 | const txOptions = { confirmations } 17 | 18 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 19 | const linkTokenAddress = networks[network.name]["linkToken"] 20 | 21 | const sm = new SubscriptionManager({ signer: accepter, linkTokenAddress, functionsRouterAddress }) 22 | await sm.initialize() 23 | 24 | const currentOwner = (await sm.getSubscriptionInfo(subscriptionId)).owner 25 | console.log(`\nAccepting ownership of subscription ${subscriptionId} from ${currentOwner}...`) 26 | const acceptTx = await sm.acceptSubTransfer({ subscriptionId, txOptions }) 27 | 28 | console.log( 29 | `Acceptance request completed in Tx: ${acceptTx.transactionHash}. \n${accepter.address} is now the owner of subscription ${subscriptionId}.` 30 | ) 31 | 32 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 33 | // parse balances into LINK for readability 34 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 35 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 36 | console.log("\nUpdated Subscription Info: ", subInfo) 37 | }) 38 | -------------------------------------------------------------------------------- /tasks/Functions-billing/add.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | 4 | task("functions-sub-add", "Adds a consumer contract to the Functions billing subscription") 5 | .addParam("subid", "Subscription ID") 6 | .addParam("contract", "Address of the Functions consumer contract to authorize for billing") 7 | .setAction(async (taskArgs) => { 8 | const consumerAddress = taskArgs.contract 9 | const subscriptionId = parseInt(taskArgs.subid) 10 | 11 | const signer = await ethers.getSigner() 12 | const linkTokenAddress = networks[network.name]["linkToken"] 13 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 14 | const txOptions = { confirmations: networks[network.name].confirmations } 15 | 16 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 17 | await sm.initialize() 18 | 19 | console.log(`\nAdding ${consumerAddress} to subscription ${subscriptionId}...`) 20 | const addConsumerTx = await sm.addConsumer({ subscriptionId, consumerAddress, txOptions }) 21 | console.log(`Added consumer contract ${consumerAddress} in Tx: ${addConsumerTx.transactionHash}`) 22 | }) 23 | -------------------------------------------------------------------------------- /tasks/Functions-billing/cancel.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | 3 | const utils = require("../utils") 4 | const { networks } = require("../../networks") 5 | 6 | task( 7 | "functions-sub-cancel", 8 | "Cancels Functions billing subscription and refunds unused balance. Cancellation is only possible if there are no pending requests" 9 | ) 10 | .addParam("subid", "Subscription ID to cancel") 11 | .addOptionalParam( 12 | "refundaddress", 13 | "Address where the remaining subscription balance is sent (defaults to caller's address)" 14 | ) 15 | .setAction(async (taskArgs) => { 16 | const subscriptionId = parseInt(taskArgs.subid) 17 | const refundAddress = taskArgs.refundaddress ?? (await ethers.getSigners())[0].address 18 | 19 | const signer = await ethers.getSigner() 20 | const linkTokenAddress = networks[network.name]["linkToken"] 21 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 22 | const confirmations = networks[network.name].confirmations 23 | const txOptions = { confirmations } 24 | 25 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 26 | await sm.initialize() 27 | 28 | await utils.prompt( 29 | `\nPlease confirm that you wish to cancel Subscription ${subscriptionId} and have its LINK balance sent to wallet ${refundAddress}.\nNote that a portion of the LINK balance will be deducted if a minimum number of requests were not performed with the subscription.\nRead the documentation for more details: https://docs.chain.link/chainlink-functions/resources/billing#withdrawing-funds` 30 | ) 31 | 32 | console.log(`Canceling subscription ${subscriptionId}`) 33 | let cancelTx 34 | try { 35 | cancelTx = await sm.cancelSubscription({ subscriptionId, refundAddress, txOptions }) 36 | } catch (error) { 37 | console.log( 38 | "Error cancelling subscription. Please ensure there are no pending requests or stale requests which have not been timed out before attempting to cancel." 39 | ) 40 | throw error 41 | } 42 | console.log(`\nSubscription ${subscriptionId} cancelled in Tx: ${cancelTx.transactionHash}`) 43 | }) 44 | -------------------------------------------------------------------------------- /tasks/Functions-billing/create.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | const chalk = require("chalk") 3 | const { networks } = require("../../networks") 4 | const utils = require("../utils") 5 | 6 | task("functions-sub-create", "Creates a new billing subscription for Functions consumer contracts") 7 | .addOptionalParam("amount", "Initial amount used to fund the subscription in LINK") 8 | .addOptionalParam( 9 | "contract", 10 | "Address of the consumer contract address authorized to use the new billing subscription" 11 | ) 12 | .setAction(async (taskArgs) => { 13 | const signer = await ethers.getSigner() 14 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 15 | const linkTokenAddress = networks[network.name]["linkToken"] 16 | 17 | const linkAmount = taskArgs.amount 18 | const confirmations = linkAmount > 0 ? networks[network.name].confirmations : 1 19 | const consumerAddress = taskArgs.contract 20 | const txOptions = { 21 | confirmations, 22 | overrides: { 23 | gasPrice: networks[network.name].gasPrice, 24 | }, 25 | } 26 | 27 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 28 | await sm.initialize() 29 | 30 | console.log("\nCreating Functions billing subscription...") 31 | const subscriptionId = await sm.createSubscription({ consumerAddress, txOptions }) 32 | console.log(`\nCreated Functions billing subscription: ${subscriptionId}`) 33 | 34 | // Fund subscription 35 | if (linkAmount) { 36 | await utils.prompt( 37 | `\nPlease confirm that you wish to fund Subscription ${subscriptionId} with ${chalk.blue( 38 | linkAmount + " LINK" 39 | )} from your wallet.` 40 | ) 41 | 42 | console.log(`\nFunding subscription ${subscriptionId} with ${linkAmount} LINK...`) 43 | const juelsAmount = ethers.utils.parseUnits(linkAmount, 18).toString() 44 | const fundTxReceipt = await sm.fundSubscription({ juelsAmount, subscriptionId, txOptions }) 45 | console.log( 46 | `\nSubscription ${subscriptionId} funded with ${linkAmount} LINK in Tx: ${fundTxReceipt.transactionHash}` 47 | ) 48 | 49 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 50 | // parse balances into LINK for readability 51 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 52 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 53 | 54 | console.log("\nSubscription Info: ", subInfo) 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /tasks/Functions-billing/fund.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | const chalk = require("chalk") 3 | const { networks } = require("../../networks") 4 | const utils = require("../utils") 5 | 6 | task("functions-sub-fund", "Funds a billing subscription for Functions consumer contracts") 7 | .addParam("amount", "Amount to fund subscription in LINK") 8 | .addParam("subid", "Subscription ID to fund") 9 | .setAction(async (taskArgs) => { 10 | const signer = await ethers.getSigner() 11 | const linkTokenAddress = networks[network.name]["linkToken"] 12 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 13 | const txOptions = { confirmations: networks[network.name].confirmations } 14 | 15 | const subscriptionId = parseInt(taskArgs.subid) 16 | const linkAmount = taskArgs.amount 17 | const juelsAmount = ethers.utils.parseUnits(linkAmount, 18).toString() 18 | 19 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 20 | await sm.initialize() 21 | 22 | await utils.prompt( 23 | `\nPlease confirm that you wish to fund Subscription ${subscriptionId} with ${chalk.blue( 24 | linkAmount + " LINK" 25 | )} from your wallet.` 26 | ) 27 | 28 | console.log(`\nFunding subscription ${subscriptionId} with ${linkAmount} LINK...`) 29 | 30 | const fundTxReceipt = await sm.fundSubscription({ juelsAmount, subscriptionId, txOptions }) 31 | console.log( 32 | `\nSubscription ${subscriptionId} funded with ${linkAmount} LINK in Tx: ${fundTxReceipt.transactionHash}` 33 | ) 34 | 35 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 36 | 37 | // parse balances into LINK for readability 38 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 39 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 40 | 41 | console.log("\nUpdated subscription Info: ", subInfo) 42 | }) 43 | -------------------------------------------------------------------------------- /tasks/Functions-billing/index.js: -------------------------------------------------------------------------------- 1 | exports.create = require("./create") 2 | exports.fund = require("./fund") 3 | exports.info = require("./info") 4 | exports.add = require("./add") 5 | exports.remove = require("./remove") 6 | exports.cancel = require("./cancel") 7 | exports.transfer = require("./transfer") 8 | exports.accept = require("./accept") 9 | exports.timeoutRequests = require("./timeoutRequests") 10 | -------------------------------------------------------------------------------- /tasks/Functions-billing/info.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | 4 | task( 5 | "functions-sub-info", 6 | "Gets the Functions billing subscription balance, owner, and list of authorized consumer contract addresses" 7 | ) 8 | .addParam("subid", "Subscription ID") 9 | .setAction(async (taskArgs) => { 10 | const subscriptionId = parseInt(taskArgs.subid) 11 | 12 | const signer = await ethers.getSigner() 13 | const linkTokenAddress = networks[network.name]["linkToken"] 14 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 15 | 16 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 17 | await sm.initialize() 18 | 19 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 20 | // parse balances into LINK for readability 21 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 22 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 23 | console.log(`\nInfo for subscription ${subscriptionId}:\n`, subInfo) 24 | }) 25 | -------------------------------------------------------------------------------- /tasks/Functions-billing/remove.js: -------------------------------------------------------------------------------- 1 | const { networks } = require("../../networks") 2 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 3 | 4 | task("functions-sub-remove", "Removes a consumer contract from an Functions billing subscription") 5 | .addParam("subid", "Subscription ID") 6 | .addParam("contract", "Address of the consumer contract to remove from billing subscription") 7 | .setAction(async (taskArgs) => { 8 | const signer = await ethers.getSigner() 9 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 10 | const linkTokenAddress = networks[network.name]["linkToken"] 11 | 12 | const consumerAddress = taskArgs.contract 13 | const subscriptionId = parseInt(taskArgs.subid) 14 | const confirmations = networks[network.name].confirmations 15 | const txOptions = { confirmations } 16 | 17 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 18 | await sm.initialize() 19 | 20 | console.log(`\nRemoving ${consumerAddress} from subscription ${subscriptionId}...`) 21 | let removeConsumerTx = await sm.removeConsumer({ subscriptionId, consumerAddress, txOptions }) 22 | 23 | const subInfo = await sm.getSubscriptionInfo(subscriptionId) 24 | // parse balances into LINK for readability 25 | subInfo.balance = ethers.utils.formatEther(subInfo.balance) + " LINK" 26 | subInfo.blockedBalance = ethers.utils.formatEther(subInfo.blockedBalance) + " LINK" 27 | console.log( 28 | `\nRemoved ${consumerAddress} from subscription ${subscriptionId} in Tx: ${removeConsumerTx.transactionHash}\nUpdated Subscription Info:\n`, 29 | subInfo 30 | ) 31 | }) 32 | -------------------------------------------------------------------------------- /tasks/Functions-billing/timeoutRequests.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager, fetchRequestCommitment } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | const { providers } = require("ethers") 4 | 5 | task( 6 | "functions-timeout-requests", 7 | "Times out expired Functions requests which have not been fulfilled within 5 minutes" 8 | ) 9 | .addParam("requestids", "1 or more request IDs to timeout separated by commas") 10 | .addOptionalParam("toblock", "Ending search block number (defaults to latest block)") 11 | .addOptionalParam("pastblockstosearch", "Number of past blocks to search", 1000, types.int) 12 | .setAction(async (taskArgs) => { 13 | const requestIdsToTimeout = taskArgs.requestids.split(",") 14 | console.log(`Timing out requests ${requestIdsToTimeout} on ${network.name}`) 15 | const toBlock = taskArgs.toblock ? Number(taskArgs.toblock) : "latest" 16 | const pastBlocksToSearch = parseInt(taskArgs.pastblockstosearch) 17 | 18 | const signer = await ethers.getSigner() 19 | const linkTokenAddress = networks[network.name]["linkToken"] 20 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 21 | const donId = networks[network.name]["donId"] 22 | const txOptions = { confirmations: networks[network.name].confirmations } 23 | 24 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 25 | await sm.initialize() 26 | 27 | const requestCommitments = [] 28 | for (const requestId of requestIdsToTimeout) { 29 | try { 30 | const requestCommitment = await fetchRequestCommitment({ 31 | requestId, 32 | provider: new providers.JsonRpcProvider(networks[network.name].url), 33 | functionsRouterAddress, 34 | donId, 35 | toBlock, 36 | pastBlocksToSearch, 37 | }) 38 | console.log(`Fetched commitment for request ID ${requestId}`) 39 | if (requestCommitment.timeoutTimestamp < BigInt(Math.round(Date.now() / 1000))) { 40 | requestCommitments.push(requestCommitment) 41 | } else { 42 | console.log(`Request ID ${requestId} has not expired yet (skipping)`) 43 | } 44 | } catch (error) { 45 | console.log(`Failed to fetch commitment for request ID ${requestId} (skipping): ${error}`) 46 | } 47 | } 48 | 49 | if (requestCommitments.length > 0) { 50 | await sm.timeoutRequests({ 51 | requestCommitments, 52 | txOptions, 53 | }) 54 | console.log("Requests successfully timed out") 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /tasks/Functions-billing/transfer.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | 3 | const { networks } = require("../../networks") 4 | const utils = require("../utils") 5 | 6 | task("functions-sub-transfer", "Request ownership of an Functions subscription be transferred to a new address") 7 | .addParam("subid", "Subscription ID") 8 | .addParam("newowner", "Address of the new owner") 9 | .setAction(async (taskArgs) => { 10 | const subscriptionId = parseInt(taskArgs.subid) 11 | const newOwner = taskArgs.newowner 12 | const confirmations = networks[network.name].confirmations 13 | const txOptions = { confirmations } 14 | 15 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 16 | const linkTokenAddress = networks[network.name]["linkToken"] 17 | 18 | const signer = (await ethers.getSigners())[0] // First wallet. 19 | 20 | await utils.prompt( 21 | `\nTransferring the subscription to a new owner will require generating a new signature for encrypted secrets. 22 | Any previous encrypted secrets will no longer work with subscription ID ${subscriptionId} and must be regenerated by the new owner.` 23 | ) 24 | 25 | const sm = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 26 | await sm.initialize() 27 | 28 | console.log(`\nRequesting transfer of subscription ${subscriptionId} to new owner ${newOwner}`) 29 | const requestTransferTx = await sm.requestSubscriptionTransfer({ subscriptionId, newOwner, txOptions }) 30 | console.log( 31 | `Transfer request completed in Tx: ${requestTransferTx.transactionHash}\nAccount ${newOwner} needs to accept transfer for it to complete.` 32 | ) 33 | }) 34 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/buildOffchainSecrets.js: -------------------------------------------------------------------------------- 1 | const { SecretsManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | const fs = require("fs") 4 | const path = require("path") 5 | const process = require("process") 6 | 7 | task( 8 | "functions-build-offchain-secrets", 9 | "Builds an off-chain secrets object that can be uploaded and referenced via URL" 10 | ) 11 | .addOptionalParam( 12 | "output", 13 | "Output JSON file name (defaults to offchain-encrypted-secrets.json)", 14 | "offchain-encrypted-secrets.json", 15 | types.string 16 | ) 17 | .addOptionalParam( 18 | "configpath", 19 | "Path to Functions request config file", 20 | `${__dirname}/../../Functions-request-config.js`, 21 | types.string 22 | ) 23 | .setAction(async (taskArgs) => { 24 | const signer = await ethers.getSigner() 25 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 26 | const donId = networks[network.name]["donId"] 27 | 28 | const secretsManager = new SecretsManager({ 29 | signer, 30 | functionsRouterAddress, 31 | donId, 32 | }) 33 | await secretsManager.initialize() 34 | 35 | // Get the secrets object from Functions-request-config.js or other specific request config. 36 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 37 | ? taskArgs.configpath 38 | : path.join(process.cwd(), taskArgs.configpath)) 39 | 40 | if (!requestConfig.secrets || requestConfig.secrets.length === 0) { 41 | console.log("No secrets found in the request config.") 42 | return 43 | } 44 | 45 | const outputfile = taskArgs.output 46 | console.log(`\nEncrypting secrets and writing to JSON file '${outputfile}'...`) 47 | 48 | const encryptedSecretsObj = await secretsManager.encryptSecrets(requestConfig.secrets) 49 | fs.writeFileSync(outputfile, JSON.stringify(encryptedSecretsObj)) 50 | 51 | console.log(`\nWrote offchain secrets file to '${outputfile}'.`) 52 | }) 53 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/checkUpkeep.js: -------------------------------------------------------------------------------- 1 | task("functions-check-upkeep", "Checks if checkUpkeep returns true for an Automation compatible contract") 2 | .addParam("contract", "Address of the contract to check") 3 | .addOptionalParam( 4 | "data", 5 | "Hex string representing bytes that are passed to the checkUpkeep function (defaults to empty bytes)" 6 | ) 7 | .setAction(async (taskArgs) => { 8 | const checkData = taskArgs.data ?? [] 9 | 10 | console.log( 11 | `Checking if upkeep is required for Automation consumer contract ${taskArgs.contract} on network ${network.name}` 12 | ) 13 | const autoConsumerContractFactory = await ethers.getContractFactory("AutomatedFunctionsConsumer") 14 | const autoConsumerContract = await autoConsumerContractFactory.attach(taskArgs.contract) 15 | 16 | const checkUpkeep = await autoConsumerContract.checkUpkeep(checkData) 17 | 18 | console.log(`\nUpkeep needed: ${checkUpkeep[0]}\nPerform data: ${checkUpkeep[1]}`) 19 | }) 20 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/deployAutoConsumer.js: -------------------------------------------------------------------------------- 1 | const { SubscriptionManager } = require("@chainlink/functions-toolkit") 2 | 3 | const { types } = require("hardhat/config") 4 | const { networks } = require("../../networks") 5 | 6 | task("functions-deploy-auto-consumer", "Deploys the AutomatedFunctionsConsumer contract") 7 | .addParam("subid", "Billing subscription ID used to pay for Functions requests") 8 | .addOptionalParam("verify", "Set to true to verify consumer contract", false, types.boolean) 9 | .addOptionalParam( 10 | "configpath", 11 | "Path to Functions request config file", 12 | `${__dirname}/../../Functions-request-config.js`, 13 | types.string 14 | ) 15 | .setAction(async (taskArgs) => { 16 | console.log("\n__Compiling Contracts__") 17 | await run("compile") 18 | 19 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 20 | const donId = networks[network.name]["donId"] 21 | const donIdBytes32 = hre.ethers.utils.formatBytes32String(donId) 22 | const signer = await ethers.getSigner() 23 | const linkTokenAddress = networks[network.name]["linkToken"] 24 | const txOptions = { confirmations: networks[network.name].confirmations } 25 | 26 | const subscriptionId = taskArgs.subid 27 | 28 | // Initialize SubscriptionManager 29 | const subManager = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 30 | await subManager.initialize() 31 | 32 | console.log(`Deploying AutomatedFunctionsConsumer contract to ${network.name}`) 33 | const autoConsumerContractFactory = await ethers.getContractFactory("AutomatedFunctionsConsumer") 34 | const autoConsumerContract = await autoConsumerContractFactory.deploy(functionsRouterAddress, donIdBytes32) 35 | 36 | console.log(`\nWaiting 1 block for transaction ${autoConsumerContract.deployTransaction.hash} to be confirmed...`) 37 | await autoConsumerContract.deployTransaction.wait(1) 38 | 39 | const consumerAddress = autoConsumerContract.address 40 | 41 | console.log(`\nAdding ${consumerAddress} to subscription ${subscriptionId}...`) 42 | const addConsumerTx = await subManager.addConsumer({ subscriptionId, consumerAddress, txOptions }) 43 | console.log(`\nAdded consumer contract ${consumerAddress} in Tx: ${addConsumerTx.transactionHash}`) 44 | 45 | const verifyContract = taskArgs.verify 46 | if ( 47 | network.name !== "localFunctionsTestnet" && 48 | verifyContract && 49 | !!networks[network.name].verifyApiKey && 50 | networks[network.name].verifyApiKey !== "UNSET" 51 | ) { 52 | try { 53 | console.log(`\nVerifying contract ${consumerAddress}...`) 54 | await autoConsumerContract.deployTransaction.wait(Math.max(6 - networks[network.name].confirmations, 0)) 55 | await run("verify:verify", { 56 | address: consumerAddress, 57 | constructorArguments: [functionsRouterAddress, donIdBytes32], 58 | }) 59 | console.log("Contract verified") 60 | } catch (error) { 61 | if (!error.message.includes("Already Verified")) { 62 | console.log("Error verifying contract. Delete the build folder and try again.") 63 | console.log(error) 64 | } else { 65 | console.log("Contract already verified") 66 | } 67 | } 68 | } else if (verifyContract && network.name !== "localFunctionsTestnet") { 69 | console.log("\nBlockchain explorer API key is missing. Skipping contract verification...") 70 | } 71 | 72 | console.log(`\nAutomatedFunctionsConsumer contract deployed to ${consumerAddress} on ${network.name}`) 73 | }) 74 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/deployConsumer.js: -------------------------------------------------------------------------------- 1 | const { types } = require("hardhat/config") 2 | const { networks } = require("../../networks") 3 | 4 | task("functions-deploy-consumer", "Deploys the FunctionsConsumer contract") 5 | .addOptionalParam("verify", "Set to true to verify contract", false, types.boolean) 6 | .setAction(async (taskArgs) => { 7 | console.log(`Deploying FunctionsConsumer contract to ${network.name}`) 8 | 9 | const functionsRouter = networks[network.name]["functionsRouter"] 10 | const donIdBytes32 = hre.ethers.utils.formatBytes32String(networks[network.name]["donId"]) 11 | 12 | console.log("\n__Compiling Contracts__") 13 | await run("compile") 14 | 15 | const overrides = {} 16 | // If specified, use the gas price from the network config instead of Ethers estimated price 17 | if (networks[network.name].gasPrice) { 18 | overrides.gasPrice = networks[network.name].gasPrice 19 | } 20 | // If specified, use the nonce from the network config instead of automatically calculating it 21 | if (networks[network.name].nonce) { 22 | overrides.nonce = networks[network.name].nonce 23 | } 24 | 25 | const consumerContractFactory = await ethers.getContractFactory("FunctionsConsumer") 26 | const consumerContract = await consumerContractFactory.deploy(functionsRouter, donIdBytes32, overrides) 27 | 28 | console.log( 29 | `\nWaiting ${networks[network.name].confirmations} blocks for transaction ${ 30 | consumerContract.deployTransaction.hash 31 | } to be confirmed...` 32 | ) 33 | await consumerContract.deployTransaction.wait(networks[network.name].confirmations) 34 | 35 | console.log("\nDeployed FunctionsConsumer contract to:", consumerContract.address) 36 | 37 | if (network.name === "localFunctionsTestnet") { 38 | return 39 | } 40 | 41 | const verifyContract = taskArgs.verify 42 | if ( 43 | network.name !== "localFunctionsTestnet" && 44 | verifyContract && 45 | !!networks[network.name].verifyApiKey && 46 | networks[network.name].verifyApiKey !== "UNSET" 47 | ) { 48 | try { 49 | console.log("\nVerifying contract...") 50 | await run("verify:verify", { 51 | address: consumerContract.address, 52 | constructorArguments: [functionsRouter, donIdBytes32], 53 | }) 54 | console.log("Contract verified") 55 | } catch (error) { 56 | if (!error.message.includes("Already Verified")) { 57 | console.log( 58 | "Error verifying contract. Ensure you are waiting for enough confirmation blocks, delete the build folder and try again." 59 | ) 60 | console.log(error) 61 | } else { 62 | console.log("Contract already verified") 63 | } 64 | } 65 | } else if (verifyContract && network.name !== "localFunctionsTestnet") { 66 | console.log("\nScanner API key is missing. Skipping contract verification...") 67 | } 68 | 69 | console.log(`\nFunctionsConsumer contract deployed to ${consumerContract.address} on ${network.name}`) 70 | }) 71 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/index.js: -------------------------------------------------------------------------------- 1 | exports.readResultAndError = require("./readResultAndError.js") 2 | exports.requestData = require("./request.js") 3 | exports.deployConsumer = require("./deployConsumer.js") 4 | exports.deployAutoConsumer = require("./deployAutoConsumer.js") 5 | exports.setDonId = require("./setDonId.js") 6 | exports.buildOffchainSecrets = require("./buildOffchainSecrets.js") 7 | exports.checkUpkeep = require("./checkUpkeep.js") 8 | exports.performUpkeep = require("./performManualUpkeep.js") 9 | exports.setAutoRequest = require("./setAutoRequest.js") 10 | exports.uploadSecretsToDon = require("./uploadSecretsToDon.js") 11 | exports.listDonSecrets = require("./listDonSecrets.js") 12 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/listDonSecrets.js: -------------------------------------------------------------------------------- 1 | const { SecretsManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | 4 | task("functions-list-don-secrets", "Displays encrypted secrets hosted on the DON").setAction(async (taskArgs) => { 5 | const signer = await ethers.getSigner() 6 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 7 | const donId = networks[network.name]["donId"] 8 | 9 | const gatewayUrls = networks[network.name]["gatewayUrls"] 10 | if (!gatewayUrls || gatewayUrls.length === 0) { 11 | throw Error(`No gatewayUrls found for ${network.name} network.`) 12 | } 13 | 14 | const secretsManager = new SecretsManager({ 15 | signer, 16 | functionsRouterAddress, 17 | donId, 18 | }) 19 | await secretsManager.initialize() 20 | 21 | const { result } = await secretsManager.listDONHostedEncryptedSecrets(gatewayUrls) 22 | console.log(`\nYour encrypted secrets currently hosted on DON ${donId}`) 23 | console.log("\n\nGateway:", result.gatewayUrl) 24 | let i = 0 25 | result.nodeResponses.forEach((nodeResponse) => { 26 | console.log(`\nNode Response #${i}`) 27 | i++ 28 | if (nodeResponse.rows) { 29 | nodeResponse.rows.forEach((row) => { 30 | console.log(row) 31 | }) 32 | } else { 33 | console.log("No encrypted secrets found") 34 | } 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/performManualUpkeep.js: -------------------------------------------------------------------------------- 1 | const { networks } = require("../../networks") 2 | 3 | task("functions-perform-upkeep", "Manually call performUpkeep in an Automation compatible contract") 4 | .addParam("contract", "Address of the contract to call") 5 | .addOptionalParam( 6 | "data", 7 | "Hex string representing bytes that are passed to the performUpkeep function (defaults to empty bytes)" 8 | ) 9 | .setAction(async (taskArgs) => { 10 | // A manual gas limit is required as the gas limit estimated by Ethers is not always accurate 11 | const overrides = { 12 | gasLimit: 1000000, 13 | gasPrice: networks[network.name].gasPrice, 14 | } 15 | 16 | // Call performUpkeep 17 | const performData = taskArgs.data ?? [] 18 | 19 | const autoConsumerContractFactory = await ethers.getContractFactory("AutomatedFunctionsConsumer") 20 | const autoConsumerContract = await autoConsumerContractFactory.attach(taskArgs.contract) 21 | 22 | console.log( 23 | `\nCalling performUpkeep for Automation consumer contract ${taskArgs.contract} on network ${network.name}${ 24 | taskArgs.data ? ` with data ${performData}` : "" 25 | }` 26 | ) 27 | const performUpkeepTx = await autoConsumerContract.performUpkeep(performData, overrides) 28 | 29 | console.log( 30 | `\nWaiting ${networks[network.name].confirmations} blocks for transaction ${ 31 | performUpkeepTx.hash 32 | } to be confirmed...` 33 | ) 34 | const events = (await performUpkeepTx.wait(networks[network.name].confirmations)).events 35 | 36 | const requestRevertedWithErrorMsg = events.find((e) => e.event === "RequestRevertedWithErrorMsg") 37 | if (requestRevertedWithErrorMsg) { 38 | console.log(`\nRequest reverted with error message: ${requestRevertedWithErrorMsg.args.reason}`) 39 | return 40 | } 41 | 42 | const requestRevertedWithoutErrorMsg = events.find((e) => e.event === "RequestRevertedWithoutErrorMsg") 43 | if (requestRevertedWithoutErrorMsg) { 44 | console.log( 45 | `\nRequest reverted without error message. Ensure your request has been set correctly, the subscription is funded and the consumer contract is authorized.\n(Raw data: ${requestRevertedWithoutErrorMsg.data})` 46 | ) 47 | return 48 | } 49 | 50 | const reqId = await autoConsumerContract.s_lastRequestId() 51 | console.log("\nLast request ID received by the Automation Consumer Contract...", reqId) 52 | }) 53 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/readResultAndError.js: -------------------------------------------------------------------------------- 1 | const { decodeResult } = require("@chainlink/functions-toolkit") 2 | const path = require("path") 3 | const process = require("process") 4 | 5 | task( 6 | "functions-read", 7 | "Reads the latest response (or error) returned to a FunctionsConsumer or AutomatedFunctionsConsumer consumer contract" 8 | ) 9 | .addParam("contract", "Address of the consumer contract to read") 10 | .addOptionalParam( 11 | "configpath", 12 | "Path to Functions request config file", 13 | `${__dirname}/../../Functions-request-config.js`, 14 | types.string 15 | ) 16 | .setAction(async (taskArgs) => { 17 | console.log(`Reading data from Functions consumer contract ${taskArgs.contract} on network ${network.name}`) 18 | const consumerContractFactory = await ethers.getContractFactory("FunctionsConsumer") 19 | const consumerContract = await consumerContractFactory.attach(taskArgs.contract) 20 | 21 | let latestError = await consumerContract.s_lastError() 22 | if (latestError.length > 0 && latestError !== "0x") { 23 | const errorString = Buffer.from(latestError.slice(2), "hex").toString() 24 | console.log(`\nOn-chain error message: ${errorString}`) 25 | } 26 | 27 | let latestResponse = await consumerContract.s_lastResponse() 28 | if (latestResponse.length > 0 && latestResponse !== "0x") { 29 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 30 | ? taskArgs.configpath 31 | : path.join(process.cwd(), taskArgs.configpath)) 32 | const decodedResult = decodeResult(latestResponse, requestConfig.expectedReturnType).toString() 33 | console.log(`\nOn-chain response represented as a hex string: ${latestResponse}\n${decodedResult}`) 34 | } else if (latestResponse == "0x") { 35 | console.log("Empty response: ", latestResponse) 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/request.js: -------------------------------------------------------------------------------- 1 | const { 2 | SubscriptionManager, 3 | SecretsManager, 4 | createGist, 5 | deleteGist, 6 | simulateScript, 7 | decodeResult, 8 | ResponseListener, 9 | Location, 10 | FulfillmentCode, 11 | } = require("@chainlink/functions-toolkit") 12 | const { networks } = require("../../networks") 13 | const utils = require("../utils") 14 | const chalk = require("chalk") 15 | const path = require("path") 16 | const process = require("process") 17 | 18 | task("functions-request", "Initiates an on-demand request from a Functions consumer contract") 19 | .addParam("contract", "Address of the consumer contract to call") 20 | .addParam("subid", "Billing subscription ID used to pay for the request") 21 | .addOptionalParam( 22 | "simulate", 23 | "Flag indicating if source JS should be run locally before making an on-chain request", 24 | true, 25 | types.boolean 26 | ) 27 | .addOptionalParam( 28 | "callbackgaslimit", 29 | "Maximum amount of gas that can be used to call fulfillRequest in the consumer contract", 30 | 100_000, 31 | types.int 32 | ) 33 | .addOptionalParam( 34 | "slotid", 35 | "Slot ID to use for uploading DON hosted secrets. If the slot is already in use, the existing encrypted secrets will be overwritten.", 36 | 0, 37 | types.int 38 | ) 39 | .addOptionalParam("requestgaslimit", "Gas limit for calling the sendRequest function", 1_500_000, types.int) 40 | .addOptionalParam( 41 | "configpath", 42 | "Path to Functions request config file", 43 | `${__dirname}/../../Functions-request-config.js`, 44 | types.string 45 | ) 46 | .setAction(async (taskArgs, hre) => { 47 | // Get the required parameters 48 | const contractAddr = taskArgs.contract 49 | const subscriptionId = parseInt(taskArgs.subid) 50 | const slotId = parseInt(taskArgs.slotid) 51 | const callbackGasLimit = parseInt(taskArgs.callbackgaslimit) 52 | 53 | // Attach to the FunctionsConsumer contract 54 | const consumerFactory = await ethers.getContractFactory("FunctionsConsumer") 55 | const consumerContract = consumerFactory.attach(contractAddr) 56 | 57 | // Get requestConfig from the specified config file 58 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 59 | ? taskArgs.configpath 60 | : path.join(process.cwd(), taskArgs.configpath)) 61 | 62 | // Simulate the request 63 | if (taskArgs.simulate) { 64 | const { responseBytesHexstring, errorString } = await simulateScript(requestConfig) 65 | if (responseBytesHexstring) { 66 | console.log( 67 | `\nResponse returned by script during local simulation: ${decodeResult( 68 | responseBytesHexstring, 69 | requestConfig.expectedReturnType 70 | ).toString()}\n` 71 | ) 72 | } 73 | if (errorString) { 74 | console.log(`\nError returned by simulated script:\n${errorString}\n`) 75 | } 76 | 77 | console.log("Local simulation of source code completed...") 78 | } 79 | 80 | // Initialize the subscription manager 81 | const signer = await ethers.getSigner() 82 | const linkTokenAddress = networks[network.name]["linkToken"] 83 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 84 | const subManager = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 85 | await subManager.initialize() 86 | 87 | // Initialize the secrets manager 88 | const donId = networks[network.name]["donId"] 89 | const secretsManager = new SecretsManager({ signer, functionsRouterAddress, donId }) 90 | await secretsManager.initialize() 91 | 92 | // Validate the consumer contract has been authorized to use the subscription 93 | const subInfo = await subManager.getSubscriptionInfo(subscriptionId) 94 | if (!subInfo.consumers.map((c) => c.toLowerCase()).includes(contractAddr.toLowerCase())) { 95 | throw Error(`Consumer contract ${contractAddr} has not been added to subscription ${subscriptionId}`) 96 | } 97 | 98 | // Estimate the cost of the request fulfillment 99 | const { gasPrice } = await hre.ethers.provider.getFeeData() 100 | const gasPriceWei = BigInt(Math.ceil(hre.ethers.utils.formatUnits(gasPrice, "wei").toString())) 101 | const estimatedCostJuels = await subManager.estimateFunctionsRequestCost({ 102 | donId, 103 | subscriptionId, 104 | callbackGasLimit, 105 | gasPriceWei, 106 | }) 107 | 108 | // Ensure that the subscription has a sufficient balance 109 | const estimatedCostLink = hre.ethers.utils.formatUnits(estimatedCostJuels, 18) 110 | const subBalanceLink = hre.ethers.utils.formatUnits(subInfo.balance, 18) 111 | if (subInfo.balance <= estimatedCostJuels) { 112 | throw Error( 113 | `Subscription ${subscriptionId} does not have sufficient funds. The estimated cost is ${estimatedCostLink} LINK, but the subscription only has ${subBalanceLink} LINK.` 114 | ) 115 | } 116 | 117 | // Print the estimated cost of the Functions request in LINK & confirm before initiating the request on-chain 118 | await utils.prompt( 119 | `If the request's callback uses all ${utils.numberWithCommas( 120 | callbackGasLimit 121 | )} gas, this request will charge the subscription an estimated ${chalk.blue(estimatedCostLink + " LINK")}` 122 | ) 123 | 124 | // Handle encrypted secrets 125 | let encryptedSecretsReference = [] 126 | let gistUrl 127 | if ( 128 | network.name !== "localFunctionsTestnet" && 129 | requestConfig.secrets && 130 | Object.keys(requestConfig.secrets).length > 0 131 | ) { 132 | const encryptedSecrets = await secretsManager.encryptSecrets(requestConfig.secrets) 133 | 134 | switch (requestConfig.secretsLocation) { 135 | case Location.Inline: 136 | throw Error("Inline encrypted secrets are not supported for requests.") 137 | 138 | case Location.Remote: 139 | if (!process.env["GITHUB_API_TOKEN"]) { 140 | throw Error("GITHUB_API_TOKEN environment variable is required to upload Remote encrypted secrets.") 141 | } 142 | gistUrl = await createGist(process.env["GITHUB_API_TOKEN"], JSON.stringify(encryptedSecrets)) 143 | encryptedSecretsReference = await secretsManager.encryptSecretsUrls([gistUrl]) 144 | break 145 | 146 | case Location.DONHosted: 147 | const { version } = await secretsManager.uploadEncryptedSecretsToDON({ 148 | encryptedSecretsHexstring: encryptedSecrets.encryptedSecrets, 149 | gatewayUrls: networks[network.name]["gatewayUrls"], 150 | slotId, 151 | minutesUntilExpiration: 5, 152 | }) 153 | encryptedSecretsReference = await secretsManager.buildDONHostedEncryptedSecretsReference({ 154 | slotId, 155 | version, 156 | }) 157 | break 158 | 159 | default: 160 | throw Error("Invalid secretsLocation in request config") 161 | } 162 | } else { 163 | requestConfig.secretsLocation = Location.Remote // Default to Remote if no secrets are used 164 | } 165 | 166 | // Instantiate response listener 167 | const responseListener = new ResponseListener({ 168 | provider: hre.ethers.provider, 169 | functionsRouterAddress, 170 | }) 171 | 172 | // Initiate the request 173 | const spinner = utils.spin() 174 | spinner.start( 175 | `Waiting for transaction for FunctionsConsumer contract ${contractAddr} on network ${network.name} to be confirmed...` 176 | ) 177 | // Use manual gas limits for the request transaction since estimated gas limit is not always accurate, 178 | // and can vary significantly based on network. 179 | higherGasNetworks = ["optimismSepolia", "baseSepolia"] // L2s appear to need more request gas. 180 | const requestGasLimit = higherGasNetworks.includes(network.name) ? 1_750_000 : taskArgs.requestgaslimit 181 | const overrides = { 182 | gasLimit: requestGasLimit, 183 | } 184 | // If specified, use the gas price from the network config instead of Ethers estimated price 185 | if (networks[network.name].gasPrice) { 186 | overrides.gasPrice = networks[network.name].gasPrice 187 | } 188 | // If specified, use the nonce from the network config instead of automatically calculating it 189 | if (networks[network.name].nonce) { 190 | overrides.nonce = networks[network.name].nonce 191 | } 192 | const requestTx = await consumerContract.sendRequest( 193 | requestConfig.source, 194 | requestConfig.secretsLocation, 195 | encryptedSecretsReference, 196 | requestConfig.args ?? [], 197 | requestConfig.bytesArgs ?? [], 198 | subscriptionId, 199 | callbackGasLimit, 200 | overrides 201 | ) 202 | const requestTxReceipt = await requestTx.wait(1) 203 | if (network.name !== "localFunctionsTestnet") { 204 | spinner.info( 205 | `Transaction confirmed, see ${ 206 | utils.getEtherscanURL(network.config.chainId) + "tx/" + requestTx.hash 207 | } for more details.` 208 | ) 209 | } 210 | 211 | // Listen for fulfillment 212 | spinner.start( 213 | `Functions request has been initiated in transaction ${requestTx.hash} with request ID ${requestTxReceipt.events[2].args.id}. Note the request ID may change if a re-org occurs, but the transaction hash will remain constant.\nWaiting for fulfillment from the Decentralized Oracle Network...\n` 214 | ) 215 | 216 | try { 217 | // localFunctionsTestnet needs 0 or 1 confirmations to work correctly as it's local. 218 | // If on live testnet or mainnet, setting to undefined then uses the functions-toolkit default of 2 confirmations. 219 | const NUM_CONFIRMATIONS = network.name === "localFunctionsTestnet" ? 1 : undefined 220 | 221 | // Get response data 222 | const { requestId, totalCostInJuels, responseBytesHexstring, errorString, fulfillmentCode } = 223 | await responseListener.listenForResponseFromTransaction(requestTx.hash, undefined, NUM_CONFIRMATIONS, undefined) 224 | 225 | switch (fulfillmentCode) { 226 | case FulfillmentCode.FULFILLED: 227 | if (responseBytesHexstring !== "0x") { 228 | spinner.succeed( 229 | `Request ${requestId} fulfilled!\nResponse has been sent to consumer contract: ${decodeResult( 230 | responseBytesHexstring, 231 | requestConfig.expectedReturnType 232 | ).toString()}\n` 233 | ) 234 | } else if (errorString.length > 0) { 235 | spinner.warn(`Request ${requestId} fulfilled with error: ${errorString}\n`) 236 | } else { 237 | spinner.succeed(`Request ${requestId} fulfilled with empty response data.\n`) 238 | } 239 | const linkCost = hre.ethers.utils.formatUnits(totalCostInJuels, 18) 240 | console.log(`Total request cost: ${chalk.blue(linkCost + " LINK")}`) 241 | break 242 | 243 | case FulfillmentCode.USER_CALLBACK_ERROR: 244 | spinner.fail( 245 | "Error encountered when calling consumer contract callback.\nEnsure the fulfillRequest function in FunctionsConsumer is correct and the --callbackgaslimit is sufficient." 246 | ) 247 | break 248 | 249 | case FulfillmentCode.COST_EXCEEDS_COMMITMENT: 250 | spinner.fail(`Request ${requestId} failed due to a gas price spike when attempting to respond.`) 251 | break 252 | 253 | default: 254 | spinner.fail( 255 | `Request ${requestId} failed with fulfillment code: ${fulfillmentCode}. Please contact Chainlink support.` 256 | ) 257 | } 258 | } catch (error) { 259 | spinner.fail("Request fulfillment was not received within 5 minute response period.") 260 | throw error 261 | } finally { 262 | // Clean up the gist if it was created 263 | if (gistUrl) { 264 | const successfulDeletion = await deleteGist(process.env["GITHUB_API_TOKEN"], gistUrl) 265 | if (!successfulDeletion) { 266 | console.log(`Failed to delete gist at ${gistUrl}. Please delete manually.`) 267 | } 268 | } 269 | } 270 | }) 271 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/setAutoRequest.js: -------------------------------------------------------------------------------- 1 | const { 2 | buildRequestCBOR, 3 | decodeResult, 4 | SecretsManager, 5 | simulateScript, 6 | SubscriptionManager, 7 | Location, 8 | } = require("@chainlink/functions-toolkit") 9 | 10 | const { types } = require("hardhat/config") 11 | const { networks } = require("../../networks") 12 | const path = require("path") 13 | const process = require("process") 14 | 15 | task( 16 | "functions-set-auto-request", 17 | "sets the CBOR-encoded Functions request in a deployed AutomatedFunctionsConsumer contract" 18 | ) 19 | .addParam("contract", "Address of the consumer contract") 20 | .addParam("subid", "Billing subscription ID used to pay for Functions requests", undefined, types.int) 21 | .addOptionalParam( 22 | "slotid", 23 | "Storage slot number 0 or higher. If the slotid is already in use, the existing secrets for that slotid will be overwritten." 24 | ) 25 | .addOptionalParam("interval", "Update interval in seconds for Automation to call performUpkeep", 300, types.int) 26 | .addOptionalParam( 27 | "ttl", 28 | "time to live - minutes until the secrets hosted on the DON expire. Defaults to 120m, and must be minimum 5m", 29 | 120, 30 | types.int 31 | ) 32 | .addOptionalParam( 33 | "gaslimit", 34 | "Maximum amount of gas that can be used to call fulfillRequest in the consumer contract", 35 | 250000, 36 | types.int 37 | ) 38 | .addOptionalParam( 39 | "simulate", 40 | "Flag indicating if simulation should be run before making an on-chain request", 41 | true, 42 | types.boolean 43 | ) 44 | .addOptionalParam( 45 | "configpath", 46 | "Path to Functions request config file", 47 | `${__dirname}/../../Functions-request-config.js`, 48 | types.string 49 | ) 50 | .setAction(async (taskArgs) => { 51 | await setAutoRequest(taskArgs.contract, taskArgs) 52 | }) 53 | 54 | const setAutoRequest = async (contract, taskArgs) => { 55 | const subscriptionId = taskArgs.subid 56 | const callbackGasLimit = taskArgs.gaslimit 57 | 58 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 59 | const donId = networks[network.name]["donId"] 60 | const signer = await ethers.getSigner() 61 | const linkTokenAddress = networks[network.name]["linkToken"] 62 | 63 | // Initialize SubscriptionManager 64 | const subManager = new SubscriptionManager({ signer, linkTokenAddress, functionsRouterAddress }) 65 | await subManager.initialize() 66 | 67 | // Validate callbackGasLimit 68 | const { gasPrice } = await hre.ethers.provider.getFeeData() 69 | const gasPriceWei = BigInt(Math.ceil(hre.ethers.utils.formatUnits(gasPrice, "wei").toString())) 70 | await subManager.estimateFunctionsRequestCost({ 71 | donId, 72 | subscriptionId, 73 | callbackGasLimit, 74 | gasPriceWei, 75 | }) 76 | 77 | // Check that consumer contract is added to subscription. 78 | const subInfo = await subManager.getSubscriptionInfo(subscriptionId) 79 | if (!subInfo.consumers.map((c) => c.toLowerCase()).includes(taskArgs.contract.toLowerCase())) { 80 | throw Error(`Consumer contract ${taskArgs.contract} has not been added to subscription ${subscriptionId}`) 81 | } 82 | 83 | const autoConsumerContractFactory = await ethers.getContractFactory("AutomatedFunctionsConsumer") 84 | const autoConsumerContract = await autoConsumerContractFactory.attach(contract) 85 | 86 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 87 | ? taskArgs.configpath 88 | : path.join(process.cwd(), taskArgs.configpath)) 89 | 90 | // Simulate the request 91 | if (taskArgs.simulate) { 92 | console.log("\nSimulating source script locally before making an on-chain request...") 93 | const { responseBytesHexstring, errorString } = await simulateScript(requestConfig) 94 | if (responseBytesHexstring) { 95 | console.log( 96 | `\nResponse returned by script during local simulation: ${decodeResult( 97 | responseBytesHexstring, 98 | requestConfig.expectedReturnType 99 | )}\n` 100 | ) 101 | } 102 | if (errorString) { 103 | console.log(`\nError returned by simulated script:\n${errorString}\n`) 104 | } 105 | 106 | console.log("Local simulation of source code completed...") 107 | } 108 | 109 | let encryptedSecretsReference 110 | let secretsLocation 111 | if (!requestConfig.secrets || Object.keys(requestConfig.secrets).length === 0) { 112 | console.log("\nNo secrets found in request config - proceeding without secrets...") 113 | } 114 | 115 | // Encrypt and upload secrets if present. 116 | if ( 117 | network.name !== "localFunctionsTestnet" && 118 | requestConfig.secrets && 119 | Object.keys(requestConfig.secrets).length > 0 120 | ) { 121 | if (requestConfig.secretsLocation !== Location.DONHosted) { 122 | throw Error( 123 | `\nThis task supports only DON-hosted secrets. The request config specifies ${ 124 | Location[requestConfig.secretsLocation] 125 | }.` 126 | ) 127 | } 128 | 129 | secretsLocation = requestConfig.secretsLocation 130 | 131 | console.log("\nEncrypting secrets and uploading to DON...") 132 | const secretsManager = new SecretsManager({ 133 | signer, 134 | functionsRouterAddress, 135 | donId, 136 | }) 137 | 138 | await secretsManager.initialize() 139 | const encryptedSecretsObj = await secretsManager.encryptSecrets(requestConfig.secrets) 140 | const minutesUntilExpiration = taskArgs.ttl 141 | const slotId = parseInt(taskArgs.slotid) 142 | 143 | if (isNaN(slotId)) { 144 | throw Error`\nSlotId missing. Please provide a slotId of 0 or higher, to upload encrypted secrets to the DON.` 145 | } 146 | 147 | const { version, success } = await secretsManager.uploadEncryptedSecretsToDON({ 148 | encryptedSecretsHexstring: encryptedSecretsObj.encryptedSecrets, 149 | gatewayUrls: networks[network.name]["gatewayUrls"], 150 | slotId, 151 | minutesUntilExpiration, 152 | }) 153 | 154 | if (!success) { 155 | throw Error("\nFailed to upload encrypted secrets to DON.") 156 | } 157 | 158 | console.log(`\nNow using DON-hosted secrets version ${version} in slot ${slotId}...`) 159 | encryptedSecretsReference = await secretsManager.buildDONHostedEncryptedSecretsReference({ 160 | slotId, 161 | version, 162 | }) 163 | } 164 | 165 | const functionsRequestCBOR = buildRequestCBOR({ 166 | codeLocation: requestConfig.codeLocation, 167 | codeLanguage: requestConfig.codeLanguage, 168 | source: requestConfig.source, 169 | args: requestConfig.args, 170 | secretsLocation, 171 | encryptedSecretsReference, 172 | }) 173 | 174 | console.log( 175 | `\nSetting the Functions request CBOR in AutomatedFunctionsConsumer contract ${contract} on ${network.name}` 176 | ) 177 | const setRequestTx = await autoConsumerContract.setRequest( 178 | taskArgs.subid, 179 | taskArgs.gaslimit, 180 | taskArgs.interval, 181 | functionsRequestCBOR 182 | ) 183 | 184 | console.log( 185 | `\nWaiting ${networks[network.name].confirmations} block for transaction ${setRequestTx.hash} to be confirmed...` 186 | ) 187 | await setRequestTx.wait(networks[network.name].confirmations) 188 | console.log("\nSet request Tx confirmed") 189 | } 190 | 191 | exports.setAutoRequest = setAutoRequest 192 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/setDonId.js: -------------------------------------------------------------------------------- 1 | const { networks } = require("../../networks") 2 | 3 | task( 4 | "functions-set-donid", 5 | "Updates the oracle address for a FunctionsConsumer consumer contract using the FunctionsOracle address from `network-config.js`" 6 | ) 7 | .addParam("contract", "Address of the consumer contract to update") 8 | .setAction(async (taskArgs) => { 9 | const donId = networks[network.name]["donId"] 10 | console.log(`Setting donId to ${donId} in Functions consumer contract ${taskArgs.contract} on ${network.name}`) 11 | const consumerContractFactory = await ethers.getContractFactory("FunctionsConsumer") 12 | const consumerContract = await consumerContractFactory.attach(taskArgs.contract) 13 | 14 | const donIdBytes32 = hre.ethers.utils.formatBytes32String(donId) 15 | const updateTx = await consumerContract.setDonId(donIdBytes32) 16 | 17 | console.log( 18 | `\nWaiting ${networks[network.name].confirmations} blocks for transaction ${updateTx.hash} to be confirmed...` 19 | ) 20 | await updateTx.wait(networks[network.name].confirmations) 21 | 22 | console.log(`\nUpdated donId to ${donId} for Functions consumer contract ${taskArgs.contract} on ${network.name}`) 23 | }) 24 | -------------------------------------------------------------------------------- /tasks/Functions-consumer/uploadSecretsToDon.js: -------------------------------------------------------------------------------- 1 | const { SecretsManager } = require("@chainlink/functions-toolkit") 2 | const { networks } = require("../../networks") 3 | const process = require("process") 4 | const path = require("path") 5 | 6 | task("functions-upload-secrets-don", "Encrypts secrets and uploads them to the DON") 7 | .addParam( 8 | "slotid", 9 | "Storage slot number 0 or higher - if the slotid is already in use, the existing secrets for that slotid will be overwritten" 10 | ) 11 | .addOptionalParam( 12 | "ttl", 13 | "Time to live - minutes until the secrets hosted on the DON expire (defaults to 10, and must be at least 5)", 14 | 10, 15 | types.int 16 | ) 17 | .addOptionalParam( 18 | "configpath", 19 | "Path to Functions request config file", 20 | `${__dirname}/../../Functions-request-config.js`, 21 | types.string 22 | ) 23 | .setAction(async (taskArgs) => { 24 | const signer = await ethers.getSigner() 25 | const functionsRouterAddress = networks[network.name]["functionsRouter"] 26 | const donId = networks[network.name]["donId"] 27 | 28 | const gatewayUrls = networks[network.name]["gatewayUrls"] 29 | 30 | const slotId = parseInt(taskArgs.slotid) 31 | const minutesUntilExpiration = taskArgs.ttl 32 | 33 | const secretsManager = new SecretsManager({ 34 | signer, 35 | functionsRouterAddress, 36 | donId, 37 | }) 38 | await secretsManager.initialize() 39 | 40 | // Get the secrets object from Functions-request-config.js or other specific request config. 41 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 42 | ? taskArgs.configpath 43 | : path.join(process.cwd(), taskArgs.configpath)) 44 | 45 | if (!requestConfig.secrets || requestConfig.secrets.length === 0) { 46 | console.log("No secrets found in the request config.") 47 | return 48 | } 49 | 50 | console.log("Encrypting secrets and uploading to DON...") 51 | const encryptedSecretsObj = await secretsManager.encryptSecrets(requestConfig.secrets) 52 | 53 | const { 54 | version, // Secrets version number (corresponds to timestamp when encrypted secrets were uploaded to DON) 55 | success, // Boolean value indicating if encrypted secrets were successfully uploaded to all nodes connected to the gateway 56 | } = await secretsManager.uploadEncryptedSecretsToDON({ 57 | encryptedSecretsHexstring: encryptedSecretsObj.encryptedSecrets, 58 | gatewayUrls, 59 | slotId, 60 | minutesUntilExpiration, 61 | }) 62 | 63 | console.log( 64 | `\nYou can now use slotId ${slotId} and version ${version} to reference the encrypted secrets hosted on the DON.` 65 | ) 66 | }) 67 | -------------------------------------------------------------------------------- /tasks/balance.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("ethers") 2 | 3 | const network = process.env.NETWORK 4 | const provider = ethers.getDefaultProvider(network) 5 | 6 | task("balance", "Prints an account's balance") 7 | .addParam("account", "The account's address") 8 | .setAction(async (taskArgs) => { 9 | const account = ethers.utils.getAddress(taskArgs.account) 10 | const balance = await provider.getBalance(account) 11 | 12 | console.log(ethers.utils.formatEther(balance), "ETH") 13 | }) 14 | 15 | module.exports = {} 16 | -------------------------------------------------------------------------------- /tasks/block-number.js: -------------------------------------------------------------------------------- 1 | task("block-number", "Prints the current block number", async (_, { ethers }) => { 2 | await ethers.provider.getBlockNumber().then((blockNumber) => { 3 | console.log("Current block number: " + blockNumber) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /tasks/index.js: -------------------------------------------------------------------------------- 1 | //exports.keepers = require('./automation') 2 | exports.FunctionsConsumer = require("./Functions-consumer") 3 | exports.FunctionsBilling = require("./Functions-billing") 4 | exports.balance = require("./balance") 5 | exports.blockNumber = require("./block-number") 6 | exports.simulateScript = require("./simulateScript") 7 | -------------------------------------------------------------------------------- /tasks/simulateScript.js: -------------------------------------------------------------------------------- 1 | const { simulateScript, decodeResult } = require("@chainlink/functions-toolkit") 2 | const path = require("path") 3 | const process = require("process") 4 | 5 | task("functions-simulate-script", "Executes the JavaScript source code locally") 6 | .addOptionalParam( 7 | "configpath", 8 | "Path to Functions request config file", 9 | `${__dirname}/../Functions-request-config.js`, 10 | types.string 11 | ) 12 | .setAction(async (taskArgs, hre) => { 13 | const requestConfig = require(path.isAbsolute(taskArgs.configpath) 14 | ? taskArgs.configpath 15 | : path.join(process.cwd(), taskArgs.configpath)) 16 | 17 | // Simulate the JavaScript execution locally 18 | const { responseBytesHexstring, errorString, capturedTerminalOutput } = await simulateScript(requestConfig) 19 | console.log(`${capturedTerminalOutput}\n`) 20 | if (responseBytesHexstring) { 21 | console.log( 22 | `Response returned by script during local simulation: ${decodeResult( 23 | responseBytesHexstring, 24 | requestConfig.expectedReturnType 25 | ).toString()}\n` 26 | ) 27 | } 28 | if (errorString) { 29 | console.log(`Error returned by simulated script:\n${errorString}\n`) 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /tasks/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("./network.js"), 3 | ...require("./price.js"), 4 | ...require("./prompt.js"), 5 | ...require("./spin.js"), 6 | ...require("./logger.js"), 7 | } 8 | -------------------------------------------------------------------------------- /tasks/utils/logger.js: -------------------------------------------------------------------------------- 1 | const { Console } = require("console") 2 | const { Transform } = require("stream") 3 | 4 | function table(input) { 5 | // @see https://stackoverflow.com/a/67859384 6 | const ts = new Transform({ 7 | transform(chunk, enc, cb) { 8 | cb(null, chunk) 9 | }, 10 | }) 11 | const logger = new Console({ stdout: ts }) 12 | logger.table(input) 13 | const table = (ts.read() || "").toString() 14 | let result = "" 15 | for (let row of table.split(/[\r\n]+/)) { 16 | let r = row.replace(/[^┬]*┬/, "┌") 17 | r = r.replace(/^├─*┼/, "├") 18 | r = r.replace(/│[^│]*/, "") 19 | r = r.replace(/^└─*┴/, "└") 20 | r = r.replace(/'/g, " ") 21 | result += `${r}\n` 22 | } 23 | console.log(result) 24 | } 25 | 26 | const logger = { table } 27 | 28 | module.exports = { 29 | logger, 30 | } 31 | -------------------------------------------------------------------------------- /tasks/utils/network.js: -------------------------------------------------------------------------------- 1 | const BASE_URLS = { 2 | 1: "https://etherscan.io/", 3 | 137: "https://polygonscan.com/", 4 | 43114: "https://snowtrace.io/", 5 | 80002: "https://api-amoy.polygonscan.com/api", 6 | 11155111: "https://sepolia.etherscan.io/", 7 | 43113: "https://testnet.snowtrace.io/", 8 | 421614: "https://sepolia.arbiscan.io/", 9 | 42161: "https://arbiscan.io/", 10 | 84532: "https://sepolia.basescan.org/", 11 | 11155420: "https://sepolia-optimistic.etherscan.io/", 12 | 8453: "https://basescan.org/", 13 | 10: "https://optimistic.etherscan.io/", 14 | 44787: "https://alfajores.celoscan.io/", 15 | 42220: "https://celoscan.io/", 16 | } 17 | 18 | /** 19 | * Returns the Etherscan API domain for a given chainId. 20 | * 21 | * @param chainId Ethereum chain ID 22 | */ 23 | function getEtherscanURL(chainId) { 24 | const idNotFound = !Object.keys(BASE_URLS).includes(chainId.toString()) 25 | if (idNotFound) { 26 | throw new Error("Invalid chain Id") 27 | } 28 | return BASE_URLS[chainId] 29 | } 30 | 31 | module.exports = { 32 | getEtherscanURL, 33 | } 34 | -------------------------------------------------------------------------------- /tasks/utils/price.js: -------------------------------------------------------------------------------- 1 | function numberWithCommas(x) { 2 | return x.toString().replace(/\B(? 11 | rl.question(query, (ans) => { 12 | rl.close() 13 | resolve(ans) 14 | }) 15 | ) 16 | } 17 | 18 | async function prompt(query) { 19 | if (!process.env.SKIP_PROMPTS) { 20 | if (query) console.log(`${query}\n`) 21 | const reply = await ask(`${chalk.green("Continue?")} Enter (y) Yes / (n) No\n`) 22 | if (reply.toLowerCase() !== "y" && reply.toLowerCase() !== "yes") { 23 | console.log("Aborted.") 24 | process.exit(1) 25 | } 26 | } 27 | } 28 | 29 | module.exports = { 30 | ask, 31 | prompt, 32 | } 33 | -------------------------------------------------------------------------------- /tasks/utils/spin.js: -------------------------------------------------------------------------------- 1 | const ora = require("ora") 2 | 3 | function spin(config = {}) { 4 | const spinner = ora({ spinner: "dots2", ...config }) 5 | spinner.start() 6 | return spinner 7 | } 8 | 9 | module.exports = { 10 | spin, 11 | } 12 | -------------------------------------------------------------------------------- /test/unit/FunctionsConsumer.spec.js: -------------------------------------------------------------------------------- 1 | // const { assert } = require("chai") 2 | // const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers") 3 | // const { network } = require("hardhat") 4 | 5 | describe("Functions Consumer Unit Tests", async function () { 6 | // We define a fixture to reuse the same setup in every test. 7 | // We use loadFixture to run this setup once, snapshot that state, 8 | // and reset Hardhat Network to that snapshot in every test. 9 | 10 | it("empty test", async () => { 11 | // TODO 12 | }) 13 | }) 14 | --------------------------------------------------------------------------------