diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5dc6e1c..7a328bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,7 @@ env: ARBITRUM_PROVIDER_URL: https://1rpc.io/arb OPTIMISM_PROVIDER_URL: https://endpoints.omniatech.io/v1/op/mainnet/public MAINNET_PROVIDER_URL: https://eth.llamarpc.com + ARBITRUM_RPC_URL: ${{ secrets.ARBITRUM_RPC_URL }} ZEROX_API_KEY: ${{ secrets.ZEROX_API_KEY }} jobs: @@ -14,7 +15,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: foundry-rs/foundry-toolchain@v1 - uses: oven-sh/setup-bun@v2 - - run: bun install + - uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun- + - run: bun install --frozen-lockfile - run: bun run lint:ci + - run: bun test - run: bun run test diff --git a/bun.lockb b/bun.lockb index af256e0..b16b24f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/hardhat.config.ts b/hardhat.config.ts index 399aec8..fdd673c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -3,7 +3,9 @@ import "@gelatonetwork/web3-functions-sdk/hardhat-plugin"; import "@nomiclabs/hardhat-ethers"; import "@foundry-rs/hardhat-anvil"; -const PRIVATE_KEY = process.env.PRIVATE_KEY; +const PRIVATE_KEY = + process.env.PRIVATE_KEY ?? + "0x0000000000000000000000000000000000000000000000000000000000000001"; const config = { w3f: { @@ -30,57 +32,57 @@ const config = { launch: true, chainId: 1, forkUrl: "https://rpc.ankr.com/eth", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, ethereum: { chainId: 1, url: "https://rpc.ankr.com/eth", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, avalanche: { url: "https://api.avax.network/ext/bc/C/rpc", chainId: 43114, - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, arbitrum: { chainId: 42161, url: "https://arb1.arbitrum.io/rpc", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, fantom: { chainId: 250, url: "https://rpc2.fantom.network", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, optimism: { chainId: 10, url: "https://mainnet.optimism.io", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, polygon: { chainId: 137, url: "https://rpc-mainnet.maticvigil.com", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, bsc: { chainId: 56, url: "https://bsc.publicnode.com", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, kava: { chainId: 2222, url: "https://kava-evm.publicnode.com", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, base: { chainId: 8453, url: "https://base.meowrpc.com", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, linea: { chainId: 59144, url: "https://rpc.linea.build", - accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], + accounts: [PRIVATE_KEY], }, }, // biome-ignore lint/complexity/noBannedTypes: Improve auto-completion diff --git a/package.json b/package.json index 81bc4d8..ee74308 100644 --- a/package.json +++ b/package.json @@ -64,11 +64,11 @@ }, "devDependencies": { "@biomejs/biome": "^1.8.3", - "@foundry-rs/easy-foundryup": "^0.1.3", "@foundry-rs/hardhat-anvil": "^0.1.7", "@nomiclabs/hardhat-ethers": "^2.2.3", "@tsconfig/recommended": "^1.0.7", "@types/bun": "latest", + "@viem/anvil": "^0.0.10", "concurrently": "^8.2.2", "ethereum-waffle": "^3.4.4", "ethers": "^5.7.2", @@ -76,6 +76,7 @@ "husky": "^9.1.1", "lint-staged": "^15.2.7", "ts-node": "10.9.2", + "type-fest": "^4.23.0", "typescript": "5.5.3" }, "dependencies": { @@ -91,7 +92,6 @@ "bufferutil", "core-js", "core-js-pure", - "cpu-features", "deno-bin", "es5-ext", "esbuild", diff --git a/test/reward-distributor.test.ts b/test/reward-distributor.test.ts new file mode 100644 index 0000000..bfb87f5 --- /dev/null +++ b/test/reward-distributor.test.ts @@ -0,0 +1,168 @@ +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + test, +} from "bun:test"; +import path from "node:path"; +import type { JsonRpcProvider } from "@ethersproject/providers"; +import type { Anvil } from "@viem/anvil"; +import type { Merge } from "type-fest"; +import { + type Address, + type Hex, + type PublicClient, + type TestClient, + encodeFunctionData, + parseAbi, +} from "viem"; +import { ARBITRUM_OPS_SAFE, ARBITRUM_SPELL } from "../utils/constants"; +import { runWeb3Function } from "./utils"; +import { setupAnvil } from "./utils/setupAnvil"; + +const w3fName = "reward-distributor"; +const w3fRootDir = path.join("web3-functions"); +const w3fPath = path.join(w3fRootDir, w3fName, "index.ts"); + +describe("Reward Distributor Web3 Function test", () => { + let anvil: Anvil; + let provider: JsonRpcProvider; + let testClient: Omit, "mode">; + let snapshotId: Hex; + + beforeAll(async () => { + ({ anvil, provider, testClient, snapshotId } = await setupAnvil({ + forkUrl: process.env.ARBITRUM_RPC_URL, + forkBlockNumber: 234134609n, + })); + }); + + afterAll(async () => { + await anvil.stop(); + }); + + afterEach(async () => { + await testClient.revert({ id: snapshotId }); + }); + + const run = async () => + runWeb3Function( + w3fPath, + { + gelatoArgs: { + chainId: (await provider.getNetwork()).chainId, + gasPrice: (await provider.getGasPrice()).toString(), + }, + userArgs: { + multiRewardDistributorAddress: + "0xbF5DC3f598AFA173135160CDFce6BFeE45c912eF", + multiRewardStakingAddresses: [ + "0x280c64c4C4869CF2A6762EaDD4701360C1B11F97", + "0xc30911b52b5752447aB08615973e434c801CD652", + ], + epochBasedDistributorAddress: + "0x111AbF466654c166Ee4AC15d6A29a3e0625533db", + epochBasedStakingAddresses: [], + }, + secrets: {}, + storage: {}, + }, + [provider], + ); + + test( + "canExec: true - Multiple distributions", + async () => { + const { result } = await run(); + + expect(result).toEqual({ + canExec: true, + callData: [ + { + data: "0x63453ae1000000000000000000000000280c64c4c4869cf2a6762eadd4701360c1b11f97", + to: "0xbF5DC3f598AFA173135160CDFce6BFeE45c912eF", + }, + { + data: "0x63453ae1000000000000000000000000c30911b52b5752447ab08615973e434c801cd652", + to: "0xbF5DC3f598AFA173135160CDFce6BFeE45c912eF", + }, + ], + }); + }, + { timeout: 60000 }, + ); + test( + "canExec: true - Single distributions", + async () => { + const execAddress = + "0x280c64c4C4869CF2A6762EaDD4701360C1B11F97" as const satisfies Address; + await testClient.sendUnsignedTransaction({ + from: ARBITRUM_OPS_SAFE, + to: "0xbF5DC3f598AFA173135160CDFce6BFeE45c912eF", + data: encodeFunctionData({ + abi: parseAbi(["function distribute(address) external"]), + functionName: "distribute", + args: [execAddress], + }), + gasPrice: 0n, + }); + + const { result } = await run(); + + expect(result).toEqual({ + canExec: true, + callData: [ + { + data: "0x63453ae1000000000000000000000000c30911b52b5752447ab08615973e434c801cd652", + to: "0xbF5DC3f598AFA173135160CDFce6BFeE45c912eF", + }, + ], + }); + }, + { timeout: 60000 }, + ); + test( + "canExec: false - No distributions to execute", + async () => { + // Increase approval + await testClient.setStorageAt({ + address: ARBITRUM_SPELL, + index: + "0xe291b9f68327d6549fd70e333daad56b6cdb38ac27f870dafc9c5d1dcd54d5a5", + value: + "0x0000000000000000000000000000000000000000002116545850052128000000", + }); + + const execAddresses = [ + "0x280c64c4C4869CF2A6762EaDD4701360C1B11F97", + "0xc30911b52b5752447ab08615973e434c801cd652", + ] as const satisfies Array
; + + await Promise.all( + execAddresses.map( + async (execAddress) => + await testClient.sendUnsignedTransaction({ + from: ARBITRUM_OPS_SAFE, + to: "0xbF5DC3f598AFA173135160CDFce6BFeE45c912eF", + data: encodeFunctionData({ + abi: parseAbi(["function distribute(address) external"]), + functionName: "distribute", + args: [execAddress], + }), + gasPrice: 0n, + }), + ), + ); + + const { result } = await run(); + + expect(result).toEqual({ + canExec: false, + message: "No distributions to execute", + }); + }, + { timeout: 60000 }, + ); +}); diff --git a/test/utils/index.ts b/test/utils/index.ts new file mode 100644 index 0000000..603e439 --- /dev/null +++ b/test/utils/index.ts @@ -0,0 +1,68 @@ +import type { JsonRpcProvider } from "@ethersproject/providers"; +import type { + MultiChainProviderConfig, + Web3FunctionContextData, + Web3FunctionRunnerOptions, +} from "@gelatonetwork/web3-functions-sdk"; +import { Web3FunctionBuilder } from "@gelatonetwork/web3-functions-sdk/builder"; +import { Web3FunctionRunner } from "@gelatonetwork/web3-functions-sdk/runtime"; + +export const MAX_RPC_LIMIT = 100; +export const MAX_DOWNLOAD_LIMIT = 10 * 1024 * 1024; +export const MAX_UPLOAD_LIMIT = 5 * 1024 * 1024; +export const MAX_REQUEST_LIMIT = 100; +export const MAX_STORAGE_LIMIT = 1 * 1024 * 1024; + +export const runWeb3Function = async ( + web3FunctionPath: string, + context: Web3FunctionContextData<"onRun">, + providers: JsonRpcProvider[], +) => { + const buildRes = await Web3FunctionBuilder.build(web3FunctionPath, { + debug: false, + }); + + if (!buildRes.success) + throw new Error(`Fail to build web3Function: ${buildRes.error}`); + + const runner = new Web3FunctionRunner(false); + const runtime: "docker" | "thread" = "thread"; + const memory = buildRes.schema.memory; + const rpcLimit = MAX_RPC_LIMIT; + const timeout = buildRes.schema.timeout * 1000; + const version = buildRes.schema.web3FunctionVersion; + + const options: Web3FunctionRunnerOptions = { + runtime, + showLogs: true, + memory, + downloadLimit: MAX_DOWNLOAD_LIMIT, + uploadLimit: MAX_UPLOAD_LIMIT, + requestLimit: MAX_REQUEST_LIMIT, + rpcLimit, + timeout, + storageLimit: MAX_STORAGE_LIMIT, + }; + const script = buildRes.filePath; + + const multiChainProviderConfig: MultiChainProviderConfig = {}; + + for (const provider of providers) { + const chainId = (await provider.getNetwork()).chainId; + + multiChainProviderConfig[chainId] = provider; + } + + const res = await runner.run("onRun", { + script, + context, + options, + version, + multiChainProviderConfig, + }); + + if (!res.success) + throw new Error(`Fail to run web3 function: ${res.error.message}`); + + return res; +}; diff --git a/test/utils/setupAnvil.ts b/test/utils/setupAnvil.ts new file mode 100644 index 0000000..4b0ff46 --- /dev/null +++ b/test/utils/setupAnvil.ts @@ -0,0 +1,36 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { type CreateAnvilOptions, createAnvil } from "@viem/anvil"; +import * as R from "remeda"; +import { http, type Chain, createTestClient, publicActions } from "viem"; + +const defaultCreateAnvilOptions: CreateAnvilOptions = { + blockBaseFeePerGas: 0n, + gasPrice: 0n, +}; + +export async function setupAnvil( + createAnvilOptions: CreateAnvilOptions = defaultCreateAnvilOptions, +) { + const anvil = createAnvil( + R.merge(defaultCreateAnvilOptions, createAnvilOptions), + ); + + await anvil.start(); + + const anvilEndpoint = `http://${anvil.host}:${anvil.port}`; + const provider = new JsonRpcProvider(anvilEndpoint); + const testClient = createTestClient({ + chain: { + contracts: { + multicall3: { + address: "0xcA11bde05977b3631167028862bE2a173976CA11", + }, + }, + } as unknown as Chain, + transport: http(`http://${anvil.host}:${anvil.port}`), + mode: "anvil", + }).extend(publicActions); + const snapshotId = await testClient.snapshot(); + + return { anvil, anvilEndpoint, provider, testClient, snapshotId }; +} diff --git a/utils/constants.ts b/utils/constants.ts index 584e0c4..c84192d 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1 +1,7 @@ +import type { Address } from "viem"; + export const DEVOPS_SAFE = "0x48c18844530c96AaCf24568fa7F912846aAc12B9"; +export const ARBITRUM_SPELL = + "0x3e6648c5a70a150a88bce65f4ad4d506fe15d2af" as const satisfies Address; +export const ARBITRUM_OPS_SAFE = + "0xA71A021EF66B03E45E0d85590432DFCfa1b7174C" as const satisfies Address;