├── .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 |
--------------------------------------------------------------------------------