├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── jest.config.ts ├── package.json ├── scripts ├── create-task-ad-board.ts ├── create-task-event-listener.ts ├── create-task-oracle.ts ├── create-task-with-secrets.ts └── forkAnvil.ts ├── test ├── advertising-board.test.ts ├── hello-world.test.ts └── utils │ ├── anvil-server.ts │ └── index.ts ├── tsconfig.json ├── web3-functions ├── advertising-board │ ├── index.ts │ ├── schema.json │ ├── storage.json │ └── userArgs.json ├── event-listener │ ├── index.ts │ ├── schema.json │ ├── storage.json │ └── userArgs.json ├── hello-world │ ├── index.ts │ ├── schema.json │ ├── storage.json │ └── userArgs.json ├── oracle │ ├── index.ts │ ├── schema.json │ ├── storage.json │ └── userArgs.json ├── private │ ├── .env.example │ ├── README.md │ ├── index.ts │ ├── onRun.js │ ├── schema.json │ └── userArgs.json ├── secrets │ ├── .env.example │ ├── index.ts │ ├── schema.json │ ├── storage.json │ └── userArgs.json └── storage │ ├── index.ts │ ├── schema.json │ ├── storage.json │ └── userArgs.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | PROVIDER_URLS="https://eth-mainnet.alchemyapi.io/v2/YOUR_ALCHEMY_ID" # your provider URLS seperated by comma 2 | PRIVATE_KEY="" ## Optional: Only needed if you wish to create a task from the CLI instead of the UI 3 | RPC="" ## PRC FOR ANVIL FORK -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | dist/ 3 | node_modules 4 | .tmp -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": ["eslint:recommended", "prettier"], 9 | "parserOptions": { 10 | "ecmaVersion": 12 11 | }, 12 | // Typescript config 13 | "overrides": [ 14 | { 15 | "files": ["*.ts"], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { "project": "./tsconfig.json" }, 18 | "plugins": ["@typescript-eslint", "prettier"], 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | "plugin:@typescript-eslint/eslint-recommended", 23 | "prettier" 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # local env variables 5 | .env 6 | .env.testing 7 | 8 | # hardhat 9 | artifacts 10 | cache 11 | deployments/localhost 12 | 13 | # cache 14 | .eslintcache 15 | 16 | # VS Code 17 | .vscode 18 | 19 | # macOS 20 | .DS_Store 21 | *.icloud 22 | 23 | dist 24 | .idea 25 | 26 | # redis 27 | dump.rdb 28 | 29 | # coverage 30 | coverage 31 | .nyc_output 32 | 33 | # typechain 34 | typechain 35 | 36 | test/contracts/types 37 | logs/ 38 | 39 | .eslintcache 40 | 41 | # web3 functions builds 42 | .tmp -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | cache 3 | dist 4 | node_modules 5 | .tmp -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web3 Functions Template 2 | Use this template to write, test and deploy Web3 Functions. 3 | 4 | ## What are Web3 Functions? 5 | Web3 Functions are decentralized cloud functions that work similarly to AWS Lambda or Google Cloud, just for web3. They enable developers to execute on-chain transactions based on arbitrary off-chain data (APIs / subgraphs, etc) & computation. These functions are written in Typescript, stored on IPFS and run by Gelato. 6 | 7 | ## Documentation 8 | 9 | You can find the official Web3 Functions documentation [here](https://docs.gelato.network/developer-services/web3-functions). 10 | 11 | ## Private Beta Restriction 12 | 13 | Web3 Functions are currently in private Beta and can only be used by whitelisted users. If you would like to be added to the waitlist, please reach out to the team on [Discord](https://discord.com/invite/ApbA39BKyJ) or apply using this [form](https://form.typeform.com/to/RrEiARiI). 14 | 15 | ## Table of Content 16 | 17 | - [What are Web3 Functions?](#what-are-web3-functions) 18 | - [Documentation](#documentation) 19 | - [Private Beta Restriction](#private-beta-restriction) 20 | - [Table of Content](#table-of-content) 21 | - [Project Setup](#project-setup) 22 | - [Write a Web3 Function](#write-a-web3-function) 23 | - [Test your web3 function](#test-your-web3-function) 24 | - [Use User arguments](#use-user-arguments) 25 | - [Use State / Storage](#use-state--storage) 26 | - [Use user secrets](#use-user-secrets) 27 | - [Deploy your Web3Function on IPFS](#deploy-your-web3function-on-ipfs) 28 | - [Create your Web3Function task](#create-your-web3function-task) 29 | - [More examples](#more-examples) 30 | - [Coingecko oracle](#coingecko-oracle) 31 | - [Event listener](#event-listener) 32 | - [Secrets](#secrets) 33 | - [Advertising Board](#advertising-board) 34 | 35 | 36 | ## Project Setup 37 | 1. Install project dependencies 38 | ``` 39 | yarn install 40 | ``` 41 | 42 | 2. Configure your local environment: 43 | - Copy `.env.example` to init your own `.env` file 44 | ``` 45 | cp .env.example .env 46 | ``` 47 | - Complete your `.env` file with your private settings 48 | ``` 49 | PROVIDER_URLS="" # your provider URLS seperated by comma (e.g. https://eth-mainnet.alchemyapi.io/v2/YOUR_ALCHEMY_ID,https://eth-goerli.alchemyapi.io/v2/YOUR_ALCHEMY_ID) 50 | 51 | PRIVATE_KEY="" # optional: only needed if you wish to create a task from the CLI instead of the UI 52 | ``` 53 | 54 | 55 | ## Write a Web3 Function 56 | 57 | - Go to `web3-functions/my-web3-function` 58 | - Write your Web3 Function logic within the `Web3Function.onRun` function. 59 | - Example: 60 | ```typescript 61 | import { Web3Function, Web3FunctionContext } from "@gelatonetwork/web3-functions-sdk"; 62 | import { Contract } from "@ethersproject/contracts"; 63 | import ky from "ky"; // we recommend using ky as axios doesn't support fetch by default 64 | 65 | const ORACLE_ABI = [ 66 | "function lastUpdated() external view returns(uint256)", 67 | "function updatePrice(uint256)", 68 | ]; 69 | 70 | Web3Function.onRun(async (context: Web3FunctionContext) => { 71 | const { userArgs, gelatoArgs, multiChainProvider } = context; 72 | 73 | const provider = multiChainProvider.default(); 74 | 75 | // Retrieve Last oracle update time 76 | const oracleAddress = "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da"; 77 | const oracle = new Contract(oracleAddress, ORACLE_ABI, provider); 78 | const lastUpdated = parseInt(await oracle.lastUpdated()); 79 | console.log(`Last oracle update: ${lastUpdated}`); 80 | 81 | // Check if it's ready for a new update 82 | const nextUpdateTime = lastUpdated + 300; // 5 min 83 | const timestamp = (await provider.getBlock("latest")).timestamp; 84 | console.log(`Next oracle update: ${nextUpdateTime}`); 85 | if (timestamp < nextUpdateTime) { 86 | return { canExec: false, message: `Time not elapsed` }; 87 | } 88 | 89 | // Get current price on coingecko 90 | const currency = "ethereum"; 91 | const priceData: any = await ky 92 | .get( 93 | `https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=usd`, 94 | { timeout: 5_000, retry: 0 } 95 | ) 96 | .json(); 97 | price = Math.floor(priceData[currency].usd); 98 | console.log(`Updating price: ${price}`); 99 | 100 | // Return execution call data 101 | return { 102 | canExec: true, 103 | callData: [{to: oracleAddress, data: oracle.interface.encodeFunctionData("updatePrice", [price]}]), 104 | }; 105 | }); 106 | ``` 107 | - Each Web3 Function has a `schema.json` file to specify the runtime configuration. In later versions you will have more optionality to define what resources your Web3 Function requires. 108 | ```json 109 | { 110 | "web3FunctionVersion": "2.0.0", 111 | "runtime": "js-1.0", 112 | "memory": 128, 113 | "timeout": 30, 114 | "userArgs": {} 115 | } 116 | ``` 117 | 118 | 119 | ## Test your web3 function 120 | 121 | ### Calling your web3 function 122 | 123 | - Use `npx w3f test FILEPATH` command to test your function 124 | 125 | - Options: 126 | - `--logs` Show internal Web3 Function logs 127 | - `--debug` Show Runtime debug messages 128 | - `--chain-id=[number]` Specify the chainId to be used for your Web3 Function (default: `5` for Goerli) 129 | 130 | - Example:
`npx w3f test web3-functions/oracle/index.ts --logs` 131 | - Output: 132 | ``` 133 | Web3Function Build result: 134 | ✓ Schema: web3-functions/oracle/schema.json 135 | ✓ Built file: /Users/chuahsonglin/Documents/GitHub/Gelato/backend/js-resolver-template/.tmp/index.js 136 | ✓ File size: 1.63mb 137 | ✓ Build time: 91.34ms 138 | 139 | Web3Function user args validation: 140 | ✓ currency: ethereum 141 | ✓ oracle: 0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da 142 | 143 | Web3Function running... 144 | 145 | Web3Function Result: 146 | ✓ Return value: { 147 | canExec: true, 148 | callData: [ 149 | { 150 | to: '0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da', 151 | data: '0x8d6cc56d0000000000000000000000000000000000000000000000000000000000000769' 152 | } 153 | ] 154 | } 155 | 156 | Web3Function Runtime stats: 157 | ✓ Duration: 3.29s 158 | ✓ Memory: 74.78mb 159 | ✓ Storage: 0.03kb 160 | ✓ Rpc calls: 3 161 | ``` 162 | 163 | ### Writing unit test for your web3 function 164 | 165 | - Define your tests in `test/hellow-world.test.ts` 166 | - Use `yarn test` command to run unit test suite. 167 | 168 | You can fork a network in your unit test. 169 | RPC methods of provider can be found in [Foundry's Anvil docs](https://book.getfoundry.sh/reference/anvil/) 170 | 171 | Example: [`test/advertising-board.test.ts`](./test/advertising-board.test.ts) 172 | 173 | ```ts 174 | import { AnvilServer } from "./utils/anvil-server"; 175 | 176 | goerliFork = await AnvilServer.fork({ 177 | forkBlockNumber: 8483100, 178 | forkUrl: "https://rpc.ankr.com/eth_goerli", 179 | }); 180 | 181 | const forkedProvider = goerliFork.provider; 182 | ``` 183 | 184 | ### Calling your web3 function against a local node, i.e. Anvil (Foundry) 185 | 1. Update your .env file with the RPC url 186 | 187 | 2. Spin your local node 188 | 189 | ``` 190 | npx run forkAnvil 191 | ``` 192 | 3. Update the PROVIDE_URLS with the local server url, i.e. http://127.0.0.1:8545 193 | 194 | 4. Run your test 195 | 196 | ``` 197 | npx w3f test web3-functions/oracle/index.ts --logs 198 | ``` 199 | 200 | ## Use User arguments 201 | 1. Declare your expected `userArgs` in your schema, accepted types are 'string', 'string[]', 'number', 'number[]', 'boolean', 'boolean[]': 202 | 203 | ```json 204 | { 205 | "web3FunctionVersion": "2.0.0", 206 | "runtime": "js-1.0", 207 | "memory": 128, 208 | "timeout": 30, 209 | "userArgs": { 210 | "currency": "string", 211 | "oracle": "string" 212 | } 213 | } 214 | ``` 215 | 216 | 2. Access your `userArgs` from the Web3Function context: 217 | 218 | ```typescript 219 | Web3Function.onRun(async (context: Web3FunctionContext) => { 220 | const { userArgs, gelatoArgs, secrets } = context; 221 | 222 | // User args: 223 | console.log("Currency:", userArgs.currency); 224 | console.log("Oracle:", userArgs.oracle); 225 | }); 226 | ``` 227 | 228 | 3. Populate `userArgs` in `userArgs.json` and test your web3 function: 229 | 230 | ```json 231 | { 232 | "currency": "ethereum", 233 | "oracle": "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da" 234 | } 235 | 236 | ``` 237 | 238 | ``` 239 | npx w3f test web3-functions/oracle/index.ts --logs 240 | ``` 241 | 242 | ## Use State / Storage 243 | 244 | Web3Functions are stateless scripts, that will run in a new & empty memory context on every execution. 245 | If you need to manage some state variable, we provide a simple key/value store that you can access from your web3 function `context`. 246 | 247 | See the below example to read & update values from your storage: 248 | 249 | ```typescript 250 | import { 251 | Web3Function, 252 | Web3FunctionContext, 253 | } from "@gelatonetwork/web3-functions-sdk"; 254 | 255 | Web3Function.onRun(async (context: Web3FunctionContext) => { 256 | const { storage, multiChainProvider } = context; 257 | 258 | const provider = multiChainProvider.default(); 259 | 260 | // Use storage to retrieve previous state (stored values are always string) 261 | const lastBlockStr = (await storage.get("lastBlockNumber")) ?? "0"; 262 | const lastBlock = parseInt(lastBlockStr); 263 | console.log(`Last block: ${lastBlock}`); 264 | 265 | const newBlock = await provider.getBlockNumber(); 266 | console.log(`New block: ${newBlock}`); 267 | if (newBlock > lastBlock) { 268 | // Update storage to persist your current state (values must be cast to string) 269 | await storage.set("lastBlockNumber", newBlock.toString()); 270 | } 271 | 272 | return { 273 | canExec: false, 274 | message: `Updated block number: ${newBlock.toString()}` 275 | }; 276 | }); 277 | ``` 278 | 279 | Test storage execution:
280 | `npx w3f test web3-functions/storage/index.ts --logs` 281 | 282 | You will see your updated key/values: 283 | ``` 284 | Simulated Web3Function Storage update: 285 | ✓ lastBlockNumber: '8944652' 286 | ``` 287 | 288 | ## Use user secrets 289 | 290 | 1. Input your secrets in `.env` file in the same directory as your web3 function. 291 | 292 | ``` 293 | COINGECKO_API=https://api.coingecko.com/api/v3 294 | ``` 295 | 296 | 2. Access your secrets from the Web3Function context: 297 | 298 | ```typescript 299 | // Get api from secrets 300 | const coingeckoApi = await context.secrets.get("COINGECKO_API"); 301 | if (!coingeckoApi) 302 | return { canExec: false, message: `COINGECKO_API not set in secrets` }; 303 | ``` 304 | 305 | 3. Test your Web3 Function using secrets:
306 | `npx w3f test web3-functions/secrets/index.ts --logs` 307 | 308 | ## Deploy your Web3Function on IPFS 309 | 310 | Use `npx w3f deploy FILEPATH` command to deploy your web3 function. 311 | 312 | Example:
313 | `npx w3f deploy web3-functions/oracle/index.ts` 314 | 315 | The deployer will output your Web3Function IPFS CID, that you can use to create your task: 316 | ``` 317 | ✓ Web3Function deployed to ipfs. 318 | ✓ CID: QmVfDbGGN6qfPs5ocu2ZuzLdBsXpu7zdfPwh14LwFUHLnc 319 | 320 | To create a task that runs your Web3 Function every minute, visit: 321 | > https://beta.app.gelato.network/new-task?cid=QmVfDbGGN6qfPs5ocu2ZuzLdBsXpu7zdfPwh14LwFUHLnc 322 | ``` 323 | 324 | 325 | ## Create your Web3Function task 326 | Use the `automate-sdk` to easily create a new task (make sure you have your private_key in .env): 327 | 328 | ```typescript 329 | const { taskId, tx } = await automate.createBatchExecTask({ 330 | name: "Web3Function - Eth Oracle", 331 | web3FunctionHash: cid, 332 | web3FunctionArgs: { 333 | oracle: oracle.address, 334 | currency: "ethereum", 335 | }, 336 | }); 337 | await tx.wait(); 338 | ``` 339 | 340 | If your task utilizes secrets, you can set them after the task has been created. 341 | 342 | ```typescript 343 | // Set task specific secrets 344 | const secrets = oracleW3f.getSecrets(); 345 | if (Object.keys(secrets).length > 0) { 346 | await web3Function.secrets.set(secrets, taskId); 347 | console.log(`Secrets set`); 348 | } 349 | ``` 350 | 351 | Test it with our sample task creation script:
352 | `yarn create-task:oracle` 353 | 354 | ``` 355 | Deploying Web3Function on IPFS... 356 | Web3Function IPFS CID: QmVfDbGGN6qfPs5ocu2ZuzLdBsXpu7zdfPwh14LwFUHLnc 357 | 358 | Creating automate task... 359 | Task created, taskId: 0x8438933eb9c6e4632d984b4db1e7672082d367b900e536f86295b2e23dbcaff3 360 | > https://beta.app.gelato.network/task/0x8438933eb9c6e4632d984b4db1e7672082d367b900e536f86295b2e23dbcaff3?chainId=5 361 | ``` 362 | 363 | ## More examples 364 | 365 | ### Coingecko oracle 366 | 367 | Fetch price data from Coingecko API to update your on-chain Oracle 368 | 369 | Source: [`web3-functions/oracle/index.ts`](./web3-functions/oracle/index.ts) 370 | 371 | Run:
372 | `npx w3f test web3-functions/oracle/index.ts --logs` 373 | 374 | Create task:
375 | `yarn create-task:oracle` 376 | 377 | 378 | ### Event listener 379 | 380 | Listen to smart contract events and use storage context to maintain your execution state. 381 | 382 | Source: [`web3-functions/event-listener/index.ts`](./web3-functions/event-listener/index.ts) 383 | 384 | Run:
385 | `npx w3f test web3-functions/event-listener/index.ts --logs` 386 | 387 | Create task:
388 | `yarn create-task:event` 389 | 390 | ### Secrets 391 | 392 | Fetch data from a private API to update your on-chain Oracle 393 | 394 | Source: [`web3-functions/secrets/index.ts`](./web3-functions/secrets/index.ts) 395 | 396 | Run:
397 | `npx w3f test web3-functions/secrets/index.ts --logs` 398 | 399 | Create task:
400 | `yarn create-task:secrets` 401 | 402 | ### Advertising Board 403 | 404 | Fetch a random quote from an API and post it on chain. 405 | 406 | Source: [`web3-functions/advertising-board/index.ts`](./web3-functions/advertising-board/index.ts) 407 | 408 | Run:
409 | `npx w3f test web3-functions/advertising-board/index.ts` 410 | 411 | Create task:
412 | `yarn create-task:ad-board` 413 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | const config: Config.InitialOptions = { 4 | roots: [""], 5 | preset: "ts-jest", 6 | testEnvironment: "node", 7 | }; 8 | export default config; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gelatonetwork/web3-functions-template", 3 | "version": "0.1.0", 4 | "description": "Gelato Web3 Functions template", 5 | "url": "https://github.com/gelatodigital/web3-functions-template", 6 | "scripts": { 7 | "build": "rm -rf dist && tsc", 8 | "format": "prettier --write '*/**/*.{js,json,md,ts}'", 9 | "format:check": "prettier --check '*/**/*.{js,json,md,ts}'", 10 | "lint": "eslint --cache .", 11 | "test": "jest", 12 | "create-task:event": "ts-node scripts/create-task-event-listener.ts", 13 | "create-task:oracle": "ts-node scripts/create-task-oracle.ts", 14 | "create-task:secrets": "ts-node scripts/create-task-with-secrets.ts", 15 | "create-task:ad-board": "ts-node scripts/create-task-ad-board.ts", 16 | "forkAnvil": "ts-node scripts/forkAnvil.ts" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "@foundry-rs/easy-foundryup": "^0.1.3", 23 | "@jest/types": "^29.4.2", 24 | "@tsconfig/recommended": "^1.0.1", 25 | "@types/jest": "^29.4.0", 26 | "@types/node": "^16.11.12", 27 | "@typescript-eslint/eslint-plugin": "^5.40.0", 28 | "@typescript-eslint/parser": "^5.6.0", 29 | "eslint": "^8.4.1", 30 | "eslint-config-prettier": "^8.3.0", 31 | "eslint-plugin-prettier": "^4.0.0", 32 | "jest": "^29.4.2", 33 | "jest-environment-node": "^29.4.2", 34 | "prettier": "^2.3.2", 35 | "ts-jest": "^29.0.5", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^4.7.0" 38 | }, 39 | "dependencies": { 40 | "@gelatonetwork/automate-sdk": "^3.0.0", 41 | "@gelatonetwork/web3-functions-sdk": "^2.1.9", 42 | "dotenv": "^16.0.3", 43 | "ky": "^0.32.2", 44 | "octokit": "^2.0.19" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scripts/create-task-ad-board.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from "@ethersproject/providers"; 2 | import { Wallet } from "@ethersproject/wallet"; 3 | import { AutomateSDK, TriggerType } from "@gelatonetwork/automate-sdk"; 4 | import { Web3FunctionBuilder } from "@gelatonetwork/web3-functions-sdk/builder"; 5 | import dotenv from "dotenv"; 6 | import path from "path"; 7 | dotenv.config(); 8 | 9 | if (!process.env.PRIVATE_KEY) throw new Error("Missing env PRIVATE_KEY"); 10 | const pk = process.env.PRIVATE_KEY; 11 | 12 | if (!process.env.PROVIDER_URLS) throw new Error("Missing env PROVIDER_URL"); 13 | const providerUrl = process.env.PROVIDER_URLS.split(",")[0]; 14 | 15 | const main = async () => { 16 | // Instanciate provider & signer 17 | const provider = new JsonRpcProvider(providerUrl); 18 | const chainId = (await provider.getNetwork()).chainId; 19 | const wallet = new Wallet(pk as string, provider); 20 | const automate = new AutomateSDK(chainId, wallet); 21 | 22 | // Deploy Web3Function on IPFS 23 | console.log("Deploying Web3Function on IPFS..."); 24 | 25 | const web3FunctionPath = path.join( 26 | "web3-functions", 27 | "advertising-board", 28 | "index.ts" 29 | ); 30 | const cid = await Web3FunctionBuilder.deploy(web3FunctionPath); 31 | console.log(`Web3Function IPFS CID: ${cid}`); 32 | 33 | // Create task using automate-sdk 34 | console.log("Creating automate task..."); 35 | const { taskId, tx } = await automate.createBatchExecTask({ 36 | name: "Web3Function - Ad Board", 37 | web3FunctionHash: cid, 38 | web3FunctionArgs: {}, 39 | trigger: { 40 | interval: 60 * 1000, 41 | type: TriggerType.TIME, 42 | }, 43 | }); 44 | await tx.wait(); 45 | console.log(`Task created, taskId: ${taskId} (tx hash: ${tx.hash})`); 46 | console.log( 47 | `> https://beta.app.gelato.network/task/${taskId}?chainId=${chainId}` 48 | ); 49 | }; 50 | 51 | main() 52 | .then(() => { 53 | process.exit(); 54 | }) 55 | .catch((err) => { 56 | console.error("Error:", err.message); 57 | process.exit(1); 58 | }); 59 | -------------------------------------------------------------------------------- /scripts/create-task-event-listener.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from "@ethersproject/providers"; 2 | import { Wallet } from "@ethersproject/wallet"; 3 | import { AutomateSDK, TriggerType } from "@gelatonetwork/automate-sdk"; 4 | import { Web3FunctionBuilder } from "@gelatonetwork/web3-functions-sdk/builder"; 5 | import dotenv from "dotenv"; 6 | import path from "path"; 7 | dotenv.config(); 8 | 9 | if (!process.env.PRIVATE_KEY) throw new Error("Missing env PRIVATE_KEY"); 10 | const pk = process.env.PRIVATE_KEY; 11 | 12 | if (!process.env.PROVIDER_URLS) throw new Error("Missing env PROVIDER_URLS"); 13 | const providerUrl = process.env.PROVIDER_URLS.split(",")[0]; 14 | 15 | const main = async () => { 16 | // Instanciate provider & signer 17 | const provider = new JsonRpcProvider(providerUrl); 18 | const chainId = (await provider.getNetwork()).chainId; 19 | const wallet = new Wallet(pk as string, provider); 20 | const automate = new AutomateSDK(chainId, wallet); 21 | 22 | // Deploy Web3Function on IPFS 23 | console.log("Deploying Web3Function on IPFS..."); 24 | const web3FunctionPath = path.join( 25 | "web3-functions", 26 | "event-listener", 27 | "index.ts" 28 | ); 29 | const cid = await Web3FunctionBuilder.deploy(web3FunctionPath); 30 | console.log(`Web3Function IPFS CID: ${cid}`); 31 | 32 | // Create task using automate-sdk 33 | console.log("Creating automate task..."); 34 | const { taskId, tx } = await automate.createBatchExecTask({ 35 | name: "Web3Function - Event Counter", 36 | web3FunctionHash: cid, 37 | web3FunctionArgs: { 38 | oracle: "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da", 39 | counter: "0x8F143A5D62de01EAdAF9ef16d4d3694380066D9F", 40 | }, 41 | trigger: { 42 | interval: 60 * 1000, 43 | type: TriggerType.TIME, 44 | }, 45 | }); 46 | await tx.wait(); 47 | console.log(`Task created, taskId: ${taskId} (tx hash: ${tx.hash})`); 48 | console.log( 49 | `> https://beta.app.gelato.network/task/${taskId}?chainId=${chainId}` 50 | ); 51 | }; 52 | 53 | main() 54 | .then(() => { 55 | process.exit(); 56 | }) 57 | .catch((err) => { 58 | console.error("Error:", err.message); 59 | process.exit(1); 60 | }); 61 | -------------------------------------------------------------------------------- /scripts/create-task-oracle.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from "@ethersproject/providers"; 2 | import { Wallet } from "@ethersproject/wallet"; 3 | import { AutomateSDK, TriggerType } from "@gelatonetwork/automate-sdk"; 4 | import { Web3FunctionBuilder } from "@gelatonetwork/web3-functions-sdk/builder"; 5 | import dotenv from "dotenv"; 6 | import path from "path"; 7 | dotenv.config(); 8 | 9 | if (!process.env.PRIVATE_KEY) throw new Error("Missing env PRIVATE_KEY"); 10 | const pk = process.env.PRIVATE_KEY; 11 | 12 | if (!process.env.PROVIDER_URLS) throw new Error("Missing env PROVIDER_URLS"); 13 | const providerUrl = process.env.PROVIDER_URLS.split(",")[0]; 14 | 15 | const main = async () => { 16 | // Instanciate provider & signer 17 | const provider = new JsonRpcProvider(providerUrl); 18 | const chainId = (await provider.getNetwork()).chainId; 19 | const wallet = new Wallet(pk as string, provider); 20 | const automate = new AutomateSDK(chainId, wallet); 21 | 22 | // Deploy Web3Function on IPFS 23 | console.log("Deploying Web3Function on IPFS..."); 24 | const web3FunctionPath = path.join("web3-functions", "oracle", "index.ts"); 25 | const cid = await Web3FunctionBuilder.deploy(web3FunctionPath); 26 | console.log(`Web3Function IPFS CID: ${cid}`); 27 | 28 | // Create task using automate-sdk 29 | console.log("Creating automate task..."); 30 | const { taskId, tx } = await automate.createBatchExecTask({ 31 | name: "Web3Function - Eth Oracle", 32 | web3FunctionHash: cid, 33 | web3FunctionArgs: { 34 | oracle: "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da", 35 | currency: "ethereum", 36 | }, 37 | trigger: { 38 | interval: 60 * 1000, 39 | type: TriggerType.TIME, 40 | }, 41 | }); 42 | await tx.wait(); 43 | console.log(`Task created, taskId: ${taskId} (tx hash: ${tx.hash})`); 44 | console.log( 45 | `> https://beta.app.gelato.network/task/${taskId}?chainId=${chainId}` 46 | ); 47 | }; 48 | 49 | main() 50 | .then(() => { 51 | process.exit(); 52 | }) 53 | .catch((err) => { 54 | console.error("Error:", err.message); 55 | process.exit(1); 56 | }); 57 | -------------------------------------------------------------------------------- /scripts/create-task-with-secrets.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from "@ethersproject/providers"; 2 | import { Wallet } from "@ethersproject/wallet"; 3 | import { 4 | AutomateSDK, 5 | TriggerType, 6 | Web3Function, 7 | } from "@gelatonetwork/automate-sdk"; 8 | import { Web3FunctionBuilder } from "@gelatonetwork/web3-functions-sdk/builder"; 9 | import { Web3FunctionLoader } from "@gelatonetwork/web3-functions-sdk/loader"; 10 | import dotenv from "dotenv"; 11 | import path from "path"; 12 | dotenv.config(); 13 | 14 | if (!process.env.PRIVATE_KEY) throw new Error("Missing env PRIVATE_KEY"); 15 | const pk = process.env.PRIVATE_KEY; 16 | 17 | if (!process.env.PROVIDER_URLS) throw new Error("Missing env PROVIDER_URLS"); 18 | const providerUrl = process.env.PROVIDER_URLS.split(",")[0]; 19 | 20 | const w3fRootDir = path.join("web3-functions"); 21 | const w3fName = "secrets"; 22 | 23 | const main = async () => { 24 | // Instanciate provider & signer 25 | const provider = new JsonRpcProvider(providerUrl); 26 | const chainId = (await provider.getNetwork()).chainId; 27 | const wallet = new Wallet(pk as string, provider); 28 | const automate = new AutomateSDK(chainId, wallet); 29 | 30 | // Deploy Web3Function on IPFS 31 | console.log("Deploying Web3Function on IPFS..."); 32 | const web3FunctionPath = path.join("web3-functions", "secrets", "index.ts"); 33 | const cid = await Web3FunctionBuilder.deploy(web3FunctionPath); 34 | console.log(`Web3Function IPFS CID: ${cid}`); 35 | 36 | // Create task using automate-sdk 37 | console.log("Creating automate task..."); 38 | const { taskId, tx } = await automate.createBatchExecTask({ 39 | name: "Web3Function - Eth Oracle Secret Api", 40 | web3FunctionHash: cid, 41 | web3FunctionArgs: { 42 | oracle: "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da", 43 | currency: "ethereum", 44 | }, 45 | trigger: { 46 | interval: 60 * 1000, 47 | type: TriggerType.TIME, 48 | }, 49 | }); 50 | await tx.wait(); 51 | console.log(`Task created, taskId: ${taskId} (tx hash: ${tx.hash})`); 52 | console.log( 53 | `> https://beta.app.gelato.network/task/${taskId}?chainId=${chainId}` 54 | ); 55 | 56 | // Set secrets 57 | const { secrets } = Web3FunctionLoader.load(w3fName, w3fRootDir); 58 | const web3FunctionHelper = new Web3Function(chainId, wallet); 59 | if (Object.keys(secrets).length > 0) { 60 | await web3FunctionHelper.secrets.set(secrets, taskId); 61 | } 62 | }; 63 | 64 | main() 65 | .then(() => { 66 | process.exit(); 67 | }) 68 | .catch((err) => { 69 | console.error("Error:", err.message); 70 | process.exit(1); 71 | }); 72 | -------------------------------------------------------------------------------- /scripts/forkAnvil.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | const forkChain = async () => { 5 | const { spawn } = await import("child_process"); 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 8 | const RPC = process.env["RPC"]!; 9 | const params = ["-f", RPC]; 10 | 11 | let blockNumber; 12 | 13 | if (blockNumber) { 14 | params.push(`--fork-block-number=${blockNumber}`); 15 | } 16 | 17 | /// You can add as much customs params as wanted 18 | const childProcess = spawn("anvil", params, { 19 | stdio: "inherit", 20 | }); 21 | 22 | childProcess.once("close", (status) => { 23 | childProcess.removeAllListeners("error"); 24 | 25 | if (status === 0) { 26 | console.log("ok"); 27 | } else { 28 | console.log("error"); 29 | } 30 | }); 31 | 32 | childProcess.once("error", (_status) => { 33 | childProcess.removeAllListeners("close"); 34 | console.log("error"); 35 | }); 36 | }; 37 | 38 | forkChain(); 39 | -------------------------------------------------------------------------------- /test/advertising-board.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Web3FunctionContextData } from "@gelatonetwork/web3-functions-sdk"; 3 | import { Web3FunctionLoader } from "@gelatonetwork/web3-functions-sdk/loader"; 4 | import { runWeb3Function } from "./utils"; 5 | import { AnvilServer } from "./utils/anvil-server"; 6 | 7 | const w3fName = "advertising-board"; 8 | const w3fRootDir = path.join("web3-functions"); 9 | const w3fPath = path.join(w3fRootDir, w3fName, "index.ts"); 10 | 11 | describe("Advertising Board Web3 Function test", () => { 12 | let context: Web3FunctionContextData; 13 | let goerliFork: AnvilServer; 14 | 15 | beforeAll(async () => { 16 | goerliFork = await AnvilServer.fork({ 17 | forkBlockNumber: 8483100, 18 | forkUrl: "https://rpc.ankr.com/eth_goerli", 19 | }); 20 | 21 | const { secrets } = Web3FunctionLoader.load(w3fName, w3fRootDir); 22 | const gasPrice = (await goerliFork.provider.getGasPrice()).toString(); 23 | 24 | 25 | 26 | context = { 27 | secrets, 28 | storage: {}, 29 | gelatoArgs: { 30 | chainId: 5, 31 | gasPrice, 32 | }, 33 | userArgs: {}, 34 | }; 35 | }, 10000); 36 | 37 | afterAll(async () => { 38 | goerliFork.kill(); 39 | }); 40 | 41 | it("canExec: false - Time not elapsed", async () => { 42 | const blockTime = (await goerliFork.provider.getBlock("latest")).timestamp; 43 | 44 | // mock storage state of "lastPost" 45 | context.storage = { lastPost: blockTime.toString() }; 46 | 47 | 48 | const res = await runWeb3Function(w3fPath, context, [goerliFork.provider]); 49 | 50 | 51 | expect(res.result.canExec).toEqual(false); 52 | }); 53 | 54 | it("canExec: True - Time elapsed", async () => { 55 | const blockTimeBefore = (await goerliFork.provider.getBlock("latest")) 56 | .timestamp; 57 | const nextPostTime = blockTimeBefore + 3600; 58 | 59 | // fast forward block time 60 | await goerliFork.provider.send("evm_mine", [nextPostTime]); 61 | 62 | // pass current block time 63 | const blockTime = (await goerliFork.provider.getBlock("latest")).timestamp; 64 | 65 | const res = await runWeb3Function(w3fPath, context, [goerliFork.provider]); 66 | 67 | expect(res.result.canExec).toEqual(true); 68 | 69 | // expect "lastPost" to be updated 70 | expect(res.storage.state).toEqual("updated"); 71 | expect(res.storage.storage["lastPost"]).toEqual(blockTime.toString()); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/hello-world.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Web3FunctionContextData } from "@gelatonetwork/web3-functions-sdk"; 3 | import { Web3FunctionLoader } from "@gelatonetwork/web3-functions-sdk/loader"; 4 | import { runWeb3Function } from "./utils"; 5 | import { parseUnits } from "@ethersproject/units"; 6 | 7 | const w3fName = "hello-world"; 8 | const w3fRootDir = path.join("web3-functions"); 9 | const w3fPath = path.join(w3fRootDir, w3fName, "index.ts"); 10 | 11 | describe("My Web3 Function test", () => { 12 | let context: Web3FunctionContextData; 13 | 14 | beforeAll(async () => { 15 | const { secrets } = Web3FunctionLoader.load(w3fName, w3fRootDir); 16 | 17 | context = { 18 | secrets, 19 | storage: {}, 20 | gelatoArgs: { 21 | chainId: 5, 22 | gasPrice: parseUnits("100", "gwei").toString(), 23 | }, 24 | userArgs: {}, 25 | }; 26 | }, 10000); 27 | 28 | it("canExec: true", async () => { 29 | const res = await runWeb3Function(w3fPath, context); 30 | 31 | expect(res.result.canExec).toEqual(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/utils/anvil-server.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import net from "net"; 3 | import { ChildProcessWithoutNullStreams, spawn } from "node:child_process"; 4 | 5 | import { getAnvilCommand, checkAnvil, run } from "@foundry-rs/easy-foundryup"; 6 | import { 7 | JsonRpcProvider, 8 | StaticJsonRpcProvider, 9 | } from "@ethersproject/providers"; 10 | 11 | export declare interface AnvilOptions { 12 | url?: string; 13 | accountKeysPath?: string; // Translates to: account_keys_path 14 | accounts?: object[] | object; 15 | hostname?: string; 16 | allowUnlimitedContractSize?: boolean; 17 | blockTime?: number; 18 | debug?: boolean; 19 | defaultBalanceEther?: number; // Translates to: default_balance_ether 20 | forkUrl?: string; 21 | forkBlockNumber?: string | number; // Translates to: fork_block_number 22 | gasLimit?: number; 23 | gasPrice?: string | number; 24 | hdPath?: string; // Translates to: hd_path 25 | install?: boolean; // Install anvil binary if missing 26 | mnemonic?: string; 27 | path?: string; // path to the anvil exec 28 | locked?: boolean; 29 | noStorageCaching?: boolean; 30 | hardfork?: string; 31 | chainId?: number; 32 | port?: number; 33 | totalAccounts?: number; // Translates to: total_accounts 34 | silent?: boolean; 35 | vmErrorsOnRPCResponse?: boolean; 36 | ws?: boolean; 37 | } 38 | 39 | export class AnvilServer { 40 | private readonly _anvil: ChildProcessWithoutNullStreams; 41 | private readonly _options: AnvilOptions; 42 | public provider: JsonRpcProvider; 43 | 44 | private constructor( 45 | options: AnvilOptions, 46 | anvil: ChildProcessWithoutNullStreams, 47 | provider: JsonRpcProvider 48 | ) { 49 | this._options = options; 50 | this._anvil = anvil; 51 | this.provider = provider; 52 | } 53 | 54 | private static async _getAvailablePort(): Promise { 55 | return new Promise((res, rej) => { 56 | const srv = net.createServer(); 57 | srv.listen(0, () => { 58 | const address = srv.address(); 59 | const port = address && typeof address === "object" ? address.port : -1; 60 | srv.close(() => (port ? res(port) : rej())); 61 | }); 62 | }); 63 | } 64 | 65 | private static _optionsToArgs(options: AnvilOptions): string[] { 66 | const anvilArgs: string[] = []; 67 | if (options.port) { 68 | anvilArgs.push("--port", options.port.toString()); 69 | } 70 | if (options.totalAccounts) { 71 | anvilArgs.push("--accounts", options.totalAccounts.toString()); 72 | } 73 | if (options.mnemonic) { 74 | anvilArgs.push("--mnemonic", options.mnemonic); 75 | } 76 | if (options.defaultBalanceEther) { 77 | anvilArgs.push("--balance", options.defaultBalanceEther.toString()); 78 | } 79 | if (options.hdPath) { 80 | anvilArgs.push("--derivation-path", options.hdPath); 81 | } 82 | if (options.silent) { 83 | anvilArgs.push("--silent", options.silent.toString()); 84 | } 85 | if (options.blockTime) { 86 | anvilArgs.push("--block-time", options.blockTime.toString()); 87 | } 88 | if (options.gasLimit) { 89 | anvilArgs.push("--gas-limit", options.gasLimit.toString()); 90 | } 91 | if (options.gasPrice && options.gasPrice !== "auto") { 92 | anvilArgs.push("--gas-price", options.gasPrice.toString()); 93 | } 94 | if (options.chainId) { 95 | anvilArgs.push("--chain-id", options.chainId.toString()); 96 | } 97 | if (options.forkUrl) { 98 | anvilArgs.push("--fork-url", options.forkUrl); 99 | if (options.forkBlockNumber) { 100 | anvilArgs.push( 101 | "--fork-block-number", 102 | options.forkBlockNumber.toString() 103 | ); 104 | } 105 | } 106 | if (options.noStorageCaching) { 107 | anvilArgs.push("--no-storage-caching"); 108 | } 109 | if (options.hardfork && options.hardfork !== "arrowGlacier") { 110 | anvilArgs.push("--hardfork", options.hardfork); 111 | } 112 | return anvilArgs; 113 | } 114 | 115 | public static async fork(options: AnvilOptions): Promise { 116 | if (options.install) { 117 | if (!(await checkAnvil())) { 118 | if (options.debug) console.log("Installing anvil"); 119 | await run(); 120 | } 121 | } 122 | 123 | if (!options.port) options.port = await AnvilServer._getAvailablePort(); 124 | 125 | if (options.debug) console.log("Launching anvil"); 126 | const start = Date.now(); 127 | const anvilPath = options.path ?? (await getAnvilCommand()); 128 | const anvilArgs = AnvilServer._optionsToArgs(options); 129 | const anvil = spawn(anvilPath, anvilArgs, { shell: true }); 130 | 131 | anvil.on("close", (code: string) => { 132 | if (options.debug) 133 | console.log(`anvil child process exited with code ${code}`); 134 | }); 135 | 136 | let isServerReady = false; 137 | let setUpTime = 0; 138 | anvil.stdout.on("data", (data: string) => { 139 | const output = data.toString(); 140 | if (output.includes("Listening")) { 141 | isServerReady = true; 142 | setUpTime = Date.now() - start; 143 | } 144 | if (options.debug) console.log(`${data}`); 145 | }); 146 | 147 | anvil.stderr.on("data", (data: string) => { 148 | if (options.debug) console.log(`${data}`); 149 | }); 150 | 151 | // wait until server ready 152 | const retries = 50; // 5secs 153 | for (let index = 0; index < retries; index++) { 154 | if (isServerReady) { 155 | if (options.debug) console.log(`anvil server ready in ${setUpTime}ms`); 156 | break; 157 | } 158 | await new Promise((resolve) => setTimeout(resolve, 100)); 159 | } 160 | 161 | const providerUrl = `http://127.0.0.1:${options.port}`; 162 | const anvilProvider = new StaticJsonRpcProvider(providerUrl); 163 | return new AnvilServer(options, anvil, anvilProvider); 164 | } 165 | 166 | public kill() { 167 | this._anvil?.kill(); 168 | } 169 | 170 | public async waitUntilClosed(): Promise { 171 | return new Promise((resolve) => { 172 | this._anvil.once("close", resolve); 173 | }); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Web3FunctionContextData, 3 | MultiChainProviderConfig, 4 | Web3FunctionRunnerOptions, 5 | } from "@gelatonetwork/web3-functions-sdk"; 6 | import { Web3FunctionRunner } from "@gelatonetwork/web3-functions-sdk/runtime"; 7 | import { Web3FunctionBuilder } from "@gelatonetwork/web3-functions-sdk/builder"; 8 | import { 9 | JsonRpcProvider, 10 | StaticJsonRpcProvider, 11 | } from "@ethersproject/providers"; 12 | 13 | export const MAX_RPC_LIMIT = 100; 14 | export const MAX_DOWNLOAD_LIMIT = 10 * 1024 * 1024; 15 | export const MAX_UPLOAD_LIMIT = 5 * 1024 * 1024; 16 | export const MAX_REQUEST_LIMIT = 100; 17 | export const MAX_STORAGE_LIMIT = 1 * 1024 * 1024; 18 | 19 | export const runWeb3Function = async ( 20 | web3FunctionPath: string, 21 | context: Web3FunctionContextData, 22 | providers?: JsonRpcProvider[] 23 | ) => { 24 | const buildRes = await Web3FunctionBuilder.build(web3FunctionPath, { 25 | debug: false, 26 | }); 27 | 28 | if (!buildRes.success) 29 | throw new Error(`Fail to build web3Function: ${buildRes.error}`); 30 | 31 | const runner = new Web3FunctionRunner(false); 32 | const runtime: "docker" | "thread" = "thread"; 33 | const memory = buildRes.schema.memory; 34 | const rpcLimit = MAX_RPC_LIMIT; 35 | const timeout = buildRes.schema.timeout * 1000; 36 | const version = buildRes.schema.web3FunctionVersion; 37 | 38 | const options: Web3FunctionRunnerOptions = { 39 | runtime, 40 | showLogs: true, 41 | memory, 42 | downloadLimit: MAX_DOWNLOAD_LIMIT, 43 | uploadLimit: MAX_UPLOAD_LIMIT, 44 | requestLimit: MAX_REQUEST_LIMIT, 45 | rpcLimit, 46 | timeout, 47 | storageLimit: MAX_STORAGE_LIMIT, 48 | }; 49 | const script = buildRes.filePath; 50 | 51 | const multiChainProviderConfig: MultiChainProviderConfig = {}; 52 | 53 | if (!providers) { 54 | if (!process.env.PROVIDER_URLS) { 55 | console.error(`Missing PROVIDER_URLS in .env file`); 56 | process.exit(); 57 | } 58 | 59 | const urls = process.env.PROVIDER_URLS.split(","); 60 | providers = []; 61 | for (const url of urls) { 62 | providers.push(new StaticJsonRpcProvider(url)); 63 | } 64 | } 65 | 66 | for (const provider of providers) { 67 | const chainId = (await provider.getNetwork()).chainId; 68 | 69 | multiChainProviderConfig[chainId] = provider; 70 | } 71 | 72 | const res = await runner.run({ 73 | script, 74 | context, 75 | options, 76 | version, 77 | multiChainProviderConfig, 78 | }); 79 | 80 | if (!res.success) 81 | throw new Error(`Fail to run web3 function: ${res.error.message}`); 82 | 83 | return res; 84 | }; 85 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["es2020"], 5 | "noImplicitAny": false, 6 | "allowSyntheticDefaultImports": true, 7 | "downlevelIteration": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "node", 10 | "pretty": true, 11 | "resolveJsonModule": true, 12 | "typeRoots": ["./node_modules/@types"], 13 | "outDir": "dist", 14 | "declaration": true, 15 | "useUnknownInCatchVariables": false 16 | }, 17 | "include": ["scripts", "web3-functions", "jest.config.ts", "test"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /web3-functions/advertising-board/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Web3Function, 3 | Web3FunctionContext, 4 | } from "@gelatonetwork/web3-functions-sdk"; 5 | import { Contract } from "@ethersproject/contracts"; 6 | import ky from "ky"; // we recommend using ky as axios doesn't support fetch by default 7 | 8 | const AD_BOARD_ABI = [ 9 | "function postMessage(string)", 10 | "function viewMessage(address)", 11 | ]; 12 | 13 | Web3Function.onRun(async (context: Web3FunctionContext) => { 14 | const { userArgs, storage, multiChainProvider } = context; 15 | 16 | const provider = multiChainProvider.default(); 17 | const adBoardAddress = 18 | (userArgs.adBoard as string) ?? 19 | "0x28a0A1C63E7E8F0DAe5ad633fe232c12b489d5f0"; 20 | 21 | const lastPost = Number(await storage.get("lastPost")) ?? 0; 22 | const adBoardContract = new Contract(adBoardAddress, AD_BOARD_ABI); 23 | 24 | const nextPostTime = lastPost + 3600; // 1h 25 | const timestamp = (await provider.getBlock("latest")).timestamp; 26 | 27 | if (timestamp < nextPostTime) { 28 | return { canExec: false, message: `Time not elapsed` }; 29 | } 30 | 31 | let message = ""; 32 | try { 33 | const randomQuoteApi = `https://zenquotes.io/api/random`; 34 | 35 | const quote: { q: string; a: string }[] = await ky 36 | .get(randomQuoteApi, { timeout: 5_000, retry: 0 }) 37 | .json(); 38 | 39 | message = `${quote[0].a}: ${quote[0].q}`; 40 | console.log(message); 41 | } catch (err) { 42 | return { canExec: false, message: `QuoteApi call failed` }; 43 | } 44 | 45 | await storage.set("lastPost", timestamp.toString()); 46 | 47 | return { 48 | canExec: true, 49 | callData: [ 50 | { 51 | to: adBoardAddress, 52 | data: adBoardContract.interface.encodeFunctionData("postMessage", [ 53 | message, 54 | ]), 55 | }, 56 | ], 57 | }; 58 | }); 59 | -------------------------------------------------------------------------------- /web3-functions/advertising-board/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3FunctionVersion": "2.0.0", 3 | "runtime": "js-1.0", 4 | "memory": 128, 5 | "timeout": 30, 6 | "userArgs": { "adBoard": "string" } 7 | } 8 | -------------------------------------------------------------------------------- /web3-functions/advertising-board/storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "lastPost": "1680505773" 3 | } 4 | -------------------------------------------------------------------------------- /web3-functions/advertising-board/userArgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "adBoard": "0x28a0A1C63E7E8F0DAe5ad633fe232c12b489d5f0" 3 | } 4 | -------------------------------------------------------------------------------- /web3-functions/event-listener/index.ts: -------------------------------------------------------------------------------- 1 | import { Log } from "@ethersproject/providers"; 2 | import { 3 | Web3Function, 4 | Web3FunctionContext, 5 | } from "@gelatonetwork/web3-functions-sdk"; 6 | import { Contract } from "@ethersproject/contracts"; 7 | 8 | const MAX_RANGE = 100; // limit range of events to comply with rpc providers 9 | const MAX_REQUESTS = 100; // limit number of requests on every execution to avoid hitting timeout 10 | const ORACLE_ABI = ["event PriceUpdated(uint256 indexed time, uint256 price)"]; 11 | const COUNTER_ABI = ["function increaseCount(uint256)"]; 12 | 13 | Web3Function.onRun(async (context: Web3FunctionContext) => { 14 | const { userArgs, storage, multiChainProvider } = context; 15 | 16 | const provider = multiChainProvider.default(); 17 | 18 | // Create oracle & counter contract 19 | const oracleAddress = 20 | (userArgs.oracle as string) ?? "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da"; 21 | const counterAddress = 22 | (userArgs.counter as string) ?? 23 | "0x8F143A5D62de01EAdAF9ef16d4d3694380066D9F"; 24 | const oracle = new Contract(oracleAddress, ORACLE_ABI, provider); 25 | const counter = new Contract(counterAddress, COUNTER_ABI, provider); 26 | const topics = [oracle.interface.getEventTopic("PriceUpdated")]; 27 | const currentBlock = await provider.getBlockNumber(); 28 | 29 | // Retrieve last processed block number & nb events matched from storage 30 | const lastBlockStr = await storage.get("lastBlockNumber"); 31 | let lastBlock = lastBlockStr ? parseInt(lastBlockStr) : currentBlock - 2000; 32 | let totalEvents = parseInt((await storage.get("totalEvents")) ?? "0"); 33 | console.log(`Last processed block: ${lastBlock}`); 34 | console.log(`Total events matched: ${totalEvents}`); 35 | 36 | // Fetch recent logs in range of 100 blocks 37 | const logs: Log[] = []; 38 | let nbRequests = 0; 39 | while (lastBlock < currentBlock && nbRequests < MAX_REQUESTS) { 40 | nbRequests++; 41 | const fromBlock = lastBlock + 1; 42 | const toBlock = Math.min(fromBlock + MAX_RANGE, currentBlock); 43 | console.log(`Fetching log events from blocks ${fromBlock} to ${toBlock}`); 44 | try { 45 | const eventFilter = { 46 | address: oracleAddress, 47 | topics, 48 | fromBlock, 49 | toBlock, 50 | }; 51 | const result = await provider.getLogs(eventFilter); 52 | logs.push(...result); 53 | lastBlock = toBlock; 54 | } catch (err) { 55 | return { canExec: false, message: `Rpc call failed: ${err.message}` }; 56 | } 57 | } 58 | 59 | // Parse retrieved events 60 | console.log(`Matched ${logs.length} new events`); 61 | const nbNewEvents = logs.length; 62 | totalEvents += logs.length; 63 | for (const log of logs) { 64 | const event = oracle.interface.parseLog(log); 65 | const [time, price] = event.args; 66 | console.log( 67 | `Price updated: ${price}$ at ${new Date(time * 1000).toUTCString()}` 68 | ); 69 | } 70 | 71 | // Update storage for next run 72 | await storage.set("lastBlockNumber", currentBlock.toString()); 73 | await storage.set("totalEvents", totalEvents.toString()); 74 | 75 | if (nbNewEvents === 0) { 76 | return { 77 | canExec: false, 78 | message: `Total events matched: ${totalEvents} (at block #${currentBlock.toString()})`, 79 | }; 80 | } 81 | 82 | // Increase number of events matched on our OracleCounter contract 83 | return { 84 | canExec: true, 85 | callData: [ 86 | { 87 | to: counterAddress, 88 | data: counter.interface.encodeFunctionData("increaseCount", [ 89 | nbNewEvents, 90 | ]), 91 | }, 92 | ], 93 | }; 94 | }); 95 | -------------------------------------------------------------------------------- /web3-functions/event-listener/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3FunctionVersion": "2.0.0", 3 | "runtime": "js-1.0", 4 | "memory": 128, 5 | "timeout": 30, 6 | "userArgs": { 7 | "counter": "string", 8 | "oracle": "string" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web3-functions/event-listener/storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "lastBlockNumber": "8765200", 3 | "totalEvents": "0" 4 | } 5 | -------------------------------------------------------------------------------- /web3-functions/event-listener/userArgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "counter": "0x8F143A5D62de01EAdAF9ef16d4d3694380066D9F", 3 | "oracle": "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da" 4 | } 5 | -------------------------------------------------------------------------------- /web3-functions/hello-world/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Web3Function, 3 | Web3FunctionContext, 4 | } from "@gelatonetwork/web3-functions-sdk"; 5 | 6 | Web3Function.onRun(async (context: Web3FunctionContext) => { 7 | const { gelatoArgs, multiChainProvider } = context; 8 | 9 | return { 10 | canExec: true, 11 | callData: [], 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /web3-functions/hello-world/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3FunctionVersion": "2.0.0", 3 | "runtime": "js-1.0", 4 | "memory": 128, 5 | "timeout": 30, 6 | "userArgs": {} 7 | } 8 | -------------------------------------------------------------------------------- /web3-functions/hello-world/storage.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /web3-functions/hello-world/userArgs.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /web3-functions/oracle/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Web3Function, 3 | Web3FunctionContext, 4 | } from "@gelatonetwork/web3-functions-sdk"; 5 | import { Contract } from "@ethersproject/contracts"; 6 | import ky from "ky"; // we recommend using ky as axios doesn't support fetch by default 7 | 8 | const ORACLE_ABI = [ 9 | "function lastUpdated() external view returns(uint256)", 10 | "function updatePrice(uint256)", 11 | ]; 12 | 13 | Web3Function.onRun(async (context: Web3FunctionContext) => { 14 | const { userArgs, multiChainProvider } = context; 15 | 16 | const provider = multiChainProvider.default(); 17 | // Retrieve Last oracle update time 18 | const oracleAddress = 19 | (userArgs.oracle as string) ?? "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da"; 20 | let lastUpdated; 21 | let oracle; 22 | try { 23 | oracle = new Contract(oracleAddress, ORACLE_ABI, provider); 24 | lastUpdated = parseInt(await oracle.lastUpdated()); 25 | console.log(`Last oracle update: ${lastUpdated}`); 26 | } catch (err) { 27 | return { canExec: false, message: `Rpc call failed` }; 28 | } 29 | 30 | // Check if it's ready for a new update 31 | const nextUpdateTime = lastUpdated + 3600; // 1h 32 | const timestamp = (await provider.getBlock("latest")).timestamp; 33 | console.log(`Next oracle update: ${nextUpdateTime}`); 34 | if (timestamp < nextUpdateTime) { 35 | return { canExec: false, message: `Time not elapsed` }; 36 | } 37 | 38 | // Get current price on coingecko 39 | const currency = (userArgs.currency as string) ?? "ethereum"; 40 | let price = 0; 41 | try { 42 | const coingeckoApi = `https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=usd`; 43 | 44 | const priceData: { [key: string]: { usd: number } } = await ky 45 | .get(coingeckoApi, { timeout: 5_000, retry: 0 }) 46 | .json(); 47 | price = Math.floor(priceData[currency].usd); 48 | } catch (err) { 49 | return { canExec: false, message: `Coingecko call failed` }; 50 | } 51 | console.log(`Updating price: ${price}`); 52 | 53 | // Return execution call data 54 | return { 55 | canExec: true, 56 | callData: [ 57 | { 58 | to: oracleAddress, 59 | data: oracle.interface.encodeFunctionData("updatePrice", [price]), 60 | }, 61 | ], 62 | }; 63 | }); 64 | -------------------------------------------------------------------------------- /web3-functions/oracle/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3FunctionVersion": "2.0.0", 3 | "runtime": "js-1.0", 4 | "memory": 128, 5 | "timeout": 30, 6 | "userArgs": { 7 | "currency": "string", 8 | "oracle": "string" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web3-functions/oracle/storage.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /web3-functions/oracle/userArgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "currency": "ethereum", 3 | "oracle": "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da" 4 | } 5 | -------------------------------------------------------------------------------- /web3-functions/private/.env.example: -------------------------------------------------------------------------------- 1 | GIST_ID=0c58ee8ce55bc7af5f42a2d75c27433c -------------------------------------------------------------------------------- /web3-functions/private/README.md: -------------------------------------------------------------------------------- 1 | # Private web3 function 2 | 3 | Private web3 function can be achieved by storing the contents of `onRun` function in a secret Github gist. This `onRun` function is fetched and executed during runtime. 4 | 5 | ## Writing your secret onRun function. 6 | 7 | onRun function should be in JavaScript and named `onRun.js`. 8 | 9 | ### 1. `onRun.js` file structure 10 | 11 | `onRun.js` should return a promise. 12 | 13 | ```js 14 | return (async () => { 15 | // ... your code here 16 | })(); 17 | ``` 18 | 19 | ### 2. Using dependencies 20 | 21 | Dependencies that are used in `onRun.js` should be imported in the web3 function `index.ts` file, not in `onRun.js`. 22 | 23 | In `/web3-functions/private/index.ts`: 24 | 25 | ```ts 26 | // import dependencies used in onRun.js 27 | import { ethers } from "ethers"; 28 | import ky from "ky"; 29 | ``` 30 | 31 | The dependencies `ky` and `ethers` are used in `onRun.js`. They will be passed into `onRun.js` 32 | 33 | In `/web3-functions/private/index.ts`: 34 | 35 | ```ts 36 | const onRunFunction = new Function("context", "ky", "ethers", onRunScript); 37 | ``` 38 | 39 | In `onRun.js`, you can use the dependencies as if they are already imported. 40 | 41 | ### 3. Accessing web3 function context 42 | 43 | Web3 function context which includes, `secrets`, `userArgs`, `multiChainProvider` can be accessed normally in `onRun.js` as `context` is passed as arguments. 44 | 45 | In `/web3-functions/private/index.ts`: 46 | 47 | ```ts 48 | const onRunFunction = new Function("context", "ky", "ethers", onRunScript); 49 | ``` 50 | 51 | ### 4. Return web3 function result 52 | 53 | Results returned in `onRun.js` will be bubbled. 54 | 55 | In `/web3-functions/private/onRun.js`: 56 | 57 | ```ts 58 | return { 59 | canExec: true, 60 | callData: [ 61 | { 62 | to: oracleAddress, 63 | data: oracle.interface.encodeFunctionData("updatePrice", [price]), 64 | }, 65 | ], 66 | }; 67 | ``` 68 | 69 | ## Creating your private web3 function task. 70 | 71 | ### Secrets required (strict) 72 | 73 | - `GIST_ID` (Github gist id to fecth `onRun.js` from) 74 | 75 | ### Arguments required (not strict) 76 | 77 | - `args` (JSON string of arguments to have more linean arguments in case content of `onRun.js` is updated) 78 | 79 | Example: 80 | 81 | In `/web3-functions/private/schema.json` 82 | 83 | ```json 84 | "userArgs": { 85 | "args": "string" 86 | } 87 | ``` 88 | 89 | In `/web3-functions/private/userArgs.json` 90 | 91 | ```json 92 | { 93 | "args": "{\"currency\":\"ethereum\",\"oracle\":\"0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da\"}" 94 | } 95 | ``` 96 | 97 | ## Testing 98 | 99 | Create a `.env` file with secrets: 100 | 101 | ``` 102 | GIST_ID=0c58ee8ce55bc7af5f42a2d75c27433c 103 | ``` 104 | 105 | Run `$ npx w3f test ./web3-functions/private/index.ts --logs --debug` 106 | -------------------------------------------------------------------------------- /web3-functions/private/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Web3Function, 3 | Web3FunctionContext, 4 | Web3FunctionResult, 5 | } from "@gelatonetwork/web3-functions-sdk"; 6 | import { Octokit } from "octokit"; 7 | 8 | // import dependencies used in onRun.js 9 | import { ethers } from "ethers"; 10 | import ky from "ky"; 11 | 12 | Web3Function.onRun(async (context: Web3FunctionContext) => { 13 | const { secrets } = context; 14 | 15 | const gistId = (await secrets.get("GIST_ID")) as string; 16 | 17 | const octokit = new Octokit(); 18 | 19 | let onRunScript: string | undefined; 20 | 21 | // fetch onRun.js from private github gist 22 | try { 23 | const gistDetails = await octokit.rest.gists.get({ 24 | gist_id: gistId, 25 | }); 26 | 27 | const files = gistDetails.data.files; 28 | 29 | if (!files) throw new Error(`No files in gist`); 30 | 31 | for (const file of Object.values(files)) { 32 | if (file?.filename === "onRun.js" && file.content) { 33 | onRunScript = file.content; 34 | break; 35 | } 36 | } 37 | 38 | if (!onRunScript) throw new Error(`No onRun.js`); 39 | } catch (err) { 40 | return { 41 | canExec: false, 42 | message: `Error fetching gist: ${err.message}`, 43 | }; 44 | } 45 | 46 | // run onRun.js 47 | try { 48 | /** 49 | * context are passed into onRun.js. 50 | * onRun.js will have access to all userArgs, secrets & storage 51 | */ 52 | const onRunFunction = new Function("context", "ky", "ethers", onRunScript); 53 | const onRunResult: Web3FunctionResult = await onRunFunction( 54 | context, 55 | ky, 56 | ethers 57 | ); 58 | 59 | if (onRunResult) { 60 | return onRunResult; 61 | } else { 62 | return { canExec: false, message: `No result returned` }; 63 | } 64 | } catch (err) { 65 | console.log(err); 66 | return { 67 | canExec: false, 68 | message: `Error running gist: ${err.message}`, 69 | }; 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /web3-functions/private/onRun.js: -------------------------------------------------------------------------------- 1 | return (async () => { 2 | const ORACLE_ABI = [ 3 | "function lastUpdated() external view returns(uint256)", 4 | "function updatePrice(uint256)", 5 | ]; 6 | 7 | const { userArgs, multiChainProvider } = context; 8 | const args = JSON.parse(userArgs.args); 9 | 10 | const provider = multiChainProvider.default(); 11 | 12 | // Retrieve Last oracle update time 13 | let lastUpdated; 14 | let oracle; 15 | 16 | const oracleAddress = 17 | args.oracle ?? "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da"; 18 | 19 | try { 20 | oracle = new ethers.Contract(oracleAddress, ORACLE_ABI, provider); 21 | lastUpdated = parseInt(await oracle.lastUpdated()); 22 | console.log(`Last oracle update: ${lastUpdated}`); 23 | } catch (err) { 24 | console.log("Error: ", err); 25 | return { canExec: false, message: `Rpc call failed` }; 26 | } 27 | 28 | // Check if it's ready for a new update 29 | const nextUpdateTime = lastUpdated + 300; // 5 min 30 | const timestamp = (await provider.getBlock("latest")).timestamp; 31 | console.log(`Next oracle update: ${nextUpdateTime}`); 32 | if (timestamp < nextUpdateTime) { 33 | return { canExec: false, message: `Time not elapsed` }; 34 | } 35 | 36 | // Get current price on coingecko 37 | const currency = args.currency ?? "ethereum"; 38 | let price = 0; 39 | try { 40 | const priceData = await ky 41 | .get( 42 | `https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=usd`, 43 | { timeout: 5_000, retry: 0 } 44 | ) 45 | .json(); 46 | price = Math.floor(priceData[currency].usd); 47 | } catch (err) { 48 | return { canExec: false, message: `Coingecko call failed` }; 49 | } 50 | console.log(`Updating price: ${price}`); 51 | 52 | // Return execution call data 53 | return { 54 | canExec: true, 55 | callData: [ 56 | { 57 | to: oracleAddress, 58 | data: oracle.interface.encodeFunctionData("updatePrice", [price]), 59 | }, 60 | ], 61 | }; 62 | })(); 63 | -------------------------------------------------------------------------------- /web3-functions/private/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3FunctionVersion": "2.0.0", 3 | "runtime": "js-1.0", 4 | "memory": 128, 5 | "timeout": 30, 6 | "userArgs": { 7 | "args": "string" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /web3-functions/private/userArgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "args": "{\"currency\":\"ethereum\",\"oracle\":\"0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da\"}" 3 | } 4 | -------------------------------------------------------------------------------- /web3-functions/secrets/.env.example: -------------------------------------------------------------------------------- 1 | COINGECKO_API=https://api.coingecko.com/api/v3 2 | -------------------------------------------------------------------------------- /web3-functions/secrets/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Web3Function, 3 | Web3FunctionContext, 4 | } from "@gelatonetwork/web3-functions-sdk"; 5 | import { Contract } from "@ethersproject/contracts"; 6 | import ky from "ky"; // we recommend using ky as axios doesn't support fetch by default 7 | 8 | const ORACLE_ABI = [ 9 | "function lastUpdated() external view returns(uint256)", 10 | "function updatePrice(uint256)", 11 | ]; 12 | 13 | Web3Function.onRun(async (context: Web3FunctionContext) => { 14 | const { userArgs, multiChainProvider } = context; 15 | 16 | const provider = multiChainProvider.default(); 17 | 18 | // Retrieve Last oracle update time 19 | const oracleAddress = 20 | (userArgs.oracle as string) ?? "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da"; 21 | let lastUpdated; 22 | let oracle; 23 | try { 24 | oracle = new Contract(oracleAddress, ORACLE_ABI, provider); 25 | lastUpdated = parseInt(await oracle.lastUpdated()); 26 | console.log(`Last oracle update: ${lastUpdated}`); 27 | } catch (err) { 28 | return { canExec: false, message: `Rpc call failed` }; 29 | } 30 | 31 | // Check if it's ready for a new update 32 | const nextUpdateTime = lastUpdated + 300; // 5 min 33 | const timestamp = (await provider.getBlock("latest")).timestamp; 34 | console.log(`Next oracle update: ${nextUpdateTime}`); 35 | if (timestamp < nextUpdateTime) { 36 | return { canExec: false, message: `Time not elapsed` }; 37 | } 38 | 39 | // Get current price on coingecko 40 | const currency = (userArgs.currency as string) ?? "ethereum"; 41 | let price = 0; 42 | try { 43 | // Get api from secrets 44 | const coingeckoApi = await context.secrets.get("COINGECKO_API"); 45 | if (!coingeckoApi) 46 | return { canExec: false, message: `COINGECKO_API not set in secrets` }; 47 | 48 | const coingeckoSimplePriceApi = `${coingeckoApi}/simple/price?ids=${currency}&vs_currencies=usd`; 49 | console.log(coingeckoSimplePriceApi); 50 | 51 | const priceData: { [key: string]: { usd: number } } = await ky 52 | .get(coingeckoSimplePriceApi, { timeout: 5_000, retry: 0 }) 53 | .json(); 54 | price = Math.floor(priceData[currency].usd); 55 | } catch (err) { 56 | return { canExec: false, message: `Coingecko call failed` }; 57 | } 58 | console.log(`Updating price: ${price}`); 59 | 60 | // Return execution call data 61 | return { 62 | canExec: true, 63 | callData: [ 64 | { 65 | to: oracleAddress, 66 | data: oracle.interface.encodeFunctionData("updatePrice", [price]), 67 | }, 68 | ], 69 | }; 70 | }); 71 | -------------------------------------------------------------------------------- /web3-functions/secrets/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3FunctionVersion": "2.0.0", 3 | "runtime": "js-1.0", 4 | "memory": 128, 5 | "timeout": 30, 6 | "userArgs": { 7 | "currency": "string", 8 | "oracle": "string" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web3-functions/secrets/storage.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /web3-functions/secrets/userArgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "currency": "ethereum", 3 | "oracle": "0x71B9B0F6C999CBbB0FeF9c92B80D54e4973214da" 4 | } 5 | -------------------------------------------------------------------------------- /web3-functions/storage/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Web3Function, 3 | Web3FunctionContext, 4 | } from "@gelatonetwork/web3-functions-sdk"; 5 | 6 | Web3Function.onRun(async (context: Web3FunctionContext) => { 7 | const { storage, multiChainProvider } = context; 8 | 9 | const provider = multiChainProvider.default(); 10 | 11 | // Use storage to retrieve previous state (stored values are always string) 12 | const lastBlockStr = (await storage.get("lastBlockNumber")) ?? "0"; 13 | const lastBlock = parseInt(lastBlockStr); 14 | console.log(`Last block: ${lastBlock}`); 15 | 16 | const newBlock = await provider.getBlockNumber(); 17 | console.log(`New block: ${newBlock}`); 18 | if (newBlock > lastBlock) { 19 | // Update storage to persist your current state (values must be cast to string) 20 | await storage.set("lastBlockNumber", newBlock.toString()); 21 | } 22 | 23 | return { 24 | canExec: false, 25 | message: `Updated block number: ${newBlock.toString()}`, 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /web3-functions/storage/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3FunctionVersion": "2.0.0", 3 | "runtime": "js-1.0", 4 | "memory": 128, 5 | "timeout": 30, 6 | "userArgs": {} 7 | } 8 | -------------------------------------------------------------------------------- /web3-functions/storage/storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "lastBlockNumber": "1000" 3 | } 4 | -------------------------------------------------------------------------------- /web3-functions/storage/userArgs.json: -------------------------------------------------------------------------------- 1 | {} 2 | --------------------------------------------------------------------------------