From dedaa34c9feb357682df0efb47d2060aa5ec7a60 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:07:36 +0330 Subject: [PATCH 001/132] merge with main repo latest changes --- .vscode/launch.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..37fcc6042b0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**"], + "program": "$${workspaceFolder}/**/*.ts", + "outFiles": ["${workspaceFolder}/**/*.js"] + } + ] +} From b4390e78b7122ea341e4940a0d87cc582ce7b634 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:05:08 +0330 Subject: [PATCH 002/132] feat: starknet deployer module --- typescript/sdk/package.json | 1 + typescript/sdk/src/deploy/StarknetDeployer.ts | 180 +++++++++++++ yarn.lock | 238 +++++++++++++++++- 3 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 typescript/sdk/src/deploy/StarknetDeployer.ts diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 133a5792753..14a773f61dd 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -22,6 +22,7 @@ "cross-fetch": "^3.1.5", "ethers": "^5.7.2", "pino": "^8.19.0", + "starknet": "6.11.0", "viem": "^1.20.0", "zod": "^3.21.2" }, diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts new file mode 100644 index 00000000000..d84dd1355fa --- /dev/null +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -0,0 +1,180 @@ +import fs from 'fs'; +import path from 'path'; +import { Logger } from 'pino'; +import { + Account, + CallData, + ContractFactory, + ContractFactoryParams, +} from 'starknet'; +import { getCompiledContract, getCompiledContractCasm } from 'starknet-core'; + +import { rootLogger } from '@hyperlane-xyz/utils'; + +export interface StarknetContractConfig { + name: string; + constructor: Record; +} + +export interface StarknetDeployConfig { + contracts: Record; + deploymentOrder: string[]; +} + +export interface StarknetDeployerOptions { + logger?: Logger; + deploymentsDir?: string; + configsDir?: string; + accountAddress?: string; + network?: string; +} + +export class StarknetDeployer { + private readonly logger: Logger; + private readonly deploymentsDir: string; + private readonly configsDir: string; + private readonly deployedContracts: Record = {}; + + constructor( + private readonly account: Account, + private readonly options: StarknetDeployerOptions = {}, + ) { + this.logger = + options.logger ?? rootLogger.child({ module: 'starknet-deployer' }); + this.deploymentsDir = options.deploymentsDir ?? 'deployments'; + this.configsDir = options.configsDir ?? 'configs'; + } + + private processConstructorArgs( + args: Record, + ): any { + return Object.entries(args).reduce((acc, [key, { type, value }]) => { + if (typeof value === 'string' && value.startsWith('$')) { + if (value === '$OWNER_ADDRESS') { + acc[key] = this.options.accountAddress; + } else if (value === '$BENEFICIARY_ADDRESS') { + acc[key] = process.env.BENEFICIARY_ADDRESS; + } else { + const contractName = value.slice(1); + if (this.deployedContracts[contractName]) { + acc[key] = this.deployedContracts[contractName]; + } else { + throw new Error( + `Contract ${contractName} not yet deployed, required for ${key}`, + ); + } + } + } else { + acc[key] = value; + } + return acc; + }, {} as any); + } + + private ensureNetworkDirectory(network: string): string { + if (!network) { + throw new Error('Network must be specified'); + } + + const networkDir = path.join(this.deploymentsDir, network); + if (!fs.existsSync(this.deploymentsDir)) { + fs.mkdirSync(this.deploymentsDir); + } + if (!fs.existsSync(networkDir)) { + fs.mkdirSync(networkDir); + } + + return networkDir; + } + + private getConfigPath(network: string): string { + if (!network) { + throw new Error('Network must be specified'); + } + + const configFileName = `${network.toLowerCase()}.json`; + const configPath = path.join(this.configsDir, configFileName); + + if (!fs.existsSync(configPath)) { + throw new Error( + `Config file not found for network ${network} at ${configPath}`, + ); + } + + return configPath; + } + + async deployContract( + contractName: string, + constructorArgs: StarknetContractConfig['constructor'], + ): Promise { + this.logger.info(`Deploying contract ${contractName}...`); + + const compiledContract = getCompiledContract(contractName); + const casm = getCompiledContractCasm(contractName); + const processedArgs = this.processConstructorArgs(constructorArgs); + const constructorCalldata = CallData.compile(processedArgs); + + const params: ContractFactoryParams = { + compiledContract, + account: this.account, + casm, + }; + + const contractFactory = new ContractFactory(params); + const contract = await contractFactory.deploy(constructorCalldata); + + let address = contract.address; + // Ensure the address is 66 characters long (including the '0x' prefix) + if (address.length < 66) { + address = '0x' + address.slice(2).padStart(64, '0'); + } + + this.logger.info( + `Contract ${contractName} deployed at address: ${address}`, + ); + this.deployedContracts[contractName] = address; + + return address; + } + + async deploy(network: string): Promise> { + try { + const configPath = this.getConfigPath(network); + const config: StarknetDeployConfig = JSON.parse( + fs.readFileSync(configPath, 'utf-8'), + ); + + const networkDir = this.ensureNetworkDirectory(network); + const deploymentsFile = path.join(networkDir, 'deployments.json'); + + for (const contractName of config.deploymentOrder) { + await this.deployContract( + contractName, + config.contracts[contractName].constructor, + ); + } + + this.logger.info( + 'All contracts deployed successfully:', + this.deployedContracts, + ); + + // Write deployments to network-specific file + fs.writeFileSync( + deploymentsFile, + JSON.stringify(this.deployedContracts, null, 2), + ); + this.logger.info(`Deployed contracts saved to ${deploymentsFile}`); + + return this.deployedContracts; + } catch (error) { + this.logger.error('Deployment failed:', error); + throw error; + } + } + + getDeployedContracts(): Record { + return { ...this.deployedContracts }; + } +} diff --git a/yarn.lock b/yarn.lock index ecd030eab6a..d495d04a09c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8087,6 +8087,7 @@ __metadata: pino: "npm:^8.19.0" prettier: "npm:^2.8.8" sinon: "npm:^13.0.2" + starknet: "npm:6.11.0" ts-node: "npm:^10.8.0" tsx: "npm:^4.7.1" typescript: "npm:5.3.3" @@ -9170,6 +9171,24 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:~1.3.0": + version: 1.3.0 + resolution: "@noble/curves@npm:1.3.0" + dependencies: + "@noble/hashes": "npm:1.3.3" + checksum: f3cbdd1af00179e30146eac5539e6df290228fb857a7a8ba36d1a772cbe59288a2ca83d06f175d3446ef00db3a80d7fd8b8347f7de9c2d4d5bf3865d8bb78252 + languageName: node + linkType: hard + +"@noble/curves@npm:~1.4.0": + version: 1.4.2 + resolution: "@noble/curves@npm:1.4.2" + dependencies: + "@noble/hashes": "npm:1.4.0" + checksum: f433a2e8811ae345109388eadfa18ef2b0004c1f79417553241db4f0ad0d59550be6298a4f43d989c627e9f7551ffae6e402a4edf0173981e6da95fc7cab5123 + languageName: node + linkType: hard + "@noble/hashes@npm:1.0.0, @noble/hashes@npm:~1.0.0": version: 1.0.0 resolution: "@noble/hashes@npm:1.0.0" @@ -9191,13 +9210,27 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:~1.3.2": +"@noble/hashes@npm:1.3.3, @noble/hashes@npm:~1.3.2, @noble/hashes@npm:~1.3.3": version: 1.3.3 resolution: "@noble/hashes@npm:1.3.3" checksum: 1025ddde4d24630e95c0818e63d2d54ee131b980fe113312d17ed7468bc18f54486ac86c907685759f8a7e13c2f9b9e83ec7b67d1cc20836f36b5e4a65bb102d languageName: node linkType: hard +"@noble/hashes@npm:1.4.0": + version: 1.4.0 + resolution: "@noble/hashes@npm:1.4.0" + checksum: e156e65794c473794c52fa9d06baf1eb20903d0d96719530f523cc4450f6c721a957c544796e6efd0197b2296e7cd70efeb312f861465e17940a3e3c7e0febc6 + languageName: node + linkType: hard + +"@noble/hashes@npm:^1.4.0": + version: 1.5.0 + resolution: "@noble/hashes@npm:1.5.0" + checksum: da7fc7af52af7afcf59810a7eea6155075464ff462ffda2572dc6d57d53e2669b1ea2ec774e814f6273f1697e567f28d36823776c9bf7068cba2a2855140f26e + languageName: node + linkType: hard + "@noble/secp256k1@npm:1.5.5, @noble/secp256k1@npm:~1.5.2": version: 1.5.5 resolution: "@noble/secp256k1@npm:1.5.5" @@ -11155,6 +11188,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:~1.1.3": + version: 1.1.9 + resolution: "@scure/base@npm:1.1.9" + checksum: f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb + languageName: node + linkType: hard + "@scure/bip32@npm:1.0.1": version: 1.0.1 resolution: "@scure/bip32@npm:1.0.1" @@ -11197,6 +11237,16 @@ __metadata: languageName: node linkType: hard +"@scure/starknet@npm:~1.0.0": + version: 1.0.0 + resolution: "@scure/starknet@npm:1.0.0" + dependencies: + "@noble/curves": "npm:~1.3.0" + "@noble/hashes": "npm:~1.3.3" + checksum: 0f7a627cfa3cf5f679adc805e63c3d087f2e2501347a73d5d9cf72d7e54f788bc27f228901c75818e0fcc82b4d847527b958f2a192e8572e88526c4fca93085c + languageName: node + linkType: hard + "@sentry/core@npm:5.30.0": version: 5.30.0 resolution: "@sentry/core@npm:5.30.0" @@ -12023,6 +12073,13 @@ __metadata: languageName: node linkType: hard +"@starknet-io/types-js@npm:^0.7.7, starknet-types-07@npm:@starknet-io/types-js@^0.7.7": + version: 0.7.7 + resolution: "@starknet-io/types-js@npm:0.7.7" + checksum: 12c80c34c167d51ebe2cbd210703749aa74faf341cefb5cc44b118418279e095efe93f3ebc90f8b828888f66a6f230522a22c87bfed53a49ddf515637c0b9b5f + languageName: node + linkType: hard + "@storybook/addon-actions@npm:7.6.20": version: 7.6.20 resolution: "@storybook/addon-actions@npm:7.6.20" @@ -14393,6 +14450,20 @@ __metadata: languageName: node linkType: hard +"abi-wan-kanabi@npm:^2.2.2": + version: 2.2.3 + resolution: "abi-wan-kanabi@npm:2.2.3" + dependencies: + ansicolors: "npm:^0.3.2" + cardinal: "npm:^2.1.1" + fs-extra: "npm:^10.0.0" + yargs: "npm:^17.7.2" + bin: + generate: dist/generate.js + checksum: 112ff2ee880ada687e033be1de3680a6ee51d411e0c13a7aa1160349ecc8ec0b79d2b11f352b8049db7aa45e8d10090bd99123905007423ee1ace9fb89f740de + languageName: node + linkType: hard + "abitype@npm:0.9.8": version: 0.9.8 resolution: "abitype@npm:0.9.8" @@ -14808,6 +14879,13 @@ __metadata: languageName: node linkType: hard +"ansicolors@npm:^0.3.2, ansicolors@npm:~0.3.2": + version: 0.3.2 + resolution: "ansicolors@npm:0.3.2" + checksum: 0704d1485d84d65a47aacd3d2d26f501f21aeeb509922c8f2496d0ec5d346dc948efa64f3151aef0571d73e5c44eb10fd02f27f59762e9292fe123bb1ea9ff7d + languageName: node + linkType: hard + "antlr4@npm:^4.13.1-patch-1": version: 4.13.1 resolution: "antlr4@npm:4.13.1" @@ -16211,6 +16289,18 @@ __metadata: languageName: node linkType: hard +"cardinal@npm:^2.1.1": + version: 2.1.1 + resolution: "cardinal@npm:2.1.1" + dependencies: + ansicolors: "npm:~0.3.2" + redeyed: "npm:~2.1.0" + bin: + cdl: ./bin/cdl.js + checksum: caf0d34739ef7b1d80e1753311f889997b62c4490906819eb5da5bd46e7f5e5caba7a8a96ca401190c7d9c18443a7749e5338630f7f9a1ae98d60cac49b9008e + languageName: node + linkType: hard + "case@npm:^1.6.3": version: 1.6.3 resolution: "case@npm:1.6.3" @@ -19766,6 +19856,16 @@ __metadata: languageName: node linkType: hard +"fetch-cookie@npm:^3.0.0": + version: 3.0.1 + resolution: "fetch-cookie@npm:3.0.1" + dependencies: + set-cookie-parser: "npm:^2.4.8" + tough-cookie: "npm:^4.0.0" + checksum: 7ca3b56e71564e51a292f2590b1103b9223137f1a5163127c00339a6b02b3f3a216e0072f554e8f93c93899c52da72939b9d9bc72e76c8060f10c2b4867cfc85 + languageName: node + linkType: hard + "fetch-retry@npm:^5.0.2": version: 5.0.6 resolution: "fetch-retry@npm:5.0.6" @@ -20547,6 +20647,15 @@ __metadata: languageName: node linkType: hard +"get-starknet-core@npm:^4.0.0-next.3": + version: 4.0.0 + resolution: "get-starknet-core@npm:4.0.0" + dependencies: + "@starknet-io/types-js": "npm:^0.7.7" + checksum: e1ae3a3b866dc86d0e8d8b59517ffcdc9b7b47105e5738068d36c957c7cf03e4d6a7c1d36eff355fb65114430b10e9defb909aba94a0df654a115961f90bbed9 + languageName: node + linkType: hard + "get-stream@npm:^3.0.0": version: 3.0.0 resolution: "get-stream@npm:3.0.0" @@ -22447,6 +22556,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^3.0.0": + version: 3.0.0 + resolution: "isomorphic-fetch@npm:3.0.0" + dependencies: + node-fetch: "npm:^2.6.1" + whatwg-fetch: "npm:^3.4.1" + checksum: 568fe0307528c63405c44dd3873b7b6c96c0d19ff795cb15846e728b6823bdbc68cc8c97ac23324509661316f12f551e43dac2929bc7030b8bc4d6aa1158b857 + languageName: node + linkType: hard + "isomorphic-unfetch@npm:^3.0.0": version: 3.1.0 resolution: "isomorphic-unfetch@npm:3.1.0" @@ -23947,6 +24066,13 @@ __metadata: languageName: node linkType: hard +"lossless-json@npm:^4.0.1": + version: 4.0.2 + resolution: "lossless-json@npm:4.0.2" + checksum: 76de08676c94e3fa31f04fbb2be0a9c0096ff9f68dbb8275d777d55f2123754501b626b5f665d0ff0aabaf56e827b3f21b355cc81c4eac554080876623318b55 + languageName: node + linkType: hard + "loupe@npm:^2.3.1": version: 2.3.4 resolution: "loupe@npm:2.3.4" @@ -25927,7 +26053,7 @@ __metadata: languageName: node linkType: hard -"pako@npm:^2.0.2": +"pako@npm:^2.0.2, pako@npm:^2.0.4": version: 2.1.0 resolution: "pako@npm:2.1.0" checksum: 38a04991d0ec4f4b92794a68b8c92bf7340692c5d980255c92148da96eb3e550df7a86a7128b5ac0c65ecddfe5ef3bbe9c6dab13e1bc315086e759b18f7c1401 @@ -26771,6 +26897,13 @@ __metadata: languageName: node linkType: hard +"psl@npm:^1.1.33": + version: 1.9.0 + resolution: "psl@npm:1.9.0" + checksum: d07879d4bfd0ac74796306a8e5a36a93cfb9c4f4e8ee8e63fbb909066c192fe1008cd8f12abd8ba2f62ca28247949a20c8fb32e1d18831d9e71285a1569720f9 + languageName: node + linkType: hard + "pstree.remy@npm:^1.1.8": version: 1.1.8 resolution: "pstree.remy@npm:1.1.8" @@ -26941,6 +27074,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 46ab16f252fd892fc29d6af60966d338cdfeea68a231e9457631ffd22d67cec1e00141e0a5236a2eb16c0d7d74175d9ec1d6f963660c6f2b1c2fc85b194c5680 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2, queue-microtask@npm:^1.2.3": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -27395,6 +27535,15 @@ __metadata: languageName: node linkType: hard +"redeyed@npm:~2.1.0": + version: 2.1.1 + resolution: "redeyed@npm:2.1.1" + dependencies: + esprima: "npm:~4.0.0" + checksum: 86880f97d54bb55bbf1c338e27fe28f18f52afc2f5afa808354a09a3777aa79b4f04e04844350d7fec80aa2d299196bde256b21f586e7e5d9b63494bd4a9db27 + languageName: node + linkType: hard + "reduce-flatten@npm:^2.0.0": version: 2.0.0 resolution: "reduce-flatten@npm:2.0.0" @@ -27628,6 +27777,13 @@ __metadata: languageName: node linkType: hard +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: 878880ee78ccdce372784f62f52a272048e2d0827c29ae31e7f99da18b62a2b9463ea03a75f277352f4697c100183debb0532371ad515a2d49d4bfe596dd4c20 + languageName: node + linkType: hard + "resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" @@ -28416,6 +28572,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.4.8": + version: 2.7.1 + resolution: "set-cookie-parser@npm:2.7.1" + checksum: c92b1130032693342bca13ea1b1bc93967ab37deec4387fcd8c2a843c0ef2fd9a9f3df25aea5bb3976cd05a91c2cf4632dd6164d6e1814208fb7d7e14edd42b4 + languageName: node + linkType: hard + "set-function-length@npm:^1.1.1": version: 1.1.1 resolution: "set-function-length@npm:1.1.1" @@ -29193,6 +29356,27 @@ __metadata: languageName: node linkType: hard +"starknet@npm:6.11.0": + version: 6.11.0 + resolution: "starknet@npm:6.11.0" + dependencies: + "@noble/curves": "npm:~1.4.0" + "@noble/hashes": "npm:^1.4.0" + "@scure/base": "npm:~1.1.3" + "@scure/starknet": "npm:~1.0.0" + abi-wan-kanabi: "npm:^2.2.2" + fetch-cookie: "npm:^3.0.0" + get-starknet-core: "npm:^4.0.0-next.3" + isomorphic-fetch: "npm:^3.0.0" + lossless-json: "npm:^4.0.1" + pako: "npm:^2.0.4" + starknet-types-07: "npm:@starknet-io/types-js@^0.7.7" + ts-mixer: "npm:^6.0.3" + url-join: "npm:^4.0.1" + checksum: f2acd29277a908dbbb54dc6ebe533443d2449fc67d280ac0cec6d98ec06bbd83af497d438bb468eb3c6e7aec182c06ba134574183398361c63872ea67dbb2025 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -30136,6 +30320,18 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^4.0.0": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" + dependencies: + psl: "npm:^1.1.33" + punycode: "npm:^2.1.1" + universalify: "npm:^0.2.0" + url-parse: "npm:^1.5.3" + checksum: 75663f4e2cd085f16af0b217e4218772adf0617fb3227171102618a54ce0187a164e505d61f773ed7d65988f8ff8a8f935d381f87da981752c1171b076b4afac + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -30262,6 +30458,13 @@ __metadata: languageName: node linkType: hard +"ts-mixer@npm:^6.0.3": + version: 6.0.4 + resolution: "ts-mixer@npm:6.0.4" + checksum: f20571a4a4ff7b5e1a2ff659208c1ea9d4180dda932b71d289edc99e25a2948c9048e2e676b930302ac0f8e88279e0da6022823183e67de3906a3f3a8b72ea80 + languageName: node + linkType: hard + "ts-node@npm:^10.8.0": version: 10.9.2 resolution: "ts-node@npm:10.9.2" @@ -30883,6 +31086,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^0.2.0": + version: 0.2.0 + resolution: "universalify@npm:0.2.0" + checksum: e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5 + languageName: node + linkType: hard + "universalify@npm:^2.0.0": version: 2.0.0 resolution: "universalify@npm:2.0.0" @@ -30953,6 +31163,13 @@ __metadata: languageName: node linkType: hard +"url-join@npm:^4.0.1": + version: 4.0.1 + resolution: "url-join@npm:4.0.1" + checksum: b53b256a9a36ed6b0f6768101e78ca97f32d7b935283fd29ce19d0bbfb6f88aa80aa6c03fd87f2f8978ab463a6539f597a63051e7086f3379685319a7495f709 + languageName: node + linkType: hard + "url-parse-lax@npm:^1.0.0": version: 1.0.0 resolution: "url-parse-lax@npm:1.0.0" @@ -30962,6 +31179,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:^1.5.3": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: c9e96bc8c5b34e9f05ddfeffc12f6aadecbb0d971b3cc26015b58d5b44676a99f50d5aeb1e5c9e61fa4d49961ae3ab1ae997369ed44da51b2f5ac010d188e6ad + languageName: node + linkType: hard + "url-set-query@npm:^1.0.0": version: 1.0.0 resolution: "url-set-query@npm:1.0.0" @@ -31835,6 +32062,13 @@ __metadata: languageName: node linkType: hard +"whatwg-fetch@npm:^3.4.1": + version: 3.6.20 + resolution: "whatwg-fetch@npm:3.6.20" + checksum: 2b4ed92acd6a7ad4f626a6cb18b14ec982bbcaf1093e6fe903b131a9c6decd14d7f9c9ca3532663c2759d1bdf01d004c77a0adfb2716a5105465c20755a8c57c + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" From 02f664a2c6db3490b52838e41083f432852b9008 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 28 Oct 2024 16:41:48 +0100 Subject: [PATCH 003/132] feat: starknet contracts & workspace --- package.json | 1 + starknet/.gitignore | 7 + starknet/build.sh | 17 + starknet/cairo/.gitignore | 2 + starknet/cairo/.tool-versions | 2 + starknet/cairo/Scarb.lock | 103 +++ starknet/cairo/Scarb.toml | 23 + starknet/cairo/crates/contracts/Scarb.toml | 30 + .../src/client/gas_router_component.cairo | 187 +++++ .../contracts/src/client/mailboxclient.cairo | 70 ++ .../src/client/mailboxclient_component.cairo | 214 ++++++ .../src/client/router_component.cairo | 280 ++++++++ .../hooks/libs/standard_hook_metadata.cairo | 212 ++++++ .../src/hooks/merkle_tree_hook.cairo | 372 ++++++++++ .../contracts/src/hooks/protocol_fee.cairo | 213 ++++++ .../crates/contracts/src/interfaces.cairo | 315 +++++++++ .../src/isms/aggregation/aggregation.cairo | 207 ++++++ .../multisig/merkleroot_multisig_ism.cairo | 250 +++++++ .../multisig/messageid_multisig_ism.cairo | 226 ++++++ .../isms/multisig/validator_announce.cairo | 315 +++++++++ .../crates/contracts/src/isms/noop_ism.cairo | 34 + .../contracts/src/isms/pausable_ism.cairo | 111 +++ .../default_fallback_routing_ism.cairo | 292 ++++++++ .../src/isms/routing/domain_routing_ism.cairo | 282 ++++++++ .../src/isms/trusted_relayer_ism.cairo | 48 ++ starknet/cairo/crates/contracts/src/lib.cairo | 48 ++ .../src/libs/aggregation_ism_metadata.cairo | 135 ++++ .../contracts/src/libs/checkpoint_lib.cairo | 74 ++ .../contracts/src/libs/enumerable_map.cairo | 486 +++++++++++++ .../crates/contracts/src/libs/math.cairo | 25 + .../crates/contracts/src/libs/message.cairo | 176 +++++ .../multisig/merkleroot_ism_metadata.cairo | 130 ++++ .../multisig/message_id_ism_metadata.cairo | 87 +++ .../cairo/crates/contracts/src/mailbox.cairo | 519 ++++++++++++++ .../contracts/src/utils/keccak256.cairo | 569 ++++++++++++++++ .../contracts/src/utils/store_arrays.cairo | 63 ++ .../crates/contracts/src/utils/utils.cairo | 11 + .../tests/hooks/test_merkle_tree_hook.cairo | 187 +++++ .../tests/hooks/test_protocol_fee.cairo | 151 ++++ .../tests/isms/test_aggregation.cairo | 108 +++ .../tests/isms/test_default_ism.cairo | 100 +++ .../tests/isms/test_merkleroot_multisig.cairo | 237 +++++++ .../tests/isms/test_messageid_multisig.cairo | 209 ++++++ .../cairo/crates/contracts/tests/lib.cairo | 20 + .../tests/libs/test_enumerable_map.cairo | 112 +++ .../test_default_fallback_routing_ism.cairo | 322 +++++++++ .../routing/test_domain_routing_ism.cairo | 322 +++++++++ .../cairo/crates/contracts/tests/setup.cairo | 585 ++++++++++++++++ .../crates/contracts/tests/test_mailbox.cairo | 644 ++++++++++++++++++ .../tests/test_validator_announce.cairo | 146 ++++ starknet/cairo/crates/mocks/Scarb.toml | 26 + .../mocks/src/enumerable_map_holder.cairo | 52 ++ .../crates/mocks/src/erc4626_component.cairo | 523 ++++++++++++++ .../cairo/crates/mocks/src/erc4626_mock.cairo | 44 ++ .../src/erc4626_yield_sharing_mock.cairo | 457 +++++++++++++ .../cairo/crates/mocks/src/fee_hook.cairo | 31 + .../cairo/crates/mocks/src/fee_token.cairo | 29 + starknet/cairo/crates/mocks/src/hook.cairo | 30 + starknet/cairo/crates/mocks/src/ism.cairo | 24 + starknet/cairo/crates/mocks/src/lib.cairo | 21 + .../crates/mocks/src/message_recipient.cairo | 55 ++ .../cairo/crates/mocks/src/mock_account.cairo | 61 ++ .../cairo/crates/mocks/src/mock_eth.cairo | 57 ++ .../src/mock_hyp_erc721_uri_storage.cairo | 213 ++++++ .../cairo/crates/mocks/src/mock_mailbox.cairo | 596 ++++++++++++++++ .../mocks/src/mock_validator_announce.cairo | 313 +++++++++ .../cairo/crates/mocks/src/test_erc20.cairo | 95 +++ .../cairo/crates/mocks/src/test_erc721.cairo | 117 ++++ .../src/test_interchain_gas_payment.cairo | 70 ++ .../cairo/crates/mocks/src/test_ism.cairo | 34 + .../mocks/src/test_post_dispatch_hook.cairo | 62 ++ .../mocks/src/xerc20_lockbox_test.cairo | 67 ++ .../cairo/crates/mocks/src/xerc20_test.cairo | 123 ++++ starknet/cairo/crates/token/Scarb.toml | 30 + .../src/components/erc721_enumerable.cairo | 221 ++++++ .../src/components/erc721_uri_storage.cairo | 117 ++++ .../src/components/fast_token_router.cairo | 300 ++++++++ .../hyp_erc20_collateral_component.cairo | 142 ++++ .../src/components/hyp_erc20_component.cairo | 180 +++++ .../hyp_erc721_collateral_component.cairo | 81 +++ .../src/components/hyp_erc721_component.cairo | 67 ++ .../src/components/hyp_native_component.cairo | 167 +++++ .../token/src/components/token_message.cairo | 79 +++ .../token/src/components/token_router.cairo | 303 ++++++++ .../token/src/extensions/fast_hyp_erc20.cairo | 198 ++++++ .../fast_hyp_erc20_collateral.cairo | 214 ++++++ .../hyp_erc20_collateral_vault_deposit.cairo | 238 +++++++ .../src/extensions/hyp_erc20_vault.cairo | 410 +++++++++++ .../hyp_erc20_vault_collateral.cairo | 338 +++++++++ .../hyp_erc721_URI_collateral.cairo | 169 +++++ .../extensions/hyp_erc721_URI_storage.cairo | 201 ++++++ .../token/src/extensions/hyp_fiat_token.cairo | 180 +++++ .../src/extensions/hyp_native_scaled.cairo | 181 +++++ .../token/src/extensions/hyp_xerc20.cairo | 178 +++++ .../src/extensions/hyp_xerc20_lockbox.cairo | 237 +++++++ .../cairo/crates/token/src/hyp_erc20.cairo | 136 ++++ .../token/src/hyp_erc20_collateral.cairo | 124 ++++ .../cairo/crates/token/src/hyp_erc721.cairo | 195 ++++++ .../token/src/hyp_erc721_collateral.cairo | 178 +++++ .../cairo/crates/token/src/hyp_native.cairo | 129 ++++ .../token/src/interfaces/ierc4626.cairo | 128 ++++ .../token/src/interfaces/ifiat_token.cairo | 5 + .../src/interfaces/imessage_recipient.cairo | 7 + .../crates/token/src/interfaces/ixerc20.cairo | 11 + .../src/interfaces/ixerc20_lockbox.cairo | 10 + starknet/cairo/crates/token/src/lib.cairo | 37 + .../crates/token/tests/hyp_erc20/common.cairo | 422 ++++++++++++ .../hyp_erc20/hyp_erc20_collateral_test.cairo | 151 ++++ .../hyp_erc20/hyp_erc20_lockbox_test.cairo | 216 ++++++ .../tests/hyp_erc20/hyp_erc20_test.cairo | 89 +++ .../tests/hyp_erc20/hyp_fiat_token_test.cairo | 62 ++ .../tests/hyp_erc20/hyp_native_test.cairo | 1 + .../tests/hyp_erc20/hyp_xerc20_test.cairo | 83 +++ .../token/tests/hyp_erc721/common.cairo | 303 ++++++++ .../hyp_erc721_collateral_test.cairo | 72 ++ ...p_erc721_collateral_uri_storage_test.cairo | 65 ++ .../tests/hyp_erc721/hyp_erc721_test.cairo | 87 +++ .../hyp_erc721_uri_storage_test.cairo | 57 ++ starknet/cairo/crates/token/tests/lib.cairo | 20 + ..._erc20_collateral_vault_deposit_test.cairo | 252 +++++++ .../hyp_erc20_vault_test.cairo | 610 +++++++++++++++++ starknet/package.json | 33 + starknet/src/config.ts | 32 + starknet/src/errors.ts | 18 + starknet/src/index.ts | 91 +++ starknet/src/types.ts | 76 +++ starknet/src/utils.ts | 99 +++ starknet/tsconfig.json | 16 + yarn.lock | 265 ++++++- 129 files changed, 20686 insertions(+), 4 deletions(-) create mode 100644 starknet/.gitignore create mode 100755 starknet/build.sh create mode 100644 starknet/cairo/.gitignore create mode 100644 starknet/cairo/.tool-versions create mode 100644 starknet/cairo/Scarb.lock create mode 100644 starknet/cairo/Scarb.toml create mode 100644 starknet/cairo/crates/contracts/Scarb.toml create mode 100644 starknet/cairo/crates/contracts/src/client/gas_router_component.cairo create mode 100644 starknet/cairo/crates/contracts/src/client/mailboxclient.cairo create mode 100644 starknet/cairo/crates/contracts/src/client/mailboxclient_component.cairo create mode 100644 starknet/cairo/crates/contracts/src/client/router_component.cairo create mode 100644 starknet/cairo/crates/contracts/src/hooks/libs/standard_hook_metadata.cairo create mode 100644 starknet/cairo/crates/contracts/src/hooks/merkle_tree_hook.cairo create mode 100644 starknet/cairo/crates/contracts/src/hooks/protocol_fee.cairo create mode 100644 starknet/cairo/crates/contracts/src/interfaces.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/aggregation/aggregation.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/multisig/merkleroot_multisig_ism.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/multisig/messageid_multisig_ism.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/multisig/validator_announce.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/noop_ism.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/pausable_ism.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/routing/default_fallback_routing_ism.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/routing/domain_routing_ism.cairo create mode 100644 starknet/cairo/crates/contracts/src/isms/trusted_relayer_ism.cairo create mode 100644 starknet/cairo/crates/contracts/src/lib.cairo create mode 100644 starknet/cairo/crates/contracts/src/libs/aggregation_ism_metadata.cairo create mode 100644 starknet/cairo/crates/contracts/src/libs/checkpoint_lib.cairo create mode 100644 starknet/cairo/crates/contracts/src/libs/enumerable_map.cairo create mode 100644 starknet/cairo/crates/contracts/src/libs/math.cairo create mode 100644 starknet/cairo/crates/contracts/src/libs/message.cairo create mode 100644 starknet/cairo/crates/contracts/src/libs/multisig/merkleroot_ism_metadata.cairo create mode 100644 starknet/cairo/crates/contracts/src/libs/multisig/message_id_ism_metadata.cairo create mode 100644 starknet/cairo/crates/contracts/src/mailbox.cairo create mode 100644 starknet/cairo/crates/contracts/src/utils/keccak256.cairo create mode 100644 starknet/cairo/crates/contracts/src/utils/store_arrays.cairo create mode 100644 starknet/cairo/crates/contracts/src/utils/utils.cairo create mode 100644 starknet/cairo/crates/contracts/tests/hooks/test_merkle_tree_hook.cairo create mode 100644 starknet/cairo/crates/contracts/tests/hooks/test_protocol_fee.cairo create mode 100644 starknet/cairo/crates/contracts/tests/isms/test_aggregation.cairo create mode 100644 starknet/cairo/crates/contracts/tests/isms/test_default_ism.cairo create mode 100644 starknet/cairo/crates/contracts/tests/isms/test_merkleroot_multisig.cairo create mode 100644 starknet/cairo/crates/contracts/tests/isms/test_messageid_multisig.cairo create mode 100644 starknet/cairo/crates/contracts/tests/lib.cairo create mode 100644 starknet/cairo/crates/contracts/tests/libs/test_enumerable_map.cairo create mode 100644 starknet/cairo/crates/contracts/tests/routing/test_default_fallback_routing_ism.cairo create mode 100644 starknet/cairo/crates/contracts/tests/routing/test_domain_routing_ism.cairo create mode 100644 starknet/cairo/crates/contracts/tests/setup.cairo create mode 100644 starknet/cairo/crates/contracts/tests/test_mailbox.cairo create mode 100644 starknet/cairo/crates/contracts/tests/test_validator_announce.cairo create mode 100644 starknet/cairo/crates/mocks/Scarb.toml create mode 100644 starknet/cairo/crates/mocks/src/enumerable_map_holder.cairo create mode 100644 starknet/cairo/crates/mocks/src/erc4626_component.cairo create mode 100644 starknet/cairo/crates/mocks/src/erc4626_mock.cairo create mode 100644 starknet/cairo/crates/mocks/src/erc4626_yield_sharing_mock.cairo create mode 100644 starknet/cairo/crates/mocks/src/fee_hook.cairo create mode 100644 starknet/cairo/crates/mocks/src/fee_token.cairo create mode 100644 starknet/cairo/crates/mocks/src/hook.cairo create mode 100644 starknet/cairo/crates/mocks/src/ism.cairo create mode 100644 starknet/cairo/crates/mocks/src/lib.cairo create mode 100644 starknet/cairo/crates/mocks/src/message_recipient.cairo create mode 100644 starknet/cairo/crates/mocks/src/mock_account.cairo create mode 100644 starknet/cairo/crates/mocks/src/mock_eth.cairo create mode 100644 starknet/cairo/crates/mocks/src/mock_hyp_erc721_uri_storage.cairo create mode 100644 starknet/cairo/crates/mocks/src/mock_mailbox.cairo create mode 100644 starknet/cairo/crates/mocks/src/mock_validator_announce.cairo create mode 100644 starknet/cairo/crates/mocks/src/test_erc20.cairo create mode 100644 starknet/cairo/crates/mocks/src/test_erc721.cairo create mode 100644 starknet/cairo/crates/mocks/src/test_interchain_gas_payment.cairo create mode 100644 starknet/cairo/crates/mocks/src/test_ism.cairo create mode 100644 starknet/cairo/crates/mocks/src/test_post_dispatch_hook.cairo create mode 100644 starknet/cairo/crates/mocks/src/xerc20_lockbox_test.cairo create mode 100644 starknet/cairo/crates/mocks/src/xerc20_test.cairo create mode 100644 starknet/cairo/crates/token/Scarb.toml create mode 100644 starknet/cairo/crates/token/src/components/erc721_enumerable.cairo create mode 100644 starknet/cairo/crates/token/src/components/erc721_uri_storage.cairo create mode 100644 starknet/cairo/crates/token/src/components/fast_token_router.cairo create mode 100644 starknet/cairo/crates/token/src/components/hyp_erc20_collateral_component.cairo create mode 100644 starknet/cairo/crates/token/src/components/hyp_erc20_component.cairo create mode 100644 starknet/cairo/crates/token/src/components/hyp_erc721_collateral_component.cairo create mode 100644 starknet/cairo/crates/token/src/components/hyp_erc721_component.cairo create mode 100644 starknet/cairo/crates/token/src/components/hyp_native_component.cairo create mode 100644 starknet/cairo/crates/token/src/components/token_message.cairo create mode 100644 starknet/cairo/crates/token/src/components/token_router.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/fast_hyp_erc20.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/fast_hyp_erc20_collateral.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_erc20_collateral_vault_deposit.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_erc20_vault.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_erc20_vault_collateral.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_erc721_URI_collateral.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_erc721_URI_storage.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_fiat_token.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_native_scaled.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_xerc20.cairo create mode 100644 starknet/cairo/crates/token/src/extensions/hyp_xerc20_lockbox.cairo create mode 100644 starknet/cairo/crates/token/src/hyp_erc20.cairo create mode 100644 starknet/cairo/crates/token/src/hyp_erc20_collateral.cairo create mode 100644 starknet/cairo/crates/token/src/hyp_erc721.cairo create mode 100644 starknet/cairo/crates/token/src/hyp_erc721_collateral.cairo create mode 100644 starknet/cairo/crates/token/src/hyp_native.cairo create mode 100644 starknet/cairo/crates/token/src/interfaces/ierc4626.cairo create mode 100644 starknet/cairo/crates/token/src/interfaces/ifiat_token.cairo create mode 100644 starknet/cairo/crates/token/src/interfaces/imessage_recipient.cairo create mode 100644 starknet/cairo/crates/token/src/interfaces/ixerc20.cairo create mode 100644 starknet/cairo/crates/token/src/interfaces/ixerc20_lockbox.cairo create mode 100644 starknet/cairo/crates/token/src/lib.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc20/common.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_collateral_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_lockbox_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc20/hyp_fiat_token_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc20/hyp_native_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc20/hyp_xerc20_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc721/common.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_collateral_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_collateral_uri_storage_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_test.cairo create mode 100644 starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_uri_storage_test.cairo create mode 100644 starknet/cairo/crates/token/tests/lib.cairo create mode 100644 starknet/cairo/crates/token/tests/vault_extensions/hyp_erc20_collateral_vault_deposit_test.cairo create mode 100644 starknet/cairo/crates/token/tests/vault_extensions/hyp_erc20_vault_test.cairo create mode 100644 starknet/package.json create mode 100644 starknet/src/config.ts create mode 100644 starknet/src/errors.ts create mode 100644 starknet/src/index.ts create mode 100644 starknet/src/types.ts create mode 100644 starknet/src/utils.ts create mode 100644 starknet/tsconfig.json diff --git a/package.json b/package.json index 072ac6c785c..63c28ce084f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "workspaces": [ "solidity", + "starknet", "typescript/*" ], "resolutions": { diff --git a/starknet/.gitignore b/starknet/.gitignore new file mode 100644 index 00000000000..ad7be9fd50d --- /dev/null +++ b/starknet/.gitignore @@ -0,0 +1,7 @@ +target +.snfoundry_cache/ +.env +dist + + + diff --git a/starknet/build.sh b/starknet/build.sh new file mode 100755 index 00000000000..44925e9d346 --- /dev/null +++ b/starknet/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Navigate to the cairo directory +cd cairo + +# Build the project using scarb +# Uncomment the next line if you want to enable the build process +scarb build || { echo "Scarb build failed"; exit 1; } + +# Copy the build output to the sdk directory +cp -R ./target ../src/target || { echo "Failed to copy target directory"; exit 1; } + +# Navigate back +cd ../ + +# Build the project +yarn build || { echo "Yarn build failed"; exit 1; } \ No newline at end of file diff --git a/starknet/cairo/.gitignore b/starknet/cairo/.gitignore new file mode 100644 index 00000000000..73aa31e608c --- /dev/null +++ b/starknet/cairo/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/starknet/cairo/.tool-versions b/starknet/cairo/.tool-versions new file mode 100644 index 00000000000..2dc945fb169 --- /dev/null +++ b/starknet/cairo/.tool-versions @@ -0,0 +1,2 @@ +scarb 2.6.5 +starknet-foundry 0.22.0 \ No newline at end of file diff --git a/starknet/cairo/Scarb.lock b/starknet/cairo/Scarb.lock new file mode 100644 index 00000000000..960c93cef1e --- /dev/null +++ b/starknet/cairo/Scarb.lock @@ -0,0 +1,103 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "alexandria_bytes" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=bcdca70#bcdca70afdf59c9976148e95cebad5cf63d75a7f" +dependencies = [ + "alexandria_data_structures", + "alexandria_math", +] + +[[package]] +name = "alexandria_data_structures" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=bcdca70#bcdca70afdf59c9976148e95cebad5cf63d75a7f" +dependencies = [ + "alexandria_encoding", +] + +[[package]] +name = "alexandria_encoding" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=bcdca70#bcdca70afdf59c9976148e95cebad5cf63d75a7f" +dependencies = [ + "alexandria_bytes", + "alexandria_math", + "alexandria_numeric", +] + +[[package]] +name = "alexandria_math" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=bcdca70#bcdca70afdf59c9976148e95cebad5cf63d75a7f" +dependencies = [ + "alexandria_data_structures", +] + +[[package]] +name = "alexandria_numeric" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=bcdca70#bcdca70afdf59c9976148e95cebad5cf63d75a7f" +dependencies = [ + "alexandria_math", + "alexandria_searching", +] + +[[package]] +name = "alexandria_searching" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=bcdca70#bcdca70afdf59c9976148e95cebad5cf63d75a7f" +dependencies = [ + "alexandria_data_structures", +] + +[[package]] +name = "alexandria_storage" +version = "0.3.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=bcdca70#bcdca70afdf59c9976148e95cebad5cf63d75a7f" + +[[package]] +name = "contracts" +version = "0.0.6" +dependencies = [ + "alexandria_bytes", + "alexandria_storage", + "mocks", + "openzeppelin", + "snforge_std", +] + +[[package]] +name = "mocks" +version = "0.0.6" +dependencies = [ + "alexandria_bytes", + "alexandria_storage", + "contracts", + "openzeppelin", + "token", +] + +[[package]] +name = "openzeppelin" +version = "0.14.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.14.0#f091c4f51ddeb10297db984acae965328c5a4e5b" + +[[package]] +name = "snforge_std" +version = "0.22.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.22.0#9b215944c6c5871c738381b4ded61bbf06e7ba35" + +[[package]] +name = "token" +version = "0.0.1" +dependencies = [ + "alexandria_bytes", + "alexandria_storage", + "contracts", + "mocks", + "openzeppelin", + "snforge_std", +] diff --git a/starknet/cairo/Scarb.toml b/starknet/cairo/Scarb.toml new file mode 100644 index 00000000000..dd55e329ba1 --- /dev/null +++ b/starknet/cairo/Scarb.toml @@ -0,0 +1,23 @@ +[workspace] +members = ["crates/*"] + +[workspace.package] +name = "hyperlane_starknet" +description = "Implementation of the Hyperlane protocol on Starknet." +version = "0.0.6" +edition = "2023_11" +cairo-version = "2.6.3" +license-file = "../LICENSE" +readme = "../README.md" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[workspace.dependencies] +starknet = "2.6.3" +alexandria_bytes = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev = "bcdca70" } +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev = "bcdca70" } +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.14.0" } + + +[workspace.tool.fmt] +sort-module-level-items = true \ No newline at end of file diff --git a/starknet/cairo/crates/contracts/Scarb.toml b/starknet/cairo/crates/contracts/Scarb.toml new file mode 100644 index 00000000000..e8a3e1d5826 --- /dev/null +++ b/starknet/cairo/crates/contracts/Scarb.toml @@ -0,0 +1,30 @@ +[package] +name = "contracts" +version = "0.0.6" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet.workspace = true +alexandria_bytes.workspace = true +alexandria_storage.workspace = true +openzeppelin.workspace = true +mocks = {path = "../mocks"} + +[dev-dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.22.0" } + + +[tool] +fmt.workspace = true + +[[target.starknet-contract]] +allowed-libfuncs-list.name = "experimental" +sierra = true +casm = true +casm-add-pythonic-hints = true +build-external-contracts = ["mocks::*"] + +[lib] +name = "contracts" diff --git a/starknet/cairo/crates/contracts/src/client/gas_router_component.cairo b/starknet/cairo/crates/contracts/src/client/gas_router_component.cairo new file mode 100644 index 00000000000..aaf267be9de --- /dev/null +++ b/starknet/cairo/crates/contracts/src/client/gas_router_component.cairo @@ -0,0 +1,187 @@ +use GasRouterComponent::GasRouterConfig; + +#[starknet::interface] +pub trait IGasRouter { + fn set_destination_gas( + ref self: TState, + gas_configs: Option>, + domain: Option, + gas: Option + ); + fn quote_gas_payment(self: @TState, destination_domain: u32) -> u256; +} + +/// # Gas Router Component Module +/// +/// This module provides a gas management mechanism for routing messages across domains. +/// It allows setting gas limits for specific destinations and quoting gas payments for +/// message dispatches. +/// +/// ## Key Concepts +/// +/// - **Gas Configuration**: This module allows users to set gas limits for specific destination +/// domains, either individually or through an array of configurations. +/// +/// - **Message Dispatching**: Gas management is integrated with the message dispatch system, +/// enabling the module to quote gas payments for dispatching messages to other domains. +/// +/// - **Ownership-based Access Control**: The ability to set gas limits is restricted to the +/// contract owner. +/// +/// - **Component Composition**: The `GasRouterComponent` integrates with other components such as +/// `RouterComponent` for dispatching messages and `MailboxclientComponent` for mailbox interactions. +#[starknet::component] +pub mod GasRouterComponent { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl + }; + use contracts::client::router_component::{ + RouterComponent, RouterComponent::RouterComponentInternalImpl, IRouter, + }; + use contracts::hooks::libs::standard_hook_metadata::standard_hook_metadata::StandardHookMetadata; + use contracts::interfaces::{IMailboxClient}; + use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::InternalImpl as OwnableInternalImpl + }; + use starknet::ContractAddress; + + #[derive(Copy, Drop, Serde)] + pub struct GasRouterConfig { + pub domain: u32, + pub gas: u256, + } + + #[storage] + struct Storage { + destination_gas: LegacyMap, + } + + #[embeddable_as(GasRouterImpl)] + impl GasRouter< + TContractState, + +HasComponent, + +Drop, + impl MailBoxClient: MailboxclientComponent::HasComponent, + impl Router: RouterComponent::HasComponent, + impl Owner: OwnableComponent::HasComponent, + > of super::IGasRouter> { + /// Sets the gas limit for a specified domain or applies an array of gas configurations. + /// + /// This function allows the contract owner to configure gas limits for one or multiple domains. + /// If an array of `GasRouterConfig` is provided via `gas_configs`, the function iterates over + /// the array and applies each configuration. If a specific `domain` and `gas` are provided, + /// the function sets the gas limit for that domain. + /// + /// # Arguments + /// + /// * `gas_configs` - An optional array of `GasRouterConfig`, each containing a `domain` and a `gas` value. + /// * `domain` - An optional `u32` representing the domain for which the gas limit is being set. + /// * `gas` - An optional `u256` representing the gas limit for the given domain. + /// + /// # Panics + /// + /// Panics if neither `gas_configs` nor a valid `domain` and `gas` are provided. + fn set_destination_gas( + ref self: ComponentState, + gas_configs: Option>, + domain: Option, + gas: Option + ) { + let owner_comp = get_dep_component!(@self, Owner); + owner_comp.assert_only_owner(); + + match gas_configs { + Option::Some(gas_configs) => { + let configs_len = gas_configs.len(); + let mut i = 0; + + while i < configs_len { + let config = *gas_configs.at(i); + self._set_destination_gas(config.domain, config.gas); + i += 1; + }; + }, + Option::None => { + match (domain, gas) { + ( + Option::Some(domain), Option::Some(gas) + ) => { self._set_destination_gas(domain, gas); }, + _ => { panic!("Set destination gas: Invalid arguments"); } + } + } + } + } + + /// Returns the quoted gas payment for dispatching a message to the specified domain. + /// + /// This function calculates and returns the gas payment required to dispatch a message + /// to a specified destination domain. It uses the router and mailbox components to compute + /// the necessary gas amount for the message dispatch. + /// + /// # Arguments + /// + /// * `destination_domain` - A `u32` representing the domain to which the message is being sent. + /// + /// # Returns + /// + /// A `u256` value representing the quoted gas payment. + fn quote_gas_payment( + self: @ComponentState, destination_domain: u32 + ) -> u256 { + let mailbox_comp = get_dep_component!(self, MailBoxClient); + let hook = mailbox_comp.get_hook(); + self._Gas_router_quote_dispatch(destination_domain, BytesTrait::new_empty(), hook) + } + } + + #[generate_trait] + pub impl GasRouterInternalImpl< + TContractState, + +HasComponent, + impl MailBoxClient: MailboxclientComponent::HasComponent, + impl Router: RouterComponent::HasComponent, + +Drop + > of InternalTrait { + fn _Gas_router_hook_metadata( + self: @ComponentState, destination: u32 + ) -> Bytes { + StandardHookMetadata::override_gas_limits(self.destination_gas.read(destination)) + } + + fn _set_destination_gas(ref self: ComponentState, domain: u32, gas: u256) { + self.destination_gas.write(domain, gas); + } + + fn _Gas_router_dispatch( + ref self: ComponentState, + destination: u32, + value: u256, + message_body: Bytes, + hook: ContractAddress + ) -> u256 { + let mut router_comp = get_dep_component_mut!(ref self, Router); + router_comp + ._Router_dispatch( + destination, + value, + message_body, + self._Gas_router_hook_metadata(destination), + hook + ) + } + + fn _Gas_router_quote_dispatch( + self: @ComponentState, + destination: u32, + message_body: Bytes, + hook: ContractAddress + ) -> u256 { + let mut router_comp = get_dep_component!(self, Router); + router_comp + ._Router_quote_dispatch( + destination, message_body, self._Gas_router_hook_metadata(destination), hook + ) + } + } +} diff --git a/starknet/cairo/crates/contracts/src/client/mailboxclient.cairo b/starknet/cairo/crates/contracts/src/client/mailboxclient.cairo new file mode 100644 index 00000000000..f90e88ab3ab --- /dev/null +++ b/starknet/cairo/crates/contracts/src/client/mailboxclient.cairo @@ -0,0 +1,70 @@ +#[starknet::contract] +mod mailboxClientProxy { + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl + }; + use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait; + use openzeppelin::access::ownable::{OwnableComponent,}; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, ClassHash}; + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[abi(embed_v0)] + impl MailboxclientImpl = + MailboxclientComponent::MailboxClientImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + } + + #[constructor] + fn constructor( + ref self: ContractState, + _mailbox: ContractAddress, + _owner: ContractAddress, + _hook: ContractAddress, + _interchain_security_module: ContractAddress + ) { + self.ownable.initializer(_owner); + self + .mailboxclient + .initialize(_mailbox, Option::Some(_hook), Option::Some(_interchain_security_module)); + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } +} diff --git a/starknet/cairo/crates/contracts/src/client/mailboxclient_component.cairo b/starknet/cairo/crates/contracts/src/client/mailboxclient_component.cairo new file mode 100644 index 00000000000..d8b2f69b172 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/client/mailboxclient_component.cairo @@ -0,0 +1,214 @@ +#[starknet::component] +pub mod MailboxclientComponent { + use alexandria_bytes::Bytes; + use contracts::interfaces::{IMailboxClient, IMailboxDispatcher, IMailboxDispatcherTrait}; + use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::InternalImpl, OwnableComponent::OwnableImpl + }; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, contract_address_const}; + + #[storage] + struct Storage { + mailbox: IMailboxDispatcher, + local_domain: u32, + hook: ContractAddress, + interchain_security_module: ContractAddress, + } + + pub mod Errors { + pub const ADDRESS_CANNOT_BE_ZERO: felt252 = 'Address cannot be zero'; + } + + #[embeddable_as(MailboxClientImpl)] + pub impl MailboxClient< + TContractState, + +HasComponent, + +Drop, + impl Owner: OwnableComponent::HasComponent + > of IMailboxClient> { + /// Sets the address of the application's custom hook. + /// Dev: callable only by the admin + /// + /// # Arguments + /// + /// * - `_hook` - The hook address to set + fn set_hook(ref self: ComponentState, _hook: ContractAddress) { + let ownable_comp = get_dep_component!(@self, Owner); + ownable_comp.assert_only_owner(); + assert(_hook != contract_address_const::<0>(), Errors::ADDRESS_CANNOT_BE_ZERO); + self.hook.write(_hook); + } + + /// Sets the address of the application's custom interchain security module. + /// Dev: Callable only by the admin + /// + /// # Arguments + /// + /// * - '_module' - The address of the interchain security module contract. + fn set_interchain_security_module( + ref self: ComponentState, _module: ContractAddress + ) { + let ownable_comp = get_dep_component!(@self, Owner); + ownable_comp.assert_only_owner(); + assert(_module != contract_address_const::<0>(), Errors::ADDRESS_CANNOT_BE_ZERO); + self.interchain_security_module.write(_module); + } + + /// Retrieves the local domain of the mailbox associated to the maiblox client. + /// + /// # Returns + /// + /// u32 - The local domain + fn get_local_domain(self: @ComponentState) -> u32 { + self.mailbox.read().get_local_domain() + } + + /// Retrieves the current hook set. + /// + /// # Returns + /// + /// ContractAddress - The hook defined + fn get_hook(self: @ComponentState) -> ContractAddress { + self.hook.read() + } + + /// Retrieves the current interchain security module + /// + /// # Returns + /// + /// ContractAddress - The defined ISM + fn interchain_security_module(self: @ComponentState) -> ContractAddress { + self.interchain_security_module.read() + } + + /// Determines if a message associated to a given id is the mailbox's latest dispatched + /// + /// # Arguments + /// + /// * - `_id ` - The id to check + /// + /// # Returns + /// + /// boolean - True if latest dispatched + fn _is_latest_dispatched(self: @ComponentState, _id: u256) -> bool { + self.mailbox.read().get_latest_dispatched_id() == _id + } + + /// Determines if a message associated to a given id is delivered + /// + /// # Arguments + /// + /// * - `_id ` - The id to check + /// + /// # Returns + /// + /// boolean - True if delivered + fn _is_delivered(self: @ComponentState, _id: u256) -> bool { + self.mailbox.read().delivered(_id) + } + + /// Returns the mailbox contract address associated to the mailbox client + /// + /// # Returns + /// + /// ContractAddress - the mailbox address associated to the client + fn mailbox(self: @ComponentState) -> ContractAddress { + let mailbox: IMailboxDispatcher = self.mailbox.read(); + mailbox.contract_address + } + + /// Dispatches a message to the destination domain & recipient. + /// + /// # Arguments + /// + /// * - `_destination_domain` Domain of destination chain + /// * - `_recipient` Address of recipient on destination chain + /// * - `_message_body` Bytes content of message body + /// * - `_fee_amount` - the payment provided for sending the message + /// * - `_hook_metadata` Metadata used by the post dispatch hook + /// * - `_hook` Custom hook to use instead of the default + /// + /// # Returns + /// + /// u256 - The message ID inserted into the Mailbox's merkle tree + fn _dispatch( + self: @ComponentState, + _destination_domain: u32, + _recipient: u256, + _message_body: Bytes, + _fee_amount: u256, + _hook_metadata: Option, + _hook: Option + ) -> u256 { + self + .mailbox + .read() + .dispatch( + _destination_domain, + _recipient, + _message_body, + _fee_amount, + _hook_metadata, + _hook + ) + } + + /// Computes quote for dispatching a message to the destination domain & recipient. + /// + /// # Arguments + /// + /// * - `_destination_domain` Domain of destination chain + /// * - `_recipient` Address of recipient on destination chain + /// * - `_message_body` Bytes content of message body + /// * - `_hook_metadata` Metadata used by the post dispatch hook + /// * - `_hook` Custom hook to use instead of the default + /// + /// # Returns + /// + /// u256 - The payment required to dispatch the message + fn quote_dispatch( + self: @ComponentState, + _destination_domain: u32, + _recipient: u256, + _message_body: Bytes, + _hook_metadata: Option, + _hook: Option + ) -> u256 { + self + .mailbox + .read() + .quote_dispatch( + _destination_domain, _recipient, _message_body, _hook_metadata, _hook + ) + } + } + + #[generate_trait] + pub impl MailboxClientInternalImpl< + TContractState, +HasComponent, + > of InternalTrait { + /// Initializes the mailbox client configuration. + /// Dev: callable on constructor + /// + /// # Arguments + /// + /// * - `_mailbox` - mailbox contract address + fn initialize( + ref self: ComponentState, + _mailbox: ContractAddress, + _hook: Option, + _interchain_security_module: Option, + ) { + let mailbox = IMailboxDispatcher { contract_address: _mailbox }; + self.mailbox.write(mailbox); + self.local_domain.write(mailbox.get_local_domain()); + if let Option::Some(hook) = _hook { + self.hook.write(hook); + } + if let Option::Some(ism) = _interchain_security_module { + self.interchain_security_module.write(ism); + } + } + } +} diff --git a/starknet/cairo/crates/contracts/src/client/router_component.cairo b/starknet/cairo/crates/contracts/src/client/router_component.cairo new file mode 100644 index 00000000000..cf4743f530e --- /dev/null +++ b/starknet/cairo/crates/contracts/src/client/router_component.cairo @@ -0,0 +1,280 @@ +use alexandria_bytes::Bytes; +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IRouter { + fn enroll_remote_router(ref self: TState, domain: u32, router: u256); + fn enroll_remote_routers(ref self: TState, domains: Array, addresses: Array); + fn unenroll_remote_router(ref self: TState, domain: u32); + fn unenroll_remote_routers(ref self: TState, domains: Array); + fn handle(ref self: TState, origin: u32, sender: u256, message: Bytes); + fn domains(self: @TState) -> Array; + fn routers(self: @TState, domain: u32) -> u256; +} + +/// # Router Component Module +/// +/// This module implements a router component that manages the enrollment and +/// unenrollment of remote routers across various domains. It provides the +/// functionality for dispatching messages to remote routers and handling incoming +/// messages. The core functionality is split across traits, with the primary logic +/// provided by the `IRouter` trait and the additional internal mechanisms handled +/// by the `RouterComponent`. +#[starknet::component] +pub mod RouterComponent { + use alexandria_bytes::Bytes; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl + }; + use contracts::interfaces::{IMailboxClient, IMailboxDispatcher, IMailboxDispatcherTrait}; + use contracts::libs::enumerable_map::{EnumerableMap, EnumerableMapTrait}; + use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::InternalImpl as OwnableInternalImpl + }; + use starknet::ContractAddress; + + #[storage] + struct Storage { + routers: EnumerableMap, + gas_router: ContractAddress, + } + + mod Err { + pub fn domain_not_found(domain: u32) { + panic!("No router enrolled for domain {}", domain); + } + } + + pub trait IMessageRecipientInternalHookTrait { + fn _handle( + ref self: ComponentState, origin: u32, sender: u256, message: Bytes + ); + } + + #[embeddable_as(RouterImpl)] + pub impl Router< + TContractState, + +HasComponent, + +MailboxclientComponent::HasComponent, + impl Owner: OwnableComponent::HasComponent, + +Drop, + impl Hook: IMessageRecipientInternalHookTrait + > of super::IRouter> { + /// Enrolls a remote router for the specified `domain`. + /// + /// This function requires ownership verification before proceeding. Once verified, + /// it calls the internal method `_enroll_remote_router` to complete the enrollment. + /// + /// # Arguments + /// + /// * `domain` - A `u32` representing the domain for which the router is being enrolled. + /// * `router` - A `u256` representing the address of the router to be enrolled. + fn enroll_remote_router( + ref self: ComponentState, domain: u32, router: u256 + ) { + let ownable_comp = get_dep_component!(@self, Owner); + ownable_comp.assert_only_owner(); + self._enroll_remote_router(domain, router); + } + + /// Enrolls multiple remote routers across multiple `domains`. + /// + /// This function requires ownership verification. It checks that the lengths of the + /// `domains` and `addresses` arrays are the same, then enrolls each router for its + /// corresponding domain using `_enroll_remote_router`. + /// + /// # Arguments + /// + /// * `domains` - An array of `u32` values representing the domains for which routers are being enrolled. + /// * `addresses` - An array of `u256` values representing the addresses of the routers to be enrolled. + /// + /// # Panics + /// + /// Panics if the lengths of `domains` and `addresses` do not match. + fn enroll_remote_routers( + ref self: ComponentState, domains: Array, addresses: Array + ) { + let ownable_comp = get_dep_component!(@self, Owner); + ownable_comp.assert_only_owner(); + + let domains_len = domains.len(); + if addresses.len() != domains_len { + panic!("Addresses array length must match domains array length"); + } + + let mut i = 0; + while i < domains_len { + self._enroll_remote_router(*domains.at(i), *addresses.at(i)); + i += 1; + } + } + + /// Unenrolls the router for the specified `domain`. + /// + /// This function requires ownership verification. Once verified, it calls the internal method + /// `_unenroll_remote_router` to complete the unenrollment. + /// + /// # Arguments + /// + /// * `domain` - A `u32` representing the domain for which the router is being unenrolled. + fn unenroll_remote_router(ref self: ComponentState, domain: u32) { + let mut ownable_comp = get_dep_component_mut!(ref self, Owner); + ownable_comp.assert_only_owner(); + + self._unenroll_remote_router(domain); + } + + /// Unenrolls the routers for multiple `domains`. + /// + /// This function removes the router for each domain in the `domains` array + /// using the `_unenroll_remote_router` method. + /// + /// # Arguments + /// + /// * `domains` - An array of `u32` values representing the domains for which routers are being unenrolled. + fn unenroll_remote_routers(ref self: ComponentState, domains: Array,) { + let domains_len = domains.len(); + let mut i = 0; + while i < domains_len { + self._unenroll_remote_router(*domains.at(i)); + i += 1; + } + } + + /// Handles an incoming message from a remote router. + /// + /// This function checks if a remote router is enrolled for the `origin` domain, verifies that the + /// `sender` matches the enrolled router, and calls the `_handle` method on the `Hook` to process + /// the message. + /// + /// # Arguments + /// + /// * `origin` - A `u32` representing the origin domain of the message. + /// * `sender` - A `u256` representing the address of the message sender. + /// * `message` - The message payload as a `Bytes` object. + /// + /// # Panics + /// + /// Panics if the sender does not match the enrolled router for the origin domain. + fn handle( + ref self: ComponentState, origin: u32, sender: u256, message: Bytes + ) { + let router = self._must_have_remote_router(origin); + assert!(router == sender, "Enrolled router does not match sender"); + + Hook::_handle(ref self, origin, sender, message); + } + + /// Returns an array of enrolled domains. + /// + /// This function reads the keys from the `routers` map, which represent the enrolled + /// domains. + /// + /// # Returns + /// + /// An array of `u32` values representing the enrolled domains. + fn domains(self: @ComponentState) -> Array { + self.routers.read().keys() + } + + /// Returns the router address for a given `domain`. + /// + /// This function retrieves the address of the enrolled router for the specified + /// `domain` from the `routers` map. + /// + /// # Arguments + /// + /// * `domain` - A `u32` representing the domain for which the router address is being queried. + /// + /// # Returns + /// + /// A `u256` value representing the router address for the specified domain. + fn routers(self: @ComponentState, domain: u32) -> u256 { + self.routers.read().get(domain) + } + } + + #[generate_trait] + pub impl RouterComponentInternalImpl< + TContractState, + +HasComponent, + +Drop, + impl MailBoxClient: MailboxclientComponent::HasComponent + > of InternalTrait { + fn _enroll_remote_router( + ref self: ComponentState, domain: u32, address: u256 + ) { + let mut routers = self.routers.read(); + let _ = routers.set(domain, address); + } + + fn _unenroll_remote_router(ref self: ComponentState, domain: u32) { + let mut routers = self.routers.read(); + routers.remove(domain); + } + + fn _is_remote_router( + self: @ComponentState, domain: u32, address: u256 + ) -> bool { + let routers = self.routers.read(); + let router = routers.get(domain); + router == address + } + + fn _must_have_remote_router(self: @ComponentState, domain: u32) -> u256 { + let routers = self.routers.read(); + let router = routers.get(domain); + + if router == 0 { + Err::domain_not_found(domain); + } + + router + } + + fn _Router_dispatch( + self: @ComponentState, + destination_domain: u32, + value: u256, + message_body: Bytes, + hook_metadata: Bytes, + hook: ContractAddress + ) -> u256 { + let router = self._must_have_remote_router(destination_domain); + let mut mailbox_comp = get_dep_component!(self, MailBoxClient); + let value = mailbox_comp + .mailbox + .read() + .dispatch( + destination_domain, + router, + message_body, + value, + Option::Some(hook_metadata), + Option::Some(hook), + ); + value + } + + fn _Router_quote_dispatch( + self: @ComponentState, + destination_domain: u32, + message_body: Bytes, + hook_metadata: Bytes, + hook: ContractAddress + ) -> u256 { + let router = self._must_have_remote_router(destination_domain); + let mut mailbox_comp = get_dep_component!(self, MailBoxClient); + mailbox_comp + .mailbox + .read() + .quote_dispatch( + destination_domain, + router, + message_body, + Option::Some(hook_metadata), + Option::Some(hook) + ) + } + } +} diff --git a/starknet/cairo/crates/contracts/src/hooks/libs/standard_hook_metadata.cairo b/starknet/cairo/crates/contracts/src/hooks/libs/standard_hook_metadata.cairo new file mode 100644 index 00000000000..1973471bc41 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/hooks/libs/standard_hook_metadata.cairo @@ -0,0 +1,212 @@ +pub mod standard_hook_metadata { + use alexandria_bytes::{Bytes, BytesTrait}; + + use starknet::ContractAddress; + struct Metadata { + variant: u16, + msg_value: u256, + gas_limit: u256, + refund_address: ContractAddress + } + + + /// Format of metadata: + /// + /// [0:2] variant + /// [2:34] msg.value + /// [34:66] Gas limit for message (IGP) + /// [66:98] Refund address for message (IGP) + /// [98:] Custom metadata + + const VARIANT_OFFSET: u8 = 0; + const MSG_VALUE_OFFSET: u8 = 2; + const GAS_LIMIT_OFFSET: u8 = 34; + const REFUND_ADDRESS_OFFSET: u8 = 66; + const MIN_METADATA_LENGTH: u256 = 98; + + pub const VARIANT: u8 = 1; + + #[generate_trait] + pub impl StandardHookMetadataImpl of StandardHookMetadata { + /// Returns the variant of the metadata. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded standard hook metadata + /// + /// # Returns + /// + /// u16 - variant of the metadata + fn variant(_metadata: Bytes) -> u16 { + if (_metadata.size() < VARIANT_OFFSET.into() + 2) { + return 0; + } + let (_, res) = _metadata.read_u16(VARIANT_OFFSET.into()); + res + } + + /// Returns the specified value for the message. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded standard hook metadata + /// * - `_default` - Default fallback value. + /// + /// # Returns + /// + /// u256 - Value for the message + fn msg_value(_metadata: Bytes, _default: u256) -> u256 { + if (_metadata.size() < MSG_VALUE_OFFSET.into() + 32) { + return _default; + } + let (_, res) = _metadata.read_u256(MSG_VALUE_OFFSET.into()); + res + } + + /// Returns the specified gas limit for the message. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded standard hook metadata + /// * - `_default` - Default fallback gas limit. + /// + /// # Returns + /// + /// u256 - Gas limit for the message + fn gas_limit(_metadata: Bytes, _default: u256) -> u256 { + if (_metadata.size() < GAS_LIMIT_OFFSET.into() + 32) { + return _default; + } + let (_, res) = _metadata.read_u256(GAS_LIMIT_OFFSET.into()); + res + } + + /// Returns the specified refund address for the message. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded standard hook metadata + /// * - `_default` - Default fallback refund address. + /// + /// # Returns + /// + /// ContractAddress - Refund address for the message + fn refund_address(_metadata: Bytes, _default: ContractAddress) -> ContractAddress { + if (_metadata.size() < REFUND_ADDRESS_OFFSET.into() + 32) { + return _default; + } + let (_, res) = _metadata.read_address(REFUND_ADDRESS_OFFSET.into()); + res + } + + ///Returns any custom metadata. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded standard hook metadata + /// + /// # Returns + /// + /// Bytes - Custom metadata. + fn get_custom_metadata(_metadata: Bytes) -> Bytes { + if (_metadata.size().into() < MIN_METADATA_LENGTH) { + return BytesTrait::new_empty(); + } + let (_, res) = _metadata + .read_bytes( + MIN_METADATA_LENGTH.try_into().unwrap(), + _metadata.size() - MIN_METADATA_LENGTH.try_into().unwrap() + ); + res + } + + fn format_metadata( + msg_value: u256, + gas_limit: u256, + refund_address: ContractAddress, + custom_metadata: Bytes + ) -> Bytes { + // NOTE: sence ContractAddress might not fit into u128, we need to convert it to u256 + // and then split it into low and high parts + let refund_address_felt: felt252 = refund_address.into(); + let refund_address_u256: u256 = refund_address_felt.into(); + let mut data: Array = array![ + VARIANT.into(), + msg_value.low, + msg_value.high, + gas_limit.low, + gas_limit.high, + refund_address_u256.low, + refund_address_u256.high + ]; + + let mut formatted_metadata = BytesTrait::new(data.len(), data); + formatted_metadata.concat(@custom_metadata); + formatted_metadata + } + + fn override_gas_limits(gas_limit: u256) -> Bytes { + StandardHookMetadata::format_metadata( + 0, gas_limit, starknet::get_caller_address(), BytesTrait::new_empty() + ) + } + } +} + + +#[cfg(test)] +mod tests { + use alexandria_bytes::{Bytes, BytesTrait}; + use starknet::{ContractAddress, contract_address_const}; + use super::standard_hook_metadata::StandardHookMetadata; + #[test] + fn test_standard_hook_metadata_default_value() { + let mut metadata = BytesTrait::new_empty(); + assert_eq!(0, StandardHookMetadata::variant(metadata.clone())); + let variant = 1; + metadata.append_u16(variant); + assert_eq!(123, StandardHookMetadata::msg_value(metadata.clone(), 123)); + let msg_value = 0x123123123; + metadata.append_u256(msg_value); + assert_eq!(4567, StandardHookMetadata::gas_limit(metadata.clone(), 4567)); + let gas_limit = 0x456456456; + metadata.append_u256(gas_limit); + let other_refunded_address = 'other_refunded'.try_into().unwrap(); + assert_eq!( + other_refunded_address, + StandardHookMetadata::refund_address(metadata.clone(), other_refunded_address) + ); + let refund_address: ContractAddress = 'refund_address'.try_into().unwrap(); + metadata.append_address(refund_address); + } + + #[test] + fn test_standard_hook_metadata() { + let mut metadata = BytesTrait::new_empty(); + let variant = 1; + let msg_value = 0x123123123; + let gas_limit = 0x456456456; + let refund_address: ContractAddress = 'refund_address'.try_into().unwrap(); + let custom_metadata = array![0x123123123123, 0x123123123]; + metadata.append_u16(variant); + metadata.append_u256(msg_value); + metadata.append_u256(gas_limit); + metadata.append_address(refund_address); + metadata.append_u256(*custom_metadata.at(0)); + metadata.append_u256(*custom_metadata.at(1)); + let mut expected_custom_metadata = BytesTrait::new_empty(); + expected_custom_metadata.append_u256(*custom_metadata.at(0)); + expected_custom_metadata.append_u256(*custom_metadata.at(1)); + assert_eq!(variant, StandardHookMetadata::variant(metadata.clone())); + assert_eq!(msg_value, StandardHookMetadata::msg_value(metadata.clone(), 0)); + assert_eq!(gas_limit, StandardHookMetadata::gas_limit(metadata.clone(), 0)); + assert_eq!( + refund_address, + StandardHookMetadata::refund_address(metadata.clone(), contract_address_const::<0>()) + ); + assert( + expected_custom_metadata == StandardHookMetadata::get_custom_metadata(metadata.clone()), + 'SHM: custom metadata mismatch' + ); + } +} diff --git a/starknet/cairo/crates/contracts/src/hooks/merkle_tree_hook.cairo b/starknet/cairo/crates/contracts/src/hooks/merkle_tree_hook.cairo new file mode 100644 index 00000000000..bffc66bbc03 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/hooks/merkle_tree_hook.cairo @@ -0,0 +1,372 @@ +#[starknet::contract] +pub mod merkle_tree_hook { + use alexandria_bytes::{Bytes, BytesTrait}; + use alexandria_math::pow; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl, + MailboxclientComponent::MailboxClientImpl + }; + use contracts::hooks::libs::standard_hook_metadata::standard_hook_metadata::{ + StandardHookMetadata, VARIANT + }; + use contracts::interfaces::{ + IMailboxClientDispatcher, IMailboxClientDispatcherTrait, Types, IMerkleTreeHook, + IPostDispatchHook, IMailboxClient, IMailboxDispatcher, IMailboxDispatcherTrait, + }; + use contracts::libs::message::{Message, MessageTrait}; + use contracts::utils::keccak256::{reverse_endianness, compute_keccak, ByteData, HASH_SIZE}; + use openzeppelin::access::ownable::OwnableComponent; + use starknet::{ContractAddress, ClassHash}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[derive(Serde, Drop)] + pub struct Tree { + pub branch: Array, + pub count: u256 + } + + type Index = usize; + pub const TREE_DEPTH: u32 = 32; + + #[storage] + struct Storage { + tree: LegacyMap, + count: u32, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } + + + pub mod Errors { + pub const MESSAGE_NOT_DISPATCHING: felt252 = 'Message not dispatching'; + pub const INVALID_METADATA_VARIANT: felt252 = 'Invalid metadata variant'; + pub const MERKLE_TREE_FULL: felt252 = 'Merkle tree full'; + pub const CANNOT_EXCEED_TREE_DEPTH: felt252 = 'Cannot exceed tree depth'; + pub const TREE_DEPTH_NOT_REACHED: felt252 = 'Tree depth not reached'; + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + InsertedIntoTree: InsertedIntoTree, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + } + + + #[derive(starknet::Event, Drop)] + pub struct InsertedIntoTree { + pub id: u256, + pub index: u32 + } + + /// Contract constructor + /// + /// # Arguments + /// + /// * `_mailbox` - The mailbox to be associated to the mailbox client + #[constructor] + fn constructor(ref self: ContractState, _mailbox: ContractAddress, _owner: ContractAddress) { + self + .mailboxclient + .initialize(_mailbox, Option::::None, Option::::None); + self.ownable.initializer(_owner); + } + + #[abi(embed_v0)] + impl IMerkleTreeHookImpl of IMerkleTreeHook { + fn count(self: @ContractState) -> u32 { + self.count.read() + } + + fn root(self: @ContractState) -> u256 { + self._root() + } + + fn tree(self: @ContractState) -> Tree { + Tree { branch: self._build_tree(), count: self.count.read().into() } + } + + fn latest_checkpoint(self: @ContractState) -> (u256, u32) { + (self._root(), self.count() - 1) + } + } + + #[abi(embed_v0)] + impl IPostDispatchHookImpl of IPostDispatchHook { + fn hook_type(self: @ContractState) -> Types { + Types::MERKLE_TREE(()) + } + /// Returns whether the hook supports metadata + /// + /// # Arguments + /// + /// * - `_metadata` - metadata + /// + /// # Returns + /// + /// boolean - whether the hook supports metadata + fn supports_metadata(self: @ContractState, _metadata: Bytes) -> bool { + _metadata.size() == 0 || StandardHookMetadata::variant(_metadata) == VARIANT.into() + } + + /// Post action after a message is dispatched via the Mailbox + /// Dev: reverts if invalid metadata variant + /// + /// # Arguments + /// + /// * - `_metadata` - the metadata required for the hook + /// * - `_message` - the message passed from the Mailbox.dispatch() call + /// * - `_fee_amount` - the payment provided for sending the message + fn post_dispatch( + ref self: ContractState, _metadata: Bytes, _message: Message, _fee_amount: u256 + ) { + assert(self.supports_metadata(_metadata.clone()), Errors::INVALID_METADATA_VARIANT); + self._post_dispatch(_metadata, _message, _fee_amount); + } + + /// Computes the payment required by the postDispatch call + /// Dev: reverts if invalid metadata variant + /// + /// # Arguments + /// + /// * - `_metadata` - The metadata required for the hook + /// * - `_message` - the message passed from the Mailbox.dispatch() call + /// + /// # Returns + /// + /// u256 - Quoted payment for the postDispatch call + fn quote_dispatch(ref self: ContractState, _metadata: Bytes, _message: Message) -> u256 { + assert(self.supports_metadata(_metadata.clone()), Errors::INVALID_METADATA_VARIANT); + self._quote_dispatch(_metadata, _message) + } + } + + #[generate_trait] + pub impl MerkleInternalImpl of InternalTrait { + fn _post_dispatch( + ref self: ContractState, _metadata: Bytes, _message: Message, _fee_amount: u256 + ) { + let (id, _) = MessageTrait::format_message(_message); + // ensure messages which were not dispatched are not inserted into the tree + assert(self.mailboxclient._is_latest_dispatched(id), Errors::MESSAGE_NOT_DISPATCHING); + let index = self.count(); + self._insert(ByteData { value: id, size: HASH_SIZE }); + self.emit(InsertedIntoTree { id, index }); + } + + /// Inserts `_node` into merkle tree + /// Dev: Reverts if tree is full + /// + /// # Arguments + /// + ///* - `_node`- Element to insert into tree (see ByteData structure) + fn _insert(ref self: ContractState, mut _node: ByteData) { + let MAX_LEAVES: u128 = pow(2_u128, TREE_DEPTH.into()) - 1; + assert(self.count.read().into() < MAX_LEAVES, Errors::MERKLE_TREE_FULL); + let mut size = self.count.read() + 1; + self.count.write(size); + let mut cur_idx = 0; + loop { + if (cur_idx == TREE_DEPTH) { + break (); + } + if (size % 2 == 1) { + self.tree.write(cur_idx, _node); + break (); + } + _node = + ByteData { + value: reverse_endianness( + compute_keccak(array![self.tree.read(cur_idx), _node].span()) + ), + size: HASH_SIZE + }; + size /= 2; + cur_idx += 1; + }; + } + + /// Calculates and returns`_tree`'s current root given array of zero hashes + /// + /// # Arguments + /// + ///* - `_zeroes`- Array of zero hashes + /// + /// # Returns + /// + /// u256 - Calculated root of `_tree` + fn _root_with_ctx(self: @ContractState, _zeroes: Array) -> u256 { + let mut cur_idx = 0; + let index = self.count.read(); + + // Not present in the solidity implementation + let mut current = ByteData { value: *_zeroes[0], size: HASH_SIZE }; + loop { + if (cur_idx == TREE_DEPTH) { + break (); + } + let ith_bit = _get_ith_bit(index.into(), cur_idx); + let next = self + .tree + .read( + cur_idx + ); // Will return 0 if no values is stored, in accordance with solidity impl + if (ith_bit == 1) { + current = + ByteData { + value: reverse_endianness(compute_keccak(array![next, current].span())), + size: HASH_SIZE + }; + } else { + current = + ByteData { + value: reverse_endianness( + compute_keccak( + array![ + current, + ByteData { value: *_zeroes.at(cur_idx), size: HASH_SIZE } + ] + .span() + ) + ), + size: HASH_SIZE + }; + } + cur_idx += 1; + }; + current.value + } + + /// Calculates and returns the merkle root for the given leaf, `_item`, a merkle branch, and the index of `_item` in the tree. + /// + /// # Arguments + /// + /// * - `_item`- Merkle leaf + /// * - `_branch`- Merkle proof + /// * - `_index`- Index of `_item` in tree + /// + /// # Returns + /// + /// u256 - Calculated merkle root + fn _branch_root(_item: ByteData, _branch: Span, _index: u256) -> u256 { + assert(_branch.len() >= TREE_DEPTH, Errors::TREE_DEPTH_NOT_REACHED); + let mut cur_idx = 0; + let mut current = _item; + loop { + if (cur_idx == TREE_DEPTH) { + break (); + } + let ith_bit = _get_ith_bit(_index, cur_idx); + let next = *_branch.at(cur_idx); + if (ith_bit == 1) { + current = + ByteData { + value: reverse_endianness(compute_keccak(array![next, current].span())), + size: HASH_SIZE + }; + } else { + current = + ByteData { + value: reverse_endianness( + compute_keccak(array![value: current, value: next].span()) + ), + size: HASH_SIZE + }; + } + cur_idx += 1; + }; + current.value + } + + /// Calculates and returns`_tree`'s current root + /// + /// # Returns + /// + /// u256 - tree current root + fn _root(self: @ContractState) -> u256 { + self._root_with_ctx(self._zero_hashes()) + } + + // build array tree from legacy map storage + fn _build_tree(self: @ContractState) -> Array { + let mut cur_idx = 0; + let mut tree = array![]; + loop { + if (cur_idx >= TREE_DEPTH) { + break; + } + tree.append(self.tree.read(cur_idx)); + cur_idx += 1; + }; + tree + } + + fn _quote_dispatch(ref self: ContractState, _metadata: Bytes, _message: Message) -> u256 { + 0_u256 + } + + // keccak256 zero hashes + fn _zero_hashes(self: @ContractState) -> Array { + array![ + 0x0000000000000000000000000000000000000000000000000000000000000000, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30, + 0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85, + 0xe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344, + 0x0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d, + 0x887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968, + 0xffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f83, + 0x9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af, + 0xcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0, + 0xf9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5, + 0xf8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf892, + 0x3490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99c, + 0xc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb, + 0x5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc, + 0xda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d2, + 0x2733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981f, + 0xe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a, + 0x5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0, + 0xb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0, + 0xc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2, + 0xf4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9, + 0x5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377, + 0x4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652, + 0xcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef, + 0x0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d, + 0xb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0, + 0x838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e, + 0x662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e, + 0x388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322, + 0x93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735, + 0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9 + ] + } + } + + /// Retrieve the i-th bit of a given index + /// + /// # Arguments + /// + /// * - `_index`- index to retrieve the i-th bit from + /// * - `i`- the position of the bit to retrieve + /// + /// # Returns + /// + /// u256 - the i-th bit + fn _get_ith_bit(_index: u256, i: u32) -> u256 { + let mask = pow(2.into(), i.into()); + (_index / mask) % 2 + } +} diff --git a/starknet/cairo/crates/contracts/src/hooks/protocol_fee.cairo b/starknet/cairo/crates/contracts/src/hooks/protocol_fee.cairo new file mode 100644 index 00000000000..77eb43af7b2 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/hooks/protocol_fee.cairo @@ -0,0 +1,213 @@ +#[starknet::contract] +pub mod protocol_fee { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::hooks::libs::standard_hook_metadata::standard_hook_metadata::{ + StandardHookMetadata, VARIANT, + }; + use contracts::interfaces::{IPostDispatchHook, Types, IProtocolFee, ETH_ADDRESS}; + use contracts::libs::message::Message; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::{ + ERC20ABI, ERC20ABIDispatcher, ERC20ABIDispatcherTrait + }; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, contract_address_const, get_contract_address}; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[storage] + struct Storage { + max_protocol_fee: u256, + protocol_fee: u256, + beneficiary: ContractAddress, + fee_token: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } + + mod Errors { + pub const INVALID_METADATA_VARIANT: felt252 = 'Invalid metadata variant'; + pub const INVALID_BENEFICARY: felt252 = 'Invalid beneficiary'; + pub const EXCEEDS_MAX_PROTOCOL_FEE: felt252 = 'Exceeds max protocol fee'; + pub const INSUFFICIENT_BALANCE: felt252 = 'Insufficient balance'; + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Insufficient allowance'; + pub const INSUFFICIENT_PROTOCOL_FEE: felt252 = 'Insufficient protocol fee'; + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + /// Constructor of the contract + /// + /// # Arguments + /// + /// * `_max_protocol_fee` - The maximum protocol fee that can be set. + /// * `_protocol_fee` - The current protocol fee.s + /// * `_beneficiary` -The beneficiary of protocol fees. + /// * `_owner` - The owner of the contract + /// * `_token_address` - The token used as fee + #[constructor] + fn constructor( + ref self: ContractState, + _max_protocol_fee: u256, + _protocol_fee: u256, + _beneficiary: ContractAddress, + _owner: ContractAddress, + _token_address: ContractAddress + ) { + self.max_protocol_fee.write(_max_protocol_fee); + self._set_protocol_fee(_protocol_fee); + self._set_beneficiary(_beneficiary); + self.ownable.initializer(_owner); + self.fee_token.write(_token_address); + } + + #[abi(embed_v0)] + impl IPostDispatchHookImpl of IPostDispatchHook { + fn hook_type(self: @ContractState) -> Types { + Types::PROTOCOL_FEE(()) + } + /// Returns whether the hook supports metadata + /// + /// # Arguments + /// + /// * - `_metadata` - metadata + /// + /// # Returns + /// + /// boolean - whether the hook supports metadata + fn supports_metadata(self: @ContractState, _metadata: Bytes) -> bool { + _metadata.size() == 0 || StandardHookMetadata::variant(_metadata) == VARIANT.into() + } + + /// Post action after a message is dispatched via the Mailbox + /// Dev: reverts if invalid metadata variant + /// + /// # Arguments + /// + /// * - `_metadata` - the metadata required for the hook + /// * - `_message` - the message passed from the Mailbox.dispatch() call + /// * - `_fee_amount` - the payment provided for sending the message + fn post_dispatch( + ref self: ContractState, _metadata: Bytes, _message: Message, _fee_amount: u256 + ) { + assert(self.supports_metadata(_metadata.clone()), Errors::INVALID_METADATA_VARIANT); + self._post_dispatch(_metadata, _message, _fee_amount); + } + + /// Computes the payment required by the postDispatch call + /// Dev: reverts if invalid metadata variant + /// + /// # Arguments + /// + /// * - `_metadata` - The metadata required for the hook + /// * - `_message` - the message passed from the Mailbox.dispatch() call + /// + /// # Returns + /// + /// u256 - Quoted payment for the postDispatch call + fn quote_dispatch(ref self: ContractState, _metadata: Bytes, _message: Message) -> u256 { + assert(self.supports_metadata(_metadata.clone()), Errors::INVALID_METADATA_VARIANT); + self._quote_dispatch(_metadata, _message) + } + } + + #[abi(embed_v0)] + pub impl IProtocolFeeImpl of IProtocolFee { + fn get_protocol_fee(self: @ContractState) -> u256 { + self.protocol_fee.read() + } + + /// Sets the protocol fee. + /// + /// # Arguments + /// + /// * - `_protocol_fee` - The new protocol fee. + fn set_protocol_fee(ref self: ContractState, _protocol_fee: u256) { + self.ownable.assert_only_owner(); + self._set_protocol_fee(_protocol_fee); + } + + fn get_beneficiary(self: @ContractState) -> ContractAddress { + self.beneficiary.read() + } + + /// Sets the beneficiary of protocol fees. + /// + /// # Arguments + /// + /// * - `_beneficiary` - The new beneficiary. + fn set_beneficiary(ref self: ContractState, _beneficiary: ContractAddress) { + self.ownable.assert_only_owner(); + self._set_beneficiary(_beneficiary); + } + + /// Collects protocol fees from the contract. + /// Fees are sent to the beneficary address + fn collect_protocol_fees(ref self: ContractState) { + let token_dispatcher = ERC20ABIDispatcher { contract_address: self.fee_token.read() }; + let contract_address = get_contract_address(); + let balance = token_dispatcher.balanceOf(contract_address); + assert(balance != 0, Errors::INSUFFICIENT_BALANCE); + token_dispatcher.transfer(self.beneficiary.read(), balance); + } + } + + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Post action after a message is dispatched via the Mailbox (in our case, nothing because the ) + /// + /// # Arguments + /// + /// * - `_metadata` - the metadata required for the hook + /// * - `_message` - the message passed from the Mailbox.dispatch() call + /// * - `_fee_amount` - the payment provided for sending the message + fn _post_dispatch( + ref self: ContractState, _metadata: Bytes, _message: Message, _fee_amount: u256 + ) { // Since payment is exact, no need for further operation + } + + /// Returns the static protocol fee + /// + /// # Arguments + /// + /// * - `_metadata` - The metadata required for the hook + /// * - `_message` - the message passed from the Mailbox.dispatch() call + /// + /// # Returns + /// + /// u256 - Quoted payment for the postDispatch call + fn _quote_dispatch(ref self: ContractState, _metadata: Bytes, _message: Message) -> u256 { + self.protocol_fee.read() + } + + /// Sets the protocol fee. + /// Dev: reverts if protocol exceeds max protocol fee + /// + /// # Arguments + /// + /// * - `_protocol_fee` - The new protocol fee. + fn _set_protocol_fee(ref self: ContractState, _protocol_fee: u256) { + assert(_protocol_fee <= self.max_protocol_fee.read(), Errors::EXCEEDS_MAX_PROTOCOL_FEE); + self.protocol_fee.write(_protocol_fee); + } + + /// Sets the beneficiary of protocol fees. + /// Dev: reverts if beneficiary is null address + /// + /// # Arguments + /// + /// * - `_beneficiary` - The new beneficiary. + fn _set_beneficiary(ref self: ContractState, _beneficiary: ContractAddress) { + assert(_beneficiary != contract_address_const::<0>(), Errors::INVALID_BENEFICARY); + self.beneficiary.write(_beneficiary); + } + } +} diff --git a/starknet/cairo/crates/contracts/src/interfaces.cairo b/starknet/cairo/crates/contracts/src/interfaces.cairo new file mode 100644 index 00000000000..cd3c68eeb31 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/interfaces.cairo @@ -0,0 +1,315 @@ +use alexandria_bytes::Bytes; +use contracts::hooks::merkle_tree_hook::merkle_tree_hook::Tree; +use contracts::libs::message::Message; +use core::array::ArrayTrait; +use starknet::ContractAddress; +use starknet::EthAddress; + +pub fn ETH_ADDRESS() -> ContractAddress { + 0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7.try_into().unwrap() +} + +#[derive(Serde, Drop, Debug, PartialEq)] +pub enum Types { + UNUSED, + ROUTING, + AGGREGATION, + MERKLE_TREE, + INTERCHAIN_GAS_PAYMASTER, + FALLBACK_ROUTING, + ID_AUTH_ISM, + PAUSABLE, + PROTOCOL_FEE, + LAYER_ZERO_V1, + Rate_Limited_Hook +} + + +#[derive(Serde, Drop, PartialEq, Debug, starknet::Store)] +pub enum ModuleType { + UNUSED: ContractAddress, + ROUTING: ContractAddress, + AGGREGATION: ContractAddress, + LEGACY_MULTISIG: ContractAddress, + MERKLE_ROOT_MULTISIG: ContractAddress, + MESSAGE_ID_MULTISIG: ContractAddress, + NULL, // used with relayer carrying no metadata + CCIP_READ: ContractAddress, +} + + +#[starknet::interface] +pub trait IMailbox { + fn get_local_domain(self: @TContractState) -> u32; + + fn delivered(self: @TContractState, _message_id: u256) -> bool; + + fn nonce(self: @TContractState) -> u32; + + fn get_default_ism(self: @TContractState) -> ContractAddress; + + fn get_default_hook(self: @TContractState) -> ContractAddress; + + fn get_required_hook(self: @TContractState) -> ContractAddress; + + fn get_latest_dispatched_id(self: @TContractState) -> u256; + + fn dispatch( + ref self: TContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes, + _fee_amount: u256, + _custom_hook_metadata: Option, + _custom_hook: Option, + ) -> u256; + + fn quote_dispatch( + self: @TContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes, + _custom_hook_metadata: Option, + _custom_hook: Option, + ) -> u256; + + fn process(ref self: TContractState, _metadata: Bytes, _message: Message); + + fn recipient_ism(self: @TContractState, _recipient: u256) -> ContractAddress; + + fn set_default_ism(ref self: TContractState, _module: ContractAddress); + + fn set_default_hook(ref self: TContractState, _hook: ContractAddress); + + fn set_required_hook(ref self: TContractState, _hook: ContractAddress); + + fn processor(self: @TContractState, _id: u256) -> ContractAddress; + + fn processed_at(self: @TContractState, _id: u256) -> u64; +} + + +#[starknet::interface] +pub trait IInterchainSecurityModule { + fn module_type(self: @TContractState) -> ModuleType; + + fn verify(self: @TContractState, _metadata: Bytes, _message: Message,) -> bool; +} + +#[starknet::interface] +pub trait IValidatorConfiguration { + fn validators_and_threshold( + self: @TContractState, _message: Message + ) -> (Span, u32); + + fn get_validators(self: @TContractState) -> Span; + + fn get_threshold(self: @TContractState) -> u32; +} + +#[starknet::interface] +pub trait ISpecifiesInterchainSecurityModule { + fn interchain_security_module(self: @TContractState) -> ContractAddress; +} + + +#[starknet::interface] +pub trait IPostDispatchHook { + fn hook_type(self: @TContractState) -> Types; + + fn supports_metadata(self: @TContractState, _metadata: Bytes) -> bool; + + fn post_dispatch( + ref self: TContractState, _metadata: Bytes, _message: Message, _fee_amount: u256 + ); + + fn quote_dispatch(ref self: TContractState, _metadata: Bytes, _message: Message) -> u256; +} + + +#[starknet::interface] +pub trait IMessageRecipient { + fn handle(ref self: TContractState, _origin: u32, _sender: u256, _message: Bytes); + + fn get_origin(self: @TContractState) -> u32; + + fn get_sender(self: @TContractState) -> u256; + + fn get_message(self: @TContractState) -> Bytes; +} + + +#[starknet::interface] +pub trait IMailboxClient { + fn set_hook(ref self: TContractState, _hook: ContractAddress); + + fn set_interchain_security_module(ref self: TContractState, _module: ContractAddress); + + fn get_hook(self: @TContractState) -> ContractAddress; + + fn get_local_domain(self: @TContractState) -> u32; + + fn interchain_security_module(self: @TContractState) -> ContractAddress; + + fn _is_latest_dispatched(self: @TContractState, _id: u256) -> bool; + + fn _is_delivered(self: @TContractState, _id: u256) -> bool; + + fn mailbox(self: @TContractState) -> ContractAddress; + + fn _dispatch( + self: @TContractState, + _destination_domain: u32, + _recipient: u256, + _message_body: Bytes, + _fee_amount: u256, + _hook_metadata: Option, + _hook: Option + ) -> u256; + + fn quote_dispatch( + self: @TContractState, + _destination_domain: u32, + _recipient: u256, + _message_body: Bytes, + _hook_metadata: Option, + _hook: Option + ) -> u256; +} + + +#[starknet::interface] +pub trait IInterchainGasPaymaster { + fn pay_for_gas( + ref self: TContractState, + _message_id: u256, + _destination_domain: u32, + _gas_amount: u256, + _payment: u256 + ); + + fn quote_gas_payment( + ref self: TContractState, _destination_domain: u32, _gas_amount: u256 + ) -> u256; +} + + +#[starknet::interface] +pub trait IDefaultFallbackRoutingIsm { + /// Returns an enum that represents the type of security model encoded by this ISM. + /// Relayers infer how to fetch and format metadata. + fn module_type(self: @TContractState) -> ModuleType; + + fn route(self: @TContractState, _message: Message) -> ContractAddress; + + fn verify(self: @TContractState, _metadata: Bytes, _message: Message) -> bool; +} + +#[starknet::interface] +pub trait IDomainRoutingIsm { + fn initialize(ref self: TContractState, _domains: Span, _modules: Span); + + fn set(ref self: TContractState, _domain: u32, _module: ContractAddress); + + fn remove(ref self: TContractState, _domain: u32); + + fn domains(self: @TContractState) -> Span; + + fn module(self: @TContractState, _origin: u32) -> ContractAddress; +} + + +#[starknet::interface] +pub trait IValidatorAnnounce { + fn get_announced_validators(self: @TContractState) -> Span; + + fn get_announced_storage_locations( + self: @TContractState, _validators: Span + ) -> Span>>; + + fn announce( + ref self: TContractState, + _validator: EthAddress, + _storage_location: Array, + _signature: Bytes + ) -> bool; + + fn get_announcement_digest(self: @TContractState, _storage_location: Array) -> u256; +} + +#[starknet::interface] +pub trait IMockValidatorAnnounce { + fn get_announced_validators(self: @TContractState) -> Span; + + fn get_announced_storage_locations( + self: @TContractState, _validators: Span + ) -> Span>>; + + fn announce( + ref self: TContractState, + _validator: EthAddress, + _storage_location: Array, + _signature: Bytes + ) -> bool; + + fn get_announcement_digest(self: @TContractState, _storage_location: Array,) -> u256; +} + +#[starknet::interface] +pub trait IAggregation { + fn module_type(self: @TContractState) -> ModuleType; + + fn modules_and_threshold( + self: @TContractState, _message: Message + ) -> (Span, u8); + + fn verify(self: @TContractState, _metadata: Bytes, _message: Message,) -> bool; + + fn get_modules(self: @TContractState) -> Span; + + fn get_threshold(self: @TContractState) -> u8; +} + + +#[starknet::interface] +pub trait IMerkleTreeHook { + fn count(self: @TContractState) -> u32; + + fn root(self: @TContractState) -> u256; + + fn tree(self: @TContractState) -> Tree; + + fn latest_checkpoint(self: @TContractState) -> (u256, u32); +} + + +#[starknet::interface] +pub trait IPausableIsm { + fn module_type(self: @TContractState) -> ModuleType; + + fn verify(self: @TContractState, _metadata: Bytes, _message: Message) -> bool; + + fn pause(ref self: TContractState); + + fn unpause(ref self: TContractState); +} + +#[starknet::interface] +pub trait IProtocolFee { + fn get_protocol_fee(self: @TContractState) -> u256; + + fn set_protocol_fee(ref self: TContractState, _protocol_fee: u256); + + fn get_beneficiary(self: @TContractState) -> ContractAddress; + + fn set_beneficiary(ref self: TContractState, _beneficiary: ContractAddress); + + fn collect_protocol_fees(ref self: TContractState); +} + + +#[starknet::interface] +pub trait IRoutingIsm { + fn route(self: @TContractState, _message: Message) -> ContractAddress; +} + diff --git a/starknet/cairo/crates/contracts/src/isms/aggregation/aggregation.cairo b/starknet/cairo/crates/contracts/src/isms/aggregation/aggregation.cairo new file mode 100644 index 00000000000..7f5632a0612 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/aggregation/aggregation.cairo @@ -0,0 +1,207 @@ +#[starknet::contract] +pub mod aggregation { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::interfaces::{ + IAggregationDispatcher, IAggregation, IAggregationDispatcherTrait, ModuleType, + IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, + }; + use contracts::libs::aggregation_ism_metadata::aggregation_ism_metadata::AggregationIsmMetadata; + use contracts::libs::message::{Message, MessageTrait}; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, contract_address_const}; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + + #[storage] + struct Storage { + modules: LegacyMap::, + threshold: u8, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + + pub mod Errors { + pub const VERIFICATION_FAILED: felt252 = 'Verification failed'; + pub const THRESHOLD_NOT_REACHED: felt252 = 'Threshold not reached'; + pub const MODULE_ADDRESS_CANNOT_BE_NULL: felt252 = 'Module address cannot be null'; + pub const THRESHOLD_NOT_SET: felt252 = 'Threshold not set'; + pub const MODULES_ALREADY_STORED: felt252 = 'Modules already stored'; + pub const NO_MODULES_PROVIDED: felt252 = 'No modules provided'; + pub const THRESHOLD_TOO_HIGH: felt252 = 'Threshold too high'; + pub const TOO_MANY_MODULES_PROVIDED: felt252 = 'Too many modules provided'; + } + + #[constructor] + fn constructor( + ref self: ContractState, _owner: ContractAddress, _modules: Span, _threshold: u8 + ) { + self.ownable.initializer(_owner); + assert(_threshold <= 255, Errors::THRESHOLD_TOO_HIGH); + self.threshold.write(_threshold); + assert(_modules.len() < 256, Errors::TOO_MANY_MODULES_PROVIDED); + self.set_modules(_modules); + } + + #[abi(embed_v0)] + impl IAggregationImpl of IAggregation { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::AGGREGATION(starknet::get_contract_address()) + } + + + /// Returns the set of ISMs responsible for verifying _message and the number of ISMs that must verify + /// Dev: Can change based on the content of _message + /// + /// # Arguments + /// + /// * - `_message` - the message to consider + /// + /// # Returns + /// + /// Span - The array of ISM addresses + /// threshold - The number of ISMs needed to verify + fn modules_and_threshold( + self: @ContractState, _message: Message + ) -> (Span, u8) { + // THE USER CAN DEFINE HERE CONDITIONS FOR THE MODULE AND THRESHOLD SELECTION + let threshold = self.threshold.read(); + (self.build_modules_span(), threshold) + } + + + /// Requires that m-of-n ISMs verify the provided interchain message. + /// Dev: Can change based on the content of _message + /// Dev: Reverts if threshold is not set + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see aggregation_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// boolean - wheter the verification succeed or not. + fn verify(self: @ContractState, _metadata: Bytes, _message: Message,) -> bool { + let (isms, mut threshold) = self.modules_and_threshold(_message.clone()); + + assert(threshold != 0, Errors::THRESHOLD_NOT_SET); + let modules = self.build_modules_span(); + let mut cur_idx: u8 = 0; + loop { + if (threshold == 0) { + break (); + } + if (cur_idx.into() == isms.len()) { + break (); + } + if (!AggregationIsmMetadata::has_metadata(_metadata.clone(), cur_idx)) { + cur_idx += 1; + continue; + } + let ism = IInterchainSecurityModuleDispatcher { + contract_address: *modules.at(cur_idx.into()) + }; + + let metadata = AggregationIsmMetadata::metadata_at(_metadata.clone(), cur_idx); + assert(ism.verify(metadata, _message.clone()), Errors::VERIFICATION_FAILED); + threshold -= 1; + cur_idx += 1; + }; + assert(threshold == 0, Errors::THRESHOLD_NOT_REACHED); + true + } + + fn get_modules(self: @ContractState) -> Span { + self.build_modules_span() + } + + fn get_threshold(self: @ContractState) -> u8 { + self.threshold.read() + } + } + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Sets the ISM modules responsible for the verification + /// Dev: reverts if module address is null or if empty array + /// Dev: Callable only once during initialization + /// + /// # Arguments + /// + /// * - `_modules` - a span of module contract addresses + /// + fn set_modules(ref self: ContractState, _modules: Span) { + assert(_modules.len() != 0, Errors::NO_MODULES_PROVIDED); + let mut last_module = contract_address_const::<0>(); + let mut cur_idx = 0; + loop { + if (cur_idx == _modules.len()) { + break (); + } + let module: ContractAddress = (*_modules.at(cur_idx)).try_into().unwrap(); + assert( + module != contract_address_const::<0>(), Errors::MODULE_ADDRESS_CANNOT_BE_NULL + ); + self.modules.write(last_module, module); + cur_idx += 1; + last_module = module; + } + } + /// Helper: finds the index associated to a module in the legacy map + /// + /// # Returns + /// + /// Option - the contract if found, else None + fn find_module_index( + self: @ContractState, _module: ContractAddress + ) -> Option { + let mut current_module: ContractAddress = 0.try_into().unwrap(); + loop { + let next_module = self.modules.read(current_module); + if next_module == _module { + break Option::Some(current_module); + } else if next_module == 0.try_into().unwrap() { + break Option::None(()); + } + current_module = next_module; + } + } + + /// Helper: Build a module span out of a storage map + /// + /// # Returns + /// + /// Span - a span of module addresses + fn build_modules_span(self: @ContractState) -> Span { + let mut cur_address = contract_address_const::<0>(); + let mut modules = array![]; + loop { + let next_address = self.modules.read(cur_address); + if (next_address == contract_address_const::<0>()) { + break (); + } + modules.append(next_address); + cur_address = next_address + }; + modules.span() + } + } +} diff --git a/starknet/cairo/crates/contracts/src/isms/multisig/merkleroot_multisig_ism.cairo b/starknet/cairo/crates/contracts/src/isms/multisig/merkleroot_multisig_ism.cairo new file mode 100644 index 00000000000..b47b421f778 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/multisig/merkleroot_multisig_ism.cairo @@ -0,0 +1,250 @@ +#[starknet::contract] +pub mod merkleroot_multisig_ism { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::hooks::merkle_tree_hook::merkle_tree_hook::MerkleInternalImpl; + use contracts::interfaces::{ + ModuleType, IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, IValidatorConfiguration, + }; + use contracts::libs::checkpoint_lib::checkpoint_lib::CheckpointLib; + use contracts::libs::message::{Message, MessageTrait}; + use contracts::libs::multisig::merkleroot_ism_metadata::merkleroot_ism_metadata::MerkleRootIsmMetadata; + use contracts::utils::keccak256::{ByteData, HASH_SIZE, bool_is_eth_signature_valid}; + use openzeppelin::access::ownable::OwnableComponent; + use starknet::ContractAddress; + use starknet::EthAddress; + use starknet::secp256_trait::{Signature, signature_from_vrs}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + + #[storage] + struct Storage { + validators: LegacyMap, + threshold: u32, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } + + mod Errors { + pub const NO_MULTISIG_THRESHOLD_FOR_MESSAGE: felt252 = 'No MultisigISM treshold present'; + pub const INVALID_MERKLE_INDEX: felt252 = 'Invalid merkle index metadata'; + pub const NO_MATCH_FOR_SIGNATURE: felt252 = 'No match for given signature'; + pub const EMPTY_METADATA: felt252 = 'Empty metadata'; + pub const VALIDATOR_ADDRESS_CANNOT_BE_NULL: felt252 = 'Validator address cannot be 0'; + pub const NO_VALIDATORS_PROVIDED: felt252 = 'No validators provided'; + pub const THRESHOLD_TOO_HIGH: felt252 = 'Threshold too high'; + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, + _owner: ContractAddress, + _validators: Span, + _threshold: u32 + ) { + self.ownable.initializer(_owner); + assert(_threshold <= 0xffffffff, Errors::THRESHOLD_TOO_HIGH); + self.threshold.write(_threshold); + self.set_validators(_validators); + } + + #[abi(embed_v0)] + impl IMerklerootMultisigIsmImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::MERKLE_ROOT_MULTISIG(starknet::get_contract_address()) + } + + /// Requires that m-of-n ISMs verify the provided interchain message. + /// Dev: Can change based on the content of _message + /// Dev: Reverts if threshold is not set or no match for signature + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see merkleroot_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// boolean - wheter the verification succeed or not. + fn verify(self: @ContractState, _metadata: Bytes, _message: Message,) -> bool { + assert(_metadata.clone().size() > 0, Errors::EMPTY_METADATA); + let digest = self.digest(_metadata.clone(), _message.clone()); + let (validators, threshold) = self.validators_and_threshold(_message); + assert(threshold > 0, Errors::NO_MULTISIG_THRESHOLD_FOR_MESSAGE); + let mut validator_index = 0; + let mut i = 0; + // Assumes that signatures are ordered by validator + loop { + if (i == threshold) { + break (); + } + let signature = self.get_signature_at(_metadata.clone(), i); + // we loop on the validators list public key in order to find a match + let is_signer_in_list = loop { + if (validator_index == validators.len()) { + break false; + } + let signer = *validators.at(validator_index); + if bool_is_eth_signature_valid(digest, signature, signer) { + // we found a match + break true; + } + validator_index += 1; + }; + assert(is_signer_in_list, Errors::NO_MATCH_FOR_SIGNATURE); + validator_index += 1; + i += 1; + }; + true + } + } + + + #[abi(embed_v0)] + impl IValidatorConfigurationImpl of IValidatorConfiguration { + fn get_validators(self: @ContractState) -> Span { + self.build_validators_span() + } + + fn get_threshold(self: @ContractState) -> u32 { + self.threshold.read() + } + + /// Returns the set of validators responsible for verifying _message and the number of signatures required + /// Dev: Can change based on the content of _message + /// + /// # Arguments + /// + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// Span - a span of ethereum validator addresses + /// u32 - The number of validator signatures needed + fn validators_and_threshold( + self: @ContractState, _message: Message + ) -> (Span, u32) { + // USER CONTRACT DEFINITION HERE + // USER CAN SPECIFY VALIDATORS SELECTION CONDITIONS + let threshold = self.threshold.read(); + (self.build_validators_span(), threshold) + } + } + + + #[generate_trait] + pub impl MerkleISMInternalImpl of InternalTrait { + /// Returns the digest to be used for signature verification. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see merkleroot_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// u256 - The digest to be signed by validators + fn digest(self: @ContractState, _metadata: Bytes, _message: Message) -> u256 { + assert( + MerkleRootIsmMetadata::message_index( + _metadata.clone() + ) <= MerkleRootIsmMetadata::signed_index(_metadata.clone()), + Errors::INVALID_MERKLE_INDEX + ); + let origin_merkle_tree_hook = MerkleRootIsmMetadata::origin_merkle_tree_hook( + _metadata.clone() + ); + let signed_index = MerkleRootIsmMetadata::signed_index(_metadata.clone()); + let signed_message_id = MerkleRootIsmMetadata::signed_message_id(_metadata.clone()); + let (id, _) = MessageTrait::format_message(_message.clone()); + let proof = MerkleRootIsmMetadata::proof(_metadata.clone()); + let message_index = MerkleRootIsmMetadata::message_index(_metadata.clone()); + let mut cur_idx = 0; + let mut formatted_proof = array![]; + loop { + if (cur_idx == proof.len()) { + break (); + } + formatted_proof.append(ByteData { value: *proof.at(cur_idx), size: HASH_SIZE }); + cur_idx += 1; + }; + let signed_root = MerkleInternalImpl::_branch_root( + ByteData { value: id, size: HASH_SIZE }, + formatted_proof.span(), + message_index.into() + ); + CheckpointLib::digest( + _message.origin, + origin_merkle_tree_hook.into(), + signed_root.into(), + signed_index, + signed_message_id + ) + } + + /// Returns the signature at a given index from the metadata. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see merkleroot_ism_metadata.cairo) + /// * - `_index` - The index of the signature to return + /// + /// # Returns + /// + /// Signature - A formatted signature (see Signature structure) + fn get_signature_at(self: @ContractState, _metadata: Bytes, _index: u32) -> Signature { + let (v, r, s) = MerkleRootIsmMetadata::signature_at(_metadata, _index); + signature_from_vrs(v.into(), r, s) + } + + /// Helper: buils an span of Ethereum validators addresses from the Storage Map + fn build_validators_span(self: @ContractState) -> Span { + let mut validators = ArrayTrait::new(); + let mut cur_idx = 0; + loop { + let validator = self.validators.read(cur_idx); + if (validator == 0.try_into().unwrap()) { + break (); + } + validators.append(validator); + cur_idx += 1; + }; + validators.span() + } + + /// Sets a span of validators responsible to verify the message + /// Dev: callable only during initialization + /// Dev: reverts if null validator address or empty span + /// + /// # Arguments + /// + /// * - `_validators` - a span of validators to set + fn set_validators(ref self: ContractState, _validators: Span) { + assert(_validators.len() != 0, Errors::NO_VALIDATORS_PROVIDED); + let mut cur_idx = 0; + + loop { + if (cur_idx == _validators.len()) { + break (); + } + let validator: EthAddress = (*_validators.at(cur_idx)).try_into().unwrap(); + assert( + validator != 0.try_into().unwrap(), Errors::VALIDATOR_ADDRESS_CANNOT_BE_NULL + ); + self.validators.write(cur_idx.into(), validator); + cur_idx += 1; + } + } + } +} diff --git a/starknet/cairo/crates/contracts/src/isms/multisig/messageid_multisig_ism.cairo b/starknet/cairo/crates/contracts/src/isms/multisig/messageid_multisig_ism.cairo new file mode 100644 index 00000000000..5315d6af70a --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/multisig/messageid_multisig_ism.cairo @@ -0,0 +1,226 @@ +#[starknet::contract] +pub mod messageid_multisig_ism { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::interfaces::{ + ModuleType, IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, IValidatorConfiguration + }; + use contracts::libs::checkpoint_lib::checkpoint_lib::CheckpointLib; + use contracts::libs::message::{Message, MessageTrait}; + use contracts::libs::multisig::message_id_ism_metadata::message_id_ism_metadata::MessageIdIsmMetadata; + use contracts::utils::keccak256::bool_is_eth_signature_valid; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::ContractAddress; + use starknet::EthAddress; + use starknet::secp256_trait::{Signature, signature_from_vrs}; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + #[storage] + struct Storage { + validators: LegacyMap, + threshold: u32, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + mod Errors { + pub const NO_MULTISIG_THRESHOLD_FOR_MESSAGE: felt252 = 'No MultisigISM treshold present'; + pub const NO_MATCH_FOR_SIGNATURE: felt252 = 'No match for given signature'; + pub const EMPTY_METADATA: felt252 = 'Empty metadata'; + pub const VALIDATOR_ADDRESS_CANNOT_BE_NULL: felt252 = 'Validator address cannot be 0'; + pub const NO_VALIDATORS_PROVIDED: felt252 = 'No validators provided'; + pub const THRESHOLD_TOO_HIGH: felt252 = 'Threshold too high'; + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + + #[constructor] + fn constructor( + ref self: ContractState, + _owner: ContractAddress, + _validators: Span, + _threshold: u32 + ) { + self.ownable.initializer(_owner); + assert(_threshold <= 0xffffffff, Errors::THRESHOLD_TOO_HIGH); + self.threshold.write(_threshold); + self.set_validators(_validators); + } + + #[abi(embed_v0)] + impl IMessageidMultisigIsmImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::MESSAGE_ID_MULTISIG(starknet::get_contract_address()) + } + + /// Requires that m-of-n ISMs verify the provided interchain message. + /// Dev: Can change based on the content of _message + /// Dev: Reverts if threshold is not set or no match for signature + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see messageid_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// boolean - wheter the verification succeed or not. + fn verify(self: @ContractState, _metadata: Bytes, _message: Message,) -> bool { + assert(_metadata.clone().size() > 0, Errors::EMPTY_METADATA); + let digest = self.digest(_metadata.clone(), _message.clone()); + let (validators, threshold) = self.validators_and_threshold(_message); + assert(threshold > 0, Errors::NO_MULTISIG_THRESHOLD_FOR_MESSAGE); + let mut validator_index = 0; + let mut i = 0; + // for each couple (sig_s, sig_r) extracted from the metadata + loop { + if (i == threshold) { + break (); + } + let signature = self.get_signature_at(_metadata.clone(), i); + // we loop on the validators list public key in order to find a match + let is_signer_in_list = loop { + if (validator_index == validators.len()) { + break false; + } + let signer = *validators.at(validator_index); + if bool_is_eth_signature_valid(digest, signature, signer) { + // we found a match + break true; + } + validator_index += 1; + }; + assert(is_signer_in_list, Errors::NO_MATCH_FOR_SIGNATURE); + validator_index += 1; + i += 1; + }; + true + } + } + + + #[abi(embed_v0)] + impl IValidorConfigurationImpl of IValidatorConfiguration { + fn get_validators(self: @ContractState) -> Span { + self.build_validators_span() + } + + fn get_threshold(self: @ContractState) -> u32 { + self.threshold.read() + } + + /// Returns the set of validators responsible for verifying _message and the number of signatures required + /// Dev: Can change based on the content of _message + /// + /// # Arguments + /// + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// Span - a span of ethereum validator addresses + /// u32 - The number of validator signatures needed + fn validators_and_threshold( + self: @ContractState, _message: Message + ) -> (Span, u32) { + // USER CONTRACT DEFINITION HERE + // USER CAN SPECIFY VALIDATORS SELECTION CONDITIONS + let threshold = self.threshold.read(); + (self.build_validators_span(), threshold) + } + } + + #[generate_trait] + pub impl MessageIdInternalImpl of InternalTrait { + /// Returns the digest to be used for signature verification. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see messageid_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// u256 - The digest to be signed by validators + fn digest(self: @ContractState, _metadata: Bytes, _message: Message) -> u256 { + let origin_merkle_tree_hook = MessageIdIsmMetadata::origin_merkle_tree_hook( + _metadata.clone() + ); + let root = MessageIdIsmMetadata::root(_metadata.clone()); + let index = MessageIdIsmMetadata::index(_metadata.clone()); + let (format_message, _) = MessageTrait::format_message(_message.clone()); + CheckpointLib::digest( + _message.origin, origin_merkle_tree_hook.into(), root.into(), index, format_message + ) + } + + /// Returns the signature at a given index from the metadata. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see messageid_ism_metadata.cairo) + /// * - `_index` - The index of the signature to return + /// + /// # Returns + /// + /// Signature - A formatted signature (see Signature structure) + fn get_signature_at(self: @ContractState, _metadata: Bytes, _index: u32) -> Signature { + let (v, r, s) = MessageIdIsmMetadata::signature_at(_metadata, _index); + signature_from_vrs(v.into(), r, s) + } + + /// Returns a span of Ethereum addresses for validators based on a stored Legacy Map. + fn build_validators_span(self: @ContractState) -> Span { + let mut validators = ArrayTrait::new(); + let mut cur_idx = 0; + loop { + let validator = self.validators.read(cur_idx); + if (validator == 0.try_into().unwrap()) { + break (); + } + validators.append(validator); + cur_idx += 1; + }; + validators.span() + } + + + /// Sets a span of validators responsible to verify the message + /// Dev: callable only during initialization + /// Dev: reverts if null validator address or empty span + /// + /// # Arguments + /// + /// * - `_validators` - a span of validators to set + fn set_validators(ref self: ContractState, _validators: Span) { + assert(_validators.len() != 0, Errors::NO_VALIDATORS_PROVIDED); + let mut cur_idx = 0; + loop { + if (cur_idx == _validators.len()) { + break (); + } + let validator: EthAddress = (*_validators.at(cur_idx)).try_into().unwrap(); + assert( + validator != 0.try_into().unwrap(), Errors::VALIDATOR_ADDRESS_CANNOT_BE_NULL + ); + self.validators.write(cur_idx.into(), validator); + cur_idx += 1; + } + } + } +} diff --git a/starknet/cairo/crates/contracts/src/isms/multisig/validator_announce.cairo b/starknet/cairo/crates/contracts/src/isms/multisig/validator_announce.cairo new file mode 100644 index 00000000000..7ad6a63b31d --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/multisig/validator_announce.cairo @@ -0,0 +1,315 @@ +#[starknet::contract] +pub mod validator_announce { + use alexandria_bytes::{Bytes, BytesTrait}; + use alexandria_data_structures::array_ext::ArrayTraitExt; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl, + MailboxclientComponent::MailboxClientImpl + }; + use contracts::interfaces::{ + IMailboxClientDispatcher, IMailboxClientDispatcherTrait, IValidatorAnnounce + }; + use contracts::libs::checkpoint_lib::checkpoint_lib::HYPERLANE_ANNOUNCEMENT; + use contracts::utils::keccak256::{ + reverse_endianness, to_eth_signature, compute_keccak, ByteData, u256_word_size, + u64_word_size, HASH_SIZE, bool_is_eth_signature_valid + }; + use contracts::utils::store_arrays::StoreFelt252Array; + use core::poseidon::poseidon_hash_span; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ + ContractAddress, ClassHash, EthAddress, secp256_trait::{Signature, signature_from_vrs} + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + storage_location_len: LegacyMap::, + storage_locations: LegacyMap::<(EthAddress, u256), Array>, + replay_protection: LegacyMap::, + validators: LegacyMap::, + } + + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ValidatorAnnouncement: ValidatorAnnouncement, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + } + + #[derive(starknet::Event, Drop)] + pub struct ValidatorAnnouncement { + pub validator: EthAddress, + pub storage_location: Span + } + + pub mod Errors { + pub const REPLAY_PROTECTION_ERROR: felt252 = 'Announce already occured'; + pub const WRONG_SIGNER: felt252 = 'Wrong signer'; + } + + #[constructor] + fn constructor(ref self: ContractState, _mailbox: ContractAddress, _owner: ContractAddress) { + self.mailboxclient.initialize(_mailbox, Option::None, Option::None); + self.ownable.initializer(_owner); + } + + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IValidatorAnnonceImpl of IValidatorAnnounce { + /// Announces a validator signature storage location + /// Dev: reverts if announce already occured or if wrong signer + /// + /// # Arguments + /// + /// * - `_validator` - The validator to consider + /// * - `_storage_location` - Information encoding the location of signed + /// * - `_signature` -The signed validator announcement + /// + /// # Returns + /// + /// boolean - True upon success + fn announce( + ref self: ContractState, + _validator: EthAddress, + _storage_location: Array, + _signature: Bytes + ) -> bool { + let felt252_validator: felt252 = _validator.into(); + let mut _input: Array = array![felt252_validator.into()]; + let mut u256_storage_location: Array = array![]; + let mut cur_idx = 0; + let span_storage_location = _storage_location.span(); + loop { + if (cur_idx == span_storage_location.len()) { + break (); + } + u256_storage_location.append((*span_storage_location.at(cur_idx)).into()); + cur_idx += 1; + }; + let replay_id = poseidon_hash_span( + array![felt252_validator].concat(@_storage_location).span() + ); + assert(!self.replay_protection.read(replay_id), Errors::REPLAY_PROTECTION_ERROR); + let announcement_digest = self.get_announcement_digest(u256_storage_location); + let signature: Signature = convert_to_signature(_signature); + assert( + bool_is_eth_signature_valid(announcement_digest, signature, _validator), + Errors::WRONG_SIGNER + ); + match self.find_validators_index(_validator) { + Option::Some => {}, + Option::None => { + let last_validator = self.find_last_validator(); + self.validators.write(last_validator, _validator); + } + }; + let mut validator_len = self.storage_location_len.read(_validator); + self.storage_locations.write((_validator, validator_len), _storage_location); + self.storage_location_len.write(_validator, validator_len + 1); + self.replay_protection.write(replay_id, true); + self + .emit( + ValidatorAnnouncement { + validator: _validator, storage_location: span_storage_location + } + ); + true + } + + + /// Returns a list of all announced storage locations + /// + /// # Arguments + /// + /// * - `_validators` - The span of validators to get registrations for + /// + /// # Returns + /// + /// Span> - A list of registered storage metadata + fn get_announced_storage_locations( + self: @ContractState, mut _validators: Span + ) -> Span>> { + let mut metadata = array![]; + loop { + match _validators.pop_front() { + Option::Some(validator) => { + let mut cur_idx = 0; + let validator_len = self.storage_location_len.read(*validator); + let mut validator_metadata = array![]; + loop { + if (cur_idx == validator_len) { + break (); + } + validator_metadata + .append(self.storage_locations.read((*validator, cur_idx))); + cur_idx += 1; + }; + metadata.append(validator_metadata.span()) + }, + Option::None => { break (); } + } + }; + metadata.span() + } + + /// Returns a list of validators that have made announcements + fn get_announced_validators(self: @ContractState) -> Span { + self.build_validators_array() + } + + + /// Returns the digest validators are expected to sign when signing announcements. + /// + /// # Arguments + /// + /// * - `_storage_location` - Storage location as array of u256 + /// + /// # Returns + /// + /// u256 - The digest of the announcement. + fn get_announcement_digest( + self: @ContractState, mut _storage_location: Array + ) -> u256 { + let domain_hash = self.domain_hash(); + let mut byte_data_storage_location = array![]; + loop { + match _storage_location.pop_front() { + Option::Some(storage) => { + byte_data_storage_location + .append( + ByteData { value: storage, size: u256_word_size(storage).into() } + ); + }, + Option::None => { break (); } + } + }; + let hash = reverse_endianness( + compute_keccak( + array![ByteData { value: domain_hash.into(), size: HASH_SIZE }] + .concat(@byte_data_storage_location) + .span() + ) + ); + to_eth_signature(hash) + } + } + + #[generate_trait] + pub impl ValidatorAnnounceInternalImpl of InternalTrait { + /// Returns the domain separator used in validator announcements. + fn domain_hash(self: @ContractState) -> u256 { + let mailbox_address: felt252 = self.mailboxclient.mailbox().try_into().unwrap(); + let mut input: Array = array![ + ByteData { value: self.mailboxclient.get_local_domain().into(), size: 4 }, + ByteData { value: mailbox_address.try_into().unwrap(), size: 32 }, + ByteData { value: HYPERLANE_ANNOUNCEMENT.into(), size: 22 } + ]; + reverse_endianness(compute_keccak(input.span())) + } + + /// Helper: finds the index associated to a given validator, if found + /// Dev: Chained list (EthereumAddress -> EthereumAddress) + /// + /// # Arguments + /// + /// * - `_validator` - The validator to consider + /// + /// # Returns + /// + /// EthAddress - the index of the validator in the Storage Map + fn find_validators_index( + self: @ContractState, _validator: EthAddress + ) -> Option { + let mut current_validator: EthAddress = 0.try_into().unwrap(); + loop { + let next_validator = self.validators.read(current_validator); + if next_validator == _validator { + break Option::Some(current_validator); + } else if next_validator == 0.try_into().unwrap() { + break Option::None; + } + current_validator = next_validator; + } + } + + /// Helper: finds the last stored validator + fn find_last_validator(self: @ContractState) -> EthAddress { + let mut current_validator = self.validators.read(0.try_into().unwrap()); + loop { + let next_validator = self.validators.read(current_validator); + if next_validator == 0.try_into().unwrap() { + break current_validator; + } + current_validator = next_validator; + } + } + + // Helper: builds a span of validators from the storage map + fn build_validators_array(self: @ContractState) -> Span { + let mut index = 0.try_into().unwrap(); + let mut validators = array![]; + loop { + let validator = self.validators.read(index); + if (validator == 0.try_into().unwrap()) { + break (); + } + validators.append(validator); + index = validator; + }; + + validators.span() + } + } + + /// Converts a byte signature into a standard singature format (see Signature structure) + /// + /// # Arguments + /// + /// * - ` _signature` - The byte encoded Signature + /// + /// # Returns + /// + /// Signature - Standardized signature + fn convert_to_signature(_signature: Bytes) -> Signature { + let (_, r) = _signature.read_u256(0); + let (_, s) = _signature.read_u256(32); + let (_, v) = _signature.read_u8(64); + signature_from_vrs(v.try_into().unwrap(), r, s) + } +} diff --git a/starknet/cairo/crates/contracts/src/isms/noop_ism.cairo b/starknet/cairo/crates/contracts/src/isms/noop_ism.cairo new file mode 100644 index 00000000000..5777a37685d --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/noop_ism.cairo @@ -0,0 +1,34 @@ +#[starknet::contract] +pub mod noop_ism { + use alexandria_bytes::Bytes; + use contracts::interfaces::{ + ModuleType, IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait + }; + use contracts::libs::message::Message; + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl IInterchainSecurityModuleImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::NULL(()) + } + + /// Requires that m-of-n ISMs verify the provided interchain message. + /// Dev: Can change based on the content of _message + /// Dev: Reverts if threshold is not set + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see aggregation_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// boolean - wheter the verification succeed or not. + fn verify(self: @ContractState, _metadata: Bytes, _message: Message) -> bool { + true + } + } +} diff --git a/starknet/cairo/crates/contracts/src/isms/pausable_ism.cairo b/starknet/cairo/crates/contracts/src/isms/pausable_ism.cairo new file mode 100644 index 00000000000..87f7f95df2a --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/pausable_ism.cairo @@ -0,0 +1,111 @@ +#[starknet::interface] +pub trait IPausableIsm { + fn pause(ref self: TContractState); + + fn unpause(ref self: TContractState); +} + + +#[starknet::contract] +pub mod pausable_ism { + use alexandria_bytes::Bytes; + use contracts::interfaces::{ + ModuleType, IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait + }; + use contracts::libs::message::{Message, MessageTrait}; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::security::pausable::PausableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, ClassHash}; + use super::{IPausableIsm, IPausableIsmDispatcher, IPausableIsmDispatcherTrait,}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: PausableComponent, storage: pausable, event: PausableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[abi(embed_v0)] + impl PausableImpl = PausableComponent::PausableImpl; + impl PausableInternalImpl = PausableComponent::InternalImpl; + + + #[storage] + struct Storage { + #[substorage(v0)] + pausable: PausableComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + PausableEvent: PausableComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, _owner: ContractAddress) { + self.ownable.initializer(_owner); + } + + #[abi(embed_v0)] + impl IInterchainSecurityModuleImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::NULL(()) + } + + /// Requires that m-of-n ISMs verify the provided interchain message. + /// Dev: Can change based on the content of _message + /// Dev: Reverts if threshold is not set + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see aggregation_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// boolean - wheter the verification succeed or not. + fn verify(self: @ContractState, _metadata: Bytes, _message: Message) -> bool { + self.pausable.assert_not_paused(); + true + } + } + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IPausableIsmImpl of IPausableIsm { + fn pause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable.pause(); + } + + fn unpause(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.pausable.unpause(); + } + } +} diff --git a/starknet/cairo/crates/contracts/src/isms/routing/default_fallback_routing_ism.cairo b/starknet/cairo/crates/contracts/src/isms/routing/default_fallback_routing_ism.cairo new file mode 100644 index 00000000000..1dafdd5f2de --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/routing/default_fallback_routing_ism.cairo @@ -0,0 +1,292 @@ +#[starknet::contract] +pub mod default_fallback_routing_ism { + use alexandria_bytes::Bytes; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl, + MailboxclientComponent::MailboxClientImpl + }; + use contracts::interfaces::{ + IDomainRoutingIsm, IRoutingIsm, IInterchainSecurityModule, ModuleType, + IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IMailboxDispatcher, IMailboxDispatcherTrait + }; + use contracts::libs::message::{Message, MessageTrait}; + use core::panic_with_felt252; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + + use starknet::{ContractAddress, ClassHash, contract_address_const}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + type Domain = u32; + type Index = u32; + + #[storage] + struct Storage { + modules: LegacyMap, + domains: LegacyMap, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + } + + mod Errors { + pub const LENGTH_MISMATCH: felt252 = 'Length mismatch'; + pub const MODULE_CANNOT_BE_ZERO: felt252 = 'Module cannot be zero'; + pub const DOMAIN_NOT_FOUND: felt252 = 'Domain not found'; + } + + #[constructor] + fn constructor(ref self: ContractState, _owner: ContractAddress, _mailbox: ContractAddress) { + self.ownable.initializer(_owner); + self.mailboxclient.initialize(_mailbox, Option::None, Option::None); + } + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IDomainRoutingIsmImpl of IDomainRoutingIsm { + /// Initializes the contract with domains and ISMs + /// Dev: Callable only by the owner + /// Dev: Panics if domains and ISMs spans length mismatch or if module address is null + /// + /// # Arguments + /// + /// * `_domains` - A span of origin domains + /// * `_modules` - A span of module addresses associated to the domains + fn initialize( + ref self: ContractState, _domains: Span, _modules: Span + ) { + self.ownable.assert_only_owner(); + assert(_domains.len() == _modules.len(), Errors::LENGTH_MISMATCH); + let mut cur_idx = 0; + loop { + if (cur_idx == _domains.len()) { + break (); + } + assert( + *_modules.at(cur_idx) != contract_address_const::<0>(), + Errors::MODULE_CANNOT_BE_ZERO + ); + self._set(*_domains.at(cur_idx), *_modules.at(cur_idx)); + cur_idx += 1; + } + } + + /// Sets the ISM to be used for the specified origin domain + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + /// * - `_module` -The ISM to use to verify messages + fn set(ref self: ContractState, _domain: u32, _module: ContractAddress) { + self.ownable.assert_only_owner(); + assert(_module != contract_address_const::<0>(), Errors::MODULE_CANNOT_BE_ZERO); + self._set(_domain, _module); + } + + /// Removes the specified origin domain + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + fn remove(ref self: ContractState, _domain: u32) { + self.ownable.assert_only_owner(); + self._remove(_domain); + } + + /// Builds a span of domains + /// + /// # Returns + /// + /// Span - a span of the stored domains + fn domains(self: @ContractState) -> Span { + let mut current_domain = self.domains.read(0); + let mut domains = array![]; + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == 0 { + domains.append(current_domain); + break (); + } + domains.append(current_domain); + current_domain = next_domain; + }; + domains.span() + } + + /// Retrieve the module associated to a given origin + /// + /// # Arguments + /// + /// * - `_origin` - The origin domain + /// + /// # Returns + /// + /// ContractAddress - the module contract address + fn module(self: @ContractState, _origin: u32) -> ContractAddress { + let module = self.modules.read(_origin); + if (module != contract_address_const::<0>()) { + module + } else { + IMailboxDispatcher { contract_address: self.mailboxclient.mailbox() } + .get_default_ism() + } + } + } + + #[abi(embed_v0)] + impl IRoutingIsmImpl of IRoutingIsm { + /// Returns the ISM responsible for verifying _message + /// Dev: Can change based on the content of _message + /// + /// # Arguments + /// + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// ContractAddress - The ISM address to use to verify _message + fn route(self: @ContractState, _message: Message) -> ContractAddress { + self.module(_message.origin) + } + } + + #[abi(embed_v0)] + impl IInterchainSecurityModuleImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::ROUTING(starknet::get_contract_address()) + } + + + /// Requires that m-of-n ISMs verify the provided interchain message. + /// Dev: Can change based on the content of _message + /// Dev: Reverts if threshold is not set + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// boolean - wheter the verification succeed or not. + fn verify(self: @ContractState, _metadata: Bytes, _message: Message) -> bool { + let ism_address = self.route(_message.clone()); + let ism_dispatcher = IInterchainSecurityModuleDispatcher { + contract_address: ism_address + }; + ism_dispatcher.verify(_metadata, _message) + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Removes the specified origin domain + /// Dev: Callable only by the admin + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + fn _remove(ref self: ContractState, _domain: u32) { + let domain_index = match self.find_domain_index(_domain) { + Option::Some(index) => index, + Option::None => { + panic_with_felt252(Errors::DOMAIN_NOT_FOUND); + 0 + } + }; + let next_domain = self.domains.read(_domain); + self.modules.write(_domain, contract_address_const::<0>()); + self.domains.write(domain_index, next_domain); + } + + /// Sets the ISM to be used for the specified origin domain + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + /// * - `_module` -The ISM to use to verify messages + fn _set(ref self: ContractState, _domain: u32, _module: ContractAddress) { + match self.find_domain_index(_domain) { + Option::Some(_) => {}, + Option::None => { + let latest_domain = self.find_last_domain(); + self.domains.write(latest_domain, _domain); + } + } + self.modules.write(_domain, _module); + } + + /// Helper: finds the last domain in the storage Legacy Map + /// + /// # Returns + /// + /// u32 - the last domain stored + fn find_last_domain(self: @ContractState) -> u32 { + let mut current_domain = self.domains.read(0); + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == 0 { + break current_domain; + } + current_domain = next_domain; + } + } + + /// Retrieves the index for a given domain + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + /// + /// # Returns + /// + /// Option - the index if found, else None + fn find_domain_index(self: @ContractState, _domain: u32) -> Option { + let mut current_domain = 0; + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == _domain { + break Option::Some(current_domain); + } else if next_domain == 0 { + break Option::None(()); + } + current_domain = next_domain; + } + } + } +} diff --git a/starknet/cairo/crates/contracts/src/isms/routing/domain_routing_ism.cairo b/starknet/cairo/crates/contracts/src/isms/routing/domain_routing_ism.cairo new file mode 100644 index 00000000000..c06133724dc --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/routing/domain_routing_ism.cairo @@ -0,0 +1,282 @@ +#[starknet::contract] +pub mod domain_routing_ism { + use alexandria_bytes::Bytes; + use contracts::interfaces::{ + IDomainRoutingIsm, IRoutingIsm, IInterchainSecurityModule, ModuleType, + IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait + }; + use contracts::libs::message::{Message, MessageTrait}; + use core::panic_with_felt252; + + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + + use starknet::{ContractAddress, contract_address_const, ClassHash}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + type Domain = u32; + type Index = u32; + + #[storage] + struct Storage { + modules: LegacyMap, + domains: LegacyMap, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + mod Errors { + pub const LENGTH_MISMATCH: felt252 = 'Length mismatch'; + pub const ORIGIN_NOT_FOUND: felt252 = 'Origin not found'; + pub const MODULE_CANNOT_BE_ZERO: felt252 = 'Module cannot be zero'; + pub const DOMAIN_NOT_FOUND: felt252 = 'Domain not found'; + } + + #[constructor] + fn constructor(ref self: ContractState, _owner: ContractAddress) { + self.ownable.initializer(_owner); + } + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Dev: allable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IDomainRoutingIsmImpl of IDomainRoutingIsm { + /// Initializes the contract with domains and ISMs + /// Dev: Callable only by the owner + /// Dev: Panics if domains and ISMs spans length mismatch or if module address is null + /// + /// # Arguments + /// + /// * `_domains` - A span of origin domains + /// * `_modules` - A span of module addresses associated to the domains + fn initialize( + ref self: ContractState, _domains: Span, _modules: Span + ) { + self.ownable.assert_only_owner(); + assert(_domains.len() == _modules.len(), Errors::LENGTH_MISMATCH); + let mut cur_idx = 0; + loop { + if (cur_idx == _domains.len()) { + break (); + } + assert( + *_modules.at(cur_idx) != contract_address_const::<0>(), + Errors::MODULE_CANNOT_BE_ZERO + ); + self._set(*_domains.at(cur_idx), *_modules.at(cur_idx)); + cur_idx += 1; + } + } + + /// Sets the ISM to be used for the specified origin domain + /// Dev: Callable only by the admin + /// Dev: Panics if module address is null or domain not found + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + /// * - `_module` -The ISM address to use to verify messages + fn set(ref self: ContractState, _domain: u32, _module: ContractAddress) { + self.ownable.assert_only_owner(); + assert(_module != contract_address_const::<0>(), Errors::MODULE_CANNOT_BE_ZERO); + self._set(_domain, _module); + } + + + /// Removes the specified origin domain + /// Dev: Callable only by the admin + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + fn remove(ref self: ContractState, _domain: u32) { + self.ownable.assert_only_owner(); + self._remove(_domain); + } + + /// Builds a span of domains + /// + /// # Returns + /// + /// Span - a span of stored domains + fn domains(self: @ContractState) -> Span { + let mut current_domain = self.domains.read(0); + let mut domains = array![]; + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == 0 { + domains.append(current_domain); + break (); + } + domains.append(current_domain); + current_domain = next_domain; + }; + domains.span() + } + + /// Retrieves the module associated to a given origin + /// + /// # Arguments + /// + /// * - `_origin` - The origin domain + /// + /// # Returns + /// + /// ContractAddress - the ISM contract address + fn module(self: @ContractState, _origin: u32) -> ContractAddress { + let module = self.modules.read(_origin); + assert(module != contract_address_const::<0>(), Errors::ORIGIN_NOT_FOUND); + module + } + } + + #[abi(embed_v0)] + impl IRoutingIsmImpl of IRoutingIsm { + /// Returns the ISM responsible for verifying _message + /// Dev: Can change based on the content of _message + /// + /// # Arguments + /// + /// * - `_message` - Formatted Hyperlane message (see Message.cairo) + /// + /// # Returns + /// + /// ContractAddress - the ISM contract address + fn route(self: @ContractState, _message: Message) -> ContractAddress { + self.module(_message.origin) + } + } + + #[abi(embed_v0)] + impl IInterchainSecurityModuleImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::ROUTING(starknet::get_contract_address()) + } + + /// Requires that m-of-n ISMs verify the provided interchain message. + /// Dev: Can change based on the content of _message + /// Dev: Reverts if threshold is not set + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see aggregation_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// boolean - wheter the verification succeed or not. + fn verify(self: @ContractState, _metadata: Bytes, _message: Message) -> bool { + let ism_address = self.route(_message.clone()); + let ism_dispatcher = IInterchainSecurityModuleDispatcher { + contract_address: ism_address + }; + ism_dispatcher.verify(_metadata, _message) + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Removes the specified origin domain + /// Dev: Callable only by the admin + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + fn _remove(ref self: ContractState, _domain: u32) { + let domain_index = match self.find_domain_index(_domain) { + Option::Some(index) => index, + Option::None(()) => { + panic_with_felt252(Errors::DOMAIN_NOT_FOUND); + 0 + } + }; + let next_domain = self.domains.read(_domain); + self.modules.write(_domain, contract_address_const::<0>()); + self.domains.write(domain_index, next_domain); + } + + /// Sets the ISM to be used for the specified origin domain + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + /// * - `_module` -The ISM to use to verify messages + fn _set(ref self: ContractState, _domain: u32, _module: ContractAddress) { + match self.find_domain_index(_domain) { + Option::Some(_) => {}, + Option::None(()) => { + let latest_domain = self.find_last_domain(); + self.domains.write(latest_domain, _domain); + } + } + self.modules.write(_domain, _module); + } + + /// Helper: finds the last domain in the storage Legacy Map + /// + /// # Returns + /// + /// u32 - the last domain stored + fn find_last_domain(self: @ContractState) -> u32 { + let mut current_domain = self.domains.read(0); + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == 0 { + break current_domain; + } + current_domain = next_domain; + } + } + + /// Retrieve the index for a given domain + /// + /// # Arguments + /// + /// * - `_domain` - The origin domain + /// + /// # Returns + /// + /// Option - the index if found, else None + fn find_domain_index(self: @ContractState, _domain: u32) -> Option { + let mut current_domain = 0; + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == _domain { + break Option::Some(current_domain); + } else if next_domain == 0 { + break Option::None(()); + } + current_domain = next_domain; + } + } + } +} diff --git a/starknet/cairo/crates/contracts/src/isms/trusted_relayer_ism.cairo b/starknet/cairo/crates/contracts/src/isms/trusted_relayer_ism.cairo new file mode 100644 index 00000000000..01ba937ab5a --- /dev/null +++ b/starknet/cairo/crates/contracts/src/isms/trusted_relayer_ism.cairo @@ -0,0 +1,48 @@ +#[starknet::contract] +pub mod trusted_relayer_ism { + use alexandria_bytes::Bytes; + use contracts::interfaces::{ + ModuleType, IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, IMailboxDispatcher, IMailboxDispatcherTrait, + }; + use contracts::libs::message::{Message, MessageTrait}; + use starknet::ContractAddress; + + #[storage] + struct Storage { + mailbox: ContractAddress, + trusted_relayer: ContractAddress, + } + + #[constructor] + fn constructor( + ref self: ContractState, _mailbox: ContractAddress, _trusted_relayer: ContractAddress + ) { + self.mailbox.write(_mailbox); + self.trusted_relayer.write(_trusted_relayer); + } + #[abi(embed_v0)] + impl IInterchainSecurityModuleImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::NULL(()) + } + + /// Requires that m-of-n ISMs verify the provided interchain message. + /// Dev: Can change based on the content of _message + /// Dev: Reverts if threshold is not set + /// + /// # Arguments + /// + /// * - `_metadata` - encoded metadata (see aggregation_ism_metadata.cairo) + /// * - `_message` - message structure containing relevant information (see message.cairo) + /// + /// # Returns + /// + /// boolean - wheter the verification succeed or not. + fn verify(self: @ContractState, _metadata: Bytes, _message: Message) -> bool { + let mailbox = IMailboxDispatcher { contract_address: self.mailbox.read() }; + let (id, _) = MessageTrait::format_message(_message); + mailbox.processor(id) == self.trusted_relayer.read() + } + } +} diff --git a/starknet/cairo/crates/contracts/src/lib.cairo b/starknet/cairo/crates/contracts/src/lib.cairo new file mode 100644 index 00000000000..e34b5cea211 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/lib.cairo @@ -0,0 +1,48 @@ +pub mod interfaces; +pub mod mailbox; +pub mod libs { + pub mod aggregation_ism_metadata; + pub mod checkpoint_lib; + pub mod enumerable_map; + pub mod math; + pub mod message; + pub mod multisig { + pub mod merkleroot_ism_metadata; + pub mod message_id_ism_metadata; + } +} +pub mod isms { + pub mod noop_ism; + pub mod pausable_ism; + pub mod trusted_relayer_ism; + pub mod multisig { + pub mod merkleroot_multisig_ism; + pub mod messageid_multisig_ism; + pub mod validator_announce; + } + pub mod routing { + pub mod default_fallback_routing_ism; + pub mod domain_routing_ism; + } + pub mod aggregation { + pub mod aggregation; + } +} +pub mod hooks { + pub mod merkle_tree_hook; + pub mod protocol_fee; + pub mod libs { + pub mod standard_hook_metadata; + } +} +pub mod client { + pub mod gas_router_component; + pub mod mailboxclient; + pub mod mailboxclient_component; + pub mod router_component; +} +pub mod utils { + pub mod keccak256; + pub mod store_arrays; + pub mod utils; +} diff --git a/starknet/cairo/crates/contracts/src/libs/aggregation_ism_metadata.cairo b/starknet/cairo/crates/contracts/src/libs/aggregation_ism_metadata.cairo new file mode 100644 index 00000000000..eeedd98608b --- /dev/null +++ b/starknet/cairo/crates/contracts/src/libs/aggregation_ism_metadata.cairo @@ -0,0 +1,135 @@ +pub mod aggregation_ism_metadata { + use alexandria_bytes::{Bytes, BytesTrait}; + use core::result::{ResultTrait, Result}; + + pub trait AggregationIsmMetadata { + fn metadata_at(_metadata: Bytes, _index: u8) -> Bytes; + fn has_metadata(_metadata: Bytes, _index: u8) -> bool; + } + /// * Format of metadata: + /// * + /// * [????:????] Metadata start/end uint32 ranges, packed as uint64 + /// * [????:????] ISM metadata, packed encoding + /// * + const RANGE_SIZE: u8 = 4; + const BYTES_PER_ELEMENT: u8 = 16; + + impl AggregationIsmMetadataImpl of AggregationIsmMetadata { + /// Returns the metadata provided for the ISM at `_index` + /// Dev: Callers must ensure _index is less than the number of metadatas provided + /// Dev: Callers must ensure `hasMetadata(_metadata, _index)` + /// + /// # Arguments + /// + /// * - `_metadata` -Encoded Aggregation ISM metadata + /// * - `_index` - The index of the ISM to check for metadata for + /// + /// # Returns + /// + /// Bytes - The metadata provided for the ISM at `_index` + fn metadata_at(_metadata: Bytes, _index: u8) -> Bytes { + let (mut start, end) = match metadata_range(_metadata.clone(), _index) { + Result::Ok((start, end)) => (start, end), + Result::Err(_) => (0, 0) + }; + let mut bytes_array = BytesTrait::new(496, array![]); + loop { + if ((end - start) <= 16) { + let (_, res) = _metadata.read_u128_packed(start, end - start); + bytes_array.append_u128(res); + break (); + } + let (_, res) = _metadata.read_u128_packed(start, BYTES_PER_ELEMENT.into()); + bytes_array.append_u128(res); + start = start + BYTES_PER_ELEMENT.into() + }; + bytes_array + } + /// Returns whether or not metadata was provided for the ISM at _index + /// Dev: Callers must ensure _index is less than the number of metadatas provided + /// + /// # Arguments + /// + /// * - `_metadata` -Encoded Aggregation ISM metadata + /// * - `_index` - The index of the ISM to check for metadata for + /// + /// # Returns + /// + /// boolean - Whether or not metadata was provided for the ISM at `_index` + fn has_metadata(_metadata: Bytes, _index: u8) -> bool { + match metadata_range(_metadata, _index) { + Result::Ok((start, _)) => start > 0, + Result::Err(_) => false + } + } + } + + /// Returns the range of the metadata provided for the ISM at _index + /// Dev: Callers must ensure _index is less than the number of metadatas provided + /// + /// # Arguments + /// + /// * - `_metadata` -Encoded Aggregation ISM metadata + /// * - `_index` - The index of the ISM to check for metadata for + /// + /// # Returns + /// + /// Result - Result on whether or not metadata was provided for the ISM at `_index` + fn metadata_range(_metadata: Bytes, _index: u8) -> Result<(u32, u32), u8> { + let start = _index.into() * RANGE_SIZE * 2; + let mid = start + RANGE_SIZE; + let (_, mid_metadata) = _metadata.read_u32(mid.into()); + let (_, start_metadata) = _metadata.read_u32(start.into()); + Result::Ok((start_metadata, mid_metadata)) + } +} + + +#[cfg(test)] +mod test { + use alexandria_bytes::{Bytes, BytesTrait}; + use super::aggregation_ism_metadata::AggregationIsmMetadata; + + #[test] + fn test_aggregation_ism_metadata() { + let encoded_metadata = BytesTrait::new( + 64, + array![ + 0x0000001800000024000000240000002C, + 0x0000002C00000034AAAAAAAAAAAAAAAA, + 0xBBBBCCCCDDDDDDDDEEEEEEEEFFFFFFFF, + 0x00000000000000000000000000000000 + ] + ); + let mut expected_result = array![ + 0xAAAAAAAAAAAAAAAABBBBCCCC_u256, 0xDDDDDDDDEEEEEEEE_u256, 0xFFFFFFFF00000000_u256 + ]; + let mut cur_idx = 0; + loop { + if (cur_idx == 3) { + break (); + } + let result = AggregationIsmMetadata::metadata_at(encoded_metadata.clone(), cur_idx); + assert( + *BytesTrait::data(result.clone())[0] == *expected_result.at(cur_idx.into()).low, + 'Agg metadata extract failed' + ); + cur_idx += 1; + }; + } + + #[test] + fn test_aggregation_ism_has_metadata() { + let encoded_metadata = BytesTrait::new( + 64, + array![ + 0x00000018000000240000000000000000, + 0x0000002C00000034AAAAAAAAAAAAAAAA, + 0xBBBBCCCCDDDDDDDDEEEEEEEEFFFFFFFF, + 0x00000000000000000000000000000000 + ] + ); + assert_eq!(AggregationIsmMetadata::has_metadata(encoded_metadata.clone(), 0), true); + assert_eq!(AggregationIsmMetadata::has_metadata(encoded_metadata.clone(), 1), false); + } +} diff --git a/starknet/cairo/crates/contracts/src/libs/checkpoint_lib.cairo b/starknet/cairo/crates/contracts/src/libs/checkpoint_lib.cairo new file mode 100644 index 00000000000..ef1adb2caf9 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/libs/checkpoint_lib.cairo @@ -0,0 +1,74 @@ +pub mod checkpoint_lib { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use contracts::libs::message::Message; + use contracts::utils::keccak256::{ + reverse_endianness, compute_keccak, ByteData, u64_word_size, u256_word_size, HASH_SIZE, + to_eth_signature + }; + + + pub trait CheckpointLib { + fn digest( + _origin: u32, + _origin_merkle_tree_hook: u256, + _checkpoint_root: u256, + _checkpoint_index: u32, + _message_id: u256 + ) -> u256; + fn domain_hash(_origin: u32, _origin_merkle_tree_hook: u256) -> u256; + } + const HYPERLANE: felt252 = 'HYPERLANE'; + pub const HYPERLANE_ANNOUNCEMENT: felt252 = 'HYPERLANE_ANNOUNCEMENT'; + + impl CheckpointLibImpl of CheckpointLib { + /// Returns the digest validators are expected to sign when signing checkpoints. + /// + /// # Arguments + /// + /// * - `_origin` - The origin domain of the checkpoint. + /// * - `_origin_merkle_tree_hook` - The address of the origin merkle tree hook + /// * - `_checkpoint_root` - The root of the checkpoint. + /// * - `_checkpoint_index` - The index of the checkpoint. + /// * - `_message_id` - The message ID of the checkpoint. + /// + /// # Returns + /// + /// u256 - the digest + fn digest( + _origin: u32, + _origin_merkle_tree_hook: u256, + _checkpoint_root: u256, + _checkpoint_index: u32, + _message_id: u256 + ) -> u256 { + let domain_hash = CheckpointLibImpl::domain_hash(_origin, _origin_merkle_tree_hook); + let mut input: Array = array![ + ByteData { value: domain_hash.into(), size: HASH_SIZE }, + ByteData { value: _checkpoint_root.into(), size: 32 }, + ByteData { value: _checkpoint_index.into(), size: 4 }, + ByteData { value: _message_id.into(), size: HASH_SIZE }, + ]; + to_eth_signature(reverse_endianness(compute_keccak(input.span()))) + } + + /// Returns the domain hash validators are expected to use when signing checkpoints. + /// + /// # Arguments + /// + /// * - `_origin` - The origin domain of the checkpoint. + /// * - `_origin_merkle_tree_hook` - The address of the origin merkle tree hook + /// + /// # Returns + /// + /// u256 - The domain hash. + fn domain_hash(_origin: u32, _origin_merkle_tree_hook: u256) -> u256 { + let mut input: Array = array![ + ByteData { value: _origin.into(), size: 4 }, + ByteData { value: _origin_merkle_tree_hook.into(), size: 32 }, + ByteData { value: HYPERLANE.into(), size: 9 } + ]; + reverse_endianness(compute_keccak(input.span())) + } + } +} + diff --git a/starknet/cairo/crates/contracts/src/libs/enumerable_map.cairo b/starknet/cairo/crates/contracts/src/libs/enumerable_map.cairo new file mode 100644 index 00000000000..c5e4c71ad17 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/libs/enumerable_map.cairo @@ -0,0 +1,486 @@ +use core::hash::{HashStateTrait}; +use core::num::traits::Zero; +use core::pedersen::PedersenTrait; +use core::poseidon::poseidon_hash_span; +use starknet::storage_access::{ + StorageBaseAddress, storage_address_from_base, storage_base_address_from_felt252 +}; +use starknet::{Store, SyscallResultTrait, SyscallResult}; +pub mod Err { + pub const NOT_IMPLEMENTED: felt252 = 'Not implemented!'; + pub const INDEX_OUT_OF_BOUNDS: felt252 = 'Index out of bounds!'; + pub const KEY_DOES_NOT_EXIST: felt252 = 'Key does not exist!'; +} + +// Enumerable map +// struct EnumerableMap { +// values: Map +// keys: List +// positions: Map +// } +#[derive(Copy, Drop)] +pub struct EnumerableMap { + address_domain: u32, + base: StorageBaseAddress +} + +/// A storage interface for an `EnumerableMap` that allows reading and writing +/// to a system store. This trait defines how to read, write, and handle storage +/// for `EnumerableMap` structures. It also provides methods to handle reading +/// and writing at specific offsets in storage. +/// +/// # Parameters: +/// - `K`: The key type. +/// - `V`: The value type. +/// +/// # Example: +/// ```rust +/// let map = EnumerableMapStore::::read(domain, address); +/// ``` +pub impl EnumerableMapStore< + K, V, +Store, +Drop, +Store, +Drop +> of Store> { + /// Reads the `EnumerableMap` from storage at the given `base` address + /// within the specified `address_domain`. + /// + /// # Arguments: + /// - `address_domain`: The domain in which the map is stored. + /// - `base`: The base storage address for the map. + /// + /// # Returns: + /// - `SyscallResult>`: The map read from storage. + #[inline(always)] + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { + SyscallResult::Ok(EnumerableMap:: { address_domain, base }) + } + + /// Attempts to write the `EnumerableMap` to storage. Currently not implemented. + /// + /// # Arguments: + /// - `address_domain`: The domain in which to write the map. + /// - `base`: The base storage address for the map. + /// - `value`: The `EnumerableMap` to write. + /// + /// # Returns: + /// - `SyscallResult<()>`: Error indicating not implemented. + #[inline(always)] + fn write( + address_domain: u32, base: StorageBaseAddress, value: EnumerableMap + ) -> SyscallResult<()> { + SyscallResult::Err(array![Err::NOT_IMPLEMENTED]) + } + + /// Attempts to read the `EnumerableMap` from storage at a specific offset. + /// + /// # Arguments: + /// - `address_domain`: The domain in which the map is stored. + /// - `base`: The base storage address for the map. + /// - `offset`: The offset in storage where the map is read from. + /// + /// # Returns: + /// - `SyscallResult>`: Error indicating not implemented. + #[inline(always)] + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8 + ) -> SyscallResult> { + SyscallResult::Err(array![Err::NOT_IMPLEMENTED]) + } + + + /// Attempts to write the `EnumerableMap` to storage at a specific offset. + /// + /// # Arguments: + /// - `address_domain`: The domain in which to write the map. + /// - `base`: The base storage address for the map. + /// - `offset`: The offset in storage where the map is written to. + /// - `value`: The `EnumerableMap` to write. + /// + /// # Returns: + /// - `SyscallResult<()>`: Error indicating not implemented. + #[inline(always)] + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8, value: EnumerableMap + ) -> SyscallResult<()> { + SyscallResult::Err(array![Err::NOT_IMPLEMENTED]) + } + + + /// Returns the size of the `EnumerableMap` in bytes. Currently set to `0`. + /// + /// # Returns: + /// - `u8`: The size of the map in bytes. + #[inline(always)] + fn size() -> u8 { + // 0 was selected because the read method doesn't actually read from storage + 0_u8 + } +} + +/// Trait defining basic operations for a key-value map where the keys are stored +/// in an enumerable way. This provides functionality to get, set, check for keys, +/// and retrieve values. +/// +/// # Parameters: +/// - `K`: The key type. +/// - `V`: The value type. +/// +/// # Example: +/// ```rust +/// let value = map.get(key); +/// map.set(key, value); +/// ``` +pub trait EnumerableMapTrait { + /// Retrieves the value associated with the specified `key`. + /// + /// # Arguments: + /// - `key`: The key for which to retrieve the value. + /// + /// # Returns: + /// - `V`: The value associated with the `key` + fn get(self: @EnumerableMap, key: K) -> V; + + /// Associates the specified `key` with the provided `val` and adds it to + /// the map if it does not already exist. + /// + /// # Arguments: + /// - `key`: The key to associate with the value. + /// - `val`: The value to associate with the key. + fn set(ref self: EnumerableMap, key: K, val: V) -> (); + + /// Returns the number of key-value pairs stored in the map. + /// + /// # Returns: + /// - `u32`: The number of elements in the map. + fn len(self: @EnumerableMap) -> u32; + + /// Checks if the map contains the specified `key`. + /// + /// # Arguments: + /// - `key`: The key to check. + /// + /// # Returns: + /// - `bool`: `true` if the key exists, `false` otherwise. + fn contains(self: @EnumerableMap, key: K) -> bool; + + /// Removes the key-value pair associated with the specified `key` from the map. + /// + /// # Arguments: + /// - `key`: The key to remove. + /// + /// # Returns: + /// - `bool`: `true` if the removal was successful, `false` if the key does not exist. + fn remove(ref self: EnumerableMap, key: K) -> bool; + + /// Retrieves the key-value pair stored at the specified `index` in the map. + /// + /// # Arguments: + /// - `index`: The index at which to retrieve the key-value pair. + /// + /// # Returns: + /// - `(K, V)`: The key-value pair at the specified index. + fn at(self: @EnumerableMap, index: u32) -> (K, V); + + /// Returns an array of all keys stored in the map. + /// + /// # Returns: + /// - `Array`: An array of all keys in the map. + fn keys(self: @EnumerableMap) -> Array; +} + +pub impl EnumerableMapImpl< + K, + V, + +Drop, + +Drop, + +Store, + +Store, + +Copy, + +Copy, + +Zero, + +Zero, + +Serde, +> of EnumerableMapTrait { + fn get(self: @EnumerableMap, key: K) -> V { + let value = EnumerableMapInternalTrait::::values_mapping_read(self, key); + assert(value.is_non_zero() || self.contains(key), Err::KEY_DOES_NOT_EXIST); + value + } + + fn set(ref self: EnumerableMap, key: K, val: V) { + let is_exists = self.contains(key); + + EnumerableMapInternalTrait::::values_mapping_write(ref self, key, val); + if !is_exists { + // appends 'key' to array and updates 'position' mapping + EnumerableMapInternalTrait::::array_append(ref self, key); + } + } + + fn len(self: @EnumerableMap) -> u32 { + Store::::read(*self.address_domain, *self.base).unwrap_syscall() + } + + fn contains(self: @EnumerableMap, key: K) -> bool { + EnumerableMapInternalTrait::::positions_mapping_read(self, key) != 0 + } + + fn remove(ref self: EnumerableMap, key: K) -> bool { + if !self.contains(key) { + return false; + } + let index = EnumerableMapInternalImpl::::positions_mapping_read(@self, key) - 1; + // Deletes `key` from 'values' mapping + EnumerableMapInternalTrait::::values_mapping_write(ref self, key, Zero::::zero()); + // Deletes `key`` from 'array' and 'positions' mapping + EnumerableMapInternalTrait::::array_remove(ref self, index) + } + + fn at(self: @EnumerableMap, index: u32) -> (K, V) { + assert(index < self.len(), Err::INDEX_OUT_OF_BOUNDS); + let key = EnumerableMapInternalTrait::::array_read(self, index); + let val = EnumerableMapInternalTrait::::values_mapping_read(self, key); + (key, val) + } + + fn keys(self: @EnumerableMap) -> Array { + let mut i = 0; + let len = self.len(); + let mut keys = array![]; + while i < len { + let key = EnumerableMapInternalTrait::::array_read(self, i); + keys.append(key); + i += 1; + }; + keys + } +} + +/// Internal trait for managing the internal structures of an `EnumerableMap`. +/// This trait handles reading and writing key-value pairs and their positions, +/// as well as managing the array of keys for enumeration. +/// +/// # Parameters: +/// - `K`: The key type. +/// - `V`: The value type. +/// +/// # Example: +/// ```rust +/// EnumerableMapInternalTrait::::values_mapping_write(map, key, value); +/// ``` +trait EnumerableMapInternalTrait { + /// Writes the specified `val` associated with the `key` into the `values` mapping. + /// + /// # Arguments: + /// - `key`: The key to associate with the value. + /// - `val`: The value to store. + fn values_mapping_write(ref self: EnumerableMap, key: K, val: V); + + /// Reads the value associated with the `key` from the `values` mapping. + /// + /// # Arguments: + /// - `key`: The key for which to read the value. + /// + /// # Returns: + /// - `V`: The value associated with the `key`. + fn values_mapping_read(self: @EnumerableMap, key: K) -> V; + + /// Writes the position of the `key` in the `positions` mapping. + /// + /// # Arguments: + /// - `key`: The key for which to store the position. + /// - `val`: The position to store. + fn positions_mapping_write(ref self: EnumerableMap, key: K, val: u32); + + /// Reads the position of the `key` from the `positions` mapping. + /// + /// # Arguments: + /// - `key`: The key for which to retrieve the position. + /// + /// # Returns: + /// - `u32`: The position associated with the `key`. + fn positions_mapping_read(self: @EnumerableMap, key: K) -> u32; + + /// Updates the length of the key array in storage. + /// + /// # Arguments: + /// - `new_len`: The new length of the array. + fn update_array_len(ref self: EnumerableMap, new_len: u32); + + /// Appends the `key` to the array of keys. + /// + /// # Arguments: + /// - `key`: The key to append to the array. + fn array_append(ref self: EnumerableMap, key: K); + + /// Removes the key-value pair at the specified `index` from the array. + /// + /// # Arguments: + /// - `index`: The index of the key-value pair to remove. + /// + /// # Returns: + /// - `bool`: `true` if the removal was successful, `false` otherwise. + fn array_remove(ref self: EnumerableMap, index: u32) -> bool; + + /// Reads the key at the specified `index` from the array of keys. + /// + /// # Arguments: + /// - `index`: The index at which to read the key. + /// + /// # Returns: + /// - `K`: The key at the specified index. + fn array_read(self: @EnumerableMap, index: u32) -> K; + + /// Writes the specified `key` at the given `index` in the array of keys. + /// + /// # Arguments: + /// - `index`: The index at which to write the key. + /// - `val`: The key to write. + fn array_write(ref self: EnumerableMap, index: u32, val: K); +} + + +impl EnumerableMapInternalImpl< + K, + V, + +Drop, + +Drop, + +Store, + +Store, + +Copy, + +Copy, + +Zero, + +Zero, + +Serde, +> of EnumerableMapInternalTrait { + fn values_mapping_write(ref self: EnumerableMap, key: K, val: V) { + let storage_base_felt: felt252 = storage_address_from_base(self.base).into(); + let mut storage_address_val = PedersenTrait::new(storage_base_felt).update('values'); + let mut serialized_key: Array = array![]; + key.serialize(ref serialized_key); + let mut i = 0; + let len = serialized_key.len(); + while i < len { + storage_address_val = storage_address_val.update(*serialized_key.at(i)); + i += 1; + }; + let storage_address_val_felt = storage_address_val.finalize(); + Store::< + V + >::write( + self.address_domain, storage_base_address_from_felt252(storage_address_val_felt), val + ) + .unwrap_syscall(); + } + + fn values_mapping_read(self: @EnumerableMap, key: K) -> V { + let storage_base_felt: felt252 = storage_address_from_base(*self.base).into(); + let mut storage_address_val = PedersenTrait::new(storage_base_felt).update('values'); + let mut serialized_key: Array = array![]; + key.serialize(ref serialized_key); + let mut i = 0; + let len = serialized_key.len(); + while i < len { + storage_address_val = storage_address_val.update(*serialized_key.at(i)); + i += 1; + }; + let storage_address_val_felt = storage_address_val.finalize(); + Store::< + V + >::read(*self.address_domain, storage_base_address_from_felt252(storage_address_val_felt)) + .unwrap_syscall() + } + + fn positions_mapping_write(ref self: EnumerableMap, key: K, val: u32) { + let storage_base_felt: felt252 = storage_address_from_base(self.base).into(); + let mut storage_address_val = PedersenTrait::new(storage_base_felt).update('positions'); + let mut serialized_key: Array = array![]; + key.serialize(ref serialized_key); + let mut i = 0; + let len = serialized_key.len(); + while i < len { + storage_address_val = storage_address_val.update(*serialized_key.at(i)); + i += 1; + }; + let storage_address_val_felt = storage_address_val.finalize(); + Store::< + u32 + >::write( + self.address_domain, storage_base_address_from_felt252(storage_address_val_felt), val + ) + .unwrap_syscall(); + } + + fn positions_mapping_read(self: @EnumerableMap, key: K) -> u32 { + let storage_base_felt: felt252 = storage_address_from_base(*self.base).into(); + let mut storage_address_val = PedersenTrait::new(storage_base_felt).update('positions'); + let mut serialized_key: Array = array![]; + key.serialize(ref serialized_key); + let mut i = 0; + let len = serialized_key.len(); + while i < len { + storage_address_val = storage_address_val.update(*serialized_key.at(i)); + i += 1; + }; + let storage_address_val_felt = storage_address_val.finalize(); + Store::< + u32 + >::read(*self.address_domain, storage_base_address_from_felt252(storage_address_val_felt)) + .unwrap_syscall() + } + + fn update_array_len(ref self: EnumerableMap, new_len: u32) { + Store::::write(self.address_domain, self.base, new_len).unwrap_syscall(); + } + + fn array_append(ref self: EnumerableMap, key: K) { + let len = Store::::read(self.address_domain, self.base).unwrap_syscall(); + self.array_write(len, key); + self.update_array_len(len + 1); + self.positions_mapping_write(key, len + 1); + } + + fn array_remove(ref self: EnumerableMap, index: u32) -> bool { + let len = Store::::read(self.address_domain, self.base).unwrap_syscall(); + if index >= len { + return false; + } + let element = self.array_read(index); + // Remove `element` from `positions` mapping + self.positions_mapping_write(element, 0); + // if element is not the last element, swap with last element and clear the last index + if index != len - 1 { + let last_element = self.array_read(len - 1); + // Updates the position of `last_element` in 'positions' mapping + self.positions_mapping_write(last_element, index + 1); + // Moves last element into 'index' and remove the last element + self.array_write(index, last_element); + // Deletes the last element from array + self.array_write(len - 1, Zero::::zero()); + } + // Decrease the array length + self.update_array_len(len - 1); + true + } + + fn array_read(self: @EnumerableMap, index: u32) -> K { + let storage_base_felt: felt252 = storage_address_from_base(*self.base).into(); + let storage_address_felt = poseidon_hash_span( + array![storage_base_felt, index.into()].span() + ); + Store::< + K + >::read(*self.address_domain, storage_base_address_from_felt252(storage_address_felt)) + .unwrap_syscall() + } + + fn array_write(ref self: EnumerableMap, index: u32, val: K) { + let storage_base_felt: felt252 = storage_address_from_base(self.base).into(); + let storage_address_felt = poseidon_hash_span( + array![storage_base_felt, index.into()].span() + ); + Store::< + K + >::write(self.address_domain, storage_base_address_from_felt252(storage_address_felt), val) + .unwrap_syscall(); + } +} diff --git a/starknet/cairo/crates/contracts/src/libs/math.cairo b/starknet/cairo/crates/contracts/src/libs/math.cairo new file mode 100644 index 00000000000..8065be54475 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/libs/math.cairo @@ -0,0 +1,25 @@ +use core::integer::{u256_wide_mul, u512_safe_div_rem_by_u256}; +use core::option::OptionTrait; +use core::traits::TryInto; +/// Multiplies two `u256` values and then divides by a third `u256` value. +/// +/// # Parameters +/// +/// - `a`: The first multiplicand, a `u256` value. +/// - `b`: The second multiplicand, a `u256` value. +/// - `c`: The divisor, a `u256` value. Must not be zero. +/// +/// # Returns +/// +/// - The result of the operation `(a * b) / c`, as a `u256` value. +/// +/// # Panics +/// +/// - Panics if `c` is zero, as division by zero is undefined. +pub fn mul_div(a: u256, b: u256, c: u256) -> u256 { + if c == 0 { + panic!("mul_div division by zero"); + } + let (q, _) = u512_safe_div_rem_by_u256(u256_wide_mul(a, b), c.try_into().unwrap()); + q.try_into().expect('mul_div result gt u256') +} diff --git a/starknet/cairo/crates/contracts/src/libs/message.cairo b/starknet/cairo/crates/contracts/src/libs/message.cairo new file mode 100644 index 00000000000..ec5de90bd45 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/libs/message.cairo @@ -0,0 +1,176 @@ +use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; +use alexandria_math::BitShift; +use contracts::utils::keccak256::{ + reverse_endianness, compute_keccak, ByteData, u256_word_size, u64_word_size, ADDRESS_SIZE, + u128_mask, +}; +use starknet::{ContractAddress, contract_address_const}; + +pub const HYPERLANE_VERSION: u8 = 3; + + +#[derive(Serde, starknet::Store, Drop, Clone)] +pub struct Message { + pub version: u8, + pub nonce: u32, + pub origin: u32, + pub sender: u256, + pub destination: u32, + pub recipient: u256, + pub body: Bytes, +} + + +#[generate_trait] +pub impl MessageImpl of MessageTrait { + /// Generate a default empty message + /// + /// # Returns + /// + /// * An empty message structure + fn default() -> Message { + Message { + version: HYPERLANE_VERSION, + nonce: 0_u32, + origin: 0_u32, + sender: 0, + destination: 0_u32, + recipient: 0, + body: BytesTrait::new_empty(), + } + } + + /// Format an input message, using + /// + /// # Arguments + /// + /// * `_message` - Message to hash + /// + /// # Returns + /// + /// * u256 representing the hash of the message + fn format_message(_message: Message) -> (u256, Message) { + let mut input: Array = array![ + ByteData { value: _message.version.into(), size: 1 }, + ByteData { value: _message.nonce.into(), size: 4 }, + ByteData { value: _message.origin.into(), size: 4 }, + ByteData { value: _message.sender, size: 32 }, + ByteData { value: _message.destination.into(), size: 4 }, + ByteData { value: _message.recipient, size: 32 }, + ]; + let message_data = _message.clone().body.data(); + let finalized_input = MessageImpl::append_span_u128_to_byte_data( + input, message_data.span(), _message.clone().body.size() + ); + (reverse_endianness(compute_keccak(finalized_input)), _message) + } + + fn append_span_u128_to_byte_data( + mut _input: Array, _to_append: Span, size: u32 + ) -> Span { + let mut cur_idx = 0; + let range = size / 16; + loop { + if (cur_idx == range) { + if (size % 16 == 0) { + break; + } else { + let remaining_size = size - cur_idx * 16; + let mask = u128_mask(remaining_size.try_into().unwrap()); + _input + .append( + ByteData { + value: (BitShift::shr( + *_to_append.at(cur_idx), ((16 - remaining_size) * 8).into() + ) + & mask) + .into(), + size: remaining_size + } + ); + break; + } + } + _input.append(ByteData { value: (*_to_append.at(cur_idx)).into(), size: 16 }); + cur_idx += 1; + }; + _input.span() + } +} + + +#[cfg(test)] +mod tests { + use super::{MessageImpl, ByteData}; + + #[test] + fn test_append_u128_to_byte_array() { + let input: Array = array![ + 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + 0xcc000000000000000000000000000000 + ]; + let output_array = MessageImpl::append_span_u128_to_byte_data(array![], input.span(), 33); + assert_eq!( + output_array, + array![ + ByteData { value: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, size: 16 }, + ByteData { value: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, size: 16 }, + ByteData { value: 0xcc, size: 1 } + ] + .span() + ); + + let input: Array = array![ + 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + 0xcccccccccccccccccccccccccccccccc + ]; + let output_array = MessageImpl::append_span_u128_to_byte_data(array![], input.span(), 48); + assert_eq!( + output_array, + array![ + ByteData { value: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, size: 16 }, + ByteData { value: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, size: 16 }, + ByteData { value: 0xcccccccccccccccccccccccccccccccc, size: 16 } + ] + .span() + ); + + let input: Array = array![ + 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + 0xcccccccccccccccccccccccccccccc00 + ]; + let output_array = MessageImpl::append_span_u128_to_byte_data(array![], input.span(), 47); + assert_eq!( + output_array, + array![ + ByteData { value: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, size: 16 }, + ByteData { value: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, size: 16 }, + ByteData { value: 0xcccccccccccccccccccccccccccccc, size: 15 } + ] + .span() + ); + + let input: Array = array![ + 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + 0xcccccccccccccccccccccccccc000000 + ]; + let output_array = MessageImpl::append_span_u128_to_byte_data(array![], input.span(), 45); + assert_eq!( + output_array, + array![ + ByteData { value: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, size: 16 }, + ByteData { value: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, size: 16 }, + ByteData { value: 0xcccccccccccccccccccccccccc, size: 13 } + ] + .span() + ); + + let input: Array = array![0x12345678000000000000000000000000]; + let output_array = MessageImpl::append_span_u128_to_byte_data(array![], input.span(), 4); + assert_eq!(output_array, array![ByteData { value: 0x12345678, size: 4 }].span()); + } +} diff --git a/starknet/cairo/crates/contracts/src/libs/multisig/merkleroot_ism_metadata.cairo b/starknet/cairo/crates/contracts/src/libs/multisig/merkleroot_ism_metadata.cairo new file mode 100644 index 00000000000..083bc554740 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/libs/multisig/merkleroot_ism_metadata.cairo @@ -0,0 +1,130 @@ +pub mod merkleroot_ism_metadata { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + + pub trait MerkleRootIsmMetadata { + fn origin_merkle_tree_hook(_metadata: Bytes) -> u256; + fn message_index(_metadata: Bytes) -> u32; + fn signed_index(_metadata: Bytes) -> u32; + fn signed_message_id(_metadata: Bytes) -> u256; + fn proof(_metadata: Bytes) -> Span; + fn signature_at(_metadata: Bytes, _index: u32) -> (u8, u256, u256); + } + + /// + /// * Format of metadata: + /// * [ 0: 32] Origin merkle tree address + /// * [ 32: 36] Index of message ID in merkle tree + /// * [ 36: 68] Signed checkpoint message ID + /// * [ 68:1092] Merkle proof + /// * [1092:1096] Signed checkpoint index (computed from proof and index) + /// * [1096:????] Validator signatures (length := threshold * 65) + /// + pub const ORIGIN_MERKLE_TREE_OFFSET: u32 = 0; + pub const MESSAGE_INDEX_OFFSET: u32 = 32; + pub const MESSAGE_ID_OFFSET: u32 = 36; + pub const MERKLE_PROOF_OFFSET: u32 = 68; + pub const MERKLE_PROOF_ITERATION: u32 = 32; + pub const MERKLE_PROOF_SIZE: u32 = 32; + pub const MERKLE_PROOF_LENGTH: u32 = MERKLE_PROOF_SIZE * MERKLE_PROOF_ITERATION; + pub const SIGNED_INDEX_OFFSET: u32 = 1092; + pub const SIGNATURES_OFFSET: u32 = 1096; + pub const SIGNATURE_LENGTH: u32 = 65; + impl MerkleRootIsmMetadataImpl of MerkleRootIsmMetadata { + /// Returns the origin merkle tree hook of the signed checkpoint + /// + /// # Arguments + /// + /// * - `_metadata` - encoded multisig ISM metadata + /// + /// # Returns + /// + /// u256 - Origin merkle tree hook of the signed checkpoint + fn origin_merkle_tree_hook(_metadata: Bytes) -> u256 { + let (_, felt) = _metadata.read_u256(ORIGIN_MERKLE_TREE_OFFSET); + felt + } + + /// Returns the index of the message being proven. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded multisig ISM metadata + /// + /// # Returns + /// + /// u32 - Index of the target message in the merkle tree. + fn message_index(_metadata: Bytes) -> u32 { + let (_, felt) = _metadata.read_u32(MESSAGE_INDEX_OFFSET); + felt + } + + /// Returns the index of the signed checkpoint. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded multisig ISM metadata + /// + /// # Returns + /// + /// u32 - Index of the signed checkpoint + fn signed_index(_metadata: Bytes) -> u32 { + let (_, felt) = _metadata.read_u32(SIGNED_INDEX_OFFSET); + felt + } + /// Returns the message ID of the signed checkpoint. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded multisig ISM metadata + /// + /// # Returns + /// + /// u256 - Message ID of the signed checkpoint + fn signed_message_id(_metadata: Bytes) -> u256 { + let (_, felt) = _metadata.read_u256(MESSAGE_ID_OFFSET); + felt + } + /// Returns the merkle proof branch of the message. + /// + /// # Arguments + /// + /// * - `_metadata` - encoded multisig ISM metadata + /// + /// # Returns + /// + /// Span - Merkle proof branch of the message. + fn proof(_metadata: Bytes) -> Span { + let mut bytes_arr = array![]; + let mut cur_idx = 0; + loop { + if (cur_idx == MERKLE_PROOF_ITERATION) { + break (); + } + let (_, res) = _metadata + .read_u256(MERKLE_PROOF_OFFSET + cur_idx * MERKLE_PROOF_SIZE); + bytes_arr.append(res); + cur_idx += 1; + }; + bytes_arr.span() + } + /// Returns the validator ECDSA signature at `_index`. + /// Dev: Assumes signatures are sorted by validator + /// Dev: Assumes `_metadata` encodes `threshold` signatures. + /// Dev: Assumes `_index` is less than `threshold` + /// # Arguments + /// + /// * - `_metadata` - encoded multisig ISM metadata + /// * - `_index` - The index of the signature to return. + /// + /// # Returns + /// + /// (u8, u256, u256) - The validator ECDSA signature at `_index`. + fn signature_at(_metadata: Bytes, _index: u32) -> (u8, u256, u256) { + // the first signer index is 0 + let (index_r, r) = _metadata.read_u256(SIGNATURES_OFFSET + SIGNATURE_LENGTH * _index); + let (index_s, s) = _metadata.read_u256(index_r); + let (_, v) = _metadata.read_u8(index_s); + (v, r, s) + } + } +} diff --git a/starknet/cairo/crates/contracts/src/libs/multisig/message_id_ism_metadata.cairo b/starknet/cairo/crates/contracts/src/libs/multisig/message_id_ism_metadata.cairo new file mode 100644 index 00000000000..1bce41f06fe --- /dev/null +++ b/starknet/cairo/crates/contracts/src/libs/multisig/message_id_ism_metadata.cairo @@ -0,0 +1,87 @@ +pub mod message_id_ism_metadata { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + + + pub trait MessageIdIsmMetadata { + fn origin_merkle_tree_hook(_metadata: Bytes) -> u256; + fn root(_metadata: Bytes) -> u256; + fn index(_metadata: Bytes) -> u32; + fn signature_at(_metadata: Bytes, _index: u32) -> (u8, u256, u256); + } + + + /// * Format of metadata: + /// * [ 0: 32] Origin merkle tree address + /// * [ 32: 64] Signed checkpoint root + /// * [ 64: 68] Signed checkpoint index + /// * [ 68:????] Validator signatures (length := threshold * 65) + + pub const ORIGIN_MERKLE_TREE_HOOK_OFFSET: u32 = 0; + pub const ROOT_OFFSET: u32 = 32; + pub const INDEX_OFFSET: u32 = 64; + pub const SIGNATURE_OFFSET: u32 = 68; + pub const SIGNATURE_LENGTH: u32 = 65; + impl MessagIdIsmMetadataImpl of MessageIdIsmMetadata { + /// Returns the origin merkle tree hook of the signed checkpoint + /// + /// # Arguments + /// + /// * - `_metadata` -Encoded MultisigISM metadata + /// + /// # Returns + /// + /// u256 - Origin merkle tree hook of the signed checkpoint + fn origin_merkle_tree_hook(_metadata: Bytes) -> u256 { + let (_, felt) = _metadata.read_u256(ORIGIN_MERKLE_TREE_HOOK_OFFSET); + felt + } + + /// Returns the merkle root of the signed checkpoint. + /// + /// # Arguments + /// + /// * - `_metadata` -Encoded MultisigISM metadata + /// + /// # Returns + /// + /// u256 - Merkle root of the signed checkpoint + fn root(_metadata: Bytes) -> u256 { + let (_, felt) = _metadata.read_u256(ROOT_OFFSET); + felt + } + /// Returns the merkle index of the signed checkpoint. + /// + /// # Arguments + /// + /// * - `_metadata` -Encoded MultisigISM metadata + /// + /// # Returns + /// + /// u32 - Merkle index of the signed checkpoint + fn index(_metadata: Bytes) -> u32 { + let (_, felt) = _metadata.read_u32(INDEX_OFFSET); + felt + } + + /// Returns the validator ECDSA signature at `_index`. + /// Dev: Assumes signatures are sorted by validator + /// Assumes `_metadata` encodes `threshold` signatures. + /// Assumes `_index` is less than `threshold` + /// + /// # Arguments + /// + /// * - `_metadata` -Encoded MultisigISM metadata + /// * - `_index` - The index of the signature to return. + /// + /// # Returns + /// + /// (u8, u256, u256) - The validator ECDSA signature at `_index`. + fn signature_at(_metadata: Bytes, _index: u32) -> (u8, u256, u256) { + // signature length set to 80 because u128 padding from the v param + let (index_r, r) = _metadata.read_u256(SIGNATURE_OFFSET + SIGNATURE_LENGTH * _index); + let (index_s, s) = _metadata.read_u256(index_r); + let (_, v) = _metadata.read_u8(index_s); + (v, r, s) + } + } +} diff --git a/starknet/cairo/crates/contracts/src/mailbox.cairo b/starknet/cairo/crates/contracts/src/mailbox.cairo new file mode 100644 index 00000000000..bef7c65ba4b --- /dev/null +++ b/starknet/cairo/crates/contracts/src/mailbox.cairo @@ -0,0 +1,519 @@ +#[starknet::contract] +pub mod mailbox { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::interfaces::{ + IMailbox, IMailboxDispatcher, IMailboxDispatcherTrait, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, IPostDispatchHookDispatcher, + ISpecifiesInterchainSecurityModuleDispatcher, + ISpecifiesInterchainSecurityModuleDispatcherTrait, IPostDispatchHookDispatcherTrait, + IMessageRecipientDispatcher, IMessageRecipientDispatcherTrait, ETH_ADDRESS, + }; + use contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; + use contracts::utils::utils::U256TryIntoContractAddress; + use core::starknet::event::EventEmitter; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::{ + ERC20ABI, ERC20ABIDispatcher, ERC20ABIDispatcherTrait + }; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ + ContractAddress, ClassHash, get_caller_address, get_block_number, contract_address_const, + get_contract_address + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[derive(Drop, Serde, starknet::Store)] + pub struct Delivery { + pub processor: ContractAddress, + pub block_number: u64, + } + + + #[storage] + struct Storage { + // Domain of chain on which the contract is deployed + local_domain: u32, + // A monotonically increasing nonce for outbound unique message IDs. + nonce: u32, + // The latest dispatched message ID used for auth in post-dispatch hooks. + latest_dispatched_id: u256, + // The default ISM, used if the recipient fails to specify one. + default_ism: ContractAddress, + // The default post dispatch hook, used for post processing of opting-in dispatches. + default_hook: ContractAddress, + // The required post dispatch hook, used for post processing of ALL dispatches. + required_hook: ContractAddress, + // Mapping of message ID to delivery context that processed the message. + deliveries: LegacyMap::, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + DefaultIsmSet: DefaultIsmSet, + DefaultHookSet: DefaultHookSet, + RequiredHookSet: RequiredHookSet, + Process: Process, + ProcessId: ProcessId, + Dispatch: Dispatch, + DispatchId: DispatchId, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + #[derive(starknet::Event, Drop)] + pub struct DefaultIsmSet { + pub module: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct DefaultHookSet { + pub hook: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct RequiredHookSet { + pub hook: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct Process { + pub origin: u32, + pub sender: u256, + pub recipient: u256 + } + + #[derive(starknet::Event, Drop)] + pub struct ProcessId { + pub id: u256 + } + + #[derive(starknet::Event, Drop)] + pub struct Dispatch { + pub sender: u256, + pub destination_domain: u32, + pub recipient_address: u256, + pub message: Message + } + + #[derive(starknet::Event, Drop)] + pub struct DispatchId { + pub id: u256 + } + + + pub mod Errors { + pub const WRONG_HYPERLANE_VERSION: felt252 = 'Wrong hyperlane version'; + pub const UNEXPECTED_DESTINATION: felt252 = 'Unexpected destination'; + pub const ALREADY_DELIVERED: felt252 = 'Mailbox: already delivered'; + pub const ISM_VERIFICATION_FAILED: felt252 = 'Mailbox:ism verification failed'; + pub const ISM_CANNOT_BE_NULL: felt252 = 'ISM cannot be null'; + pub const OWNER_CANNOT_BE_NULL: felt252 = 'ISM cannot be null'; + pub const HOOK_CANNOT_BE_NULL: felt252 = 'Hook cannot be null'; + pub const NO_ISM_FOUND: felt252 = 'ISM: no ISM found'; + pub const NEW_OWNER_IS_ZERO: felt252 = 'Ownable: new owner cannot be 0'; + pub const ALREADY_OWNER: felt252 = 'Ownable: already owner'; + pub const INSUFFICIENT_BALANCE: felt252 = 'Insufficient balance'; + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Insufficient allowance'; + pub const NOT_ENOUGH_FEE_PROVIDED: felt252 = 'Provided fee < needed fee'; + pub const SIZE_DOES_NOT_MATCH_MESSAGE_BODY: felt252 = 'Size does not match msg body'; + pub const SIZE_DOES_NOT_MATCH_METADATA: felt252 = 'Size does not match metadata'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + _local_domain: u32, + owner: ContractAddress, + _default_ism: ContractAddress, + _default_hook: ContractAddress, + _required_hook: ContractAddress + ) { + assert(_default_ism != contract_address_const::<0>(), Errors::ISM_CANNOT_BE_NULL); + assert(_default_hook != contract_address_const::<0>(), Errors::HOOK_CANNOT_BE_NULL); + assert(_default_hook != contract_address_const::<0>(), Errors::HOOK_CANNOT_BE_NULL); + assert(owner != contract_address_const::<0>(), Errors::OWNER_CANNOT_BE_NULL); + self.local_domain.write(_local_domain); + self.default_ism.write(_default_ism); + self.default_hook.write(_default_hook); + self.required_hook.write(_required_hook); + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IMailboxImpl of IMailbox { + fn get_local_domain(self: @ContractState) -> u32 { + self.local_domain.read() + } + + fn get_default_ism(self: @ContractState) -> ContractAddress { + self.default_ism.read() + } + + fn get_default_hook(self: @ContractState) -> ContractAddress { + self.default_hook.read() + } + + fn get_required_hook(self: @ContractState) -> ContractAddress { + self.required_hook.read() + } + + fn get_latest_dispatched_id(self: @ContractState) -> u256 { + self.latest_dispatched_id.read() + } + + /// Sets the default ISM for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new default ISM + fn set_default_ism(ref self: ContractState, _module: ContractAddress) { + self.ownable.assert_only_owner(); + self.default_ism.write(_module); + self.emit(DefaultIsmSet { module: _module }); + } + + /// Sets the default post dispatch hook for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new default post dispatch hook. + fn set_default_hook(ref self: ContractState, _hook: ContractAddress) { + self.ownable.assert_only_owner(); + self.default_hook.write(_hook); + self.emit(DefaultHookSet { hook: _hook }); + } + + /// Sets the required post dispatch hook for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new required post dispatch hook. + fn set_required_hook(ref self: ContractState, _hook: ContractAddress) { + self.ownable.assert_only_owner(); + self.required_hook.write(_hook); + self.emit(RequiredHookSet { hook: _hook }); + } + + + /// Dispatches a message to the destination domain & recipient using the default hook and empty metadata. + /// + /// # Arguments + /// + /// * `_destination_domain` - Domain of destination chain + /// * `_recipient_address` - Address of recipient on destination chain + /// * `_message_body` - Raw bytes content of message body + /// * `_fee_amount` - the payment provided for sending the message + /// * `_custom_hook_metadata` - Metadata used by the post dispatch hook + /// * `_custom_hook` - Custom hook to use instead of the default + /// + /// # Returns + /// + /// * The message ID inserted into the Mailbox's merkle tree + fn dispatch( + ref self: ContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes, + _fee_amount: u256, + _custom_hook_metadata: Option, + _custom_hook: Option + ) -> u256 { + let hook = match _custom_hook { + Option::Some(hook) => hook, + Option::None(()) => self.default_hook.read(), + }; + let hook_metadata = match _custom_hook_metadata { + Option::Some(hook_metadata) => { + let mut sanitized_bytes_metadata = BytesTrait::new_empty(); + sanitized_bytes_metadata.concat(@hook_metadata); + assert( // what does this exactly checks + sanitized_bytes_metadata == hook_metadata, + Errors::SIZE_DOES_NOT_MATCH_METADATA + ); + hook_metadata + }, + Option::None(()) => BytesTrait::new_empty() + }; + let mut sanitized_bytes_message_body = BytesTrait::new_empty(); + sanitized_bytes_message_body.concat(@_message_body); + assert( + sanitized_bytes_message_body == _message_body, + Errors::SIZE_DOES_NOT_MATCH_MESSAGE_BODY + ); + let (id, message) = build_message( + @self, _destination_domain, _recipient_address, _message_body + ); + self.latest_dispatched_id.write(id); + let current_nonce = self.nonce.read(); + self.nonce.write(current_nonce + 1); + let caller: felt252 = get_caller_address().into(); + self + .emit( + Dispatch { + sender: caller.into(), + destination_domain: _destination_domain, + recipient_address: _recipient_address, + message: message.clone() + } + ); + self.emit(DispatchId { id: id }); + + // HOOKS + + let required_hook_address = self.required_hook.read(); + let required_hook = IPostDispatchHookDispatcher { + contract_address: required_hook_address + }; + let mut required_fee = required_hook + .quote_dispatch(hook_metadata.clone(), message.clone()); + + let hook_dispatcher = IPostDispatchHookDispatcher { contract_address: hook }; + let default_fee = hook_dispatcher + .quote_dispatch(hook_metadata.clone(), message.clone()); + + assert(_fee_amount >= required_fee + default_fee, Errors::NOT_ENOUGH_FEE_PROVIDED); + + let caller_address = get_caller_address(); + let contract_address = get_contract_address(); + + let token_dispatcher = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + let user_balance = token_dispatcher.balanceOf(caller_address); + + assert(user_balance >= required_fee + default_fee, Errors::INSUFFICIENT_BALANCE); + + assert( + token_dispatcher.allowance(caller_address, contract_address) >= _fee_amount, + Errors::INSUFFICIENT_ALLOWANCE + ); + + if (required_fee > 0) { + token_dispatcher.transferFrom(caller_address, required_hook_address, required_fee); + } + required_hook.post_dispatch(hook_metadata.clone(), message.clone(), required_fee); + + if (default_fee > 0) { + token_dispatcher.transferFrom(caller_address, hook, default_fee); + } + hook_dispatcher.post_dispatch(hook_metadata, message, default_fee); + + id + } + + /// Returns true if the message has been processed. + /// + /// # Arguments + /// + /// * `_message_id` - The message ID to check. + /// + /// # Returns + /// + /// * True if the message has been delivered. + fn delivered(self: @ContractState, _message_id: u256) -> bool { + self.deliveries.read(_message_id).block_number > 0 + } + + fn nonce(self: @ContractState) -> u32 { + self.nonce.read() + } + + /// Attempts to deliver `_message` to its recipient. Verifies `_message` via the recipient's ISM using the provided `_metadata` + /// + /// # Arguments + /// + /// * `_metadata` - Metadata used by the ISM to verify `_message`. + /// * `_message` - Formatted Hyperlane message (ref: message.cairo) + fn process(ref self: ContractState, _metadata: Bytes, _message: Message) { + let mut sanitized_bytes_metadata = BytesTrait::new_empty(); + sanitized_bytes_metadata.concat(@_metadata); + assert(sanitized_bytes_metadata == _metadata, Errors::SIZE_DOES_NOT_MATCH_METADATA); + let mut sanitized_bytes_message_body = BytesTrait::new_empty(); + sanitized_bytes_message_body.concat(@_message.body); + assert( + sanitized_bytes_message_body == _message.body, + Errors::SIZE_DOES_NOT_MATCH_MESSAGE_BODY + ); + + assert(_message.version == HYPERLANE_VERSION, Errors::WRONG_HYPERLANE_VERSION); + assert( + _message.destination == self.local_domain.read(), Errors::UNEXPECTED_DESTINATION + ); + let (id, _) = MessageTrait::format_message(_message.clone()); + let caller = get_caller_address(); + let block_number = get_block_number(); + assert(!self.delivered(id), Errors::ALREADY_DELIVERED); + + self.deliveries.write(id, Delivery { processor: caller, block_number: block_number }); + + let recipient_ism = self.recipient_ism(_message.recipient); + let ism = IInterchainSecurityModuleDispatcher { contract_address: recipient_ism }; + + self + .emit( + Process { + origin: _message.origin, + sender: _message.sender, + recipient: _message.recipient + } + ); + self.emit(ProcessId { id: id }); + + assert(ism.verify(_metadata, _message.clone()), Errors::ISM_VERIFICATION_FAILED); + + let message_recipient = IMessageRecipientDispatcher { + contract_address: _message.recipient.try_into().unwrap() + }; + message_recipient.handle(_message.origin, _message.sender, _message.body); + } + + /// Computes quote for dispatching a message to the destination domain & recipient. + /// + /// # Arguments + /// + /// * `_destination_domain` - Domain of destination chain + /// * `_recipient_address` - Address of recipient on destination chain + /// * `_message_body` - Raw bytes content of message body + /// * `_custom_hook_metadata` - Metadata used by the post dispatch hook + /// * `_custom_hook` - Custom hook to use instead of the default + /// + /// # Returns + /// + /// * The payment required to dispatch the message + fn quote_dispatch( + self: @ContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes, + _custom_hook_metadata: Option, + _custom_hook: Option, + ) -> u256 { + let hook_address = match _custom_hook { + Option::Some(hook) => hook, + Option::None(()) => self.default_hook.read() + }; + let hook_metadata = match _custom_hook_metadata { + Option::Some(hook_metadata) => hook_metadata, + Option::None(()) => BytesTrait::new_empty(), + }; + let (_, message) = build_message( + self, _destination_domain, _recipient_address, _message_body.clone() + ); + let required_hook_address = self.required_hook.read(); + let required_hook = IPostDispatchHookDispatcher { + contract_address: required_hook_address + }; + let hook = IPostDispatchHookDispatcher { contract_address: hook_address }; + required_hook.quote_dispatch(hook_metadata.clone(), message.clone()) + + hook.quote_dispatch(hook_metadata, message) + } + + /// Returns the ISM to use for the recipient, defaulting to the default ISM if none is specified. + /// + /// # Arguments + /// + /// * `_recipient` - The message recipient whose ISM should be returned. + /// + /// # Returns + /// + /// * The ISM to use for `_recipient` + fn recipient_ism(self: @ContractState, _recipient: u256) -> ContractAddress { + let mut call_data: Array = ArrayTrait::new(); + let mut res = starknet::syscalls::call_contract_syscall( + _recipient.try_into().unwrap(), + selector!("interchain_security_module"), + call_data.span() + ); + let mut ism_res = match res { + Result::Ok(ism) => ism, + Result::Err(revert_reason) => { + assert(revert_reason == array!['ENTRYPOINT_FAILED'], Errors::NO_ISM_FOUND); + array![].span() + } + }; + if (ism_res.len() != 0) { + let ism_address = Serde::::deserialize(ref ism_res).unwrap(); + if (ism_address != contract_address_const::<0>()) { + return ism_address; + } + } + self.default_ism.read() + } + + /// Returns the account that processed the message. + /// + /// # Arguments + /// + /// * `_id` - The message ID to check. + /// + /// # Returns + /// + /// * The account that processed the message. + fn processor(self: @ContractState, _id: u256) -> ContractAddress { + self.deliveries.read(_id).processor + } + + /// Returns the account that processed the message. + /// + /// # Arguments + /// + /// * `_id` - The message ID to check. + /// + /// # Returns + /// + /// * The number of the block that the message was processed at. + fn processed_at(self: @ContractState, _id: u256) -> u64 { + self.deliveries.read(_id).block_number + } + } + + fn build_message( + self: @ContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes + ) -> (u256, Message) { + let nonce = self.nonce.read(); + let local_domain = self.local_domain.read(); + let caller: felt252 = get_caller_address().into(); + MessageTrait::format_message( + Message { + version: HYPERLANE_VERSION, + nonce: nonce, + origin: local_domain, + sender: caller.into(), + destination: _destination_domain, + recipient: _recipient_address, + body: _message_body + } + ) + } +} + diff --git a/starknet/cairo/crates/contracts/src/utils/keccak256.cairo b/starknet/cairo/crates/contracts/src/utils/keccak256.cairo new file mode 100644 index 00000000000..bd84d4339b7 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/utils/keccak256.cairo @@ -0,0 +1,569 @@ +use alexandria_math::BitShift; +use contracts::libs::checkpoint_lib::checkpoint_lib::HYPERLANE_ANNOUNCEMENT; +use core::byte_array::{ByteArray, ByteArrayTrait}; +use core::integer::u128_byte_reverse; +use core::keccak::cairo_keccak; +use core::starknet::SyscallResultTrait; +use core::to_byte_array::{FormatAsByteArray, AppendFormattedToByteArray}; +use starknet::{EthAddress, eth_signature::is_eth_signature_valid, secp256_trait::Signature}; + +pub const ETH_SIGNED_MESSAGE: felt252 = '\x19Ethereum Signed Message:\n32'; + + +// TYPE DEFINITION +type Words64 = Span; + +// CONSTANTS DEFINITION + +pub const ONE_SHIFT_64: u128 = 0x10000000000000000; +pub const FELT252_MASK: u256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; +pub const ADDRESS_SIZE: usize = 32; +pub const HASH_SIZE: usize = 32; +const KECCAK_FULL_RATE_IN_U64S: usize = 17; + +/// +/// Structure specifying for each element, the value of this element as u256 and the size (in bytes) of this element +/// +#[derive(Copy, Drop, Serde, starknet::Store, Debug, PartialEq)] +pub struct ByteData { + pub value: u256, + pub size: usize +} + +fn zero_keccak_hash(index: u32) -> u256 { + let zero_hashes = array![ + 0x70a4855d04d8fa7b3b2782ca53b600e5c003c7dcb27d7e923c23f7860146d2c5, + 0x8ac9bc64e0a996ff9165d677b4f712667d818f822942463614281e7a9e7836bc, + 0x9867d524c957cd4a34ab5cb41cf07a61b9a21b01fd478bb4bf153c65abc0a854, + 0x4786f66fb0dfef4cc6c44c8c079a50602ceb5ae16212a13195fce125910dff99, + 0x4c24d3cb4a3a6364b152e840bd5e68f7b70bbf4b7b4c3655b9736f582676e7e8, + 0xec20d0c108ce727d4435781105cb4c026a879dd1da80204aea049855e78915c4, + 0xb3d5b06f1965e76affc51cc6f4fd3573c17aca0e130b851b1b082ee0a5e13750, + 0x2fee92aa6bc7129f61059dec151e37832a0c5391ff956a0501a55e7254e0cbdf, + 0xce7b1eba890df1f0c6e5831b1d7f164b7e814c9ccf439104f1018cdd034d1b01, + 0x715325eaf93144c3121df9be1e502565203c8c2d1c7d8cab1625d69d205e31ad, + 0x07b634fd0ff4184560dbb4b1f8fb1246c6fd24bf58934233eecb08d46bddd26b, + 0xc4885e2eaa4fe0e9a9a4b9a9bd5468438b29849c0d56a2eae38807d900039c6f, + 0x1173b4507b58cf903327ca3b7f6b2b18e6c4a14fc58c1a8a213c2faddabfe230, + 0x57e542ab2576985e8144bb5c35db1189474ea3fb34f456db95044b5286fc8431, + 0xdda769b8a7519cf92bc60f37373ea91d07ec8114c189833b9e3a512ed7c86691, + 0xc0a98ba674651a884e48389eb69e8735e12c90b4faed3106cb58f4d8e93910bf, + 0xb42369a2f775c0b3d24d3ab0e99b6fc728aa52882013ebab5fa3c82029de90f4, + 0x6d99cc2723be666f8d40e78fc5198f83792aaa7f41a5d71bfe9d5822ba7e7d5b, + 0x278b338d887ab8cc46e85863c221336c9d3b27cb6e6114b9070409cdecf38a5d, + 0x7bb968fd99be569582d15c8d43c9148fd5558ca10c9c70de9b57488ec2fd2954, + 0x2a316717a4aa2d60bed23e95fc3595fdd71f4ade789c8db98ea581aeb7c78053, + 0x584efaff23ec6158ce7d30ca6b514abb1892923e656c973c21a193c40571655a, + 0x0e89b39caa331523d4b98c3abdae38563bed440ec164ac81ecc738804bd46255, + 0xd8383520305dd28bde1cef07a7234d4381a1c9bad3aba3a9bf050b43f9f9b9e2, + 0xcd5c4ff8dae018efefdba2ebf35857546284b6b8912cceecbda0a2bd9b657b82, + 0xaba91af7fd52ef0bceaaee96fc1fc46feb087fb1985720e739cdc5b6b11dd4c0, + 0xb2dc520d17b9a82a12f5ec7af5ed8882b468b7a3f8492188181c2c4448db33a5, + 0x5d1e89019bf642bbc63a008d7aef19c40a461dcf7adade5029ca4592802f6459, + 0x2ec59c21bd5581d0c5b10d6b392a2d40a0ce500e29ccb5c7f75d50a01e0396b6, + 0x6f00c6b91b5194e948cefb0042862c5f36525b11ccf87581a3debd79cbcd1c47, + 0x7c312f53e36ec8bcca3e9530f94cc0d5acc3fd2cdf88258cd72e52321ce748f5, + 0xcfb409c77e66d490e1b4b57818f255deb978c8415aecf3952d51991445d0fe15, + 0x63e5f30e16932f36f608404895bca64bc86f3888a94503d6a8628b54d9ec0d29 + ]; + *zero_hashes.at(index) +} + +/// Reverses the endianness of an u256 +/// +/// # Arguments +/// +/// * `value` - Value to reverse +/// +/// # Returns +/// +/// the reverse equivalent +pub fn reverse_endianness(value: u256) -> u256 { + let new_low = u128_byte_reverse(value.high); + let new_high = u128_byte_reverse(value.low); + u256 { low: new_low, high: new_high } +} + + +/// Determines the Ethereum compatible signature for a given hash +/// dev : Since call this function for hash only, the ETH_SIGNED_MESSAGE size will always be 32 +/// +/// # Arguments +/// +/// * `hash` - Hash to sign +/// +/// # Returns the corresponding hash as big endian +pub fn to_eth_signature(hash: u256) -> u256 { + let input = array![ + ByteData { + value: ETH_SIGNED_MESSAGE.into(), size: u256_word_size(ETH_SIGNED_MESSAGE.into()).into() + }, + ByteData { value: hash, size: HASH_SIZE } + ]; + let hash = compute_keccak(input.span()); + reverse_endianness(hash) +} + +/// Determines the correctness of an ethereum signature given a digest, signer and signature +/// +/// # Arguments +/// +/// * - `_msg_hash` - to digest used to sign the message +/// * - `_signature` - the signature to check +/// * - `_signer` - the signer ethereum address +/// +/// # Returns +/// +/// boolean - True if valid +pub fn bool_is_eth_signature_valid( + msg_hash: u256, signature: Signature, signer: EthAddress +) -> bool { + match is_eth_signature_valid(msg_hash, signature, signer) { + Result::Ok(()) => true, + Result::Err(_) => false + } +} + + +/// Determines the size of a u64 element, by successive division +/// +/// # Arguments +/// +/// * `word` - u64 word to consider +/// +/// # Returns +/// +/// The size (in bytes) for the given word +pub fn u64_word_size(word: u64) -> u8 { + let mut word_len = 0; + while word_len < 8 { + if word < one_shift_left_bytes_u64(word_len) { + break; + } + word_len += 1; + }; + word_len +} + + +/// Determines the size of a u256 element, by successive division +/// +/// # Arguments +/// +/// * `word` - u256 word to consider +/// +/// # Returns +/// +/// The size (in bytes) for the given word +pub fn u256_word_size(word: u256) -> u8 { + let mut word_len = 0; + while word_len < 32 { + if word < one_shift_left_bytes_u256(word_len) { + break; + } + word_len += 1; + }; + word_len +} + + +/// Shifts helper for u64 +/// dev : panics if u64 overflow +/// +/// # Arguments +/// +/// * `n_bytes` - The number of bytes shift +/// +/// # Returns +/// +/// u64 representing the shifting number associated to the given number +pub fn one_shift_left_bytes_u64(n_bytes: u8) -> u64 { + match n_bytes { + 0 => 0x1, + 1 => 0x100, + 2 => 0x10000, + 3 => 0x1000000, + 4 => 0x100000000, + 5 => 0x10000000000, + 6 => 0x1000000000000, + 7 => 0x100000000000000, + _ => core::panic_with_felt252('n_bytes too big'), + } +} + + +/// Shifts helper for u256 +/// dev : panics if u256 overflow +/// +/// # Arguments +/// +/// * `n_bytes` - The number of bytes shift +/// +/// # Returns +/// +/// u256 representing the shifting number associated to the given number +pub fn one_shift_left_bytes_u256(n_bytes: u8) -> u256 { + match n_bytes { + 0 => 0x1, + 1 => 0x100, + 2 => 0x10000, + 3 => 0x1000000, + 4 => 0x100000000, + 5 => 0x10000000000, + 6 => 0x1000000000000, + 7 => 0x100000000000000, + 8 => 0x10000000000000000, + 9 => 0x1000000000000000000, + 10 => 0x100000000000000000000, + 11 => 0x10000000000000000000000, + 12 => 0x1000000000000000000000000, + 13 => 0x100000000000000000000000000, + 14 => 0x10000000000000000000000000000, + 15 => 0x1000000000000000000000000000000, + 16 => 0x100000000000000000000000000000000, + 17 => 0x10000000000000000000000000000000000, + 18 => 0x1000000000000000000000000000000000000, + 19 => 0x100000000000000000000000000000000000000, + 20 => 0x10000000000000000000000000000000000000000, + 21 => 0x1000000000000000000000000000000000000000000, + 22 => 0x100000000000000000000000000000000000000000000, + 23 => 0x10000000000000000000000000000000000000000000000, + 24 => 0x1000000000000000000000000000000000000000000000000, + 25 => 0x100000000000000000000000000000000000000000000000000, + 26 => 0x10000000000000000000000000000000000000000000000000000, + 27 => 0x1000000000000000000000000000000000000000000000000000000, + 28 => 0x100000000000000000000000000000000000000000000000000000000, + 29 => 0x10000000000000000000000000000000000000000000000000000000000, + 30 => 0x1000000000000000000000000000000000000000000000000000000000000, + 31 => 0x100000000000000000000000000000000000000000000000000000000000000, + _ => core::panic_with_felt252('n_bytes too big'), + } +} + +/// Shifts equivalent u128 mask for a given number of bytes +/// dev : panics if u128 overflow +/// +/// # Arguments +/// +/// * `n_bytes` - The number of bytes shift +/// +/// # Returns +/// +/// u256 representing the associated mask +pub fn u128_mask(n_bytes: u8) -> u128 { + match n_bytes { + 0 => 0, + 1 => 0xFF, + 2 => 0xFFFF, + 3 => 0xFFFFFF, + 4 => 0xFFFFFFFF, + 5 => 0xFFFFFFFFFF, + 6 => 0xFFFFFFFFFFFF, + 7 => 0xFFFFFFFFFFFFFF, + 8 => 0xFFFFFFFFFFFFFFFF, + 9 => 0xFFFFFFFFFFFFFFFFFF, + 10 => 0xFFFFFFFFFFFFFFFFFFFF, + 11 => 0xFFFFFFFFFFFFFFFFFFFFFF, + 12 => 0xFFFFFFFFFFFFFFFFFFFFFFFF, + 13 => 0xFFFFFFFFFFFFFFFFFFFFFFFFFF, + 14 => 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 15 => 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 16 => 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + _ => core::panic_with_felt252('n_bytes too big'), + } +} + +/// Givens a span of ByteData, returns a concatenated string (ByteArray) of the input +/// +/// # Arguments +/// +/// * `bytes` - a span of ByteData containing the information that need to be hash +/// +/// # Returns +/// +/// ByteArray representing the concatenation of the input (bytes31). +fn concatenate_input(bytes: Span) -> ByteArray { + let mut output_string: ByteArray = Default::default(); + let mut cur_idx = 0; + loop { + if cur_idx == bytes.len() { + break; + } + let byte = *bytes.at(cur_idx); + if byte.size == 32 { + // Extract the upper 1-byte part + let up_byte = up_bytes(byte.value); + output_string.append_word(up_byte.try_into().unwrap(), 1); + + // Extract the lower 31-byte part + let down_byte = down_bytes(byte.value); + output_string.append_word(down_byte.try_into().unwrap(), 31); + } else { + output_string.append_word(byte.value.try_into().unwrap(), byte.size); + } + cur_idx += 1; + }; + output_string +} + + +// -------------------------------------------------------- +// This section is part of the cairo core contract (see: https://github.com/starkware-libs/cairo/blob/953afd5e7ede296c99deaf189e18e361229517c0/corelib/src/keccak.cairo) +pub fn compute_keccak_byte_array(arr: @ByteArray) -> u256 { + let mut input = array![]; + let mut i = 0; + let mut inner = 0; + let mut limb: u64 = 0; + let mut factor: u64 = 1; + while let Option::Some(b) = arr + .at(i) { + limb = limb + b.into() * factor; + i += 1; + inner += 1; + if inner == 8 { + input.append(limb); + inner = 0; + limb = 0; + factor = 1; + } else { + factor *= 0x100; + } + }; + add_padding(ref input, limb, inner); + starknet::syscalls::keccak_syscall(input.span()).unwrap_syscall() +} + + +/// The padding in keccak256 is "1 0* 1". +/// `last_input_num_bytes` (0-7) is the number of bytes in the last u64 input - `last_input_word`. +fn add_padding(ref input: Array, last_input_word: u64, last_input_num_bytes: usize) { + let words_divisor = KECCAK_FULL_RATE_IN_U64S.try_into().unwrap(); + // `last_block_num_full_words` is in range [0, KECCAK_FULL_RATE_IN_U64S - 1] + let (_, last_block_num_full_words) = core::integer::u32_safe_divmod(input.len(), words_divisor); + + // The first word to append would be of the form + // 0x1<`last_input_num_bytes` LSB bytes of `last_input_word`>. + // For example, for `last_input_num_bytes == 4`: + // 0x1000000 + (last_input_word & 0xffffff) + let first_word_to_append = if last_input_num_bytes == 0 { + // This case is handled separately to avoid unnecessary computations. + 1 + } else { + let first_padding_byte_part = if last_input_num_bytes == 1 { + 0x100 + } else if last_input_num_bytes == 2 { + 0x10000 + } else if last_input_num_bytes == 3 { + 0x1000000 + } else if last_input_num_bytes == 4 { + 0x100000000 + } else if last_input_num_bytes == 5 { + 0x10000000000 + } else if last_input_num_bytes == 6 { + 0x1000000000000 + } else if last_input_num_bytes == 7 { + 0x100000000000000 + } else { + core::panic_with_felt252('Keccak last input word >7b') + }; + let (_, r) = core::integer::u64_safe_divmod( + last_input_word, first_padding_byte_part.try_into().unwrap() + ); + first_padding_byte_part + r + }; + + if last_block_num_full_words == KECCAK_FULL_RATE_IN_U64S - 1 { + input.append(0x8000000000000000 + first_word_to_append); + return; + } + + // last_block_num_full_words < KECCAK_FULL_RATE_IN_U64S - 1 + input.append(first_word_to_append); + finalize_padding(ref input, KECCAK_FULL_RATE_IN_U64S - 1 - last_block_num_full_words); +} + +/// Finalize the padding by appending "0* 1". +fn finalize_padding(ref input: Array, num_padding_words: u32) { + if (num_padding_words == 1) { + input.append(0x8000000000000000); + return; + } + + input.append(0); + finalize_padding(ref input, num_padding_words - 1); +} + + +// -------------------------------------------------------------------------------------------- +// END SECTION +/// Retrieve the 1 up byte of a given u256 input +fn up_bytes(input: u256) -> u256 { + BitShift::shr(input, 248) & 0xFF +} + +/// Retrieve the 31 low byte of a given u256 input +fn down_bytes(input: u256) -> u256 { + input & FELT252_MASK +} + +/// The general function that computes the keccak hash for an input span of ByteData +/// +/// # Arguments +/// +/// * `bytes` - a span of ByteData containing the information for the hash computation +/// +/// # Returns +/// +/// The corresponding keccak hash for the input arguments +pub fn compute_keccak(bytes: Span) -> u256 { + if (bytes.is_empty()) { + return zero_keccak_hash(0); + } + if (*bytes.at(0).value == 0 && bytes.len() == 1) { + return zero_keccak_hash(*bytes.at(0).size); + } + let concatenate_input = concatenate_input(bytes); + compute_keccak_byte_array(@concatenate_input) +} + + +#[cfg(test)] +mod tests { + use alexandria_bytes::{Bytes, BytesTrait}; + use starknet::contract_address_const; + use super::{ + reverse_endianness, ByteData, HYPERLANE_ANNOUNCEMENT, compute_keccak, u64_word_size, + zero_keccak_hash, ADDRESS_SIZE, up_bytes, down_bytes + }; + const TEST_STARKNET_DOMAIN: u32 = 23448594; + + #[test] + fn test_up_bytes() { + let input = 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + let expected = 0x01; + assert_eq!(up_bytes(input), expected); + + let input = 0x11FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + let expected = 0x11; + assert_eq!(up_bytes(input), expected); + + let input = 0x11; + let expected = 0; + assert_eq!(up_bytes(input), expected); + } + #[test] + fn test_down_bytes() { + let input = 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + let expected = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + assert_eq!(down_bytes(input), expected); + + let input = 0x0100FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + let expected = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + assert_eq!(down_bytes(input), expected); + } + + #[test] + fn test_reverse_endianness() { + let big_endian_number: u256 = u256 { high: 0x12345678, low: 0 }; + let expected_result: u256 = u256 { high: 0, low: 0x78563412000000000000000000000000 }; + assert( + reverse_endianness(big_endian_number) == expected_result, 'Failed to realise reverse' + ); + } + + #[test] + fn test_compute_keccak() { + let array = array![ByteData { value: HYPERLANE_ANNOUNCEMENT.into(), size: 22 }]; + assert_eq!( + compute_keccak(array.span()), + 0x4CE82A3F02824445F403FB5B69D4AB0FFFFC358BBAF61B0A130C971AB0CB15DA + ); + + let array = array![ + ByteData { + value: 0x007a9a2e1663480b3845df0d714e8caa49f9241e13a826a678da3f366e546f2a, + size: ADDRESS_SIZE + } + ]; + assert_eq!( + compute_keccak(array.span()), + 0x9D3185A7830200BD62EF9D26D44D9169A544C1FFA0FB98D0D56AAAA3BA8FE354 + ); + + let array = array![ByteData { value: TEST_STARKNET_DOMAIN.into(), size: 4 }]; + assert_eq!( + compute_keccak(array.span()), + 0xBC54A343AEF444F26F67F8538FE9F045A340D250AE50D019CB7528444FA32AEC + ); + + let array = array![ + ByteData { value: TEST_STARKNET_DOMAIN.into(), size: 4 }, + ByteData { + value: 0x007a9a2e1663480b3845df0d714e8caa49f9241e13a826a678da3f366e546f2a, + size: ADDRESS_SIZE + } + ]; + assert_eq!( + compute_keccak(array.span()), + 0x5DD6FF889DE1B20CF9B497A6716210C826DE3739FCAF50CD66F42F1DBE8626F2 + ); + + let array = array![ + ByteData { value: TEST_STARKNET_DOMAIN.into(), size: 4 }, + ByteData { + value: 0x007a9a2e1663480b3845df0d714e8caa49f9241e13a826a678da3f366e546f2a, + size: ADDRESS_SIZE + }, + ByteData { value: HYPERLANE_ANNOUNCEMENT.into(), size: 22 } + ]; + assert_eq!( + compute_keccak(array.span()), + 0xFD8977CB20EE179678A5008D11A591D101FBDCC7669BC5CA31B92439A7E7FB4E + ); + + let array = array![ + ByteData { + value: 0x61a4bcca63b5e8a46da3abe2080f75c16c18467d5838f00b375d9ba4c7c313dd, + size: ADDRESS_SIZE + }, + ByteData { + value: 0x49d35915d0abec0a28990198bb32aa570e681e7eb41a001c0094c7c36a712671, + size: ADDRESS_SIZE + } + ]; + assert_eq!( + compute_keccak(array.span()), + 0x8310DAC21721349FCFA72BB5499303F0C6FAB4006FA2A637D02F7D6BB2188B47 + ); + + let array = array![ByteData { value: 0, size: 1 }]; + assert_eq!(compute_keccak(array.span()), zero_keccak_hash(1)); + + let array = array![ + ByteData { value: 0, size: 10 }, + ByteData { + value: 0x007a9a2e1663480b3845df0d714e8caa49f9241e13a826a678da3f366e546f2a, size: 32 + }, + ]; + assert_eq!( + compute_keccak(array.span()), + 0xA9EC21A66254DD00FA8F01E445CACEAA0D16A1E91700C85FB3ED6C1229B38D2A + ); + } + + #[test] + fn test_u64_word_size() { + let test = 0x12345; + assert_eq!(u64_word_size(test), 3); + let test = 0x1234567890; + assert_eq!(u64_word_size(test), 5); + let test = 0xfffffffffffffff; + assert_eq!(u64_word_size(test), 8); + let test = 0xfff; + assert_eq!(u64_word_size(test), 2); + let test = 0x123456; + assert_eq!(u64_word_size(test), 3); + let test = 0x1; + assert_eq!(u64_word_size(test), 1); + } +} diff --git a/starknet/cairo/crates/contracts/src/utils/store_arrays.cairo b/starknet/cairo/crates/contracts/src/utils/store_arrays.cairo new file mode 100644 index 00000000000..0139df50cef --- /dev/null +++ b/starknet/cairo/crates/contracts/src/utils/store_arrays.cairo @@ -0,0 +1,63 @@ +use starknet::storage_access::{Store, StorageBaseAddress,}; +// ************************************************************************* +// IMPORTS +// ************************************************************************* + +// Code from https://book.starknet.io/ch02-07-01-02-million-dollar-homepage.html +// Core lib imports. +use starknet::{ContractAddress, SyscallResult,}; + +pub impl StoreFelt252Array of Store> { + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { + StoreFelt252Array::read_at_offset(address_domain, base, 0) + } + fn write( + address_domain: u32, base: StorageBaseAddress, value: Array + ) -> SyscallResult<()> { + StoreFelt252Array::write_at_offset(address_domain, base, 0, value) + } + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8 + ) -> SyscallResult> { + let mut arr: Array = ArrayTrait::new(); + // Read the stored array's length. If the length is superior to 255, the read will fail. + let len: u8 = Store::::read_at_offset(address_domain, base, offset) + .expect('Storage Span too large'); + + offset += 1; + + // Sequentially read all stored elements and append them to the array. + let exit = len + offset; + loop { + if offset >= exit { + break; + } + let value = Store::::read_at_offset(address_domain, base, offset).unwrap(); + arr.append(value); + offset += Store::::size(); + }; + Result::Ok(arr) + } + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8, mut value: Array + ) -> SyscallResult<()> { + // // Store the length of the array in the first storage slot. 255 of elements is max + let len: u8 = value.len().try_into().expect('Storage - Span too large'); + Store::::write_at_offset(address_domain, base, offset, len).unwrap(); + offset += 1; + // Store the array elements sequentially + loop { + match value.pop_front() { + Option::Some(element) => { + Store::::write_at_offset(address_domain, base, offset, element) + .unwrap(); + offset += Store::::size(); + }, + Option::None => { break Result::Ok(()); } + }; + } + } + fn size() -> u8 { + 255 / Store::::size() + } +} diff --git a/starknet/cairo/crates/contracts/src/utils/utils.cairo b/starknet/cairo/crates/contracts/src/utils/utils.cairo new file mode 100644 index 00000000000..f7482c3f883 --- /dev/null +++ b/starknet/cairo/crates/contracts/src/utils/utils.cairo @@ -0,0 +1,11 @@ +use starknet::ContractAddress; + +pub impl U256TryIntoContractAddress of TryInto { + fn try_into(self: u256) -> Option { + let maybe_value: Option = self.try_into(); + match maybe_value { + Option::Some(value) => value.try_into(), + Option::None => Option::None, + } + } +} diff --git a/starknet/cairo/crates/contracts/tests/hooks/test_merkle_tree_hook.cairo b/starknet/cairo/crates/contracts/tests/hooks/test_merkle_tree_hook.cairo new file mode 100644 index 00000000000..e7cb23bcc2a --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/hooks/test_merkle_tree_hook.cairo @@ -0,0 +1,187 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::hooks::merkle_tree_hook::merkle_tree_hook; +use contracts::interfaces::{ + Types, IPostDispatchHookDispatcher, IPostDispatchHookDispatcherTrait, IMerkleTreeHook, + IMailboxDispatcher, IMailboxDispatcherTrait, IMerkleTreeHookDispatcher, + IMerkleTreeHookDispatcherTrait +}; +use contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; +use contracts::utils::keccak256::{ByteData, HASH_SIZE}; +use contracts::utils::utils::U256TryIntoContractAddress; +use merkle_tree_hook::{InternalTrait, treeContractMemberStateTrait, countContractMemberStateTrait}; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::cheatcodes::events::EventAssertions; +use snforge_std::{start_prank, CheatTarget}; +use super::super::setup::{ + setup_merkle_tree_hook, MAILBOX, LOCAL_DOMAIN, VALID_OWNER, VALID_RECIPIENT, DESTINATION_DOMAIN +}; + +#[test] +fn test_merkle_tree_hook_type() { + let (_, merkle_tree_hook, _) = setup_merkle_tree_hook(); + assert_eq!(merkle_tree_hook.hook_type(), Types::MERKLE_TREE(())); +} + +#[test] +fn test_supports_metadata() { + let mut metadata = BytesTrait::new_empty(); + let (_, merkle_tree_hook, _) = setup_merkle_tree_hook(); + assert_eq!(merkle_tree_hook.supports_metadata(metadata.clone()), true); + let variant = 1; + metadata.append_u16(variant); + assert_eq!(merkle_tree_hook.supports_metadata(metadata), true); + metadata = BytesTrait::new_empty(); + metadata.append_u16(variant + 1); + assert_eq!(merkle_tree_hook.supports_metadata(metadata), false); +} + +#[test] +fn test_post_dispatch() { + let (merkle_tree, post_dispatch_hook, mut spy) = setup_merkle_tree_hook(); + let mailbox = IMailboxDispatcher { contract_address: MAILBOX() }; + let ownable = IOwnableDispatcher { contract_address: MAILBOX() }; + start_prank(CheatTarget::One(ownable.contract_address), VALID_OWNER().try_into().unwrap()); + let id = mailbox + .dispatch( + DESTINATION_DOMAIN, + VALID_RECIPIENT(), + BytesTrait::new_empty(), + 0, + Option::None, + Option::None + ); + assert(mailbox.get_latest_dispatched_id() == id, 'Dispatch failed'); + let nonce = 0; + let local_domain = mailbox.get_local_domain(); + let count = merkle_tree.count(); + let mut metadata = BytesTrait::new_empty(); + let variant = 1; + metadata.append_u16(variant); + let message = Message { + version: HYPERLANE_VERSION, + nonce: nonce, + origin: local_domain, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: BytesTrait::new_empty(), + }; + post_dispatch_hook.post_dispatch(metadata, message, 0); + let expected_event = merkle_tree_hook::Event::InsertedIntoTree( + merkle_tree_hook::InsertedIntoTree { id: id, index: count.try_into().unwrap() } + ); + spy.assert_emitted(@array![(merkle_tree.contract_address, expected_event),]); + assert_eq!(merkle_tree.count(), count + 1); +} + +#[test] +#[should_panic(expected: ('Message not dispatching',))] +fn test_post_dispatch_fails_if_message_not_dispatching() { + let (_, post_dispatch_hook, _) = setup_merkle_tree_hook(); + let mut metadata = BytesTrait::new_empty(); + let variant = 1; + metadata.append_u16(variant); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0_u32, + origin: 0_u32, + sender: VALID_OWNER(), + destination: 0_u32, + recipient: VALID_RECIPIENT(), + body: BytesTrait::new_empty(), + }; + post_dispatch_hook.post_dispatch(metadata, message, 0); +} +#[test] +#[should_panic(expected: ('Invalid metadata variant',))] +fn test_post_dispatch_fails_if_invalid_variant() { + let (_, post_dispatch_hook, _) = setup_merkle_tree_hook(); + let mut metadata = BytesTrait::new_empty(); + let variant = 2; + metadata.append_u16(variant); + let message = MessageTrait::default(); + post_dispatch_hook.post_dispatch(metadata, message, 0); +} + +#[test] +fn test_quote_dispatch() { + let (_, post_dispatch_hook, _) = setup_merkle_tree_hook(); + let mut metadata = BytesTrait::new_empty(); + let variant = 1; + metadata.append_u16(variant); + let message = MessageTrait::default(); + assert_eq!(post_dispatch_hook.quote_dispatch(metadata, message), 0); +} + +#[test] +#[should_panic(expected: ('Invalid metadata variant',))] +fn test_quote_dispatch_fails_if_invalid_variant() { + let (_, post_dispatch_hook, _) = setup_merkle_tree_hook(); + let mut metadata = BytesTrait::new_empty(); + let variant = 2; + metadata.append_u16(variant); + let message = MessageTrait::default(); + post_dispatch_hook.quote_dispatch(metadata, message); +} + +#[test] +fn test_count() { + let (merkle_tree, _, _) = setup_merkle_tree_hook(); + let count = merkle_tree.count(); + assert_eq!(count, 0); +} + + +// Test internal functions + +#[test] +fn test_insert_node_into_merkle_tree_hook() { + let mut state = merkle_tree_hook::contract_state_for_testing(); + assert_eq!(state.count.read(), 0); + + let node_1: u256 = 'node_1'.try_into().unwrap(); + state._insert(ByteData { value: node_1, size: 6 }); + assert_eq!(state.count.read(), 1); + assert_eq!(state.tree.read(0), ByteData { value: node_1, size: 6 }); + + let node_2: u256 = 'node_2'.try_into().unwrap(); + let expected_hash = 0x61a4bcca63b5e8a46da3abe2080f75c16c18467d5838f00b375d9ba4c7c313dd; + state._insert(ByteData { value: node_2, size: 6 }); + assert_eq!(state.count.read(), 2); + assert_eq!(state.tree.read(0), ByteData { value: node_1, size: 6 }); + assert_eq!(state.tree.read(1), ByteData { value: expected_hash, size: HASH_SIZE }); + + let node_3: u256 = 'node_3'.try_into().unwrap(); + state._insert(ByteData { value: node_3, size: 6 }); + assert_eq!(state.count.read(), 3); + assert_eq!(state.tree.read(0), ByteData { value: node_3, size: 6 }); + assert_eq!(state.tree.read(1), ByteData { value: expected_hash, size: HASH_SIZE }); + + let node_4: u256 = 'node_4'.try_into().unwrap(); + let expected_hash_2 = 0x478b18b26b7d2fd037a6a26f00b4fac6f0039349b52ba7cf9f342117c2da1083; + state._insert(ByteData { value: node_4, size: 6 }); + + assert_eq!(state.count.read(), 4); + assert_eq!(state.tree.read(0), ByteData { value: node_3, size: 6 }); + assert_eq!(state.tree.read(1), ByteData { value: expected_hash, size: HASH_SIZE }); + assert_eq!(state.tree.read(2), ByteData { value: expected_hash_2, size: HASH_SIZE }); + let mut expected_result = array![ + ByteData { value: node_3, size: 6 }, + ByteData { value: expected_hash, size: HASH_SIZE }, + ByteData { value: expected_hash_2, size: HASH_SIZE }, + ]; + let mut cur_idx = 0; + loop { + if (cur_idx >= merkle_tree_hook::TREE_DEPTH - 3) { + break; + } + expected_result.append(ByteData { value: 0, size: 0 }); + cur_idx += 1; + }; + assert(state._build_tree() == expected_result, 'build tree failed'); + assert(state._root() != 0, 'root computation failed'); + let (root, count) = state.latest_checkpoint(); + assert_eq!(root, state._root()); + assert_eq!(count, 3); +} + diff --git a/starknet/cairo/crates/contracts/tests/hooks/test_protocol_fee.cairo b/starknet/cairo/crates/contracts/tests/hooks/test_protocol_fee.cairo new file mode 100644 index 00000000000..659c424ec1c --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/hooks/test_protocol_fee.cairo @@ -0,0 +1,151 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::{ + Types, IProtocolFeeDispatcher, IProtocolFeeDispatcherTrait, IPostDispatchHookDispatcher, + IPostDispatchHookDispatcherTrait, ETH_ADDRESS +}; +use contracts::libs::message::{Message, MessageTrait}; +use contracts::utils::utils::U256TryIntoContractAddress; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use snforge_std::{start_prank, CheatTarget, stop_prank}; +use super::super::setup::{ + setup_protocol_fee, OWNER, MAX_PROTOCOL_FEE, BENEFICIARY, PROTOCOL_FEE, INITIAL_SUPPLY, + setup_mock_token +}; + + +#[test] +fn test_hook_type() { + let (_, protocol_fee) = setup_protocol_fee(); + assert_eq!(protocol_fee.hook_type(), Types::PROTOCOL_FEE(())); +} + +#[test] +fn test_set_protocol_fee() { + let (protocol_fee, _) = setup_protocol_fee(); + let ownable = IOwnableDispatcher { contract_address: protocol_fee.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let new_protocol_fee = 20000; + protocol_fee.set_protocol_fee(new_protocol_fee); + assert_eq!(protocol_fee.get_protocol_fee(), new_protocol_fee); +} + + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_protocol_fee_fails_if_not_owner() { + let (protocol_fee, _) = setup_protocol_fee(); + let new_protocol_fee = 20000; + protocol_fee.set_protocol_fee(new_protocol_fee); +} + +#[test] +#[should_panic(expected: ('Exceeds max protocol fee',))] +fn test_set_protocol_fee_fails_if_higher_than_max() { + let (protocol_fee, _) = setup_protocol_fee(); + let ownable = IOwnableDispatcher { contract_address: protocol_fee.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let new_protocol_fee = MAX_PROTOCOL_FEE + 1; + protocol_fee.set_protocol_fee(new_protocol_fee); + assert_eq!(protocol_fee.get_protocol_fee(), new_protocol_fee); +} + + +#[test] +fn test_set_beneficiary() { + let (protocol_fee, _) = setup_protocol_fee(); + let ownable = IOwnableDispatcher { contract_address: protocol_fee.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let new_beneficiary = 'NEW_BENEFICIARY'.try_into().unwrap(); + protocol_fee.set_beneficiary(new_beneficiary); + assert_eq!(protocol_fee.get_beneficiary(), new_beneficiary); +} + + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_beneficiary_fails_if_not_owner() { + let (protocol_fee, _) = setup_protocol_fee(); + let new_beneficiary = 'NEW_BENEFICIARY'.try_into().unwrap(); + protocol_fee.set_beneficiary(new_beneficiary); +} + + +#[test] +fn test_collect_protocol_fee() { + let fee_token = setup_mock_token(); + let (protocol_fee, _) = setup_protocol_fee(); + let ownable = IOwnableDispatcher { contract_address: fee_token.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + + // First transfer the token to the contract + fee_token.transfer(protocol_fee.contract_address, PROTOCOL_FEE); + assert_eq!(fee_token.balanceOf(protocol_fee.contract_address), PROTOCOL_FEE); + stop_prank(CheatTarget::One(ownable.contract_address)); + + protocol_fee.collect_protocol_fees(); + assert_eq!(fee_token.balanceOf(BENEFICIARY()), PROTOCOL_FEE); + assert_eq!(fee_token.balanceOf(protocol_fee.contract_address), 0); +} + +#[test] +#[should_panic(expected: ('Insufficient balance',))] +fn test_collect_protocol_fee_fails_if_insufficient_balance() { + setup_mock_token(); + let (protocol_fee, _) = setup_protocol_fee(); + protocol_fee.collect_protocol_fees(); +} + + +#[test] +fn test_supports_metadata() { + let mut metadata = BytesTrait::new_empty(); + let (_, post_dispatch_hook) = setup_protocol_fee(); + assert_eq!(post_dispatch_hook.supports_metadata(metadata.clone()), true); + let variant = 1; + metadata.append_u16(variant); + assert_eq!(post_dispatch_hook.supports_metadata(metadata), true); + metadata = BytesTrait::new_empty(); + metadata.append_u16(variant + 1); + assert_eq!(post_dispatch_hook.supports_metadata(metadata), false); +} + + +#[test] +#[should_panic(expected: ('Invalid metadata variant',))] +fn test_post_dispatch_fails_if_invalid_variant() { + let fee_token = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + let (_, post_dispatch_hook) = setup_protocol_fee(); + let ownable = IOwnableDispatcher { contract_address: fee_token.contract_address }; + let mut metadata = BytesTrait::new_empty(); + let variant = 2; + metadata.append_u16(variant); + let message = MessageTrait::default(); + stop_prank(CheatTarget::One(ownable.contract_address)); + let ownable = IOwnableDispatcher { contract_address: post_dispatch_hook.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + post_dispatch_hook.post_dispatch(metadata, message, PROTOCOL_FEE); +} + + +#[test] +fn test_quote_dispatch() { + let (_, post_dispatch_hook) = setup_protocol_fee(); + let mut metadata = BytesTrait::new_empty(); + let variant = 1; + let message = MessageTrait::default(); + metadata.append_u16(variant); + assert_eq!(post_dispatch_hook.quote_dispatch(metadata, message), PROTOCOL_FEE); +} + + +#[test] +#[should_panic(expected: ('Invalid metadata variant',))] +fn test_quote_dispatch_fails_if_invalid_variant() { + let (_, post_dispatch_hook) = setup_protocol_fee(); + let mut metadata = BytesTrait::new_empty(); + let variant = 2; + metadata.append_u16(variant); + let message = MessageTrait::default(); + post_dispatch_hook.quote_dispatch(metadata, message); +} diff --git a/starknet/cairo/crates/contracts/tests/isms/test_aggregation.cairo b/starknet/cairo/crates/contracts/tests/isms/test_aggregation.cairo new file mode 100644 index 00000000000..317302d7fcf --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/isms/test_aggregation.cairo @@ -0,0 +1,108 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::{ + ModuleType, IAggregationDispatcher, IAggregationDispatcherTrait, + IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IValidatorConfigurationDispatcher, IValidatorConfigurationDispatcherTrait, +}; +use contracts::isms::aggregation::aggregation; +use contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; +use contracts::utils::utils::U256TryIntoContractAddress; + +use openzeppelin::access::ownable::OwnableComponent; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::{start_prank, CheatTarget}; +use starknet::ContractAddress; +use super::super::setup::{ + setup_aggregation, OWNER, setup_messageid_multisig_ism, get_message_and_signature, LOCAL_DOMAIN, + DESTINATION_DOMAIN, build_messageid_metadata, VALID_OWNER, VALID_RECIPIENT, setup_noop_ism, + MODULES, CONTRACT_MODULES +}; + +#[test] +fn test_aggregation_module_type() { + let threshold = 2; + let aggregation = setup_aggregation(MODULES(), threshold); + assert( + aggregation.module_type() == ModuleType::AGGREGATION(aggregation.contract_address), + 'Aggregation: Wrong module type' + ); +} + +#[test] +#[should_panic] +fn test_aggregation_initialize_with_too_many_modules() { + let threshold = 2; + let mut modules = array![]; + let mut cur_idx = 0; + loop { + if (cur_idx == 256) { + break; + } + modules.append('module_1'.into()); + cur_idx += 1; + }; + setup_aggregation(modules.span(), threshold); +} + + +#[test] +#[should_panic] +fn test_setup_aggregation_with_null_module_address() { + let threshold = 2; + let modules: Span = array![0, 'module_1'].span(); + setup_aggregation(modules, threshold); +} + +#[test] +fn test_get_modules() { + let threshold = 2; + let aggregation = setup_aggregation(MODULES(), threshold); + let ownable = IOwnableDispatcher { contract_address: aggregation.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + assert(aggregation.get_modules() == CONTRACT_MODULES(), 'set modules failed'); +} + + +#[test] +fn test_aggregation_verify() { + let threshold = 2; + + // MESSAGEID + + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_message_and_signature(); + let (messageid, _) = setup_messageid_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let root: u256 = 'root'.try_into().unwrap(); + let index = 1; + let message_id_metadata = build_messageid_metadata(origin_merkle_tree, root, index); + // Noop ism + let noop_ism = setup_noop_ism(); + let aggregation = setup_aggregation( + array![messageid.contract_address.into(), noop_ism.contract_address.into(),].span(), + threshold.try_into().unwrap() + ); + let ownable = IOwnableDispatcher { contract_address: aggregation.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let mut concat_metadata = BytesTrait::new_empty(); + concat_metadata.append_u128(0x00000010000001A0000001A0000001A9); + concat_metadata.concat(@message_id_metadata); + // dummy metadata for noop ism + concat_metadata.concat(@message_id_metadata); + assert(aggregation.verify(concat_metadata, message), 'Aggregation: verify failed'); +} + diff --git a/starknet/cairo/crates/contracts/tests/isms/test_default_ism.cairo b/starknet/cairo/crates/contracts/tests/isms/test_default_ism.cairo new file mode 100644 index 00000000000..00f350e184d --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/isms/test_default_ism.cairo @@ -0,0 +1,100 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::{ + ModuleType, IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IMailboxDispatcher, IMailboxDispatcherTrait, IPausableIsmDispatcher, IPausableIsmDispatcherTrait +}; +use contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; +use contracts::utils::utils::U256TryIntoContractAddress; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::{start_prank, CheatTarget}; +use super::super::setup::{ + setup_trusted_relayer_ism, setup_noop_ism, setup_pausable_ism, mock_setup, DESTINATION_DOMAIN, + OWNER, LOCAL_DOMAIN, DESTINATION_MAILBOX +}; + + +#[test] +fn test_verify_noop_ism() { + let noop_ism = setup_noop_ism(); + let message = MessageTrait::default(); + let metadata = BytesTrait::new_empty(); + assert_eq!(noop_ism.verify(metadata, message), true); + assert_eq!(noop_ism.module_type(), ModuleType::NULL(())); +} + + +#[test] +fn test_verify_trusted_relayer_ism() { + let trusted_ism = setup_trusted_relayer_ism(); + let ownable = IOwnableDispatcher { contract_address: DESTINATION_MAILBOX() }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let mailbox = IMailboxDispatcher { contract_address: DESTINATION_MAILBOX() }; + mailbox.set_default_ism(trusted_ism.contract_address); + let (mock_recipient, _) = mock_setup(trusted_ism.contract_address); + // mailbox.set_local_domain(DESTINATION_DOMAIN); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let recipient: felt252 = mock_recipient.contract_address.into(); + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: recipient.into(), + body: message_body.clone() + }; + let metadata = message_body; + mailbox.process(metadata.clone(), message.clone()); + assert_eq!(trusted_ism.verify(metadata, message), true); + assert_eq!(trusted_ism.module_type(), ModuleType::NULL(())); +} + + +#[test] +fn test_pause_unpause_pausable_ism() { + let (_, pausable_ism) = setup_pausable_ism(); + let ownable = IOwnableDispatcher { contract_address: pausable_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + pausable_ism.pause(); + pausable_ism.unpause(); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_pause_pausable_ism_fails_if_not_owner() { + let (_, pausable_ism) = setup_pausable_ism(); + pausable_ism.pause(); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_unpause_pausable_ism_fails_if_not_owner() { + let (_, pausable_ism) = setup_pausable_ism(); + pausable_ism.unpause(); +} + +#[test] +fn test_verify_pausable_ism() { + let (pausable_ism, _) = setup_pausable_ism(); + let message = MessageTrait::default(); + let metadata = BytesTrait::new_empty(); + assert_eq!(pausable_ism.verify(metadata, message), true); + assert_eq!(pausable_ism.module_type(), ModuleType::NULL(())); +} + +#[test] +#[should_panic(expected: ('Pausable: paused',))] +fn test_veriy_pausable_ism_fails_if_paused() { + let (ism, pausable) = setup_pausable_ism(); + let message = MessageTrait::default(); + let metadata = BytesTrait::new_empty(); + let ownable = IOwnableDispatcher { contract_address: pausable.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + pausable.pause(); + ism.verify(metadata, message); +} diff --git a/starknet/cairo/crates/contracts/tests/isms/test_merkleroot_multisig.cairo b/starknet/cairo/crates/contracts/tests/isms/test_merkleroot_multisig.cairo new file mode 100644 index 00000000000..6e7ec8d61f2 --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/isms/test_merkleroot_multisig.cairo @@ -0,0 +1,237 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::IMessageRecipientDispatcherTrait; +use contracts::interfaces::{ + IMailboxDispatcher, IMailboxDispatcherTrait, ModuleType, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, IInterchainSecurityModule, + IValidatorConfigurationDispatcher, IValidatorConfigurationDispatcherTrait, +}; +use contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; +use contracts::libs::multisig::merkleroot_ism_metadata::merkleroot_ism_metadata::MerkleRootIsmMetadata; +use contracts::utils::utils::U256TryIntoContractAddress; +use core::array::ArrayTrait; +use core::array::SpanTrait; +use core::option::OptionTrait; +use openzeppelin::access::ownable::OwnableComponent; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::cheatcodes::events::EventAssertions; +use snforge_std::{start_prank, CheatTarget}; +use super::super::setup::{ + setup_merkleroot_multisig_ism, OWNER, NEW_OWNER, VALIDATOR_ADDRESS_1, VALIDATOR_ADDRESS_2, + get_merkle_message_and_signature, LOCAL_DOMAIN, DESTINATION_DOMAIN, TEST_PROOF, + build_merkle_metadata, VALID_OWNER, VALID_RECIPIENT, build_fake_merkle_metadata +}; + + +#[test] +fn test_set_validators() { + let threshold = 2; + let new_validators: Array = array![ + VALIDATOR_ADDRESS_1().into(), VALIDATOR_ADDRESS_2().into() + ]; + let (_, validators) = setup_merkleroot_multisig_ism(new_validators.span(), threshold); + let ownable = IOwnableDispatcher { contract_address: validators.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let validators_span = validators.get_validators(); + assert_eq!(*validators_span.at(0).into(), (*new_validators.at(0)).try_into().unwrap()); + assert_eq!(*validators_span.at(1).into(), (*new_validators.at(1)).try_into().unwrap()); +} + + +#[test] +fn test_set_threshold() { + let threshold = 3; + let (_, validators) = setup_merkleroot_multisig_ism(array!['validator_1'].span(), threshold); + let ownable = IOwnableDispatcher { contract_address: validators.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + assert(validators.get_threshold() == threshold, 'wrong validator threshold'); +} + + +#[test] +#[should_panic] +fn test_set_validators_fails_if_null_validator() { + let threshold = 2; + let new_validators = array![VALIDATOR_ADDRESS_1().into(), 0].span(); + setup_merkleroot_multisig_ism(new_validators, threshold); +} + + +#[test] +fn test_merkleroot_ism_metadata() { + let origin_merkle_tree_hook: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let message_index: u32 = 1; + let signed_index: u32 = 2; + let signed_message_id: u256 = 'signed_message_id'.try_into().unwrap(); + let metadata = build_merkle_metadata( + origin_merkle_tree_hook, message_index, signed_index, signed_message_id + ); + let proof = TEST_PROOF(); + let (_, _, signatures) = get_merkle_message_and_signature(); + assert( + MerkleRootIsmMetadata::origin_merkle_tree_hook(metadata.clone()) == origin_merkle_tree_hook, + 'wrong merkle tree hook' + ); + assert( + MerkleRootIsmMetadata::message_index(metadata.clone()) == message_index, + 'wrong message_index' + ); + assert( + MerkleRootIsmMetadata::signed_index(metadata.clone()) == signed_index, 'wrong signed index' + ); + assert( + MerkleRootIsmMetadata::signed_message_id(metadata.clone()) == signed_message_id, + 'wrong signed_message_id' + ); + assert(MerkleRootIsmMetadata::proof(metadata.clone()) == proof, 'wrong proof'); + let y_parity = 0x01; + let mut cur_idx = 0; + loop { + if (cur_idx == signatures.len()) { + break (); + } + assert( + MerkleRootIsmMetadata::signature_at( + metadata.clone(), cur_idx + ) == (y_parity, *signatures.at(cur_idx).r, *signatures.at(cur_idx).s), + 'wrong signature ' + ); + cur_idx += 1; + } +} + + +#[test] +fn test_merkle_root_multisig_module_type() { + let threshold = 2; + let (merkleroot_ism, _) = setup_merkleroot_multisig_ism( + array!['validator_1'].span(), threshold + ); + assert( + merkleroot_ism + .module_type() == ModuleType::MERKLE_ROOT_MULTISIG(merkleroot_ism.contract_address), + 'Wrong module type' + ); +} + + +#[test] +fn test_merkle_root_multisig_verify_with_4_valid_signatures() { + let threshold = 4; + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_merkle_message_and_signature(); + let (merkleroot_ism, _) = setup_merkleroot_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree_hook: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let message_index: u32 = 1; + let signed_index: u32 = 2; + let signed_message_id: u256 = 'signed_message_id'.try_into().unwrap(); + let metadata = build_merkle_metadata( + origin_merkle_tree_hook, message_index, signed_index, signed_message_id + ); + assert(merkleroot_ism.verify(metadata, message) == true, 'verification failed'); +} + + +#[test] +#[should_panic(expected: ('No match for given signature',))] +fn test_merkle_root_multisig_verify_with_insufficient_valid_signatures() { + let threshold = 4; + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_merkle_message_and_signature(); + let (merkleroot_ism, _) = setup_merkleroot_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree_hook: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let message_index: u32 = 1; + let signed_index: u32 = 2; + let signed_message_id: u256 = 'signed_message_id'.try_into().unwrap(); + let mut metadata = build_merkle_metadata( + origin_merkle_tree_hook, message_index, signed_index, signed_message_id + ); + metadata.update_at(1100, 0); + assert(merkleroot_ism.verify(metadata, message) == true, 'verification failed'); +} + + +#[test] +#[should_panic(expected: ('Empty metadata',))] +fn test_merkle_root_multisig_verify_with_empty_metadata() { + let threshold = 4; + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_merkle_message_and_signature(); + let (merkle_root_ism, _) = setup_merkleroot_multisig_ism(validators_address.span(), threshold); + let bytes_metadata = BytesTrait::new_empty(); + assert(merkle_root_ism.verify(bytes_metadata, message) == true, 'verification failed'); +} + + +#[test] +#[should_panic(expected: ('No match for given signature',))] +fn test_merkle_root_multisig_verify_with_4_valid_signatures_fails_if_duplicate_signatures() { + let threshold = 4; + + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_merkle_message_and_signature(); + let (merkleroot_ism, _) = setup_merkleroot_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree_hook: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let message_index: u32 = 1; + let signed_index: u32 = 2; + let signed_message_id: u256 = 'signed_message_id'.try_into().unwrap(); + let metadata = build_fake_merkle_metadata( + origin_merkle_tree_hook, message_index, signed_index, signed_message_id + ); + assert(merkleroot_ism.verify(metadata, message) == true, 'verification failed'); +} diff --git a/starknet/cairo/crates/contracts/tests/isms/test_messageid_multisig.cairo b/starknet/cairo/crates/contracts/tests/isms/test_messageid_multisig.cairo new file mode 100644 index 00000000000..139781838f2 --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/isms/test_messageid_multisig.cairo @@ -0,0 +1,209 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::IMessageRecipientDispatcherTrait; +use contracts::interfaces::{ + IMailbox, IMailboxDispatcher, IMailboxDispatcherTrait, ModuleType, + IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IInterchainSecurityModule, IValidatorConfigurationDispatcher, + IValidatorConfigurationDispatcherTrait, +}; +use contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; +use contracts::libs::multisig::message_id_ism_metadata::message_id_ism_metadata::MessageIdIsmMetadata; +use contracts::mailbox::mailbox; +use contracts::utils::utils::U256TryIntoContractAddress; +use core::array::ArrayTrait; +use core::array::SpanTrait; +use openzeppelin::access::ownable::OwnableComponent; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::cheatcodes::events::EventAssertions; +use snforge_std::{start_prank, CheatTarget}; +use super::super::setup::{ + setup_messageid_multisig_ism, OWNER, NEW_OWNER, VALIDATOR_ADDRESS_1, VALIDATOR_ADDRESS_2, + get_message_and_signature, LOCAL_DOMAIN, DESTINATION_DOMAIN, RECIPIENT_ADDRESS, + build_messageid_metadata, VALID_OWNER, VALID_RECIPIENT, build_fake_messageid_metadata +}; + + +#[test] +fn test_set_validators() { + let threshold = 2; + let new_validators: Array = array![ + VALIDATOR_ADDRESS_1().into(), VALIDATOR_ADDRESS_2().into() + ]; + let (_, validators) = setup_messageid_multisig_ism(new_validators.span(), threshold); + let ownable = IOwnableDispatcher { contract_address: validators.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let validators_span = validators.get_validators(); + assert_eq!(*validators_span.at(0).into(), (*new_validators.at(0)).try_into().unwrap()); + assert_eq!(*validators_span.at(1).into(), (*new_validators.at(1)).try_into().unwrap()); +} + +#[test] +fn test_set_threshold() { + let threshold = 3; + let (_, validators) = setup_messageid_multisig_ism(array!['validator_1'].span(), threshold); + assert(validators.get_threshold() == threshold, 'wrong validator threshold'); +} + + +#[test] +#[should_panic] +fn test_set_validators_fails_if_null_validator() { + let threshold = 2; + let new_validators: Span = array![VALIDATOR_ADDRESS_1().try_into().unwrap(), 0].span(); + setup_messageid_multisig_ism(new_validators, threshold); +} + + +#[test] +fn test_message_id_ism_metadata() { + let origin_merkle_tree: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let root: u256 = 'root'.try_into().unwrap(); + let y_parity = 0x01; + let index = 1; + let (_, _, signatures) = get_message_and_signature(); + let metadata = build_messageid_metadata(origin_merkle_tree, root, index); + assert( + MessageIdIsmMetadata::origin_merkle_tree_hook(metadata.clone()) == origin_merkle_tree, + 'wrong merkle tree hook' + ); + assert(MessageIdIsmMetadata::root(metadata.clone()) == root, 'wrong root'); + assert(MessageIdIsmMetadata::index(metadata.clone()) == index, 'wrong index'); + let mut cur_idx = 0; + loop { + if (cur_idx == signatures.len()) { + break (); + } + assert( + MessageIdIsmMetadata::signature_at( + metadata.clone(), cur_idx + ) == (y_parity, *signatures.at(cur_idx).r, *signatures.at(cur_idx).s), + 'wrong signature ' + ); + cur_idx += 1; + } +} + + +#[test] +fn test_message_id_multisig_module_type() { + let threshold = 4; + let (messageid, _) = setup_messageid_multisig_ism(array!['validator_1'].span(), threshold); + assert( + messageid.module_type() == ModuleType::MESSAGE_ID_MULTISIG(messageid.contract_address), + 'Wrong module type' + ); +} + + +#[test] +fn test_message_id_multisig_verify_with_4_valid_signatures() { + let threshold = 4; + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_message_and_signature(); + let (messageid, _) = setup_messageid_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let root: u256 = 'root'.try_into().unwrap(); + let index = 1; + let metadata = build_messageid_metadata(origin_merkle_tree, root, index); + assert(messageid.verify(metadata, message) == true, 'verification failed'); +} + + +#[test] +#[should_panic(expected: ('No match for given signature',))] +fn test_message_id_multisig_verify_with_insufficient_valid_signatures() { + let threshold = 4; + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_message_and_signature(); + let (messageid, _) = setup_messageid_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let root: u256 = 'root'.try_into().unwrap(); + let index = 1; + let mut metadata = build_messageid_metadata(origin_merkle_tree, root, index); + // introduce an error for the signature + metadata.update_at(80, 0); + assert(messageid.verify(metadata, message) == true, 'verification failed'); +} + + +#[test] +#[should_panic(expected: ('Empty metadata',))] +fn test_message_id_multisig_verify_with_empty_metadata() { + let threshold = 4; + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_message_and_signature(); + let (messageid, _) = setup_messageid_multisig_ism(validators_address.span(), threshold); + let bytes_metadata = BytesTrait::new_empty(); + assert(messageid.verify(bytes_metadata, message) == true, 'verification failed'); +} + + +#[test] +#[should_panic(expected: ('No match for given signature',))] +fn test_message_id_multisig_verify_with_4_valid_signatures_fails_if_duplicate_signatures() { + let threshold = 4; + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_message_and_signature(); + let (messageid, _) = setup_messageid_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let root: u256 = 'root'.try_into().unwrap(); + let index = 1; + let metadata = build_fake_messageid_metadata(origin_merkle_tree, root, index); + assert(messageid.verify(metadata, message) == true, 'verification failed'); +} diff --git a/starknet/cairo/crates/contracts/tests/lib.cairo b/starknet/cairo/crates/contracts/tests/lib.cairo new file mode 100644 index 00000000000..b290f00a14a --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/lib.cairo @@ -0,0 +1,20 @@ +pub mod setup; +pub mod test_mailbox; +pub mod test_validator_announce; +pub mod isms { + pub mod test_aggregation; + pub mod test_default_ism; + pub mod test_merkleroot_multisig; + pub mod test_messageid_multisig; +} +pub mod hooks { + pub mod test_merkle_tree_hook; + pub mod test_protocol_fee; +} +pub mod routing { + pub mod test_default_fallback_routing_ism; + pub mod test_domain_routing_ism; +} +pub mod libs { + pub mod test_enumerable_map; +} diff --git a/starknet/cairo/crates/contracts/tests/libs/test_enumerable_map.cairo b/starknet/cairo/crates/contracts/tests/libs/test_enumerable_map.cairo new file mode 100644 index 00000000000..c6f15abfe05 --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/libs/test_enumerable_map.cairo @@ -0,0 +1,112 @@ +use contracts::libs::enumerable_map::{EnumerableMapTrait}; +use mocks::enumerable_map_holder::{ + IEnumerableMapHolderDispatcher, IEnumerableMapHolderDispatcherTrait +}; +use snforge_std::{declare, ContractClassTrait}; +use starknet::{ClassHash, ContractAddress}; + + +fn setup() -> IEnumerableMapHolderDispatcher { + let contract = declare("EnumerableMapHolder").unwrap(); + let (contract_address, _) = contract.deploy(@array![]).unwrap(); + IEnumerableMapHolderDispatcher { contract_address } +} + +#[test] +fn test_initialize_empty_map() { + let contract = setup(); + assert_eq!(contract.do_get_len(), 0, "EnumerableMap is not empty"); +} + +#[test] +fn test_fuzz_set(key: u32, val: u256) { + let mut contract = setup(); + assert_eq!(contract.do_get_len(), 0, "EnumerableMap is not empty"); + contract.do_set_key(key, val); + // check len increased + assert_eq!(contract.do_get_len(), 1, "EnumerableMap is empty"); + // check value stored in 'values' map correctly and test get method + assert_eq!(contract.do_get_value(key), val, "Value not stored properly"); + // check value key correctly stored in keys array and test at method + let (_key, _value) = contract.do_at(0); + assert_eq!(key, key, "Key mismatch"); + assert_eq!(_value, val, "Value mismatch"); + // check if its been correctly setted in 'positions' mapping + assert!(contract.do_contains(key), "Key not registered to positions mapping"); +} + +#[test] +fn test_fuzz_contains(key: u32, val: u256, should_contain: u8) { + let mut contract = setup(); + let should_contain: bool = should_contain % 2 == 1; + if should_contain { + contract.do_set_key(key, val); + } + assert_eq!(contract.do_contains(key), should_contain); +} + +#[test] +fn test_fuzz_should_remove(key: u32, val: u256) { + let mut contract = setup(); + contract.do_set_key(key, val); + // check len increased + assert_eq!(contract.do_get_len(), 1, "EnumerableMap is empty"); + // check value stored in 'values' map correctly + assert_eq!(contract.do_get_value(key), val, "Value not stored properly"); + // check value key correctly stored in keys array + let (_key, _value) = contract.do_at(0); + assert_eq!(key, key, "Key mismatch"); + assert_eq!(_value, val, "Value mismatch"); + // check if its been correctly setted in 'positions' mapping + assert!(contract.do_contains(key), "Key not registered to positions mapping"); + assert!(contract.do_remove(key), "Failed to remove element"); + // check len decreased + assert_eq!(contract.do_get_len(), 0, "EnumerableMap len not decreased"); + // check if its been correctly removed in 'positions' mapping + assert!(!contract.do_contains(key), "Key not removed from positions mapping"); +} + +#[test] +fn test_fuzz_get_keys( + mut key1: u32, mut key2: u32, mut key3: u32, val1: u256, val2: u256, val3: u256 +) { + if key1 == key2 { + key2 += 1; + } + if key1 == key3 { + key3 += 1; + } + if key2 == key3 { + key3 += 1; + } + let keys_to_add: Span = array![key1, key2, key3].span(); + let values_to_add: Span = array![val1, val2, val3].span(); + let mut contract = setup(); + let mut i = 0; + let len = keys_to_add.len(); + while i < len { + contract.do_set_key(*keys_to_add.at(i), *values_to_add.at(i)); + i += 1; + }; + assert_eq!(contract.do_get_len(), len, "Length mismatch"); + let keys = contract.do_get_keys(); + let mut i = 0; + while i < len { + assert_eq!(*keys.at(i), *keys_to_add.at(i), "key mismatch"); + i += 1; + }; + + // remove the middle elem and get again + contract.do_remove(key2); + assert!(!contract.do_contains(key2), "Key2 not removed from positions mapping"); + let expected_keys: Span = array![key1, key3].span(); + assert_eq!(contract.do_get_len(), expected_keys.len(), "Length mismatch"); + + let keys = contract.do_get_keys(); + let len = keys.len(); + let mut i = 0; + while i < len { + assert_eq!(*keys.at(i), *expected_keys.at(i), "key mismatch"); + i += 1; + }; +} diff --git a/starknet/cairo/crates/contracts/tests/routing/test_default_fallback_routing_ism.cairo b/starknet/cairo/crates/contracts/tests/routing/test_default_fallback_routing_ism.cairo new file mode 100644 index 00000000000..c5037ee22f3 --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/routing/test_default_fallback_routing_ism.cairo @@ -0,0 +1,322 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::{ + ModuleType, IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IRoutingIsmDispatcher, IRoutingIsmDispatcherTrait, IDomainRoutingIsmDispatcher, + IDomainRoutingIsmDispatcherTrait, IValidatorConfigurationDispatcher, + IValidatorConfigurationDispatcherTrait, IMailboxClientDispatcher, IMailboxClientDispatcherTrait, + IMailboxDispatcher, IMailboxDispatcherTrait +}; +use contracts::libs::message::{Message, HYPERLANE_VERSION}; +use contracts::utils::utils::U256TryIntoContractAddress; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::{start_prank, CheatTarget, ContractClassTrait, declare}; +use starknet::{ContractAddress, contract_address_const}; +use super::super::setup::{ + OWNER, setup_default_fallback_routing_ism, build_messageid_metadata, LOCAL_DOMAIN, VALID_OWNER, + VALID_RECIPIENT, DESTINATION_DOMAIN, setup_messageid_multisig_ism, get_message_and_signature, + setup_mailbox, MAILBOX +}; + +#[test] +fn test_initialize() { + let _domains: Array = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + assert(fallback_routing_ism.domains() == _domains.span(), 'wrong domains init'); + let mut cur_idx = 0; + loop { + if (cur_idx == _domains.len()) { + break (); + } + assert_eq!(*_modules.at(cur_idx), fallback_routing_ism.module(*_domains.at(cur_idx))); + cur_idx += 1; + } +} + + +#[test] +fn get_default_module() { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let default_fallback_routing_ism = declare("default_fallback_routing_ism").unwrap(); + let params: Array = array![ + OWNER().try_into().unwrap(), mailbox.contract_address.into() + ]; + let (default_fallback_routing_ism_addr, _) = default_fallback_routing_ism + .deploy(@params) + .unwrap(); + let test_address = contract_address_const::<0x1331341>(); + let dispatcher = IDomainRoutingIsmDispatcher { + contract_address: default_fallback_routing_ism_addr + }; + let mailbox = IMailboxDispatcher { contract_address: mailbox.contract_address }; + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + mailbox.set_default_ism(test_address); + assert_eq!(dispatcher.module(1234), test_address); +} +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_initialize_fails_if_caller_not_owner() { + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + fallback_routing_ism.initialize(_domains.span(), _modules.span()) +} + +#[test] +#[should_panic(expected: ('Module cannot be zero',))] +fn test_initialize_fails_if_module_is_zero() { + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0>() + ]; + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()) +} + +#[test] +#[should_panic(expected: ('Length mismatch',))] +fn test_initialize_fails_if_length_mismatch() { + let _domains = array![12345, 1123322, 312441, 131321]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); +} + + +#[test] +fn test_remove_domain() { + let mut _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + fallback_routing_ism.remove(12345); + _domains.pop_front().unwrap(); + assert(_domains.span() == fallback_routing_ism.domains(), 'wrong domain del'); +} +#[test] +fn test_remove_domain_module_check() { + let mut _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + fallback_routing_ism.remove(12345); + let mailbox_dispatcher = IMailboxDispatcher { contract_address: MAILBOX() }; + assert_eq!(fallback_routing_ism.module(12345), mailbox_dispatcher.get_default_ism()); +} + + +#[test] +#[should_panic(expected: ('Domain not found',))] +fn test_remove_domain_fails_if_domain_not_found() { + let _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + fallback_routing_ism.remove(1); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_remove_domain_fails_if_caller_not_owner() { + let _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + fallback_routing_ism.remove(12345); +} + + +#[test] +fn test_set_domain_and_module() { + let mut _domains = array![12345, 1123322, 312441]; + let mut _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + let new_domain = 111111; + let new_module = contract_address_const::<0x2134242342342>(); + fallback_routing_ism.set(new_domain, new_module); + _domains.append(new_domain); + _modules.append(new_module); + assert(_domains.span() == fallback_routing_ism.domains(), 'wrong domain add'); + let mut cur_idx = 0; + loop { + if (cur_idx == _domains.len()) { + break (); + } + assert_eq!(*_modules.at(cur_idx), fallback_routing_ism.module(*_domains.at(cur_idx))); + cur_idx += 1; + } +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_domain_and_module_fails_if_caller_is_not_owner() { + let mut _domains = array![12345, 1123322, 312441]; + let mut _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + let new_domain = 111111; + let new_module = contract_address_const::<0x2134242342342>(); + fallback_routing_ism.set(new_domain, new_module); +} + + +#[test] +fn test_route_ism() { + let mut message = Message { + version: 3_u8, + nonce: 0_u32, + origin: 12345, + sender: 'SENDER'.try_into().unwrap(), + destination: 0_u32, + recipient: 'RECIPIENT'.try_into().unwrap(), + body: BytesTrait::new_empty(), + }; + let mut _domains = array![12345, 1123322, 312441]; + let mut _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, ism, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + assert_eq!(ism.route(message), *_modules.at(0)); + message = + Message { + version: 3_u8, + nonce: 0_u32, + origin: 1123322, + sender: 'SENDER'.try_into().unwrap(), + destination: 0_u32, + recipient: 'RECIPIENT'.try_into().unwrap(), + body: BytesTrait::new_empty(), + }; + assert_eq!(ism.route(message), *_modules.at(1)); + message = + Message { + version: 3_u8, + nonce: 0_u32, + origin: 312441, + sender: 'SENDER'.try_into().unwrap(), + destination: 0_u32, + recipient: 'RECIPIENT'.try_into().unwrap(), + body: BytesTrait::new_empty(), + }; + assert_eq!(ism.route(message), *_modules.at(2)); + message = + Message { + version: 3_u8, + nonce: 0_u32, + origin: 1, + sender: 'SENDER'.try_into().unwrap(), + destination: 0_u32, + recipient: 'RECIPIENT'.try_into().unwrap(), + body: BytesTrait::new_empty(), + }; + let mailbox_dispatcher = IMailboxDispatcher { contract_address: MAILBOX() }; + assert_eq!(ism.route(message), mailbox_dispatcher.get_default_ism()); +} + + +#[test] +fn test_module_type() { + let (ism, _, _) = setup_default_fallback_routing_ism(); + assert_eq!(ism.module_type(), ModuleType::ROUTING(ism.contract_address)); +} + +// for this test, we will reuse existing tests +#[test] +fn test_verify() { + let threshold = 4; + // ISM MESSAGE AND METADATA CONFIGURATION + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_message_and_signature(); + let (messageid, _) = setup_messageid_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let root: u256 = 'root'.try_into().unwrap(); + let index = 1; + let metadata = build_messageid_metadata(origin_merkle_tree, root, index); + + // ROUTING TESTING + let mut _domains = array![LOCAL_DOMAIN, 1123322, 312441]; + let mut _modules: Array = array![ + messageid.contract_address, + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (ism, _, fallback_routing_ism) = setup_default_fallback_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: fallback_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + fallback_routing_ism.initialize(_domains.span(), _modules.span()); + assert_eq!(ism.verify(metadata, message), true); +} diff --git a/starknet/cairo/crates/contracts/tests/routing/test_domain_routing_ism.cairo b/starknet/cairo/crates/contracts/tests/routing/test_domain_routing_ism.cairo new file mode 100644 index 00000000000..915a5d0acef --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/routing/test_domain_routing_ism.cairo @@ -0,0 +1,322 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::{ + ModuleType, IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IRoutingIsmDispatcher, IRoutingIsmDispatcherTrait, IDomainRoutingIsmDispatcher, + IDomainRoutingIsmDispatcherTrait, IValidatorConfigurationDispatcher, + IValidatorConfigurationDispatcherTrait, IMailboxClientDispatcher, IMailboxClientDispatcherTrait, + IMailboxDispatcher, IMailboxDispatcherTrait +}; +use contracts::libs::message::{Message, HYPERLANE_VERSION}; +use contracts::utils::utils::U256TryIntoContractAddress; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::{start_prank, CheatTarget, stop_prank, ContractClassTrait}; +use starknet::{ContractAddress, contract_address_const}; +use super::super::setup::{ + OWNER, setup_domain_routing_ism, build_messageid_metadata, LOCAL_DOMAIN, DESTINATION_DOMAIN, + setup_messageid_multisig_ism, get_message_and_signature, VALID_OWNER, VALID_RECIPIENT +}; + + +#[test] +fn test_initialize() { + let _domains: Array = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + assert(domain_routing_ism.domains() == _domains.span(), 'wrong domains init'); + let mut cur_idx = 0; + loop { + if (cur_idx == _domains.len()) { + break (); + } + assert_eq!(*_modules.at(cur_idx), domain_routing_ism.module(*_domains.at(cur_idx))); + cur_idx += 1; + } +} + + +#[test] +#[should_panic(expected: ('Origin not found',))] +fn get_module_fails_if_origin_not_found() { + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + domain_routing_ism.module(1233); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_initialize_fails_if_caller_not_owner() { + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + let _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + domain_routing_ism.initialize(_domains.span(), _modules.span()) +} + +#[test] +#[should_panic(expected: ('Module cannot be zero',))] +fn test_initialize_fails_if_module_is_zero() { + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + let _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0>() + ]; + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()) +} + +#[test] +#[should_panic(expected: ('Length mismatch',))] +fn test_initialize_fails_if_length_mismatch() { + let _domains = array![12345, 1123322, 312441, 131321]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); +} + + +#[test] +fn test_remove_domain() { + let mut _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + domain_routing_ism.remove(12345); + _domains.pop_front().unwrap(); + assert(_domains.span() == domain_routing_ism.domains(), 'wrong domain del'); +} + +#[test] +#[should_panic(expected: ('Origin not found',))] +fn test_remove_domain_check_module() { + let mut _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + domain_routing_ism.remove(12345); + domain_routing_ism.module(12345); +} + +#[test] +#[should_panic(expected: ('Domain not found',))] +fn test_remove_domain_fails_if_domain_not_found() { + let _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + domain_routing_ism.remove(1); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_remove_domain_fails_if_caller_not_owner() { + let _domains = array![12345, 1123322, 312441]; + let _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + domain_routing_ism.remove(12345); +} + + +#[test] +fn test_set_domain_and_module() { + let mut _domains = array![12345, 1123322, 312441]; + let mut _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + let new_domain = 111111; + let new_module = contract_address_const::<0x2134242342342>(); + domain_routing_ism.set(new_domain, new_module); + _domains.append(new_domain); + _modules.append(new_module); + assert(_domains.span() == domain_routing_ism.domains(), 'wrong domain add'); + let mut cur_idx = 0; + loop { + if (cur_idx == _domains.len()) { + break (); + } + assert_eq!(*_modules.at(cur_idx), domain_routing_ism.module(*_domains.at(cur_idx))); + cur_idx += 1; + } +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_domain_and_module_fails_if_caller_is_not_owner() { + let mut _domains = array![12345, 1123322, 312441]; + let mut _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, _, domain_routing_ism) = setup_domain_routing_ism(); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + let new_domain = 111111; + let new_module = contract_address_const::<0x2134242342342>(); + domain_routing_ism.set(new_domain, new_module); +} + + +#[test] +fn test_route_ism() { + let mut message = Message { + version: 3_u8, + nonce: 0_u32, + origin: 12345, + sender: 'SENDER'.try_into().unwrap(), + destination: 0_u32, + recipient: 'RECIPIENT'.try_into().unwrap(), + body: BytesTrait::new_empty(), + }; + let mut _domains = array![12345, 1123322, 312441]; + let mut _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, ism, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + assert_eq!(ism.route(message), *_modules.at(0)); + message = + Message { + version: 3_u8, + nonce: 0_u32, + origin: 1123322, + sender: 'SENDER'.try_into().unwrap(), + destination: 0_u32, + recipient: 'RECIPIENT'.try_into().unwrap(), + body: BytesTrait::new_empty(), + }; + assert_eq!(ism.route(message), *_modules.at(1)); + message = + Message { + version: 3_u8, + nonce: 0_u32, + origin: 312441, + sender: 'SENDER'.try_into().unwrap(), + destination: 0_u32, + recipient: 'RECIPIENT'.try_into().unwrap(), + body: BytesTrait::new_empty(), + }; + assert_eq!(ism.route(message), *_modules.at(2)); +} + +#[test] +#[should_panic(expected: ('Origin not found',))] +fn test_route_ism_fails_if_origin_not_found() { + let mut message = Message { + version: 3_u8, + nonce: 0_u32, + origin: 1, + sender: 'SENDER'.try_into().unwrap(), + destination: 0_u32, + recipient: 'RECIPIENT'.try_into().unwrap(), + body: BytesTrait::new_empty(), + }; + let mut _domains = array![12345, 1123322, 312441]; + let mut _modules: Array = array![ + contract_address_const::<0x111>(), + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (_, ism, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + ism.route(message); +} + + +#[test] +fn test_module_type() { + let (ism, _, _) = setup_domain_routing_ism(); + assert_eq!(ism.module_type(), ModuleType::ROUTING(ism.contract_address)); +} + +// for this test, we will reuse existing tests +#[test] +fn test_verify() { + let threshold = 4; + // ISM MESSAGE AND METADATA CONFIGURATION + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: VALID_OWNER(), + destination: DESTINATION_DOMAIN, + recipient: VALID_RECIPIENT(), + body: message_body.clone() + }; + let (_, validators_address, _) = get_message_and_signature(); + let (messageid, _) = setup_messageid_multisig_ism(validators_address.span(), threshold); + let origin_merkle_tree: u256 = 'origin_merkle_tree_hook'.try_into().unwrap(); + let root: u256 = 'root'.try_into().unwrap(); + let index = 1; + let metadata = build_messageid_metadata(origin_merkle_tree, root, index); + + // ROUTING TESTING + let mut _domains = array![LOCAL_DOMAIN, 1123322, 312441]; + let mut _modules: Array = array![ + messageid.contract_address, + contract_address_const::<0x222>(), + contract_address_const::<0x333>() + ]; + let (ism, _, domain_routing_ism) = setup_domain_routing_ism(); + let ownable = IOwnableDispatcher { contract_address: domain_routing_ism.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + domain_routing_ism.initialize(_domains.span(), _modules.span()); + assert_eq!(ism.verify(metadata, message), true); +} diff --git a/starknet/cairo/crates/contracts/tests/setup.cairo b/starknet/cairo/crates/contracts/tests/setup.cairo new file mode 100644 index 00000000000..a2235193385 --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/setup.cairo @@ -0,0 +1,585 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::{ + IMailboxDispatcher, IMailboxDispatcherTrait, IMessageRecipientDispatcher, + IMessageRecipientDispatcherTrait, IInterchainSecurityModule, + IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IValidatorAnnounceDispatcher, IValidatorAnnounceDispatcherTrait, IMailboxClientDispatcher, + IMailboxClientDispatcherTrait, IAggregationDispatcher, IAggregationDispatcherTrait, + IValidatorConfigurationDispatcher, IMerkleTreeHookDispatcher, IMerkleTreeHookDispatcherTrait, + IPostDispatchHookDispatcher, IProtocolFeeDispatcher, IPostDispatchHookDispatcherTrait, + IProtocolFeeDispatcherTrait, IMockValidatorAnnounceDispatcher, + ISpecifiesInterchainSecurityModuleDispatcher, ISpecifiesInterchainSecurityModuleDispatcherTrait, + IRoutingIsmDispatcher, IRoutingIsmDispatcherTrait, IDomainRoutingIsmDispatcher, + IDomainRoutingIsmDispatcherTrait, IPausableIsmDispatcher, IPausableIsmDispatcherTrait, + ETH_ADDRESS +}; +use contracts::libs::multisig::merkleroot_ism_metadata::merkleroot_ism_metadata::MERKLE_PROOF_ITERATION; +use openzeppelin::account::utils::signature::EthSignature; +use openzeppelin::token::erc20::interface::{ERC20ABI, ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use snforge_std::{ + declare, ContractClassTrait, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn +}; +use starknet::{ContractAddress, contract_address_const, EthAddress}; + +pub const LOCAL_DOMAIN: u32 = 534352; +pub const DESTINATION_DOMAIN: u32 = 9841001; +pub const MAX_PROTOCOL_FEE: u256 = 1000000000; +pub const PROTOCOL_FEE: u256 = 1000000; +pub const INITIAL_SUPPLY: u256 = 10000000000; + + +pub fn OWNER() -> u256 { + 'OWNER'.into() +} + +pub fn NEW_OWNER() -> u256 { + 'NEW_OWNER'.into() +} + +pub fn VALID_OWNER() -> u256 { + 0x1a4bcca63b5e8a46da3abe2080f75c16c18467d5838f00b375d9ba4c7c313dd +} + +pub fn VALID_RECIPIENT() -> u256 { + 0x1d35915d0abec0a28990198bb32aa570e681e7eb41a001c0094c7c36a712671 +} + +pub fn DEFAULT_ISM() -> ContractAddress { + contract_address_const::<'DEFAULT_ISM'>() +} + +pub fn DEFAULT_HOOK() -> ContractAddress { + contract_address_const::<'DEFAULT_HOOK'>() +} + +pub fn REQUIRED_HOOK() -> ContractAddress { + contract_address_const::<'REQUIRED_HOOK'>() +} + +pub fn NEW_DEFAULT_ISM() -> ContractAddress { + contract_address_const::<'NEW_DEFAULT_ISM'>() +} + +pub fn NEW_DEFAULT_HOOK() -> ContractAddress { + contract_address_const::<'NEW_DEFAULT_HOOK'>() +} + +pub fn NEW_REQUIRED_HOOK() -> ContractAddress { + contract_address_const::<'NEW_REQUIRED_HOOK'>() +} + +pub fn RECIPIENT_ADDRESS() -> u256 { + 'RECIPIENT_ADDRESS'.into() +} + +pub fn VALIDATOR_ADDRESS_1() -> EthAddress { + 'VALIDATOR_ADDRESS_1'.try_into().unwrap() +} + +pub fn VALIDATOR_ADDRESS_2() -> EthAddress { + 'VALIDATOR_ADDRESS_2'.try_into().unwrap() +} + +pub fn BENEFICIARY() -> ContractAddress { + 'BENEFICIARY'.try_into().unwrap() +} + +pub fn MAILBOX() -> ContractAddress { + 'MAILBOX'.try_into().unwrap() +} + +pub fn DESTINATION_MAILBOX() -> ContractAddress { + 'DESTINATION_MAILBOX'.try_into().unwrap() +} + +pub fn MAILBOX_CLIENT() -> ContractAddress { + 'MAILBOX_CLIENT'.try_into().unwrap() +} +pub fn MODULES() -> Span { + array!['module_1', 'module_2'].span() +} + +pub fn CONTRACT_MODULES() -> Span { + let module_1 = 'module_1'.try_into().unwrap(); + let module_2 = 'module_2'.try_into().unwrap(); + array![module_1, module_2].span() +} +pub fn TEST_PROOF() -> Span { + array![ + 0x09020304050607080910111213141516, + 0x01020304050607080920111213141516, + 0x01020304050607080910000000000000, + 0x02010304050607080910111213141516, + 0x03000000000000000000000000000000, + 0x09020304050607080910111213141516, + 0x01020304050607080920111213141516, + 0x01020304050607080910000000000000, + 0x02010304050607080910111213141516, + 0x03000000000000000000000000000000, + 0x09020304050607080910111213141516, + 0x01020304050607080920111213141516, + 0x01020304050607080910000000000000, + 0x02010304050607080910111213141516, + 0x03000000000000000000000000000000, + 0x09020304050607080910111213141516, + 0x01020304050607080920111213141516, + 0x01020304050607080910000000000000, + 0x02010304050607080910111213141516, + 0x03000000000000000000000000000000, + 0x09020304050607080910111213141516, + 0x01020304050607080920111213141516, + 0x01020304050607080910000000000000, + 0x02010304050607080910111213141516, + 0x03000000000000000000000000000000, + 0x09020304050607080910111213141516, + 0x01020304050607080920111213141516, + 0x01020304050607080910000000000000, + 0x02010304050607080910111213141516, + 0x03000000000000000000000000000000, + 0x09020304050607080910111213141516, + 0x01020304050607080920111213141516, + ] + .span() +} +pub fn setup_mailbox( + mailbox_address: ContractAddress, + _required_hook: Option, + _default_mock_hook: Option +) -> ( + IMailboxDispatcher, EventSpy, IPostDispatchHookDispatcher, IInterchainSecurityModuleDispatcher +) { + let domain = if (mailbox_address == MAILBOX()) { + LOCAL_DOMAIN + } else { + DESTINATION_DOMAIN + }; + let mailbox_class = declare("mailbox").unwrap(); + let required_hook = match _required_hook { + Option::Some(address) => address, + Option::None => { + let mock_hook_dispatcher = setup_mock_hook(); + mock_hook_dispatcher.contract_address + } + }; + let default_hook = match _default_mock_hook { + Option::Some(address) => address, + Option::None => { required_hook } + }; + let mock_ism = setup_mock_ism(); + setup_mock_token(); + let params: Array = array![ + domain.into(), + OWNER().try_into().unwrap(), + mock_ism.contract_address.into(), + default_hook.into(), + required_hook.into() + ]; + mailbox_class.deploy_at(@params, mailbox_address).unwrap(); + let mut spy = spy_events(SpyOn::One(mailbox_address)); + ( + IMailboxDispatcher { contract_address: mailbox_address }, + spy, + IPostDispatchHookDispatcher { contract_address: required_hook }, + mock_ism + ) +} + +pub fn mock_setup( + mock_ism_address: ContractAddress +) -> (IMessageRecipientDispatcher, ISpecifiesInterchainSecurityModuleDispatcher,) { + let message_recipient_class = declare("message_recipient").unwrap(); + let (message_recipient_addr, _) = message_recipient_class + .deploy(@array![mock_ism_address.into()]) + .unwrap(); + ( + IMessageRecipientDispatcher { contract_address: message_recipient_addr }, + ISpecifiesInterchainSecurityModuleDispatcher { contract_address: message_recipient_addr }, + ) +} + +pub fn setup_mock_ism() -> IInterchainSecurityModuleDispatcher { + let mock_ism = declare("ism").unwrap(); + let (mock_ism_addr, _) = mock_ism.deploy(@array![]).unwrap(); + IInterchainSecurityModuleDispatcher { contract_address: mock_ism_addr } +} +pub fn setup_messageid_multisig_ism( + validators: Span, threshold: u32 +) -> (IInterchainSecurityModuleDispatcher, IValidatorConfigurationDispatcher) { + let messageid_multisig_class = declare("messageid_multisig_ism").unwrap(); + let mut parameters = Default::default(); + let owner: felt252 = OWNER().try_into().unwrap(); + Serde::serialize(@owner, ref parameters); + Serde::serialize(@validators, ref parameters); + Serde::serialize(@threshold, ref parameters); + let (messageid_multisig_addr, _) = messageid_multisig_class.deploy(@parameters).unwrap(); + ( + IInterchainSecurityModuleDispatcher { contract_address: messageid_multisig_addr }, + IValidatorConfigurationDispatcher { contract_address: messageid_multisig_addr } + ) +} + + +pub fn setup_merkleroot_multisig_ism( + validators: Span, threshold: u32 +) -> (IInterchainSecurityModuleDispatcher, IValidatorConfigurationDispatcher) { + let merkleroot_multisig_class = declare("merkleroot_multisig_ism").unwrap(); + let mut parameters = Default::default(); + let owner: felt252 = OWNER().try_into().unwrap(); + Serde::serialize(@owner, ref parameters); + Serde::serialize(@validators, ref parameters); + Serde::serialize(@threshold, ref parameters); + let (merkleroot_multisig_addr, _) = merkleroot_multisig_class.deploy(@parameters).unwrap(); + ( + IInterchainSecurityModuleDispatcher { contract_address: merkleroot_multisig_addr }, + IValidatorConfigurationDispatcher { contract_address: merkleroot_multisig_addr } + ) +} + + +pub fn setup_mailbox_client() -> IMailboxClientDispatcher { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let mailboxclient_class = declare("mailboxclient").unwrap(); + let params: Array = array![ + mailbox.contract_address.into(), OWNER().try_into().unwrap() + ]; + let res = mailboxclient_class.deploy_at(@params, MAILBOX_CLIENT()); + if (res.is_err()) { + panic(res.unwrap_err()); + } + let (mailboxclient_addr, _) = res.unwrap(); + IMailboxClientDispatcher { contract_address: mailboxclient_addr } +} + +pub fn setup_default_fallback_routing_ism() -> ( + IInterchainSecurityModuleDispatcher, IRoutingIsmDispatcher, IDomainRoutingIsmDispatcher +) { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let default_fallback_routing_ism = declare("default_fallback_routing_ism").unwrap(); + let params = array![OWNER().try_into().unwrap(), mailbox.contract_address.into()]; + let (default_fallback_routing_ism_addr, _) = default_fallback_routing_ism + .deploy(@params) + .unwrap(); + ( + IInterchainSecurityModuleDispatcher { contract_address: default_fallback_routing_ism_addr }, + IRoutingIsmDispatcher { contract_address: default_fallback_routing_ism_addr }, + IDomainRoutingIsmDispatcher { contract_address: default_fallback_routing_ism_addr } + ) +} + +pub fn setup_domain_routing_ism() -> ( + IInterchainSecurityModuleDispatcher, IRoutingIsmDispatcher, IDomainRoutingIsmDispatcher +) { + let domain_routing_ism = declare("domain_routing_ism").unwrap(); + let (domain_routing_ism_addr, _) = domain_routing_ism + .deploy(@array![OWNER().try_into().unwrap()]) + .unwrap(); + ( + IInterchainSecurityModuleDispatcher { contract_address: domain_routing_ism_addr }, + IRoutingIsmDispatcher { contract_address: domain_routing_ism_addr }, + IDomainRoutingIsmDispatcher { contract_address: domain_routing_ism_addr } + ) +} + +pub fn setup_validator_announce() -> (IValidatorAnnounceDispatcher, EventSpy) { + let validator_announce_class = declare("validator_announce").unwrap(); + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let (validator_announce_addr, _) = validator_announce_class + .deploy(@array![mailbox.contract_address.into(), OWNER().try_into().unwrap()]) + .unwrap(); + let mut spy = spy_events(SpyOn::One(validator_announce_addr)); + (IValidatorAnnounceDispatcher { contract_address: validator_announce_addr }, spy) +} + +pub fn setup_mock_validator_announce( + mailbox_address: ContractAddress, domain: u32 +) -> IMockValidatorAnnounceDispatcher { + let validator_announce_class = declare("mock_validator_announce").unwrap(); + let (validator_announce_addr, _) = validator_announce_class + .deploy(@array![mailbox_address.into(), domain.into()]) + .unwrap(); + IMockValidatorAnnounceDispatcher { contract_address: validator_announce_addr } +} + +pub fn setup_aggregation(modules: Span, threshold: u8) -> IAggregationDispatcher { + let aggregation_class = declare("aggregation").unwrap(); + let mut parameters = Default::default(); + let owner: felt252 = OWNER().try_into().unwrap(); + Serde::serialize(@owner, ref parameters); + Serde::serialize(@modules, ref parameters); + Serde::serialize(@threshold, ref parameters); + let (aggregation_addr, _) = aggregation_class.deploy(@parameters).unwrap(); + IAggregationDispatcher { contract_address: aggregation_addr } +} + +pub fn setup_merkle_tree_hook() -> ( + IMerkleTreeHookDispatcher, IPostDispatchHookDispatcher, EventSpy +) { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let merkle_tree_hook_class = declare("merkle_tree_hook").unwrap(); + let res = merkle_tree_hook_class + .deploy(@array![mailbox.contract_address.into(), OWNER().try_into().unwrap()]); + if (res.is_err()) { + panic(res.unwrap_err()); + } + let (merkle_tree_hook_addr, _) = res.unwrap(); + let mut spy = spy_events(SpyOn::One(merkle_tree_hook_addr)); + + ( + IMerkleTreeHookDispatcher { contract_address: merkle_tree_hook_addr }, + IPostDispatchHookDispatcher { contract_address: merkle_tree_hook_addr }, + spy + ) +} + +pub fn setup_mock_fee_hook() -> IPostDispatchHookDispatcher { + let mock_hook = declare("fee_hook").unwrap(); + let (mock_hook_addr, _) = mock_hook.deploy(@array![]).unwrap(); + IPostDispatchHookDispatcher { contract_address: mock_hook_addr } +} + +pub fn setup_mock_hook() -> IPostDispatchHookDispatcher { + let mock_hook = declare("hook").unwrap(); + let (mock_hook_addr, _) = mock_hook.deploy(@array![]).unwrap(); + IPostDispatchHookDispatcher { contract_address: mock_hook_addr } +} + +pub fn setup_noop_ism() -> IInterchainSecurityModuleDispatcher { + let noop_ism = declare("noop_ism").unwrap(); + let (noop_ism_addr, _) = noop_ism.deploy(@array![]).unwrap(); + IInterchainSecurityModuleDispatcher { contract_address: noop_ism_addr } +} + + +pub fn setup_pausable_ism() -> (IInterchainSecurityModuleDispatcher, IPausableIsmDispatcher) { + let pausable_ism = declare("pausable_ism").unwrap(); + let (pausable_ism_addr, _) = pausable_ism.deploy(@array![OWNER().try_into().unwrap()]).unwrap(); + ( + IInterchainSecurityModuleDispatcher { contract_address: pausable_ism_addr }, + IPausableIsmDispatcher { contract_address: pausable_ism_addr } + ) +} + +pub fn setup_trusted_relayer_ism() -> IInterchainSecurityModuleDispatcher { + let (mailbox, _, _, _) = setup_mailbox(DESTINATION_MAILBOX(), Option::None, Option::None); + let trusted_relayer_ism = declare("trusted_relayer_ism").unwrap(); + let (trusted_relayer_ism_addr, _) = trusted_relayer_ism + .deploy(@array![mailbox.contract_address.into(), OWNER().try_into().unwrap()]) + .unwrap(); + IInterchainSecurityModuleDispatcher { contract_address: trusted_relayer_ism_addr } +} + +pub fn build_messageid_metadata(origin_merkle_tree_hook: u256, root: u256, index: u32) -> Bytes { + let y_parity = 0x01; + let (_, _, signatures) = get_message_and_signature(); + let mut metadata = BytesTrait::new_empty(); + metadata.append_u256(origin_merkle_tree_hook); + metadata.append_u256(root); + metadata.append_u32(index); + let mut cur_idx = 0; + loop { + if (cur_idx == signatures.len()) { + break (); + } + metadata.append_u256(*signatures.at(cur_idx).r); + metadata.append_u256(*signatures.at(cur_idx).s); + metadata.append_u8(y_parity); + cur_idx += 1; + }; + metadata +} + +pub fn build_fake_messageid_metadata( + origin_merkle_tree_hook: u256, root: u256, index: u32 +) -> Bytes { + let y_parity = 0x01; + let (_, _, signatures) = get_message_and_signature(); + let mut metadata = BytesTrait::new_empty(); + metadata.append_u256(origin_merkle_tree_hook); + metadata.append_u256(root); + metadata.append_u32(index); + let mut cur_idx = 0; + loop { + if (cur_idx == signatures.len()) { + break (); + } + metadata.append_u256(*signatures.at(0).r); + metadata.append_u256(*signatures.at(0).s); + metadata.append_u8(y_parity); + cur_idx += 1; + }; + metadata +} + + +// Configuration from the main cairo repo: https://github.com/starkware-libs/cairo/blob/main/corelib/src/test/secp256k1_test.cairo +pub fn get_message_and_signature() -> (u256, Array, Array) { + let msg_hash = 0xD2B30308834E6E76F50891F0B45E742BE4F4A163BAF697B465DD14C968777AD0; + let validators_array: Array = array![ + 0xfa3ed5df8369fb40e75978937607b6f0c0e04fb8.try_into().unwrap(), + 0x2f7008b31a614685a1d24a684827cf05f15dc17a.try_into().unwrap(), + 0x108e9c0c2a24c02d70b224ee9dd97136cdc1e072.try_into().unwrap(), + 0x7851fee3ec28606dfb18041396876524e8fa6256.try_into().unwrap(), + 0xaef2f8fa001982939416fd49e6e533e9fba65a1b.try_into().unwrap(), + ]; + let signatures = array![ + EthSignature { + r: 0x83db08d4e1590714aef8600f5f1e3c967ab6a3b9f93bb4242de0306510e688ea, + s: 0x0af5d1d51ea7e51a291789ff4866a1e36bc4134d956870799380d2d71f5dbf3d, + }, + EthSignature { + r: 0xf81a5dd3f871ad2d27a3b538e73663d723f8263fb3d289514346d43d000175f5, + s: 0x083df770623e9ae52a7bb154473961e24664bb003bdfdba6100fb5e540875ce1, + }, + EthSignature { + r: 0x76b194f951f94492ca582dab63dc413b9ac1ca9992c22bc2186439e9ab8fdd3c, + s: 0x62a6a6f402edaa53e9bdc715070a61edb0d98d4e14e182f60bdd4ae932b40b29, + }, + EthSignature { + r: 0x35932eefd85897d868aaacd4ba7aee81a2384e42ba062133f6d37fdfebf94ad4, + s: 0x78cce49db96ee27c3f461800388ac95101476605baa64a194b7dd4d56d2d4a4d, + }, + EthSignature { + r: 0x6b38d4353d69396e91c57542254348d16459d448ab887574e9476a6ff76d49a1, + s: 0x3527627295bde423d7d799afef22affac4f00c70a5b651ad14c8879aeb9b6e03, + } + ]; + + (msg_hash, validators_array, signatures) +} + + +pub fn build_merkle_metadata( + origin_merkle_tree_hook: u256, message_index: u32, signed_index: u32, signed_message_id: u256 +) -> Bytes { + let proof = TEST_PROOF(); + let y_parity = 0x01; + let (_, _, signatures) = get_merkle_message_and_signature(); + let mut metadata = BytesTrait::new_empty(); + metadata.append_u256(origin_merkle_tree_hook); + metadata.append_u32(message_index); + metadata.append_u256(signed_message_id); + let mut cur_idx = 0; + loop { + if (cur_idx == MERKLE_PROOF_ITERATION) { + break (); + } + metadata.append_u256(*proof.at(cur_idx)); + cur_idx += 1; + }; + metadata.append_u32(signed_index); + cur_idx = 0; + loop { + if (cur_idx == signatures.len()) { + break (); + } + metadata.append_u256(*signatures.at(cur_idx).r); + metadata.append_u256(*signatures.at(cur_idx).s); + metadata.append_u8(y_parity); + cur_idx += 1; + }; + metadata +} + + +pub fn build_fake_merkle_metadata( + origin_merkle_tree_hook: u256, message_index: u32, signed_index: u32, signed_message_id: u256 +) -> Bytes { + let proof = TEST_PROOF(); + let y_parity = 0x01; + let (_, _, signatures) = get_merkle_message_and_signature(); + let mut metadata = BytesTrait::new_empty(); + metadata.append_u256(origin_merkle_tree_hook); + metadata.append_u32(message_index); + metadata.append_u256(signed_message_id); + let mut cur_idx = 0; + loop { + if (cur_idx == MERKLE_PROOF_ITERATION) { + break (); + } + metadata.append_u256(*proof.at(cur_idx)); + cur_idx += 1; + }; + metadata.append_u32(signed_index); + cur_idx = 0; + loop { + if (cur_idx == signatures.len()) { + break (); + } + metadata.append_u256(*signatures.at(0).r); + metadata.append_u256(*signatures.at(0).s); + metadata.append_u8(y_parity); + cur_idx += 1; + }; + metadata +} + + +// Configuration from the main cairo repo: https://github.com/starkware-libs/cairo/blob/main/corelib/src/test/secp256k1_test.cairo +pub fn get_merkle_message_and_signature() -> (u256, Array, Array) { + let msg_hash = 0xC9E505E3C9DDD36638A10238CAD43278311CDF09894EF7EFBE6AFBF510507907; + let validators_array: Array = array![ + 0x2614ea33eaf750585f8ae3b59a45b2c800b952ed.try_into().unwrap(), + 0xc69bb3f1bedb35543f1c28c0ff4f6428622068d3.try_into().unwrap(), + 0x7f2c0c840dd8855e0625f1e1ede157d4e09bc9c4.try_into().unwrap(), + 0x6d937801434bbf68eb109d3ab2b103a64afcbac2.try_into().unwrap(), + 0xfe5fb670978ee5c686db8a7180509b5682f9797d.try_into().unwrap(), + ]; + let signatures = array![ + EthSignature { + r: 0x83db08d4e1590714aef8600f5f1e3c967ab6a3b9f93bb4242de0306510e688ea, + s: 0x0af5d1d51ea7e51a291789ff4866a1e36bc4134d956870799380d2d71f5dbf3d, + }, + EthSignature { + r: 0xf81a5dd3f871ad2d27a3b538e73663d723f8263fb3d289514346d43d000175f5, + s: 0x083df770623e9ae52a7bb154473961e24664bb003bdfdba6100fb5e540875ce1, + }, + EthSignature { + r: 0x76b194f951f94492ca582dab63dc413b9ac1ca9992c22bc2186439e9ab8fdd3c, + s: 0x62a6a6f402edaa53e9bdc715070a61edb0d98d4e14e182f60bdd4ae932b40b29, + }, + EthSignature { + r: 0x35932eefd85897d868aaacd4ba7aee81a2384e42ba062133f6d37fdfebf94ad4, + s: 0x78cce49db96ee27c3f461800388ac95101476605baa64a194b7dd4d56d2d4a4d, + }, + EthSignature { + r: 0x6b38d4353d69396e91c57542254348d16459d448ab887574e9476a6ff76d49a1, + s: 0x3527627295bde423d7d799afef22affac4f00c70a5b651ad14c8879aeb9b6e03, + } + ]; + + (msg_hash, validators_array, signatures) +} + +pub fn setup_mock_token() -> ERC20ABIDispatcher { + let fee_token_class = declare("mock_fee_token").unwrap(); + let (fee_token_addr, _) = fee_token_class + .deploy_at( + @array![ + INITIAL_SUPPLY.low.into(), INITIAL_SUPPLY.high.into(), OWNER().try_into().unwrap() + ], + ETH_ADDRESS() + ) + .unwrap(); + ERC20ABIDispatcher { contract_address: fee_token_addr } +} + +pub fn setup_protocol_fee() -> (IProtocolFeeDispatcher, IPostDispatchHookDispatcher) { + let protocol_fee_class = declare("protocol_fee").unwrap(); + let (protocol_fee_addr, _) = protocol_fee_class + .deploy( + @array![ + MAX_PROTOCOL_FEE.low.into(), + MAX_PROTOCOL_FEE.high.into(), + PROTOCOL_FEE.low.into(), + PROTOCOL_FEE.high.into(), + BENEFICIARY().into(), + OWNER().try_into().unwrap(), + ETH_ADDRESS().into() + ] + ) + .unwrap(); + ( + IProtocolFeeDispatcher { contract_address: protocol_fee_addr }, + IPostDispatchHookDispatcher { contract_address: protocol_fee_addr } + ) +} diff --git a/starknet/cairo/crates/contracts/tests/test_mailbox.cairo b/starknet/cairo/crates/contracts/tests/test_mailbox.cairo new file mode 100644 index 00000000000..9651433c097 --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/test_mailbox.cairo @@ -0,0 +1,644 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::interfaces::{ + IMailbox, IMailboxDispatcher, IMailboxDispatcherTrait, IMessageRecipientDispatcherTrait, + ETH_ADDRESS +}; +use contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; +use contracts::mailbox::mailbox; +use contracts::utils::utils::U256TryIntoContractAddress; +use openzeppelin::access::ownable::OwnableComponent; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use openzeppelin::token::erc20::interface::{ERC20ABI, ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use snforge_std::cheatcodes::events::EventAssertions; +use snforge_std::{start_prank, CheatTarget, stop_prank}; +use super::setup::{ + setup_mailbox, mock_setup, OWNER, LOCAL_DOMAIN, NEW_OWNER, DEFAULT_ISM, NEW_DEFAULT_ISM, + NEW_DEFAULT_HOOK, NEW_REQUIRED_HOOK, DESTINATION_DOMAIN, RECIPIENT_ADDRESS, MAILBOX, + DESTINATION_MAILBOX, setup_protocol_fee, setup_mock_hook, PROTOCOL_FEE, INITIAL_SUPPLY, + setup_mock_fee_hook +}; + + +#[test] +fn test_local_domain() { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + assert(mailbox.get_local_domain() == LOCAL_DOMAIN, 'Wrong local domain'); +} +#[test] +fn test_owner() { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + let owner: felt252 = ownable.owner().into(); + assert(owner.into() == OWNER(), 'Wrong contract owner'); +} + +#[test] +fn test_transfer_ownership() { + let (mailbox, mut spy, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + ownable.transfer_ownership(NEW_OWNER().try_into().unwrap()); + stop_prank(CheatTarget::One(ownable.contract_address)); + let owner: felt252 = ownable.owner().into(); + assert(owner == NEW_OWNER().try_into().unwrap(), 'Failed transfer ownership'); + + let expected_event = OwnableComponent::OwnershipTransferred { + previous_owner: OWNER().try_into().unwrap(), new_owner: NEW_OWNER().try_into().unwrap() + }; + spy + .assert_emitted( + @array![ + ( + ownable.contract_address, + OwnableComponent::Event::OwnershipTransferred(expected_event) + ) + ] + ); +} + +#[test] +fn test_set_default_hook() { + let (mailbox, mut spy, mock_hook, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + mailbox.set_default_hook(mock_hook.contract_address); + assert(mailbox.get_default_hook() == mock_hook.contract_address, 'Failed to set default hook'); + let expected_event = mailbox::Event::DefaultHookSet( + mailbox::DefaultHookSet { hook: mock_hook.contract_address } + ); + spy.assert_emitted(@array![(mailbox.contract_address, expected_event)]); +} + +#[test] +fn test_set_required_hook() { + let (mailbox, mut spy, mock_hook, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + mailbox.set_required_hook(mock_hook.contract_address); + assert( + mailbox.get_required_hook() == mock_hook.contract_address, 'Failed to set required hook' + ); + let expected_event = mailbox::Event::RequiredHookSet( + mailbox::RequiredHookSet { hook: mock_hook.contract_address } + ); + spy.assert_emitted(@array![(mailbox.contract_address, expected_event)]); +} + +#[test] +fn test_set_default_ism() { + let (mailbox, mut spy, _, mock_ism) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + mailbox.set_default_ism(mock_ism.contract_address); + assert(mailbox.get_default_ism() == mock_ism.contract_address, 'Failed to set default ism'); + let expected_event = mailbox::Event::DefaultIsmSet( + mailbox::DefaultIsmSet { module: mock_ism.contract_address } + ); + spy.assert_emitted(@array![(mailbox.contract_address, expected_event)]); +} +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_default_hook_fails_if_not_owner() { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER().try_into().unwrap()); + mailbox.set_default_hook(NEW_DEFAULT_HOOK()); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_required_hook_fails_if_not_owner() { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER().try_into().unwrap()); + mailbox.set_required_hook(NEW_REQUIRED_HOOK()); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_default_ism_fails_if_not_owner() { + let (mailbox, _, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER().try_into().unwrap()); + mailbox.set_default_ism(NEW_DEFAULT_ISM()); +} + +#[test] +fn test_dispatch() { + let (mailbox, mut spy, _, _) = setup_mailbox(MAILBOX(), Option::None, Option::None); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: RECIPIENT_ADDRESS(), + body: message_body.clone() + }; + let (message_id, _) = MessageTrait::format_message(message.clone()); + mailbox + .dispatch( + DESTINATION_DOMAIN, RECIPIENT_ADDRESS(), message_body, 0, Option::None, Option::None + ); + let expected_event = mailbox::Event::Dispatch( + mailbox::Dispatch { + sender: OWNER(), + destination_domain: DESTINATION_DOMAIN, + recipient_address: RECIPIENT_ADDRESS(), + message: message + } + ); + let expected_event_id = mailbox::Event::DispatchId(mailbox::DispatchId { id: message_id }); + + spy + .assert_emitted( + @array![ + (mailbox.contract_address, expected_event), + (mailbox.contract_address, expected_event_id) + ] + ); + + assert(mailbox.get_latest_dispatched_id() == message_id, 'Failed to fetch latest id'); +} + + +#[test] +fn test_dispatch_with_protocol_fee_hook() { + let (_, protocol_fee_hook) = setup_protocol_fee(); + let mock_hook = setup_mock_hook(); + let (mailbox, mut spy, _, _) = setup_mailbox( + MAILBOX(), + Option::Some(protocol_fee_hook.contract_address), + Option::Some(mock_hook.contract_address) + ); + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + erc20_dispatcher.approve(MAILBOX(), PROTOCOL_FEE); + stop_prank(CheatTarget::One(ownable.contract_address)); + // The owner has the initial fee token supply + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: RECIPIENT_ADDRESS(), + body: message_body.clone() + }; + let (message_id, _) = MessageTrait::format_message(message.clone()); + mailbox + .dispatch( + DESTINATION_DOMAIN, + RECIPIENT_ADDRESS(), + message_body, + PROTOCOL_FEE, + Option::None, + Option::None + ); + let expected_event = mailbox::Event::Dispatch( + mailbox::Dispatch { + sender: OWNER(), + destination_domain: DESTINATION_DOMAIN, + recipient_address: RECIPIENT_ADDRESS(), + message: message + } + ); + let expected_event_id = mailbox::Event::DispatchId(mailbox::DispatchId { id: message_id }); + + spy + .assert_emitted( + @array![ + (mailbox.contract_address, expected_event), + (mailbox.contract_address, expected_event_id) + ] + ); + + // balance check + assert_eq!( + erc20_dispatcher.balanceOf(OWNER().try_into().unwrap()), INITIAL_SUPPLY - PROTOCOL_FEE + ); + assert(mailbox.get_latest_dispatched_id() == message_id, 'Failed to fetch latest id'); +} + + +#[test] +fn test_dispatch_with_two_fee_hook() { + let (_, protocol_fee_hook) = setup_protocol_fee(); + let mock_hook = setup_mock_fee_hook(); + let (mailbox, mut spy, _, _) = setup_mailbox( + MAILBOX(), + Option::Some(protocol_fee_hook.contract_address), + Option::Some(mock_hook.contract_address) + ); + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + // (mock_fee_hook consummes 3 * PROTOCOL_FEE) + erc20_dispatcher.approve(MAILBOX(), 5 * PROTOCOL_FEE); + stop_prank(CheatTarget::One(ownable.contract_address)); + // The owner has the initial fee token supply + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: RECIPIENT_ADDRESS(), + body: message_body.clone() + }; + let (message_id, _) = MessageTrait::format_message(message.clone()); + mailbox + .dispatch( + DESTINATION_DOMAIN, + RECIPIENT_ADDRESS(), + message_body, + 5 * PROTOCOL_FEE, + Option::None, + Option::None + ); + let expected_event = mailbox::Event::Dispatch( + mailbox::Dispatch { + sender: OWNER(), + destination_domain: DESTINATION_DOMAIN, + recipient_address: RECIPIENT_ADDRESS(), + message: message + } + ); + let expected_event_id = mailbox::Event::DispatchId(mailbox::DispatchId { id: message_id }); + + spy + .assert_emitted( + @array![ + (mailbox.contract_address, expected_event), + (mailbox.contract_address, expected_event_id) + ] + ); + + // balance check + assert_eq!( + erc20_dispatcher.balanceOf(OWNER().try_into().unwrap()), INITIAL_SUPPLY - 4 * PROTOCOL_FEE + ); + assert(mailbox.get_latest_dispatched_id() == message_id, 'Failed to fetch latest id'); +} + +#[test] +#[should_panic(expected: ('Provided fee < needed fee',))] +fn test_dispatch_with_two_fee_hook_fails_if_greater_than_required_and_lower_than_default() { + let (_, protocol_fee_hook) = setup_protocol_fee(); + let mock_hook = setup_mock_fee_hook(); + let (mailbox, mut spy, _, _) = setup_mailbox( + MAILBOX(), + Option::Some(protocol_fee_hook.contract_address), + Option::Some(mock_hook.contract_address) + ); + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + // (mock_fee_hook consummes 3 * PROTOCOL_FEE) + erc20_dispatcher.approve(MAILBOX(), 3 * PROTOCOL_FEE); + stop_prank(CheatTarget::One(ownable.contract_address)); + // The owner has the initial fee token supply + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: RECIPIENT_ADDRESS(), + body: message_body.clone() + }; + let (message_id, _) = MessageTrait::format_message(message.clone()); + mailbox + .dispatch( + DESTINATION_DOMAIN, + RECIPIENT_ADDRESS(), + message_body, + 3 * PROTOCOL_FEE, + Option::None, + Option::None + ); + let expected_event = mailbox::Event::Dispatch( + mailbox::Dispatch { + sender: OWNER(), + destination_domain: DESTINATION_DOMAIN, + recipient_address: RECIPIENT_ADDRESS(), + message: message + } + ); + let expected_event_id = mailbox::Event::DispatchId(mailbox::DispatchId { id: message_id }); + + spy + .assert_emitted( + @array![ + (mailbox.contract_address, expected_event), + (mailbox.contract_address, expected_event_id) + ] + ); + + // balance check + assert_eq!( + erc20_dispatcher.balance_of(OWNER().try_into().unwrap()), INITIAL_SUPPLY - 4 * PROTOCOL_FEE + ); + assert(mailbox.get_latest_dispatched_id() == message_id, 'Failed to fetch latest id'); +} +#[test] +#[should_panic(expected: ('Provided fee < needed fee',))] +fn test_dispatch_with_protocol_fee_hook_fails_if_provided_fee_lower_than_required_fee() { + let (_, protocol_fee_hook) = setup_protocol_fee(); + let mock_hook = setup_mock_hook(); + + let (mailbox, _, _, _) = setup_mailbox( + MAILBOX(), + Option::Some(protocol_fee_hook.contract_address), + Option::Some(mock_hook.contract_address) + ); + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + // We transfer some token to the new owner + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + erc20_dispatcher.transfer(NEW_OWNER().try_into().unwrap(), PROTOCOL_FEE - 10); + + // The new owner has has PROTOCOL_FEE -10 tokens so the required hook post dispatch fails + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER().try_into().unwrap()); + erc20_dispatcher.approve(MAILBOX(), PROTOCOL_FEE - 10); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + mailbox + .dispatch( + DESTINATION_DOMAIN, + RECIPIENT_ADDRESS(), + message_body, + PROTOCOL_FEE - 10, + Option::None, + Option::None + ); +} + + +#[test] +#[should_panic(expected: ('Insufficient balance',))] +fn test_dispatch_with_protocol_fee_hook_fails_if_user_balance_lower_than_fee_amount() { + let (_, protocol_fee_hook) = setup_protocol_fee(); + let mock_hook = setup_mock_hook(); + + let (mailbox, _, _, _) = setup_mailbox( + MAILBOX(), + Option::Some(protocol_fee_hook.contract_address), + Option::Some(mock_hook.contract_address) + ); + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + // We transfer some token to the new owner + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + erc20_dispatcher.transfer(NEW_OWNER().try_into().unwrap(), PROTOCOL_FEE - 10); + + // The new owner has has PROTOCOL_FEE -10 tokens so the required hook post dispatch fails + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER().try_into().unwrap()); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER().try_into().unwrap()); + erc20_dispatcher.approve(MAILBOX(), PROTOCOL_FEE - 10); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + mailbox + .dispatch( + DESTINATION_DOMAIN, + RECIPIENT_ADDRESS(), + message_body, + PROTOCOL_FEE, + Option::None, + Option::None + ); +} + + +#[test] +#[should_panic(expected: ('Insufficient allowance',))] +fn test_dispatch_with_protocol_fee_hook_fails_if_insufficient_allowance() { + let (_, protocol_fee_hook) = setup_protocol_fee(); + let mock_hook = setup_mock_hook(); + + let (mailbox, _, _, _) = setup_mailbox( + MAILBOX(), + Option::Some(protocol_fee_hook.contract_address), + Option::Some(mock_hook.contract_address) + ); + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + // We transfer some token to the new owner + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: ETH_ADDRESS() }; + erc20_dispatcher.transfer(NEW_OWNER().try_into().unwrap(), PROTOCOL_FEE); + + // The new owner has has PROTOCOL_FEE -10 tokens so the required hook post dispatch fails + let ownable = IOwnableDispatcher { contract_address: ETH_ADDRESS() }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER().try_into().unwrap()); + erc20_dispatcher.approve(MAILBOX(), PROTOCOL_FEE - 10); + + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER().try_into().unwrap()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + mailbox + .dispatch( + DESTINATION_DOMAIN, + RECIPIENT_ADDRESS(), + message_body, + PROTOCOL_FEE, + Option::None, + Option::None + ); +} + + +#[test] +fn test_process() { + let (mailbox, mut spy, _, _) = setup_mailbox(DESTINATION_MAILBOX(), Option::None, Option::None); + let mock_ism_address = mailbox.get_default_ism(); + let (mock_recipient, _) = mock_setup(mock_ism_address); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let recipient: felt252 = mock_recipient.contract_address.into(); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: recipient.into(), + body: message_body.clone() + }; + let (message_id, _) = MessageTrait::format_message(message.clone()); + let metadata = message_body; + mailbox.process(metadata.clone(), message); + let expected_event = mailbox::Event::Process( + mailbox::Process { origin: LOCAL_DOMAIN, sender: OWNER(), recipient: recipient.into(), } + ); + let expected_event_id = mailbox::Event::ProcessId(mailbox::ProcessId { id: message_id }); + + spy + .assert_emitted( + @array![ + (mailbox.contract_address, expected_event), + (mailbox.contract_address, expected_event_id) + ] + ); + let block_number = starknet::get_block_number(); + assert(mailbox.delivered(message_id), 'Failed to delivered(id)'); + assert(mailbox.processor(message_id) == OWNER().try_into().unwrap(), 'Wrong processor'); + assert(mailbox.processed_at(message_id) == block_number, 'Wrong processed block number'); + assert(mock_recipient.get_origin() == LOCAL_DOMAIN, 'Failed to retrieve origin'); + assert(mock_recipient.get_sender() == OWNER(), 'Failed to retrieve sender'); + assert(mock_recipient.get_message() == metadata, 'Failed to retrieve metadata'); +} + +#[test] +#[should_panic(expected: ('Wrong hyperlane version',))] +fn test_process_fails_if_version_mismatch() { + let (mailbox, _, _, _) = setup_mailbox(DESTINATION_MAILBOX(), Option::None, Option::None); + let mock_ism_address = mailbox.get_default_ism(); + let (mock_recipient, _) = mock_setup(mock_ism_address); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let recipient: felt252 = mock_recipient.contract_address.into(); + let message = Message { + version: HYPERLANE_VERSION + 1, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: recipient.into(), + body: message_body.clone() + }; + let metadata = message_body; + mailbox.process(metadata.clone(), message); +} + +#[test] +#[should_panic(expected: ('Unexpected destination',))] +fn test_process_fails_if_destination_domain_does_not_match_local_domain() { + let (mailbox, _, _, _) = setup_mailbox(DESTINATION_MAILBOX(), Option::None, Option::None); + let mock_ism_address = mailbox.get_default_ism(); + let (mock_recipient, _) = mock_setup(mock_ism_address); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let recipient: felt252 = mock_recipient.contract_address.into(); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN + 1, + recipient: recipient.into(), + body: message_body.clone() + }; + let metadata = message_body; + mailbox.process(metadata.clone(), message); +} + +#[test] +#[should_panic(expected: ('Mailbox: already delivered',))] +fn test_process_fails_if_already_delivered() { + let (mailbox, _, _, _) = setup_mailbox(DESTINATION_MAILBOX(), Option::None, Option::None); + let mock_ism_address = mailbox.get_default_ism(); + let (mock_recipient, _) = mock_setup(mock_ism_address); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER().try_into().unwrap()); + // mailbox.set_local_domain(DESTINATION_DOMAIN); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let recipient: felt252 = mock_recipient.contract_address.into(); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: recipient.into(), + body: message_body.clone() + }; + let metadata = message_body; + mailbox.process(metadata.clone(), message.clone()); + let (message_id, _) = MessageTrait::format_message(message.clone()); + assert(mailbox.delivered(message_id), 'Delivered status did not change'); + mailbox.process(metadata.clone(), message); +} + diff --git a/starknet/cairo/crates/contracts/tests/test_validator_announce.cairo b/starknet/cairo/crates/contracts/tests/test_validator_announce.cairo new file mode 100644 index 00000000000..d21800a385d --- /dev/null +++ b/starknet/cairo/crates/contracts/tests/test_validator_announce.cairo @@ -0,0 +1,146 @@ +use alexandria_bytes::{Bytes, BytesTrait, BytesIndex}; +use contracts::interfaces::{ + IMockValidatorAnnounceDispatcher, IMockValidatorAnnounceDispatcherTrait, + IValidatorAnnounceDispatcher, IValidatorAnnounceDispatcherTrait, +}; +use contracts::isms::multisig::validator_announce::validator_announce; +use contracts::libs::checkpoint_lib::checkpoint_lib::{HYPERLANE_ANNOUNCEMENT}; +use snforge_std::cheatcodes::events::EventAssertions; +use starknet::{contract_address_const, EthAddress}; +use super::setup::{setup_mock_validator_announce, setup_validator_announce}; + +pub const TEST_STARKNET_DOMAIN: u32 = 23448593; + + +#[test] +fn test_announce() { + let (validator_announce, mut spy) = setup_validator_announce(); + let validator_address: EthAddress = 0xf85362bdff5a3561481819b8c9010770384aaecf + .try_into() + .unwrap(); + let mut _storage_location: Array = array![ + 180946006308525359965345158532346553211983108462325076142963585023296502126, + 90954189295124463684969781689350429239725285131197301894846683156275291225, + 276191619276790668637754154763775604 + ]; + let mut signature = BytesTrait::new_empty(); + signature.append_u256(0x8e3c967ab6a3b9f93bb4242de0306510e688ea3db08d4e1590714aef8600f5f1); + signature.append_u256(0x0f4866a1e36bc4134d9568af5d1d51ea7e51a291789f70799380d2d71f5dbf3d); + signature.append_u8(0x1); + let res = validator_announce.announce(validator_address, _storage_location.clone(), signature); + assert_eq!(res, true); + let expected_event = validator_announce::Event::ValidatorAnnouncement( + validator_announce::ValidatorAnnouncement { + validator: validator_address, storage_location: _storage_location.span() + } + ); + spy.assert_emitted(@array![(validator_announce.contract_address, expected_event),]); + let validators = validator_announce.get_announced_validators(); + assert(validators == array![validator_address].span(), 'validator array mismatch'); + let storage_location = validator_announce.get_announced_storage_locations(validators); + assert((*storage_location.at(0)).at(0) == @_storage_location, 'wrong storage location'); +} + + +#[test] +fn test_double_announce() { + let mailbox_address = contract_address_const::< + 0x0228c4f640b613dba2107cabf930564bbdb1b4e2d283ba1843b91e6327f09f8e + >(); + + let validator_announce = setup_mock_validator_announce(mailbox_address, TEST_STARKNET_DOMAIN); + let validator_address: EthAddress = 0xe6076407ca06f2b0a0ec716db2b5361beccdcfa8 + .try_into() + .unwrap(); + let mut _storage_location: Array = array![ + 180946006308525359965345158532346553211983108462325076142963585023296502126, + 90954189295124463684969781689350429239725285131197301894846683156275291225, + 276191619276790668637754154763775604 + ]; + let mut signature = BytesTrait::new_empty(); + signature.append_u256(0x8e3c967ab6a3b9f93bb4242de0306510e688ea3db08d4e1590714aef8600f5f1); + signature.append_u256(0x0f4866a1e36bc4134d9568af5d1d51ea7e51a291789f70799380d2d71f5dbf3d); + signature.append_u8(0x1); + let res = validator_announce + .announce(validator_address, _storage_location.clone(), signature.clone()); + assert_eq!(res, true); + let mut _storage_location_2: Array = array![ + 90954189295124463684969781689350429239725285131197301894846683156275291225, + 180946006308525359965345158532346553211983108462325076142963585023296502126, + 276191619276790668637754154763775604 + ]; + validator_announce.announce(validator_address, _storage_location_2.clone(), signature); + let validators = validator_announce.get_announced_validators(); + assert(validators == array![validator_address].span(), 'validator array mismatch'); + let storage_location = validator_announce.get_announced_storage_locations(validators); + assert((*storage_location.at(0)).at(0) == @_storage_location, 'wrong storage location'); + assert((*storage_location.at(0)).at(1) == @_storage_location_2, 'wrong storage location'); +} +#[test] +#[should_panic(expected: ('Wrong signer',))] +fn test_announce_fails_if_wrong_signer() { + let (validator_announce, _) = setup_validator_announce(); + let validator_address: EthAddress = 'wrong_signer'.try_into().unwrap(); + let mut storage_location: Array = array![ + 180946006308525359965345158532346553211983108462325076142963585023296502126, + 90954189295124463684969781689350429239725285131197301894846683156275291225, + 276191619276790668637754154763775604 + ]; + let mut signature = BytesTrait::new_empty(); + signature.append_u256(0x8e3c967ab6a3b9f93bb4242de0306510e688ea3db08d4e1590714aef8600f5f1); + signature.append_u256(0x0f4866a1e36bc4134d9568af5d1d51ea7e51a291789f70799380d2d71f5dbf3d); + signature.append_u8(0x1); + validator_announce.announce(validator_address, storage_location.clone(), signature.clone()); + validator_announce.announce(validator_address, storage_location, signature); +} + +#[test] +#[should_panic(expected: ('Announce already occured',))] +fn test_announce_fails_if_replay() { + let (validator_announce, _) = setup_validator_announce(); + let validator_address: EthAddress = 0xf85362bdff5a3561481819b8c9010770384aaecf + .try_into() + .unwrap(); + let mut storage_location: Array = array![ + 180946006308525359965345158532346553211983108462325076142963585023296502126, + 90954189295124463684969781689350429239725285131197301894846683156275291225, + 276191619276790668637754154763775604 + ]; + let mut signature = BytesTrait::new_empty(); + signature.append_u256(0x8e3c967ab6a3b9f93bb4242de0306510e688ea3db08d4e1590714aef8600f5f1); + signature.append_u256(0x0f4866a1e36bc4134d9568af5d1d51ea7e51a291789f70799380d2d71f5dbf3d); + signature.append_u8(0x1); + validator_announce.announce(validator_address, storage_location.clone(), signature.clone()); + validator_announce.announce(validator_address, storage_location, signature); +} +#[test] +fn test_digest_computation() { + let mailbox_address = contract_address_const::< + 0x0228c4f640b613dba2107cabf930564bbdb1b4e2d283ba1843b91e6327f09f8e + >(); + + let va = setup_mock_validator_announce(mailbox_address, TEST_STARKNET_DOMAIN); + + // file:///var/folders/kr/z3l_6qyn3znb6gbnddtvgsn40000gn/T/.tmpdY51LU/checkpoint + let mut _storage_location: Array = array![ + 180946006308525359965345158532346553211983108462325076142963585023296502126, + 90954189295124463684969781689350429239725285131197301894846683156275291225, + 276191619276790668637754154763775604 + ]; + + let mut u256_storage_location: Array = array![]; + + loop { + match _storage_location.pop_front() { + Option::Some(storage) => { u256_storage_location.append(storage.into()); }, + Option::None(()) => { break (); }, + } + }; + let digest = va.get_announcement_digest(u256_storage_location); + + // digest printed in an e2e local test of the hyperlane validator + assert( + digest == 68490098148397702232337918459455233145663417151157276422147736490102791983827, + 'Wrong digest' + ); +} diff --git a/starknet/cairo/crates/mocks/Scarb.toml b/starknet/cairo/crates/mocks/Scarb.toml new file mode 100644 index 00000000000..804a5d554ee --- /dev/null +++ b/starknet/cairo/crates/mocks/Scarb.toml @@ -0,0 +1,26 @@ +[package] +name = "mocks" +version.workspace = true +edition.workspace = true +cairo-version.workspace = true + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet.workspace = true +alexandria_bytes.workspace = true +alexandria_storage.workspace = true +openzeppelin.workspace = true +contracts = { path = "../contracts" } +token = { path = "../token" } + + + +[tool] +fmt.workspace = true + +[[target.starknet-contract]] +casm = true + +[lib] +name = "mocks" diff --git a/starknet/cairo/crates/mocks/src/enumerable_map_holder.cairo b/starknet/cairo/crates/mocks/src/enumerable_map_holder.cairo new file mode 100644 index 00000000000..7997c504e81 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/enumerable_map_holder.cairo @@ -0,0 +1,52 @@ +#[starknet::interface] +pub trait IEnumerableMapHolder { + fn do_get_len(self: @TContractState) -> u32; + fn do_set_key(ref self: TContractState, key: u32, value: u256); + fn do_get_value(self: @TContractState, key: u32) -> u256; + fn do_contains(self: @TContractState, key: u32) -> bool; + fn do_remove(ref self: TContractState, key: u32) -> bool; + fn do_at(self: @TContractState, index: u32) -> (u32, u256); + fn do_get_keys(self: @TContractState) -> Array; +} + +#[starknet::contract] +pub mod EnumerableMapHolder { + use contracts::libs::enumerable_map::{EnumerableMap, EnumerableMapTrait}; + + #[storage] + struct Storage { + routers: EnumerableMap + } + + #[abi(embed_v0)] + impl Holder of super::IEnumerableMapHolder { + fn do_get_len(self: @ContractState) -> u32 { + let routers = self.routers.read(); + routers.len() + } + fn do_set_key(ref self: ContractState, key: u32, value: u256) { + let mut routers = self.routers.read(); + routers.set(key, value); + } + fn do_get_value(self: @ContractState, key: u32) -> u256 { + let routers = self.routers.read(); + routers.get(key) + } + fn do_contains(self: @ContractState, key: u32) -> bool { + let routers = self.routers.read(); + routers.contains(key) + } + fn do_remove(ref self: ContractState, key: u32) -> bool { + let mut routers = self.routers.read(); + routers.remove(key) + } + fn do_at(self: @ContractState, index: u32) -> (u32, u256) { + let routers = self.routers.read(); + routers.at(index) + } + fn do_get_keys(self: @ContractState) -> Array { + let routers = self.routers.read(); + routers.keys() + } + } +} diff --git a/starknet/cairo/crates/mocks/src/erc4626_component.cairo b/starknet/cairo/crates/mocks/src/erc4626_component.cairo new file mode 100644 index 00000000000..26c9391bf21 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/erc4626_component.cairo @@ -0,0 +1,523 @@ +//! Modified from {https://github.com/0xHashstack/hashstack_contracts/blob/main/src/token/erc4626/erc4626component.cairo} +//! Modified from {https://github.com/nodeset-org/erc4626-cairo/blob/main/src/erc4626/erc4626.cairo} +use starknet::ContractAddress; + +#[starknet::component] +pub mod ERC4626Component { + use core::integer::BoundedInt; + use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; + use openzeppelin::introspection::src5::{ + SRC5Component, SRC5Component::SRC5Impl, SRC5Component::InternalTrait as SRC5INternalTrait + }; + use openzeppelin::token::erc20::ERC20Component::InternalTrait as ERC20InternalTrait; + use openzeppelin::token::erc20::interface::{ + IERC20, IERC20Metadata, ERC20ABIDispatcher, ERC20ABIDispatcherTrait, + }; + use openzeppelin::token::erc20::{ + ERC20Component, ERC20HooksEmptyImpl, ERC20Component::Errors as ERC20Errors + }; + use starknet::{ContractAddress, get_caller_address, get_contract_address}; + use token::interfaces::ierc4626::{IERC4626, IERC4626Camel, IERC4626Metadata}; + + #[storage] + struct Storage { + ERC4626_asset: ContractAddress, + ERC4626_underlying_decimals: u8, + ERC4626_offset: u8, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Deposit: Deposit, + Withdraw: Withdraw, + } + + #[derive(Drop, starknet::Event)] + struct Deposit { + #[key] + sender: ContractAddress, + #[key] + owner: ContractAddress, + assets: u256, + shares: u256 + } + + #[derive(Drop, starknet::Event)] + struct Withdraw { + #[key] + sender: ContractAddress, + #[key] + receiver: ContractAddress, + #[key] + owner: ContractAddress, + assets: u256, + shares: u256 + } + + pub mod Errors { + pub const EXCEEDED_MAX_DEPOSIT: felt252 = 'ERC4626: exceeded max deposit'; + pub const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeded max mint'; + pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeded max redeem'; + pub const EXCEEDED_MAX_WITHDRAW: felt252 = 'ERC4626: exceeded max withdraw'; + } + + pub trait ERC4626HooksTrait { + fn before_deposit( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ); + fn after_deposit( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ); + + fn before_withdraw( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ); + + fn after_withdraw( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ); + } + + #[embeddable_as(ERC4626Impl)] + pub impl ERC4626< + TContractState, + +HasComponent, + impl ERC20: ERC20Component::HasComponent, + +ERC4626HooksTrait, + +SRC5Component::HasComponent, + +Drop + > of IERC4626> { + fn name(self: @ComponentState) -> ByteArray { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.name() + } + + fn symbol(self: @ComponentState) -> ByteArray { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.symbol() + } + + fn decimals(self: @ComponentState) -> u8 { + self.ERC4626_underlying_decimals.read() + self.ERC4626_offset.read() + } + + fn total_supply(self: @ComponentState) -> u256 { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.total_supply() + } + + fn balance_of(self: @ComponentState, account: ContractAddress) -> u256 { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.balance_of(account) + } + + fn allowance( + self: @ComponentState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.allowance(owner, spender) + } + + fn transfer( + ref self: ComponentState, recipient: ContractAddress, amount: u256 + ) -> bool { + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.transfer(recipient, amount) + } + + fn transfer_from( + ref self: ComponentState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.transfer_from(sender, recipient, amount) + } + + fn approve( + ref self: ComponentState, spender: ContractAddress, amount: u256 + ) -> bool { + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.approve(spender, amount) + } + + fn asset(self: @ComponentState) -> ContractAddress { + self.ERC4626_asset.read() + } + + fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares, false) + } + + fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets, false) + } + + fn deposit( + ref self: ComponentState, assets: u256, receiver: ContractAddress + ) -> u256 { + let max_assets = self.max_deposit(receiver); + assert(max_assets >= assets, Errors::EXCEEDED_MAX_DEPOSIT); + + let caller = get_caller_address(); + let shares = self.preview_deposit(assets); + self._deposit(caller, receiver, assets, shares); + shares + } + + fn mint( + ref self: ComponentState, shares: u256, receiver: ContractAddress + ) -> u256 { + let max_shares = self.max_mint(receiver); + assert(max_shares >= shares, Errors::EXCEEDED_MAX_MINT); + + let caller = get_caller_address(); + let assets = self.preview_mint(shares); + self._deposit(caller, receiver, assets, shares); + assets + } + + fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets, false) + } + + fn preview_mint(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares, true) + } + + fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares, false) + } + + fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets, true) + } + + fn max_deposit(self: @ComponentState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn max_mint(self: @ComponentState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn max_redeem(self: @ComponentState, owner: ContractAddress) -> u256 { + let erc20 = get_dep_component!(self, ERC20); + erc20.balance_of(owner) + } + + fn max_withdraw(self: @ComponentState, owner: ContractAddress) -> u256 { + let erc20 = get_dep_component!(self, ERC20); + let balance = erc20.balance_of(owner); + self._convert_to_assets(balance, false) + } + + fn redeem( + ref self: ComponentState, + shares: u256, + receiver: ContractAddress, + owner: ContractAddress + ) -> u256 { + let max_shares = self.max_redeem(owner); + assert(shares <= max_shares, Errors::EXCEEDED_MAX_REDEEM); + + let caller = get_caller_address(); + let assets = self.preview_redeem(shares); + self._withdraw(caller, receiver, owner, assets, shares); + assets + } + + fn total_assets(self: @ComponentState) -> u256 { + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.balance_of(get_contract_address()) + } + + fn withdraw( + ref self: ComponentState, + assets: u256, + receiver: ContractAddress, + owner: ContractAddress + ) -> u256 { + let max_assets = self.max_withdraw(owner); + assert(assets <= max_assets, Errors::EXCEEDED_MAX_WITHDRAW); + + let caller = get_caller_address(); + let shares = self.preview_withdraw(assets); + self._withdraw(caller, receiver, owner, assets, shares); + + shares + } + } + + #[embeddable_as(ERC4626MetadataImpl)] + pub impl ERC4626Metadata< + TContractState, + +HasComponent, + impl ERC20: ERC20Component::HasComponent, + +SRC5Component::HasComponent, + +Drop + > of IERC4626Metadata> { + fn name(self: @ComponentState) -> ByteArray { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.name() + } + fn symbol(self: @ComponentState) -> ByteArray { + let erc20_comp = get_dep_component!(ref self, ERC20); + erc20_comp.symbol() + } + fn decimals(self: @ComponentState) -> u8 { + self.ERC4626_underlying_decimals.read() + self.ERC4626_offset.read() + } + } + + #[embeddable_as(ERC4626CamelImpl)] + pub impl ERC4626Camel< + TContractState, + +HasComponent, + +ERC20Component::HasComponent, + +SRC5Component::HasComponent, + +ERC4626HooksTrait, + +Drop + > of IERC4626Camel> { + fn totalSupply(self: @ComponentState) -> u256 { + self.total_supply() + } + fn balanceOf(self: @ComponentState, account: ContractAddress) -> u256 { + self.balance_of(account) + } + fn transferFrom( + ref self: ComponentState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.transfer_from(sender, recipient, amount) + } + + fn convertToAssets(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares, false) + } + + fn convertToShares(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets, false) + } + + fn previewDeposit(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets, false) + } + + fn previewMint(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares, true) + } + + fn previewRedeem(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares, false) + } + + fn previewWithdraw(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets, true) + } + + fn totalAssets(self: @ComponentState) -> u256 { + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.balanceOf(get_contract_address()) + } + + fn maxDeposit(self: @ComponentState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn maxMint(self: @ComponentState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn maxRedeem(self: @ComponentState, owner: ContractAddress) -> u256 { + self.max_redeem(owner) + } + + fn maxWithdraw(self: @ComponentState, owner: ContractAddress) -> u256 { + self.max_withdraw(owner) + } + } + + fn pow_256(self: u256, mut exponent: u8) -> u256 { + if self == 0 { + return 0; + } + let mut result = 1; + let mut base = self; + + loop { + if exponent & 1 == 1 { + result = result * base; + } + + exponent = exponent / 2; + if exponent == 0 { + break result; + } + + base = base * base; + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl ERC20: ERC20Component::HasComponent, + +SRC5Component::HasComponent, + impl Hooks: ERC4626HooksTrait, + +Drop + > of InternalImplTrait { + fn initializer( + ref self: ComponentState, + asset: ContractAddress, + name: ByteArray, + symbol: ByteArray, + offset: u8 + ) { + let dispatcher = ERC20ABIDispatcher { contract_address: asset }; + self.ERC4626_offset.write(offset); + let decimals = dispatcher.decimals(); + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.initializer(name, symbol); + self.ERC4626_asset.write(asset); + self.ERC4626_underlying_decimals.write(decimals); + } + + fn _convert_to_assets( + self: @ComponentState, shares: u256, round: bool + ) -> u256 { + let total_assets = self.total_assets() + 1; + let total_shares = self.total_supply() + pow_256(10, self.ERC4626_offset.read()); + let assets = shares * total_assets / total_shares; + if round && ((assets * total_shares) / total_assets < shares) { + assets + 1 + } else { + assets + } + } + + fn _convert_to_shares( + self: @ComponentState, assets: u256, round: bool + ) -> u256 { + let total_assets = self.total_assets() + 1; + let total_shares = self.total_supply() + pow_256(10, self.ERC4626_offset.read()); + let share = assets * total_shares / total_assets; + if round && ((share * total_assets) / total_shares < assets) { + share + 1 + } else { + share + } + } + + fn _deposit( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) { + Hooks::before_deposit(ref self, caller, receiver, assets, shares); + + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.transfer_from(caller, get_contract_address(), assets); + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + erc20_comp_mut.mint(receiver, shares); + self.emit(Deposit { sender: caller, owner: receiver, assets, shares }); + + Hooks::after_deposit(ref self, caller, receiver, assets, shares); + } + + fn _withdraw( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) { + Hooks::before_withdraw(ref self, caller, receiver, owner, assets, shares); + + let mut erc20_comp_mut = get_dep_component_mut!(ref self, ERC20); + if (caller != owner) { + let erc20_comp = get_dep_component!(@self, ERC20); + let allowance = erc20_comp.allowance(owner, caller); + if (allowance != BoundedInt::max()) { + assert(allowance >= shares, ERC20Errors::APPROVE_FROM_ZERO); + erc20_comp_mut.ERC20_allowances.write((owner, caller), allowance - shares); + } + } + + erc20_comp_mut.burn(owner, shares); + + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.transfer(receiver, assets); + + self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); + + Hooks::after_withdraw(ref self, caller, receiver, owner, assets, shares); + } + + fn _decimals_offset(self: @ComponentState) -> u8 { + self.ERC4626_offset.read() + } + } +} + +pub impl ERC4626HooksEmptyImpl< + TContractState +> of ERC4626Component::ERC4626HooksTrait { + fn before_deposit( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) {} + fn after_deposit( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) {} + + fn before_withdraw( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) {} + fn after_withdraw( + ref self: ERC4626Component::ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) {} +} diff --git a/starknet/cairo/crates/mocks/src/erc4626_mock.cairo b/starknet/cairo/crates/mocks/src/erc4626_mock.cairo new file mode 100644 index 00000000000..7a0e6f2e087 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/erc4626_mock.cairo @@ -0,0 +1,44 @@ +#[starknet::contract] +mod ERC4626Mock { + use mocks::erc4626_component::{ERC4626Component, ERC4626HooksEmptyImpl}; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::{get_contract_address, ContractAddress}; + + component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + #[abi(embed_v0)] + impl ERC4626Impl = ERC4626Component::ERC4626Impl; + impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc4626: ERC4626Component::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC4626Event: ERC4626Component::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, asset: ContractAddress, name: ByteArray, symbol: ByteArray, + ) { + self.erc4626.initializer(asset, name, symbol, 0); + } +} diff --git a/starknet/cairo/crates/mocks/src/erc4626_yield_sharing_mock.cairo b/starknet/cairo/crates/mocks/src/erc4626_yield_sharing_mock.cairo new file mode 100644 index 00000000000..7212a6679c1 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/erc4626_yield_sharing_mock.cairo @@ -0,0 +1,457 @@ +//! Modified from {https://github.com/nodeset-org/erc4626-cairo/blob/main/src/erc4626/erc4626.cairo} +#[starknet::interface] +pub trait IERC4626YieldSharing { + fn set_fee(ref self: TContractState, new_fee: u256); + fn get_claimable_fees(self: @TContractState) -> u256; + fn scale(self: @TContractState) -> u256; + fn accumulated_fees(self: @TContractState) -> u256; + fn last_vault_balance(self: @TContractState) -> u256; +} + +#[starknet::contract] +mod ERC4626YieldSharingMock { + use contracts::libs::math; + use core::integer::BoundedInt; + use openzeppelin::access::ownable::{OwnableComponent}; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::{get_contract_address, get_caller_address, ContractAddress}; + use token::interfaces::ierc4626::{IERC4626, IERC4626Camel}; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // E18 + const SCALE: u256 = 1_000_000_000_000_000_000; + + pub mod Errors { + pub const EXCEEDED_MAX_DEPOSIT: felt252 = 'ERC4626: exceeded max deposit'; + pub const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeded max mint'; + pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeded max redeem'; + pub const EXCEEDED_MAX_WITHDRAW: felt252 = 'ERC4626: exceeded max withdraw'; + } + + #[storage] + struct Storage { + fee: u256, + accumulated_fees: u256, + last_vault_balance: u256, + ERC4626_asset: ContractAddress, + ERC4626_underlying_decimals: u8, + ERC4626_offset: u8, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Deposit: Deposit, + Withdraw: Withdraw, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event + } + + #[derive(Drop, starknet::Event)] + struct Deposit { + #[key] + sender: ContractAddress, + #[key] + owner: ContractAddress, + assets: u256, + shares: u256 + } + + #[derive(Drop, starknet::Event)] + struct Withdraw { + #[key] + sender: ContractAddress, + #[key] + receiver: ContractAddress, + #[key] + owner: ContractAddress, + assets: u256, + shares: u256 + } + + #[constructor] + fn constructor( + ref self: ContractState, + asset: ContractAddress, + name: ByteArray, + symbol: ByteArray, + initial_fee: u256 + ) { + let dispatcher = ERC20ABIDispatcher { contract_address: asset }; + self.ERC4626_offset.write(0); + let decimals = dispatcher.decimals(); + self.erc20.initializer(name, symbol); + self.ERC4626_asset.write(asset); + self.ERC4626_underlying_decimals.write(decimals); + self.fee.write(initial_fee); + self.ownable.initializer(get_caller_address()); + } + + #[abi(embed_v0)] + pub impl ERC4626YieldSharingImpl of super::IERC4626YieldSharing { + fn set_fee(ref self: ContractState, new_fee: u256) { + self.ownable.assert_only_owner(); + self.fee.write(new_fee); + } + + fn get_claimable_fees(self: @ContractState) -> u256 { + let new_vault_balance = ERC20ABIDispatcher { + contract_address: self.ERC4626_asset.read() + } + .balance_of(get_contract_address()); + let last_vault_balance = self.last_vault_balance.read(); + if new_vault_balance <= last_vault_balance { + return self.accumulated_fees.read(); + } + + let new_yield = new_vault_balance - last_vault_balance; + let new_fees = math::mul_div(new_yield, self.fee.read(), SCALE); + + self.accumulated_fees.read() + new_fees + } + + fn scale(self: @ContractState) -> u256 { + SCALE + } + fn accumulated_fees(self: @ContractState) -> u256 { + self.accumulated_fees.read() + } + + fn last_vault_balance(self: @ContractState) -> u256 { + self.last_vault_balance.read() + } + } + + #[abi(embed_v0)] + pub impl ERC4626Impl of IERC4626 { + fn name(self: @ContractState) -> ByteArray { + self.erc20.name() + } + + fn symbol(self: @ContractState) -> ByteArray { + self.erc20.symbol() + } + + fn decimals(self: @ContractState) -> u8 { + self.ERC4626_underlying_decimals.read() + self.ERC4626_offset.read() + } + + fn total_supply(self: @ContractState) -> u256 { + self.erc20.total_supply() + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.balance_of(account) + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.erc20.allowance(owner, spender) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + self.erc20.transfer(recipient, amount) + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.erc20.transfer_from(sender, recipient, amount) + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + self.erc20.approve(spender, amount) + } + + fn asset(self: @ContractState) -> ContractAddress { + self.ERC4626_asset.read() + } + + fn convert_to_assets(self: @ContractState, shares: u256) -> u256 { + self._convert_to_assets(shares, false) + } + + fn convert_to_shares(self: @ContractState, assets: u256) -> u256 { + self._convert_to_shares(assets, false) + } + // Overriden + fn deposit(ref self: ContractState, assets: u256, receiver: ContractAddress) -> u256 { + let last_vault_balance = self.last_vault_balance.read(); + self.last_vault_balance.write(last_vault_balance + assets); + let max_assets = self.max_deposit(receiver); + assert(max_assets >= assets, Errors::EXCEEDED_MAX_DEPOSIT); + + let caller = get_caller_address(); + let shares = self.preview_deposit(assets); + self._deposit(caller, receiver, assets, shares); + + shares + } + + fn mint(ref self: ContractState, shares: u256, receiver: ContractAddress) -> u256 { + let max_shares = self.max_mint(receiver); + assert(max_shares >= shares, Errors::EXCEEDED_MAX_MINT); + + let caller = get_caller_address(); + let assets = self.preview_mint(shares); + self._deposit(caller, receiver, assets, shares); + + assets + } + + fn preview_deposit(self: @ContractState, assets: u256) -> u256 { + self._convert_to_shares(assets, false) + } + + fn preview_mint(self: @ContractState, shares: u256) -> u256 { + self._convert_to_assets(shares, true) + } + + fn preview_redeem(self: @ContractState, shares: u256) -> u256 { + self._convert_to_assets(shares, false) + } + + fn preview_withdraw(self: @ContractState, assets: u256) -> u256 { + self._convert_to_shares(assets, true) + } + + fn max_deposit(self: @ContractState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn max_mint(self: @ContractState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn max_redeem(self: @ContractState, owner: ContractAddress) -> u256 { + self.erc20.balance_of(owner) + } + + fn max_withdraw(self: @ContractState, owner: ContractAddress) -> u256 { + let balance = self.erc20.balance_of(owner); + let shares = self._convert_to_assets(balance, false); + shares + } + // Overriden + fn redeem( + ref self: ContractState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256 { + self._accrue_yield(); + let max_shares = self.max_redeem(owner); + assert(shares <= max_shares, Errors::EXCEEDED_MAX_REDEEM); + + let caller = get_caller_address(); + let assets = self.preview_redeem(shares); + self._withdraw(caller, receiver, owner, assets, shares); + assets + } + + fn total_assets(self: @ContractState) -> u256 { + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.balance_of(get_contract_address()) - self.get_claimable_fees() + } + + fn withdraw( + ref self: ContractState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256 { + let max_assets = self.max_withdraw(owner); + assert(assets <= max_assets, Errors::EXCEEDED_MAX_WITHDRAW); + + let caller = get_caller_address(); + let shares = self.preview_withdraw(assets); + self._withdraw(caller, receiver, owner, assets, shares); + + shares + } + } + + #[abi(embed_v0)] + pub impl ERC4626CamelImpl of IERC4626Camel { + fn totalSupply(self: @ContractState) -> u256 { + ERC4626Impl::total_supply(self) + } + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + ERC4626Impl::balance_of(self, account) + } + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + ERC4626Impl::transfer_from(ref self, sender, recipient, amount) + } + + fn convertToAssets(self: @ContractState, shares: u256) -> u256 { + self._convert_to_assets(shares, false) + } + + fn convertToShares(self: @ContractState, assets: u256) -> u256 { + self._convert_to_shares(assets, false) + } + + fn previewDeposit(self: @ContractState, assets: u256) -> u256 { + self._convert_to_shares(assets, false) + } + + fn previewMint(self: @ContractState, shares: u256) -> u256 { + self._convert_to_assets(shares, true) + } + + fn previewRedeem(self: @ContractState, shares: u256) -> u256 { + self._convert_to_assets(shares, false) + } + + fn previewWithdraw(self: @ContractState, assets: u256) -> u256 { + self._convert_to_shares(assets, true) + } + + fn totalAssets(self: @ContractState) -> u256 { + self.total_assets() + } + + fn maxDeposit(self: @ContractState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn maxMint(self: @ContractState, receiver: ContractAddress) -> u256 { + BoundedInt::max() + } + + fn maxRedeem(self: @ContractState, owner: ContractAddress) -> u256 { + self.max_redeem(owner) + } + + fn maxWithdraw(self: @ContractState, owner: ContractAddress) -> u256 { + self.max_withdraw(owner) + } + } + + fn pow_256(self: u256, mut exponent: u8) -> u256 { + if self == 0 { + return 0; + } + let mut result = 1; + let mut base = self; + + loop { + if exponent & 1 == 1 { + result = result * base; + } + + exponent = exponent / 2; + if exponent == 0 { + break result; + } + + base = base * base; + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _accrue_yield(ref self: ContractState) { + let new_vault_balance = ERC20ABIDispatcher { + contract_address: self.ERC4626_asset.read() + } + .balance_of(get_contract_address()); + let last_vault_balance = self.last_vault_balance.read(); + if new_vault_balance > last_vault_balance { + let new_yield = new_vault_balance - last_vault_balance; + let new_fees = math::mul_div(new_yield, self.fee.read(), SCALE); + let accumulated_fees = self.accumulated_fees.read(); + self.accumulated_fees.write(accumulated_fees + new_fees); + self.last_vault_balance.write(new_vault_balance); + } + } + + fn _convert_to_assets(self: @ContractState, shares: u256, round: bool) -> u256 { + let total_assets = ERC4626Impl::total_assets(self) + 1; + let total_shares = ERC4626Impl::total_supply(self) + + pow_256(10, self.ERC4626_offset.read()); + let assets = shares * total_assets / total_shares; + if round && ((assets * total_shares) / total_assets < shares) { + assets + 1 + } else { + assets + } + } + + fn _convert_to_shares(self: @ContractState, assets: u256, round: bool) -> u256 { + let total_assets = ERC4626Impl::total_assets(self) + 1; + let total_shares = ERC4626Impl::total_supply(self) + + pow_256(10, self.ERC4626_offset.read()); + let share = assets * total_shares / total_assets; + if round && ((share * total_assets) / total_shares < assets) { + share + 1 + } else { + share + } + } + + fn _deposit( + ref self: ContractState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) { + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.transfer_from(caller, get_contract_address(), assets); + self.erc20.mint(receiver, shares); + self.emit(Deposit { sender: caller, owner: receiver, assets, shares }); + } + + fn _withdraw( + ref self: ContractState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) { + if (caller != owner) { + let allowance = self.erc20.allowance(owner, caller); + if (allowance != BoundedInt::max()) { + assert(allowance >= shares, ERC20Component::Errors::APPROVE_FROM_ZERO); + self.erc20.ERC20_allowances.write((owner, caller), allowance - shares); + } + } + + self.erc20.burn(owner, shares); + + let dispatcher = ERC20ABIDispatcher { contract_address: self.ERC4626_asset.read() }; + dispatcher.transfer(receiver, assets); + + self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); + } + + fn _decimals_offset(self: @ContractState) -> u8 { + self.ERC4626_offset.read() + } + } +} diff --git a/starknet/cairo/crates/mocks/src/fee_hook.cairo b/starknet/cairo/crates/mocks/src/fee_hook.cairo new file mode 100644 index 00000000000..282efa2f54b --- /dev/null +++ b/starknet/cairo/crates/mocks/src/fee_hook.cairo @@ -0,0 +1,31 @@ +#[starknet::contract] +pub mod fee_hook { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use contracts::interfaces::{ + IPostDispatchHook, IPostDispatchHookDispatcher, IPostDispatchHookDispatcherTrait, Types + }; + use contracts::libs::message::Message; + use starknet::ContractAddress; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl IPostDispatchHookImpl of IPostDispatchHook { + fn hook_type(self: @ContractState) -> Types { + Types::UNUSED(()) + } + + fn supports_metadata(self: @ContractState, _metadata: Bytes) -> bool { + true + } + + fn post_dispatch( + ref self: ContractState, _metadata: Bytes, _message: Message, _fee_amount: u256, + ) {} + + fn quote_dispatch(ref self: ContractState, _metadata: Bytes, _message: Message) -> u256 { + 3000000 + } + } +} diff --git a/starknet/cairo/crates/mocks/src/fee_token.cairo b/starknet/cairo/crates/mocks/src/fee_token.cairo new file mode 100644 index 00000000000..2ea86e770fd --- /dev/null +++ b/starknet/cairo/crates/mocks/src/fee_token.cairo @@ -0,0 +1,29 @@ +#[starknet::contract] +pub mod mock_fee_token { + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl InternalImpl = ERC20Component::InternalImpl; + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { + self.erc20.initializer("FeeToken", "FT"); + self.erc20.mint(recipient, initial_supply); + } +} diff --git a/starknet/cairo/crates/mocks/src/hook.cairo b/starknet/cairo/crates/mocks/src/hook.cairo new file mode 100644 index 00000000000..a797c366022 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/hook.cairo @@ -0,0 +1,30 @@ +#[starknet::contract] +pub mod hook { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use contracts::interfaces::{ + IPostDispatchHook, IPostDispatchHookDispatcher, IPostDispatchHookDispatcherTrait, Types + }; + use contracts::libs::message::Message; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl IPostDispatchHookImpl of IPostDispatchHook { + fn hook_type(self: @ContractState) -> Types { + Types::UNUSED(()) + } + + fn supports_metadata(self: @ContractState, _metadata: Bytes) -> bool { + true + } + + fn post_dispatch( + ref self: ContractState, _metadata: Bytes, _message: Message, _fee_amount: u256 + ) {} + + fn quote_dispatch(ref self: ContractState, _metadata: Bytes, _message: Message) -> u256 { + 0_u256 + } + } +} diff --git a/starknet/cairo/crates/mocks/src/ism.cairo b/starknet/cairo/crates/mocks/src/ism.cairo new file mode 100644 index 00000000000..ed1006cdd0b --- /dev/null +++ b/starknet/cairo/crates/mocks/src/ism.cairo @@ -0,0 +1,24 @@ +#[starknet::contract] +pub mod ism { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use contracts::interfaces::{ + IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, ModuleType + }; + use contracts::libs::message::{Message, MessageTrait}; + use starknet::ContractAddress; + use starknet::EthAddress; + + #[storage] + struct Storage {} + #[abi(embed_v0)] + impl IMessageidMultisigIsmImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::MESSAGE_ID_MULTISIG(starknet::get_contract_address()) + } + + fn verify(self: @ContractState, _metadata: Bytes, _message: Message,) -> bool { + true + } + } +} diff --git a/starknet/cairo/crates/mocks/src/lib.cairo b/starknet/cairo/crates/mocks/src/lib.cairo new file mode 100644 index 00000000000..b9dca8cc29a --- /dev/null +++ b/starknet/cairo/crates/mocks/src/lib.cairo @@ -0,0 +1,21 @@ +pub mod enumerable_map_holder; +pub mod erc4626_component; +pub mod erc4626_mock; +pub mod erc4626_yield_sharing_mock; +pub mod fee_hook; +pub mod fee_token; +pub mod hook; +pub mod ism; +pub mod message_recipient; +pub mod mock_account; +pub mod mock_eth; +pub mod mock_hyp_erc721_uri_storage; +pub mod mock_mailbox; +pub mod mock_validator_announce; +pub mod test_erc20; +pub mod test_erc721; +pub mod test_interchain_gas_payment; +pub mod test_ism; +pub mod test_post_dispatch_hook; +pub mod xerc20_lockbox_test; +pub mod xerc20_test; diff --git a/starknet/cairo/crates/mocks/src/message_recipient.cairo b/starknet/cairo/crates/mocks/src/message_recipient.cairo new file mode 100644 index 00000000000..307d8037651 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/message_recipient.cairo @@ -0,0 +1,55 @@ +#[starknet::contract] +pub mod message_recipient { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use contracts::interfaces::{ + IMessageRecipient, IMessageRecipientDispatcher, IMessageRecipientDispatcherTrait, + ModuleType, ISpecifiesInterchainSecurityModule, + ISpecifiesInterchainSecurityModuleDispatcher, + ISpecifiesInterchainSecurityModuleDispatcherTrait + }; + use starknet::ContractAddress; + + + #[storage] + struct Storage { + origin: u32, + sender: u256, + message: Bytes, + ism: ContractAddress + } + + #[constructor] + fn constructor(ref self: ContractState, _ism: ContractAddress) { + self.ism.write(_ism); + } + + #[abi(embed_v0)] + impl IMessageRecipientImpl of IMessageRecipient { + fn handle(ref self: ContractState, _origin: u32, _sender: u256, _message: Bytes) { + self.message.write(_message); + self.origin.write(_origin); + self.sender.write(_sender); + } + + fn get_origin(self: @ContractState) -> u32 { + self.origin.read() + } + + fn get_sender(self: @ContractState) -> u256 { + self.sender.read() + } + + fn get_message(self: @ContractState) -> Bytes { + self.message.read() + } + } + + #[abi(embed_v0)] + impl ISpecifiesInterchainSecurityModuleImpl of ISpecifiesInterchainSecurityModule< + ContractState + > { + fn interchain_security_module(self: @ContractState) -> ContractAddress { + self.ism.read() + } + } +} diff --git a/starknet/cairo/crates/mocks/src/mock_account.cairo b/starknet/cairo/crates/mocks/src/mock_account.cairo new file mode 100644 index 00000000000..e7e1a859f6f --- /dev/null +++ b/starknet/cairo/crates/mocks/src/mock_account.cairo @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.14.0 (presets/account.cairo) + +/// # Account Preset +/// +/// OpenZeppelin's upgradeable account which can change its public key and declare, deploy, or call contracts. +#[starknet::contract(account)] +pub(crate) mod MockAccount { + use openzeppelin::account::AccountComponent; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::upgrades::UpgradeableComponent; + use openzeppelin::upgrades::interface::IUpgradeable; + use starknet::ClassHash; + + component!(path: AccountComponent, storage: account, event: AccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Account Mixin + #[abi(embed_v0)] + pub(crate) impl AccountMixinImpl = + AccountComponent::AccountMixinImpl; + impl AccountInternalImpl = AccountComponent::InternalImpl; + + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + account: AccountComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccountEvent: AccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + pub(crate) fn constructor(ref self: ContractState, public_key: felt252) { + self.account.initializer(public_key); + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.account.assert_only_self(); + self.upgradeable.upgrade(new_class_hash); + } + } +} diff --git a/starknet/cairo/crates/mocks/src/mock_eth.cairo b/starknet/cairo/crates/mocks/src/mock_eth.cairo new file mode 100644 index 00000000000..37fea06ab26 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/mock_eth.cairo @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts for Cairo ^0.15.0 + +#[starknet::interface] +pub trait MockEth { + fn mint(ref self: TContractState, recipient: starknet::ContractAddress, amount: u256); +} + +#[starknet::contract] +mod Ether { + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::ERC20HooksEmptyImpl; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + #[abi(embed_v0)] + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl; + + impl ERC20InternalImpl = ERC20Component::InternalImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.erc20.initializer("Ether", "ETH"); + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl MockEthImpl of super::MockEth { + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.ownable.assert_only_owner(); + self.erc20.mint(recipient, amount); + } + } +} diff --git a/starknet/cairo/crates/mocks/src/mock_hyp_erc721_uri_storage.cairo b/starknet/cairo/crates/mocks/src/mock_hyp_erc721_uri_storage.cairo new file mode 100644 index 00000000000..e7b041d0545 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/mock_hyp_erc721_uri_storage.cairo @@ -0,0 +1,213 @@ +#[starknet::interface] +trait IMockHypERC721URIStorage { + fn set_token_uri(ref self: TContractState, token_id: u256, uri: ByteArray); +} + +#[starknet::contract] +pub mod MockHypERC721URIStorage { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, get_caller_address}; + use token::components::erc721_uri_storage::ERC721URIStorageComponent; + use token::components::hyp_erc721_component::{HypErc721Component}; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: HypErc721Component, storage: hyp_erc721, event: HypErc721Event); + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!( + path: ERC721URIStorageComponent, storage: erc721_uri_storage, event: ERC721UriStorageEvent + ); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + + //Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + impl RouterInternalImpl = RouterComponent::RouterComponentInternalImpl; + + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + impl TokenRouterInternalImpl = TokenRouterComponent::TokenRouterInternalImpl; + + //HypERC721 + impl HypErc721InternalImpl = HypErc721Component::HypErc721InternalImpl; + + //ERC721 + #[abi(embed_v0)] + impl ERC721URIStorageImpl = + ERC721URIStorageComponent::ERC721URIStorageImpl; + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721CamelOnlyImpl = ERC721Component::ERC721CamelOnlyImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + impl ERC721URIStorageInternalImpl = + ERC721URIStorageComponent::ERC721URIStorageInternalImpl; + + //upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + hyp_erc721: HypErc721Component::Storage, + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + #[substorage(v0)] + erc721_uri_storage: ERC721URIStorageComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + HypErc721Event: HypErc721Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + ERC721UriStorageEvent: ERC721URIStorageComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + _mint_amount: u256, + _name: ByteArray, + _symbol: ByteArray, + _hook: ContractAddress, + _interchainSecurityModule: ContractAddress, + owner: ContractAddress, + ) { + self.ownable.initializer(owner); + self + .mailboxclient + .initialize(mailbox, Option::Some(_hook), Option::Some(_interchainSecurityModule)); + self.hyp_erc721.initialize(_mint_amount, _name, _symbol); + } + + #[abi(embed_v0)] + impl IMockHypERC721URIStorageImpl of super::IMockHypERC721URIStorage { + fn set_token_uri(ref self: ContractState, token_id: u256, uri: ByteArray) { + self.erc721_uri_storage._set_token_uri(token_id, uri); + } + } + + #[abi(embed_v0)] + impl HypErc721Upgradeable of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let contract_state = TokenRouterComponent::HasComponent::get_contract(@self); + let token_owner = contract_state.erc721.owner_of(amount_or_id); + assert!(token_owner == get_caller_address(), "Caller is not owner of token"); + + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + contract_state.erc721.burn(amount_or_id); + + BytesTrait::new_empty() + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let recipient_felt: felt252 = recipient.try_into().expect('u256 to felt failed'); + let recipient: ContractAddress = recipient_felt.try_into().unwrap(); + + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + + let metadata_byteArray = bytes_to_byte_array(metadata); + contract_state.erc721_uri_storage._set_token_uri(amount_or_id, metadata_byteArray); + contract_state.erc721.mint(recipient, amount_or_id); + } + } + + // free function + fn bytes_to_byte_array(self: Bytes) -> ByteArray { + let mut res: ByteArray = Default::default(); + let mut offset = 0; + while offset < self + .size() { + if offset + 31 <= self.size() { + let (new_offset, value) = self.read_bytes31(offset); + res.append_word(value.into(), 31); + offset = new_offset; + } else { + let (new_offset, value) = self.read_u8(offset); + res.append_byte(value); + offset = new_offset; + } + }; + res + } +} diff --git a/starknet/cairo/crates/mocks/src/mock_mailbox.cairo b/starknet/cairo/crates/mocks/src/mock_mailbox.cairo new file mode 100644 index 00000000000..6682953b42f --- /dev/null +++ b/starknet/cairo/crates/mocks/src/mock_mailbox.cairo @@ -0,0 +1,596 @@ +use alexandria_bytes::Bytes; +use contracts::libs::message::Message; +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IMockMailbox { + fn add_remote_mail_box(ref self: TContractState, domain: u32, mailbox: ContractAddress); + fn add_inbound_message(ref self: TContractState, message: Message); + fn process_next_inbound_message(ref self: TContractState); + fn get_local_domain(self: @TContractState) -> u32; + fn delivered(self: @TContractState, _message_id: u256) -> bool; + fn nonce(self: @TContractState) -> u32; + fn get_default_ism(self: @TContractState) -> ContractAddress; + fn get_default_hook(self: @TContractState) -> ContractAddress; + fn get_required_hook(self: @TContractState) -> ContractAddress; + fn get_latest_dispatched_id(self: @TContractState) -> u256; + fn dispatch( + ref self: TContractState, + destination_domain: u32, + recipient_address: u256, + message_body: Bytes, + fee_amount: u256, + metadata: Option, + hook: Option + ) -> u256; + fn quote_dispatch( + self: @TContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes, + _custom_hook_metadata: Option, + _custom_hook: Option, + ) -> u256; + fn process(ref self: TContractState, _metadata: Bytes, _message: Message); + fn recipient_ism(self: @TContractState, _recipient: u256) -> ContractAddress; + fn set_default_ism(ref self: TContractState, _module: ContractAddress); + fn set_default_hook(ref self: TContractState, _hook: ContractAddress); + fn set_required_hook(ref self: TContractState, _hook: ContractAddress); + fn processor(self: @TContractState, _id: u256) -> ContractAddress; + fn processed_at(self: @TContractState, _id: u256) -> u64; +} + +#[starknet::contract] +pub mod MockMailbox { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::interfaces::{ + IMailbox, IMailboxDispatcher, IMailboxDispatcherTrait, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, ISpecifiesInterchainSecurityModuleDispatcher, + ISpecifiesInterchainSecurityModuleDispatcherTrait, IMessageRecipientDispatcher, + IMessageRecipientDispatcherTrait, ETH_ADDRESS, + }; + use contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; + use contracts::utils::utils::U256TryIntoContractAddress; + use core::starknet::event::EventEmitter; + use mocks::test_post_dispatch_hook::{ + ITestPostDispatchHookDispatcher, ITestPostDispatchHookDispatcherTrait + }; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::{ + ERC20ABI, ERC20ABIDispatcher, ERC20ABIDispatcherTrait + }; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ + ContractAddress, ClassHash, get_caller_address, get_block_number, contract_address_const, + get_contract_address + }; + use super::{IMockMailboxDispatcherTrait, IMockMailboxDispatcher}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[derive(Drop, Serde, starknet::Store)] + pub struct Delivery { + pub processor: ContractAddress, + pub block_number: u64, + } + + + #[storage] + struct Storage { + inbound_unprocessed_nonce: u32, + inbound_processed_nonce: u32, + remote_mailboxes: LegacyMap, + inbound_messages: LegacyMap, + eth_address: ContractAddress, + // Domain of chain on which the contract is deployed + local_domain: u32, + // A monotonically increasing nonce for outbound unique message IDs. + nonce: u32, + // The latest dispatched message ID used for auth in post-dispatch hooks. + latest_dispatched_id: u256, + // The default ISM, used if the recipient fails to specify one. + default_ism: ContractAddress, + // The default post dispatch hook, used for post processing of opting-in dispatches. + default_hook: ContractAddress, + // The required post dispatch hook, used for post processing of ALL dispatches. + required_hook: ContractAddress, + // Mapping of message ID to delivery context that processed the message. + deliveries: LegacyMap::, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + DefaultIsmSet: DefaultIsmSet, + DefaultHookSet: DefaultHookSet, + RequiredHookSet: RequiredHookSet, + Process: Process, + ProcessId: ProcessId, + Dispatch: Dispatch, + DispatchId: DispatchId, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + #[derive(starknet::Event, Drop)] + pub struct DefaultIsmSet { + pub module: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct DefaultHookSet { + pub hook: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct RequiredHookSet { + pub hook: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct Process { + pub origin: u32, + pub sender: u256, + pub recipient: u256 + } + + #[derive(starknet::Event, Drop)] + pub struct ProcessId { + pub id: u256 + } + + #[derive(starknet::Event, Drop)] + pub struct Dispatch { + pub sender: u256, + pub destination_domain: u32, + pub recipient_address: u256, + pub message: Message + } + + #[derive(starknet::Event, Drop)] + pub struct DispatchId { + pub id: u256 + } + + + pub mod Errors { + pub const WRONG_HYPERLANE_VERSION: felt252 = 'Wrong hyperlane version'; + pub const UNEXPECTED_DESTINATION: felt252 = 'Unexpected destination'; + pub const ALREADY_DELIVERED: felt252 = 'Mailbox: already delivered'; + pub const ISM_VERIFICATION_FAILED: felt252 = 'Mailbox:ism verification failed'; + pub const ISM_CANNOT_BE_NULL: felt252 = 'ISM cannot be null'; + pub const OWNER_CANNOT_BE_NULL: felt252 = 'ISM cannot be null'; + pub const HOOK_CANNOT_BE_NULL: felt252 = 'Hook cannot be null'; + pub const NO_ISM_FOUND: felt252 = 'ISM: no ISM found'; + pub const NEW_OWNER_IS_ZERO: felt252 = 'Ownable: new owner cannot be 0'; + pub const ALREADY_OWNER: felt252 = 'Ownable: already owner'; + pub const INSUFFICIENT_BALANCE: felt252 = 'Insufficient balance'; + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Insufficient allowance'; + pub const NOT_ENOUGH_FEE_PROVIDED: felt252 = 'Provided fee < needed fee'; + pub const SIZE_DOES_NOT_MATCH_MESSAGE_BODY: felt252 = 'Size does not match msg body'; + pub const SIZE_DOES_NOT_MATCH_METADATA: felt252 = 'Size does not match metadata'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + _local_domain: u32, + _default_ism: ContractAddress, + hook: ContractAddress, + eth_address: ContractAddress + ) { + self.local_domain.write(_local_domain); + self.default_ism.write(_default_ism); + self.default_hook.write(hook); + self.required_hook.write(hook); + self.eth_address.write(eth_address); + self.ownable.initializer(get_caller_address()); + } + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IMailboxImpl of super::IMockMailbox { + fn add_remote_mail_box(ref self: ContractState, domain: u32, mailbox: ContractAddress) { + self.remote_mailboxes.write(domain, mailbox); + } + fn dispatch( + ref self: ContractState, + destination_domain: u32, + recipient_address: u256, + message_body: Bytes, + fee_amount: u256, + metadata: Option, + hook: Option + ) -> u256 { + let hook = match hook { + Option::Some(hook) => { hook }, + Option::None(()) => { self.default_hook.read() } + }; + let (id, message) = build_message( + @self, destination_domain, recipient_address, message_body.clone() + ); + self.latest_dispatched_id.write(id); + let curr_nonce = self.nonce.read(); + self.nonce.write(curr_nonce + 1); + let caller_address: felt252 = starknet::get_caller_address().into(); + self + .emit( + Dispatch { + sender: caller_address.into(), + destination_domain, + recipient_address, + message: message.clone() + } + ); + self.emit(DispatchId { id }); + + let metadata = match metadata { + Option::Some(metadata) => metadata, + Option::None(()) => BytesTrait::new_empty() + }; + let required_hook = ITestPostDispatchHookDispatcher { + contract_address: self.required_hook.read() + }; + required_hook.post_dispatch(metadata.clone(), message.clone()); + let hook = ITestPostDispatchHookDispatcher { contract_address: hook }; + hook.post_dispatch(metadata, message.clone()); + let remote_mailbox = self.remote_mailboxes.read(destination_domain); + assert!(remote_mailbox != contract_address_const::<0>()); + IMockMailboxDispatcher { contract_address: remote_mailbox } + .add_inbound_message(message); + id + } + + fn add_inbound_message(ref self: ContractState, message: Message) { + self.inbound_messages.write(self.inbound_unprocessed_nonce.read(), message); + self.inbound_unprocessed_nonce.write(self.inbound_unprocessed_nonce.read() + 1); + } + + fn process_next_inbound_message(ref self: ContractState) { + let message = self.inbound_messages.read(self.inbound_processed_nonce.read()); + IMailboxDispatcher { contract_address: starknet::get_contract_address() } + .process(BytesTrait::new_empty(), message); + self.inbound_processed_nonce.write(self.inbound_processed_nonce.read() + 1); + } + fn get_local_domain(self: @ContractState) -> u32 { + self.local_domain.read() + } + + fn get_default_ism(self: @ContractState) -> ContractAddress { + self.default_ism.read() + } + + fn get_default_hook(self: @ContractState) -> ContractAddress { + self.default_hook.read() + } + + fn get_required_hook(self: @ContractState) -> ContractAddress { + self.required_hook.read() + } + + fn get_latest_dispatched_id(self: @ContractState) -> u256 { + self.latest_dispatched_id.read() + } + + /// Sets the default ISM for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new default ISM + fn set_default_ism(ref self: ContractState, _module: ContractAddress) { + self.ownable.assert_only_owner(); + self.default_ism.write(_module); + self.emit(DefaultIsmSet { module: _module }); + } + + /// Sets the default post dispatch hook for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new default post dispatch hook. + fn set_default_hook(ref self: ContractState, _hook: ContractAddress) { + self.ownable.assert_only_owner(); + self.default_hook.write(_hook); + self.emit(DefaultHookSet { hook: _hook }); + } + + /// Sets the required post dispatch hook for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new required post dispatch hook. + fn set_required_hook(ref self: ContractState, _hook: ContractAddress) { + self.ownable.assert_only_owner(); + self.required_hook.write(_hook); + self.emit(RequiredHookSet { hook: _hook }); + } + + + /// Returns true if the message has been processed. + /// + /// # Arguments + /// + /// * `_message_id` - The message ID to check. + /// + /// # Returns + /// + /// * True if the message has been delivered. + fn delivered(self: @ContractState, _message_id: u256) -> bool { + self.deliveries.read(_message_id).block_number > 0 + } + + fn nonce(self: @ContractState) -> u32 { + self.nonce.read() + } + + /// Attempts to deliver `_message` to its recipient. Verifies `_message` via the recipient's ISM using the provided `_metadata` + /// + /// # Arguments + /// + /// * `_metadata` - Metadata used by the ISM to verify `_message`. + /// * `_message` - Formatted Hyperlane message (ref: message.cairo) + fn process(ref self: ContractState, _metadata: Bytes, _message: Message) { + let mut sanitized_bytes_metadata = BytesTrait::new_empty(); + sanitized_bytes_metadata.concat(@_metadata); + assert(sanitized_bytes_metadata == _metadata, Errors::SIZE_DOES_NOT_MATCH_METADATA); + let mut sanitized_bytes_message_body = BytesTrait::new_empty(); + sanitized_bytes_message_body.concat(@_message.body); + assert( + sanitized_bytes_message_body == _message.body, + Errors::SIZE_DOES_NOT_MATCH_MESSAGE_BODY + ); + + assert(_message.version == HYPERLANE_VERSION, Errors::WRONG_HYPERLANE_VERSION); + assert( + _message.destination == self.local_domain.read(), Errors::UNEXPECTED_DESTINATION + ); + let (id, _) = MessageTrait::format_message(_message.clone()); + let caller = get_caller_address(); + let block_number = get_block_number(); + assert(!self.delivered(id), Errors::ALREADY_DELIVERED); + + self.deliveries.write(id, Delivery { processor: caller, block_number: block_number }); + + let recipient_ism = self.recipient_ism(_message.recipient); + let ism = IInterchainSecurityModuleDispatcher { contract_address: recipient_ism }; + + self + .emit( + Process { + origin: _message.origin, + sender: _message.sender, + recipient: _message.recipient + } + ); + self.emit(ProcessId { id: id }); + + assert(ism.verify(_metadata, _message.clone()), Errors::ISM_VERIFICATION_FAILED); + + let message_recipient = IMessageRecipientDispatcher { + contract_address: _message.recipient.try_into().unwrap() + }; + message_recipient.handle(_message.origin, _message.sender, _message.body); + } + + /// Computes quote for dispatching a message to the destination domain & recipient. + /// + /// # Arguments + /// + /// * `_destination_domain` - Domain of destination chain + /// * `_recipient_address` - Address of recipient on destination chain + /// * `_message_body` - Raw bytes content of message body + /// * `_custom_hook_metadata` - Metadata used by the post dispatch hook + /// * `_custom_hook` - Custom hook to use instead of the default + /// + /// # Returns + /// + /// * The payment required to dispatch the message + fn quote_dispatch( + self: @ContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes, + _custom_hook_metadata: Option, + _custom_hook: Option, + ) -> u256 { + let hook_address = match _custom_hook { + Option::Some(hook) => hook, + Option::None(()) => self.default_hook.read() + }; + let hook_metadata = match _custom_hook_metadata { + Option::Some(hook_metadata) => hook_metadata, + Option::None(()) => BytesTrait::new_empty(), + }; + let (_, message) = build_message( + self, _destination_domain, _recipient_address, _message_body.clone() + ); + let required_hook_address = self.required_hook.read(); + let required_hook = ITestPostDispatchHookDispatcher { + contract_address: required_hook_address + }; + let hook = ITestPostDispatchHookDispatcher { contract_address: hook_address }; + required_hook.quote_dispatch(hook_metadata.clone(), message.clone()) + + hook.quote_dispatch(hook_metadata, message) + } + + /// Returns the ISM to use for the recipient, defaulting to the default ISM if none is specified. + /// + /// # Arguments + /// + /// * `_recipient` - The message recipient whose ISM should be returned. + /// + /// # Returns + /// + /// * The ISM to use for `_recipient` + fn recipient_ism(self: @ContractState, _recipient: u256) -> ContractAddress { + let mut call_data: Array = ArrayTrait::new(); + let mut res = starknet::syscalls::call_contract_syscall( + _recipient.try_into().unwrap(), + selector!("interchain_security_module"), + call_data.span() + ); + let mut ism_res = match res { + Result::Ok(ism) => ism, + Result::Err(revert_reason) => { + assert(revert_reason == array!['ENTRYPOINT_FAILED'], Errors::NO_ISM_FOUND); + array![].span() + } + }; + if (ism_res.len() != 0) { + let ism_address = Serde::::deserialize(ref ism_res).unwrap(); + if (ism_address != contract_address_const::<0>()) { + return ism_address; + } + } + self.default_ism.read() + } + + /// Returns the account that processed the message. + /// + /// # Arguments + /// + /// * `_id` - The message ID to check. + /// + /// # Returns + /// + /// * The account that processed the message. + fn processor(self: @ContractState, _id: u256) -> ContractAddress { + self.deliveries.read(_id).processor + } + + /// Returns the account that processed the message. + /// + /// # Arguments + /// + /// * `_id` - The message ID to check. + /// + /// # Returns + /// + /// * The number of the block that the message was processed at. + fn processed_at(self: @ContractState, _id: u256) -> u64 { + self.deliveries.read(_id).block_number + } + } + + fn build_message( + self: @ContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes + ) -> (u256, Message) { + let nonce = self.nonce.read(); + let local_domain = self.local_domain.read(); + let caller: felt252 = get_caller_address().into(); + MessageTrait::format_message( + Message { + version: HYPERLANE_VERSION, + nonce: nonce, + origin: local_domain, + sender: caller.into(), + destination: _destination_domain, + recipient: _recipient_address, + body: _message_body + } + ) + } + #[generate_trait] + impl Private of PrivateTrait { + fn _dispatch( + ref self: ContractState, + _destination_domain: u32, + _recipient_address: u256, + _message_body: Bytes, + _fee_amount: u256, + _custom_hook_metadata: Option, + _custom_hook: Option + ) -> u256 { + let hook = match _custom_hook { + Option::Some(hook) => hook, + Option::None(()) => self.default_hook.read(), + }; + let hook_metadata = match _custom_hook_metadata { + Option::Some(hook_metadata) => { hook_metadata }, + Option::None(()) => BytesTrait::new_empty() + }; + let (id, message) = build_message( + @self, _destination_domain, _recipient_address, _message_body + ); + self.latest_dispatched_id.write(id); + let current_nonce = self.nonce.read(); + self.nonce.write(current_nonce + 1); + let caller: felt252 = get_caller_address().into(); + self + .emit( + Dispatch { + sender: caller.into(), + destination_domain: _destination_domain, + recipient_address: _recipient_address, + message: message.clone() + } + ); + self.emit(DispatchId { id: id }); + + // HOOKS + + let required_hook_address = self.required_hook.read(); + let required_hook = ITestPostDispatchHookDispatcher { + contract_address: required_hook_address + }; + let mut required_fee = required_hook + .quote_dispatch(hook_metadata.clone(), message.clone()); + let hook_dispatcher = ITestPostDispatchHookDispatcher { contract_address: hook }; + let default_fee = hook_dispatcher + .quote_dispatch(hook_metadata.clone(), message.clone()); + + assert(_fee_amount >= required_fee + default_fee, Errors::NOT_ENOUGH_FEE_PROVIDED); + + let caller_address = get_caller_address(); + let contract_address = get_contract_address(); + + let token_dispatcher = ERC20ABIDispatcher { contract_address: self.eth_address.read() }; + let user_balance = token_dispatcher.balanceOf(caller_address); + assert(user_balance >= required_fee + default_fee, Errors::INSUFFICIENT_BALANCE); + + assert( + token_dispatcher.allowance(caller_address, contract_address) >= _fee_amount, + Errors::INSUFFICIENT_ALLOWANCE + ); + + if (required_fee > 0) { + token_dispatcher.transferFrom(caller_address, required_hook_address, required_fee); + } + required_hook.post_dispatch(hook_metadata.clone(), message.clone()); + if (default_fee > 0) { + token_dispatcher.transferFrom(caller_address, hook, default_fee); + } + hook_dispatcher.post_dispatch(hook_metadata, message.clone()); + id + } + } +} diff --git a/starknet/cairo/crates/mocks/src/mock_validator_announce.cairo b/starknet/cairo/crates/mocks/src/mock_validator_announce.cairo new file mode 100644 index 00000000000..f4f49c3e8eb --- /dev/null +++ b/starknet/cairo/crates/mocks/src/mock_validator_announce.cairo @@ -0,0 +1,313 @@ +#[starknet::contract] +pub mod mock_validator_announce { + use alexandria_bytes::{Bytes, BytesTrait}; + use alexandria_data_structures::array_ext::ArrayTraitExt; + use contracts::interfaces::{ + IMailboxClientDispatcher, IMailboxClientDispatcherTrait, IValidatorAnnounce + }; + use contracts::libs::checkpoint_lib::checkpoint_lib::HYPERLANE_ANNOUNCEMENT; + use contracts::utils::keccak256::{ + reverse_endianness, to_eth_signature, compute_keccak, ByteData, u256_word_size, + u64_word_size, HASH_SIZE, bool_is_eth_signature_valid + }; + use contracts::utils::store_arrays::StoreFelt252Array; + use core::poseidon::poseidon_hash_span; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ + ContractAddress, ClassHash, EthAddress, secp256_trait::{Signature, signature_from_vrs} + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + mailbox: ContractAddress, // for testing purpose, we set directly the mailbox address here + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + domain: u32, + storage_location_len: LegacyMap::, + storage_locations: LegacyMap::<(EthAddress, u256), Array>, + replay_protection: LegacyMap::, + validators: LegacyMap::, + } + + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ValidatorAnnouncement: ValidatorAnnouncement, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + #[derive(starknet::Event, Drop)] + pub struct ValidatorAnnouncement { + pub validator: EthAddress, + pub storage_location: Span + } + + pub mod Errors { + pub const REPLAY_PROTECTION_ERROR: felt252 = 'Announce already occured'; + pub const WRONG_SIGNER: felt252 = 'Wrong signer'; + } + #[constructor] + fn constructor(ref self: ContractState, _mailbox: ContractAddress, _domain: u32) { + self.mailbox.write(_mailbox); + self.domain.write(_domain); + } + + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IValidatorAnnonceImpl of IValidatorAnnounce { + /// Announces a validator signature storage location + /// Dev: reverts if announce already occured or if wrong signer + /// + /// # Arguments + /// + /// * - `_validator` - The validator to consider + /// * - `_storage_location` - Information encoding the location of signed + /// * - `_signature` -The signed validator announcement + /// + /// # Returns + /// + /// boolean - True upon success + fn announce( + ref self: ContractState, + _validator: EthAddress, + _storage_location: Array, + _signature: Bytes + ) -> bool { + let felt252_validator: felt252 = _validator.into(); + let mut _input: Array = array![felt252_validator.into()]; + let mut u256_storage_location: Array = array![]; + let mut cur_idx = 0; + let span_storage_location = _storage_location.span(); + loop { + if (cur_idx == span_storage_location.len()) { + break (); + } + u256_storage_location.append((*span_storage_location.at(cur_idx)).into()); + cur_idx += 1; + }; + let replay_id = poseidon_hash_span( + array![felt252_validator].concat(@_storage_location).span() + ); + assert(!self.replay_protection.read(replay_id), Errors::REPLAY_PROTECTION_ERROR); + // let announcement_digest = self.get_announcement_digest(u256_storage_location); + // let signature: Signature = self.convert_to_signature(_signature); + // Remove the signature verification ONLY for testing double announcement (assuming validator signature is correct in both cases) + // assert( + // bool_is_eth_signature_valid(announcement_digest, signature, _validator), + // Errors::WRONG_SIGNER + // ); + match self.find_validators_index(_validator) { + Option::Some => {}, + Option::None => { + let last_validator = self.find_last_validator(); + self.validators.write(last_validator, _validator); + } + }; + let mut validator_len = self.storage_location_len.read(_validator); + self.storage_locations.write((_validator, validator_len), _storage_location); + self.storage_location_len.write(_validator, validator_len + 1); + self.replay_protection.write(replay_id, true); + self + .emit( + ValidatorAnnouncement { + validator: _validator, storage_location: span_storage_location + } + ); + true + } + + + /// Returns a list of all announced storage locations + /// + /// # Arguments + /// + /// * - `_validators` - The span of validators to get registrations for + /// + /// # Returns + /// + /// Span> - A list of registered storage metadata + fn get_announced_storage_locations( + self: @ContractState, mut _validators: Span + ) -> Span>> { + let mut metadata = array![]; + loop { + match _validators.pop_front() { + Option::Some(validator) => { + let mut cur_idx = 0; + let validator_len = self.storage_location_len.read(*validator); + let mut validator_metadata = array![]; + loop { + if (cur_idx == validator_len) { + break (); + } + validator_metadata + .append(self.storage_locations.read((*validator, cur_idx))); + cur_idx += 1; + }; + metadata.append(validator_metadata.span()) + }, + Option::None => { break (); } + } + }; + metadata.span() + } + + /// Returns a list of validators that have made announcements + fn get_announced_validators(self: @ContractState) -> Span { + self.build_validators_array() + } + + + /// Returns the digest validators are expected to sign when signing announcements. + /// + /// # Arguments + /// + /// * - `_storage_location` - Storage location as array of u256 + /// + /// # Returns + /// + /// u256 - The digest of the announcement. + fn get_announcement_digest( + self: @ContractState, mut _storage_location: Array + ) -> u256 { + let mailboxclient_address = self.mailbox.read(); + let domain = self.domain.read(); + let domain_hash = self.domain_hash(mailboxclient_address, domain); + let mut byte_data_storage_location = array![]; + loop { + match _storage_location.pop_front() { + Option::Some(storage) => { + byte_data_storage_location + .append( + ByteData { value: storage, size: u256_word_size(storage).into() } + ); + }, + Option::None(_) => { break (); } + } + }; + let hash = reverse_endianness( + compute_keccak( + array![ByteData { value: domain_hash, size: HASH_SIZE }] + .concat(@byte_data_storage_location) + .span() + ) + ); + to_eth_signature(hash) + } + } + + #[generate_trait] + pub impl ValidatorAnnounceInternalImpl of InternalTrait { + /// Converts a byte signature into a standard singature format (see Signature structure) + /// + /// # Arguments + /// + /// * - ` _signature` - The byte encoded Signature + /// + /// # Returns + /// + /// Signature - Standardized signature + fn convert_to_signature(self: @ContractState, _signature: Bytes) -> Signature { + let (_, r) = _signature.read_u256(0); + let (_, s) = _signature.read_u256(32); + let (_, v) = _signature.read_u8(64); + signature_from_vrs(v.try_into().unwrap(), r, s) + } + + /// Returns the domain separator used in validator announcements. + fn domain_hash( + self: @ContractState, _mailbox_address: ContractAddress, _domain: u32 + ) -> u256 { + let felt_address: felt252 = _mailbox_address.into(); + let mut input: Array = array![ + ByteData { value: _domain.into(), size: u64_word_size(_domain.into()).into() }, + ByteData { + value: felt_address.into(), size: u256_word_size(felt_address.into()).into() + }, + ByteData { value: HYPERLANE_ANNOUNCEMENT.into(), size: 22 } + ]; + reverse_endianness(compute_keccak(input.span())) + } + /// Helper: finds the index associated to a given validator, if found + /// Dev: Chained list (EthereumAddress -> EthereumAddress) + /// + /// # Arguments + /// + /// * - `_validator` - The validator to consider + /// + /// # Returns + /// + /// EthAddress - the index of the validator in the Storage Map + fn find_validators_index( + self: @ContractState, _validator: EthAddress + ) -> Option { + let mut current_validator: EthAddress = 0.try_into().unwrap(); + loop { + let next_validator = self.validators.read(current_validator); + if next_validator == _validator { + break Option::Some(current_validator); + } else if next_validator == 0.try_into().unwrap() { + break Option::None; + } + current_validator = next_validator; + } + } + + /// Helper: finds the last stored validator + fn find_last_validator(self: @ContractState) -> EthAddress { + let mut current_validator = self.validators.read(0.try_into().unwrap()); + loop { + let next_validator = self.validators.read(current_validator); + if next_validator == 0.try_into().unwrap() { + break current_validator; + } + current_validator = next_validator; + } + } + + // Helper: builds a span of validators from the storage map + fn build_validators_array(self: @ContractState) -> Span { + let mut index = 0.try_into().unwrap(); + let mut validators = array![]; + loop { + let validator = self.validators.read(index); + if (validator == 0.try_into().unwrap()) { + break (); + } + validators.append(validator); + index = validator; + }; + + validators.span() + } + } +} diff --git a/starknet/cairo/crates/mocks/src/test_erc20.cairo b/starknet/cairo/crates/mocks/src/test_erc20.cairo new file mode 100644 index 00000000000..e4fd962d554 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/test_erc20.cairo @@ -0,0 +1,95 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait ITestERC20 { + fn total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; + fn decimals(self: @TContractState) -> u8; + fn mint(ref self: TContractState, to: ContractAddress, amount: u256) -> bool; + fn mint_to(ref self: TContractState, to: ContractAddress, amount: u256); + fn burn_from(ref self: TContractState, from: ContractAddress, amount: u256); + fn burn(ref self: TContractState, amount: u256); +} + +#[starknet::contract] +pub mod TestERC20 { + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + decimals: u8, + #[substorage(v0)] + erc20: ERC20Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, total_supply: u256, decimals: u8) { + self.decimals.write(decimals); + self.erc20.mint(starknet::get_caller_address(), total_supply); + } + + #[abi(embed_v0)] + impl ITestERC20 of super::ITestERC20 { + fn total_supply(self: @ContractState) -> u256 { + self.erc20.total_supply() + } + fn decimals(self: @ContractState) -> u8 { + self.decimals.read() + } + fn mint(ref self: ContractState, to: ContractAddress, amount: u256) -> bool { + self.erc20.mint(to, amount); + true + } + fn mint_to(ref self: ContractState, to: ContractAddress, amount: u256) { + self.erc20.mint(to, amount); + } + fn burn_from(ref self: ContractState, from: ContractAddress, amount: u256) { + self.erc20.burn(from, amount); + } + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + self.erc20.approve(spender, amount) + } + fn burn(ref self: ContractState, amount: u256) { + self.erc20.burn(starknet::get_caller_address(), amount); + } + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + self.erc20.transfer(recipient, amount) + } + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.balance_of(account) + } + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.erc20.transfer_from(sender, recipient, amount) + } + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.erc20.allowance(owner, spender) + } + } +} + diff --git a/starknet/cairo/crates/mocks/src/test_erc721.cairo b/starknet/cairo/crates/mocks/src/test_erc721.cairo new file mode 100644 index 00000000000..2e2483d541c --- /dev/null +++ b/starknet/cairo/crates/mocks/src/test_erc721.cairo @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts for Cairo ^0.15.0 +use starknet::ContractAddress; + +#[starknet::interface] +pub trait ITestERC721 { + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn owner_of(self: @TState, token_id: u256) -> ContractAddress; + fn safe_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ); + fn transfer_from(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256); + fn approve(ref self: TState, to: ContractAddress, token_id: u256); + fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); + fn get_approved(self: @TState, token_id: u256) -> ContractAddress; + fn is_approved_for_all( + self: @TState, owner: ContractAddress, operator: ContractAddress + ) -> bool; + fn token_uri(self: @TState, token_id: u256) -> ByteArray; +} + +#[starknet::contract] +mod TestERC721 { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::ERC721Component; + use openzeppelin::token::erc721::ERC721HooksEmptyImpl; + use starknet::ContractAddress; + + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + impl ERC721Impl = ERC721Component::ERC721Impl; + impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, mint_amount: u256) { + self.erc721.initializer("Hyperlane Hedgehogs", "HHH", "http://bit.ly/3reJLpx"); + let mut i: u256 = 0; + while i < mint_amount { + self.erc721.mint(starknet::get_caller_address(), i); + i += 1; + } + } + + #[abi(embed_v0)] + impl TestERC721Impl of super::ITestERC721 { + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc721.balance_of(account) + } + + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + self.erc721.owner_of(token_id) + } + + fn safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ) { + self.erc721.safe_transfer_from(from, to, token_id, data) + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + self.erc721.transfer_from(from, to, token_id) + } + + fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { + self.erc721.approve(to, token_id) + } + + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool + ) { + self.erc721.set_approval_for_all(operator, approved) + } + + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + self.erc721.get_approved(token_id) + } + + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.erc721.is_approved_for_all(owner, operator) + } + + fn token_uri(self: @ContractState, token_id: u256) -> ByteArray { + self.erc721.token_uri(token_id) + } + } +} diff --git a/starknet/cairo/crates/mocks/src/test_interchain_gas_payment.cairo b/starknet/cairo/crates/mocks/src/test_interchain_gas_payment.cairo new file mode 100644 index 00000000000..f42dcb5dec7 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/test_interchain_gas_payment.cairo @@ -0,0 +1,70 @@ +use alexandria_bytes::Bytes; +use contracts::libs::message::Message; + +#[starknet::interface] +pub trait ITestInterchainGasPayment { + fn quote_gas_payment(self: @TContractState, gas_amount: u256) -> u256; + fn get_default_gas_usage(self: @TContractState) -> u256; + fn gas_price(self: @TContractState) -> u256; + fn post_dispatch(ref self: TContractState, metadata: Bytes, message: Message); +} + +#[starknet::contract] +pub mod TestInterchainGasPayment { + use alexandria_bytes::Bytes; + use contracts::libs::message::{Message, MessageTrait}; + use openzeppelin::access::ownable::OwnableComponent; + use starknet::ContractAddress; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[storage] + struct Storage { + gas_price: u256, + beneficiary: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + #[constructor] + fn constructor(ref self: ContractState) { + let caller = starknet::get_caller_address(); + self.initialize(caller, caller); + } + + #[abi(embed_v0)] + impl TestInterchainGasPaymentImpl of super::ITestInterchainGasPayment { + fn quote_gas_payment(self: @ContractState, gas_amount: u256) -> u256 { + self.gas_price.read() * gas_amount + } + + fn get_default_gas_usage(self: @ContractState) -> u256 { + 50_000 + } + + fn gas_price(self: @ContractState) -> u256 { + self.gas_price.read() + } + fn post_dispatch(ref self: ContractState, metadata: Bytes, message: Message) {} + } + + #[generate_trait] + impl Private of PrivateTrait { + fn initialize( + ref self: ContractState, owner: ContractAddress, beneficiary: ContractAddress, + ) { + self.gas_price.write(10); + self.beneficiary.write(beneficiary); + self.ownable.initializer(owner); + } + } +} diff --git a/starknet/cairo/crates/mocks/src/test_ism.cairo b/starknet/cairo/crates/mocks/src/test_ism.cairo new file mode 100644 index 00000000000..cdaaebc7cd3 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/test_ism.cairo @@ -0,0 +1,34 @@ +use alexandria_bytes::Bytes; +use contracts::libs::message::Message; +#[starknet::interface] +pub trait ITestISM { + fn set_verify(ref self: TContractState, verify: bool); + fn verify(self: @TContractState, _metadata: Bytes, _message: Message) -> bool; +} + +#[starknet::contract] +pub mod TestISM { + use alexandria_bytes::Bytes; + use super::ITestISMDispatcher; + + #[storage] + struct Storage { + verify_result: bool, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.verify_result.write(true); + } + + #[abi(embed_v0)] + impl TestISMImpl of super::ITestISM { + fn set_verify(ref self: ContractState, verify: bool) { + self.verify_result.write(verify); + } + + fn verify(self: @ContractState, _metadata: Bytes, _message: super::Message) -> bool { + self.verify_result.read() + } + } +} diff --git a/starknet/cairo/crates/mocks/src/test_post_dispatch_hook.cairo b/starknet/cairo/crates/mocks/src/test_post_dispatch_hook.cairo new file mode 100644 index 00000000000..35b1e9a7837 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/test_post_dispatch_hook.cairo @@ -0,0 +1,62 @@ +use alexandria_bytes::Bytes; +use contracts::libs::message::{Message, MessageTrait}; + +#[starknet::interface] +pub trait ITestPostDispatchHook { + fn hook_type(self: @TContractState) -> u8; + fn supports_metadata(self: @TContractState, _metadata: Bytes) -> bool; + fn set_fee(ref self: TContractState, fee: u256); + fn message_dispatched(self: @TContractState, message_id: u256) -> bool; + fn post_dispatch(ref self: TContractState, metadata: Bytes, message: Message); + fn quote_dispatch(ref self: TContractState, metadata: Bytes, message: Message) -> u256; +} + +#[starknet::contract] +pub mod TestPostDispatchHook { + use alexandria_bytes::Bytes; + use contracts::libs::message::{Message, MessageTrait}; + use core::keccak::keccak_u256s_le_inputs; + + #[storage] + struct Storage { + fee: u256, + message_dispatched: LegacyMap, + } + + #[abi(embed_v0)] + impl TestPostDispatchHookImpl of super::ITestPostDispatchHook { + fn hook_type(self: @ContractState) -> u8 { + 0 + } + + fn supports_metadata(self: @ContractState, _metadata: Bytes) -> bool { + true + } + + fn set_fee(ref self: ContractState, fee: u256) { + self.fee.write(fee); + } + + fn message_dispatched(self: @ContractState, message_id: u256) -> bool { + self.message_dispatched.read(message_id) + } + + fn post_dispatch(ref self: ContractState, metadata: Bytes, message: Message) { + let hash = keccak_u256s_le_inputs( + array![ + message.nonce.into(), + message.origin.into(), + message.sender, + message.destination.into(), + message.recipient + ] + .span() + ); + self.message_dispatched.write(hash, true); + } + + fn quote_dispatch(ref self: ContractState, metadata: Bytes, message: Message) -> u256 { + self.fee.read() + } + } +} diff --git a/starknet/cairo/crates/mocks/src/xerc20_lockbox_test.cairo b/starknet/cairo/crates/mocks/src/xerc20_lockbox_test.cairo new file mode 100644 index 00000000000..a2a258a3249 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/xerc20_lockbox_test.cairo @@ -0,0 +1,67 @@ +#[starknet::interface] +pub trait IXERC20LockboxTest { + fn xerc20(self: @TContractState) -> starknet::ContractAddress; + fn erc20(self: @TContractState) -> starknet::ContractAddress; + fn deposit_to(ref self: TContractState, user: starknet::ContractAddress, amount: u256); + fn deposit(ref self: TContractState, amount: u256); + fn withdraw_to(ref self: TContractState, user: u256, amount: u256); + fn withdraw(ref self: TContractState, amount: u256); +} + +#[starknet::contract] +pub mod XERC20LockboxTest { + use mocks::{ + test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait}, + xerc20_test::{IXERC20TestDispatcher, IXERC20TestDispatcherTrait} + }; + use starknet::ContractAddress; + + #[storage] + struct Storage { + XERC20: ContractAddress, + ERC20: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, xerc20: ContractAddress, erc20: ContractAddress) { + self.XERC20.write(xerc20); + self.ERC20.write(erc20); + } + + #[abi(embed_v0)] + impl IXERC20LockboxTest of super::IXERC20LockboxTest { + fn xerc20(self: @ContractState) -> ContractAddress { + self.XERC20.read() + } + + fn erc20(self: @ContractState) -> ContractAddress { + self.ERC20.read() + } + + fn deposit_to(ref self: ContractState, user: starknet::ContractAddress, amount: u256) { + let erc20 = ITestERC20Dispatcher { contract_address: self.ERC20.read() }; + erc20 + .transfer_from( + starknet::get_caller_address(), starknet::get_contract_address(), amount + ); + let xerc20 = IXERC20TestDispatcher { contract_address: self.XERC20.read() }; + xerc20.mint(user, amount); + } + fn deposit(ref self: ContractState, amount: u256) { + self.deposit_to(starknet::get_caller_address(), amount); + } + + fn withdraw_to(ref self: ContractState, user: u256, amount: u256) { + let xerc20 = IXERC20TestDispatcher { contract_address: self.XERC20.read() }; + xerc20.burn(starknet::get_caller_address(), amount); + let erc20 = ITestERC20Dispatcher { contract_address: self.ERC20.read() }; + let user_address: felt252 = user.try_into().unwrap(); + erc20.mint_to(user_address.try_into().unwrap(), amount); + } + + fn withdraw(ref self: ContractState, amount: u256) { + let caller_address: felt252 = starknet::get_caller_address().into(); + self.withdraw_to(caller_address.into(), amount); + } + } +} diff --git a/starknet/cairo/crates/mocks/src/xerc20_test.cairo b/starknet/cairo/crates/mocks/src/xerc20_test.cairo new file mode 100644 index 00000000000..dbf24628d58 --- /dev/null +++ b/starknet/cairo/crates/mocks/src/xerc20_test.cairo @@ -0,0 +1,123 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IXERC20Test { + fn mint(ref self: TContractState, account: ContractAddress, amount: u256); + fn burn(ref self: TContractState, account: ContractAddress, amount: u256); + fn set_limits(ref self: TContractState, address: ContractAddress, arg1: u256, arg2: u256); + fn owner(self: @TContractState) -> ContractAddress; + fn burning_current_limit_of(self: @TContractState, bridge: ContractAddress) -> u256; + fn minting_current_limit_of(self: @TContractState, bridge: ContractAddress) -> u256; + fn minting_max_limit_of(self: @TContractState, bridge: ContractAddress) -> u256; + fn burning_max_limit_of(self: @TContractState, bridge: ContractAddress) -> u256; + fn total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; +} + +#[starknet::contract] +pub mod XERC20Test { + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + decimals: u8, + #[substorage(v0)] + erc20: ERC20Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, total_supply: u256, decimals: u8) { + self.decimals.write(decimals); + self.erc20.mint(starknet::get_caller_address(), total_supply); + } + + #[abi(embed_v0)] + impl IXERC20TestImpl of super::IXERC20Test { + fn mint(ref self: ContractState, account: starknet::ContractAddress, amount: u256) { + self.erc20.mint(account, amount); + } + + fn burn(ref self: ContractState, account: starknet::ContractAddress, amount: u256) { + self.erc20.burn(account, amount); + } + + fn set_limits( + ref self: ContractState, address: starknet::ContractAddress, arg1: u256, arg2: u256 + ) { + assert!(false); + } + + fn owner(self: @ContractState) -> starknet::ContractAddress { + starknet::contract_address_const::<0x0>() + } + + fn burning_current_limit_of( + self: @ContractState, bridge: starknet::ContractAddress + ) -> u256 { + core::integer::BoundedInt::::max() + } + + fn minting_current_limit_of( + self: @ContractState, bridge: starknet::ContractAddress + ) -> u256 { + core::integer::BoundedInt::::max() + } + + fn minting_max_limit_of(self: @ContractState, bridge: starknet::ContractAddress) -> u256 { + core::integer::BoundedInt::::max() + } + + fn burning_max_limit_of(self: @ContractState, bridge: starknet::ContractAddress) -> u256 { + core::integer::BoundedInt::::max() + } + fn total_supply(self: @ContractState) -> u256 { + self.erc20.total_supply() + } + fn balance_of(self: @ContractState, account: starknet::ContractAddress) -> u256 { + self.erc20.balance_of(account) + } + fn allowance( + self: @ContractState, + owner: starknet::ContractAddress, + spender: starknet::ContractAddress + ) -> u256 { + self.erc20.allowance(owner, spender) + } + fn transfer( + ref self: ContractState, recipient: starknet::ContractAddress, amount: u256 + ) -> bool { + self.erc20.transfer(recipient, amount) + } + fn transfer_from( + ref self: ContractState, + sender: starknet::ContractAddress, + recipient: starknet::ContractAddress, + amount: u256 + ) -> bool { + self.erc20.transfer_from(sender, recipient, amount) + } + fn approve( + ref self: ContractState, spender: starknet::ContractAddress, amount: u256 + ) -> bool { + self.erc20.approve(spender, amount) + } + } +} diff --git a/starknet/cairo/crates/token/Scarb.toml b/starknet/cairo/crates/token/Scarb.toml new file mode 100644 index 00000000000..16c65c5c176 --- /dev/null +++ b/starknet/cairo/crates/token/Scarb.toml @@ -0,0 +1,30 @@ +[package] +name = "token" +version = "0.0.1" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet.workspace = true +alexandria_bytes.workspace = true +alexandria_storage.workspace = true +openzeppelin.workspace = true +contracts = { path = "../contracts" } +mocks = { path = "../mocks" } + +[dev-dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.22.0" } + +[tool] +fmt.workspace = true + +[[target.starknet-contract]] +allowed-libfuncs-list.name = "experimental" +sierra = true +casm = true +casm-add-pythonic-hints = true +build-external-contracts = ["mocks::*"] + +[lib] +name = "token" diff --git a/starknet/cairo/crates/token/src/components/erc721_enumerable.cairo b/starknet/cairo/crates/token/src/components/erc721_enumerable.cairo new file mode 100644 index 00000000000..d4e3f2b1e88 --- /dev/null +++ b/starknet/cairo/crates/token/src/components/erc721_enumerable.cairo @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 +// (token/erc721/extensions/erc721_enumerable/interface.cairo) + +use starknet::ContractAddress; + +pub const IERC721ENUMERABLE_ID: felt252 = + 0x16bc0f502eeaf65ce0b3acb5eea656e2f26979ce6750e8502a82f377e538c87; + +#[starknet::interface] +pub trait IERC721Enumerable { + fn total_supply(self: @TState) -> u256; + fn token_by_index(self: @TState, index: u256) -> u256; + fn token_of_owner_by_index(self: @TState, owner: ContractAddress, index: u256) -> u256; +} + +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.16.0 +// (token/erc721/extensions/erc721_enumerable/erc721_enumerable.cairo) + +/// # ERC721Enumerable Component +/// +/// Extension of ERC721 as defined in the EIP that adds enumerability of all the token ids in the +/// contract as well as all token ids owned by each account. +/// This extension allows contracts to publish their entire list of NFTs and make them discoverable. +/// +/// NOTE: Implementing ERC721Component is a requirement for this component to be implemented. +/// +/// WARNING: To properly track token ids, this extension requires that +/// the ERC721EnumerableComponent::before_update function is called after +/// every transfer, mint, or burn operation. +/// For this, the ERC721HooksTrait::before_update hook must be used. +#[starknet::component] +pub mod ERC721EnumerableComponent { + use core::num::traits::Zero; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::ERC721Component::ERC721Impl; + use openzeppelin::token::erc721::ERC721Component::InternalImpl as ERC721InternalImpl; + use openzeppelin::token::erc721::ERC721Component; + use starknet::ContractAddress; + + #[storage] + pub struct Storage { + pub ERC721Enumerable_owned_tokens: LegacyMap<(ContractAddress, u256), u256>, + pub ERC721Enumerable_owned_tokens_index: LegacyMap, + pub ERC721Enumerable_all_tokens_len: u256, + pub ERC721Enumerable_all_tokens: LegacyMap, + pub ERC721Enumerable_all_tokens_index: LegacyMap + } + + pub mod Errors { + pub const OUT_OF_BOUNDS_INDEX: felt252 = 'ERC721Enum: out of bounds index'; + } + + #[embeddable_as(ERC721EnumerableImpl)] + impl ERC721Enumerable< + TContractState, + +HasComponent, + impl ERC721: ERC721Component::HasComponent, + +ERC721Component::ERC721HooksTrait, + +SRC5Component::HasComponent, + +Drop + > of super::IERC721Enumerable> { + /// Returns the total amount of tokens stored by the contract. + fn total_supply(self: @ComponentState) -> u256 { + self.ERC721Enumerable_all_tokens_len.read() + } + + /// Returns a token id at a given `index` of all the tokens stored by the contract. + /// Use along with `total_supply` to enumerate all tokens. + /// + /// Requirements: + /// + /// - `index` is less than the total token supply. + fn token_by_index(self: @ComponentState, index: u256) -> u256 { + assert(index < self.total_supply(), Errors::OUT_OF_BOUNDS_INDEX); + self.ERC721Enumerable_all_tokens.read(index) + } + + /// Returns the token id owned by `owner` at a given `index` of its token list. + /// Use along with `ERC721::balance_of` to enumerate all of `owner`'s tokens. + /// + /// Requirements: + /// + /// - `index` is less than `owner`'s token balance. + /// - `owner` is not the zero address. + fn token_of_owner_by_index( + self: @ComponentState, owner: ContractAddress, index: u256 + ) -> u256 { + let erc721_component = get_dep_component!(self, ERC721); + assert(index < erc721_component.balance_of(owner), Errors::OUT_OF_BOUNDS_INDEX); + self.ERC721Enumerable_owned_tokens.read((owner, index)) + } + } + + // + // Internal + // + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl ERC721: ERC721Component::HasComponent, + +ERC721Component::ERC721HooksTrait, + impl SRC5: SRC5Component::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the contract by declaring support for the `IERC721Enumerable` + /// interface id. + fn initializer(ref self: ComponentState) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(super::IERC721ENUMERABLE_ID); + } + + /// Updates the ownership and token-tracking data structures. + /// + /// When a token is minted (or burned), `token_id` is added to (or removed from) + /// the token-tracking structures. + /// + /// When a token is transferred, minted, or burned, the ownership-tracking data structures + /// reflect the change in ownership of `token_id`. + /// + /// This must be added to the implementing contract's `ERC721HooksTrait::before_update` + /// hook. + fn before_update( + ref self: ComponentState, to: ContractAddress, token_id: u256 + ) { + let erc721_component = get_dep_component!(@self, ERC721); + let previous_owner = erc721_component._owner_of(token_id); + + if previous_owner.is_zero() { + self._add_token_to_all_tokens_enumeration(token_id); + } else if previous_owner != to { + self._remove_token_from_owner_enumeration(previous_owner, token_id); + } + + if to.is_zero() { + self._remove_token_from_all_tokens_enumeration(token_id); + } else if previous_owner != to { + self._add_token_to_owner_enumeration(to, token_id); + } + } + + /// Adds token to this extension's ownership-tracking data structures. + fn _add_token_to_owner_enumeration( + ref self: ComponentState, to: ContractAddress, token_id: u256 + ) { + let mut erc721_component = get_dep_component_mut!(ref self, ERC721); + let len = erc721_component.balance_of(to); + self.ERC721Enumerable_owned_tokens.write((to, len), token_id); + self.ERC721Enumerable_owned_tokens_index.write(token_id, len); + } + + /// Adds token to this extension's token-tracking data structures. + fn _add_token_to_all_tokens_enumeration( + ref self: ComponentState, token_id: u256 + ) { + let supply = self.total_supply(); + self.ERC721Enumerable_all_tokens_index.write(token_id, supply); + self.ERC721Enumerable_all_tokens.write(supply, token_id); + self.ERC721Enumerable_all_tokens_len.write(supply + 1); + } + + /// Removes a token from this extension's ownership-tracking data structures. + /// + /// This has 0(1) time complexity but alters the indexed order of owned-tokens by + /// swapping `token_id` and the index thereof with the last token id and the index + /// thereof. + fn _remove_token_from_owner_enumeration( + ref self: ComponentState, from: ContractAddress, token_id: u256 + ) { + let erc721_component = get_dep_component!(@self, ERC721); + let last_token_index = erc721_component.balance_of(from) - 1; + let this_token_index = self.ERC721Enumerable_owned_tokens_index.read(token_id); + + // To prevent a gap in the token indexing of `from`, we store the last token + // in the index of the token to delete and then remove the last slot (swap and pop). + // When `token_id` is the last token, the swap operation is unnecessary + if this_token_index != last_token_index { + let last_token_id = self + .ERC721Enumerable_owned_tokens + .read((from, last_token_index)); + // Set `token_id` index to point to the last token id + self.ERC721Enumerable_owned_tokens.write((from, this_token_index), last_token_id); + // Set the last token id index to point to `token_id`'s index position + self.ERC721Enumerable_owned_tokens_index.write(last_token_id, this_token_index); + } + + // Set the last token index and `token_id` to zero + self.ERC721Enumerable_owned_tokens.write((from, last_token_index), 0); + self.ERC721Enumerable_owned_tokens_index.write(token_id, 0); + } + + /// Removes `token_id` from this extension's token-tracking data structures. + /// + /// This has 0(1) time complexity but alters the indexed order by swapping + /// `token_id` and the index thereof with the last token id and the index thereof. + fn _remove_token_from_all_tokens_enumeration( + ref self: ComponentState, token_id: u256 + ) { + let last_token_index = self.total_supply() - 1; + let this_token_index = self.ERC721Enumerable_all_tokens_index.read(token_id); + let last_token_id = self.ERC721Enumerable_all_tokens.read(last_token_index); + + // Set last token index to zero + self.ERC721Enumerable_all_tokens.write(last_token_index, 0); + // Set `token_id` index to 0 + self.ERC721Enumerable_all_tokens_index.write(token_id, 0); + // Remove one from total supply + self.ERC721Enumerable_all_tokens_len.write(last_token_index); + + // When the token to delete is the last token, the swap operation is unnecessary. + // However, since this occurs rarely (when the last minted token is burnt), we still do + // the swap which avoids the additional expense of including an `if` statement + self.ERC721Enumerable_all_tokens_index.write(last_token_id, this_token_index); + self.ERC721Enumerable_all_tokens.write(this_token_index, last_token_id); + } + } +} diff --git a/starknet/cairo/crates/token/src/components/erc721_uri_storage.cairo b/starknet/cairo/crates/token/src/components/erc721_uri_storage.cairo new file mode 100644 index 00000000000..42acccef7a3 --- /dev/null +++ b/starknet/cairo/crates/token/src/components/erc721_uri_storage.cairo @@ -0,0 +1,117 @@ +#[starknet::interface] +pub trait IERC721URIStorage { + fn name(self: @TContractState) -> ByteArray; + fn symbol(self: @TContractState) -> ByteArray; + fn token_uri(self: @TContractState, token_id: u256) -> ByteArray; +} + +#[starknet::component] +pub mod ERC721URIStorageComponent { + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::interface::IERC721Metadata; + use openzeppelin::token::erc721::{ + ERC721Component, ERC721Component::InternalTrait as ERC721InternalTrait, + ERC721Component::ERC721HooksTrait, ERC721Component::ERC721MetadataImpl + }; + + #[storage] + struct Storage { + token_uris: LegacyMap + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + MetadataUpdate: MetadataUpdate, + } + + #[derive(Drop, starknet::Event)] + struct MetadataUpdate { + token_id: u256, + } + + #[embeddable_as(ERC721URIStorageImpl)] + pub impl ERC721URIStorage< + TContractState, + +HasComponent, + +Drop, + +SRC5Component::HasComponent, + +ERC721HooksTrait, + impl ERC721: ERC721Component::HasComponent, + > of super::IERC721URIStorage> { + // returns the NFT name + fn name(self: @ComponentState) -> ByteArray { + let erc721_component = get_dep_component!(self, ERC721); + erc721_component.name() + } + + // returns the NFT symbol + fn symbol(self: @ComponentState) -> ByteArray { + let erc721_component = get_dep_component!(self, ERC721); + erc721_component.symbol() + } + + /// Returns the URI associated with a given `token_id`. + /// + /// This function retrieves the URI for an ERC721 token based on its `token_id`. + /// It first ensures that the token is owned by the caller, then checks the token-specific URI. + /// If the token has no specific URI, it appends the token's base URI if one exists. + /// + /// # Arguments + /// + /// * `token_id` - A `u256` representing the ID of the token whose URI is being queried. + /// + /// # Returns + /// + /// A `ByteArray` representing the URI associated with the token. If a specific URI is not found, + /// it may return the base URI or the token's metadata URI. + fn token_uri(self: @ComponentState, token_id: u256) -> ByteArray { + let erc721_component = get_dep_component!(self, ERC721); + erc721_component._require_owned(token_id); + + let token_uri = self.token_uris.read(token_id); + let mut base = erc721_component._base_uri(); + + if base.len() == 0 { + return token_uri; + } + + if token_uri.len() > 0 { + base.append(@token_uri); + return base; + } + + erc721_component.token_uri(token_id) + } + } + + #[generate_trait] + pub impl ERC721URIStorageInternalImpl< + TContractState, + +HasComponent, + +Drop, + +SRC5Component::HasComponent, + +ERC721HooksTrait, + +ERC721Component::HasComponent, + > of InternalTrait { + // Sets the URI for a specific `token_id`. + /// + /// This internal function allows setting a URI for an ERC721 token. After setting the URI, + /// it emits a `MetadataUpdate` event to indicate that the token's metadata has been updated. + /// + /// # Arguments + /// + /// * `token_id` - A `u256` representing the ID of the token whose URI is being set. + /// * `token_uri` - A `ByteArray` representing the new URI for the token. + /// + /// # Emits + /// + /// Emits a `MetadataUpdate` event once the token URI has been updated. + fn _set_token_uri( + ref self: ComponentState, token_id: u256, token_uri: ByteArray + ) { + self.token_uris.write(token_id, token_uri); + self.emit(MetadataUpdate { token_id }); + } + } +} diff --git a/starknet/cairo/crates/token/src/components/fast_token_router.cairo b/starknet/cairo/crates/token/src/components/fast_token_router.cairo new file mode 100644 index 00000000000..96d9d848d3e --- /dev/null +++ b/starknet/cairo/crates/token/src/components/fast_token_router.cairo @@ -0,0 +1,300 @@ +#[starknet::interface] +pub trait IFastTokenRouter { + fn fill_fast_transfer( + ref self: TState, + recipient: u256, + amount: u256, + fast_fee: u256, + origin: u32, + fast_transfer_id: u256 + ); + fn fast_transfer_remote( + ref self: TState, + destination: u32, + recipient: u256, + amount_or_id: u256, + fast_fee: u256, + value: u256 + ) -> u256; +} + +#[starknet::component] +pub mod FastTokenRouterComponent { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::{ + GasRouterComponent, GasRouterComponent::GasRouterInternalImpl + }; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl, + MailboxclientComponent::MailboxClient + }; + use contracts::client::router_component::{ + RouterComponent, RouterComponent::RouterComponentInternalImpl, + RouterComponent::IMessageRecipientInternalHookTrait, IRouter + }; + use contracts::utils::utils::U256TryIntoContractAddress; + use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::InternalImpl as OwnableInternalImpl + }; + use starknet::ContractAddress; + use token::components::token_message::TokenMessageTrait; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterInternalImpl, + TokenRouterComponent::TokenRouterHooksTrait + }; + + #[storage] + struct Storage { + fast_transfer_id: u256, + filled_fast_transfers: LegacyMap, + } + + pub trait FastTokenRouterHooksTrait { + fn fast_transfer_to_hook( + ref self: ComponentState, recipient: u256, amount: u256 + ); + fn fast_receive_from_hook( + ref self: ComponentState, sender: ContractAddress, amount: u256 + ); + } + + pub impl MessageRecipientInternalHookImpl< + TContractState, + +HasComponent, + +Drop, + +TokenRouterHooksTrait, + +FastTokenRouterHooksTrait, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + impl TokenRouter: TokenRouterComponent::HasComponent, + > of IMessageRecipientInternalHookTrait { + fn _handle( + ref self: RouterComponent::ComponentState, + origin: u32, + sender: u256, + message: Bytes + ) { + let recipient = message.recipient(); + let amount = message.amount(); + let metadata = message.metadata(); + + let mut contract_state = RouterComponent::HasComponent::get_contract_mut(ref self); + let mut component_state = HasComponent::get_component_mut(ref contract_state); + component_state._transfer_to(recipient, amount, origin, metadata); + let mut component_state = TokenRouterComponent::HasComponent::get_component_mut( + ref contract_state + ); + component_state + .emit(TokenRouterComponent::ReceivedTransferRemote { origin, recipient, amount }); + } + } + + #[embeddable_as(FastTokenRouterImpl)] + impl FastTokenRouter< + TContractState, + +HasComponent, + +Drop, + +TokenRouterHooksTrait, + impl FTRHooks: FastTokenRouterHooksTrait, + impl MailBoxClient: MailboxclientComponent::HasComponent, + impl Router: RouterComponent::HasComponent, + impl Owner: OwnableComponent::HasComponent, + impl GasRouter: GasRouterComponent::HasComponent, + impl TokenRouter: TokenRouterComponent::HasComponent, + > of super::IFastTokenRouter> { + /// Fills a fast transfer request by transferring the specified amount minus the fast fee to the recipient. + /// + /// This function is used to process a fast transfer request, ensuring that the transfer has not already been filled. + /// It deducts the fast fee from the total amount and transfers the remaining amount to the recipient. The function also + /// records the sender's address in the filled fast transfer mapping. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient of the fast transfer. + /// * `amount` - A `u256` representing the total amount of the fast transfer. + /// * `fast_fee` - A `u256` representing the fee to be deducted from the transfer amount. + /// * `origin` - A `u32` representing the domain of origin for the transfer. + /// * `fast_transfer_id` - A `u256` representing the unique ID of the fast transfer request. + /// + /// # Panics + /// + /// Panics if the fast transfer has already been filled. + fn fill_fast_transfer( + ref self: ComponentState, + recipient: u256, + amount: u256, + fast_fee: u256, + origin: u32, + fast_transfer_id: u256 + ) { + let filled_fast_transfer_key = self + ._get_fast_transfers_key(origin, fast_transfer_id, amount, fast_fee, recipient); + + assert!( + self + .filled_fast_transfers + .read(filled_fast_transfer_key) == starknet::contract_address_const::<0>(), + "Fast transfer: request already filled" + ); + + let caller = starknet::get_caller_address(); + self.filled_fast_transfers.write(filled_fast_transfer_key, caller); + + FTRHooks::fast_receive_from_hook(ref self, caller, amount - fast_fee); + FTRHooks::fast_transfer_to_hook(ref self, recipient, amount - fast_fee); + } + + /// Initiates a fast transfer to a remote domain and returns the message ID for tracking. + /// + /// This function sends a fast transfer to a recipient in a specified remote domain. It deducts the fast fee + /// from the total amount and dispatches the transfer using the gas router and mailbox components. The function + /// emits an event for the sent transfer and returns the message ID for tracking the transfer. + /// + /// # Arguments + /// + /// * `destination` - A `u32` representing the destination domain. + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount to transfer or the token ID. + /// * `fast_fee` - A `u256` representing the fast transfer fee. + /// * `value` - A `u256` representing the value being transferred with the message. + /// + /// # Returns + /// + /// A `u256` representing the message ID for the dispatched fast transfer. + fn fast_transfer_remote( + ref self: ComponentState, + destination: u32, + recipient: u256, + amount_or_id: u256, + fast_fee: u256, + value: u256, + ) -> u256 { + let mut gas_router_comp = get_dep_component_mut!(ref self, GasRouter); + let mut mailbox_comp = get_dep_component_mut!(ref self, MailBoxClient); + let mut token_router_comp = get_dep_component_mut!(ref self, TokenRouter); + + let fast_transfer_id = self.fast_transfer_id.read() + 1; + self.fast_transfer_id.write(fast_transfer_id); + + let metadata = self + ._fast_transfer_from_sender(amount_or_id, fast_fee, fast_transfer_id); + + let message_body = TokenMessageTrait::format(recipient, amount_or_id, metadata); + let hook = mailbox_comp.get_hook(); + let message_id = gas_router_comp + ._Gas_router_dispatch(destination, value, message_body, hook); + + token_router_comp + .emit( + TokenRouterComponent::SentTransferRemote { + destination, recipient, amount: amount_or_id, + } + ); + message_id + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +Drop, + +TokenRouterHooksTrait, + impl FTRHooks: FastTokenRouterHooksTrait, + impl MailBoxClient: MailboxclientComponent::HasComponent, + impl Router: RouterComponent::HasComponent, + impl Owner: OwnableComponent::HasComponent, + impl GasRouter: GasRouterComponent::HasComponent, + impl TokenRouter: TokenRouterComponent::HasComponent, + > of InternalTrait { + fn _transfer_to( + ref self: ComponentState, + recipient: u256, + amount: u256, + origin: u32, + metadata: Bytes + ) { + let token_recipient = self._get_token_recipient(recipient, amount, origin, metadata); + + FTRHooks::fast_transfer_to_hook(ref self, token_recipient, amount); + } + + fn _get_token_recipient( + self: @ComponentState, + recipient: u256, + amount: u256, + origin: u32, + metadata: Bytes + ) -> u256 { + if metadata.size() == 0 { + return recipient; + } + + let (_, fast_fee) = metadata.read_u256(0); + let (_, fast_transfer_id) = metadata.read_u256(2); + + let filler_address = self + ._get_fast_transfers_key(origin, fast_transfer_id, amount, fast_fee, recipient); + if filler_address == 0 { + return filler_address; + } + + recipient + } + + fn _get_fast_transfers_key( + self: @ComponentState, + origin: u32, + fast_transfer_id: u256, + amount: u256, + fast_fee: u256, + recipient: u256 + ) -> u256 { + let data = BytesTrait::new( + 9, + array![ + origin.into(), + fast_transfer_id.low, + fast_transfer_id.high, + amount.low, + amount.high, + fast_fee.low, + fast_fee.high, + recipient.low, + recipient.high + ] + ); + data.keccak() + } + + fn _fast_transfer_from_sender( + ref self: ComponentState, + amount: u256, + fast_fee: u256, + fast_transfer_id: u256 + ) -> Bytes { + FTRHooks::fast_receive_from_hook(ref self, starknet::get_caller_address(), amount); + BytesTrait::new( + 4, array![fast_fee.low, fast_fee.high, fast_transfer_id.low, fast_transfer_id.high] + ) + } + } +} + + +pub impl FastTokenRouterHooksEmptyImpl< + TContractState +> of FastTokenRouterComponent::FastTokenRouterHooksTrait { + fn fast_transfer_to_hook( + ref self: FastTokenRouterComponent::ComponentState, + recipient: u256, + amount: u256 + ) {} + fn fast_receive_from_hook( + ref self: FastTokenRouterComponent::ComponentState, + sender: starknet::ContractAddress, + amount: u256 + ) {} +} diff --git a/starknet/cairo/crates/token/src/components/hyp_erc20_collateral_component.cairo b/starknet/cairo/crates/token/src/components/hyp_erc20_collateral_component.cairo new file mode 100644 index 00000000000..c4c2ea70a06 --- /dev/null +++ b/starknet/cairo/crates/token/src/components/hyp_erc20_collateral_component.cairo @@ -0,0 +1,142 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IHypErc20Collateral { + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn get_wrapped_token(self: @TState) -> ContractAddress; +} + +#[starknet::component] +pub mod HypErc20CollateralComponent { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::{ + GasRouterComponent, + GasRouterComponent::{GasRouterInternalImpl, InternalTrait as GasRouterInternalTrait} + }; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientImpl + }; + use contracts::client::router_component::{ + RouterComponent, + RouterComponent::{InternalTrait as RouterInternalTrait, RouterComponentInternalImpl} + }; + use contracts::interfaces::IMailboxClient; + use contracts::utils::utils::{U256TryIntoContractAddress}; + + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use starknet::ContractAddress; + use token::components::token_message::TokenMessageTrait; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterInternalImpl, + TokenRouterComponent::TokenRouterHooksTrait + }; + + #[storage] + struct Storage { + wrapped_token: ERC20ABIDispatcher + } + + pub impl TokenRouterHooksImpl< + TContractState, + +HasComponent, + +Drop, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + +TokenRouterComponent::HasComponent, + > of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let mut component_state = HasComponent::get_component_mut(ref contract_state); + component_state._transfer_from_sender(amount_or_id) + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let mut component_state = HasComponent::get_component_mut(ref contract_state); + component_state._transfer_to(recipient, amount_or_id); + } + } + + #[embeddable_as(HypErc20CollateralImpl)] + impl HypeErc20CollateralComponentImpl< + TContractState, + +HasComponent, + +Drop, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + +TokenRouterComponent::HasComponent, + > of super::IHypErc20Collateral> { + /// Returns the balance of the given account for the wrapped ERC20 token. + /// + /// This function retrieves the balance of the wrapped ERC20 token for a specified account by calling the + /// `balance_of` function on the wrapped token dispatcher. + /// + /// # Arguments + /// + /// * `account` - A `ContractAddress` representing the account whose balance is being queried. + /// + /// # Returns + /// + /// A `u256` representing the balance of the specified account. + fn balance_of(self: @ComponentState, account: ContractAddress) -> u256 { + self.wrapped_token.read().balance_of(account) + } + + /// Returns the contract address of the wrapped ERC20 token. + /// + /// This function retrieves the contract address of the wrapped ERC20 token from the token dispatcher. + /// + /// # Returns + /// + /// A `ContractAddress` representing the address of the wrapped ERC20 token. + fn get_wrapped_token(self: @ComponentState) -> ContractAddress { + let wrapped_token: ERC20ABIDispatcher = self.wrapped_token.read(); + wrapped_token.contract_address + } + } + + #[generate_trait] + pub impl HypErc20CollateralInternalImpl< + TContractState, + +HasComponent, + +Drop, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + +TokenRouterComponent::HasComponent, + impl Mailboxclient: MailboxclientComponent::HasComponent, + > of InternalTrait { + fn initialize(ref self: ComponentState, wrapped_token: ContractAddress) { + self.wrapped_token.write(ERC20ABIDispatcher { contract_address: wrapped_token }); + } + + fn _transfer_from_sender(ref self: ComponentState, amount: u256) -> Bytes { + self + .wrapped_token + .read() + .transfer_from( + starknet::get_caller_address(), starknet::get_contract_address(), amount + ); + BytesTrait::new_empty() + } + + fn _transfer_to(ref self: ComponentState, recipient: u256, amount: u256) { + self + .wrapped_token + .read() + .transfer(recipient.try_into().expect('u256 to ContractAddress failed'), amount); + } + } +} diff --git a/starknet/cairo/crates/token/src/components/hyp_erc20_component.cairo b/starknet/cairo/crates/token/src/components/hyp_erc20_component.cairo new file mode 100644 index 00000000000..9f9fd6583bf --- /dev/null +++ b/starknet/cairo/crates/token/src/components/hyp_erc20_component.cairo @@ -0,0 +1,180 @@ +use starknet::ContractAddress; + +#[starknet::component] +pub mod HypErc20Component { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::{ + GasRouterComponent, + GasRouterComponent::{GasRouterInternalImpl, InternalTrait as GasRouterInternalTrait} + }; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientImpl + }; + use contracts::client::router_component::{ + RouterComponent, + RouterComponent::{InternalTrait as RouterInternalTrait, RouterComponentInternalImpl} + }; + use contracts::interfaces::IMailboxClient; + use contracts::utils::utils::{U256TryIntoContractAddress}; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::{ + ERC20Component::{InternalImpl as ERC20InternalImpl, ERC20HooksTrait}, + interface::IERC20Metadata, + }; + + use starknet::ContractAddress; + use token::components::token_message::TokenMessageTrait; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterInternalImpl, + TokenRouterComponent::TokenRouterHooksTrait + }; + + #[storage] + struct Storage { + decimals: u8, + } + + pub impl TokenRouterHooksImpl< + TContractState, + +HasComponent, + +Drop, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + +TokenRouterComponent::HasComponent, + +ERC20HooksTrait, + +ERC20Component::HasComponent + > of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let mut component_state = HasComponent::get_component_mut(ref contract_state); + component_state._transfer_from_sender(amount_or_id) + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let mut component_state = HasComponent::get_component_mut(ref contract_state); + component_state._transfer_to(recipient, amount_or_id); + } + } + + #[embeddable_as(HypErc20MetadataImpl)] + impl HypErc20Metadata< + TContractState, + +HasComponent, + +Drop, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + +TokenRouterComponent::HasComponent, + +ERC20HooksTrait, + impl ERC20: ERC20Component::HasComponent + > of IERC20Metadata> { + /// Returns the name of the ERC20 token. + /// + /// This function retrieves the name of the token by reading from the `ERC20_name` field + /// of the ERC20 component. + /// + /// # Returns + /// + /// A `ByteArray` representing the name of the token. + fn name(self: @ComponentState) -> ByteArray { + let erc20 = get_dep_component!(self, ERC20); + erc20.ERC20_name.read() + } + + /// Returns the symbol of the ERC20 token. + /// + /// This function retrieves the symbol, or ticker, of the token by reading from the `ERC20_symbol` + /// field of the ERC20 component. + /// + /// # Returns + /// + /// A `ByteArray` representing the token's symbol. + fn symbol(self: @ComponentState) -> ByteArray { + let erc20 = get_dep_component!(self, ERC20); + erc20.ERC20_symbol.read() + } + + /// Returns the number of decimals used to represent the token. + /// + /// This function returns the number of decimals defined for the token, which represents the + /// smallest unit of the token used in its user-facing operations. The value is read from the + /// `decimals` field of the component's storage. + /// + /// # Returns + /// + /// A `u8` representing the number of decimals used by the token. + fn decimals(self: @ComponentState) -> u8 { + self.decimals.read() + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +Drop, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + +TokenRouterComponent::HasComponent, + +ERC20HooksTrait, + impl ERC20: ERC20Component::HasComponent + > of InternalTrait { + /// Initializes the token with a specific number of decimals. + /// + /// This function sets the `decimals` value for the token during the initialization phase, defining + /// how many decimal places the token will support. + /// + /// # Arguments + /// + /// * `decimals` - A `u8` value representing the number of decimals for the token. + fn initialize(ref self: ComponentState, decimals: u8) { + self.decimals.write(decimals); + } + + /// Burns tokens from the sender's account. + /// + /// This function transfers the specified amount of tokens from the sender's account by + /// calling the `burn` function on the ERC20 component. + /// + /// # Arguments + /// + /// * `amount` - A `u256` value representing the amount of tokens to be burned. + /// + /// # Returns + /// + /// A `Bytes` object representing an empty payload. + fn _transfer_from_sender(ref self: ComponentState, amount: u256) -> Bytes { + let mut erc20 = get_dep_component_mut!(ref self, ERC20); + erc20.burn(starknet::get_caller_address(), amount); + BytesTrait::new_empty() + } + + /// Mints tokens to the specified recipient. + /// + /// This function mints new tokens and transfers them to the recipient's account by calling + /// the `mint` function on the ERC20 component. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` value representing the recipient's address. + /// * `amount` - A `u256` value representing the amount of tokens to mint. + fn _transfer_to(ref self: ComponentState, recipient: u256, amount: u256) { + let mut erc20 = get_dep_component_mut!(ref self, ERC20); + erc20.mint(recipient.try_into().expect('u256 to ContractAddress failed'), amount); + } + } +} diff --git a/starknet/cairo/crates/token/src/components/hyp_erc721_collateral_component.cairo b/starknet/cairo/crates/token/src/components/hyp_erc721_collateral_component.cairo new file mode 100644 index 00000000000..b93b51a3abe --- /dev/null +++ b/starknet/cairo/crates/token/src/components/hyp_erc721_collateral_component.cairo @@ -0,0 +1,81 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IHypErc721Collateral { + fn owner_of(self: @TState, token_id: u256) -> ContractAddress; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn get_wrapped_token(self: @TState) -> ContractAddress; +} + +#[starknet::component] +pub mod HypErc721CollateralComponent { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl, + MailboxclientComponent::MailboxClient + }; + use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::InternalImpl, OwnableComponent::OwnableImpl + }; + use openzeppelin::token::erc721::interface::{ERC721ABIDispatcher, ERC721ABIDispatcherTrait}; + use starknet::ContractAddress; + + #[storage] + struct Storage { + wrapped_token: ERC721ABIDispatcher, + } + + #[embeddable_as(HypErc721CollateralImpl)] + impl HypErc721CollateralComponentImpl< + TContractState, + +HasComponent, + +Drop, + +OwnableComponent::HasComponent, + impl Mailboxclient: MailboxclientComponent::HasComponent, + > of super::IHypErc721Collateral> { + /// Returns the owner of a given ERC721 token ID. + /// + /// This function queries the wrapped ERC721 token contract to retrieve the address of the owner + /// of the specified `token_id`. + /// + /// # Arguments + /// + /// * `token_id` - A `u256` representing the ID of the token whose owner is being queried. + /// + /// # Returns + /// + /// A `ContractAddress` representing the owner of the specified token. + fn owner_of(self: @ComponentState, token_id: u256) -> ContractAddress { + self.wrapped_token.read().owner_of(token_id) + } + + /// Returns the balance of ERC721 tokens held by a given account. + /// + /// This function retrieves the number of ERC721 tokens held by the specified account by querying + /// the wrapped ERC721 token contract. + /// + /// # Arguments + /// + /// * `account` - A `ContractAddress` representing the account whose balance is being queried. + /// + /// # Returns + /// + /// A `u256` representing the number of tokens held by the specified account. + fn balance_of(self: @ComponentState, account: ContractAddress) -> u256 { + self.wrapped_token.read().balance_of(account) + } + + /// Returns the contract address of the wrapped ERC721 token. + /// + /// This function retrieves the contract address of the wrapped ERC721 token from the component's + /// storage. + /// + /// # Returns + /// + /// A `ContractAddress` representing the address of the wrapped ERC721 token. + fn get_wrapped_token(self: @ComponentState) -> ContractAddress { + let wrapped_token: ERC721ABIDispatcher = self.wrapped_token.read(); + wrapped_token.contract_address + } + } +} diff --git a/starknet/cairo/crates/token/src/components/hyp_erc721_component.cairo b/starknet/cairo/crates/token/src/components/hyp_erc721_component.cairo new file mode 100644 index 00000000000..a3de895755d --- /dev/null +++ b/starknet/cairo/crates/token/src/components/hyp_erc721_component.cairo @@ -0,0 +1,67 @@ +#[starknet::interface] +pub trait IHypErc721 { + fn initialize(ref self: TState, mint_amount: u256, name: ByteArray, symbol: ByteArray); +} + +#[starknet::component] +pub mod HypErc721Component { + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl, + MailboxclientComponent::MailboxClient + }; + use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::InternalImpl as OwnableInternalImpl + }; + use openzeppelin::introspection::src5::{ + SRC5Component, SRC5Component::SRC5Impl, SRC5Component::InternalTrait as SRC5InternalTrait + }; + use openzeppelin::token::erc721::{ + ERC721Component, ERC721Component::ERC721Impl, + ERC721Component::InternalTrait as ERC721InternalTrait, ERC721Component::ERC721HooksTrait, + }; + + + #[storage] + struct Storage {} + + #[generate_trait] + pub impl HypErc721InternalImpl< + TContractState, + +HasComponent, + +Drop, + +OwnableComponent::HasComponent, + +SRC5Component::HasComponent, + +ERC721HooksTrait, + impl Mailboxclient: MailboxclientComponent::HasComponent, + impl ERC721: ERC721Component::HasComponent, + > of HypErc721InternalTrait { + /// Initializes the ERC721 token contract with a specified mint amount, name, and symbol. + /// + /// This function sets the name and symbol for the ERC721 token contract and mints the specified number + /// of tokens to the caller's address. The initialization process ensures that the contract is set up + /// with the given name, symbol, and initial minting operation. + /// + /// # Arguments + /// + /// * `mint_amount` - A `u256` representing the number of tokens to mint initially. + /// * `name` - A `ByteArray` representing the name of the token. + /// * `symbol` - A `ByteArray` representing the symbol (ticker) of the token. + fn initialize( + ref self: ComponentState, + mint_amount: u256, + name: ByteArray, + symbol: ByteArray, + ) { + let mut erc721_comp = get_dep_component_mut!(ref self, ERC721); + erc721_comp.initializer(name, symbol, ""); + + let caller = starknet::get_caller_address(); + + let mut i = 0; + while i < mint_amount { + erc721_comp.mint(caller, i.into()); + i += 1; + }; + } + } +} diff --git a/starknet/cairo/crates/token/src/components/hyp_native_component.cairo b/starknet/cairo/crates/token/src/components/hyp_native_component.cairo new file mode 100644 index 00000000000..11a6e98f4de --- /dev/null +++ b/starknet/cairo/crates/token/src/components/hyp_native_component.cairo @@ -0,0 +1,167 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IHypNative { + // fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn receive(ref self: TState, amount: u256); +} + +#[starknet::component] +pub mod HypNativeComponent { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::{ + GasRouterComponent, + GasRouterComponent::{GasRouterInternalImpl, InternalTrait as GasRouterInternalTrait} + }; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientImpl + }; + use contracts::client::router_component::{ + RouterComponent, + RouterComponent::{InternalTrait as RouterInternalTrait, RouterComponentInternalImpl} + }; + use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::InternalImpl, OwnableComponent::OwnableImpl + }; + use openzeppelin::token::erc20::{ + interface::{IERC20, ERC20ABIDispatcher, ERC20ABIDispatcherTrait}, ERC20Component + }; + use starknet::ContractAddress; + use token::components::token_message::TokenMessageTrait; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterInternalImpl, + TokenRouterComponent::TokenRouterHooksTrait, ITokenRouter, + TokenRouterTransferRemoteHookDefaultImpl + }; + use token::interfaces::imessage_recipient::IMessageRecipient; + + + #[storage] + struct Storage { + eth_token: ERC20ABIDispatcher, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Donation: Donation, + } + + #[derive(Drop, starknet::Event)] + struct Donation { + sender: ContractAddress, + amount: u256, + } + + #[embeddable_as(HypNativeImpl)] + impl HypNative< + TContractState, + +HasComponent, + +Drop, + +OwnableComponent::HasComponent, + +RouterComponent::HasComponent, + +GasRouterComponent::HasComponent, + +ERC20Component::HasComponent, + impl Mailboxclient: MailboxclientComponent::HasComponent, + impl TokenRouter: TokenRouterComponent::HasComponent, + > of super::IHypNative> { + fn receive(ref self: ComponentState, amount: u256) { + self.eth_token.read().transfer(starknet::get_contract_address(), amount); + + self.emit(Donation { sender: starknet::get_caller_address(), amount }); + } + } + + pub impl TokenRouterHooksImpl< + TContractState, + +HasComponent, + +Drop, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + +TokenRouterComponent::HasComponent, + +ERC20Component::HasComponent + > of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let mut component_state = HasComponent::get_component_mut(ref contract_state); + component_state._transfer_from_sender(amount_or_id) + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let mut component_state = HasComponent::get_component_mut(ref contract_state); + component_state._transfer_to(recipient, amount_or_id); + } + } + + #[embeddable_as(TokenRouterImpl)] + impl TokenRouter< + TContractState, + +HasComponent, + +Drop, + impl MailboxClient: MailboxclientComponent::HasComponent, + impl Router: RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + impl GasRouter: GasRouterComponent::HasComponent, + impl TokenRouterComp: TokenRouterComponent::HasComponent, + impl ERC20: ERC20Component::HasComponent + > of ITokenRouter> { + fn transfer_remote( + ref self: ComponentState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256 { + assert!(value >= amount_or_id, "Native: amount exceeds msg.value"); + let hook_payment = value - amount_or_id; + + let mut token_router_comp = get_dep_component_mut!(ref self, TokenRouterComp); + TokenRouterTransferRemoteHookDefaultImpl::_transfer_remote( + ref token_router_comp, + destination, + recipient, + amount_or_id, + hook_payment, + Option::None, + Option::None + ) + } + } + + #[generate_trait] + pub impl HypNativeInternalImpl< + TContractState, + +HasComponent, + +Drop, + +OwnableComponent::HasComponent, + +RouterComponent::HasComponent, + +GasRouterComponent::HasComponent, + +ERC20Component::HasComponent, + impl Mailboxclient: MailboxclientComponent::HasComponent, + impl TokenRouter: TokenRouterComponent::HasComponent, + > of InternalTrait { + fn _transfer_from_sender(ref self: ComponentState, amount: u256) -> Bytes { + BytesTrait::new_empty() + } + + fn _transfer_to(ref self: ComponentState, recipient: u256, amount: u256) { + let contract_address = starknet::get_contract_address(); // this address or eth address + let erc20_dispatcher = ERC20ABIDispatcher { contract_address }; + let recipient_felt: felt252 = recipient.try_into().expect('u256 to felt failed'); + let recipient: ContractAddress = recipient_felt.try_into().unwrap(); + erc20_dispatcher.transfer(recipient, amount); + } + } +} diff --git a/starknet/cairo/crates/token/src/components/token_message.cairo b/starknet/cairo/crates/token/src/components/token_message.cairo new file mode 100644 index 00000000000..7fc295e56e0 --- /dev/null +++ b/starknet/cairo/crates/token/src/components/token_message.cairo @@ -0,0 +1,79 @@ +use alexandria_bytes::{Bytes, BytesTrait}; + + +#[generate_trait] +pub impl TokenMessage of TokenMessageTrait { + /// Formats a token message with the recipient, amount, and metadata. + /// + /// This function creates a token message by combining the recipient address, the transfer amount, + /// and any additional metadata. The resulting message is returned as a `Bytes` object. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount` - A `u256` representing the amount of tokens to transfer. + /// * `metadata` - A `Bytes` object representing additional metadata for the transfer. + /// + /// # Returns + /// + /// A `Bytes` object containing the formatted token message. + fn format(recipient: u256, amount: u256, metadata: Bytes) -> Bytes { + let mut bytes = BytesTrait::new_empty(); + bytes.append_u256(recipient); + bytes.append_u256(amount); + bytes.concat(@metadata); + bytes + } + + /// Extracts the recipient address from the token message. + /// + /// This function reads the recipient address from the token message, starting at the beginning of + /// the message data. The recipient is returned as a `u256`. + /// + /// # Returns + /// + /// A `u256` representing the recipient address. + fn recipient(self: @Bytes) -> u256 { + let (_, recipient) = self.read_u256(0); + recipient + } + + /// Extracts the transfer amount from the token message. + /// + /// This function reads the amount of tokens to be transferred from the token message, starting at + /// byte offset 32. The amount is returned as a `u256`. + /// + /// # Returns + /// + /// A `u256` representing the amount of tokens to be transferred. + fn amount(self: @Bytes) -> u256 { + let (_, amount) = self.read_u256(32); + amount + } + + /// Extracts the token ID from the token message. + /// + /// This function is equivalent to the `amount` function, as in certain token standards the token + /// ID is encoded in the same field as the transfer amount. + /// + /// # Returns + /// + /// A `u256` representing the token ID or transfer amount. + fn token_id(self: @Bytes) -> u256 { + self.amount() + } + + /// Extracts the metadata from the token message. + /// + /// This function reads and returns the metadata portion of the token message, starting at byte + /// offset 64 and extending to the end of the message. + /// + /// # Returns + /// + /// A `Bytes` object representing the metadata included in the token message. + fn metadata(self: @Bytes) -> Bytes { + let (_, bytes) = self.read_bytes(64, self.size() - 64); + bytes + } +} + diff --git a/starknet/cairo/crates/token/src/components/token_router.cairo b/starknet/cairo/crates/token/src/components/token_router.cairo new file mode 100644 index 00000000000..091ba64fe3d --- /dev/null +++ b/starknet/cairo/crates/token/src/components/token_router.cairo @@ -0,0 +1,303 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::client::gas_router_component::{ + GasRouterComponent, GasRouterComponent::InternalTrait as GasRouterComponentInternalTrait +}; +use contracts::client::mailboxclient_component::MailboxclientComponent; +use contracts::client::router_component::{ + RouterComponent, RouterComponent::InternalTrait as RouterComponentInternalTrait +}; +use contracts::interfaces::IMailboxClient; +use openzeppelin::access::ownable::OwnableComponent; +use starknet::ContractAddress; +use token::components::token_message::TokenMessageTrait; + +#[starknet::interface] +pub trait ITokenRouter { + fn transfer_remote( + ref self: TState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256; +} + +#[starknet::component] +pub mod TokenRouterComponent { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::{ + GasRouterComponent, GasRouterComponent::GasRouterInternalImpl + }; + use contracts::client::mailboxclient_component::{ + MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl, + MailboxclientComponent::MailboxClient + }; + use contracts::client::router_component::{ + RouterComponent, RouterComponent::RouterComponentInternalImpl, + RouterComponent::IMessageRecipientInternalHookTrait, + }; + use openzeppelin::access::ownable::{ + OwnableComponent, OwnableComponent::InternalImpl as OwnableInternalImpl + }; + use starknet::ContractAddress; + use token::components::token_message::TokenMessageTrait; + + #[storage] + struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + SentTransferRemote: SentTransferRemote, + ReceivedTransferRemote: ReceivedTransferRemote, + } + + #[derive(Drop, starknet::Event)] + pub struct SentTransferRemote { + #[key] + pub destination: u32, + #[key] + pub recipient: u256, + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct ReceivedTransferRemote { + #[key] + pub origin: u32, + #[key] + pub recipient: u256, + pub amount: u256, + } + + pub trait TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: ComponentState, amount_or_id: u256 + ) -> Bytes; + + fn transfer_to_hook( + ref self: ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ); + } + + pub trait TokenRouterTransferRemoteHookTrait { + fn _transfer_remote( + ref self: ComponentState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256; + } + + pub impl MessageRecipientInternalHookImpl< + TContractState, + +HasComponent, + +RouterComponent::HasComponent, + impl Hooks: TokenRouterHooksTrait, + +Drop, + > of IMessageRecipientInternalHookTrait { + /// Handles the receipt of a message and processes a token transfer. + /// + /// This function is invoked when a message is received, processing the transfer of tokens to the recipient. + /// It retrieves the recipient, amount, and metadata from the message and triggers the appropriate hook to + /// handle the transfer. The function also emits a `ReceivedTransferRemote` event after processing the transfer. + /// + /// # Arguments + /// + /// * `origin` - A `u32` representing the origin domain. + /// * `sender` - A `u256` representing the sender's address. + /// * `message` - A `Bytes` object representing the incoming message. + fn _handle( + ref self: RouterComponent::ComponentState, + origin: u32, + sender: u256, + message: Bytes + ) { + let recipient = message.recipient(); + let amount = message.amount(); + let metadata = message.metadata(); + let mut contract_state = RouterComponent::HasComponent::get_contract_mut(ref self); + let mut component_state = HasComponent::get_component_mut(ref contract_state); + Hooks::transfer_to_hook(ref component_state, recipient, amount, metadata); + component_state.emit(ReceivedTransferRemote { origin, recipient, amount }); + } + } + + #[embeddable_as(TokenRouterImpl)] + pub impl TokenRouter< + TContractState, + +HasComponent, + +Drop, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +GasRouterComponent::HasComponent, + +TokenRouterHooksTrait, + impl TransferRemoteHook: TokenRouterTransferRemoteHookTrait + > of super::ITokenRouter> { + /// Initiates a token transfer to a remote domain. + /// + /// This function dispatches a token transfer to the specified recipient on a remote domain, transferring + /// either an amount of tokens or a token ID. It supports optional hooks and metadata for additional + /// processing during the transfer. The function emits a `SentTransferRemote` event once the transfer is initiated. + /// + /// # Arguments + /// + /// * `destination` - A `u32` representing the destination domain. + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `value` - A `u256` representing the value of the transfer. + /// * `hook_metadata` - An optional `Bytes` object representing metadata for the hook. + /// * `hook` - An optional `ContractAddress` representing the contract hook to invoke during the transfer. + /// + /// # Returns + /// + /// A `u256` representing the message ID of the dispatched transfer. + fn transfer_remote( + ref self: ComponentState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256 { + match hook_metadata { + Option::Some(hook_metadata) => { + TransferRemoteHook::_transfer_remote( + ref self, + destination, + recipient, + amount_or_id, + value, + Option::Some(hook_metadata), + hook + ) + }, + Option::None => { + TransferRemoteHook::_transfer_remote( + ref self, + destination, + recipient, + amount_or_id, + value, + Option::None, + Option::None + ) + } + } + } + } + + #[generate_trait] + pub impl TokenRouterInternalImpl< + TContractState, + +HasComponent, + +Drop, + +OwnableComponent::HasComponent, + impl MailBoxClient: MailboxclientComponent::HasComponent, + impl Router: RouterComponent::HasComponent, + impl GasRouter: GasRouterComponent::HasComponent, + impl Hooks: TokenRouterHooksTrait + > of InternalTrait { + fn _transfer_from_sender( + ref self: ComponentState, amount_or_id: u256 + ) -> Bytes { + Hooks::transfer_from_sender_hook(ref self, amount_or_id) + } + + fn _transfer_to( + ref self: ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + Hooks::transfer_to_hook(ref self, recipient, amount_or_id, metadata); + } + } +} + +//pub impl TokenRouterEmptyHooksImpl< +// TContractState +//> of TokenRouterComponent::TokenRouterHooksTrait { +// fn transfer_from_sender_hook( +// ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 +// ) -> Bytes { +// alexandria_bytes::BytesTrait::new_empty() +// } +// +// fn transfer_to_hook( +// ref self: TokenRouterComponent::ComponentState, +// recipient: u256, +// amount_or_id: u256, +// metadata: Bytes +// ) {} +//} + +pub impl TokenRouterTransferRemoteHookDefaultImpl< + TContractState, + +Drop, + +TokenRouterComponent::HasComponent, + +MailboxclientComponent::HasComponent, + +RouterComponent::HasComponent, + +GasRouterComponent::HasComponent, + +OwnableComponent::HasComponent, + +TokenRouterComponent::TokenRouterHooksTrait +> of TokenRouterComponent::TokenRouterTransferRemoteHookTrait { + fn _transfer_remote( + ref self: TokenRouterComponent::ComponentState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256 { + let token_metadata = TokenRouterComponent::TokenRouterInternalImpl::_transfer_from_sender( + ref self, amount_or_id + ); + let token_message = TokenMessageTrait::format(recipient, amount_or_id, token_metadata); + let contract_state = TokenRouterComponent::HasComponent::get_contract(@self); + let mut router_comp = RouterComponent::HasComponent::get_component(contract_state); + let mailbox_comp = MailboxclientComponent::HasComponent::get_component(contract_state); + let gas_router_comp = GasRouterComponent::HasComponent::get_component(contract_state); + + let mut message_id = 0; + + match hook_metadata { + Option::Some(hook_metadata) => { + if !hook.is_some() { + panic!("Transfer remote invalid arguments, missing hook"); + } + + message_id = router_comp + ._Router_dispatch( + destination, value, token_message, hook_metadata, hook.unwrap() + ); + }, + Option::None => { + let hook_metadata = gas_router_comp._Gas_router_hook_metadata(destination); + let hook = mailbox_comp.get_hook(); + message_id = router_comp + ._Router_dispatch(destination, value, token_message, hook_metadata, hook); + } + } + + self + .emit( + TokenRouterComponent::SentTransferRemote { + destination, recipient, amount: amount_or_id, + } + ); + + message_id + } +} diff --git a/starknet/cairo/crates/token/src/extensions/fast_hyp_erc20.cairo b/starknet/cairo/crates/token/src/extensions/fast_hyp_erc20.cairo new file mode 100644 index 00000000000..6d2a75db350 --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/fast_hyp_erc20.cairo @@ -0,0 +1,198 @@ +#[starknet::contract] +pub mod FastHypERC20 { + use alexandria_bytes::Bytes; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use contracts::utils::utils::U256TryIntoContractAddress; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::{ + hyp_erc20_component::{HypErc20Component, HypErc20Component::TokenRouterHooksImpl}, + token_message::TokenMessageTrait, + token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterTransferRemoteHookDefaultImpl + }, + fast_token_router::{ + FastTokenRouterComponent, FastTokenRouterComponent::FastTokenRouterHooksTrait, + FastTokenRouterComponent::MessageRecipientInternalHookImpl + } + }; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!( + path: FastTokenRouterComponent, storage: fast_token_router, event: FastTokenRouterEvent + ); + component!(path: HypErc20Component, storage: hyp_erc20, event: HypErc20Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + // ERC20 + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + // HypERC20 + #[abi(embed_v0)] + impl HypErc20MetadataImpl = + HypErc20Component::HypErc20MetadataImpl; + impl HypErc20InternalImpl = HypErc20Component::InternalImpl; + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + // FastTokenRouter + #[abi(embed_v0)] + impl FastTokenRouterImpl = + FastTokenRouterComponent::FastTokenRouterImpl; + impl FastTokenRouterInternalImpl = FastTokenRouterComponent::InternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + hyp_erc20: HypErc20Component::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + fast_token_router: FastTokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20Event: HypErc20Component::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + FastTokenRouterEvent: FastTokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + decimals: u8, + mailbox: ContractAddress, + total_supply: u256, + name: ByteArray, + symbol: ByteArray, + hook: ContractAddress, + interchain_security_module: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self.hyp_erc20.initialize(decimals); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.erc20.initializer(name, symbol); + self.erc20.mint(starknet::get_caller_address(), total_supply); + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: core::starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + pub impl FastTokenRouterHooksImpl of FastTokenRouterHooksTrait { + /// Transfers tokens to the recipient as part of the fast token router process. + /// + /// This function mints tokens to the recipient as part of the fast token router process by calling + /// the `mint` function of the ERC20 component. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount` - A `u256` representing the amount of tokens to mint. + fn fast_transfer_to_hook( + ref self: FastTokenRouterComponent::ComponentState, + recipient: u256, + amount: u256 + ) { + let mut contract_state = FastTokenRouterComponent::HasComponent::get_contract_mut( + ref self + ); + contract_state + .erc20 + .mint(recipient.try_into().expect('u256 to ContractAddress failed'), amount); + } + + /// Receives tokens from the sender as part of the fast token router process. + /// + /// This function burns tokens from the sender as part of the fast token router process by calling + /// the `burn` function of the ERC20 component. + /// + /// # Arguments + /// + /// * `sender` - A `ContractAddress` representing the sender's address. + /// * `amount` - A `u256` representing the amount of tokens to burn. + fn fast_receive_from_hook( + ref self: FastTokenRouterComponent::ComponentState, + sender: ContractAddress, + amount: u256 + ) { + let mut contract_state = FastTokenRouterComponent::HasComponent::get_contract_mut( + ref self + ); + contract_state.erc20.burn(sender, amount); + } + } +} diff --git a/starknet/cairo/crates/token/src/extensions/fast_hyp_erc20_collateral.cairo b/starknet/cairo/crates/token/src/extensions/fast_hyp_erc20_collateral.cairo new file mode 100644 index 00000000000..29d2d763d1d --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/fast_hyp_erc20_collateral.cairo @@ -0,0 +1,214 @@ +#[starknet::interface] +pub trait IFastHypERC20 { + fn balance_of(self: @TState, account: starknet::ContractAddress) -> u256; +} + +#[starknet::contract] +pub mod FastHypERC20Collateral { + use alexandria_bytes::Bytes; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use contracts::utils::utils::U256TryIntoContractAddress; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::{ + hyp_erc20_collateral_component::{ + HypErc20CollateralComponent, HypErc20CollateralComponent::TokenRouterHooksImpl + }, + token_message::TokenMessageTrait, + token_router::{TokenRouterComponent, TokenRouterTransferRemoteHookDefaultImpl}, + fast_token_router::{ + FastTokenRouterComponent, FastTokenRouterComponent::FastTokenRouterHooksTrait, + FastTokenRouterComponent::MessageRecipientInternalHookImpl + } + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!( + path: FastTokenRouterComponent, storage: fast_token_router, event: FastTokenRouterEvent + ); + component!( + path: HypErc20CollateralComponent, storage: collateral, event: HypErc20CollateralEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + // HypERC20Collateral + #[abi(embed_v0)] + impl HypErc20CollateralImpl = + HypErc20CollateralComponent::HypErc20CollateralImpl; + impl HypErc20CollateralInternalImpl = + HypErc20CollateralComponent::HypErc20CollateralInternalImpl; + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + impl TokenRouterInternalImpl = TokenRouterComponent::TokenRouterInternalImpl; + // FastTokenRouter + #[abi(embed_v0)] + impl FastTokenRouterImpl = + FastTokenRouterComponent::FastTokenRouterImpl; + impl FastTokenRouterInternalImpl = FastTokenRouterComponent::InternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + collateral: HypErc20CollateralComponent::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + fast_token_router: FastTokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20CollateralEvent: HypErc20CollateralComponent::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + FastTokenRouterEvent: FastTokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + wrapped_token: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.collateral.initialize(wrapped_token); + } + + impl FastHypERC20Impl of super::IFastHypERC20 { + /// Returns the balance of the specified account for the wrapped ERC20 token. + /// + /// This function retrieves the balance of the wrapped ERC20 token for a given account by calling + /// the `balance_of` function on the `HypErc20CollateralComponent`. + /// + /// # Arguments + /// + /// * `account` - A `ContractAddress` representing the account whose token balance is being queried. + /// + /// # Returns + /// + /// A `u256` representing the balance of the specified account. + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.collateral.balance_of(account) + } + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: core::starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + pub impl FastTokenRouterHooksImpl of FastTokenRouterHooksTrait { + /// Transfers tokens to the recipient as part of the fast token router process. + /// + /// This function handles the fast token transfer process by invoking the `transfer` method of the + /// wrapped token from the `HypErc20CollateralComponent`. The recipient receives the transferred amount. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount` - A `u256` representing the amount of tokens to transfer. + fn fast_transfer_to_hook( + ref self: FastTokenRouterComponent::ComponentState, + recipient: u256, + amount: u256 + ) { + let mut contract_state = FastTokenRouterComponent::HasComponent::get_contract_mut( + ref self + ); + contract_state + .collateral + .wrapped_token + .read() + .transfer(recipient.try_into().expect('u256 to ContractAddress failed'), amount); + } + + /// Receives tokens from the sender as part of the fast token router process. + /// + /// This function handles the receipt of tokens from the sender by calling the `transfer_from` method + /// of the wrapped token within the `HypErc20CollateralComponent`. + /// + /// # Arguments + /// + /// * `sender` - A `ContractAddress` representing the sender's address. + /// * `amount` - A `u256` representing the amount of tokens to receive. + fn fast_receive_from_hook( + ref self: FastTokenRouterComponent::ComponentState, + sender: ContractAddress, + amount: u256 + ) { + let mut contract_state = FastTokenRouterComponent::HasComponent::get_contract_mut( + ref self + ); + contract_state + .collateral + .wrapped_token + .read() + .transfer_from(sender, starknet::get_contract_address(), amount); + } + } +} diff --git a/starknet/cairo/crates/token/src/extensions/hyp_erc20_collateral_vault_deposit.cairo b/starknet/cairo/crates/token/src/extensions/hyp_erc20_collateral_vault_deposit.cairo new file mode 100644 index 00000000000..a26b50c5e86 --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_erc20_collateral_vault_deposit.cairo @@ -0,0 +1,238 @@ +#[starknet::interface] +pub trait IHypERC20CollateralVaultDeposit { + fn sweep(ref self: TState); + // getters + fn get_vault(self: @TState) -> starknet::ContractAddress; + fn get_asset_deposited(self: @TState) -> u256; +} + +#[starknet::contract] +pub mod HypERC20CollateralVaultDeposit { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use contracts::utils::utils::U256TryIntoContractAddress; + use core::integer::BoundedInt; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ERC20ABIDispatcherTrait}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::{ + token_router::{ + TokenRouterComponent, TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterComponent::TokenRouterHooksTrait, TokenRouterTransferRemoteHookDefaultImpl + }, + hyp_erc20_collateral_component::HypErc20CollateralComponent, + }; + use token::interfaces::ierc4626::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!( + path: HypErc20CollateralComponent, storage: collateral, event: HypErc20CollateralEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + // HypERC20Collateral + #[abi(embed_v0)] + impl HypErc20CollateralImpl = + HypErc20CollateralComponent::HypErc20CollateralImpl; + impl HypErc20CollateralInternalImpl = + HypErc20CollateralComponent::HypErc20CollateralInternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + vault: ERC4626ABIDispatcher, + asset_deposited: u256, + #[substorage(v0)] + collateral: HypErc20CollateralComponent::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20CollateralEvent: HypErc20CollateralComponent::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + ExcessSharesSwept: ExcessSharesSwept + } + + #[derive(Drop, starknet::Event)] + struct ExcessSharesSwept { + amount: u256, + assets_redeemed: u256 + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + vault: ContractAddress, + owner: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + let vault_dispatcher = ERC4626ABIDispatcher { contract_address: vault }; + let erc20 = vault_dispatcher.asset(); + self.collateral.initialize(erc20); + self.vault.write(vault_dispatcher); + self.collateral.wrapped_token.read().approve(vault, BoundedInt::max()); + } + #[abi(embed_v0)] + impl HypERC20CollateralVaultDepositImpl of super::IHypERC20CollateralVaultDeposit< + ContractState + > { + /// Sweeps excess shares from the vault. + /// + /// This function checks for excess shares in the vault, which are shares that exceed the amount + /// that was initially deposited. It redeems these excess shares and transfers the redeemed assets + /// to the contract owner. The function emits an `ExcessSharesSwept` event after completing the sweep. + fn sweep(ref self: ContractState) { + self.ownable.assert_only_owner(); + let this_address = starknet::get_contract_address(); + let vault = self.vault.read(); + let excess_shares = vault.max_redeem(this_address) + - vault.convert_to_shares(self.asset_deposited.read()); + let assets_redeemed = vault + .redeem(excess_shares, self.ownable.Ownable_owner.read(), this_address); + self + .emit( + ExcessSharesSwept { amount: excess_shares, assets_redeemed: assets_redeemed } + ); + } + + /// Returns the contract address of the vault. + /// + /// This function retrieves the contract address of the vault that is being used for collateral + /// deposits and withdrawals. + /// + /// # Returns + /// + /// A `ContractAddress` representing the vault's contract address. + fn get_vault(self: @ContractState) -> ContractAddress { + self.vault.read().contract_address + } + + /// Returns the total amount of assets deposited in the vault. + /// + /// This function returns the total amount of assets that have been deposited into the vault by + /// this contract. + /// + /// # Returns + /// + /// A `u256` representing the total assets deposited. + fn get_asset_deposited(self: @ContractState) -> u256 { + self.asset_deposited.read() + } + } + + impl TokenRouterHooksTraitImpl of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let metadata = + HypErc20CollateralComponent::TokenRouterHooksImpl::transfer_from_sender_hook( + ref self, amount_or_id + ); + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + contract_state._deposit_into_vault(amount_or_id); + metadata + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + contract_state + ._withdraw_from_vault( + amount_or_id, recipient.try_into().expect('u256 to ContractAddress failed') + ); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Deposits the specified amount into the vault. + /// + /// This internal function deposits the specified amount of assets into the vault and updates the + /// total amount of assets deposited by the contract. + /// + /// # Arguments + /// + /// * `amount` - A `u256` representing the amount of assets to deposit. + fn _deposit_into_vault(ref self: ContractState, amount: u256) { + let asset_deposited = self.asset_deposited.read(); + self.asset_deposited.write(asset_deposited + amount); + self.vault.read().deposit(amount, starknet::get_contract_address()); + } + + // Returns the total amount of assets deposited in the vault. + /// + /// This function returns the total amount of assets that have been deposited into the vault by + /// this contract. + /// + /// # Returns + /// + /// A `u256` representing the total assets deposited. + fn _withdraw_from_vault(ref self: ContractState, amount: u256, recipient: ContractAddress) { + let asset_deposited = self.asset_deposited.read(); + self.asset_deposited.write(asset_deposited - amount); + self.vault.read().withdraw(amount, recipient, starknet::get_contract_address()); + } + } +} diff --git a/starknet/cairo/crates/token/src/extensions/hyp_erc20_vault.cairo b/starknet/cairo/crates/token/src/extensions/hyp_erc20_vault.cairo new file mode 100644 index 00000000000..6eedb45c89e --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_erc20_vault.cairo @@ -0,0 +1,410 @@ +#[starknet::interface] +pub trait IHypErc20Vault { + fn assets_to_shares(self: @TContractState, amount: u256) -> u256; + fn shares_to_assets(self: @TContractState, shares: u256) -> u256; + fn share_balance_of(self: @TContractState, account: starknet::ContractAddress) -> u256; + // getters + fn get_precision(self: @TContractState) -> u256; + fn get_collateral_domain(self: @TContractState) -> u32; + fn get_exchange_rate(self: @TContractState) -> u256; +} + +#[starknet::contract] +mod HypErc20Vault { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::{ + RouterComponent, RouterComponent::IMessageRecipientInternalHookTrait + }; + use contracts::libs::math; + use core::zeroable::NonZero; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ + ERC20Component, ERC20HooksEmptyImpl, interface::{IERC20, IERC20CamelOnly} + }; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::token_message::TokenMessageTrait; + use token::components::{ + hyp_erc20_component::{HypErc20Component, HypErc20Component::TokenRouterHooksImpl,}, + token_router::{ + TokenRouterComponent, + TokenRouterComponent::{TokenRouterHooksTrait, TokenRouterTransferRemoteHookTrait} + } + }; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: HypErc20Component, storage: hyp_erc20, event: HypErc20Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + impl RouterInternalImpl = RouterComponent::RouterComponentInternalImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + impl GasRouterInternalImpl = GasRouterComponent::GasRouterInternalImpl; + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + // ERC20 + impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + // HypERC20 + #[abi(embed_v0)] + impl HypErc20MetadataImpl = + HypErc20Component::HypErc20MetadataImpl; + impl HypErc20InternalImpl = HypErc20Component::InternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + // E10 + const E10: u256 = 10_000_000_000; + const PRECISION: u256 = E10; + + #[storage] + struct Storage { + exchange_rate: u256, + collateral_domain: u32, + #[substorage(v0)] + hyp_erc20: HypErc20Component::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20Event: HypErc20Component::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + decimals: u8, + mailbox: ContractAddress, + total_supply: u256, + name: ByteArray, + symbol: ByteArray, + collateral_domain: u32, + wrapped_token: ContractAddress, + owner: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.hyp_erc20.initialize(decimals); + self.erc20.initializer(name, symbol); + self.erc20.mint(starknet::get_caller_address(), total_supply); + self.collateral_domain.write(collateral_domain); + self.exchange_rate.write(E10); + } + + impl TokenRouterTransferRemoteHookImpl of TokenRouterTransferRemoteHookTrait { + /// Initiates a remote token transfer with optional hooks and metadata. + /// + /// This function handles the transfer of tokens to a recipient on a remote domain. It converts + /// the token amount to shares, generates the token message, and dispatches the message to the + /// specified destination. The transfer can optionally use a hook for additional processing. + /// + /// # Arguments + /// + /// * `destination` - A `u32` representing the destination domain. + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `value` - A `u256` representing the value associated with the transfer. + /// * `hook_metadata` - An optional `Bytes` object containing metadata for the hook. + /// * `hook` - An optional `ContractAddress` representing the hook for additional processing. + /// + /// # Returns + /// + /// A `u256` representing the message ID of the dispatched transfer. + fn _transfer_remote( + ref self: TokenRouterComponent::ComponentState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256 { + let contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let shares = contract_state.assets_to_shares(amount_or_id); + TokenRouterHooksImpl::transfer_from_sender_hook(ref self, shares); + let token_message = TokenMessageTrait::format( + recipient, shares, BytesTrait::new_empty() + ); + let mut message_id = 0; + + match hook_metadata { + Option::Some(hook_metadata) => { + if !hook.is_some() { + panic!("Transfer remote invalid arguments, missing hook"); + } + + message_id = contract_state + .router + ._Router_dispatch( + destination, value, token_message, hook_metadata, hook.unwrap() + ); + }, + Option::None => { + let hook_metadata = contract_state + .gas_router + ._Gas_router_hook_metadata(destination); + let hook = contract_state.mailbox.get_hook(); + message_id = contract_state + .router + ._Router_dispatch(destination, value, token_message, hook_metadata, hook); + } + } + + self + .emit( + TokenRouterComponent::SentTransferRemote { + destination, recipient, amount: amount_or_id, + } + ); + + message_id + } + } + + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl HypeErc20Vault of super::IHypErc20Vault { + // Converts a specified amount of assets to shares based on the current exchange rate. + /// + /// This function calculates the number of shares corresponding to the given amount of assets + /// by using the exchange rate stored in the contract. + /// + /// # Arguments + /// + /// * `amount` - A `u256` representing the amount of assets to convert. + /// + /// # Returns + /// + /// A `u256` representing the number of shares equivalent to the given amount of assets. + fn assets_to_shares(self: @ContractState, amount: u256) -> u256 { + math::mul_div(amount, PRECISION, self.exchange_rate.read()) + } + + /// Converts a specified number of shares to assets based on the current exchange rate. + /// + /// This function calculates the number of assets corresponding to the given number of shares + /// by using the exchange rate stored in the contract. + /// + /// # Arguments + /// + /// * `shares` - A `u256` representing the number of shares to convert. + /// + /// # Returns + /// + /// A `u256` representing the number of assets equivalent to the given number of shares. + /// + fn shares_to_assets(self: @ContractState, shares: u256) -> u256 { + math::mul_div(shares, self.exchange_rate.read(), PRECISION) + } + + /// Returns the balance of shares for the specified account. + /// + /// This function retrieves the number of shares owned by the given account. The shares are represented + /// by the balance in the ERC20 component. + /// + /// # Arguments + /// + /// * `account` - A `ContractAddress` representing the account whose share balance is being queried. + /// + /// # Returns + /// + /// A `u256` representing the share balance of the specified account. + fn share_balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.erc20.balance_of(account) + } + + /// Returns the precision value used for calculations in the vault. + /// + /// This function returns the precision value applied to vault calculations, which is a constant + /// defined in the contract. + /// + /// # Returns + /// + /// A `u256` representing the precision value. + fn get_precision(self: @ContractState) -> u256 { + PRECISION + } + + /// Returns the collateral domain used by the vault. + /// + /// This function retrieves the collateral domain in which the vault operates, which is defined + /// at the time of contract deployment. + /// + /// # Returns + /// + /// A `u32` representing the collateral domain. + fn get_collateral_domain(self: @ContractState) -> u32 { + self.collateral_domain.read() + } + + /// Returns the current exchange rate between assets and shares. + /// + /// This function retrieves the current exchange rate used by the vault for converting assets + /// to shares and vice versa. + /// + /// # Returns + /// + /// A `u256` representing the exchange rate. + fn get_exchange_rate(self: @ContractState) -> u256 { + self.exchange_rate.read() + } + } + + impl MessageRecipientInternalHookImpl of IMessageRecipientInternalHookTrait { + /// Handles incoming messages and updates the exchange rate if necessary. + /// + /// This internal function processes messages received from remote domains. If the message + /// is from the collateral domain, it updates the vault's exchange rate based on the metadata + /// contained in the message. + /// + /// # Arguments + /// + /// * `origin` - A `u32` representing the origin domain of the message. + /// * `sender` - A `u256` representing the sender of the message. + /// * `message` - A `Bytes` object containing the message data. + fn _handle( + ref self: RouterComponent::ComponentState, + origin: u32, + sender: u256, + message: Bytes + ) { + let mut contract_state = RouterComponent::HasComponent::get_contract_mut(ref self); + if origin == contract_state.collateral_domain.read() { + let (_, exchange_rate) = message.metadata().read_u256(0); + contract_state.exchange_rate.write(exchange_rate); + } + TokenRouterComponent::MessageRecipientInternalHookImpl::_handle( + ref self, origin, sender, message + ); + } + } + + #[abi(embed_v0)] + impl ERC20VaultImpl of IERC20 { + fn total_supply(self: @ContractState) -> u256 { + self.erc20.total_supply() + } + + // Overrides ERC20.balance_of() + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + let balance = self.erc20.balance_of(account); + self.shares_to_assets(balance) + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.erc20.allowance(owner, spender) + } + // Overrides ERC20.transfer() + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + self.erc20.transfer(recipient, self.assets_to_shares(amount)); + true + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.erc20.transfer_from(sender, recipient, amount) + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + self.erc20.approve(spender, amount) + } + } + + #[abi(embed_v0)] + impl ERC20VaultCamelOnlyImpl of IERC20CamelOnly { + fn totalSupply(self: @ContractState) -> u256 { + self.erc20.totalSupply() + } + + // Overrides ERC20.balanceOf() + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + let balance = self.erc20.balance_of(account); + self.shares_to_assets(balance) + } + + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + self.erc20.transferFrom(sender, recipient, amount) + } + } +} diff --git a/starknet/cairo/crates/token/src/extensions/hyp_erc20_vault_collateral.cairo b/starknet/cairo/crates/token/src/extensions/hyp_erc20_vault_collateral.cairo new file mode 100644 index 00000000000..507e0ec9aad --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_erc20_vault_collateral.cairo @@ -0,0 +1,338 @@ +#[starknet::interface] +pub trait IHypErc20VaultCollateral { + fn rebase(ref self: TContractState, destination_domain: u32, value: u256); + // getters + fn get_vault(self: @TContractState) -> starknet::ContractAddress; + fn get_precision(self: @TContractState) -> u256; + fn get_null_recipient(self: @TContractState) -> u256; +} + +#[starknet::contract] +mod HypErc20VaultCollateral { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use contracts::libs::math; + use contracts::utils::utils::U256TryIntoContractAddress; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ERC20ABIDispatcherTrait}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::token_message::TokenMessageTrait; + use token::components::{ + token_router::{ + TokenRouterComponent, TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterComponent::{TokenRouterHooksTrait, TokenRouterTransferRemoteHookTrait} + }, + hyp_erc20_collateral_component::HypErc20CollateralComponent, + }; + use token::interfaces::ierc4626::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!( + path: HypErc20CollateralComponent, storage: collateral, event: HypErc20CollateralEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + impl RouterInternalImpl = RouterComponent::RouterComponentInternalImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + impl GasRouterInternalImpl = GasRouterComponent::GasRouterInternalImpl; + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + // HypERC20Collateral + #[abi(embed_v0)] + impl HypErc20CollateralImpl = + HypErc20CollateralComponent::HypErc20CollateralImpl; + impl HypErc20CollateralInternalImpl = + HypErc20CollateralComponent::HypErc20CollateralInternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + // E10 + const PRECISION: u256 = 10_000_000_000; + const NULL_RECIPIENT: u256 = 1; + + #[storage] + struct Storage { + vault: ERC4626ABIDispatcher, + #[substorage(v0)] + collateral: HypErc20CollateralComponent::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20CollateralEvent: HypErc20CollateralComponent::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + vault: ContractAddress, + owner: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + let vault_dispatcher = ERC4626ABIDispatcher { contract_address: vault }; + let erc20 = vault_dispatcher.asset(); + self.collateral.initialize(erc20); + self.vault.write(vault_dispatcher); + } + + impl TokenRouterTransferRemoteHookImpl of TokenRouterTransferRemoteHookTrait { + /// Initiates a remote token transfer with optional hooks and metadata. + /// + /// This function handles the process of transferring tokens to a recipient on a remote domain. + /// It deposits the token amount into the vault, calculates the exchange rate, and appends it to the token metadata. + /// The transfer is then dispatched to the specified destination domain using the provided hook and metadata. + /// + /// # Arguments + /// + /// * `destination` - A `u32` representing the destination domain. + /// * `recipient` - A `u256` representing the recipient's address on the remote domain. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `value` - A `u256` representing the value associated with the transfer. + /// * `hook_metadata` - An optional `Bytes` object containing metadata for the hook. + /// * `hook` - An optional `ContractAddress` representing the hook for additional processing. + /// + /// # Returns + /// + /// A `u256` representing the message ID of the dispatched transfer. + fn _transfer_remote( + ref self: TokenRouterComponent::ComponentState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256 { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + TokenRouterHooksTraitImpl::transfer_from_sender_hook(ref self, amount_or_id); + let shares = contract_state._deposit_into_vault(amount_or_id); + let vault = contract_state.vault.read(); + let exchange_rate = math::mul_div( + PRECISION, vault.total_assets(), vault.total_supply(), + ); + let mut token_metadata: Bytes = BytesTrait::new_empty(); + token_metadata.append_u256(exchange_rate); + let token_message = TokenMessageTrait::format(recipient, shares, token_metadata); + let mut message_id = 0; + + match hook_metadata { + Option::Some(hook_metadata) => { + if !hook.is_some() { + panic!("Transfer remote invalid arguments, missing hook"); + } + + message_id = contract_state + .router + ._Router_dispatch( + destination, value, token_message, hook_metadata, hook.unwrap() + ); + }, + Option::None => { + let hook_metadata = contract_state + .gas_router + ._Gas_router_hook_metadata(destination); + let hook = contract_state.mailbox.get_hook(); + message_id = contract_state + .router + ._Router_dispatch(destination, value, token_message, hook_metadata, hook); + } + } + + self + .emit( + TokenRouterComponent::SentTransferRemote { + destination, recipient, amount: amount_or_id, + } + ); + message_id + } + } + + impl TokenRouterHooksTraitImpl of TokenRouterHooksTrait { + /// Transfers tokens from the sender and generates metadata. + /// + /// This hook is invoked during the transfer of tokens from the sender as part of the token router process. + /// It generates metadata for the token transfer based on the amount or token ID provided and processes the + /// transfer by depositing the amount into the vault. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// + /// # Returns + /// + /// A `Bytes` object representing the metadata associated with the token transfer. + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + HypErc20CollateralComponent::TokenRouterHooksImpl::transfer_from_sender_hook( + ref self, amount_or_id + ) + } + + /// Processes a token transfer to a recipient. + /// + /// This hook handles the transfer of tokens to the recipient as part of the token router process. It withdraws + /// the specified amount from the vault and transfers it to the recipient's address. The hook also processes any + /// associated metadata. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `metadata` - A `Bytes` object containing metadata associated with the transfer. + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let recipient: ContractAddress = recipient.try_into().unwrap(); + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + // withdraw with the specified amount of shares + contract_state + .vault + .read() + .redeem( + amount_or_id, + recipient.try_into().expect('u256 to ContractAddress failed'), + starknet::get_contract_address() + ); + } + } + #[abi(embed_v0)] + impl HypeErc20VaultCollateral of super::IHypErc20VaultCollateral { + /// Rebases the vault collateral and sends a message to a remote domain. + /// + /// This function handles rebalancing the vault collateral by sending a rebase operation + /// to the specified remote domain. It sends a message indicating the amount of the rebase + /// without specifying a recipient (null recipient). + /// + /// # Arguments + /// + /// * `destination_domain` - A `u32` representing the destination domain to which the rebase message is sent. + /// * `value` - A `u256` representing the value to be used for the rebase operation. + fn rebase(ref self: ContractState, destination_domain: u32, value: u256) { + TokenRouterTransferRemoteHookImpl::_transfer_remote( + ref self.token_router, + destination_domain, + NULL_RECIPIENT, + 0, + value, + Option::None, + Option::None, + ); + } + + /// Returns the contract address of the vault. + /// + /// This function retrieves the vault's contract address where the ERC20 collateral is stored. + /// + /// # Returns + /// + /// A `ContractAddress` representing the vault's contract address. + fn get_vault(self: @ContractState) -> ContractAddress { + self.vault.read().contract_address + } + + // Returns the precision value used for calculations in the vault. + /// + /// This function returns the precision value that is applied to the vault's calculations, + /// which is a constant value. + /// + /// # Returns + /// + /// A `u256` representing the precision used in the vault. + fn get_precision(self: @ContractState) -> u256 { + PRECISION + } + + /// Returns the null recipient used in rebase operations. + /// + /// This function retrieves the null recipient, which is a constant used in certain vault operations, + /// particularly during rebase operations. + /// + /// # Returns + /// + /// A `u256` representing the null recipient. + fn get_null_recipient(self: @ContractState) -> u256 { + NULL_RECIPIENT + } + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _deposit_into_vault(ref self: ContractState, amount: u256) -> u256 { + let vault = self.vault.read(); + self.collateral.wrapped_token.read().approve(vault.contract_address, amount); + vault.deposit(amount, starknet::get_contract_address()) + } + } +} diff --git a/starknet/cairo/crates/token/src/extensions/hyp_erc721_URI_collateral.cairo b/starknet/cairo/crates/token/src/extensions/hyp_erc721_URI_collateral.cairo new file mode 100644 index 00000000000..725156e86cd --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_erc721_URI_collateral.cairo @@ -0,0 +1,169 @@ +#[starknet::interface] +pub trait IHypERC721URICollateral { + fn initialize(ref self: TState); + fn owner_of(self: @TState, token_id: u256) -> u256; + fn balance_of(self: @TState, account: u256) -> u256; +} + +#[starknet::contract] +pub mod HypERC721URICollateral { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc721::interface::{ERC721ABIDispatcher, ERC721ABIDispatcherTrait,}; + use starknet::ContractAddress; + use token::components::hyp_erc721_collateral_component::{ + HypErc721CollateralComponent, IHypErc721Collateral + }; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!( + path: HypErc721CollateralComponent, + storage: hyp_erc721_collateral, + event: HypErc721CollateralEvent + ); + + // HypERC721 + #[abi(embed_v0)] + impl HypErc721CollateralImpl = + HypErc721CollateralComponent::HypErc721CollateralImpl; + + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + impl TokenRouterInternalImpl = TokenRouterComponent::TokenRouterInternalImpl; + + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Ownable + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + + #[storage] + struct Storage { + erc721: ContractAddress, + mailbox: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + hyp_erc721_collateral: HypErc721CollateralComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + HypErc721CollateralEvent: HypErc721CollateralComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + erc721: ContractAddress, + mailbox: ContractAddress, + hook: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self.mailboxclient.initialize(mailbox, Option::Some(hook), Option::None); + + self + .hyp_erc721_collateral + .wrapped_token + .write(ERC721ABIDispatcher { contract_address: erc721 }); + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + /// Transfers the token from the sender and retrieves its metadata. + /// + /// This hook handles the transfer of a token from the sender and appends its URI to the metadata. + /// It retrieves the token URI from the ERC721 contract and appends it to the metadata for processing + /// as part of the transfer message. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the token ID being transferred. + /// + /// # Returns + /// + /// A `Bytes` object containing the token's URI as metadata. + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + contract_state + .hyp_erc721_collateral + .wrapped_token + .read() + .transfer_from( + starknet::get_caller_address(), starknet::get_contract_address(), amount_or_id + ); + + let uri = contract_state + .hyp_erc721_collateral + .wrapped_token + .read() + .token_uri(amount_or_id); + + let mut metadata = BytesTrait::new_empty(); + + let len = uri.len(); + let mut i = 0; + while i < len { + metadata.append_u8(uri.at(i).expect('Invalid metadata')); + i += 1; + }; + + metadata + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) {} + } +} diff --git a/starknet/cairo/crates/token/src/extensions/hyp_erc721_URI_storage.cairo b/starknet/cairo/crates/token/src/extensions/hyp_erc721_URI_storage.cairo new file mode 100644 index 00000000000..bf62f038ff8 --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_erc721_URI_storage.cairo @@ -0,0 +1,201 @@ +#[starknet::contract] +pub mod HypERC721URIStorage { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl,}; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, get_caller_address}; + use token::components::erc721_uri_storage::ERC721URIStorageComponent; + use token::components::hyp_erc721_component::{HypErc721Component}; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: HypErc721Component, storage: hyp_erc721, event: HypErc721Event); + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!( + path: ERC721URIStorageComponent, storage: erc721_uri_storage, event: ERC721UriStorageEvent + ); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + + //Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + impl RouterInternalImpl = RouterComponent::RouterComponentInternalImpl; + + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + impl TokenRouterInternalImpl = TokenRouterComponent::TokenRouterInternalImpl; + + //HypERC721 + impl HypErc721InternalImpl = HypErc721Component::HypErc721InternalImpl; + + //ERC721 + #[abi(embed_v0)] + impl ERC721URIStorageImpl = + ERC721URIStorageComponent::ERC721URIStorageImpl; + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721CamelOnlyImpl = ERC721Component::ERC721CamelOnlyImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + impl ERC721URIStorageInternalImpl = + ERC721URIStorageComponent::ERC721URIStorageInternalImpl; + + //upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + hyp_erc721: HypErc721Component::Storage, + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + #[substorage(v0)] + erc721_uri_storage: ERC721URIStorageComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + HypErc721Event: HypErc721Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + ERC721UriStorageEvent: ERC721URIStorageComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + _mint_amount: u256, + _name: ByteArray, + _symbol: ByteArray, + _hook: ContractAddress, + _interchainSecurityModule: ContractAddress, + owner: ContractAddress, + ) { + self.ownable.initializer(owner); + self + .mailboxclient + .initialize(mailbox, Option::Some(_hook), Option::Some(_interchainSecurityModule)); + self.hyp_erc721.initialize(_mint_amount, _name, _symbol); + } + + #[abi(embed_v0)] + impl HypErc721Upgradeable of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let contract_state = TokenRouterComponent::HasComponent::get_contract(@self); + let token_owner = contract_state.erc721.owner_of(amount_or_id); + assert!(token_owner == get_caller_address(), "Caller is not owner of token"); + + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + contract_state.erc721.burn(amount_or_id); + + BytesTrait::new_empty() + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let recipient_felt: felt252 = recipient.try_into().expect('u256 to felt failed'); + let recipient: ContractAddress = recipient_felt.try_into().unwrap(); + + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + + let metadata_byteArray = bytes_to_byte_array(metadata); + contract_state.erc721_uri_storage._set_token_uri(amount_or_id, metadata_byteArray); + contract_state.erc721.mint(recipient, amount_or_id); + } + } + + // free function + fn bytes_to_byte_array(self: Bytes) -> ByteArray { + let mut res: ByteArray = Default::default(); + let mut offset = 0; + while offset < self + .size() { + if offset + 31 <= self.size() { + let (new_offset, value) = self.read_bytes31(offset); + res.append_word(value.into(), 31); + offset = new_offset; + } else { + let (new_offset, value) = self.read_u8(offset); + res.append_byte(value); + offset = new_offset; + } + }; + res + } +} diff --git a/starknet/cairo/crates/token/src/extensions/hyp_fiat_token.cairo b/starknet/cairo/crates/token/src/extensions/hyp_fiat_token.cairo new file mode 100644 index 00000000000..7a67b4a0866 --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_fiat_token.cairo @@ -0,0 +1,180 @@ +#[starknet::contract] +pub mod HypFiatToken { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use contracts::utils::utils::U256TryIntoContractAddress; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::{ + hyp_erc20_collateral_component::HypErc20CollateralComponent, + token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }, + }; + use token::interfaces::ifiat_token::{IFiatTokenDispatcher, IFiatTokenDispatcherTrait}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!( + path: HypErc20CollateralComponent, storage: collateral, event: HypErc20CollateralEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + impl TokenRouterInternalImpl = TokenRouterComponent::TokenRouterInternalImpl; + + // HypERC20Collateral + #[abi(embed_v0)] + impl HypErc20CollateralImpl = + HypErc20CollateralComponent::HypErc20CollateralImpl; + impl HypErc20CollateralInternalImpl = + HypErc20CollateralComponent::HypErc20CollateralInternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + collateral: HypErc20CollateralComponent::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20CollateralEvent: HypErc20CollateralComponent::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + fiat_token: ContractAddress, + mailbox: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.collateral.initialize(fiat_token); + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + /// Transfers tokens from the sender and burns them. + /// + /// This hook transfers tokens from the sender and then burns the corresponding amount of the fiat token. + /// It retrieves the token metadata and ensures that the correct amount is transferred from the sender. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// + /// # Returns + /// + /// A `Bytes` object containing the metadata for the transfer. + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let metadata = contract_state.collateral._transfer_from_sender(amount_or_id); + let token: ERC20ABIDispatcher = contract_state.collateral.wrapped_token.read(); + IFiatTokenDispatcher { contract_address: token.contract_address }.burn(amount_or_id); + metadata + } + + /// Transfers tokens to the recipient and mints new fiat tokens. + /// + /// This hook transfers tokens to the recipient and mints the corresponding amount of fiat tokens + /// based on the provided amount or token ID. It ensures that the mint operation is successful. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `metadata` - A `Bytes` object containing metadata associated with the transfer. + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let token: ERC20ABIDispatcher = contract_state.collateral.wrapped_token.read(); + assert!( + IFiatTokenDispatcher { contract_address: token.contract_address } + .mint( + recipient.try_into().expect('u256 to ContractAddress failed'), amount_or_id + ), + "FiatToken mint failed" + ); + } + } +} + diff --git a/starknet/cairo/crates/token/src/extensions/hyp_native_scaled.cairo b/starknet/cairo/crates/token/src/extensions/hyp_native_scaled.cairo new file mode 100644 index 00000000000..41fd4fb2b69 --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_native_scaled.cairo @@ -0,0 +1,181 @@ +#[starknet::interface] +trait IHypNativeScaled { + fn get_scale(self: @TState) -> u256; +} + +#[starknet::contract] +pub mod HypNativeScaled { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ + interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}, ERC20Component, + ERC20HooksEmptyImpl + }; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::hyp_native_component::{HypNativeComponent}; + use token::components::token_message::TokenMessageTrait; + use token::components::token_router::{ + TokenRouterComponent, ITokenRouter, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: HypNativeComponent, storage: hyp_native, event: HypNativeEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // ERC20 + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20MixinImpl; + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // HypNative + #[abi(embed_v0)] + impl HypNativeImpl = HypNativeComponent::HypNativeImpl; + #[abi(embed_v0)] + impl HypNativeTokenRouterImpl = + HypNativeComponent::TokenRouterImpl; + impl HypNativeInternalImpl = HypNativeComponent::HypNativeInternalImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // TokenRouter + impl TokenRouterImpl = TokenRouterComponent::TokenRouterInternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + scale: u256, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + hyp_native: HypNativeComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + HypNativeEvent: HypNativeComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + ERC20Event: ERC20Component::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, owner: ContractAddress, scale: u256, mailbox: ContractAddress + ) { + self.mailboxclient.initialize(mailbox, Option::None, Option::None); + self.ownable.initializer(owner); + self.scale.write(scale); + } + + impl HypNativeScaled of super::IHypNativeScaled { + fn get_scale(self: @ContractState) -> u256 { + self.scale.read() + } + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + #[embeddable_as(TokenRouterImpl)] + impl TokenRouter of ITokenRouter { + fn transfer_remote( + ref self: ContractState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256 { + let hook_payment = value - amount_or_id; + let scaled_amount = amount_or_id / self.scale.read(); + TokenRouterTransferRemoteHookDefaultImpl::_transfer_remote( + ref self.token_router, + destination, + recipient, + scaled_amount, + hook_payment, + Option::None, + Option::None + ) + } + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + BytesTrait::new_empty() + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let scaled_amount = amount_or_id * contract_state.scale.read(); + contract_state.hyp_native._transfer_to(recipient, scaled_amount); + } + } +} diff --git a/starknet/cairo/crates/token/src/extensions/hyp_xerc20.cairo b/starknet/cairo/crates/token/src/extensions/hyp_xerc20.cairo new file mode 100644 index 00000000000..9d559102e70 --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_xerc20.cairo @@ -0,0 +1,178 @@ +#[starknet::contract] +pub mod HypXERC20 { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use contracts::utils::utils::U256TryIntoContractAddress; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::hyp_erc20_collateral_component::HypErc20CollateralComponent; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }; + use token::interfaces::ixerc20::{IXERC20Dispatcher, IXERC20DispatcherTrait}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!( + path: HypErc20CollateralComponent, + storage: hyp_erc20_collateral, + event: HypErc20CollateralEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + // HypERC20Collateral + #[abi(embed_v0)] + impl HypErc20CollateralImpl = + HypErc20CollateralComponent::HypErc20CollateralImpl; + impl HypErc20CollateralInternalImpl = + HypErc20CollateralComponent::HypErc20CollateralInternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + // Token Router + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + hyp_erc20_collateral: HypErc20CollateralComponent::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20CollateralEvent: HypErc20CollateralComponent::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + wrapped_token: ContractAddress, + owner: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.hyp_erc20_collateral.initialize(wrapped_token); + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + /// Transfers tokens from the sender, burns the xERC20 tokens, and returns metadata. + /// + /// This hook transfers tokens from the sender, burns the corresponding xERC20 tokens, and returns any metadata + /// associated with the transfer. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// + /// # Returns + /// + /// A `Bytes` object representing the metadata associated with the transfer. + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let token: ERC20ABIDispatcher = contract_state + .hyp_erc20_collateral + .wrapped_token + .read(); + IXERC20Dispatcher { contract_address: token.contract_address } + .burn(starknet::get_caller_address(), amount_or_id); + BytesTrait::new_empty() + } + + /// Mints xERC20 tokens for the recipient and returns the transferred amount. + /// + /// This hook mints xERC20 tokens for the recipient based on the transferred amount of tokens and updates the + /// corresponding ERC20 balances. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `metadata` - A `Bytes` object containing metadata associated with the transfer. + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let token: ERC20ABIDispatcher = contract_state + .hyp_erc20_collateral + .wrapped_token + .read(); + IXERC20Dispatcher { contract_address: token.contract_address } + .mint(recipient.try_into().unwrap(), amount_or_id); + } + } +} diff --git a/starknet/cairo/crates/token/src/extensions/hyp_xerc20_lockbox.cairo b/starknet/cairo/crates/token/src/extensions/hyp_xerc20_lockbox.cairo new file mode 100644 index 00000000000..49b968c6841 --- /dev/null +++ b/starknet/cairo/crates/token/src/extensions/hyp_xerc20_lockbox.cairo @@ -0,0 +1,237 @@ +#[starknet::interface] +pub trait IHypXERC20Lockbox { + fn approve_lockbox(ref self: TState); + fn lockbox(ref self: TState) -> starknet::ContractAddress; + fn xERC20(ref self: TState) -> starknet::ContractAddress; +} + +#[starknet::contract] +pub mod HypXERC20Lockbox { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use contracts::utils::utils::U256TryIntoContractAddress; + use core::integer::BoundedInt; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::{ + hyp_erc20_collateral_component::HypErc20CollateralComponent, + token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }, + }; + use token::interfaces::ixerc20::{IXERC20Dispatcher, IXERC20DispatcherTrait}; + use token::interfaces::ixerc20_lockbox::{ + IXERC20LockboxDispatcher, IXERC20LockboxDispatcherTrait + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!( + path: HypErc20CollateralComponent, storage: collateral, event: HypErc20CollateralEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + impl TokenRouterInternalImpl = TokenRouterComponent::TokenRouterInternalImpl; + // HypERC20Collateral + #[abi(embed_v0)] + impl HypErc20CollateralImpl = + HypErc20CollateralComponent::HypErc20CollateralImpl; + impl HypErc20CollateralInternalImpl = + HypErc20CollateralComponent::HypErc20CollateralInternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + collateral: HypErc20CollateralComponent::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + lockbox: IXERC20LockboxDispatcher, + xerc20: IXERC20Dispatcher, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20CollateralEvent: HypErc20CollateralComponent::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + lockbox: ContractAddress, + owner: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + let lockbox_dispatcher = IXERC20LockboxDispatcher { contract_address: lockbox }; + let erc20 = lockbox_dispatcher.erc20(); + self.collateral.initialize(erc20); + let xerc20 = lockbox_dispatcher.xerc20(); + self.xerc20.write(IXERC20Dispatcher { contract_address: xerc20 }); + self.lockbox.write(lockbox_dispatcher); + self.approve_lockbox(); + } + + #[abi(embed_v0)] + impl HypXERC20LockboxImpl of super::IHypXERC20Lockbox { + /// Approves the lockbox for both the ERC20 and xERC20 tokens. + /// + /// This function approves the lockbox contract to handle the maximum allowed amount of both the ERC20 and xERC20 tokens. + /// It ensures that both the ERC20 and xERC20 tokens are authorized for transfer to the lockbox. + fn approve_lockbox(ref self: ContractState) { + let lockbox_address = self.lockbox.read().contract_address; + assert!( + self.collateral.wrapped_token.read().approve(lockbox_address, BoundedInt::max()), + "erc20 lockbox approve failed" + ); + assert!( + ERC20ABIDispatcher { contract_address: self.xerc20.read().contract_address } + .approve(lockbox_address, BoundedInt::max()), + "xerc20 lockbox approve failed" + ); + } + + /// Retrieves the contract address of the lockbox. + /// + /// This function returns the `ContractAddress` of the lockbox that has been approved for the ERC20 and xERC20 tokens. + /// + /// # Returns + /// + /// The `ContractAddress` of the lockbox. + fn lockbox(ref self: ContractState) -> ContractAddress { + self.lockbox.read().contract_address + } + + /// Retrieves the contract address of the xERC20 token. + /// + /// This function returns the `ContractAddress` of the xERC20 token that is used in conjunction with the lockbox. + /// + /// # Returns + /// + /// The `ContractAddress` of the xERC20 token. + fn xERC20(ref self: ContractState) -> ContractAddress { + self.xerc20.read().contract_address + } + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + /// Transfers tokens from the sender, deposits them into the lockbox, and burns the corresponding xERC20 tokens. + /// + /// This hook first transfers tokens from the sender, deposits them into the lockbox, and then burns the + /// corresponding xERC20 tokens associated with the transfer. + /// + /// # Arguments + /// + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// + /// # Returns + /// + /// A `Bytes` object representing the transfer metadata. + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + contract_state.collateral._transfer_from_sender(amount_or_id); + + contract_state.lockbox.read().deposit(amount_or_id); + + contract_state.xerc20.read().burn(starknet::get_contract_address(), amount_or_id); + BytesTrait::new_empty() + } + + /// Transfers tokens to the recipient, mints xERC20 tokens, and withdraws tokens from the lockbox. + /// + /// This hook first mints the corresponding xERC20 tokens and then withdraws the corresponding amount + /// of ERC20 tokens from the lockbox to the specified recipient. + /// + /// # Arguments + /// + /// * `recipient` - A `u256` representing the recipient's address. + /// * `amount_or_id` - A `u256` representing the amount of tokens or token ID to transfer. + /// * `metadata` - A `Bytes` object containing metadata associated with the transfer. + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + + contract_state.xerc20.read().mint(starknet::get_contract_address(), amount_or_id); + contract_state.lockbox.read().withdraw_to(recipient, amount_or_id); + } + } +} + diff --git a/starknet/cairo/crates/token/src/hyp_erc20.cairo b/starknet/cairo/crates/token/src/hyp_erc20.cairo new file mode 100644 index 00000000000..6cd18e2b1a1 --- /dev/null +++ b/starknet/cairo/crates/token/src/hyp_erc20.cairo @@ -0,0 +1,136 @@ +#[starknet::contract] +pub mod HypErc20 { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::{ + hyp_erc20_component::{HypErc20Component, HypErc20Component::TokenRouterHooksImpl}, + token_router::{ + TokenRouterComponent, TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + } + }; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: HypErc20Component, storage: hyp_erc20, event: HypErc20Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + // ERC20 + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + // HypERC20 + #[abi(embed_v0)] + impl HypErc20MetadataImpl = + HypErc20Component::HypErc20MetadataImpl; + impl HypErc20InternalImpl = HypErc20Component::InternalImpl; + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + hyp_erc20: HypErc20Component::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20Event: HypErc20Component::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + decimals: u8, + mailbox: ContractAddress, + total_supply: u256, + name: ByteArray, + symbol: ByteArray, + hook: ContractAddress, + interchain_security_module: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.hyp_erc20.initialize(decimals); + self.erc20.initializer(name, symbol); + self.erc20.mint(starknet::get_caller_address(), total_supply); + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } +} diff --git a/starknet/cairo/crates/token/src/hyp_erc20_collateral.cairo b/starknet/cairo/crates/token/src/hyp_erc20_collateral.cairo new file mode 100644 index 00000000000..72f9b770140 --- /dev/null +++ b/starknet/cairo/crates/token/src/hyp_erc20_collateral.cairo @@ -0,0 +1,124 @@ +#[starknet::contract] +pub mod HypErc20Collateral { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::{ + token_router::{ + TokenRouterComponent, TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }, + hyp_erc20_collateral_component::{ + HypErc20CollateralComponent, HypErc20CollateralComponent::TokenRouterHooksImpl + }, + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: MailboxclientComponent, storage: mailbox, event: MailBoxClientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!( + path: HypErc20CollateralComponent, storage: collateral, event: HypErc20CollateralEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + // HypERC20Collateral + #[abi(embed_v0)] + impl HypErc20CollateralImpl = + HypErc20CollateralComponent::HypErc20CollateralImpl; + impl HypErc20CollateralInternalImpl = + HypErc20CollateralComponent::HypErc20CollateralInternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + collateral: HypErc20CollateralComponent::Storage, + #[substorage(v0)] + mailbox: MailboxclientComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + HypErc20CollateralEvent: HypErc20CollateralComponent::Event, + #[flat] + MailBoxClientEvent: MailboxclientComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + erc20: ContractAddress, + owner: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailbox + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.collateral.initialize(erc20); + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } +} diff --git a/starknet/cairo/crates/token/src/hyp_erc721.cairo b/starknet/cairo/crates/token/src/hyp_erc721.cairo new file mode 100644 index 00000000000..8f1f2122eae --- /dev/null +++ b/starknet/cairo/crates/token/src/hyp_erc721.cairo @@ -0,0 +1,195 @@ +use alexandria_bytes::Bytes; +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IHypErc721 { + fn initialize(ref self: TState); + fn balance_of(self: @TState) -> u256; + fn handle(ref self: TState, origin: u32, sender: ContractAddress, message: Bytes); +} + +#[starknet::contract] +pub mod HypErc721 { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::ContractAddress; + use token::components::erc721_enumerable::ERC721EnumerableComponent; + use token::components::hyp_erc721_component::HypErc721Component; + use token::components::token_message::TokenMessageTrait; + use token::components::token_router::{ + TokenRouterComponent, ITokenRouter, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }; + use token::interfaces::imessage_recipient::IMessageRecipient; + // also needs {https://github.com/OpenZeppelin/cairo-contracts/blob/main/packages/token/src/erc721/extensions/erc721_enumerable/erc721_enumerable.cairo} + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + component!(path: HypErc721Component, storage: hyp_erc721, event: HypErc721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!( + path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent + ); + // ERC721 Mixin + #[abi(embed_v0)] + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // ERC721Enumerable + #[abi(embed_v0)] + impl ERC721EnumerableImpl = + ERC721EnumerableComponent::ERC721EnumerableImpl; + // impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl; + + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + // HypERC721 + impl HypErc721InternalImpl = HypErc721Component::HypErc721InternalImpl; + + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + impl TokenRouterInternalImpl = TokenRouterComponent::TokenRouterInternalImpl; + + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + impl GasRouterInternalImpl = GasRouterComponent::GasRouterInternalImpl; + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + impl RouterInternalImpl = RouterComponent::RouterComponentInternalImpl; + + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + #[substorage(v0)] + hyp_erc721: HypErc721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + erc721_enumerable: ERC721EnumerableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + HypErc721Event: HypErc721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + ERC721EnumerableEvent: ERC721EnumerableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + name: ByteArray, + symbol: ByteArray, + mint_amount: u256, + hook: ContractAddress, + interchain_security_module: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailboxclient + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.hyp_erc721.initialize(mint_amount, name, symbol); + } + + #[abi(embed_v0)] + impl HypErc721Upgradeable of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let contract_state = TokenRouterComponent::HasComponent::get_contract(@self); + let token_owner = contract_state.erc721.owner_of(amount_or_id); + assert!(token_owner == starknet::get_caller_address(), "Caller is not owner"); + + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + contract_state.erc721.burn(amount_or_id); + + BytesTrait::new_empty() + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let recipient_felt: felt252 = recipient.try_into().expect('u256 to felt failed'); + let recipient: ContractAddress = recipient_felt.try_into().unwrap(); + + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + contract_state.erc721.mint(recipient, amount_or_id); + } + } +} + diff --git a/starknet/cairo/crates/token/src/hyp_erc721_collateral.cairo b/starknet/cairo/crates/token/src/hyp_erc721_collateral.cairo new file mode 100644 index 00000000000..f6699585ce8 --- /dev/null +++ b/starknet/cairo/crates/token/src/hyp_erc721_collateral.cairo @@ -0,0 +1,178 @@ +#[starknet::contract] +pub mod HypErc721Collateral { + use alexandria_bytes::{Bytes, BytesTrait}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc721::interface::{ERC721ABIDispatcher, ERC721ABIDispatcherTrait,}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::hyp_erc721_collateral_component::{HypErc721CollateralComponent}; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl, + TokenRouterTransferRemoteHookDefaultImpl + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!( + path: HypErc721CollateralComponent, + storage: hyp_erc721_collateral, + event: HypErc721CollateralEvent + ); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // HypERC721 + #[abi(embed_v0)] + impl HypErc721CollateralImpl = + HypErc721CollateralComponent::HypErc721CollateralImpl; + + // TokenRouter + #[abi(embed_v0)] + impl TokenRouterImpl = TokenRouterComponent::TokenRouterImpl; + impl TokenRouterInternalImpl = TokenRouterComponent::TokenRouterInternalImpl; + + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + wrapped_token: ERC721ABIDispatcher, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + hyp_erc721_collateral: HypErc721CollateralComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + HypErc721CollateralEvent: HypErc721CollateralComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + erc721: ContractAddress, + mailbox: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailboxclient + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + self.wrapped_token.write(ERC721ABIDispatcher { contract_address: erc721 }); + } + + impl TokenRouterHooksImpl of TokenRouterHooksTrait { + fn transfer_from_sender_hook( + ref self: TokenRouterComponent::ComponentState, amount_or_id: u256 + ) -> Bytes { + let contract_state = TokenRouterComponent::HasComponent::get_contract(@self); + contract_state + .wrapped_token + .read() + .transfer_from( + starknet::get_caller_address(), starknet::get_contract_address(), amount_or_id + ); + + BytesTrait::new_empty() + } + + fn transfer_to_hook( + ref self: TokenRouterComponent::ComponentState, + recipient: u256, + amount_or_id: u256, + metadata: Bytes + ) { + let mut contract_state = TokenRouterComponent::HasComponent::get_contract_mut(ref self); + let recipient_felt: felt252 = recipient.try_into().expect('u256 to felt failed'); + let recipient: ContractAddress = recipient_felt.try_into().unwrap(); + + let metadata_array_u128 = metadata.data(); + let mut metadata_array_felt252: Array = array![]; + + let len = metadata_array_u128.len(); + let mut i = 0; + while i < len { + let metadata_felt252: felt252 = (*metadata_array_u128.at(i)) + .try_into() + .expect('u128 to felt failed'); + metadata_array_felt252.append(metadata_felt252); + i = i + 1; + }; + + contract_state + .wrapped_token + .read() + .safe_transfer_from( + starknet::get_contract_address(), + recipient, + amount_or_id, + metadata_array_felt252.span() + ); + } + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } +} diff --git a/starknet/cairo/crates/token/src/hyp_native.cairo b/starknet/cairo/crates/token/src/hyp_native.cairo new file mode 100644 index 00000000000..ce546a1860e --- /dev/null +++ b/starknet/cairo/crates/token/src/hyp_native.cairo @@ -0,0 +1,129 @@ +#[starknet::contract] +pub mod HypNative { + use alexandria_bytes::bytes::{BytesTrait, Bytes}; + use contracts::client::gas_router_component::GasRouterComponent; + use contracts::client::mailboxclient_component::MailboxclientComponent; + use contracts::client::router_component::RouterComponent; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin::upgrades::interface::IUpgradeable; + use openzeppelin::upgrades::upgradeable::UpgradeableComponent; + use starknet::ContractAddress; + use token::components::hyp_native_component::{ + HypNativeComponent, HypNativeComponent::TokenRouterHooksImpl + }; + use token::components::token_router::{ + TokenRouterComponent, TokenRouterComponent::TokenRouterHooksTrait, + TokenRouterComponent::MessageRecipientInternalHookImpl + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: TokenRouterComponent, storage: token_router, event: TokenRouterEvent); + component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent); + component!(path: RouterComponent, storage: router, event: RouterEvent); + component!(path: GasRouterComponent, storage: gas_router, event: GasRouterEvent); + component!(path: HypNativeComponent, storage: hyp_native, event: HypNativeEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + // HypNative + #[abi(embed_v0)] + impl HypNativeImpl = HypNativeComponent::HypNativeImpl; + #[abi(embed_v0)] + impl HypNativeTokenRouterImpl = + HypNativeComponent::TokenRouterImpl; + impl HypNativeInternalImpl = HypNativeComponent::HypNativeInternalImpl; + + // GasRouter + #[abi(embed_v0)] + impl GasRouterImpl = GasRouterComponent::GasRouterImpl; + + // Ownable + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + // Router + #[abi(embed_v0)] + impl RouterImpl = RouterComponent::RouterImpl; + + // MailboxClient + #[abi(embed_v0)] + impl MailboxClientImpl = + MailboxclientComponent::MailboxClientImpl; + impl MailboxClientInternalImpl = + MailboxclientComponent::MailboxClientInternalImpl; + // ERC20 + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + // Upgradeable + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + token_router: TokenRouterComponent::Storage, + #[substorage(v0)] + mailboxclient: MailboxclientComponent::Storage, + #[substorage(v0)] + router: RouterComponent::Storage, + #[substorage(v0)] + gas_router: GasRouterComponent::Storage, + #[substorage(v0)] + hyp_native: HypNativeComponent::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + TokenRouterEvent: TokenRouterComponent::Event, + #[flat] + MailboxclientEvent: MailboxclientComponent::Event, + #[flat] + RouterEvent: RouterComponent::Event, + #[flat] + GasRouterEvent: GasRouterComponent::Event, + #[flat] + HypNativeEvent: HypNativeComponent::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + mailbox: ContractAddress, + hook: ContractAddress, + interchain_security_module: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self + .mailboxclient + .initialize(mailbox, Option::Some(hook), Option::Some(interchain_security_module)); + } + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the contract to a new implementation. + /// Callable only by the owner + /// # Arguments + /// + /// * `new_class_hash` - The class hash of the new implementation. + fn upgrade(ref self: ContractState, new_class_hash: starknet::ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable.upgrade(new_class_hash); + } + } +} diff --git a/starknet/cairo/crates/token/src/interfaces/ierc4626.cairo b/starknet/cairo/crates/token/src/interfaces/ierc4626.cairo new file mode 100644 index 00000000000..84e2a1fbe6c --- /dev/null +++ b/starknet/cairo/crates/token/src/interfaces/ierc4626.cairo @@ -0,0 +1,128 @@ +//! ERC4626 Interface +//! For details see {https://eips.ethereum.org/EIPS/eip-4626} +//! Modified from {https://github.com/0xHashstack/hashstack_contracts/blob/main/src/token/erc4626/IERC4626.cairo} +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC4626 { + // ************************************ + // * IERC20 + // ************************************ + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + // ************************************ + // * IERC20 metadata + // ************************************ + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; + // ************************************ + // * IERC4626 + // ************************************ + fn asset(self: @TState) -> ContractAddress; + fn convert_to_assets(self: @TState, shares: u256) -> u256; + fn convert_to_shares(self: @TState, assets: u256) -> u256; + fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + fn preview_deposit(self: @TState, assets: u256) -> u256; + fn preview_mint(self: @TState, shares: u256) -> u256; + fn preview_redeem(self: @TState, shares: u256) -> u256; + fn preview_withdraw(self: @TState, assets: u256) -> u256; + fn redeem( + ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + fn total_assets(self: @TState) -> u256; + fn withdraw( + ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + fn max_redeem(self: @TState, owner: ContractAddress) -> u256; +} + +#[starknet::interface] +pub trait IERC4626Metadata { + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; +} + +#[starknet::interface] +pub trait IERC4626Camel { + fn totalSupply(self: @TState) -> u256; + fn totalAssets(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn previewDeposit(self: @TState, assets: u256) -> u256; + fn previewMint(self: @TState, shares: u256) -> u256; + fn previewRedeem(self: @TState, shares: u256) -> u256; + fn previewWithdraw(self: @TState, assets: u256) -> u256; + fn convertToAssets(self: @TState, shares: u256) -> u256; + fn convertToShares(self: @TState, assets: u256) -> u256; + fn maxDeposit(self: @TState, receiver: ContractAddress) -> u256; + fn maxMint(self: @TState, receiver: ContractAddress) -> u256; + fn maxWithdraw(self: @TState, owner: ContractAddress) -> u256; + fn maxRedeem(self: @TState, owner: ContractAddress) -> u256; +} + +#[starknet::interface] +pub trait ERC4626ABI { + // ************************************ + // * IERC4626 Metadata + // ************************************ + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; + // ************************************ + // * IERC4626 Snake Case + // ************************************ + fn asset(self: @TState) -> ContractAddress; + fn convert_to_assets(self: @TState, shares: u256) -> u256; + fn convert_to_shares(self: @TState, assets: u256) -> u256; + fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + fn preview_deposit(self: @TState, assets: u256) -> u256; + fn preview_mint(self: @TState, shares: u256) -> u256; + fn preview_redeem(self: @TState, shares: u256) -> u256; + fn preview_withdraw(self: @TState, assets: u256) -> u256; + fn redeem( + ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + fn total_supply(self: @TState) -> u256; + fn total_assets(self: @TState) -> u256; + fn withdraw( + ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + // ************************************ + // * IERC4626 Camel Case + // ************************************ + fn totalSupply(self: @TState) -> u256; + fn totalAssets(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn previewDeposit(self: @TState, assets: u256) -> u256; + fn previewMint(self: @TState, shares: u256) -> u256; + fn previewRedeem(self: @TState, shares: u256) -> u256; + fn previewWithdraw(self: @TState, assets: u256) -> u256; + fn convertToAssets(self: @TState, shares: u256) -> u256; + fn convertToShares(self: @TState, assets: u256) -> u256; + fn maxDeposit(self: @TState, receiver: ContractAddress) -> u256; + fn maxMint(self: @TState, receiver: ContractAddress) -> u256; + fn maxWithdraw(self: @TState, owner: ContractAddress) -> u256; + fn maxRedeem(self: @TState, owner: ContractAddress) -> u256; +} diff --git a/starknet/cairo/crates/token/src/interfaces/ifiat_token.cairo b/starknet/cairo/crates/token/src/interfaces/ifiat_token.cairo new file mode 100644 index 00000000000..48693f6d3ec --- /dev/null +++ b/starknet/cairo/crates/token/src/interfaces/ifiat_token.cairo @@ -0,0 +1,5 @@ +#[starknet::interface] +pub trait IFiatToken { + fn burn(ref self: TState, amount: u256); + fn mint(ref self: TState, to: starknet::ContractAddress, amount: u256) -> bool; +} diff --git a/starknet/cairo/crates/token/src/interfaces/imessage_recipient.cairo b/starknet/cairo/crates/token/src/interfaces/imessage_recipient.cairo new file mode 100644 index 00000000000..b649372a9e0 --- /dev/null +++ b/starknet/cairo/crates/token/src/interfaces/imessage_recipient.cairo @@ -0,0 +1,7 @@ +use alexandria_bytes::Bytes; +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IMessageRecipient { + fn handle(ref self: TState, origin: u32, sender: u256, message: Bytes); +} diff --git a/starknet/cairo/crates/token/src/interfaces/ixerc20.cairo b/starknet/cairo/crates/token/src/interfaces/ixerc20.cairo new file mode 100644 index 00000000000..9067b3a4122 --- /dev/null +++ b/starknet/cairo/crates/token/src/interfaces/ixerc20.cairo @@ -0,0 +1,11 @@ +#[starknet::interface] +pub trait IXERC20 { + fn mint(ref self: TState, user: starknet::ContractAddress, amount: u256); + fn burn(ref self: TState, user: starknet::ContractAddress, amount: u256); + fn set_limits(ref self: TState, bridge: u256, minting_limit: u256, burning_limit: u256); + fn owner(self: @TState) -> u256; + fn burning_current_limit_of(self: @TState, bridge: u256) -> u256; + fn minting_current_limit_of(self: @TState, bridge: u256) -> u256; + fn minting_max_limit_of(self: @TState, minter: u256) -> u256; + fn burning_max_limit_of(self: @TState, bridge: u256) -> u256; +} diff --git a/starknet/cairo/crates/token/src/interfaces/ixerc20_lockbox.cairo b/starknet/cairo/crates/token/src/interfaces/ixerc20_lockbox.cairo new file mode 100644 index 00000000000..1ff9d2a5ac9 --- /dev/null +++ b/starknet/cairo/crates/token/src/interfaces/ixerc20_lockbox.cairo @@ -0,0 +1,10 @@ +#[starknet::interface] +pub trait IXERC20Lockbox { + fn xerc20(self: @TState) -> starknet::ContractAddress; + fn erc20(self: @TState) -> starknet::ContractAddress; + fn deposit(ref self: TState, amount: u256); + fn deposit_to(ref self: TState, user: u256, amount: u256); + fn deposit_native_to(ref self: TState, user: u256); + fn withdraw(ref self: TState, amount: u256); + fn withdraw_to(ref self: TState, user: u256, amount: u256); +} diff --git a/starknet/cairo/crates/token/src/lib.cairo b/starknet/cairo/crates/token/src/lib.cairo new file mode 100644 index 00000000000..291ab19d200 --- /dev/null +++ b/starknet/cairo/crates/token/src/lib.cairo @@ -0,0 +1,37 @@ +pub mod hyp_erc20; +pub mod hyp_erc20_collateral; +pub mod hyp_erc721; +pub mod hyp_erc721_collateral; +pub mod hyp_native; +pub mod extensions { + pub mod fast_hyp_erc20; + pub mod fast_hyp_erc20_collateral; + pub mod hyp_erc20_collateral_vault_deposit; + pub mod hyp_erc20_vault; + pub mod hyp_erc20_vault_collateral; + pub mod hyp_erc721_URI_collateral; + pub mod hyp_erc721_URI_storage; + pub mod hyp_fiat_token; + pub mod hyp_native_scaled; + pub mod hyp_xerc20; + pub mod hyp_xerc20_lockbox; +} +pub mod interfaces { + pub mod ierc4626; + pub mod ifiat_token; + pub mod imessage_recipient; + pub mod ixerc20; + pub mod ixerc20_lockbox; +} +pub mod components { + pub mod erc721_enumerable; + pub mod erc721_uri_storage; + pub mod fast_token_router; + pub mod hyp_erc20_collateral_component; + pub mod hyp_erc20_component; + pub mod hyp_erc721_collateral_component; + pub mod hyp_erc721_component; + pub mod hyp_native_component; + pub mod token_message; + pub mod token_router; +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc20/common.cairo b/starknet/cairo/crates/token/tests/hyp_erc20/common.cairo new file mode 100644 index 00000000000..2ffed90c832 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc20/common.cairo @@ -0,0 +1,422 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::client::gas_router_component::{ + GasRouterComponent::GasRouterConfig, IGasRouterDispatcher, IGasRouterDispatcherTrait +}; +use contracts::client::router_component::{IRouterDispatcher, IRouterDispatcherTrait}; +use contracts::interfaces::{ + IMailboxDispatcher, IMailboxDispatcherTrait, IMessageRecipientDispatcher, + IMessageRecipientDispatcherTrait, IMailboxClientDispatcher, IMailboxClientDispatcherTrait +}; +use mocks::{ + test_post_dispatch_hook::{ + ITestPostDispatchHookDispatcher, ITestPostDispatchHookDispatcherTrait + }, + mock_mailbox::{IMockMailboxDispatcher, IMockMailboxDispatcherTrait}, + test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait}, + test_interchain_gas_payment::{ + ITestInterchainGasPaymentDispatcher, ITestInterchainGasPaymentDispatcherTrait + }, + mock_eth::{MockEthDispatcher, MockEthDispatcherTrait} +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + declare, ContractClassTrait, ContractClass, CheatTarget, EventSpy, EventAssertions, spy_events, + SpyOn, start_prank, stop_prank, EventFetcher, event_name_hash +}; +use starknet::ContractAddress; +use token::components::token_router::{ITokenRouterDispatcher, ITokenRouterDispatcherTrait}; + +pub const E18: u256 = 1_000_000_000_000_000_000; +pub const ORIGIN: u32 = 11; +pub const DESTINATION: u32 = 12; +pub const DECIMALS: u8 = 18; +pub const TOTAL_SUPPLY: u256 = 1_000_000 * E18; +pub const GAS_LIMIT: u256 = 10_000; +pub const TRANSFER_AMT: u256 = 100 * E18; +pub const REQUIRED_VALUE: u256 = 0; +pub const ZERO_SUPPLY: u256 = 0; +// const NAME: ByteArray = "HyperlaneInu"; +// const SYMBOL: ByteArray = "HYP"; +fn IGP() -> ContractAddress { + starknet::contract_address_const::<'IGP'>() +} +pub fn OWNER() -> ContractAddress { + starknet::contract_address_const::<'OWNER'>() +} +pub fn ALICE() -> ContractAddress { + starknet::contract_address_const::<0x1>() +} +pub fn BOB() -> ContractAddress { + starknet::contract_address_const::<0x2>() +} +pub fn CAROL() -> ContractAddress { + starknet::contract_address_const::<0x3>() +} +pub fn DANIEL() -> ContractAddress { + starknet::contract_address_const::<0x4>() +} +fn PROXY_ADMIN() -> ContractAddress { + starknet::contract_address_const::<0x37>() +} + +pub fn NAME() -> ByteArray { + "HyperlaneInu" +} +pub fn SYMBOL() -> ByteArray { + "HYP" +} + +#[starknet::interface] +pub trait IHypERC20Test { + // Collateral + fn transfer_from_sender_hook(ref self: TContractState, amount_or_id: u256) -> Bytes; + fn transfer_to_hook( + ref self: TContractState, recipient: ContractAddress, amount: u256, metadata: Bytes + ) -> bool; + fn get_wrapped_token(self: @TContractState) -> ContractAddress; + // MailboxClient + fn set_hook(ref self: TContractState, _hook: ContractAddress); + fn set_interchain_security_module(ref self: TContractState, _module: ContractAddress); + fn get_hook(self: @TContractState) -> ContractAddress; + fn get_local_domain(self: @TContractState) -> u32; + fn interchain_security_module(self: @TContractState) -> ContractAddress; + // Router + fn enroll_remote_router(ref self: TContractState, domain: u32, router: u256); + fn enroll_remote_routers(ref self: TContractState, domains: Array, addresses: Array); + fn unenroll_remote_router(ref self: TContractState, domain: u32); + fn unenroll_remote_routers(ref self: TContractState, domains: Array); + fn handle(ref self: TContractState, origin: u32, sender: u256, message: Bytes); + fn domains(self: @TContractState) -> Array; + fn routers(self: @TContractState, domain: u32) -> u256; + // GasRouter + fn set_destination_gas( + ref self: TContractState, + gas_configs: Option>, + domain: Option, + gas: Option + ); + fn quote_gas_payment(self: @TContractState, destination_domain: u32) -> u256; + // TokenRouter + fn transfer_remote( + ref self: TContractState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256; + // ERC20 + fn total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; + // HypERC20 + fn decimals(self: @TContractState) -> u8; +} + +#[derive(Copy, Drop)] +pub struct Setup { + pub noop_hook: ITestPostDispatchHookDispatcher, + pub local_mailbox: IMockMailboxDispatcher, + pub remote_mailbox: IMockMailboxDispatcher, + pub primary_token: ITestERC20Dispatcher, + pub implementation: IHypERC20TestDispatcher, + pub remote_token: IHypERC20TestDispatcher, + pub local_token: IHypERC20TestDispatcher, + pub igp: ITestInterchainGasPaymentDispatcher, + pub erc20_token: ITestERC20Dispatcher, + pub eth_token: MockEthDispatcher, + pub mock_mailbox_contract: ContractClass +} + +pub fn setup() -> Setup { + let contract = declare("TestISM").unwrap(); + let (default_ism, _) = contract.deploy(@array![]).unwrap(); + + let contract = declare("TestPostDispatchHook").unwrap(); + let (noop_hook, _) = contract.deploy(@array![]).unwrap(); + let noop_hook = ITestPostDispatchHookDispatcher { contract_address: noop_hook }; + + let contract = declare("Ether").unwrap(); + let mut calldata: Array = array![]; + starknet::get_contract_address().serialize(ref calldata); + let (eth_address, _) = contract.deploy(@calldata).unwrap(); + let eth_token = MockEthDispatcher { contract_address: eth_address }; + eth_token.mint(ALICE(), 10 * E18); + + let mock_mailbox_contract = declare("MockMailbox").unwrap(); + let (local_mailbox, _) = mock_mailbox_contract + .deploy( + @array![ + ORIGIN.into(), + default_ism.into(), + noop_hook.contract_address.into(), + eth_address.into() + ] + ) + .unwrap(); + let local_mailbox = IMockMailboxDispatcher { contract_address: local_mailbox }; + + let (remote_mailbox, _) = mock_mailbox_contract + .deploy( + @array![ + DESTINATION.into(), + default_ism.into(), + noop_hook.contract_address.into(), + eth_address.into() + ] + ) + .unwrap(); + let remote_mailbox = IMockMailboxDispatcher { contract_address: remote_mailbox }; + + local_mailbox.add_remote_mail_box(DESTINATION, remote_mailbox.contract_address); + remote_mailbox.add_remote_mail_box(ORIGIN, local_mailbox.contract_address); + + local_mailbox.set_default_hook(noop_hook.contract_address); + local_mailbox.set_required_hook(noop_hook.contract_address); + remote_mailbox.set_default_hook(noop_hook.contract_address); + remote_mailbox.set_required_hook(noop_hook.contract_address); + + let contract = declare("TestERC20").unwrap(); + let mut calldata: Array = array![]; + TOTAL_SUPPLY.serialize(ref calldata); + DECIMALS.serialize(ref calldata); + let (primary_token, _) = contract.deploy(@calldata).unwrap(); + let primary_token = ITestERC20Dispatcher { contract_address: primary_token }; + + let (erc20_token, _) = contract.deploy(@calldata).unwrap(); + let erc20_token = ITestERC20Dispatcher { contract_address: erc20_token }; + + let hyp_erc20_contract = declare("HypErc20").unwrap(); + let mut calldata: Array = array![]; + DECIMALS.serialize(ref calldata); + remote_mailbox.contract_address.serialize(ref calldata); + TOTAL_SUPPLY.serialize(ref calldata); + NAME().serialize(ref calldata); + SYMBOL().serialize(ref calldata); + noop_hook.contract_address.serialize(ref calldata); + default_ism.serialize(ref calldata); + OWNER().serialize(ref calldata); + let (implementation, _) = hyp_erc20_contract.deploy(@calldata).unwrap(); + let implementation = IHypERC20TestDispatcher { contract_address: implementation }; + + let contract = declare("TestInterchainGasPayment").unwrap(); + let (igp, _) = contract.deploy(@array![]).unwrap(); + let igp = ITestInterchainGasPaymentDispatcher { contract_address: igp }; + + let mut calldata: Array = array![]; + DECIMALS.serialize(ref calldata); + remote_mailbox.contract_address.serialize(ref calldata); + TOTAL_SUPPLY.serialize(ref calldata); + NAME().serialize(ref calldata); + SYMBOL().serialize(ref calldata); + noop_hook.contract_address.serialize(ref calldata); + igp.contract_address.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + let (remote_token, _) = hyp_erc20_contract.deploy(@calldata).unwrap(); + let remote_token = IHypERC20TestDispatcher { contract_address: remote_token }; + + let mut calldata: Array = array![]; + DECIMALS.serialize(ref calldata); + local_mailbox.contract_address.serialize(ref calldata); + TOTAL_SUPPLY.serialize(ref calldata); + NAME().serialize(ref calldata); + SYMBOL().serialize(ref calldata); + noop_hook.contract_address.serialize(ref calldata); + igp.contract_address.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + let (local_token, _) = hyp_erc20_contract.deploy(@calldata).unwrap(); + let local_token = IHypERC20TestDispatcher { contract_address: local_token }; + + let local_token_address: felt252 = local_token.contract_address.into(); + remote_token.enroll_remote_router(ORIGIN, local_token_address.into()); + + local_token.transfer(ALICE(), 1000 * E18); + + Setup { + noop_hook, + local_mailbox, + remote_mailbox, + primary_token, + implementation, + remote_token, + local_token, + igp, + erc20_token, + eth_token, + mock_mailbox_contract + } +} + +pub fn enroll_local_router(setup: @Setup) { + let remote_token_address: felt252 = (*setup).remote_token.contract_address.into(); + (*setup).local_token.enroll_remote_router(DESTINATION, remote_token_address.into()); +} + +pub fn enroll_remote_router(setup: @Setup) { + let local_token_address: felt252 = (*setup).local_token.contract_address.into(); + (*setup).remote_token.enroll_remote_router(ORIGIN, local_token_address.into()); +} + +pub fn connect_routers(setup: @Setup, domains: Span, addresses: Span) { + let n = domains.len(); + + let mut i: usize = 0; + while i < n { + let mut complement_domains: Array = array![]; + let mut complement_routers: Array = array![]; + + let mut k: usize = 0; + while k < n { + if k != i { + complement_domains.append(*domains.at(k)); + complement_routers.append(*addresses.at(k)); + } + k += 1; + }; + let address_felt: felt252 = (*addresses.at(i)).try_into().unwrap(); + let contract_address: ContractAddress = address_felt.try_into().unwrap(); + let router = IRouterDispatcher { contract_address }; + router.enroll_remote_routers(complement_domains, complement_routers); + i += 1; + }; +} + +pub fn expect_remote_balance(setup: @Setup, user: ContractAddress, balance: u256) { + let remote_token = IERC20Dispatcher { + contract_address: (*setup).remote_token.contract_address + }; + assert_eq!(remote_token.balance_of(user), balance); +} + +pub fn process_transfers(setup: @Setup, recipient: ContractAddress, amount: u256) { + start_prank( + CheatTarget::One((*setup).remote_token.contract_address), + (*setup).remote_mailbox.contract_address + ); + let mut message = BytesTrait::new_empty(); + message.append_address(recipient); + message.append_u256(amount); + let address_felt: felt252 = (*setup).local_token.contract_address.into(); + let local_token_address: u256 = address_felt.into(); + (*setup).remote_token.handle(ORIGIN, local_token_address, message); + stop_prank(CheatTarget::One((*setup).remote_token.contract_address)); +} + +pub fn handle_local_transfer(setup: @Setup, transfer_amount: u256) { + start_prank( + CheatTarget::One((*setup).local_token.contract_address), + (*setup).local_mailbox.contract_address + ); + let mut message = BytesTrait::new_empty(); + message.append_address(ALICE()); + message.append_u256(transfer_amount); + + let address_felt: felt252 = (*setup).remote_token.contract_address.into(); + let contract_address: u256 = address_felt.into(); + (*setup).local_token.handle(DESTINATION, contract_address, message); + stop_prank(CheatTarget::One((*setup).local_token.contract_address)); +} + +pub fn mint_and_approve( + setup: @Setup, amount: u256, mint_to: ContractAddress, approve_to: ContractAddress +) { + (*setup).primary_token.mint(mint_to, amount); + (*setup).primary_token.approve(approve_to, amount); +} + +pub fn set_custom_gas_config(setup: @Setup) { + (*setup).local_token.set_hook((*setup).igp.contract_address); + let config = array![GasRouterConfig { domain: DESTINATION, gas: GAS_LIMIT }]; + (*setup).local_token.set_destination_gas(Option::Some(config), Option::None, Option::None); +} + +pub fn perform_remote_transfer(setup: @Setup, msg_value: u256, amount: u256) { + start_prank(CheatTarget::One((*setup).local_token.contract_address), ALICE()); + + let bob_felt: felt252 = BOB().into(); + let bob_address: u256 = bob_felt.into(); + (*setup) + .local_token + .transfer_remote(DESTINATION, bob_address, amount, msg_value, Option::None, Option::None); + + process_transfers(setup, BOB(), amount); + + let remote_token = IERC20Dispatcher { + contract_address: (*setup).remote_token.contract_address + }; + assert_eq!(remote_token.balance_of(BOB()), amount); + + stop_prank(CheatTarget::One((*setup).local_token.contract_address)); +} + +pub fn perform_remote_transfer_and_gas( + setup: @Setup, msg_value: u256, amount: u256, gas_overhead: u256 +) { + perform_remote_transfer(setup, msg_value + gas_overhead, amount); +} + +// NOTE: not implemented because it calls the above fn internally +pub fn perform_remote_transfer_with_emit() {} + +pub fn perform_remote_transfer_and_gas_with_hook( + setup: @Setup, msg_value: u256, amount: u256, hook: ContractAddress, hook_metadata: Bytes +) -> u256 { + start_prank(CheatTarget::One((*setup).local_token.contract_address), ALICE()); + let token_router = ITokenRouterDispatcher { + contract_address: (*setup).local_token.contract_address + }; + let bob_felt: felt252 = BOB().into(); + let bob_address: u256 = bob_felt.into(); + let message_id = token_router + .transfer_remote( + DESTINATION, + bob_address, + amount, + msg_value, + Option::Some(hook_metadata), + Option::Some(hook) + ); + process_transfers(setup, BOB(), amount); + + let remote_token = IERC20Dispatcher { + contract_address: (*setup).remote_token.contract_address + }; + assert_eq!(remote_token.balance_of(BOB()), amount); + stop_prank(CheatTarget::One((*setup).local_token.contract_address)); + message_id +} + +pub fn test_transfer_with_hook_specified(setup: @Setup, fee: u256, metadata: Bytes) { + let contract = declare("TestPostDispatchHook").unwrap(); + let (hook, _) = contract.deploy(@array![]).unwrap(); + let hook = ITestPostDispatchHookDispatcher { contract_address: hook }; + + hook.set_fee(fee); + + start_prank(CheatTarget::One((*setup).primary_token.contract_address), ALICE()); + let primary_token = IERC20Dispatcher { + contract_address: (*setup).primary_token.contract_address + }; + primary_token.approve((*setup).local_token.contract_address, TRANSFER_AMT); + + let message_id = perform_remote_transfer_and_gas_with_hook( + setup, 0, TRANSFER_AMT, hook.contract_address, metadata + ); + + assert!(hook.message_dispatched(message_id) == true, "Hook did not dispatch"); +} + +// NOTE: Not applicable on Starknet +fn test_benchmark_overhead_gas_usage() {} + +#[test] +fn test_hyp_erc20_setup() { + //let _ = setup(); + assert!(true, ""); +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_collateral_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_collateral_test.cairo new file mode 100644 index 00000000000..6239a7ba653 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_collateral_test.cairo @@ -0,0 +1,151 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::client::gas_router_component::{ + GasRouterComponent::GasRouterConfig, IGasRouterDispatcher, IGasRouterDispatcherTrait +}; +use contracts::utils::utils::U256TryIntoContractAddress; +use mocks::{ + test_post_dispatch_hook::{ + ITestPostDispatchHookDispatcher, ITestPostDispatchHookDispatcherTrait + }, + mock_mailbox::{IMockMailboxDispatcher, IMockMailboxDispatcherTrait}, + test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait}, + test_interchain_gas_payment::{ + ITestInterchainGasPaymentDispatcher, ITestInterchainGasPaymentDispatcherTrait + }, + mock_eth::{MockEthDispatcher, MockEthDispatcherTrait} +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + start_prank, stop_prank, declare, ContractClassTrait, CheatTarget, spy_events, SpyOn +}; +use starknet::ContractAddress; +use super::common::{ + setup, Setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, DESTINATION, TRANSFER_AMT, ALICE, BOB, + perform_remote_transfer_with_emit, perform_remote_transfer_and_gas, E18, + IHypERC20TestDispatcher, IHypERC20TestDispatcherTrait, enroll_remote_router, + enroll_local_router, set_custom_gas_config, REQUIRED_VALUE, GAS_LIMIT +}; +use token::hyp_erc20_collateral::HypErc20Collateral; + +fn setup_hyp_erc20_collateral() -> (IHypERC20TestDispatcher, Setup) { + let setup = setup(); + let hyp_erc20_collateral_contract = declare("HypErc20Collateral").unwrap(); + let constructor_args: Array = array![ + setup.local_mailbox.contract_address.into(), + setup.primary_token.contract_address.into(), + ALICE().into(), + setup.noop_hook.contract_address.into(), + setup.primary_token.contract_address.into() // just a placeholder + ]; + + let (collateral_address, _) = hyp_erc20_collateral_contract.deploy(@constructor_args).unwrap(); + let collateral = IHypERC20TestDispatcher { contract_address: collateral_address }; + + // Enroll remote router + let remote_token_address: felt252 = setup.remote_token.contract_address.into(); + start_prank(CheatTarget::One(collateral.contract_address), ALICE()); + collateral.enroll_remote_router(DESTINATION, remote_token_address.into()); + stop_prank(CheatTarget::One(collateral.contract_address)); + + // Transfer tokens to collateral contract and ALICE + setup.primary_token.transfer(collateral.contract_address, 1000 * E18); + setup.primary_token.transfer(ALICE(), 1000 * E18); + let addr: felt252 = collateral.contract_address.into(); + // Enroll remote router for the remote token + setup.remote_token.enroll_remote_router(ORIGIN, addr.into()); + (collateral, setup) +} + +fn perform_remote_transfer_collateral( + setup: @Setup, + collateral: @IHypERC20TestDispatcher, + msg_value: u256, + extra_gas: u256, + amount: u256, + approve: bool +) { + // Approve + if approve { + start_prank(CheatTarget::One(*setup.primary_token.contract_address), ALICE()); + (*setup.primary_token).approve(*collateral.contract_address, amount); + stop_prank(CheatTarget::One(*setup.primary_token.contract_address)); + } + // Remote transfer + start_prank(CheatTarget::One(*collateral.contract_address), ALICE()); + let bob_felt: felt252 = BOB().into(); + let bob_address: u256 = bob_felt.into(); + (*collateral) + .transfer_remote(DESTINATION, bob_address, amount, msg_value, Option::None, Option::None); + + process_transfers_collateral(setup, collateral, BOB(), amount); + + let remote_token = IERC20Dispatcher { + contract_address: (*setup).remote_token.contract_address + }; + assert_eq!(remote_token.balance_of(BOB()), amount); + + stop_prank(CheatTarget::One(*collateral.contract_address)); +} + +fn process_transfers_collateral( + setup: @Setup, collateral: @IHypERC20TestDispatcher, recipient: ContractAddress, amount: u256 +) { + start_prank( + CheatTarget::One((*setup).remote_token.contract_address), + (*setup).remote_mailbox.contract_address + ); + let local_token_address: felt252 = (*collateral).contract_address.into(); + let mut message = BytesTrait::new_empty(); + message.append_address(recipient); + message.append_u256(amount); + (*setup).remote_token.handle(ORIGIN, local_token_address.into(), message); + stop_prank(CheatTarget::One((*setup).remote_token.contract_address)); +} + +#[test] +fn test_remote_transfer() { + let (collateral, setup) = setup_hyp_erc20_collateral(); + let balance_before = collateral.balance_of(ALICE()); + start_prank(CheatTarget::One(collateral.contract_address), ALICE()); + perform_remote_transfer_collateral(@setup, @collateral, REQUIRED_VALUE, 0, TRANSFER_AMT, true); + stop_prank(CheatTarget::One(collateral.contract_address)); + // Check balance after transfer + assert_eq!( + collateral.balance_of(ALICE()), + balance_before - TRANSFER_AMT, + "Incorrect balance after transfer" + ); +} + +#[test] +#[should_panic] +fn test_remote_transfer_invalid_allowance() { + let (collateral, setup) = setup_hyp_erc20_collateral(); + start_prank(CheatTarget::One(collateral.contract_address), ALICE()); + perform_remote_transfer_collateral(@setup, @collateral, REQUIRED_VALUE, 0, TRANSFER_AMT, false); + stop_prank(CheatTarget::One(collateral.contract_address)); +} + +#[test] +fn test_remote_transfer_with_custom_gas_config() { + let (collateral, setup) = setup_hyp_erc20_collateral(); + // Check balance before transfer + let balance_before = collateral.balance_of(ALICE()); + start_prank(CheatTarget::One(collateral.contract_address), ALICE()); + // Set custom gas config + collateral.set_hook(setup.igp.contract_address); + let config = array![GasRouterConfig { domain: DESTINATION, gas: GAS_LIMIT }]; + collateral.set_destination_gas(Option::Some(config), Option::None, Option::None); + // Do a remote transfer + perform_remote_transfer_collateral( + @setup, @collateral, REQUIRED_VALUE, setup.igp.gas_price(), TRANSFER_AMT, true + ); + + stop_prank(CheatTarget::One(collateral.contract_address)); + // Check balance after transfer + assert_eq!( + collateral.balance_of(ALICE()), + balance_before - TRANSFER_AMT, + "Incorrect balance after transfer" + ); +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_lockbox_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_lockbox_test.cairo new file mode 100644 index 00000000000..33182d35fbd --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_lockbox_test.cairo @@ -0,0 +1,216 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::client::gas_router_component::GasRouterComponent::GasRouterConfig; +use mocks::test_interchain_gas_payment::ITestInterchainGasPaymentDispatcherTrait; +use mocks::{ + test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait}, + xerc20_lockbox_test::{IXERC20LockboxTestDispatcher, IXERC20LockboxTestDispatcherTrait}, + xerc20_test::{IXERC20TestDispatcher, IXERC20TestDispatcherTrait} +}; +use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use snforge_std::{ + declare, ContractClassTrait, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn, + start_prank, stop_prank, EventFetcher, event_name_hash +}; +use starknet::ContractAddress; +use super::common::{ + setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, TRANSFER_AMT, perform_remote_transfer_with_emit, ALICE, + BOB, E18, IHypERC20TestDispatcher, IHypERC20TestDispatcherTrait, enroll_local_router, + set_custom_gas_config, REQUIRED_VALUE, GAS_LIMIT, DESTINATION, Setup, ZERO_SUPPLY +}; + + +const MAX_INT: u256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + +#[starknet::interface] +pub trait IHypERC20LockboxTest { + // MailboxClient + fn set_hook(ref self: TContractState, _hook: ContractAddress); + fn set_interchain_security_module(ref self: TContractState, _module: ContractAddress); + fn get_hook(self: @TContractState) -> ContractAddress; + fn get_local_domain(self: @TContractState) -> u32; + fn interchain_security_module(self: @TContractState) -> ContractAddress; + // Router + fn enroll_remote_router(ref self: TContractState, domain: u32, router: u256); + fn enroll_remote_routers(ref self: TContractState, domains: Array, addresses: Array); + fn unenroll_remote_router(ref self: TContractState, domain: u32); + fn unenroll_remote_routers(ref self: TContractState, domains: Array); + fn handle(ref self: TContractState, origin: u32, sender: u256, message: Bytes); + fn domains(self: @TContractState) -> Array; + fn routers(self: @TContractState, domain: u32) -> u256; + // GasRouter + fn set_destination_gas( + ref self: TContractState, + gas_configs: Option>, + domain: Option, + gas: Option + ); + fn quote_gas_payment(self: @TContractState, destination_domain: u32) -> u256; + // TokenRouter + fn transfer_remote( + ref self: TContractState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256; + // XERC20Lockbox + fn xerc20(self: @TContractState) -> ContractAddress; + fn erc20(self: @TContractState) -> ContractAddress; + fn deposit(ref self: TContractState, amount: u256); + fn deposit_to(ref self: TContractState, user: u256, amount: u256); + fn deposit_native_to(ref self: TContractState, user: u256); + fn withdraw(ref self: TContractState, amount: u256); + fn withdraw_to(ref self: TContractState, user: u256, amount: u256); + fn lockbox(self: @TContractState) -> ContractAddress; + fn xERC20(self: @TContractState) -> ContractAddress; + // HypERC20Collateral + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; +} + +fn setup_lockbox() -> (Setup, IHypERC20LockboxTestDispatcher) { + let mut setup = setup(); + + let mut calldata: Array = array![]; + ZERO_SUPPLY.serialize(ref calldata); + DECIMALS.serialize(ref calldata); + + let contract = declare("XERC20Test").unwrap(); + let (xerc20, _) = contract.deploy(@calldata).unwrap(); + let xerc20 = IXERC20TestDispatcher { contract_address: xerc20 }; + + let contract = declare("XERC20LockboxTest").unwrap(); + + let mut calldata: Array = array![]; + xerc20.contract_address.serialize(ref calldata); + setup.erc20_token.contract_address.serialize(ref calldata); + + let (lockbox, _) = contract.deploy(@calldata).unwrap(); + let lockbox = IXERC20LockboxTestDispatcher { contract_address: lockbox }; + let contract = declare("HypXERC20Lockbox").unwrap(); + + let mut calldata: Array = array![]; + setup.local_mailbox.contract_address.serialize(ref calldata); + lockbox.contract_address.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + setup.igp.contract_address.serialize(ref calldata); + + let (xerc20lockbox, _) = contract.deploy(@calldata).unwrap(); + let xerc20lockbox = IHypERC20LockboxTestDispatcher { contract_address: xerc20lockbox }; + + let remote_token_address: felt252 = setup.remote_token.contract_address.into(); + xerc20lockbox.enroll_remote_router(DESTINATION, remote_token_address.into()); + + setup.primary_token = setup.erc20_token; + setup.primary_token.transfer(ALICE(), 1000 * E18); + enroll_remote_router(@setup, xerc20lockbox); + (setup, xerc20lockbox) +} + +#[test] +fn test_erc20_lockbox_approval() { + let (_, xerc20lockbox) = setup_lockbox(); + + let xerc20 = xerc20lockbox.xERC20(); + let dispatcher = ERC20ABIDispatcher { contract_address: xerc20 }; + assert_eq!( + dispatcher.allowance(xerc20lockbox.contract_address, xerc20lockbox.lockbox()), MAX_INT + ); +} + +#[test] +fn test_erc20_lockbox_transfer() { + let (setup, xerc20lockbox) = setup_lockbox(); + + let balance_before = xerc20lockbox.balance_of(ALICE()); + + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + setup.primary_token.approve(xerc20lockbox.contract_address, TRANSFER_AMT); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + perform_remote_transfer_and_gas(@setup, xerc20lockbox, REQUIRED_VALUE, TRANSFER_AMT, 0); + + assert_eq!(xerc20lockbox.balance_of(ALICE()), balance_before - TRANSFER_AMT); +} + +#[test] +fn test_erc20_lockbox_handle() { + let (setup, local_token) = setup_lockbox(); + + let balance_before = local_token.balance_of(ALICE()); + + handle_local_transfer(@setup, local_token, TRANSFER_AMT); + + assert_eq!(local_token.balance_of(ALICE()), balance_before + TRANSFER_AMT); +} + +pub fn handle_local_transfer( + setup: @Setup, local_token: IHypERC20LockboxTestDispatcher, transfer_amount: u256 +) { + start_prank( + CheatTarget::One(local_token.contract_address), (*setup).local_mailbox.contract_address + ); + let mut message = BytesTrait::new_empty(); + message.append_address(ALICE()); + message.append_u256(transfer_amount); + + let address_felt: felt252 = (*setup).remote_token.contract_address.into(); + let contract_address: u256 = address_felt.into(); + local_token.handle(DESTINATION, contract_address, message); + stop_prank(CheatTarget::One(local_token.contract_address)); +} + +pub fn enroll_remote_router(setup: @Setup, lockbox: IHypERC20LockboxTestDispatcher) { + let local_token_address: felt252 = lockbox.contract_address.into(); + (*setup).remote_token.enroll_remote_router(ORIGIN, local_token_address.into()); +} + + +pub fn perform_remote_transfer( + setup: @Setup, local_token: IHypERC20LockboxTestDispatcher, msg_value: u256, amount: u256 +) { + start_prank(CheatTarget::One(local_token.contract_address), ALICE()); + + let bob_felt: felt252 = BOB().into(); + let bob_address: u256 = bob_felt.into(); + local_token + .transfer_remote(DESTINATION, bob_address, amount, msg_value, Option::None, Option::None); + process_transfers(setup, local_token, BOB(), amount); + + let remote_token = ERC20ABIDispatcher { + contract_address: (*setup).remote_token.contract_address + }; + assert_eq!(remote_token.balance_of(BOB()), amount); + + stop_prank(CheatTarget::One(local_token.contract_address)); +} + +pub fn perform_remote_transfer_and_gas( + setup: @Setup, + local_token: IHypERC20LockboxTestDispatcher, + msg_value: u256, + amount: u256, + gas_overhead: u256 +) { + perform_remote_transfer(setup, local_token, msg_value + gas_overhead, amount); +} + +pub fn process_transfers( + setup: @Setup, + local_token: IHypERC20LockboxTestDispatcher, + recipient: ContractAddress, + amount: u256 +) { + start_prank( + CheatTarget::One((*setup).remote_token.contract_address), + (*setup).remote_mailbox.contract_address + ); + let mut message = BytesTrait::new_empty(); + message.append_address(recipient); + message.append_u256(amount); + let address_felt: felt252 = local_token.contract_address.into(); + let local_token_address: u256 = address_felt.into(); + (*setup).remote_token.handle(ORIGIN, local_token_address, message); + stop_prank(CheatTarget::One((*setup).remote_token.contract_address)); +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_test.cairo new file mode 100644 index 00000000000..f6f7626dad4 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_erc20_test.cairo @@ -0,0 +1,89 @@ +use mocks::test_interchain_gas_payment::ITestInterchainGasPaymentDispatcherTrait; +use snforge_std::{ + declare, ContractClassTrait, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn, + start_prank, stop_prank, EventFetcher, event_name_hash +}; +use super::common::{ + setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, TRANSFER_AMT, perform_remote_transfer_with_emit, + perform_remote_transfer_and_gas, ALICE, BOB, E18, IHypERC20TestDispatcher, + IHypERC20TestDispatcherTrait, enroll_remote_router, enroll_local_router, + perform_remote_transfer, set_custom_gas_config, REQUIRED_VALUE, GAS_LIMIT +}; + +#[test] +fn test_erc20_total_supply() { + let setup = setup(); + let erc20_token = setup.local_token; + let total_supply = erc20_token.total_supply(); + assert_eq!(total_supply, TOTAL_SUPPLY); +} + +#[test] +fn test_erc20_decimals() { + let setup = setup(); + let erc20_token = setup.local_token; + let decimals = erc20_token.decimals(); + assert_eq!(decimals, DECIMALS); +} + +#[test] +fn test_erc20_local_transfer() { + let setup = setup(); + let erc20_token = setup.local_token; + let alice = erc20_token.balance_of(ALICE()); + let bob = erc20_token.balance_of(BOB()); + assert_eq!(alice, 1000 * E18); + assert_eq!(bob, 0); + + start_prank(CheatTarget::One((setup).local_token.contract_address), ALICE()); + erc20_token.transfer(BOB(), 100 * E18); + stop_prank(CheatTarget::One(erc20_token.contract_address)); + + let alice = erc20_token.balance_of(ALICE()); + let bob = erc20_token.balance_of(BOB()); + assert_eq!(alice, 900 * E18); + assert_eq!(bob, 100 * E18); +} + +#[test] +fn test_erc20_remote_transfer() { + let setup = setup(); + let erc20_token = setup.local_token; + enroll_remote_router(@setup); + enroll_local_router(@setup); + + let local_token_address: felt252 = setup.local_token.contract_address.into(); + setup.remote_token.enroll_remote_router(ORIGIN, local_token_address.into()); + + let balance_before = erc20_token.balance_of(ALICE()); + perform_remote_transfer_and_gas(@setup, REQUIRED_VALUE, TRANSFER_AMT, 0); + let balance_after = erc20_token.balance_of(ALICE()); + assert_eq!(balance_after, balance_before - TRANSFER_AMT); +} + +#[test] +#[should_panic] +fn test_erc20_remote_transfer_invalid_amount() { + let setup = setup(); + + perform_remote_transfer(@setup, REQUIRED_VALUE, TRANSFER_AMT * 11); +} + +#[test] +fn test_erc20_remote_transfer_with_custom_gas_config() { + let setup = setup(); + let erc20_token = setup.local_token; + enroll_remote_router(@setup); + enroll_local_router(@setup); + + let local_token_address: felt252 = setup.local_token.contract_address.into(); + setup.remote_token.enroll_remote_router(ORIGIN, local_token_address.into()); + + set_custom_gas_config(@setup); + + let gas_price = setup.igp.gas_price(); + let balance_before = erc20_token.balance_of(ALICE()); + perform_remote_transfer_and_gas(@setup, REQUIRED_VALUE, TRANSFER_AMT, GAS_LIMIT * gas_price); + let balance_after = erc20_token.balance_of(ALICE()); + assert_eq!(balance_after, balance_before - TRANSFER_AMT); +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc20/hyp_fiat_token_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_fiat_token_test.cairo new file mode 100644 index 00000000000..ce3ea0e7c56 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_fiat_token_test.cairo @@ -0,0 +1,62 @@ +use mocks::test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait}; +use mocks::test_interchain_gas_payment::ITestInterchainGasPaymentDispatcherTrait; +use snforge_std::{ + declare, ContractClassTrait, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn, + start_prank, stop_prank, EventFetcher, event_name_hash +}; +use starknet::ContractAddress; +use super::common::{ + setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, TRANSFER_AMT, DESTINATION, OWNER, + perform_remote_transfer_with_emit, perform_remote_transfer_and_gas, ALICE, BOB, E18, + IHypERC20TestDispatcher, IHypERC20TestDispatcherTrait, enroll_remote_router, + enroll_local_router, perform_remote_transfer, set_custom_gas_config, REQUIRED_VALUE, GAS_LIMIT, + Setup, handle_local_transfer +}; + +fn fiat_token_setup() -> Setup { + let mut setup = setup(); + + let local_token = declare("HypFiatToken").unwrap(); + + let mut calldata: Array = array![]; + setup.primary_token.contract_address.serialize(ref calldata); + setup.local_mailbox.contract_address.serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + setup.igp.contract_address.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + + let (fiat_token, _) = local_token.deploy(@calldata).unwrap(); + let fiat_token = IHypERC20TestDispatcher { contract_address: fiat_token }; + + let remote_token_address: felt252 = setup.remote_token.contract_address.into(); + fiat_token.enroll_remote_router(DESTINATION, remote_token_address.into()); + + setup.primary_token.transfer(setup.local_token.contract_address, 1000 * E18); + setup.primary_token.transfer(ALICE(), 1000 * E18); + + setup.local_token = fiat_token; + enroll_remote_router(@setup); + + setup +} + +#[test] +fn test_fiat_token_remote_transfer() { + let setup = fiat_token_setup(); + + let balance_before = setup.local_token.balance_of(ALICE()); + + start_prank(CheatTarget::One((setup).primary_token.contract_address), ALICE()); + setup.primary_token.approve(setup.local_token.contract_address, TRANSFER_AMT); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + + perform_remote_transfer_and_gas(@setup, REQUIRED_VALUE, TRANSFER_AMT, 0); + let balance_after = setup.local_token.balance_of(ALICE()); + assert_eq!(balance_after, balance_before - TRANSFER_AMT); +} + +#[test] +fn test_fiat_token_handle() { + let setup = fiat_token_setup(); + handle_local_transfer(@setup, TRANSFER_AMT); +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc20/hyp_native_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_native_test.cairo new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_native_test.cairo @@ -0,0 +1 @@ + diff --git a/starknet/cairo/crates/token/tests/hyp_erc20/hyp_xerc20_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_xerc20_test.cairo new file mode 100644 index 00000000000..5f4fcabe398 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc20/hyp_xerc20_test.cairo @@ -0,0 +1,83 @@ +use mocks::xerc20_test::{XERC20Test, IXERC20TestDispatcher, IXERC20TestDispatcherTrait}; +use mocks::{test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait},}; +use snforge_std::{declare, ContractClassTrait, CheatTarget, start_prank, stop_prank,}; +use starknet::ContractAddress; +use super::common::{ + Setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, TRANSFER_AMT, ALICE, BOB, E18, REQUIRED_VALUE, + DESTINATION, IHypERC20TestDispatcher, IHypERC20TestDispatcherTrait, setup, + perform_remote_transfer_and_gas, enroll_remote_router, enroll_local_router, + perform_remote_transfer, handle_local_transfer +}; + +fn setup_xerc20() -> Setup { + let mut setup = setup(); + let default_ism = setup.implementation.interchain_security_module(); + + let contract = declare("XERC20Test").unwrap(); + let mut calldata: Array = array![]; + TOTAL_SUPPLY.serialize(ref calldata); + DECIMALS.serialize(ref calldata); + let (xerc20, _) = contract.deploy(@calldata).unwrap(); + setup.primary_token = ITestERC20Dispatcher { contract_address: xerc20 }; + + let contract = declare("HypXERC20").unwrap(); + let (local_token, _) = contract + .deploy( + @array![ + setup.local_mailbox.contract_address.into(), + xerc20.into(), + starknet::get_contract_address().into(), + setup.noop_hook.contract_address.into(), + default_ism.into() + ] + ) + .unwrap(); + setup.local_token = IHypERC20TestDispatcher { contract_address: local_token }; + + setup + .local_token + .enroll_remote_router( + DESTINATION, + Into::::into(setup.remote_token.contract_address).into() + ); + setup.primary_token.transfer(local_token, 1000 * E18); + setup.primary_token.transfer(ALICE(), 1000 * E18); + setup + .remote_token + .enroll_remote_router( + ORIGIN, + Into::::into(setup.local_token.contract_address).into() + ); + setup +} + +#[test] +fn test_remote_transfer() { + let mut setup = setup_xerc20(); + let xerc20 = setup.local_token; + start_prank(CheatTarget::One((setup).primary_token.contract_address), ALICE()); + setup.primary_token.approve(xerc20.contract_address, TRANSFER_AMT); + stop_prank(CheatTarget::One((setup).primary_token.contract_address)); + + let balance_before = xerc20.balance_of(ALICE()); + let total_supply_before = setup.primary_token.total_supply(); + perform_remote_transfer(@setup, REQUIRED_VALUE, TRANSFER_AMT); + let balance_after = xerc20.balance_of(ALICE()); + let total_supply_after = setup.primary_token.total_supply(); + assert_eq!(total_supply_after, total_supply_before - TRANSFER_AMT); + assert_eq!(balance_after, balance_before - TRANSFER_AMT); +} + +#[test] +fn test_handle() { + let mut setup = setup_xerc20(); + let xerc20 = setup.local_token; + let balance_before = xerc20.balance_of(ALICE()); + let total_supply_before = setup.primary_token.total_supply(); + handle_local_transfer(@setup, TRANSFER_AMT); + let balance_after = xerc20.balance_of(ALICE()); + let total_supply_after = setup.primary_token.total_supply(); + + assert_eq!(total_supply_after, total_supply_before + TRANSFER_AMT); + assert_eq!(balance_after, balance_before + TRANSFER_AMT); +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc721/common.cairo b/starknet/cairo/crates/token/tests/hyp_erc721/common.cairo new file mode 100644 index 00000000000..284497b93bd --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc721/common.cairo @@ -0,0 +1,303 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use contracts::client::gas_router_component::{ + GasRouterComponent::GasRouterConfig, IGasRouterDispatcher, IGasRouterDispatcherTrait +}; +use contracts::client::router_component::{IRouterDispatcher, IRouterDispatcherTrait}; +use contracts::interfaces::{ + IMailboxDispatcher, IMailboxDispatcherTrait, IMessageRecipientDispatcher, + IMessageRecipientDispatcherTrait, IMailboxClientDispatcher, IMailboxClientDispatcherTrait +}; +use mocks::{ + test_post_dispatch_hook::{ + ITestPostDispatchHookDispatcher, ITestPostDispatchHookDispatcherTrait + }, + mock_mailbox::{IMockMailboxDispatcher, IMockMailboxDispatcherTrait}, + // test_erc721::{ITestERC721Dispatcher, ITestERC721DispatcherTrait}, + test_interchain_gas_payment::{ + ITestInterchainGasPaymentDispatcher, ITestInterchainGasPaymentDispatcherTrait + }, + test_erc721::{ITestERC721Dispatcher, ITestERC721DispatcherTrait} +}; +use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; +use snforge_std::cheatcodes::contract_class::ContractClass; +use snforge_std::{ + declare, ContractClassTrait, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn, + start_prank, stop_prank, EventFetcher, event_name_hash +}; +use starknet::ContractAddress; +use token::components::hyp_erc721_collateral_component::{ + IHypErc721CollateralDispatcher, IHypErc721CollateralDispatcherTrait +}; +use token::components::hyp_erc721_component::{IHypErc721Dispatcher, IHypErc721DispatcherTrait}; +use token::components::token_router::{ITokenRouterDispatcher, ITokenRouterDispatcherTrait}; + +const PUB_KEY: felt252 = 0x1; +const ZERO_SUPPLY: u256 = 0; +pub fn ZERO_ADDRESS() -> ContractAddress { + starknet::contract_address_const::<'0x0'>() +} +fn EMPTY_STRING() -> ByteArray { + "" +} +pub fn NAME() -> ByteArray { + "Hyperlane Hedgehogs" +} +pub fn SYMBOL() -> ByteArray { + "HHH" +} +pub fn ALICE() -> ContractAddress { + starknet::contract_address_const::<'0x1'>() +} +pub fn BOB() -> ContractAddress { + starknet::contract_address_const::<'0x2'>() +} +fn PROXY_ADMIN() -> ContractAddress { + starknet::contract_address_const::<'0x37'>() +} +pub const INITIAL_SUPPLY: u256 = 10; +pub const ORIGIN: u32 = 11; +pub const DESTINATION: u32 = 22; +pub const TRANSFER_ID: u256 = 0; +pub fn URI() -> ByteArray { + "http://bit.ly/3reJLpx" +} + +#[starknet::interface] +pub trait IHypErc721Test { + // MailboxClient + fn set_hook(ref self: TContractState, _hook: ContractAddress); + fn set_interchain_security_module(ref self: TContractState, _module: ContractAddress); + fn get_hook(self: @TContractState) -> ContractAddress; + fn get_local_domain(self: @TContractState) -> u32; + fn interchain_security_module(self: @TContractState) -> ContractAddress; + // Router + fn enroll_remote_router(ref self: TContractState, domain: u32, router: u256); + fn enroll_remote_routers(ref self: TContractState, domains: Array, addresses: Array); + fn unenroll_remote_router(ref self: TContractState, domain: u32); + fn unenroll_remote_routers(ref self: TContractState, domains: Array); + fn handle(ref self: TContractState, origin: u32, sender: u256, message: Bytes); + fn domains(self: @TContractState) -> Array; + fn routers(self: @TContractState, domain: u32) -> u256; + // GasRouter + fn set_destination_gas( + ref self: TContractState, + gas_configs: Option>, + domain: Option, + gas: Option + ); + fn quote_gas_payment(self: @TContractState, destination_domain: u32) -> u256; + // TokenRouter + fn transfer_remote( + ref self: TContractState, + destination: u32, + recipient: u256, + amount_or_id: u256, + value: u256, + hook_metadata: Option, + hook: Option + ) -> u256; + // ERC721 + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; + fn safe_transfer_from( + ref self: TContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ); + fn transfer_from( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ); + fn approve(ref self: TContractState, to: ContractAddress, token_id: u256); + fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool); + fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress; + fn is_approved_for_all( + self: @TContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool; + // HypERC721Collateral + fn get_wrapped_token(self: @TContractState) -> ContractAddress; + // HypERC721 + fn initialize(ref self: TContractState, mint_amount: u256, name: ByteArray, symbol: ByteArray); + // HypERC721URIStorage + fn name(self: @TContractState) -> ByteArray; + fn symbol(self: @TContractState) -> ByteArray; + fn token_uri(self: @TContractState, token_id: u256) -> ByteArray; + fn set_token_uri(ref self: TContractState, token_id: u256, uri: ByteArray); +} + +#[derive(Copy, Drop)] +pub struct Setup { + pub local_primary_token: ITestERC721Dispatcher, + pub remote_primary_token: ITestERC721Dispatcher, + pub noop_hook: ITestPostDispatchHookDispatcher, + pub default_ism: ContractAddress, + pub local_mailbox: IMockMailboxDispatcher, + pub remote_mailbox: IMockMailboxDispatcher, + pub remote_token: IHypErc721TestDispatcher, + pub local_token: IHypErc721TestDispatcher, + pub hyp_erc721_contract: ContractClass, + pub hyp_erc721_collateral_contract: ContractClass, + pub alice: ContractAddress, + pub bob: ContractAddress, +} + +pub fn setup() -> Setup { + let contract = declare("TestERC721").unwrap(); + let mut calldata: Array = array![]; + (INITIAL_SUPPLY * 2).serialize(ref calldata); + let (primary_token, _) = contract.deploy(@calldata).unwrap(); + let local_primary_token = ITestERC721Dispatcher { contract_address: primary_token }; + + let (remote_primary_token, _) = contract.deploy(@calldata).unwrap(); + let remote_primary_token = ITestERC721Dispatcher { contract_address: remote_primary_token }; + + let contract = declare("TestPostDispatchHook").unwrap(); + let (noop_hook, _) = contract.deploy(@array![]).unwrap(); + let noop_hook = ITestPostDispatchHookDispatcher { contract_address: noop_hook }; + + let contract = declare("TestISM").unwrap(); + let (default_ism, _) = contract.deploy(@array![]).unwrap(); + + let contract = declare("Ether").unwrap(); + let mut calldata: Array = array![]; + starknet::get_contract_address().serialize(ref calldata); + let (eth_address, _) = contract.deploy(@calldata).unwrap(); + //let eth = MockEthDispatcher { contract_address: eth_address }; + + let contract = declare("MockMailbox").unwrap(); + let (local_mailbox, _) = contract + .deploy( + @array![ + ORIGIN.into(), + default_ism.into(), + noop_hook.contract_address.into(), + eth_address.into() + ] + ) + .unwrap(); + let local_mailbox = IMockMailboxDispatcher { contract_address: local_mailbox }; + + let (remote_mailbox, _) = contract + .deploy( + @array![ + DESTINATION.into(), + default_ism.into(), + noop_hook.contract_address.into(), + eth_address.into() + ] + ) + .unwrap(); + let remote_mailbox = IMockMailboxDispatcher { contract_address: remote_mailbox }; + + local_mailbox.set_default_hook(noop_hook.contract_address); + local_mailbox.set_required_hook(noop_hook.contract_address); + + let hyp_erc721_collateral_contract = declare("HypErc721Collateral").unwrap(); + let (remote_token, _) = hyp_erc721_collateral_contract + .deploy( + @array![ + remote_primary_token.contract_address.into(), + remote_mailbox.contract_address.into(), + noop_hook.contract_address.into(), + default_ism.into(), + starknet::get_contract_address().into() + ] + ) + .unwrap(); + let remote_token = IHypErc721TestDispatcher { contract_address: remote_token }; + + let hyp_erc721_contract = declare("HypErc721").unwrap(); + let mut calldata: Array = array![]; + local_mailbox.contract_address.serialize(ref calldata); + EMPTY_STRING().serialize(ref calldata); + EMPTY_STRING().serialize(ref calldata); + INITIAL_SUPPLY.serialize(ref calldata); + calldata.append(noop_hook.contract_address.into()); + calldata.append(default_ism.into()); + calldata.append(starknet::get_contract_address().into()); + let (local_token, _) = hyp_erc721_contract.deploy(@calldata).unwrap(); + let local_token = IHypErc721TestDispatcher { contract_address: local_token }; + + let contract = declare("MockAccount").unwrap(); + let (alice, _) = contract.deploy(@array![PUB_KEY]).unwrap(); + let (bob, _) = contract.deploy(@array![PUB_KEY]).unwrap(); + + local_mailbox.add_remote_mail_box(DESTINATION, remote_mailbox.contract_address); + remote_mailbox.add_remote_mail_box(ORIGIN, local_mailbox.contract_address); + + Setup { + local_primary_token, + remote_primary_token, + noop_hook, + default_ism, + local_mailbox, + remote_mailbox, + remote_token, + local_token, + hyp_erc721_contract, + hyp_erc721_collateral_contract, + alice, + bob, + } +} + +pub fn deploy_remote_token(mut setup: Setup, is_collateral: bool) -> Setup { + if is_collateral { + let mut calldata: Array = array![]; + setup.remote_primary_token.contract_address.serialize(ref calldata); + setup.remote_mailbox.contract_address.serialize(ref calldata); + ZERO_ADDRESS().serialize(ref calldata); + ZERO_ADDRESS().serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + let (remote_token, _) = setup.hyp_erc721_collateral_contract.deploy(@calldata).unwrap(); + setup.remote_token = IHypErc721TestDispatcher { contract_address: remote_token }; + setup + .remote_primary_token + .transfer_from( + starknet::get_contract_address(), setup.remote_token.contract_address, 0 + ); + } else { + let mut calldata: Array = array![]; + setup.remote_mailbox.contract_address.serialize(ref calldata); + NAME().serialize(ref calldata); + SYMBOL().serialize(ref calldata); + ZERO_SUPPLY.serialize(ref calldata); + ZERO_ADDRESS().serialize(ref calldata); + ZERO_ADDRESS().serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + let (remote_token, _) = setup.hyp_erc721_contract.deploy(@calldata).unwrap(); + setup.remote_token = IHypErc721TestDispatcher { contract_address: remote_token }; + } + let local_token_address: felt252 = setup.local_token.contract_address.into(); + setup.remote_token.enroll_remote_router(ORIGIN, local_token_address.into()); + setup +} + +pub fn process_transfer(setup: @Setup, recipient: ContractAddress, token_id: u256) { + start_prank( + CheatTarget::One((*setup).remote_token.contract_address), + (*setup).remote_mailbox.contract_address + ); + let mut message = BytesTrait::new_empty(); + message.append_address(recipient); + message.append_u256(token_id); + let local_token_address: felt252 = (*setup).local_token.contract_address.into(); + (*setup).remote_token.handle(ORIGIN, local_token_address.into(), message); +} + +pub fn perform_remote_transfer(setup: @Setup, msg_value: u256, token_id: u256) { + let alice_address: felt252 = (*setup).alice.into(); + (*setup) + .local_token + .transfer_remote( + DESTINATION, alice_address.into(), token_id, msg_value, Option::None, Option::None + ); + process_transfer(setup, (*setup).bob, token_id); + assert_eq!((*setup).remote_token.balance_of((*setup).bob), 1); +} + +#[test] +fn test_erc721_setup() { + let _ = setup(); +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_collateral_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_collateral_test.cairo new file mode 100644 index 00000000000..18ff8617ee4 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_collateral_test.cairo @@ -0,0 +1,72 @@ +use alexandria_bytes::Bytes; +use contracts::client::router_component::{IRouterDispatcher, IRouterDispatcherTrait}; +use mocks::test_erc721::{ITestERC721Dispatcher, ITestERC721DispatcherTrait}; +use snforge_std::cheatcodes::contract_class::{ContractClass, ContractClassTrait}; +use starknet::ContractAddress; +use super::common::{ + setup, DESTINATION, INITIAL_SUPPLY, Setup, IHypErc721TestDispatcher, + IHypErc721TestDispatcherTrait, ALICE, BOB, deploy_remote_token, perform_remote_transfer, + ZERO_ADDRESS +}; +use token::components::token_router::{ITokenRouterDispatcher, ITokenRouterDispatcherTrait}; + +fn setup_erc721_collateral() -> Setup { + let mut setup = setup(); + + let mut calldata: Array = array![]; + setup.local_primary_token.contract_address.serialize(ref calldata); + setup.local_mailbox.contract_address.serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + setup.default_ism.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + + let (local_token, _) = setup.hyp_erc721_collateral_contract.deploy(@calldata).unwrap(); + let local_token = IHypErc721TestDispatcher { contract_address: local_token }; + + setup.local_token = local_token; + + let remote_token_address: felt252 = setup.remote_token.contract_address.into(); + setup.local_token.enroll_remote_router(DESTINATION, remote_token_address.into()); + + setup + .local_primary_token + .transfer_from( + starknet::get_contract_address(), setup.local_token.contract_address, INITIAL_SUPPLY + 1 + ); + + setup +} + +#[test] +fn test_erc721_collateral_remote_transfer() { + let mut setup = setup_erc721_collateral(); + + let setup = deploy_remote_token(setup, false); + setup.local_primary_token.approve(setup.local_token.contract_address, 0); + perform_remote_transfer(@setup, 2500, 0); + + assert_eq!( + setup.local_token.balance_of(starknet::get_contract_address()), INITIAL_SUPPLY * 2 - 2 + ); +} + +#[test] +#[should_panic] +fn test_erc721_collateral_remote_transfer_revert_unowned() { + let mut setup = setup_erc721_collateral(); + + setup.local_primary_token.transfer_from(starknet::get_contract_address(), setup.bob, 1); + + let setup = deploy_remote_token(setup, false); + perform_remote_transfer(@setup, 2500, 1); +} + +#[test] +#[should_panic] +fn test_erc721_collateral_remote_transfer_revert_invalid_token_id() { + let mut setup = setup_erc721_collateral(); + + let setup = deploy_remote_token(setup, false); + perform_remote_transfer(@setup, 2500, INITIAL_SUPPLY * 2); +} + diff --git a/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_collateral_uri_storage_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_collateral_uri_storage_test.cairo new file mode 100644 index 00000000000..93663694b43 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_collateral_uri_storage_test.cairo @@ -0,0 +1,65 @@ +use alexandria_bytes::Bytes; +use contracts::client::router_component::{IRouterDispatcher, IRouterDispatcherTrait}; +use mocks::test_erc721::{ITestERC721Dispatcher, ITestERC721DispatcherTrait}; +use snforge_std::cheatcodes::contract_class::{ContractClass, ContractClassTrait}; +use snforge_std::{ + declare, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn, start_prank, stop_prank, + EventFetcher, event_name_hash +}; +use starknet::ContractAddress; +use super::common::{ + setup, DESTINATION, INITIAL_SUPPLY, Setup, IHypErc721TestDispatcher, + IHypErc721TestDispatcherTrait, ALICE, BOB, deploy_remote_token, perform_remote_transfer, + ZERO_ADDRESS, NAME, SYMBOL, URI, TRANSFER_ID, process_transfer +}; +use token::components::token_router::{ITokenRouterDispatcher, ITokenRouterDispatcherTrait}; + +fn setup_erc721_collateral_uri_storage() -> Setup { + let mut setup = setup(); + + let contract = declare("HypERC721URICollateral").unwrap(); + let mut calldata: Array = array![]; + setup.local_primary_token.contract_address.serialize(ref calldata); + setup.local_mailbox.contract_address.serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + let (hyp_erc721_uri_collateral, _) = contract.deploy(@calldata).unwrap(); + let hyp_erc721_uri_collateral = IHypErc721TestDispatcher { + contract_address: hyp_erc721_uri_collateral + }; + + let remote_token_address: felt252 = setup.remote_token.contract_address.into(); + hyp_erc721_uri_collateral.enroll_remote_router(DESTINATION, remote_token_address.into()); + + setup + .local_primary_token + .transfer_from( + starknet::get_contract_address(), + hyp_erc721_uri_collateral.contract_address, + INITIAL_SUPPLY + 1 + ); + + setup.local_token = hyp_erc721_uri_collateral; + + setup +} + +#[test] +fn test_erc721_collateral_uri_storage_remote_transfer_revert_burned() { + let setup = setup_erc721_collateral_uri_storage(); + + let setup = deploy_remote_token(setup, false); + setup.local_primary_token.approve(setup.local_token.contract_address, 0); + let bob_address: felt252 = setup.bob.into(); + setup + .local_token + .transfer_remote( + DESTINATION, bob_address.into(), TRANSFER_ID, 2500, Option::None, Option::None + ); + process_transfer(@setup, setup.bob, 0); + assert_eq!(setup.remote_token.balance_of(setup.bob), 1); + assert_eq!( + setup.local_token.balance_of(starknet::get_contract_address()), INITIAL_SUPPLY * 2 - 2 + ); +} + diff --git a/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_test.cairo new file mode 100644 index 00000000000..7ca9dbc2412 --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_test.cairo @@ -0,0 +1,87 @@ +use alexandria_bytes::Bytes; +use contracts::client::router_component::{IRouterDispatcher, IRouterDispatcherTrait}; +use starknet::ContractAddress; +use super::common::{ + setup, DESTINATION, INITIAL_SUPPLY, Setup, IHypErc721TestDispatcher, + IHypErc721TestDispatcherTrait, ALICE, BOB, deploy_remote_token, perform_remote_transfer +}; +use token::components::token_router::{ITokenRouterDispatcher, ITokenRouterDispatcherTrait}; + +fn hyp_erc721_setup() -> Setup { + let mut setup = setup(); + + let remote_token_address: felt252 = setup.remote_token.contract_address.into(); + setup.local_token.enroll_remote_router(DESTINATION, remote_token_address.into()); + + setup +} + +#[test] +fn test_erc721_total_supply() { + let setup = hyp_erc721_setup(); + + let balance = setup.local_token.balance_of(starknet::get_contract_address()); + assert_eq!(balance, INITIAL_SUPPLY); +} + +#[test] +fn test_erc721_owner_of() { + let setup = hyp_erc721_setup(); + + let owner = setup.local_token.owner_of(0); + assert_eq!(owner, starknet::get_contract_address()); +} + +#[test] +fn test_erc721_local_transfer() { + let setup = hyp_erc721_setup(); + + let this_address = starknet::get_contract_address(); + setup.local_token.transfer_from(this_address, ALICE(), 0); + assert_eq!(setup.local_token.balance_of(this_address), INITIAL_SUPPLY - 1); + assert_eq!(setup.local_token.balance_of(ALICE()), 1); +} + +#[test] +#[should_panic] +fn test_erc721_local_transfer_invalid_token_id() { + let setup = hyp_erc721_setup(); + + let this_address = starknet::get_contract_address(); + setup.local_token.transfer_from(this_address, ALICE(), INITIAL_SUPPLY); +} + +#[test] +fn test_erc721_remote_transfer(is_collateral: u8) { + let mut setup = hyp_erc721_setup(); + + let is_collateral = if is_collateral % 2 == 0 { + true + } else { + false + }; + + let setup = deploy_remote_token(setup, is_collateral); + perform_remote_transfer(@setup, 2500, 0); + assert_eq!(setup.local_token.balance_of(starknet::get_contract_address()), INITIAL_SUPPLY - 1); +} + +#[test] +#[should_panic] +fn test_erc721_remote_transfer_revert_unowned() { + let setup = hyp_erc721_setup(); + + setup.local_token.transfer_from(starknet::get_contract_address(), BOB(), 1); + + let setup = deploy_remote_token(setup, false); + perform_remote_transfer(@setup, 2500, 1); +} + +#[test] +#[should_panic] +fn test_erc721_remote_transfer_revert_invalid_token_id() { + let setup = hyp_erc721_setup(); + + let setup = deploy_remote_token(setup, true); + perform_remote_transfer(@setup, 2500, INITIAL_SUPPLY); +} diff --git a/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_uri_storage_test.cairo b/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_uri_storage_test.cairo new file mode 100644 index 00000000000..b3d4f4e8c4f --- /dev/null +++ b/starknet/cairo/crates/token/tests/hyp_erc721/hyp_erc721_uri_storage_test.cairo @@ -0,0 +1,57 @@ +use alexandria_bytes::Bytes; +use contracts::client::router_component::{IRouterDispatcher, IRouterDispatcherTrait}; +use mocks::test_erc721::{ITestERC721Dispatcher, ITestERC721DispatcherTrait}; +use snforge_std::cheatcodes::contract_class::{ContractClass, ContractClassTrait}; +use snforge_std::{ + declare, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn, start_prank, stop_prank, + EventFetcher, event_name_hash +}; +use starknet::ContractAddress; +use super::common::{ + setup, DESTINATION, INITIAL_SUPPLY, Setup, IHypErc721TestDispatcher, + IHypErc721TestDispatcherTrait, ALICE, BOB, deploy_remote_token, perform_remote_transfer, + ZERO_ADDRESS, NAME, SYMBOL, URI +}; +use token::components::token_router::{ITokenRouterDispatcher, ITokenRouterDispatcherTrait}; + +fn setup_erc721_uri_storage() -> Setup { + let mut setup = setup(); + + let contract = declare("MockHypERC721URIStorage").unwrap(); + let mut calldata: Array = array![]; + setup.local_mailbox.contract_address.serialize(ref calldata); + INITIAL_SUPPLY.serialize(ref calldata); + NAME().serialize(ref calldata); + SYMBOL().serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + setup.default_ism.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + let (hyp_erc721_uri_storage, _) = contract.deploy(@calldata).unwrap(); + let hyp_erc721_uri_storage = IHypErc721TestDispatcher { + contract_address: hyp_erc721_uri_storage + }; + + hyp_erc721_uri_storage.set_token_uri(0, URI()); + let remote_token_address: felt252 = setup.remote_token.contract_address.into(); + hyp_erc721_uri_storage.enroll_remote_router(DESTINATION, remote_token_address.into()); + + setup.local_token = hyp_erc721_uri_storage; + + setup +} + +#[test] +#[should_panic] +fn test_erc721_uri_storage_remote_transfer_revert_burned() { + let setup = setup_erc721_uri_storage(); + + let setup = deploy_remote_token(setup, false); + perform_remote_transfer(@setup, 2500, 0); + + let balance = setup.local_token.balance_of(starknet::get_contract_address()); + assert_eq!(balance, INITIAL_SUPPLY - 1); + + let uri = setup.local_token.token_uri(0); + assert_eq!(uri, URI()); +} + diff --git a/starknet/cairo/crates/token/tests/lib.cairo b/starknet/cairo/crates/token/tests/lib.cairo new file mode 100644 index 00000000000..4fb61d606f2 --- /dev/null +++ b/starknet/cairo/crates/token/tests/lib.cairo @@ -0,0 +1,20 @@ +pub mod hyp_erc20 { + pub mod common; + pub mod hyp_erc20_collateral_test; + pub mod hyp_erc20_lockbox_test; + pub mod hyp_erc20_test; + pub mod hyp_fiat_token_test; + pub mod hyp_native_test; + pub mod hyp_xerc20_test; +} +pub mod hyp_erc721 { + pub mod common; + pub mod hyp_erc721_collateral_test; + pub mod hyp_erc721_collateral_uri_storage_test; + pub mod hyp_erc721_test; + pub mod hyp_erc721_uri_storage_test; +} +pub mod vault_extensions { + pub mod hyp_erc20_collateral_vault_deposit_test; + pub mod hyp_erc20_vault_test; +} diff --git a/starknet/cairo/crates/token/tests/vault_extensions/hyp_erc20_collateral_vault_deposit_test.cairo b/starknet/cairo/crates/token/tests/vault_extensions/hyp_erc20_collateral_vault_deposit_test.cairo new file mode 100644 index 00000000000..3a2d4097821 --- /dev/null +++ b/starknet/cairo/crates/token/tests/vault_extensions/hyp_erc20_collateral_vault_deposit_test.cairo @@ -0,0 +1,252 @@ +use mocks::mock_mailbox::{IMockMailboxDispatcher, IMockMailboxDispatcherTrait}; +use mocks::{test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait},}; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::{declare, ContractClassTrait, CheatTarget, start_prank, stop_prank,}; +use starknet::ContractAddress; +use super::super::hyp_erc20::common::{ + Setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, TRANSFER_AMT, ALICE, BOB, E18, REQUIRED_VALUE, + DESTINATION, IHypERC20TestDispatcher, IHypERC20TestDispatcherTrait, setup, + perform_remote_transfer_and_gas, enroll_remote_router, enroll_local_router, + perform_remote_transfer, handle_local_transfer, mint_and_approve +}; +use token::extensions::hyp_erc20_collateral_vault_deposit::{ + IHypERC20CollateralVaultDepositDispatcher, IHypERC20CollateralVaultDepositDispatcherTrait +}; +use token::interfaces::ierc4626::{IERC4626Dispatcher, IERC4626DispatcherTrait}; + +const DUST_AMOUNT: u256 = 100_000_000_000; // E11 + +fn _transfer_roundtrip_and_increase_yields( + setup: @Setup, vault: ContractAddress, transfer_amount: u256, yield_amount: u256 +) { + // Transfer from Alice to Bob + start_prank(CheatTarget::One((*setup).primary_token.contract_address), ALICE()); + (*setup).primary_token.approve((*setup).local_token.contract_address, transfer_amount); + stop_prank(CheatTarget::One((*setup).primary_token.contract_address)); + perform_remote_transfer(setup, 0, transfer_amount); + // Increase vault balance, which will reduce share redeemed for the same amount + (*setup).primary_token.mint(vault, yield_amount); + start_prank(CheatTarget::One((*setup).remote_token.contract_address), BOB()); + (*setup) + .remote_token + .transfer_remote( + ORIGIN, + Into::::into(BOB()) + .into(), // orginal test has Bob here as well but not sure, should it be alice + transfer_amount, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One((*setup).remote_token.contract_address)); +} + +fn assert_approx_eq_abs(lhs: u256, rhs: u256, relaxation: u256) { + let diff = if lhs >= rhs { + lhs - rhs + } else { + rhs - lhs + }; + assert!(diff <= relaxation, "Values are not approximately equal"); +} + +fn setup_vault() -> (Setup, IERC4626Dispatcher, IHypERC20CollateralVaultDepositDispatcher) { + let mut setup = setup(); + let contract = declare("ERC4626Mock").unwrap(); + let mut calldata: Array = array![]; + setup.primary_token.contract_address.serialize(ref calldata); + let name: ByteArray = "Regular Vault"; + let symbol: ByteArray = "RV"; + name.serialize(ref calldata); + symbol.serialize(ref calldata); + let (vault, _) = contract.deploy(@calldata).unwrap(); + + let contract = declare("HypERC20CollateralVaultDeposit").unwrap(); + let mut calldata: Array = array![]; + setup.local_mailbox.contract_address.serialize(ref calldata); + vault.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + setup.implementation.interchain_security_module().serialize(ref calldata); + let (implementation, _) = contract.deploy(@calldata).unwrap(); + setup.local_token = IHypERC20TestDispatcher { contract_address: implementation }; + setup + .local_token + .enroll_remote_router( + DESTINATION, + Into::::into(setup.remote_token.contract_address).into() + ); + + setup.remote_mailbox.set_default_hook(setup.noop_hook.contract_address); + setup.remote_mailbox.set_required_hook(setup.noop_hook.contract_address); + + setup.primary_token.transfer(ALICE(), 1000 * E18); + + setup + .remote_token + .enroll_remote_router( + ORIGIN, + Into::::into(setup.local_token.contract_address).into() + ); + ( + setup, + IERC4626Dispatcher { contract_address: vault }, + IHypERC20CollateralVaultDepositDispatcher { contract_address: implementation } + ) +} + +fn erc4626_vault_deposit_remote_transfer_deposits_into_vault( + mut transfer_amount: u256 +) -> (Setup, IERC4626Dispatcher, IHypERC20CollateralVaultDepositDispatcher) { + transfer_amount %= TOTAL_SUPPLY + 1; + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + mint_and_approve(@setup, transfer_amount, ALICE(), setup.local_token.contract_address); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + // Check vault shares balance before and after transfer + assert_eq!(vault.max_redeem(erc20_collateral_vault_deposit.contract_address), 0); + assert_eq!(erc20_collateral_vault_deposit.get_asset_deposited(), 0); + + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + setup.primary_token.approve(setup.local_token.contract_address, transfer_amount); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + perform_remote_transfer(@setup, 0, transfer_amount); + assert_approx_eq_abs( + vault.max_redeem(erc20_collateral_vault_deposit.contract_address), transfer_amount, 1 + ); + assert_eq!(erc20_collateral_vault_deposit.get_asset_deposited(), transfer_amount); + (setup, vault, erc20_collateral_vault_deposit) +} + +#[test] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_deposits_into_vault(mut transfer_amount: u256) { + erc4626_vault_deposit_remote_transfer_deposits_into_vault(transfer_amount); +} + +#[test] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_withdraws_from_vault(mut transfer_amount: u256) { + transfer_amount %= TOTAL_SUPPLY + 1; + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + mint_and_approve(@setup, transfer_amount, ALICE(), setup.local_token.contract_address); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, transfer_amount, DUST_AMOUNT + ); + // Check Alice's local token balance + let prev_balance = setup.local_token.balance_of(ALICE()); + handle_local_transfer(@setup, transfer_amount); + let after_balance = setup.local_token.balance_of(ALICE()); + assert_eq!(after_balance, prev_balance + transfer_amount); + assert_eq!(erc20_collateral_vault_deposit.get_asset_deposited(), 0); +} + +#[test] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_withdraw_less_shares(mut reward_amount: u256) { + reward_amount %= TOTAL_SUPPLY + 1; + if reward_amount < DUST_AMOUNT { + reward_amount += DUST_AMOUNT; + } + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, TRANSFER_AMT, reward_amount + ); + // Check Alice's local token balance + let prev_balance = setup.local_token.balance_of(ALICE()); + handle_local_transfer(@setup, TRANSFER_AMT); + let after_balance = setup.local_token.balance_of(ALICE()); + assert_eq!(after_balance, prev_balance + TRANSFER_AMT); + // Has leftover shares, but no assets deposited] + assert_eq!(erc20_collateral_vault_deposit.get_asset_deposited(), 0); + assert_gt!(vault.max_redeem(erc20_collateral_vault_deposit.contract_address), 0); +} + +#[test] +#[should_panic] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_sweep_revert_non_owner(mut reward_amount: u256) { + reward_amount %= TOTAL_SUPPLY + 1; + if reward_amount < DUST_AMOUNT { + reward_amount += DUST_AMOUNT; + } + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, TRANSFER_AMT, reward_amount + ); + start_prank(CheatTarget::One(erc20_collateral_vault_deposit.contract_address), BOB()); + erc20_collateral_vault_deposit.sweep(); + stop_prank(CheatTarget::One(erc20_collateral_vault_deposit.contract_address)); +} + +#[test] +fn test_fuzz_erc4626_vault_deposit_remote_transfer_sweep_no_excess_shares( + mut transfer_amount: u256 +) { + let (mut setup, _, mut erc20_collateral_vault_deposit) = + erc4626_vault_deposit_remote_transfer_deposits_into_vault( + transfer_amount + ); + let owner = IOwnableDispatcher { + contract_address: erc20_collateral_vault_deposit.contract_address + } + .owner(); + let owner_balance_prev = setup.primary_token.balance_of(owner); + erc20_collateral_vault_deposit.sweep(); + let owner_balance_after = setup.primary_token.balance_of(owner); + assert_eq!(owner_balance_prev, owner_balance_after); +} + +#[test] +fn test_erc4626_vault_deposit_remote_transfer_sweep_excess_shares_12312(mut reward_amount: u256) { + reward_amount %= TOTAL_SUPPLY + 1; + if reward_amount < DUST_AMOUNT { + reward_amount += DUST_AMOUNT; + } + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, TRANSFER_AMT, reward_amount + ); + handle_local_transfer(@setup, TRANSFER_AMT); + let owner = IOwnableDispatcher { + contract_address: erc20_collateral_vault_deposit.contract_address + } + .owner(); + let owner_balance_prev = setup.primary_token.balance_of(owner); + let excess_amount = vault.max_redeem(erc20_collateral_vault_deposit.contract_address); + erc20_collateral_vault_deposit.sweep(); + let owner_balance_after = setup.primary_token.balance_of(owner); + assert_gt!(owner_balance_after, owner_balance_prev + excess_amount); +} + +#[test] +fn test_erc4626_vault_deposit_remote_transfer_sweep_excess_shares_multiple_deposit( + mut reward_amount: u256 +) { + reward_amount %= TOTAL_SUPPLY + 1; + if reward_amount < DUST_AMOUNT { + reward_amount += DUST_AMOUNT; + } + let (mut setup, mut vault, mut erc20_collateral_vault_deposit) = setup_vault(); + _transfer_roundtrip_and_increase_yields( + @setup, vault.contract_address, TRANSFER_AMT, reward_amount + ); + handle_local_transfer(@setup, TRANSFER_AMT); + + let owner = IOwnableDispatcher { + contract_address: erc20_collateral_vault_deposit.contract_address + } + .owner(); + let owner_balance_prev = setup.primary_token.balance_of(owner); + let excess_amount = vault.max_redeem(erc20_collateral_vault_deposit.contract_address); + // Deposit again for Alice + start_prank(CheatTarget::One(setup.primary_token.contract_address), ALICE()); + setup.primary_token.approve(setup.local_token.contract_address, TRANSFER_AMT); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + perform_remote_transfer(@setup, 0, TRANSFER_AMT); + // Sweep and check + erc20_collateral_vault_deposit.sweep(); + let owner_balance_after = setup.primary_token.balance_of(owner); + assert_gt!(owner_balance_after, owner_balance_prev + excess_amount); +} + +// NOTE: Not applicable on Starknet +fn test_benchmark_overhead_gas_usage() {} diff --git a/starknet/cairo/crates/token/tests/vault_extensions/hyp_erc20_vault_test.cairo b/starknet/cairo/crates/token/tests/vault_extensions/hyp_erc20_vault_test.cairo new file mode 100644 index 00000000000..b3865747996 --- /dev/null +++ b/starknet/cairo/crates/token/tests/vault_extensions/hyp_erc20_vault_test.cairo @@ -0,0 +1,610 @@ +use contracts::interfaces::{IMailboxClientDispatcher, IMailboxClientDispatcherTrait}; +use mocks::erc4626_yield_sharing_mock::{ + IERC4626YieldSharingDispatcher, IERC4626YieldSharingDispatcherTrait +}; +use mocks::mock_mailbox::{IMockMailboxDispatcher, IMockMailboxDispatcherTrait}; +use mocks::{test_erc20::{ITestERC20Dispatcher, ITestERC20DispatcherTrait},}; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::{declare, CheatTarget, start_prank, stop_prank, ContractClass, ContractClassTrait}; +use starknet::ContractAddress; +use super::super::hyp_erc20::common::{ + Setup, TOTAL_SUPPLY, DECIMALS, ORIGIN, TRANSFER_AMT, ALICE, BOB, DANIEL, CAROL, E18, + REQUIRED_VALUE, DESTINATION, IHypERC20TestDispatcher, IHypERC20TestDispatcherTrait, + perform_remote_transfer_and_gas, enroll_remote_router, enroll_local_router, + perform_remote_transfer, handle_local_transfer, mint_and_approve, connect_routers +}; +use super::super::hyp_erc20::common; +use token::components::token_router::{ITokenRouterDispatcher, ITokenRouterDispatcherTrait}; +use token::extensions::{ + hyp_erc20_vault_collateral::{ + IHypErc20VaultCollateralDispatcher, IHypErc20VaultCollateralDispatcherTrait + }, + hyp_erc20_vault::{IHypErc20VaultDispatcher, IHypErc20VaultDispatcherTrait} +}; +use token::interfaces::ierc4626::{IERC4626Dispatcher, IERC4626DispatcherTrait}; + +const PEER_DESTINATION: u32 = 13; +const YIELD: u256 = 5 * E18; +const YIELD_FEES: u256 = E18 / 10; // E17 +const E14: u256 = 100_000_000_000_000; +const E10: u256 = 10_000_000_000; + +fn setup_vault() -> ( + Setup, + IHypErc20VaultCollateralDispatcher, + IERC4626Dispatcher, + IERC4626Dispatcher, + IERC4626YieldSharingDispatcher, + IMockMailboxDispatcher +) { + let mut setup = common::setup(); + // multi-synthetic setup + let default_ism = setup.implementation.interchain_security_module(); + + let (peer_mailbox, _) = setup + .mock_mailbox_contract + .deploy( + @array![ + PEER_DESTINATION.into(), + default_ism.into(), + setup.noop_hook.contract_address.into(), + setup.eth_token.contract_address.into() + ] + ) + .unwrap(); + + let peer_mailbox_dispatcher = IMockMailboxDispatcher { contract_address: peer_mailbox }; + setup.local_mailbox.add_remote_mail_box(PEER_DESTINATION, peer_mailbox); + setup.remote_mailbox.add_remote_mail_box(PEER_DESTINATION, peer_mailbox); + peer_mailbox_dispatcher.add_remote_mail_box(DESTINATION, setup.remote_mailbox.contract_address); + peer_mailbox_dispatcher.add_remote_mail_box(ORIGIN, setup.local_mailbox.contract_address); + + let contract = declare("ERC4626YieldSharingMock").unwrap(); + let mut calldata: Array = array![]; + setup.primary_token.contract_address.serialize(ref calldata); + let name: ByteArray = "Regular Vault"; + let symbol: ByteArray = "RV"; + name.serialize(ref calldata); + symbol.serialize(ref calldata); + YIELD_FEES.serialize(ref calldata); + start_prank(CheatTarget::All, DANIEL()); + let (vault, _) = contract.deploy(@calldata).unwrap(); + stop_prank(CheatTarget::All); + + let contract = declare("HypErc20VaultCollateral").unwrap(); + let (local_token, _) = contract + .deploy( + @array![ + setup.local_mailbox.contract_address.into(), + vault.into(), + starknet::get_contract_address().into(), + setup.noop_hook.contract_address.into(), + default_ism.into() + ] + ) + .unwrap(); + + let dummy_name: ByteArray = "Dummy Name"; + let dummy_symbol: ByteArray = "DUM"; + let contract = declare("HypErc20Vault").unwrap(); + let mut calldata: Array = array![]; + setup.primary_token.decimals().serialize(ref calldata); + setup.remote_mailbox.contract_address.serialize(ref calldata); + TOTAL_SUPPLY.serialize(ref calldata); + dummy_name.serialize(ref calldata); + dummy_symbol.serialize(ref calldata); + setup.local_mailbox.get_local_domain().serialize(ref calldata); + setup.primary_token.contract_address.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + default_ism.serialize(ref calldata); + + let (remote_token, _) = contract.deploy(@calldata).unwrap(); + + let mut calldata: Array = array![]; + setup.primary_token.decimals().serialize(ref calldata); + peer_mailbox.serialize(ref calldata); + TOTAL_SUPPLY.serialize(ref calldata); + dummy_name.serialize(ref calldata); + dummy_symbol.serialize(ref calldata); + setup.local_mailbox.get_local_domain().serialize(ref calldata); + setup.primary_token.contract_address.serialize(ref calldata); + starknet::get_contract_address().serialize(ref calldata); + setup.noop_hook.contract_address.serialize(ref calldata); + default_ism.serialize(ref calldata); + let (peer_token, _) = contract.deploy(@calldata).unwrap(); + + let local_rebasing_token = IHypErc20VaultCollateralDispatcher { contract_address: local_token }; + let remote_rebasing_token = IERC4626Dispatcher { contract_address: remote_token }; + let peer_rebasing_token = IERC4626Dispatcher { contract_address: peer_token }; + setup.primary_token.transfer(ALICE(), 1000 * E18); + let domains = array![ORIGIN, DESTINATION, PEER_DESTINATION]; + let addresses_u256 = array![ + Into::::into(local_token).into(), + Into::::into(remote_token).into(), + Into::::into(peer_token).into() + ]; + + connect_routers(@setup, domains.span(), addresses_u256.span()); + + ( + setup, + local_rebasing_token, + remote_rebasing_token, + peer_rebasing_token, + IERC4626YieldSharingDispatcher { contract_address: vault }, + peer_mailbox_dispatcher + ) +} + +#[test] +fn test_collateral_domain() { + let (_, local_rebasing_token, remote_rebasing_token, _, _, _) = setup_vault(); + assert_eq!( + IHypErc20VaultDispatcher { contract_address: remote_rebasing_token.contract_address } + .get_collateral_domain(), + IMailboxClientDispatcher { contract_address: local_rebasing_token.contract_address } + .get_local_domain() + ); +} + +#[test] +fn test_remote_transfer_rebase_after() { + let ( + mut setup, mut local_rebasing_token, remote_rebasing_token, _, mut yield_sharing_vault, _ + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + _accrue_yield(@setup, yield_sharing_vault.contract_address); + + local_rebasing_token.rebase(DESTINATION, 0); + + setup.remote_mailbox.process_next_inbound_message(); + + assert_eq!( + remote_rebasing_token.balance_of(BOB()), + TRANSFER_AMT + _discounted_yield(yield_sharing_vault) + ); +} + +#[test] +fn test_rebase_with_transfer() { + let ( + mut setup, mut local_rebasing_token, remote_rebasing_token, _, mut yield_sharing_vault, _ + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + _accrue_yield(@setup, yield_sharing_vault.contract_address); + + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + + assert_approx_eq_rel( + remote_rebasing_token.balance_of(BOB()), + 2 * TRANSFER_AMT + _discounted_yield(yield_sharing_vault), + E14, + ); +} + +#[test] +fn test_synthetic_transfers_with_rebase() { + let ( + mut setup, mut local_rebasing_token, remote_rebasing_token, _, mut yield_sharing_vault, _ + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + _accrue_yield(@setup, yield_sharing_vault.contract_address); + + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + start_prank(CheatTarget::All, BOB()); + remote_rebasing_token.transfer(CAROL(), TRANSFER_AMT); + stop_prank(CheatTarget::All); + assert_approx_eq_rel( + remote_rebasing_token.balance_of(BOB()), + TRANSFER_AMT + _discounted_yield(yield_sharing_vault), + E14, + ); + assert_approx_eq_rel(remote_rebasing_token.balance_of(CAROL()), TRANSFER_AMT, E14,); +} + +#[test] +fn test_withdrawal_without_yield() { + let (mut setup, mut local_rebasing_token, remote_rebasing_token, _, _, _) = setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + start_prank(CheatTarget::One(remote_rebasing_token.contract_address), BOB()); + ITokenRouterDispatcher { contract_address: remote_rebasing_token.contract_address } + .transfer_remote( + ORIGIN, + Into::::into(BOB()).into(), + TRANSFER_AMT, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(remote_rebasing_token.contract_address)); + + setup.local_mailbox.process_next_inbound_message(); + assert_eq!(setup.primary_token.balance_of(BOB()), TRANSFER_AMT); +} + +#[test] +fn test_withdrawal_with_yield() { + let ( + mut setup, mut local_rebasing_token, remote_rebasing_token, _, mut yield_sharing_vault, _ + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + _accrue_yield(@setup, yield_sharing_vault.contract_address); + + start_prank(CheatTarget::One(remote_rebasing_token.contract_address), BOB()); + ITokenRouterDispatcher { contract_address: remote_rebasing_token.contract_address } + .transfer_remote( + ORIGIN, + Into::::into(BOB()).into(), + TRANSFER_AMT, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(remote_rebasing_token.contract_address)); + + setup.local_mailbox.process_next_inbound_message(); + // BOB gets the yield even though it didn't rebase + let bob_balance = setup.primary_token.balance_of(BOB()); + let expected_balance = TRANSFER_AMT + _discounted_yield(yield_sharing_vault); + assert_approx_eq_rel(bob_balance, expected_balance, E14); + assert_lt!(bob_balance, expected_balance, "Transfer remote should round down"); + assert_eq!(yield_sharing_vault.accumulated_fees(), YIELD / 10); +} + +#[test] +fn test_withdrawal_after_yield() { + let ( + mut setup, mut local_rebasing_token, remote_rebasing_token, _, mut yield_sharing_vault, _ + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + _accrue_yield(@setup, yield_sharing_vault.contract_address); + + local_rebasing_token.rebase(DESTINATION, 0); + setup.remote_mailbox.process_next_inbound_message(); + + // Use balance here since it might be off by <1bp + let bob_balance_remote = remote_rebasing_token.balance_of(BOB()); + + start_prank(CheatTarget::One(remote_rebasing_token.contract_address), BOB()); + ITokenRouterDispatcher { contract_address: remote_rebasing_token.contract_address } + .transfer_remote( + ORIGIN, + Into::::into(BOB()).into(), + bob_balance_remote, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(remote_rebasing_token.contract_address)); + setup.local_mailbox.process_next_inbound_message(); + let bob_balance_primary = setup.primary_token.balance_of(BOB()); + assert_approx_eq_rel( + bob_balance_primary, TRANSFER_AMT + _discounted_yield(yield_sharing_vault), E14 + ); + assert_eq!(yield_sharing_vault.accumulated_fees(), YIELD / 10); +} + +#[test] +fn test_withdrawal_in_flight() { + let ( + mut setup, mut local_rebasing_token, remote_rebasing_token, _, mut yield_sharing_vault, _ + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + setup.primary_token.mint(CAROL(), TRANSFER_AMT); + start_prank(CheatTarget::One(setup.primary_token.contract_address), CAROL()); + setup.primary_token.approve(local_rebasing_token.contract_address, TRANSFER_AMT); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + start_prank(CheatTarget::One(local_rebasing_token.contract_address), CAROL()); + ITokenRouterDispatcher { contract_address: local_rebasing_token.contract_address } + .transfer_remote( + DESTINATION, + Into::::into(CAROL()).into(), + TRANSFER_AMT, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(local_rebasing_token.contract_address)); + + setup.remote_mailbox.process_next_inbound_message(); + + _accrue_yield(@setup, yield_sharing_vault.contract_address); + _accrue_yield(@setup, yield_sharing_vault.contract_address); // earning 2x yield to be split + + local_rebasing_token.rebase(DESTINATION, 0); + start_prank(CheatTarget::One(remote_rebasing_token.contract_address), CAROL()); + ITokenRouterDispatcher { contract_address: remote_rebasing_token.contract_address } + .transfer_remote( + ORIGIN, + Into::::into(CAROL()).into(), + TRANSFER_AMT, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(remote_rebasing_token.contract_address)); + + setup.local_mailbox.process_next_inbound_message(); + + let claimable_fees = IERC4626YieldSharingDispatcher { + contract_address: yield_sharing_vault.contract_address + } + .get_claimable_fees(); + let carol_balance_primary = setup.primary_token.balance_of(CAROL()); + assert_approx_eq_rel(carol_balance_primary, TRANSFER_AMT + YIELD - (claimable_fees / 2), E14); + + // until we process the rebase, the yield is not added on the remote + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + setup.remote_mailbox.process_next_inbound_message(); + assert_approx_eq_rel( + remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT + YIELD - (claimable_fees / 2), E14 + ); + + assert_eq!(yield_sharing_vault.accumulated_fees(), YIELD / 5); // 0.1 * 2 * yield +} + +#[test] +fn test_withdrawal_after_drawdown() { + let ( + mut setup, mut local_rebasing_token, remote_rebasing_token, _, mut yield_sharing_vault, _ + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + // decrease collateral in vault by 10% + let drawdown = 5 * E18; + start_prank( + CheatTarget::One(setup.primary_token.contract_address), yield_sharing_vault.contract_address + ); + setup.primary_token.burn(drawdown); + stop_prank(CheatTarget::One(setup.primary_token.contract_address)); + + local_rebasing_token.rebase(DESTINATION, 0); + setup.remote_mailbox.process_next_inbound_message(); + + // Use balance here since it might be off by <1bp + let bob_balance_remote = remote_rebasing_token.balance_of(BOB()); + start_prank(CheatTarget::One(remote_rebasing_token.contract_address), BOB()); + ITokenRouterDispatcher { contract_address: remote_rebasing_token.contract_address } + .transfer_remote( + ORIGIN, + Into::::into(BOB()).into(), + bob_balance_remote, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(remote_rebasing_token.contract_address)); + setup.local_mailbox.process_next_inbound_message(); + assert_approx_eq_rel(setup.primary_token.balance_of(BOB()), TRANSFER_AMT - drawdown, E14); +} + +#[test] +fn test_exchange_rate_set_only_by_collateral() { + let ( + mut setup, + mut local_rebasing_token, + remote_rebasing_token, + peer_rebasing_token, + mut yield_sharing_vault, + mut peer_mailbox + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + _accrue_yield(@setup, yield_sharing_vault.contract_address); + + local_rebasing_token.rebase(DESTINATION, 0); + setup.remote_mailbox.process_next_inbound_message(); + + start_prank(CheatTarget::One(remote_rebasing_token.contract_address), BOB()); + ITokenRouterDispatcher { contract_address: remote_rebasing_token.contract_address } + .transfer_remote( + PEER_DESTINATION, + Into::::into(BOB()).into(), + TRANSFER_AMT, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(remote_rebasing_token.contract_address)); + peer_mailbox.process_next_inbound_message(); + + assert_eq!( + IHypErc20VaultDispatcher { contract_address: remote_rebasing_token.contract_address } + .get_exchange_rate(), + 10_450_000_000 + ); // 5 * 0.9 = 4.5% yield + assert_eq!( + IHypErc20VaultDispatcher { contract_address: peer_rebasing_token.contract_address } + .get_exchange_rate(), + E10 + ); // asserting that transfers by the synthetic variant don't impact the exchang rate + + local_rebasing_token.rebase(PEER_DESTINATION, 0); + peer_mailbox.process_next_inbound_message(); + + assert_eq!( + IHypErc20VaultDispatcher { contract_address: peer_rebasing_token.contract_address } + .get_exchange_rate(), + 10_450_000_000 + ); // asserting that the exchange rate is set finally by the collateral variant +} + +#[test] +fn test_cyclic_transfers() { + // ALICE: local -> remote(BOB) + let ( + mut setup, + mut local_rebasing_token, + remote_rebasing_token, + peer_rebasing_token, + mut yield_sharing_vault, + mut peer_mailbox + ) = + setup_vault(); + _perform_remote_transfer_without_expectation( + @setup, local_rebasing_token.contract_address, 0, TRANSFER_AMT + ); + assert_eq!(remote_rebasing_token.balance_of(BOB()), TRANSFER_AMT); + + _accrue_yield(@setup, yield_sharing_vault.contract_address); + + local_rebasing_token.rebase(DESTINATION, 0); // yield is added + setup.remote_mailbox.process_next_inbound_message(); + // BOB: remote -> peer(BOB) (yield is leftover) + start_prank(CheatTarget::One(remote_rebasing_token.contract_address), BOB()); + ITokenRouterDispatcher { contract_address: remote_rebasing_token.contract_address } + .transfer_remote( + PEER_DESTINATION, + Into::::into(BOB()).into(), + TRANSFER_AMT, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(remote_rebasing_token.contract_address)); + peer_mailbox.process_next_inbound_message(); + + local_rebasing_token.rebase(PEER_DESTINATION, 0); + peer_mailbox.process_next_inbound_message(); + + // BOB: peer -> local(CAROL) + start_prank(CheatTarget::One(peer_rebasing_token.contract_address), BOB()); + ITokenRouterDispatcher { contract_address: peer_rebasing_token.contract_address } + .transfer_remote( + ORIGIN, + Into::::into(CAROL()).into(), + TRANSFER_AMT, + 0, + Option::None, + Option::None + ); + stop_prank(CheatTarget::One(peer_rebasing_token.contract_address)); + setup.local_mailbox.process_next_inbound_message(); + + assert_approx_eq_rel( + remote_rebasing_token.balance_of(BOB()), _discounted_yield(yield_sharing_vault), E14 + ); + assert_eq!(peer_rebasing_token.balance_of(BOB()), 0); + assert_approx_eq_rel(setup.primary_token.balance_of(CAROL()), TRANSFER_AMT, E14); +} + +// skipped in solidity version as well +//#[test] +//fn test_transfer_with_hook_specified() { +// assert(true, ''); +//} + +// NOTE: Not applicable on Starknet +fn test_benchmark_overhead_gas_usage() {} + +/////////////////////////////////////////////////////// +/// Helper functions +/////////////////////////////////////////////////////// + +/// ALICE: local -> remote(BOB) +fn _perform_remote_transfer_without_expectation( + setup: @Setup, local_token: ContractAddress, msg_value: u256, amount: u256 +) { + start_prank(CheatTarget::One((*setup).primary_token.contract_address), ALICE()); + (*setup).primary_token.approve(local_token, TRANSFER_AMT); + stop_prank(CheatTarget::One((*setup).primary_token.contract_address)); + + start_prank(CheatTarget::One(local_token), ALICE()); + ITokenRouterDispatcher { contract_address: local_token } + .transfer_remote( + DESTINATION, + Into::::into(BOB()).into(), + amount, + msg_value, + Option::None, + Option::None + ); + + stop_prank(CheatTarget::One(local_token)); + + (*setup).remote_mailbox.process_next_inbound_message(); +} + +fn _accrue_yield(setup: @Setup, vault: ContractAddress) { + (*setup).primary_token.mint(vault, YIELD); +} + +fn _discounted_yield(vault: IERC4626YieldSharingDispatcher) -> u256 { + YIELD - vault.get_claimable_fees() +} + +/// see {https://github.com/foundry-rs/foundry/blob/e16a75b615f812db6127ea22e23c3ee65504c1f1/crates/cheatcodes/src/test/assert.rs#L533} +fn assert_approx_eq_rel(lhs: u256, rhs: u256, max_delta: u256) { + if lhs == 0 { + if rhs == 0 { + return; + } else { + panic!("eq_rel_assertion error lhs {}, rhs {}, max_delta {}", lhs, rhs, max_delta); + } + } + + let mut delta = if lhs > rhs { + lhs - rhs + } else { + rhs - lhs + }; + + delta *= E18; + delta /= rhs; + + if delta > max_delta { + panic!( + "eq_rel_assertion error lhs {}, rhs {}, max_delta {}, real_delta {}", + lhs, + rhs, + max_delta, + delta + ); + } +} diff --git a/starknet/package.json b/starknet/package.json new file mode 100644 index 00000000000..6db59f816af --- /dev/null +++ b/starknet/package.json @@ -0,0 +1,33 @@ +{ + "name": "@hyperlane-xyz/starknet-core", + "description": "Core cairo contracts for Hyperlane", + "version": "1.0.0", + "type": "module", + "homepage": "https://www.hyperlane.xyz", + "author": "", + "license": "Apache-2.0", + "scripts": { + "build": "./build.sh && tsc && cp -R ./src/target ./dist/target" + }, + "devDependencies": { + "@types/node": "^22.7.9", + "eslint": "^9.13.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.3.3", + "ts-node": "^10.9.2", + "tsx": "^4.19.1", + "typescript": "5.3.3" + }, + "exports": { + ".": "./dist/index.js" + }, + "types": "./dist/index.d.ts", + "keywords": [ + "Hyperlane", + "Cairo", + "Starknet" + ], + "engines": { + "node": ">=16" + } +} diff --git a/starknet/src/config.ts b/starknet/src/config.ts new file mode 100644 index 00000000000..1051a10379c --- /dev/null +++ b/starknet/src/config.ts @@ -0,0 +1,32 @@ +export const CONFIG = { + PATHS: { + MAIN: 'target/dev', + }, + SUFFIXES: { + STANDARD: '.contract_class.json', + COMPILED: '.compiled_contract_class.json', + }, + ERROR_CODES: { + FILE_NOT_FOUND: 'FILE_NOT_FOUND', + PARSE_ERROR: 'PARSE_ERROR', + INVALID_INPUT: 'INVALID_INPUT', + }, + CONTRACT_NAME_VALIDATION: { + MAX_LENGTH: 128, + MIN_LENGTH: 1, + FORBIDDEN_CHARS: [ + '..', + '/', + '\\', + ' ', + '*', + '?', + '<', + '>', + '|', + '"', + ':', + ] as const, + PATTERN: /^[a-zA-Z0-9_-]+$/, + }, +} as const; diff --git a/starknet/src/errors.ts b/starknet/src/errors.ts new file mode 100644 index 00000000000..2d8c5fb1c99 --- /dev/null +++ b/starknet/src/errors.ts @@ -0,0 +1,18 @@ +import { CONFIG } from './config.js'; + +export class ContractError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly details?: unknown, + ) { + super(`[${code}] ${message}`); + this.name = 'ContractError'; + } +} + +export const ErrorMessages = { + [CONFIG.ERROR_CODES.FILE_NOT_FOUND]: 'Contract file not found', + [CONFIG.ERROR_CODES.PARSE_ERROR]: 'Failed to parse contract', + [CONFIG.ERROR_CODES.INVALID_INPUT]: 'Invalid input parameters', +} as const; diff --git a/starknet/src/index.ts b/starknet/src/index.ts new file mode 100644 index 00000000000..31595d5003b --- /dev/null +++ b/starknet/src/index.ts @@ -0,0 +1,91 @@ +import { existsSync, readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +import { CONFIG } from './config.js'; +import { ContractError, ErrorMessages } from './errors.js'; +import { CompiledContractCasm, ContractData } from './types.js'; +import { assertValidContractName } from './utils.js'; + +const currentDirectory = dirname(fileURLToPath(import.meta.url)); +const TARGET_DEV_PATH = join(currentDirectory, CONFIG.PATHS.MAIN); + +/** + * @notice Retrieves and parses the standard compiled contract data + * @dev Reads the contract file with STANDARD suffix and parses it as JSON + * @param name The name of the contract to retrieve + * @returns {ContractData} The parsed contract data + * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid + */ +export const getCompiledContract = (name: string): ContractData => { + try { + return JSON.parse( + readFileSync(findContractFile(name, 'STANDARD'), 'utf-8'), + ); + } catch (error: unknown) { + if (error instanceof ContractError) throw error; + throw new ContractError( + ErrorMessages[CONFIG.ERROR_CODES.PARSE_ERROR], + CONFIG.ERROR_CODES.PARSE_ERROR, + { + name, + error: (error as Error).message, + }, + ); + } +}; + +/** + * @notice Retrieves and parses the CASM compiled contract data + * @dev Reads the contract file with COMPILED suffix and parses it as JSON + * @param name The name of the contract to retrieve + * @returns {CompiledContractCasm} The parsed CASM contract data + * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid + */ +export const getCompiledContractCasm = (name: string): CompiledContractCasm => { + try { + return JSON.parse( + readFileSync(findContractFile(name, 'COMPILED'), 'utf-8'), + ); + } catch (error: unknown) { + if (error instanceof ContractError) throw error; + throw new ContractError( + ErrorMessages[CONFIG.ERROR_CODES.PARSE_ERROR], + CONFIG.ERROR_CODES.PARSE_ERROR, + { + name, + error: (error as Error).message, + }, + ); + } +}; + +/** + * @notice Locates a contract file with the specified suffix + * @dev Combines the target path with contract name and suffix, validates file existence + * @param name The name of the contract to find + * @param suffix The suffix type from CONFIG.SUFFIXES to append to the filename + * @returns The full path to the contract file + * @throws {ContractError} If the file is not found or name is invalid + */ +function findContractFile( + name: string, + suffix: keyof typeof CONFIG.SUFFIXES, +): string { + assertValidContractName(name); + const mainPath = `${TARGET_DEV_PATH}/${name}${CONFIG.SUFFIXES[suffix]}`; + + if (!existsSync(mainPath)) { + throw new ContractError( + ErrorMessages[CONFIG.ERROR_CODES.FILE_NOT_FOUND], + CONFIG.ERROR_CODES.FILE_NOT_FOUND, + { + name, + suffix, + path: mainPath, + }, + ); + } + + return mainPath; +} diff --git a/starknet/src/types.ts b/starknet/src/types.ts new file mode 100644 index 00000000000..f3f23316b84 --- /dev/null +++ b/starknet/src/types.ts @@ -0,0 +1,76 @@ +export interface SierraProgram { + sierra_program: string[]; + sierra_program_debug_info?: { + type_information?: unknown; + libfunc_declarations?: unknown; + user_func_declarations?: unknown; + }; + contract_class_version: string; + entry_points_by_type: { + CONSTRUCTOR: EntryPoint[]; + EXTERNAL: EntryPoint[]; + L1_HANDLER: EntryPoint[]; + }; + abi: ContractAbi; +} + +export interface EntryPoint { + selector: string; + function_idx: number; +} + +export interface ContractAbi { + type: string; + name?: string; + inputs?: AbiInput[]; + outputs?: AbiOutput[]; + state_mutability?: string; + functions: AbiFunction[]; + events: AbiEvent[]; + structs: AbiStruct[]; + l1_handler?: boolean; +} + +export interface AbiFunction { + name: string; + inputs: AbiInput[]; + outputs: AbiOutput[]; + state_mutability: string; +} + +export interface AbiEvent { + name: string; + inputs: AbiInput[]; +} + +export interface AbiInput { + name: string; + type: string; +} + +export interface AbiOutput { + type: string; +} + +export interface AbiStruct { + name: string; + size: number; + members: AbiStructMember[]; +} + +export interface AbiStructMember { + name: string; + type: string; + offset: number; +} + +// Update the main ContractData interface +export interface ContractData extends SierraProgram { + [key: string]: unknown; // Keep this for backward compatibility +} + +export interface CompiledContractCasm { + prime: string; // e.g. "0x800000000000011000000000000000000000000000000000000000000000001" + compiler_version: string; // e.g. "2.6.4" + bytecode: string[]; // Array of hex strings representing bytecode instructions +} diff --git a/starknet/src/utils.ts b/starknet/src/utils.ts new file mode 100644 index 00000000000..290c5b57dfe --- /dev/null +++ b/starknet/src/utils.ts @@ -0,0 +1,99 @@ +import { CONFIG } from './config.js'; +import { ContractError, ErrorMessages } from './errors.js'; + +/** + * @notice Represents different types of contract name validation errors + */ +export type ValidationError = + | { type: 'empty' } + | { type: 'tooLong'; maxLength: number } + | { type: 'invalidChars'; chars: string[] } + | { type: 'invalidPattern' }; + +/** + * @notice Validates a contract name and returns any validation errors + * @dev Checks for empty strings, length limits, forbidden characters, and pattern matching + * @param name The contract name to validate + * @returns An array of validation errors, empty if valid + */ +export function validateContractName(name: string): ValidationError[] { + const errors: ValidationError[] = []; + + // Check for empty or whitespace-only names + if (!name.trim()) { + errors.push({ type: 'empty' }); + return errors; // Return early as other checks don't matter + } + + // Check length + if (name.length > CONFIG.CONTRACT_NAME_VALIDATION.MAX_LENGTH) { + errors.push({ + type: 'tooLong', + maxLength: CONFIG.CONTRACT_NAME_VALIDATION.MAX_LENGTH, + }); + } + + // Check for forbidden characters + const foundForbiddenChars = + CONFIG.CONTRACT_NAME_VALIDATION.FORBIDDEN_CHARS.filter((char) => + name.includes(char), + ); + + if (foundForbiddenChars.length > 0) { + errors.push({ type: 'invalidChars', chars: foundForbiddenChars }); + } + + // Check pattern match + if (!CONFIG.CONTRACT_NAME_VALIDATION.PATTERN.test(name)) { + errors.push({ type: 'invalidPattern' }); + } + + return errors; +} + +/** + * @notice Gets a human-readable error message for validation errors + * @param errors Array of validation errors + * @returns A formatted error message + */ +export function getValidationErrorMessage(errors: ValidationError[]): string { + if (errors.length === 0) return ''; + + const messages = errors.map((error) => { + switch (error.type) { + case 'empty': + return 'Contract name cannot be empty or only whitespace'; + case 'tooLong': + return `Contract name cannot exceed ${error.maxLength} characters`; + case 'invalidChars': + return `Contract name contains invalid characters: ${error.chars.join( + ' ', + )}`; + case 'invalidPattern': + return 'Contract name can only contain letters, numbers, underscores, and hyphens'; + } + }); + + return messages.join('. '); +} + +/** + * @notice Validates a contract name and throws if invalid + * @dev Combines validation and error throwing into a single function + * @param name The contract name to validate + * @throws {ContractError} If the name is invalid + */ +export function assertValidContractName(name: string): void { + const errors = validateContractName(name); + + if (errors.length > 0) { + throw new ContractError( + ErrorMessages[CONFIG.ERROR_CODES.INVALID_INPUT], + CONFIG.ERROR_CODES.INVALID_INPUT, + { + name, + reason: getValidationErrorMessage(errors), + }, + ); + } +} diff --git a/starknet/tsconfig.json b/starknet/tsconfig.json new file mode 100644 index 00000000000..62ca1a78920 --- /dev/null +++ b/starknet/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "importHelpers": true, + "noEmitHelpers": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "**/*.spec.ts", "/cairo"], + "ts-node": { + "experimentalSpecifierResolution": "node", + "experimentalResolver": true, + "files": true + } +} diff --git a/yarn.lock b/yarn.lock index d495d04a09c..d7497df915f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6575,6 +6575,13 @@ __metadata: languageName: node linkType: hard +"@eslint-community/regexpp@npm:^4.11.0": + version: 4.12.1 + resolution: "@eslint-community/regexpp@npm:4.12.1" + checksum: c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc + languageName: node + linkType: hard + "@eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": version: 4.10.0 resolution: "@eslint-community/regexpp@npm:4.10.0" @@ -6582,6 +6589,24 @@ __metadata: languageName: node linkType: hard +"@eslint/config-array@npm:^0.18.0": + version: 0.18.0 + resolution: "@eslint/config-array@npm:0.18.0" + dependencies: + "@eslint/object-schema": "npm:^2.1.4" + debug: "npm:^4.3.1" + minimatch: "npm:^3.1.2" + checksum: 60ccad1eb4806710b085cd739568ec7afd289ee5af6ca0383f0876f9fe375559ef525f7b3f86bdb3f961493de952f2cf3ab4aa4a6ccaef0ae3cd688267cabcb3 + languageName: node + linkType: hard + +"@eslint/core@npm:^0.7.0": + version: 0.7.0 + resolution: "@eslint/core@npm:0.7.0" + checksum: 69227f33fddd9b402b7b0830732a6e84cae77d202cb5b56f0dbcc462882e07d00e80216b796cf2f243f5b775af3ef27545a0c439d78e66122eab71da4773b81c + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^2.1.4": version: 2.1.4 resolution: "@eslint/eslintrc@npm:2.1.4" @@ -6599,6 +6624,23 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^3.1.0": + version: 3.1.0 + resolution: "@eslint/eslintrc@npm:3.1.0" + dependencies: + ajv: "npm:^6.12.4" + debug: "npm:^4.3.2" + espree: "npm:^10.0.1" + globals: "npm:^14.0.0" + ignore: "npm:^5.2.0" + import-fresh: "npm:^3.2.1" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" + strip-json-comments: "npm:^3.1.1" + checksum: 02bf892d1397e1029209dea685e9f4f87baf643315df2a632b5f121ec7e8548a3b34f428a007234fa82772218fa8a3ac2d10328637b9ce63b7f8344035b74db3 + languageName: node + linkType: hard + "@eslint/js@npm:8.57.0": version: 8.57.0 resolution: "@eslint/js@npm:8.57.0" @@ -6606,6 +6648,29 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:9.13.0": + version: 9.13.0 + resolution: "@eslint/js@npm:9.13.0" + checksum: aa7a4c45044a6cf6e14666ecc0b56ad41c80f022bd4718620b4a7e3d892111312f4e4ac4787fd11b3bf5abdb6ff9a95fdae7e73ef790528f150d86e9be1754a2 + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/object-schema@npm:2.1.4" + checksum: 221e8d9f281c605948cd6e030874aacce83fe097f8f9c1964787037bccf08e82b7aa9eff1850a30fffac43f1d76555727ec22a2af479d91e268e89d1e035131e + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.2.0": + version: 0.2.1 + resolution: "@eslint/plugin-kit@npm:0.2.1" + dependencies: + levn: "npm:^0.4.1" + checksum: 28c409788b923a20d8839470125633eb7a865caf7d1434564fc686c9212b12055cceb1464fb87ec66eae1452ce701262c4909e0c4bc3b48e476e0dc977df0760 + languageName: node + linkType: hard + "@eth-optimism/contracts-bedrock@npm:0.16.2": version: 0.16.2 resolution: "@eth-optimism/contracts-bedrock@npm:0.16.2" @@ -7771,6 +7836,23 @@ __metadata: languageName: node linkType: hard +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 270d936be483ab5921702623bc74ce394bf12abbf57d9145a69e8a0d1c87eb1c768bd2d93af16c5705041e257e6d9cc7529311f63a1349f3678abc776fc28523 + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.5": + version: 0.16.6 + resolution: "@humanfs/node@npm:0.16.6" + dependencies: + "@humanfs/core": "npm:^0.19.1" + "@humanwhocodes/retry": "npm:^0.3.0" + checksum: 6d43c6727463772d05610aa05c83dab2bfbe78291022ee7a92cb50999910b8c720c76cc312822e2dea2b497aa1b3fef5fe9f68803fc45c9d4ed105874a65e339 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" @@ -7796,6 +7878,13 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/retry@npm:^0.3.0, @humanwhocodes/retry@npm:^0.3.1": + version: 0.3.1 + resolution: "@humanwhocodes/retry@npm:0.3.1" + checksum: eb457f699529de7f07649679ec9e0353055eebe443c2efe71c6dd950258892475a038e13c6a8c5e13ed1fb538cdd0a8794faa96b24b6ffc4c87fb1fc9f70ad7f + languageName: node + linkType: hard + "@hyperlane-xyz/ccip-server@workspace:typescript/ccip-server": version: 0.0.0-use.local resolution: "@hyperlane-xyz/ccip-server@workspace:typescript/ccip-server" @@ -8100,6 +8189,20 @@ __metadata: languageName: unknown linkType: soft +"@hyperlane-xyz/starknet-core@workspace:starknet": + version: 0.0.0-use.local + resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" + dependencies: + "@types/node": "npm:^22.7.9" + eslint: "npm:^9.13.0" + eslint-config-prettier: "npm:^9.1.0" + prettier: "npm:^3.3.3" + ts-node: "npm:^10.9.2" + tsx: "npm:^4.19.1" + typescript: "npm:5.3.3" + languageName: unknown + linkType: soft + "@hyperlane-xyz/utils@npm:5.6.2, @hyperlane-xyz/utils@workspace:typescript/utils": version: 0.0.0-use.local resolution: "@hyperlane-xyz/utils@workspace:typescript/utils" @@ -13313,6 +13416,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.6": + version: 1.0.6 + resolution: "@types/estree@npm:1.0.6" + checksum: 9d35d475095199c23e05b431bcdd1f6fec7380612aed068b14b2a08aa70494de8a9026765a5a91b1073f636fb0368f6d8973f518a31391d519e20c59388ed88d + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:^4.17.33": version: 4.19.5 resolution: "@types/express-serve-static-core@npm:4.19.5" @@ -13437,7 +13547,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -13700,6 +13810,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.7.9": + version: 22.8.1 + resolution: "@types/node@npm:22.8.1" + dependencies: + undici-types: "npm:~6.19.8" + checksum: ae969e3d956dead1422c35d568ea5d48dd124a38a1a337cbd120fec6e13cc92b45c7308f91f1139fcd2337a67d4704d5614d6a2c444b1fb268f85e9f1d24c713 + languageName: node + linkType: hard + "@types/node@npm:^8.0.0": version: 8.10.66 resolution: "@types/node@npm:8.10.66" @@ -14610,6 +14729,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.12.0": + version: 8.14.0 + resolution: "acorn@npm:8.14.0" + bin: + acorn: bin/acorn + checksum: 6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 + languageName: node + linkType: hard + "acorn@npm:^8.4.1, acorn@npm:^8.9.0": version: 8.11.3 resolution: "acorn@npm:8.11.3" @@ -19007,6 +19135,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:^8.1.0": + version: 8.1.0 + resolution: "eslint-scope@npm:8.1.0" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 4c34a12fbeb0677822a9e93e81f2027e39e6f27557c17bc1e5ff76debbd41e748c3673517561792bda9e276245f89fbfd9b0b24fcec3b33a04ee2196729b3489 + languageName: node + linkType: hard + "eslint-visitor-keys@npm:^3.3.0": version: 3.3.0 resolution: "eslint-visitor-keys@npm:3.3.0" @@ -19028,6 +19166,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^4.1.0": + version: 4.1.0 + resolution: "eslint-visitor-keys@npm:4.1.0" + checksum: 3fb5bd1b2f36db89d0ac57ddd66d36ccd3b1e3cddb2a55a0f9f6f1c85268cfcc1cc32e7eda4990e3423107a120dd254fb6cb52d6154cf81d344d8c3fa671f7c2 + languageName: node + linkType: hard + "eslint@npm:^8.57.0": version: 8.57.0 resolution: "eslint@npm:8.57.0" @@ -19076,6 +19221,67 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^9.13.0": + version: 9.13.0 + resolution: "eslint@npm:9.13.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.11.0" + "@eslint/config-array": "npm:^0.18.0" + "@eslint/core": "npm:^0.7.0" + "@eslint/eslintrc": "npm:^3.1.0" + "@eslint/js": "npm:9.13.0" + "@eslint/plugin-kit": "npm:^0.2.0" + "@humanfs/node": "npm:^0.16.5" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@humanwhocodes/retry": "npm:^0.3.1" + "@types/estree": "npm:^1.0.6" + "@types/json-schema": "npm:^7.0.15" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.2" + debug: "npm:^4.3.2" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^8.1.0" + eslint-visitor-keys: "npm:^4.1.0" + espree: "npm:^10.2.0" + esquery: "npm:^1.5.0" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^8.0.0" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + text-table: "npm:^0.2.0" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: 4342cc24a8d73581676f1b4959c2ddac18ed169731d9c55b708d2eacfc066ed5bdbc2c3c129e1f70142f0704bc25884a1a9ae580e15be5921f9c7f7d0f3ebe68 + languageName: node + linkType: hard + +"espree@npm:^10.0.1, espree@npm:^10.2.0": + version: 10.2.0 + resolution: "espree@npm:10.2.0" + dependencies: + acorn: "npm:^8.12.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^4.1.0" + checksum: 365076a963ca84244c1e2d36e4f812362d21cfa7e7df10d67f7b82b759467796df81184721d153c4e235d9ef5eb5b4d044167dd66be3be00f53a21a515b1bfb1 + languageName: node + linkType: hard + "espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -19116,6 +19322,15 @@ __metadata: languageName: node linkType: hard +"esquery@npm:^1.5.0": + version: 1.6.0 + resolution: "esquery@npm:1.6.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: c587fb8ec9ed83f2b1bc97cf2f6854cc30bf784a79d62ba08c6e358bf22280d69aee12827521cf38e69ae9761d23fb7fde593ce315610f85655c139d99b05e5a + languageName: node + linkType: hard + "esrecurse@npm:^4.3.0": version: 4.3.0 resolution: "esrecurse@npm:4.3.0" @@ -19891,6 +20106,15 @@ __metadata: languageName: node linkType: hard +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" + dependencies: + flat-cache: "npm:^4.0.0" + checksum: afe55c4de4e0d226a23c1eae62a7219aafb390859122608a89fa4df6addf55c7fd3f1a2da6f5b41e7cdff496e4cf28bbd215d53eab5c817afa96d2b40c81bfb0 + languageName: node + linkType: hard + "file-system-cache@npm:2.3.0": version: 2.3.0 resolution: "file-system-cache@npm:2.3.0" @@ -20039,6 +20263,16 @@ __metadata: languageName: node linkType: hard +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.4" + checksum: 58ce851d9045fffc7871ce2bd718bc485ad7e777bf748c054904b87c351ff1080c2c11da00788d78738bfb51b71e4d5ea12d13b98eb36e3358851ffe495b62dc + languageName: node + linkType: hard + "flat@npm:^5.0.2": version: 5.0.2 resolution: "flat@npm:5.0.2" @@ -20055,6 +20289,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:^3.2.9": + version: 3.3.1 + resolution: "flatted@npm:3.3.1" + checksum: 7b8376061d5be6e0d3658bbab8bde587647f68797cf6bfeae9dea0e5137d9f27547ab92aaff3512dd9d1299086a6d61be98e9d48a56d17531b634f77faadbc49 + languageName: node + linkType: hard + "flow-parser@npm:0.*": version: 0.242.0 resolution: "flow-parser@npm:0.242.0" @@ -20960,6 +21201,13 @@ __metadata: languageName: node linkType: hard +"globals@npm:^14.0.0": + version: 14.0.0 + resolution: "globals@npm:14.0.0" + checksum: 03939c8af95c6df5014b137cac83aa909090c3a3985caef06ee9a5a669790877af8698ab38007e4c0186873adc14c0b13764acc754b16a754c216cc56aa5f021 + languageName: node + linkType: hard + "globalthis@npm:^1.0.1, globalthis@npm:^1.0.3": version: 1.0.3 resolution: "globalthis@npm:1.0.3" @@ -23536,7 +23784,7 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": +"keyv@npm:^4.5.3, keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" dependencies: @@ -26648,6 +26896,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^3.3.3": + version: 3.3.3 + resolution: "prettier@npm:3.3.3" + bin: + prettier: bin/prettier.cjs + checksum: 5beac1f30b5b40162532b8e2f7c3a4eb650910a2695e9c8512a62ffdc09dae93190c29db9107fa7f26d1b6c71aad3628ecb9b5de1ecb0911191099be109434d7 + languageName: node + linkType: hard + "pretty-format@npm:^27.0.2": version: 27.5.1 resolution: "pretty-format@npm:27.5.1" @@ -30465,7 +30722,7 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.8.0": +"ts-node@npm:^10.8.0, ts-node@npm:^10.9.2": version: 10.9.2 resolution: "ts-node@npm:10.9.2" dependencies: @@ -30958,7 +31215,7 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.2": +"undici-types@npm:~6.19.2, undici-types@npm:~6.19.8": version: 6.19.8 resolution: "undici-types@npm:6.19.8" checksum: cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 From c0f840185d9113ef7d8819cf1d4711ce34571211 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:41:17 +0330 Subject: [PATCH 004/132] feat: build starknet core and import on sdk --- starknet/build.sh | 17 ----------------- starknet/package.json | 6 +++++- starknet/src/index.ts | 10 +++++----- typescript/sdk/package.json | 1 + typescript/sdk/src/deploy/StarknetDeployer.ts | 5 ++++- yarn.lock | 4 +++- 6 files changed, 18 insertions(+), 25 deletions(-) delete mode 100755 starknet/build.sh diff --git a/starknet/build.sh b/starknet/build.sh deleted file mode 100755 index 44925e9d346..00000000000 --- a/starknet/build.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -# Navigate to the cairo directory -cd cairo - -# Build the project using scarb -# Uncomment the next line if you want to enable the build process -scarb build || { echo "Scarb build failed"; exit 1; } - -# Copy the build output to the sdk directory -cp -R ./target ../src/target || { echo "Failed to copy target directory"; exit 1; } - -# Navigate back -cd ../ - -# Build the project -yarn build || { echo "Yarn build failed"; exit 1; } \ No newline at end of file diff --git a/starknet/package.json b/starknet/package.json index 6db59f816af..5bf833fdb2e 100644 --- a/starknet/package.json +++ b/starknet/package.json @@ -7,7 +7,8 @@ "author": "", "license": "Apache-2.0", "scripts": { - "build": "./build.sh && tsc && cp -R ./src/target ./dist/target" + "compile": "cd cairo && scarb build && cp -R ./target ../src/target && cd ..", + "build": "yarn compile && tsc && cp -R ./src/target ./dist/target" }, "devDependencies": { "@types/node": "^22.7.9", @@ -29,5 +30,8 @@ ], "engines": { "node": ">=16" + }, + "dependencies": { + "starknet": "6.11.0" } } diff --git a/starknet/src/index.ts b/starknet/src/index.ts index 31595d5003b..74728e62e5f 100644 --- a/starknet/src/index.ts +++ b/starknet/src/index.ts @@ -1,10 +1,10 @@ import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; +import { CairoAssembly, CompiledContract } from 'starknet'; import { fileURLToPath } from 'url'; import { CONFIG } from './config.js'; import { ContractError, ErrorMessages } from './errors.js'; -import { CompiledContractCasm, ContractData } from './types.js'; import { assertValidContractName } from './utils.js'; const currentDirectory = dirname(fileURLToPath(import.meta.url)); @@ -14,10 +14,10 @@ const TARGET_DEV_PATH = join(currentDirectory, CONFIG.PATHS.MAIN); * @notice Retrieves and parses the standard compiled contract data * @dev Reads the contract file with STANDARD suffix and parses it as JSON * @param name The name of the contract to retrieve - * @returns {ContractData} The parsed contract data + * @returns {CompiledContract} The parsed contract data * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid */ -export const getCompiledContract = (name: string): ContractData => { +export const getCompiledContract = (name: string): CompiledContract => { try { return JSON.parse( readFileSync(findContractFile(name, 'STANDARD'), 'utf-8'), @@ -39,10 +39,10 @@ export const getCompiledContract = (name: string): ContractData => { * @notice Retrieves and parses the CASM compiled contract data * @dev Reads the contract file with COMPILED suffix and parses it as JSON * @param name The name of the contract to retrieve - * @returns {CompiledContractCasm} The parsed CASM contract data + * @returns {CairoAssembly} The parsed CASM contract data * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid */ -export const getCompiledContractCasm = (name: string): CompiledContractCasm => { +export const getCompiledContractCasm = (name: string): CairoAssembly => { try { return JSON.parse( readFileSync(findContractFile(name, 'COMPILED'), 'utf-8'), diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 14a773f61dd..37ec311ee61 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -8,6 +8,7 @@ "@cosmjs/cosmwasm-stargate": "^0.32.4", "@cosmjs/stargate": "^0.32.4", "@hyperlane-xyz/core": "5.6.1", + "@hyperlane-xyz/starknet-core": "1.0.0", "@hyperlane-xyz/utils": "5.6.2", "@safe-global/api-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0", diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index d84dd1355fa..f51cf662f7b 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -7,8 +7,11 @@ import { ContractFactory, ContractFactoryParams, } from 'starknet'; -import { getCompiledContract, getCompiledContractCasm } from 'starknet-core'; +import { + getCompiledContract, + getCompiledContractCasm, +} from '@hyperlane-xyz/starknet-core'; import { rootLogger } from '@hyperlane-xyz/utils'; export interface StarknetContractConfig { diff --git a/yarn.lock b/yarn.lock index d7497df915f..713f3323ddd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8147,6 +8147,7 @@ __metadata: "@cosmjs/cosmwasm-stargate": "npm:^0.32.4" "@cosmjs/stargate": "npm:^0.32.4" "@hyperlane-xyz/core": "npm:5.6.1" + "@hyperlane-xyz/starknet-core": "npm:1.0.0" "@hyperlane-xyz/utils": "npm:5.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" @@ -8189,7 +8190,7 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/starknet-core@workspace:starknet": +"@hyperlane-xyz/starknet-core@npm:1.0.0, @hyperlane-xyz/starknet-core@workspace:starknet": version: 0.0.0-use.local resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" dependencies: @@ -8197,6 +8198,7 @@ __metadata: eslint: "npm:^9.13.0" eslint-config-prettier: "npm:^9.1.0" prettier: "npm:^3.3.3" + starknet: "npm:6.11.0" ts-node: "npm:^10.9.2" tsx: "npm:^4.19.1" typescript: "npm:5.3.3" From 8e4c75495d3d36abb1f372f74a9075d05003d278 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:18:12 +0330 Subject: [PATCH 005/132] minor: fix starknet contracts name prefix --- starknet/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starknet/src/index.ts b/starknet/src/index.ts index 74728e62e5f..eb1798f02fd 100644 --- a/starknet/src/index.ts +++ b/starknet/src/index.ts @@ -73,7 +73,7 @@ function findContractFile( suffix: keyof typeof CONFIG.SUFFIXES, ): string { assertValidContractName(name); - const mainPath = `${TARGET_DEV_PATH}/${name}${CONFIG.SUFFIXES[suffix]}`; + const mainPath = `${TARGET_DEV_PATH}/contracts_${name}${CONFIG.SUFFIXES[suffix]}`; if (!existsSync(mainPath)) { throw new ContractError( From 877474775cdb7c0c03c38d1bd6f0f8d9f4291a66 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:18:46 +0330 Subject: [PATCH 006/132] fix: do not write starknet deployments in seprated files --- typescript/sdk/src/deploy/StarknetDeployer.ts | 61 ++----------------- 1 file changed, 4 insertions(+), 57 deletions(-) diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index f51cf662f7b..2d66deea838 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -1,5 +1,3 @@ -import fs from 'fs'; -import path from 'path'; import { Logger } from 'pino'; import { Account, @@ -34,18 +32,15 @@ export interface StarknetDeployerOptions { export class StarknetDeployer { private readonly logger: Logger; - private readonly deploymentsDir: string; - private readonly configsDir: string; private readonly deployedContracts: Record = {}; constructor( private readonly account: Account, + private readonly config: StarknetDeployConfig, private readonly options: StarknetDeployerOptions = {}, ) { this.logger = options.logger ?? rootLogger.child({ module: 'starknet-deployer' }); - this.deploymentsDir = options.deploymentsDir ?? 'deployments'; - this.configsDir = options.configsDir ?? 'configs'; } private processConstructorArgs( @@ -74,39 +69,6 @@ export class StarknetDeployer { }, {} as any); } - private ensureNetworkDirectory(network: string): string { - if (!network) { - throw new Error('Network must be specified'); - } - - const networkDir = path.join(this.deploymentsDir, network); - if (!fs.existsSync(this.deploymentsDir)) { - fs.mkdirSync(this.deploymentsDir); - } - if (!fs.existsSync(networkDir)) { - fs.mkdirSync(networkDir); - } - - return networkDir; - } - - private getConfigPath(network: string): string { - if (!network) { - throw new Error('Network must be specified'); - } - - const configFileName = `${network.toLowerCase()}.json`; - const configPath = path.join(this.configsDir, configFileName); - - if (!fs.existsSync(configPath)) { - throw new Error( - `Config file not found for network ${network} at ${configPath}`, - ); - } - - return configPath; - } - async deployContract( contractName: string, constructorArgs: StarknetContractConfig['constructor'], @@ -141,20 +103,12 @@ export class StarknetDeployer { return address; } - async deploy(network: string): Promise> { + async deploy(): Promise> { try { - const configPath = this.getConfigPath(network); - const config: StarknetDeployConfig = JSON.parse( - fs.readFileSync(configPath, 'utf-8'), - ); - - const networkDir = this.ensureNetworkDirectory(network); - const deploymentsFile = path.join(networkDir, 'deployments.json'); - - for (const contractName of config.deploymentOrder) { + for (const contractName of this.config.deploymentOrder) { await this.deployContract( contractName, - config.contracts[contractName].constructor, + this.config.contracts[contractName].constructor, ); } @@ -163,13 +117,6 @@ export class StarknetDeployer { this.deployedContracts, ); - // Write deployments to network-specific file - fs.writeFileSync( - deploymentsFile, - JSON.stringify(this.deployedContracts, null, 2), - ); - this.logger.info(`Deployed contracts saved to ${deploymentsFile}`); - return this.deployedContracts; } catch (error) { this.logger.error('Deployment failed:', error); From 0569af5bcfcf60a2ac6dfa0e1f05db19fe31c531 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:19:03 +0330 Subject: [PATCH 007/132] feat: starknet core module base --- typescript/sdk/src/core/StarknetCoreModule.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 typescript/sdk/src/core/StarknetCoreModule.ts diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts new file mode 100644 index 00000000000..638c26afbdf --- /dev/null +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -0,0 +1,46 @@ +import { Contract, RpcProvider } from 'starknet'; + +import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; +import { Domain, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; + +import { MultiProvider } from '../providers/MultiProvider.js'; + +import { + HyperlaneModule, + HyperlaneModuleParams, +} from './AbstractHyperlaneModule.js'; +import { EvmCoreReader } from './EvmCoreReader.js'; +import { DeployedCoreAddresses } from './schemas.js'; +import { CoreConfig } from './types.js'; + +export class StarknetCoreModule extends HyperlaneModule< + ProtocolType.Ethereum, + CoreConfig, + DeployedCoreAddresses +> { + protected logger = rootLogger.child({ module: 'EvmCoreModule' }); + protected coreReader: EvmCoreReader; + public readonly chainName: string; + + // We use domainId here because MultiProvider.getDomainId() will always + // return a number, and EVM the domainId and chainId are the same. + public readonly domainId: Domain; + + constructor( + protected readonly multiProvider: MultiProvider, + args: HyperlaneModuleParams, + ) { + super(args); + this.coreReader = new EvmCoreReader(multiProvider, this.args.chain); + this.chainName = this.multiProvider.getChainName(this.args.chain); + this.domainId = multiProvider.getDomainId(args.chain); + } + + public read(address: string): Promise { + const mailboxContract = getCompiledContract('mailbox'); + const provider = new RpcProvider({ + nodeUrl: '', + }); + const mailbox = new Contract(mailboxContract.abi, address, provider); + } +} From 626e7f3d78767bcd30a3f7bd4a2c2fb8188da9f3 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:47:20 +0330 Subject: [PATCH 008/132] minor: simplify starknet deployer --- typescript/sdk/src/deploy/StarknetDeployer.ts | 83 +------------------ 1 file changed, 4 insertions(+), 79 deletions(-) diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index 2d66deea838..271264b6d4f 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -12,73 +12,23 @@ import { } from '@hyperlane-xyz/starknet-core'; import { rootLogger } from '@hyperlane-xyz/utils'; -export interface StarknetContractConfig { - name: string; - constructor: Record; -} - -export interface StarknetDeployConfig { - contracts: Record; - deploymentOrder: string[]; -} - -export interface StarknetDeployerOptions { - logger?: Logger; - deploymentsDir?: string; - configsDir?: string; - accountAddress?: string; - network?: string; -} - export class StarknetDeployer { private readonly logger: Logger; private readonly deployedContracts: Record = {}; - constructor( - private readonly account: Account, - private readonly config: StarknetDeployConfig, - private readonly options: StarknetDeployerOptions = {}, - ) { - this.logger = - options.logger ?? rootLogger.child({ module: 'starknet-deployer' }); - } - - private processConstructorArgs( - args: Record, - ): any { - return Object.entries(args).reduce((acc, [key, { type, value }]) => { - if (typeof value === 'string' && value.startsWith('$')) { - if (value === '$OWNER_ADDRESS') { - acc[key] = this.options.accountAddress; - } else if (value === '$BENEFICIARY_ADDRESS') { - acc[key] = process.env.BENEFICIARY_ADDRESS; - } else { - const contractName = value.slice(1); - if (this.deployedContracts[contractName]) { - acc[key] = this.deployedContracts[contractName]; - } else { - throw new Error( - `Contract ${contractName} not yet deployed, required for ${key}`, - ); - } - } - } else { - acc[key] = value; - } - return acc; - }, {} as any); + constructor(private readonly account: Account) { + this.logger = rootLogger.child({ module: 'starknet-deployer' }); } async deployContract( contractName: string, - constructorArgs: StarknetContractConfig['constructor'], + constructorArgs: Record, ): Promise { this.logger.info(`Deploying contract ${contractName}...`); const compiledContract = getCompiledContract(contractName); const casm = getCompiledContractCasm(contractName); - const processedArgs = this.processConstructorArgs(constructorArgs); - const constructorCalldata = CallData.compile(processedArgs); + const constructorCalldata = CallData.compile(constructorArgs); const params: ContractFactoryParams = { compiledContract, @@ -102,29 +52,4 @@ export class StarknetDeployer { return address; } - - async deploy(): Promise> { - try { - for (const contractName of this.config.deploymentOrder) { - await this.deployContract( - contractName, - this.config.contracts[contractName].constructor, - ); - } - - this.logger.info( - 'All contracts deployed successfully:', - this.deployedContracts, - ); - - return this.deployedContracts; - } catch (error) { - this.logger.error('Deployment failed:', error); - throw error; - } - } - - getDeployedContracts(): Record { - return { ...this.deployedContracts }; - } } From 98a59fefe27698163c8681eb4ead3cbcf6063c17 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:42:31 +0330 Subject: [PATCH 009/132] feat: make starknet deployer usable for cli use cases --- typescript/cli/package.json | 1 + typescript/sdk/src/deploy/StarknetDeployer.ts | 3 ++- typescript/sdk/src/index.ts | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/typescript/cli/package.json b/typescript/cli/package.json index 50d2d4f49cf..c85a0d22445 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -17,6 +17,7 @@ "chalk": "^5.3.0", "ethers": "^5.7.2", "latest-version": "^8.0.0", + "starknet": "6.11.0", "terminal-link": "^3.0.0", "tsx": "^4.7.1", "yaml": "2.4.5", diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index 271264b6d4f..7fe8f1808d2 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -4,6 +4,7 @@ import { CallData, ContractFactory, ContractFactoryParams, + RawArgs, } from 'starknet'; import { @@ -22,7 +23,7 @@ export class StarknetDeployer { async deployContract( contractName: string, - constructorArgs: Record, + constructorArgs: RawArgs, ): Promise { this.logger.info(`Deploying contract ${contractName}...`); diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 4688acd28ca..bb971a1380c 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -92,6 +92,7 @@ export { DeployerOptions, HyperlaneDeployer, } from './deploy/HyperlaneDeployer.js'; +export { StarknetDeployer } from './deploy/StarknetDeployer.js'; export { HyperlaneProxyFactoryDeployer } from './deploy/HyperlaneProxyFactoryDeployer.js'; export { CheckerViolation, From 8be1d66f131b4e779129344960935f3c705aae33 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:32:48 +0330 Subject: [PATCH 010/132] feat: define starknet as a protocol --- typescript/sdk/src/providers/ProviderType.ts | 50 +++++++++++++++++-- .../sdk/src/providers/explorerHealthTest.ts | 2 + .../sdk/src/providers/providerBuilders.ts | 15 +++++- typescript/sdk/src/token/TokenStandard.ts | 2 + typescript/utils/src/types.ts | 2 + yarn.lock | 1 + 6 files changed, 67 insertions(+), 5 deletions(-) diff --git a/typescript/sdk/src/providers/ProviderType.ts b/typescript/sdk/src/providers/ProviderType.ts index c74a9995d48..4dc5a1c6f97 100644 --- a/typescript/sdk/src/providers/ProviderType.ts +++ b/typescript/sdk/src/providers/ProviderType.ts @@ -15,6 +15,12 @@ import type { providers as EV5Providers, PopulatedTransaction as EV5Transaction, } from 'ethers'; +import { + Contract as StarknetContract, + RpcProvider as StarknetProvider, + ReceiptTx as StarknetReceiptTx, + V3TransactionDetails as StarknetTransaction, +} from 'starknet'; import type { GetContractReturnType, PublicClient, @@ -31,6 +37,7 @@ export enum ProviderType { CosmJs = 'cosmjs', CosmJsWasm = 'cosmjs-wasm', GnosisTxBuilder = 'gnosis-txBuilder', + Starknet = 'starknet', } export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< @@ -40,6 +47,7 @@ export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< [ProtocolType.Ethereum]: ProviderType.EthersV5, [ProtocolType.Sealevel]: ProviderType.SolanaWeb3, [ProtocolType.Cosmos]: ProviderType.CosmJsWasm, + [ProtocolType.Starknet]: ProviderType.Starknet, }; export type ProviderMap = Partial>; @@ -63,6 +71,12 @@ type ProtocolTypesMapping = { contract: CosmJsWasmContract; receipt: CosmJsWasmTransactionReceipt; }; + [ProtocolType.Starknet]: { + transaction: StarknetJsTransaction; + provider: StarknetJsProvider; + contract: StarknetJsContract; + receipt: StarknetJsTransactionReceipt; + }; }; type ProtocolTyped< @@ -124,13 +138,20 @@ export interface CosmJsWasmProvider provider: Promise; } +export interface StarknetJsProvider + extends TypedProviderBase { + type: ProviderType.Starknet; + provider: StarknetProvider; +} + export type TypedProvider = | EthersV5Provider // | EthersV6Provider | ViemProvider | SolanaWeb3Provider | CosmJsProvider - | CosmJsWasmProvider; + | CosmJsWasmProvider + | StarknetJsProvider; /** * Contracts with discriminated union of provider type @@ -169,13 +190,20 @@ export interface CosmJsWasmContract contract: CosmWasmContract; } +export interface StarknetJsContract + extends TypedContractBase { + type: ProviderType.Starknet; + contract: StarknetContract; +} + export type TypedContract = | EthersV5Contract // | EthersV6Contract | ViemContract | SolanaWeb3Contract | CosmJsContract - | CosmJsWasmContract; + | CosmJsWasmContract + | StarknetJsContract; /** * Transactions with discriminated union of provider type @@ -218,13 +246,20 @@ export interface CosmJsWasmTransaction transaction: ExecuteInstruction; } +export interface StarknetJsTransaction + extends TypedTransactionBase { + type: ProviderType.Starknet; + transaction: StarknetTransaction; +} + export type TypedTransaction = | EthersV5Transaction // | EthersV6Transaction | ViemTransaction | SolanaWeb3Transaction | CosmJsTransaction - | CosmJsWasmTransaction; + | CosmJsWasmTransaction + | StarknetJsTransaction; /** * Transaction receipt/response with discriminated union of provider type @@ -265,9 +300,16 @@ export interface CosmJsWasmTransactionReceipt receipt: DeliverTxResponse; } +export interface StarknetJsTransactionReceipt + extends TypedTransactionReceiptBase { + type: ProviderType.Starknet; + receipt: StarknetReceiptTx; +} + export type TypedTransactionReceipt = | EthersV5TransactionReceipt | ViemTransactionReceipt | SolanaWeb3TransactionReceipt | CosmJsTransactionReceipt - | CosmJsWasmTransactionReceipt; + | CosmJsWasmTransactionReceipt + | StarknetJsTransactionReceipt; diff --git a/typescript/sdk/src/providers/explorerHealthTest.ts b/typescript/sdk/src/providers/explorerHealthTest.ts index 4e6d8be3ff8..ed21679f41d 100644 --- a/typescript/sdk/src/providers/explorerHealthTest.ts +++ b/typescript/sdk/src/providers/explorerHealthTest.ts @@ -11,6 +11,8 @@ const PROTOCOL_TO_ADDRESS: Record = { [ProtocolType.Ethereum]: '0x0000000000000000000000000000000000000000', [ProtocolType.Sealevel]: '11111111111111111111111111111111', [ProtocolType.Cosmos]: 'cosmos100000000000000000000000000000000000000', + [ProtocolType.Starknet]: + '0x4d98390d383cfafe1f13f516783b32e988a95505ee85a390d2170c7297e565b', }; const PROTOCOL_TO_TX_HASH: Partial> = { diff --git a/typescript/sdk/src/providers/providerBuilders.ts b/typescript/sdk/src/providers/providerBuilders.ts index bc8051b1baa..cf710e651df 100644 --- a/typescript/sdk/src/providers/providerBuilders.ts +++ b/typescript/sdk/src/providers/providerBuilders.ts @@ -2,6 +2,7 @@ import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'; import { StargateClient } from '@cosmjs/stargate'; import { Connection } from '@solana/web3.js'; import { providers } from 'ethers'; +import { RpcProvider as StarknetRpcProvider } from 'starknet'; import { createPublicClient, http } from 'viem'; import { ProtocolType, isNumeric } from '@hyperlane-xyz/utils'; @@ -14,6 +15,7 @@ import { EthersV5Provider, ProviderType, SolanaWeb3Provider, + StarknetJsProvider, TypedProvider, ViemProvider, } from './ProviderType.js'; @@ -109,6 +111,15 @@ export function defaultCosmJsWasmProviderBuilder( }; } +export function defaultStarknetJsProviderBuilder( + rpcUrls: RpcUrl[], +): StarknetJsProvider { + const provider = new StarknetRpcProvider({ + nodeUrl: rpcUrls[0].http, + }); + return { provider, type: ProviderType.Starknet }; +} + // Kept for backwards compatibility export function defaultProviderBuilder( rpcUrls: RpcUrl[], @@ -127,7 +138,8 @@ export const defaultProviderBuilderMap: ProviderBuilderMap = { [ProviderType.Viem]: defaultViemProviderBuilder, [ProviderType.SolanaWeb3]: defaultSolProviderBuilder, [ProviderType.CosmJs]: defaultCosmJsProviderBuilder, - [ProviderType.CosmJsWasm]: defaultCosmJsWasmProviderBuilder, + [ProviderType.CosmJsWasm]: defaultStarknetJsProviderBuilder, + [ProviderType.Starknet]: defaultStarknetJsProviderBuilder, }; export const protocolToDefaultProviderBuilder: Record< @@ -137,4 +149,5 @@ export const protocolToDefaultProviderBuilder: Record< [ProtocolType.Ethereum]: defaultEthersV5ProviderBuilder, [ProtocolType.Sealevel]: defaultSolProviderBuilder, [ProtocolType.Cosmos]: defaultCosmJsWasmProviderBuilder, + [ProtocolType.Starknet]: defaultStarknetJsProviderBuilder, }; diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index ee434b77752..185ed4a8e3a 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -169,4 +169,6 @@ export const PROTOCOL_TO_NATIVE_STANDARD: Record = [ProtocolType.Ethereum]: TokenStandard.EvmNative, [ProtocolType.Cosmos]: TokenStandard.CosmosNative, [ProtocolType.Sealevel]: TokenStandard.SealevelNative, + // Starknet Todo: define starknet token types based on cairo contracts + [ProtocolType.Starknet]: TokenStandard.EvmNative, }; diff --git a/typescript/utils/src/types.ts b/typescript/utils/src/types.ts index 3d79f0058d6..9bf068f7157 100644 --- a/typescript/utils/src/types.ts +++ b/typescript/utils/src/types.ts @@ -5,6 +5,7 @@ export enum ProtocolType { Ethereum = 'ethereum', Sealevel = 'sealevel', Cosmos = 'cosmos', + Starknet = 'starknet', } // A type that also allows for literal values of the enum export type ProtocolTypeValue = `${ProtocolType}`; @@ -13,6 +14,7 @@ export const ProtocolSmallestUnit = { [ProtocolType.Ethereum]: 'wei', [ProtocolType.Sealevel]: 'lamports', [ProtocolType.Cosmos]: 'uATOM', + [ProtocolType.Starknet]: 'wei', }; /********* BASIC TYPES *********/ diff --git a/yarn.lock b/yarn.lock index 03698aac108..f57e51dd9ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7936,6 +7936,7 @@ __metadata: latest-version: "npm:^8.0.0" mocha: "npm:^10.2.0" prettier: "npm:^2.8.8" + starknet: "npm:6.11.0" terminal-link: "npm:^3.0.0" tsx: "npm:^4.7.1" typescript: "npm:5.3.3" From fedf478c7ae40c8b58a2f10df8733511e5940914 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:56:47 +0330 Subject: [PATCH 011/132] feat: pow deploying starknet contract with cli --- typescript/cli/src/deploy/core.ts | 19 +++++++ typescript/cli/src/deploy/utils.ts | 9 +++- typescript/sdk/src/core/StarknetCoreModule.ts | 50 ++++--------------- typescript/sdk/src/index.ts | 1 + 4 files changed, 38 insertions(+), 41 deletions(-) diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index ec1d6b27184..8b071240a18 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -1,3 +1,4 @@ +import { Account, RpcProvider } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; @@ -9,7 +10,9 @@ import { CoreConfig, EvmCoreModule, ExplorerLicenseType, + StarknetCoreModule, } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js'; import { requestAndSaveApiKeys } from '../context/context.js'; @@ -62,6 +65,22 @@ export async function runCoreDeploy(params: DeployParams) { ); } + if (multiProvider.tryGetProtocol(chain) === ProtocolType.Starknet) { + const provider = new RpcProvider({ + nodeUrl: 'http://127.0.0.1:5050', + }); + const account = new Account( + provider, + '0x4acc9b79dae485fb71f309f5b62501a1329789f4418bb4c25353ad5617be4d4', + '0x000000000000000000000000000000002f663fafebbee32e0698f7e13f886c73', + ); + const starknetCoreModule = new StarknetCoreModule(account); + const deployments = await starknetCoreModule.deploy(); + console.log(deployments); + logGreen('✅ Core contract deployments complete:\n'); + return; + } + let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry); diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index d8ced32dc70..00643e16643 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -47,8 +47,13 @@ export async function runPreflightChecksForChains({ for (const chain of chains) { const metadata = multiProvider.tryGetChainMetadata(chain); if (!metadata) throw new Error(`No chain config found for ${chain}`); - if (metadata.protocol !== ProtocolType.Ethereum) - throw new Error('Only Ethereum chains are supported for now'); + if ( + metadata.protocol !== ProtocolType.Ethereum && + metadata.protocol !== ProtocolType.Starknet + ) + throw new Error( + 'Only Ethereum and Starknet chains are supported for now', + ); } logGreen('✅ Chains are valid'); diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 638c26afbdf..81d986c90c0 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -1,46 +1,18 @@ -import { Contract, RpcProvider } from 'starknet'; +import { Account } from 'starknet'; -import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; -import { Domain, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; +import { rootLogger } from '@hyperlane-xyz/utils'; -import { MultiProvider } from '../providers/MultiProvider.js'; +import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; -import { - HyperlaneModule, - HyperlaneModuleParams, -} from './AbstractHyperlaneModule.js'; -import { EvmCoreReader } from './EvmCoreReader.js'; -import { DeployedCoreAddresses } from './schemas.js'; -import { CoreConfig } from './types.js'; - -export class StarknetCoreModule extends HyperlaneModule< - ProtocolType.Ethereum, - CoreConfig, - DeployedCoreAddresses -> { - protected logger = rootLogger.child({ module: 'EvmCoreModule' }); - protected coreReader: EvmCoreReader; - public readonly chainName: string; - - // We use domainId here because MultiProvider.getDomainId() will always - // return a number, and EVM the domainId and chainId are the same. - public readonly domainId: Domain; - - constructor( - protected readonly multiProvider: MultiProvider, - args: HyperlaneModuleParams, - ) { - super(args); - this.coreReader = new EvmCoreReader(multiProvider, this.args.chain); - this.chainName = this.multiProvider.getChainName(this.args.chain); - this.domainId = multiProvider.getDomainId(args.chain); +export class StarknetCoreModule { + protected logger = rootLogger.child({ module: 'StarknetCoreModule' }); + protected deployer: StarknetDeployer; + constructor(protected readonly signer: Account) { + this.deployer = new StarknetDeployer(signer); } - public read(address: string): Promise { - const mailboxContract = getCompiledContract('mailbox'); - const provider = new RpcProvider({ - nodeUrl: '', - }); - const mailbox = new Contract(mailboxContract.abi, address, provider); + async deploy() { + const NoopISM = await this.deployer.deployContract('noop_ism', []); + return { NoopISM }; } } diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index bb971a1380c..d775d6c7b76 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -535,6 +535,7 @@ export { } from './utils/gnosisSafe.js'; export { EvmCoreModule } from './core/EvmCoreModule.js'; +export { StarknetCoreModule } from './core/StarknetCoreModule.js'; export { proxyAdmin, isProxy, From f75b15bf878fd9d00a72758054175368e8fce8fd Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:39:01 +0330 Subject: [PATCH 012/132] feat: deploy ism in core deployment --- typescript/cli/src/deploy/core.ts | 5 +- typescript/sdk/src/core/StarknetCoreModule.ts | 21 ++++- typescript/sdk/src/deploy/StarknetDeployer.ts | 85 ++++++++++++++++++- typescript/sdk/src/ism/starknet-utils.ts | 24 ++++++ typescript/sdk/src/ism/types.ts | 4 + 5 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 typescript/sdk/src/ism/starknet-utils.ts diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 8b071240a18..6ddaf0539c6 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -75,7 +75,10 @@ export async function runCoreDeploy(params: DeployParams) { '0x000000000000000000000000000000002f663fafebbee32e0698f7e13f886c73', ); const starknetCoreModule = new StarknetCoreModule(account); - const deployments = await starknetCoreModule.deploy(); + const deployments = await starknetCoreModule.deploy({ + chain, + config, + }); console.log(deployments); logGreen('✅ Core contract deployments complete:\n'); return; diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 81d986c90c0..cabafe119b0 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -3,6 +3,9 @@ import { Account } from 'starknet'; import { rootLogger } from '@hyperlane-xyz/utils'; import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; +import { ChainNameOrId } from '../types.js'; + +import { CoreConfig } from './types.js'; export class StarknetCoreModule { protected logger = rootLogger.child({ module: 'StarknetCoreModule' }); @@ -11,8 +14,20 @@ export class StarknetCoreModule { this.deployer = new StarknetDeployer(signer); } - async deploy() { - const NoopISM = await this.deployer.deployContract('noop_ism', []); - return { NoopISM }; + async deploy(params: { + config: CoreConfig; + chain: ChainNameOrId; + }): Promise<{ mailbox: string; ism: string }> { + const { config, chain } = params; + + const ism = await this.deployer.deployIsm({ + chain: chain.toString(), + ismConfig: config.defaultIsm, + mailbox: '', + }); + return { + mailbox: '', + ism, + }; } } diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index 7fe8f1808d2..d497aa5343b 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -11,7 +11,18 @@ import { getCompiledContract, getCompiledContractCasm, } from '@hyperlane-xyz/starknet-core'; -import { rootLogger } from '@hyperlane-xyz/utils'; +import { Address, assert, rootLogger } from '@hyperlane-xyz/utils'; + +import { + StarknetIsmContractName, + SupportedIsmTypesOnStarknet, +} from '../ism/starknet-utils.js'; +import { + IsmConfig, + IsmType, + SupportedIsmTypesOnStarknetType, +} from '../ism/types.js'; +import { ChainName } from '../types.js'; export class StarknetDeployer { private readonly logger: Logger; @@ -53,4 +64,76 @@ export class StarknetDeployer { return address; } + + async deployIsm(params: { + chain: ChainName; + ismConfig: IsmConfig; + mailbox: Address; + }): Promise
{ + const { chain, ismConfig, mailbox } = params; + if (typeof ismConfig === 'string') return ''; + const ismType = ismConfig.type; + const logger = this.logger.child({ destination: chain, ismType }); + logger.debug(`Deploying ${ismType} to ${chain}`); + + assert( + SupportedIsmTypesOnStarknet.includes( + ismType as SupportedIsmTypesOnStarknetType, + ), + `ISM type ${ismType} is not supported on Starknet`, + ); + + const contractName = + StarknetIsmContractName[ismType as SupportedIsmTypesOnStarknetType]; + let constructorArgs: RawArgs | undefined; + switch (ismType) { + case IsmType.MERKLE_ROOT_MULTISIG: + case IsmType.MESSAGE_ID_MULTISIG: + constructorArgs = [ + this.account.address, + ismConfig.validators, + ismConfig.threshold, + ]; + + break; + case IsmType.ROUTING: + constructorArgs = [ismConfig.owner]; + + break; + case IsmType.PAUSABLE: + constructorArgs = [ismConfig.owner]; + + break; + case IsmType.AGGREGATION: + const addresses: Address[] = []; + for (const module of ismConfig.modules) { + const submodule = await this.deployIsm({ + chain, + ismConfig: module, + mailbox, + }); + addresses.push(submodule); + } + constructorArgs = [ + this.account.address, + addresses, + ismConfig.threshold, + ]; + + break; + case IsmType.TRUSTED_RELAYER: + constructorArgs = [mailbox, ismConfig.relayer]; + break; + case IsmType.FALLBACK_ROUTING: + constructorArgs = [ismConfig.owner, mailbox]; + break; + default: + constructorArgs = undefined; + } + assert( + contractName && constructorArgs, + 'ISM contract or constructor args are not provided', + ); + return this.deployContract(contractName, constructorArgs); + } } diff --git a/typescript/sdk/src/ism/starknet-utils.ts b/typescript/sdk/src/ism/starknet-utils.ts new file mode 100644 index 00000000000..6317770c84c --- /dev/null +++ b/typescript/sdk/src/ism/starknet-utils.ts @@ -0,0 +1,24 @@ +import { IsmType, SupportedIsmTypesOnStarknetType } from './types.js'; + +export const SupportedIsmTypesOnStarknet = [ + IsmType.MESSAGE_ID_MULTISIG, + IsmType.MERKLE_ROOT_MULTISIG, + IsmType.TRUSTED_RELAYER, + IsmType.ROUTING, + IsmType.PAUSABLE, + IsmType.AGGREGATION, + IsmType.FALLBACK_ROUTING, +] as const satisfies readonly IsmType[]; + +export const StarknetIsmContractName: Record< + SupportedIsmTypesOnStarknetType, + string +> = { + [IsmType.MESSAGE_ID_MULTISIG]: 'messageid_multisig_ism', + [IsmType.MERKLE_ROOT_MULTISIG]: 'merkleroot_multisig_ism', + [IsmType.TRUSTED_RELAYER]: 'trusted_relayer_ism', + [IsmType.ROUTING]: 'domain_routing_ism', + [IsmType.PAUSABLE]: 'pausable_ism', + [IsmType.AGGREGATION]: 'aggregation', + [IsmType.FALLBACK_ROUTING]: 'default_fallback_routing_ism', +}; diff --git a/typescript/sdk/src/ism/types.ts b/typescript/sdk/src/ism/types.ts index 9558af08934..8600a13b945 100644 --- a/typescript/sdk/src/ism/types.ts +++ b/typescript/sdk/src/ism/types.ts @@ -27,6 +27,7 @@ import { TrustedRelayerIsmConfigSchema, WeightedMultisigIsmConfigSchema, } from './schemas.js'; +import { SupportedIsmTypesOnStarknet } from './starknet-utils.js'; // this enum should match the IInterchainSecurityModule.sol enum // meant for the relayer @@ -158,3 +159,6 @@ export type RoutingIsmDelta = { owner?: Address; // is the owner different mailbox?: Address; // is the mailbox different (only for fallback routing) }; + +export type SupportedIsmTypesOnStarknetType = + (typeof SupportedIsmTypesOnStarknet)[number]; From 7e229dfed3fe12291b51608370cedac32eaf7b16 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:04:32 +0330 Subject: [PATCH 013/132] feat: deploy hooks and isms, in core deploy and connect them to mailbox --- typescript/sdk/src/core/StarknetCoreModule.ts | 88 +++++++++++++++++-- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index cabafe119b0..cca30f62c75 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -1,8 +1,10 @@ -import { Account } from 'starknet'; +import { Account, Contract } from 'starknet'; -import { rootLogger } from '@hyperlane-xyz/utils'; +import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; +import { assert, rootLogger } from '@hyperlane-xyz/utils'; import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; +import { HookType } from '../hook/types.js'; import { ChainNameOrId } from '../types.js'; import { CoreConfig } from './types.js'; @@ -17,17 +19,89 @@ export class StarknetCoreModule { async deploy(params: { config: CoreConfig; chain: ChainNameOrId; - }): Promise<{ mailbox: string; ism: string }> { + }): Promise> { const { config, chain } = params; + assert( + typeof config.requiredHook !== 'string', + 'string required hook is not accepted', + ); + assert( + config.requiredHook.type === HookType.PROTOCOL_FEE, + 'only protocolFee hook is accepted for required hook', + ); - const ism = await this.deployer.deployIsm({ + const noopIsm = await this.deployer.deployContract('noop_ism', []); + + // deploy hook + const hook = await this.deployer.deployContract('hook', []); + + const protocolFee = await this.deployer.deployContract('protocol_fee', [ + '1000000000000000000', + '0', + '10000000000000000', + '0', + config.requiredHook.beneficiary, + config.owner, + '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', + ]); + + // deploy mailbox + const mailbox = await this.deployer.deployContract('mailbox', [ + '888', + config.owner, + noopIsm, + hook, + protocolFee, + ]); + + const { abi } = await getCompiledContract('mailbox'); + const mailboxContract = new Contract(abi, mailbox, this.signer); + + //TODO: skip next two steps if default ism not specified + const defaultIsm = await this.deployer.deployIsm({ chain: chain.toString(), ismConfig: config.defaultIsm, - mailbox: '', + mailbox, }); + + // Updating not a deployment + console.log(`🧩 Updating default ism ${defaultIsm}..`); + const { transaction_hash: defaultIsmUpdateTxHash } = + await mailboxContract.invoke('set_default_ism', [defaultIsm]); + + await this.signer.waitForTransaction(defaultIsmUpdateTxHash); + console.log( + `⚡️ Transaction hash for updated default ism: ${defaultIsmUpdateTxHash}`, + ); + + const merkleTreeHook = await this.deployer.deployContract( + 'merkle_tree_hook', + [mailbox, config.owner], + ); + + // Updating not a deployment + console.log(`🧩 Updating required hook ${merkleTreeHook}..`); + const { transaction_hash: requiredHookUpdateTxHash } = + await mailboxContract.invoke('set_required_hook', [merkleTreeHook]); + + await this.signer.waitForTransaction(requiredHookUpdateTxHash); + console.log( + `⚡️ Transaction hash for updated required hook: ${requiredHookUpdateTxHash}`, + ); + + const validatorAnnounce = await this.deployer.deployContract( + 'validator_announce', + [mailbox, config.owner], + ); + return { - mailbox: '', - ism, + noopIsm, + hook, + mailbox, + defaultIsm, + validatorAnnounce, + protocolFee, + merkleTreeHook, }; } } From 43c28e564476558455504b91c487eac1427a4b7c Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:18:58 +0330 Subject: [PATCH 014/132] feat: deployMailbox function on starknet core module --- typescript/sdk/src/core/StarknetCoreModule.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index cca30f62c75..b6eabccb2fd 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -46,16 +46,12 @@ export class StarknetCoreModule { ]); // deploy mailbox - const mailbox = await this.deployer.deployContract('mailbox', [ - '888', + const mailboxContract = await this.deployMailbox( config.owner, noopIsm, hook, protocolFee, - ]); - - const { abi } = await getCompiledContract('mailbox'); - const mailboxContract = new Contract(abi, mailbox, this.signer); + ); //TODO: skip next two steps if default ism not specified const defaultIsm = await this.deployer.deployIsm({ @@ -97,11 +93,29 @@ export class StarknetCoreModule { return { noopIsm, hook, - mailbox, + mailbox: mailboxContract.address, defaultIsm, validatorAnnounce, protocolFee, merkleTreeHook, }; } + + async deployMailbox( + owner: string, + defaultIsm: string, + defaultHook: string, + requiredHook: string, + ) { + const mailboxAddress = await this.deployer.deployContract('mailbox', [ + '888', // TODO: put domain id here + owner, + defaultIsm, + defaultHook, + requiredHook, + ]); + + const { abi } = getCompiledContract('mailbox'); + return new Contract(abi, mailboxAddress, this.signer); + } } From df47542e4aee2bdc69aa4e96976971bc51881e4f Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:25:14 +0330 Subject: [PATCH 015/132] feat: StarknetCoreModule :: update function --- typescript/sdk/src/core/StarknetCoreModule.ts | 89 +++++++++++-------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index b6eabccb2fd..bd98c095b17 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -29,10 +29,8 @@ export class StarknetCoreModule { config.requiredHook.type === HookType.PROTOCOL_FEE, 'only protocolFee hook is accepted for required hook', ); - const noopIsm = await this.deployer.deployContract('noop_ism', []); - // deploy hook const hook = await this.deployer.deployContract('hook', []); const protocolFee = await this.deployer.deployContract('protocol_fee', [ @@ -45,7 +43,6 @@ export class StarknetCoreModule { '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', ]); - // deploy mailbox const mailboxContract = await this.deployMailbox( config.owner, noopIsm, @@ -53,51 +50,25 @@ export class StarknetCoreModule { protocolFee, ); - //TODO: skip next two steps if default ism not specified - const defaultIsm = await this.deployer.deployIsm({ - chain: chain.toString(), - ismConfig: config.defaultIsm, - mailbox, + const { defaultIsm, requiredHook } = await this.update(config, { + chain, + mailboxContract, + owner: config.owner, }); - // Updating not a deployment - console.log(`🧩 Updating default ism ${defaultIsm}..`); - const { transaction_hash: defaultIsmUpdateTxHash } = - await mailboxContract.invoke('set_default_ism', [defaultIsm]); - - await this.signer.waitForTransaction(defaultIsmUpdateTxHash); - console.log( - `⚡️ Transaction hash for updated default ism: ${defaultIsmUpdateTxHash}`, - ); - - const merkleTreeHook = await this.deployer.deployContract( - 'merkle_tree_hook', - [mailbox, config.owner], - ); - - // Updating not a deployment - console.log(`🧩 Updating required hook ${merkleTreeHook}..`); - const { transaction_hash: requiredHookUpdateTxHash } = - await mailboxContract.invoke('set_required_hook', [merkleTreeHook]); - - await this.signer.waitForTransaction(requiredHookUpdateTxHash); - console.log( - `⚡️ Transaction hash for updated required hook: ${requiredHookUpdateTxHash}`, - ); - const validatorAnnounce = await this.deployer.deployContract( 'validator_announce', - [mailbox, config.owner], + [mailboxContract.address, config.owner], ); return { noopIsm, hook, mailbox: mailboxContract.address, - defaultIsm, + defaultIsm: defaultIsm || noopIsm, validatorAnnounce, protocolFee, - merkleTreeHook, + requiredHook: requiredHook || '', }; } @@ -118,4 +89,50 @@ export class StarknetCoreModule { const { abi } = getCompiledContract('mailbox'); return new Contract(abi, mailboxAddress, this.signer); } + + async update( + expectedConfig: Partial, + args: { mailboxContract: Contract; chain: ChainNameOrId; owner: string }, + ): Promise<{ defaultIsm?: string; requiredHook?: string }> { + const result: { defaultIsm?: string; requiredHook?: string } = {}; + if (expectedConfig.defaultIsm) { + const defaultIsm = await this.deployer.deployIsm({ + chain: args.chain.toString(), + ismConfig: expectedConfig.defaultIsm, + mailbox: args.mailboxContract.address, + }); + + this.logger.trace(`Updating default ism ${defaultIsm}..`); + const { transaction_hash: defaultIsmUpdateTxHash } = + await args.mailboxContract.invoke('set_default_ism', [defaultIsm]); + + await this.signer.waitForTransaction(defaultIsmUpdateTxHash); + this.logger.trace( + `Transaction hash for updated default ism: ${defaultIsmUpdateTxHash}`, + ); + result.defaultIsm = defaultIsm; + } + + if (expectedConfig.requiredHook) { + const merkleTreeHook = await this.deployer.deployContract( + 'merkle_tree_hook', + [args.mailboxContract.address, args.owner], + ); + + this.logger.trace(`Updating required hook ${merkleTreeHook}..`); + const { transaction_hash: requiredHookUpdateTxHash } = + await args.mailboxContract.invoke('set_required_hook', [ + merkleTreeHook, + ]); + + await this.signer.waitForTransaction(requiredHookUpdateTxHash); + this.logger.trace( + `Transaction hash for updated required hook: ${requiredHookUpdateTxHash}`, + ); + + result.requiredHook = merkleTreeHook; + } + + return result; + } } From c44079657a95d60fe9e528074a4a81e6bb5f8f53 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:04:47 +0330 Subject: [PATCH 016/132] feat: StarknetCoreModule:: read function --- typescript/cli/package.json | 2 +- typescript/sdk/package.json | 2 +- typescript/sdk/src/core/StarknetCoreModule.ts | 16 +++++++++++- yarn.lock | 25 ++++++++++++++++--- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/typescript/cli/package.json b/typescript/cli/package.json index c85a0d22445..4931fa4ab80 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -17,7 +17,7 @@ "chalk": "^5.3.0", "ethers": "^5.7.2", "latest-version": "^8.0.0", - "starknet": "6.11.0", + "starknet": "6.17.0", "terminal-link": "^3.0.0", "tsx": "^4.7.1", "yaml": "2.4.5", diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 37ec311ee61..10677783f11 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -23,7 +23,7 @@ "cross-fetch": "^3.1.5", "ethers": "^5.7.2", "pino": "^8.19.0", - "starknet": "6.11.0", + "starknet": "6.17.0", "viem": "^1.20.0", "zod": "^3.21.2" }, diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index bd98c095b17..3e10f9d545d 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -1,4 +1,4 @@ -import { Account, Contract } from 'starknet'; +import { Account, Contract, num } from 'starknet'; import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; import { assert, rootLogger } from '@hyperlane-xyz/utils'; @@ -95,6 +95,7 @@ export class StarknetCoreModule { args: { mailboxContract: Contract; chain: ChainNameOrId; owner: string }, ): Promise<{ defaultIsm?: string; requiredHook?: string }> { const result: { defaultIsm?: string; requiredHook?: string } = {}; + if (expectedConfig.defaultIsm) { const defaultIsm = await this.deployer.deployIsm({ chain: args.chain.toString(), @@ -135,4 +136,17 @@ export class StarknetCoreModule { return result; } + + //TODO: return CoreConfig by fetching each contract details + async read(mailboxContract: Contract) { + const [defaultIsm, defaultHook, requiredHook, owner] = ( + await Promise.all([ + mailboxContract.call('get_default_ism', [], { parseResponse: true }), + mailboxContract.call('get_default_hook'), + mailboxContract.call('get_required_hook'), + mailboxContract.call('owner'), + ]) + ).map((res) => num.toHex64(res.toString())); + return { defaultIsm, defaultHook, requiredHook, owner }; + } } diff --git a/yarn.lock b/yarn.lock index f57e51dd9ef..ce8439b84b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7936,7 +7936,7 @@ __metadata: latest-version: "npm:^8.0.0" mocha: "npm:^10.2.0" prettier: "npm:^2.8.8" - starknet: "npm:6.11.0" + starknet: "npm:6.17.0" terminal-link: "npm:^3.0.0" tsx: "npm:^4.7.1" typescript: "npm:5.3.3" @@ -8178,7 +8178,7 @@ __metadata: pino: "npm:^8.19.0" prettier: "npm:^2.8.8" sinon: "npm:^13.0.2" - starknet: "npm:6.11.0" + starknet: "npm:6.17.0" ts-node: "npm:^10.8.0" tsx: "npm:^4.7.1" typescript: "npm:5.3.3" @@ -14575,7 +14575,7 @@ __metadata: languageName: node linkType: hard -"abi-wan-kanabi@npm:^2.2.2": +"abi-wan-kanabi@npm:^2.2.2, abi-wan-kanabi@npm:^2.2.3": version: 2.2.3 resolution: "abi-wan-kanabi@npm:2.2.3" dependencies: @@ -29640,6 +29640,25 @@ __metadata: languageName: node linkType: hard +"starknet@npm:6.17.0": + version: 6.17.0 + resolution: "starknet@npm:6.17.0" + dependencies: + "@noble/curves": "npm:~1.3.0" + "@noble/hashes": "npm:~1.3.0" + "@scure/base": "npm:~1.1.3" + "@scure/starknet": "npm:~1.0.0" + abi-wan-kanabi: "npm:^2.2.3" + fetch-cookie: "npm:^3.0.0" + isomorphic-fetch: "npm:^3.0.0" + lossless-json: "npm:^4.0.1" + pako: "npm:^2.0.4" + starknet-types-07: "npm:@starknet-io/types-js@^0.7.7" + ts-mixer: "npm:^6.0.3" + checksum: 709c44f933ea20b7c925c493b1dcc8e1cd3c6b3b5776fd726a8fc56e3bb514187cd81e4899f4e3999231dcf389ed591a8d0515ce4a27f0378b874e556b539f79 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" From 2dcc47b3209dc0a63fdfb06e282074d5cdb05509 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:05:27 +0330 Subject: [PATCH 017/132] minor: cleanup --- typescript/sdk/src/core/StarknetCoreModule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 3e10f9d545d..ec40a27ef6c 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -141,7 +141,7 @@ export class StarknetCoreModule { async read(mailboxContract: Contract) { const [defaultIsm, defaultHook, requiredHook, owner] = ( await Promise.all([ - mailboxContract.call('get_default_ism', [], { parseResponse: true }), + mailboxContract.call('get_default_ism'), mailboxContract.call('get_default_hook'), mailboxContract.call('get_required_hook'), mailboxContract.call('owner'), From 63fcc2fc3fbab0248fe2ff7dba0406007e0edecf Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:59:02 +0330 Subject: [PATCH 018/132] feat: include update owner in owner function --- typescript/sdk/src/core/StarknetCoreModule.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index ec40a27ef6c..ed428dc845e 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -93,8 +93,14 @@ export class StarknetCoreModule { async update( expectedConfig: Partial, args: { mailboxContract: Contract; chain: ChainNameOrId; owner: string }, - ): Promise<{ defaultIsm?: string; requiredHook?: string }> { - const result: { defaultIsm?: string; requiredHook?: string } = {}; + ): Promise<{ defaultIsm?: string; requiredHook?: string; owner?: string }> { + const result: { + defaultIsm?: string; + requiredHook?: string; + owner?: string; + } = {}; + + const actualDefaultIsmConfig = await this.read(args.mailboxContract); if (expectedConfig.defaultIsm) { const defaultIsm = await this.deployer.deployIsm({ @@ -134,6 +140,24 @@ export class StarknetCoreModule { result.requiredHook = merkleTreeHook; } + if ( + expectedConfig.owner && + actualDefaultIsmConfig.owner !== expectedConfig.owner + ) { + this.logger.trace(`Updating mailbox owner ${expectedConfig.owner}..`); + const { transaction_hash: transferOwnershipTxHash } = + await args.mailboxContract.invoke('transfer_ownership', [ + expectedConfig.owner, + ]); + + await this.signer.waitForTransaction(transferOwnershipTxHash); + this.logger.trace( + `Transaction hash for updated required hook: ${transferOwnershipTxHash}`, + ); + + result.owner = expectedConfig.owner; + } + return result; } From ba0d6eafa115c5fe6f6c70a5af0b2ba92247cb4a Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 7 Nov 2024 16:39:03 +0100 Subject: [PATCH 019/132] feat: Singe/MultiVM signer strategy --- typescript/cli/cli.ts | 5 +- typescript/cli/src/commands/options.ts | 1 + typescript/cli/src/commands/strategy.ts | 207 ++++++++++++++++++ typescript/cli/src/commands/warp.ts | 1 - typescript/cli/src/config/strategy.ts | 16 ++ typescript/cli/src/context/context.ts | 48 +++- .../cli/src/context/manager/ContextManager.ts | 44 ++++ .../src/context/strategies/signer/signer.ts | 192 ++++++++++++++++ .../submitter/GnosisSafeStrategy.ts | 15 ++ .../strategies/submitter/JsonRpcStrategy.ts | 24 ++ .../strategies/submitter/SubmitterStrategy.ts | 22 ++ .../submitter/SubmitterStrategyFactory.ts | 21 ++ typescript/cli/src/context/types.ts | 9 + typescript/cli/src/deploy/core.ts | 6 +- typescript/cli/src/deploy/utils.ts | 25 ++- typescript/cli/src/deploy/warp.ts | 44 +--- typescript/cli/src/send/transfer.ts | 15 +- typescript/cli/src/utils/balances.ts | 5 +- .../transactions/submitter/schemas.ts | 1 + 19 files changed, 635 insertions(+), 66 deletions(-) create mode 100644 typescript/cli/src/commands/strategy.ts create mode 100644 typescript/cli/src/config/strategy.ts create mode 100644 typescript/cli/src/context/manager/ContextManager.ts create mode 100644 typescript/cli/src/context/strategies/signer/signer.ts create mode 100644 typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts create mode 100644 typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts create mode 100644 typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts create mode 100644 typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts diff --git a/typescript/cli/cli.ts b/typescript/cli/cli.ts index 77be0b86f85..672d84247dd 100644 --- a/typescript/cli/cli.ts +++ b/typescript/cli/cli.ts @@ -24,10 +24,11 @@ import { registryCommand } from './src/commands/registry.js'; import { relayerCommand } from './src/commands/relayer.js'; import { sendCommand } from './src/commands/send.js'; import { statusCommand } from './src/commands/status.js'; +import { strategyCommand } from './src/commands/strategy.js'; import { submitCommand } from './src/commands/submit.js'; import { validatorCommand } from './src/commands/validator.js'; import { warpCommand } from './src/commands/warp.js'; -import { contextMiddleware } from './src/context/context.js'; +import { contextMiddleware, signerMiddleware } from './src/context/context.js'; import { configureLogger, errorRed } from './src/logger.js'; import { checkVersion } from './src/utils/version-check.js'; import { VERSION } from './src/version.js'; @@ -55,6 +56,7 @@ try { configureLogger(argv.log as LogFormat, argv.verbosity as LogLevel); }, contextMiddleware, + signerMiddleware, ]) .command(avsCommand) .command(configCommand) @@ -69,6 +71,7 @@ try { .command(submitCommand) .command(validatorCommand) .command(warpCommand) + .command(strategyCommand) .version(VERSION) .demandCommand() .strict() diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index f23194c8041..3ffffa410f5 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -95,6 +95,7 @@ export const DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH = './configs/warp-route-deployment.yaml'; export const DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH = './configs/core-config.yaml'; +export const DEFAULT_STRATEGY_CONFIG_PATH = './configs/default-strategy.yaml'; export const warpDeploymentConfigCommandOption: Options = { type: 'string', diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts new file mode 100644 index 00000000000..24253891294 --- /dev/null +++ b/typescript/cli/src/commands/strategy.ts @@ -0,0 +1,207 @@ +// import { input, select } from '@inquirer/prompts'; +import { input, select } from '@inquirer/prompts'; +import { ethers } from 'ethers'; +import { stringify as yamlStringify } from 'yaml'; +import { CommandModule } from 'yargs'; + +import { + ChainSubmissionStrategy, + ChainSubmissionStrategySchema, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType, assert } from '@hyperlane-xyz/utils'; + +import { CommandModuleWithWriteContext } from '../context/types.js'; +import { + errorRed, + log, + logBlue, + logCommandHeader, + logGreen, +} from '../logger.js'; +import { runSingleChainSelectionStep } from '../utils/chains.js'; +import { + indentYamlOrJson, + mergeYamlOrJson, + readYamlOrJson, + writeYamlOrJson, +} from '../utils/files.js'; + +import { + DEFAULT_STRATEGY_CONFIG_PATH, + outputFileCommandOption, +} from './options.js'; + +/** + * Parent command + */ +export const strategyCommand: CommandModule = { + command: 'strategy', + describe: 'Manage Hyperlane deployment strategies', + builder: (yargs) => yargs.command(init).version(false).demandCommand(), + handler: () => log('Command required'), +}; + +export const init: CommandModuleWithWriteContext<{ + chain: string; + config: string; +}> = { + command: 'init', + describe: 'Initiates strategy', + builder: { + config: outputFileCommandOption( + DEFAULT_STRATEGY_CONFIG_PATH, + false, + 'The path to output a Key Config JSON or YAML file.', + ), + type: { + type: 'string', + description: + 'Type of submitter (jsonRpc, impersonatedAccount, gnosisSafe, gnosisSafeTxBuilder)', + }, + safeAddress: { + type: 'string', + description: + 'Safe address (required for gnosisSafe and gnosisSafeTxBuilder types)', + }, + userAddress: { + type: 'string', + description: 'User address (required for impersonatedAccount type)', + }, + }, + handler: async ({ + context, + type: inputType, + safeAddress: inputSafeAddress, + userAddress: inputUserAddress, + }) => { + logCommandHeader(`Hyperlane Key Init`); + + try { + await readYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH); + } catch (e) { + writeYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH, {}, 'yaml'); + } + + const chain = await runSingleChainSelectionStep(context.chainMetadata); + const chainProtocol = context.chainMetadata[chain].protocol; + assert(chainProtocol === ProtocolType.Ethereum, 'Incompatible protocol'); + + // If type wasn't provided via command line, prompt for it + const type = + inputType || + (await select({ + message: 'Enter the type of submitter', + choices: Object.values(TxSubmitterType).map((value) => ({ + name: value, + value: value, + })), + })); + + let submitter: any = { + type: type, + }; + + // Configure submitter based on type + switch (type) { + case TxSubmitterType.JSON_RPC: + const privateKey = await input({ + message: 'Enter your private key', + validate: (pk) => isValidPrivateKey(pk), + }); + submitter.privateKey = privateKey; + break; + + case TxSubmitterType.IMPERSONATED_ACCOUNT: + const userAddress = + inputUserAddress || + (await input({ + message: 'Enter the user address to impersonate', + validate: (address) => { + try { + return ethers.utils.isAddress(address) + ? true + : 'Invalid Ethereum address'; + } catch { + return 'Invalid Ethereum address'; + } + }, + })); + assert( + userAddress, + 'User address is required for impersonated account', + ); + submitter.userAddress = userAddress; + break; + + case TxSubmitterType.GNOSIS_SAFE: + case TxSubmitterType.GNOSIS_TX_BUILDER: + const safeAddress = + inputSafeAddress || + (await input({ + message: 'Enter the Safe address', + validate: (address) => { + try { + return ethers.utils.isAddress(address) + ? true + : 'Invalid Safe address'; + } catch { + return 'Invalid Safe address'; + } + }, + })); + assert(safeAddress, 'Safe address is required for Gnosis Safe'); + + submitter = { + type: type, + chain: chain, + safeAddress: safeAddress, + }; + + if (type === TxSubmitterType.GNOSIS_TX_BUILDER) { + const version = await input({ + message: 'Enter the Safe version (default: 1.0)', + default: '1.0', + }); + submitter.version = version; + } + break; + + default: + throw new Error(`Unsupported submitter type: ${type}`); + } + + let result: ChainSubmissionStrategy = { + [chain]: { + submitter: submitter, + }, + }; + + try { + const strategyConfig = ChainSubmissionStrategySchema.parse(result); + logBlue( + `Strategy config is valid, writing to file ${DEFAULT_STRATEGY_CONFIG_PATH}:\n`, + ); + log(indentYamlOrJson(yamlStringify(strategyConfig, null, 2), 4)); + + mergeYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH, strategyConfig); + logGreen('✅ Successfully created new key config.'); + } catch (e) { + errorRed( + `Key config is invalid, please check the submitter configuration.`, + ); + throw e; + } + process.exit(0); + }, +}; + +function isValidPrivateKey(privateKey: string): boolean { + try { + // Attempt to create a Wallet instance with the private key + const wallet = new ethers.Wallet(privateKey); + return wallet.privateKey === privateKey; + } catch (error) { + return false; + } +} diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index b7fb4562430..388c478b032 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -138,7 +138,6 @@ export const deploy: CommandModuleWithWriteContext<{ try { await runWarpRouteDeploy({ context, - warpRouteDeploymentConfigPath: config, }); } catch (error: any) { evaluateIfDryRunFailure(error, dryRun); diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts new file mode 100644 index 00000000000..36925250a67 --- /dev/null +++ b/typescript/cli/src/config/strategy.ts @@ -0,0 +1,16 @@ +import { + ChainSubmissionStrategy, + ChainSubmissionStrategySchema, +} from '@hyperlane-xyz/sdk'; + +import { readYamlOrJson } from '../utils/files.js'; + +export async function readDefaultStrategyConfig( + filePath: string, +): Promise { + let config = readYamlOrJson(filePath); + if (!config) + throw new Error(`No default strategy config found at ${filePath}`); + + return ChainSubmissionStrategySchema.parse(config); +} diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 30390f4b405..f641157cf30 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -16,14 +16,20 @@ import { } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; +import { + DEFAULT_STRATEGY_CONFIG_PATH, // DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, +} from '../commands/options.js'; import { isSignCommand } from '../commands/signCommands.js'; +import { readDefaultStrategyConfig } from '../config/strategy.js'; +// import { readWarpRouteDeployConfig } from '../config/warp.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; -import { getImpersonatedSigner, getSigner } from '../utils/keys.js'; +import { getImpersonatedSigner } from '../utils/keys.js'; +import { SignerStrategyFactory } from './strategies/signer/signer.js'; import { CommandContext, ContextSettings, @@ -32,6 +38,7 @@ import { export async function contextMiddleware(argv: Record) { const isDryRun = !isNullish(argv.dryRun); + const requiresKey = isSignCommand(argv); const settings: ContextSettings = { registryUri: argv.registry, @@ -42,16 +49,44 @@ export async function contextMiddleware(argv: Record) { disableProxy: argv.disableProxy, skipConfirmation: argv.yes, }; + if (!isDryRun && settings.fromAddress) throw new Error( "'--from-address' or '-f' should only be used for dry-runs", ); + const context = isDryRun ? await getDryRunContext(settings, argv.dryRun) : await getContext(settings); argv.context = context; } +export async function signerMiddleware(argv: Record) { + const { requiresKey, multiProvider } = argv.context; + + if (!requiresKey) return argv; + + const defaultStrategy = await readDefaultStrategyConfig( + argv.strategy || DEFAULT_STRATEGY_CONFIG_PATH, + ); + + // Select appropriate strategy + const strategy = SignerStrategyFactory.createStrategy(argv); + + // Determine chains + const chains = await strategy.determineChains(argv); + + // Create context manager + const contextManager = strategy.createContextManager(chains, defaultStrategy); + + // Figure out if a command requires --origin and --destination + + // Configure signers + await strategy.configureSigners(argv, multiProvider, contextManager); + + return argv; +} + /** * Retrieves context for the user-selected command * @returns context for the current command @@ -63,21 +98,18 @@ export async function getContext({ requiresKey, skipConfirmation, disableProxy = false, + signers, }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy); - - let signer: ethers.Wallet | undefined = undefined; - if (key || requiresKey) { - ({ key, signer } = await getSigner({ key, skipConfirmation })); - } - const multiProvider = await getMultiProvider(registry, signer); + const multiProvider = await getMultiProvider(registry); return { registry, + requiresKey, chainMetadata: multiProvider.metadata, multiProvider, key, - signer, + signers, skipConfirmation: !!skipConfirmation, } as CommandContext; } diff --git a/typescript/cli/src/context/manager/ContextManager.ts b/typescript/cli/src/context/manager/ContextManager.ts new file mode 100644 index 00000000000..baa68e5ee7b --- /dev/null +++ b/typescript/cli/src/context/manager/ContextManager.ts @@ -0,0 +1,44 @@ +import { Signer } from 'ethers'; + +import { ChainName, TxSubmitterType } from '@hyperlane-xyz/sdk'; + +import { ISubmitterStrategy } from '../strategies/submitter/SubmitterStrategy.js'; +import { SubmitterStrategyFactory } from '../strategies/submitter/SubmitterStrategyFactory.js'; + +export class ContextManager { + private strategy: ISubmitterStrategy; + + constructor( + defaultStrategy: any, + private chains: ChainName[], + submitterType: TxSubmitterType, + ) { + this.strategy = SubmitterStrategyFactory.createStrategy( + submitterType, + defaultStrategy, + ); + } + + async getChainKeys(): Promise< + Array<{ chainName: ChainName; privateKey: string }> + > { + const chainKeys = await Promise.all( + this.chains.map(async (chain) => ({ + chainName: chain, + privateKey: await this.strategy.getPrivateKey(chain), + })), + ); + + return chainKeys; + } + + async getSigners(): Promise> { + const chainKeys = await this.getChainKeys(); + return Object.fromEntries( + chainKeys.map(({ chainName, privateKey }) => [ + chainName, + this.strategy.getSigner(privateKey), + ]), + ); + } +} diff --git a/typescript/cli/src/context/strategies/signer/signer.ts b/typescript/cli/src/context/strategies/signer/signer.ts new file mode 100644 index 00000000000..3da8bf9c709 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/signer.ts @@ -0,0 +1,192 @@ +import { confirm } from '@inquirer/prompts'; + +import { ChainName, MultiProvider, TxSubmitterType } from '@hyperlane-xyz/sdk'; + +import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; +import { readWarpRouteDeployConfig } from '../../../config/warp.js'; +import { runSingleChainSelectionStep } from '../../../utils/chains.js'; +import { isFile, runFileSelectionStep } from '../../../utils/files.js'; +import { ContextManager } from '../../manager/ContextManager.js'; + +export interface WarpDeployContextResult { + warpRouteConfig: Record; + chains: ChainName[]; +} + +export interface SignerStrategy { + /** + * Determines the chains to be used for signing + * @param argv Command arguments + * @returns Array of chain names + */ + determineChains(argv: Record): Promise; + + /** + * Creates a context manager for the selected chains + * @param chains Selected chains + * @param defaultStrategy Default strategy configuration + * @returns ContextManager instance + */ + createContextManager( + chains: ChainName[], + defaultStrategy: any, + ): ContextManager; + + /** + * Configures signers for the multi-provider + * @param argv Command arguments + * @param multiProvider MultiProvider instance + * @param contextManager ContextManager instance + */ + configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise; +} + +export class SingleChainSignerStrategy implements SignerStrategy { + async determineChains(argv: Record): Promise { + const chain: ChainName = + argv.chain || + (await runSingleChainSelectionStep( + argv.context.chainMetadata, + 'Select chain to connect:', + )); + + argv.chain = chain; + return [chain]; // Explicitly return as single-item array + } + + createContextManager( + chains: ChainName[], + defaultStrategy: any, + ): ContextManager { + return new ContextManager( + defaultStrategy, + chains, + TxSubmitterType.JSON_RPC, + ); + } + + async configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise { + const signers = await contextManager.getSigners(); + multiProvider.setSigners(signers); + argv.context.multiProvider = multiProvider; + argv.contextManager = contextManager; + } +} + +export class WarpDeploySignerStrategy implements SignerStrategy { + async determineChains(argv: Record): Promise { + const { warpRouteConfig, chains } = await getWarpDeployContext({ + configPath: argv.wd || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, + skipConfirmation: argv.skipConfirmation, + context: argv.context, + }); + + argv.context.warpRouteConfig = warpRouteConfig; + argv.context.chains = chains; + return chains; + } + + createContextManager( + chains: ChainName[], + defaultStrategy: any, + ): ContextManager { + return new ContextManager( + defaultStrategy, + chains, + TxSubmitterType.JSON_RPC, + ); + } + + async configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise { + const signers = await contextManager.getSigners(); + multiProvider.setSigners(signers); + argv.context.multiProvider = multiProvider; + argv.contextManager = contextManager; + } +} + +export class SignerStrategyFactory { + static createStrategy(argv: Record): SignerStrategy { + if ( + argv._[0] === 'warp' && + (argv._[1] === 'deploy' || argv._[1] === 'send') + ) { + return new WarpDeploySignerStrategy(); + } + + if (argv._[0] === 'send') { + // You might want to create a specific multi-chain send strategy + return new WarpDeploySignerStrategy(); + } + + return new SingleChainSignerStrategy(); + } +} + +export async function getWarpDeployContext({ + configPath = DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, + skipConfirmation = false, + context, +}: { + configPath?: string; + skipConfirmation?: boolean; + context: any; +}): Promise { + // Validate config path + if (!configPath || !isFile(configPath)) { + if (skipConfirmation) { + throw new Error('Warp route deployment config is required'); + } + + // Interactive file selection if no path provided + configPath = await runFileSelectionStep( + './configs', + 'Warp route deployment config', + 'warp', + ); + } else { + console.log(`Using warp route deployment config at ${configPath}`); + } + + // Read warp route deployment configuration + const warpRouteConfig = await readWarpRouteDeployConfig(configPath, context); + + // Extract chains from configuration + const chains = Object.keys(warpRouteConfig) as ChainName[]; + + // Validate chains + if (chains.length === 0) { + throw new Error('No chains found in warp route deployment config'); + } + + // Optional: Confirm multi-chain deployment + if (!skipConfirmation && chains.length > 1) { + const confirmMultiChain = await confirm({ + message: `Deploy warp route across ${chains.length} chains: ${chains.join( + ', ', + )}?`, + default: true, + }); + + if (!confirmMultiChain) { + throw new Error('Deployment cancelled by user'); + } + } + + return { + warpRouteConfig, + chains, + }; +} diff --git a/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts b/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts new file mode 100644 index 00000000000..8369be9b00e --- /dev/null +++ b/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts @@ -0,0 +1,15 @@ +import { TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; + +import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; + +export class GnosisSafeStrategy extends BaseSubmitterStrategy { + async getPrivateKey(chain: ChainName): Promise { + // Implement Gnosis Safe specific logic + throw new Error('Not implemented'); + } + + getType(): TxSubmitterType { + return TxSubmitterType.GNOSIS_SAFE; + } +} diff --git a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts new file mode 100644 index 00000000000..55ca6369312 --- /dev/null +++ b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts @@ -0,0 +1,24 @@ +import { password } from '@inquirer/prompts'; + +import { TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; + +import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; + +export class JsonRpcStrategy extends BaseSubmitterStrategy { + async getPrivateKey(chain: ChainName): Promise { + let pk = this.config[chain]?.submitter?.privateKey; + + if (!pk) { + pk = await password({ + message: `Please enter the private key for chain ${chain}`, + }); + } + + return pk; + } + + getType(): TxSubmitterType { + return TxSubmitterType.JSON_RPC; + } +} diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts new file mode 100644 index 00000000000..10bc3f3123b --- /dev/null +++ b/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts @@ -0,0 +1,22 @@ +import { ethers } from 'ethers'; + +import { TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; + +export interface ISubmitterStrategy { + getPrivateKey(chain: ChainName): Promise; + getSigner(privateKey: string): ethers.Signer; + getType(): TxSubmitterType; +} + +export abstract class BaseSubmitterStrategy implements ISubmitterStrategy { + constructor(protected config: any) {} + + abstract getPrivateKey(chain: ChainName): Promise; + + getSigner(privateKey: string): ethers.Signer { + return new ethers.Wallet(privateKey); + } + + abstract getType(): TxSubmitterType; +} diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts new file mode 100644 index 00000000000..9c32f992c7d --- /dev/null +++ b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts @@ -0,0 +1,21 @@ +import { TxSubmitterType } from '@hyperlane-xyz/sdk'; + +import { GnosisSafeStrategy } from './GnosisSafeStrategy.js'; +import { JsonRpcStrategy } from './JsonRpcStrategy.js'; +import { ISubmitterStrategy } from './SubmitterStrategy.js'; + +export class SubmitterStrategyFactory { + static createStrategy( + type: TxSubmitterType, + config: any, + ): ISubmitterStrategy { + switch (type) { + case TxSubmitterType.JSON_RPC: + return new JsonRpcStrategy(config); + case TxSubmitterType.GNOSIS_SAFE: + return new GnosisSafeStrategy(config); + default: + throw new Error(`Unsupported submitter type: ${type}`); + } + } +} diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 6c3a17c5fff..e3ada01b2b6 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -5,9 +5,13 @@ import type { IRegistry } from '@hyperlane-xyz/registry'; import type { ChainMap, ChainMetadata, + ChainName, MultiProvider, + WarpRouteDeployConfig, } from '@hyperlane-xyz/sdk'; +// TODO: revisit ContextSettings & CommandContext for improvements + export interface ContextSettings { registryUri: string; registryOverrideUri: string; @@ -16,6 +20,7 @@ export interface ContextSettings { requiresKey?: boolean; disableProxy?: boolean; skipConfirmation?: boolean; + signers?: any; } export interface CommandContext { @@ -25,6 +30,10 @@ export interface CommandContext { skipConfirmation: boolean; key?: string; signer?: ethers.Signer; + signers?: ethers.Signer[]; + chain?: ChainName; + chains?: ChainName[]; + warpRouteConfig?: WarpRouteDeployConfig; } export interface WriteCommandContext extends CommandContext { diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index f0848458fcf..6eaed956f85 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -42,7 +42,6 @@ export async function runCoreDeploy(params: DeployParams) { let chain = params.chain; const { - signer, isDryRun, chainMetadata, dryRunChain, @@ -61,13 +60,14 @@ export async function runCoreDeploy(params: DeployParams) { 'Select chain to connect:', ); } - let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await getOrRequestApiKeys([chain], chainMetadata); + const signer = multiProvider.getSigner(chain); + const deploymentParams: DeployParams = { - context, + context: { ...context, signer }, chain, config, }; diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index d8ced32dc70..71ea6cbb650 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -41,7 +41,7 @@ export async function runPreflightChecksForChains({ chainsToGasCheck?: ChainName[]; }) { log('Running pre-flight checks for chains...'); - const { signer, multiProvider } = context; + const { multiProvider } = context; if (!chains?.length) throw new Error('Empty chain selection'); for (const chain of chains) { @@ -49,15 +49,15 @@ export async function runPreflightChecksForChains({ if (!metadata) throw new Error(`No chain config found for ${chain}`); if (metadata.protocol !== ProtocolType.Ethereum) throw new Error('Only Ethereum chains are supported for now'); + const signer = multiProvider.getSigner(chain); + assertSigner(signer); + logGreen(`✅ ${chain} signer is valid`); } logGreen('✅ Chains are valid'); - assertSigner(signer); - logGreen('✅ Signer is valid'); - await nativeBalancesAreSufficient( multiProvider, - signer, + null, chainsToGasCheck ?? chains, minGas, ); @@ -70,8 +70,13 @@ export async function runDeployPlanStep({ context: WriteCommandContext; chain: ChainName; }) { - const { signer, chainMetadata: chainMetadataMap, skipConfirmation } = context; - const address = await signer.getAddress(); + const { + chainMetadata: chainMetadataMap, + multiProvider, + skipConfirmation, + } = context; + + const address = await multiProvider.getSigner(chain).getAddress(); logBlue('\nDeployment plan'); logGray('==============='); @@ -124,7 +129,7 @@ export function isZODISMConfig(filepath: string): boolean { export async function prepareDeploy( context: WriteCommandContext, - userAddress: Address, + userAddress: Address | null, chains: ChainName[], ): Promise> { const { multiProvider, isDryRun } = context; @@ -134,6 +139,7 @@ export async function prepareDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); + const userAddress = await multiProvider.getSigner(chain).getAddress(); const currentBalance = await provider.getBalance(userAddress); initialBalances[chain] = currentBalance; }), @@ -145,7 +151,7 @@ export async function completeDeploy( context: WriteCommandContext, command: string, initialBalances: Record, - userAddress: Address, + userAddress: Address | null, chains: ChainName[], ) { const { multiProvider, isDryRun } = context; @@ -154,6 +160,7 @@ export async function completeDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); + const userAddress = await multiProvider.getSigner(chain).getAddress(); const currentBalance = await provider.getBalance(userAddress); const balanceDelta = initialBalances[chain].sub(currentBalance); if (isDryRun && balanceDelta.lt(0)) break; diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 36bbc2ad8fe..71eed010c5d 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -62,7 +62,6 @@ import { retryAsync, } from '@hyperlane-xyz/utils'; -import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { getOrRequestApiKeys } from '../context/context.js'; import { WriteCommandContext } from '../context/types.js'; @@ -70,9 +69,7 @@ import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; import { getSubmitterBuilder } from '../submit/submit.js'; import { indentYamlOrJson, - isFile, readYamlOrJson, - runFileSelectionStep, writeYamlOrJson, } from '../utils/files.js'; @@ -95,43 +92,24 @@ interface WarpApplyParams extends DeployParams { export async function runWarpRouteDeploy({ context, - warpRouteDeploymentConfigPath, }: { context: WriteCommandContext; - warpRouteDeploymentConfigPath?: string; }) { - const { signer, skipConfirmation, chainMetadata } = context; - - if ( - !warpRouteDeploymentConfigPath || - !isFile(warpRouteDeploymentConfigPath) - ) { - if (skipConfirmation) - throw new Error('Warp route deployment config required'); - warpRouteDeploymentConfigPath = await runFileSelectionStep( - './configs', - 'Warp route deployment config', - 'warp', - ); - } else { - log( - `Using warp route deployment config at ${warpRouteDeploymentConfigPath}`, - ); - } - const warpRouteConfig = await readWarpRouteDeployConfig( - warpRouteDeploymentConfigPath, - context, - ); - - const chains = Object.keys(warpRouteConfig); + const { + skipConfirmation, + chainMetadata, + warpRouteConfig, + chains: contextChains, + } = context; + const chains = contextChains!; let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await getOrRequestApiKeys(chains, chainMetadata); const deploymentParams = { context, - warpDeployConfig: warpRouteConfig, + warpDeployConfig: warpRouteConfig!, }; await runDeployPlanStep(deploymentParams); @@ -142,9 +120,7 @@ export async function runWarpRouteDeploy({ minGas: MINIMUM_WARP_DEPLOY_GAS, }); - const userAddress = await signer.getAddress(); - - const initialBalances = await prepareDeploy(context, userAddress, chains); + const initialBalances = await prepareDeploy(context, null, chains); const deployedContracts = await executeDeploy(deploymentParams, apiKeys); @@ -155,7 +131,7 @@ export async function runWarpRouteDeploy({ await writeDeploymentArtifacts(warpCoreConfig, context); - await completeDeploy(context, 'warp', initialBalances, userAddress, chains); + await completeDeploy(context, 'warp', initialBalances, null, chains!); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index a89eb6aa99e..2e94fdd53fe 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -106,8 +106,9 @@ async function executeDelivery({ skipWaitForDelivery: boolean; selfRelay?: boolean; }) { - const { signer, multiProvider, registry } = context; + const { multiProvider, registry } = context; + const signer = multiProvider.getSigner(origin); const signerAddress = await signer.getAddress(); recipient ||= signerAddress; @@ -136,12 +137,12 @@ async function executeDelivery({ token = warpCore.findToken(origin, routerAddress)!; } - const senderAddress = await signer.getAddress(); + // const senderAddress = await multiProvider.getSigner(origin).getAddress(); const errors = await warpCore.validateTransfer({ originTokenAmount: token.amount(amount), destination, - recipient: recipient ?? senderAddress, - sender: senderAddress, + recipient: recipient ?? signerAddress, + sender: signerAddress, }); if (errors) { logRed('Error validating transfer', JSON.stringify(errors)); @@ -152,8 +153,8 @@ async function executeDelivery({ const transferTxs = await warpCore.getTransferRemoteTxs({ originTokenAmount: new TokenAmount(amount, token), destination, - sender: senderAddress, - recipient: recipient ?? senderAddress, + sender: signerAddress, + recipient: recipient ?? signerAddress, }); const txReceipts = []; @@ -172,7 +173,7 @@ async function executeDelivery({ const parsed = parseWarpRouteMessage(message.parsed.body); logBlue( - `Sent transfer from sender (${senderAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`, + `Sent transfer from sender (${signerAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`, ); logBlue(`Message ID: ${message.id}`); log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`); diff --git a/typescript/cli/src/utils/balances.ts b/typescript/cli/src/utils/balances.ts index 5cf8019771e..ef497e62612 100644 --- a/typescript/cli/src/utils/balances.ts +++ b/typescript/cli/src/utils/balances.ts @@ -7,14 +7,13 @@ import { logGreen, logRed } from '../logger.js'; export async function nativeBalancesAreSufficient( multiProvider: MultiProvider, - signer: ethers.Signer, + signer: ethers.Signer | null, chains: ChainName[], minGas: string, ) { - const address = await signer.getAddress(); - const sufficientBalances: boolean[] = []; for (const chain of chains) { + const address = multiProvider.getSigner(chain).getAddress(); const provider = multiProvider.getProvider(chain); const gasPrice = await provider.getGasPrice(); const minBalanceWei = gasPrice.mul(minGas).toString(); diff --git a/typescript/sdk/src/providers/transactions/submitter/schemas.ts b/typescript/sdk/src/providers/transactions/submitter/schemas.ts index 89c891c09ca..81dfba7e452 100644 --- a/typescript/sdk/src/providers/transactions/submitter/schemas.ts +++ b/typescript/sdk/src/providers/transactions/submitter/schemas.ts @@ -10,6 +10,7 @@ import { export const SubmitterMetadataSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal(TxSubmitterType.JSON_RPC), + privateKey: z.string().optional(), }), z.object({ type: z.literal(TxSubmitterType.IMPERSONATED_ACCOUNT), From 21c6474c14969409bfeaa3c87e57d8c5dee6ab89 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:39:35 +0330 Subject: [PATCH 020/132] cleanup: manage core deploy with switching over protocol types --- typescript/cli/src/deploy/core.ts | 105 ++++++++++-------- typescript/cli/src/deploy/utils.ts | 9 +- typescript/sdk/src/core/StarknetCoreModule.ts | 12 +- 3 files changed, 68 insertions(+), 58 deletions(-) diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 6ddaf0539c6..d7f16b0385f 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -2,6 +2,7 @@ import { Account, RpcProvider } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; +import { ChainAddresses } from '@hyperlane-xyz/registry'; import { DeployedCoreAddresses } from '@hyperlane-xyz/sdk'; import { ChainMap, @@ -65,25 +66,6 @@ export async function runCoreDeploy(params: DeployParams) { ); } - if (multiProvider.tryGetProtocol(chain) === ProtocolType.Starknet) { - const provider = new RpcProvider({ - nodeUrl: 'http://127.0.0.1:5050', - }); - const account = new Account( - provider, - '0x4acc9b79dae485fb71f309f5b62501a1329789f4418bb4c25353ad5617be4d4', - '0x000000000000000000000000000000002f663fafebbee32e0698f7e13f886c73', - ); - const starknetCoreModule = new StarknetCoreModule(account); - const deployments = await starknetCoreModule.deploy({ - chain, - config, - }); - console.log(deployments); - logGreen('✅ Core contract deployments complete:\n'); - return; - } - let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry); @@ -95,33 +77,66 @@ export async function runCoreDeploy(params: DeployParams) { }; await runDeployPlanStep(deploymentParams); - await runPreflightChecksForChains({ - ...deploymentParams, - chains: [chain], - minGas: MINIMUM_CORE_DEPLOY_GAS, - }); - - const userAddress = await signer.getAddress(); - - const initialBalances = await prepareDeploy(context, userAddress, [chain]); - const contractVerifier = new ContractVerifier( - multiProvider, - apiKeys, - coreBuildArtifact, - ExplorerLicenseType.MIT, - ); - - logBlue('🚀 All systems ready, captain! Beginning deployment...'); - const evmCoreModule = await EvmCoreModule.create({ - chain, - config, - multiProvider, - contractVerifier, - }); - - await completeDeploy(context, 'core', initialBalances, userAddress, [chain]); - const deployedAddresses = evmCoreModule.serialize(); + let deployedAddresses: ChainAddresses; + switch (multiProvider.tryGetProtocol(chain)) { + case ProtocolType.Ethereum: + { + await runPreflightChecksForChains({ + ...deploymentParams, + chains: [chain], + minGas: MINIMUM_CORE_DEPLOY_GAS, + }); + + const userAddress = await signer.getAddress(); + + const initialBalances = await prepareDeploy(context, userAddress, [ + chain, + ]); + + const contractVerifier = new ContractVerifier( + multiProvider, + apiKeys, + coreBuildArtifact, + ExplorerLicenseType.MIT, + ); + + logBlue('🚀 All systems ready, captain! Beginning deployment...'); + const evmCoreModule = await EvmCoreModule.create({ + chain, + config, + multiProvider, + contractVerifier, + }); + + await completeDeploy(context, 'core', initialBalances, userAddress, [ + chain, + ]); + deployedAddresses = evmCoreModule.serialize(); + } + break; + + case ProtocolType.Starknet: + { + const provider = new RpcProvider({ + nodeUrl: 'http://127.0.0.1:5050', + }); + const account = new Account( + provider, + '0x4acc9b79dae485fb71f309f5b62501a1329789f4418bb4c25353ad5617be4d4', + '0x000000000000000000000000000000002f663fafebbee32e0698f7e13f886c73', + ); + const starknetCoreModule = new StarknetCoreModule(account); + deployedAddresses = await starknetCoreModule.deploy({ + chain, + config, + }); + } + break; + + default: + throw new Error('Chain protocol is not supported yet!'); + } if (!isDryRun) { await registry.updateChain({ diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index 00643e16643..d8ced32dc70 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -47,13 +47,8 @@ export async function runPreflightChecksForChains({ for (const chain of chains) { const metadata = multiProvider.tryGetChainMetadata(chain); if (!metadata) throw new Error(`No chain config found for ${chain}`); - if ( - metadata.protocol !== ProtocolType.Ethereum && - metadata.protocol !== ProtocolType.Starknet - ) - throw new Error( - 'Only Ethereum and Starknet chains are supported for now', - ); + if (metadata.protocol !== ProtocolType.Ethereum) + throw new Error('Only Ethereum chains are supported for now'); } logGreen('✅ Chains are valid'); diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index ed428dc845e..6b656eccc81 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -31,7 +31,7 @@ export class StarknetCoreModule { ); const noopIsm = await this.deployer.deployContract('noop_ism', []); - const hook = await this.deployer.deployContract('hook', []); + const defaultHook = await this.deployer.deployContract('hook', []); const protocolFee = await this.deployer.deployContract('protocol_fee', [ '1000000000000000000', @@ -46,7 +46,7 @@ export class StarknetCoreModule { const mailboxContract = await this.deployMailbox( config.owner, noopIsm, - hook, + defaultHook, protocolFee, ); @@ -63,12 +63,12 @@ export class StarknetCoreModule { return { noopIsm, - hook, - mailbox: mailboxContract.address, + defaultHook, defaultIsm: defaultIsm || noopIsm, - validatorAnnounce, protocolFee, - requiredHook: requiredHook || '', + mailbox: mailboxContract.address, + merkleTreeHook: requiredHook || '', + validatorAnnounce, }; } From 006b1a281a3ef536bf91ccd2b58fe0fb1612a4d0 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:48:51 +0330 Subject: [PATCH 021/132] feat: read chain domain id from configs for starknet --- typescript/cli/src/deploy/core.ts | 3 ++- typescript/sdk/src/core/StarknetCoreModule.ts | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index d7f16b0385f..78d69194e40 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -118,6 +118,7 @@ export async function runCoreDeploy(params: DeployParams) { case ProtocolType.Starknet: { + const domainId = multiProvider.getDomainId(chain); const provider = new RpcProvider({ nodeUrl: 'http://127.0.0.1:5050', }); @@ -126,7 +127,7 @@ export async function runCoreDeploy(params: DeployParams) { '0x4acc9b79dae485fb71f309f5b62501a1329789f4418bb4c25353ad5617be4d4', '0x000000000000000000000000000000002f663fafebbee32e0698f7e13f886c73', ); - const starknetCoreModule = new StarknetCoreModule(account); + const starknetCoreModule = new StarknetCoreModule(account, domainId); deployedAddresses = await starknetCoreModule.deploy({ chain, config, diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 6b656eccc81..e328e8cbdf5 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -12,7 +12,10 @@ import { CoreConfig } from './types.js'; export class StarknetCoreModule { protected logger = rootLogger.child({ module: 'StarknetCoreModule' }); protected deployer: StarknetDeployer; - constructor(protected readonly signer: Account) { + constructor( + protected readonly signer: Account, + protected readonly domainId: number, + ) { this.deployer = new StarknetDeployer(signer); } @@ -79,7 +82,7 @@ export class StarknetCoreModule { requiredHook: string, ) { const mailboxAddress = await this.deployer.deployContract('mailbox', [ - '888', // TODO: put domain id here + this.domainId, owner, defaultIsm, defaultHook, From de52d1b0d4a2db081be26fdfde22c8e0ca9d5dac Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:16:48 +0330 Subject: [PATCH 022/132] feat: read protocol fee hook config from chain config --- typescript/sdk/src/core/StarknetCoreModule.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index e328e8cbdf5..247a7e6ebfd 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -37,13 +37,13 @@ export class StarknetCoreModule { const defaultHook = await this.deployer.deployContract('hook', []); const protocolFee = await this.deployer.deployContract('protocol_fee', [ - '1000000000000000000', + config.requiredHook.maxProtocolFee || '1000000000000000000', '0', - '10000000000000000', + config.requiredHook.protocolFee || '10000000000000000', '0', config.requiredHook.beneficiary, config.owner, - '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', + '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', // ETH address on Starknet chains ]); const mailboxContract = await this.deployMailbox( From b0b682c406841f5e8fe6169441941241ed2b9a00 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:39:06 +0330 Subject: [PATCH 023/132] cleanup: deploy ism function --- typescript/sdk/src/deploy/StarknetDeployer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index d497aa5343b..1f22a777e89 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -71,10 +71,12 @@ export class StarknetDeployer { mailbox: Address; }): Promise
{ const { chain, ismConfig, mailbox } = params; - if (typeof ismConfig === 'string') return ''; + assert( + typeof ismConfig !== 'string', + 'String ism config is not supported on starknet', + ); const ismType = ismConfig.type; - const logger = this.logger.child({ destination: chain, ismType }); - logger.debug(`Deploying ${ismType} to ${chain}`); + this.logger.debug(`Deploying ${ismType} to ${chain}`); assert( SupportedIsmTypesOnStarknet.includes( From 2bf349b0202a7089e69bfb3e5426702b91750e29 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 8 Nov 2024 16:58:49 +0100 Subject: [PATCH 024/132] feat: Deployment strategies & refactoring --- typescript/cli/src/commands/options.ts | 2 +- typescript/cli/src/commands/signCommands.ts | 2 +- typescript/cli/src/commands/strategy.ts | 14 +- typescript/cli/src/context/context.ts | 44 ++-- .../cli/src/context/manager/ContextManager.ts | 28 ++- .../signer/OriginDestinationSignerStrategy.ts | 62 ++++++ .../strategies/signer/SignerStrategy.ts | 45 ++++ .../signer/SignerStrategyFactory.ts | 23 +++ .../signer/SingleChainSignerStrategy.ts | 49 +++++ .../signer/WarpDeploySignerStrategy.ts | 117 +++++++++++ .../src/context/strategies/signer/signer.ts | 192 ------------------ typescript/cli/src/context/types.ts | 2 - typescript/cli/src/deploy/warp.ts | 33 ++- 13 files changed, 384 insertions(+), 229 deletions(-) create mode 100644 typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts create mode 100644 typescript/cli/src/context/strategies/signer/SignerStrategy.ts create mode 100644 typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts create mode 100644 typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts create mode 100644 typescript/cli/src/context/strategies/signer/WarpDeploySignerStrategy.ts delete mode 100644 typescript/cli/src/context/strategies/signer/signer.ts diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 3ffffa410f5..7f34ac8cb01 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -95,7 +95,7 @@ export const DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH = './configs/warp-route-deployment.yaml'; export const DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH = './configs/core-config.yaml'; -export const DEFAULT_STRATEGY_CONFIG_PATH = './configs/default-strategy.yaml'; +export const DEFAULT_STRATEGY_CONFIG_PATH = `${os.homedir()}/.hyperlane/strategies/default-strategy.yaml`; export const warpDeploymentConfigCommandOption: Options = { type: 'string', diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 79c243ca6f2..e420677958c 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -1,7 +1,7 @@ // Commands that send tx and require a key to sign. // It's useful to have this listed here so the context // middleware can request keys up front when required. -export const SIGN_COMMANDS = ['deploy', 'send', 'status', 'submit']; +export const SIGN_COMMANDS = ['deploy', 'send', 'status', 'submit', 'apply']; export function isSignCommand(argv: any): boolean { return ( diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index 24253891294..8bc92a43657 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -22,7 +22,6 @@ import { import { runSingleChainSelectionStep } from '../utils/chains.js'; import { indentYamlOrJson, - mergeYamlOrJson, readYamlOrJson, writeYamlOrJson, } from '../utils/files.js'; @@ -76,11 +75,15 @@ export const init: CommandModuleWithWriteContext<{ userAddress: inputUserAddress, }) => { logCommandHeader(`Hyperlane Key Init`); - + let defaultStrategy; try { - await readYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH); + defaultStrategy = await readYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH); } catch (e) { - writeYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH, {}, 'yaml'); + defaultStrategy = writeYamlOrJson( + DEFAULT_STRATEGY_CONFIG_PATH, + {}, + 'yaml', + ); } const chain = await runSingleChainSelectionStep(context.chainMetadata); @@ -172,6 +175,7 @@ export const init: CommandModuleWithWriteContext<{ } let result: ChainSubmissionStrategy = { + ...defaultStrategy, [chain]: { submitter: submitter, }, @@ -184,7 +188,7 @@ export const init: CommandModuleWithWriteContext<{ ); log(indentYamlOrJson(yamlStringify(strategyConfig, null, 2), 4)); - mergeYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH, strategyConfig); + writeYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH, strategyConfig); logGreen('✅ Successfully created new key config.'); } catch (e) { errorRed( diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index f641157cf30..bae7754b840 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -16,12 +16,9 @@ import { } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; -import { - DEFAULT_STRATEGY_CONFIG_PATH, // DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, -} from '../commands/options.js'; +import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; import { isSignCommand } from '../commands/signCommands.js'; import { readDefaultStrategyConfig } from '../config/strategy.js'; -// import { readWarpRouteDeployConfig } from '../config/warp.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; @@ -29,7 +26,7 @@ import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner } from '../utils/keys.js'; -import { SignerStrategyFactory } from './strategies/signer/signer.js'; +import { SignerStrategyFactory } from './strategies/signer/SignerStrategyFactory.js'; import { CommandContext, ContextSettings, @@ -66,25 +63,33 @@ export async function signerMiddleware(argv: Record) { if (!requiresKey) return argv; - const defaultStrategy = await readDefaultStrategyConfig( - argv.strategy || DEFAULT_STRATEGY_CONFIG_PATH, - ); - - // Select appropriate strategy - const strategy = SignerStrategyFactory.createStrategy(argv); + const strategyUrl = argv.strategy || DEFAULT_STRATEGY_CONFIG_PATH; + const strategyConfig = await readDefaultStrategyConfig(strategyUrl); - // Determine chains - const chains = await strategy.determineChains(argv); + // Select the appropriate signing strategy based on the provided hyperlane command + // e.g command `core deploy` uses SingleChainSignerStrategy + const signerStrategy = SignerStrategyFactory.createStrategy(argv); - // Create context manager - const contextManager = strategy.createContextManager(chains, defaultStrategy); + // Determine the chains that will be used for signing based on the selected strategy + // e.g. SingleChainSignerStrategy extracts jsonRpc private key from strategyConfig else prompts user PK input + const chains = await signerStrategy.determineChains(argv); - // Figure out if a command requires --origin and --destination + // Create a context manager for the signer, which will manage the signing context for the specified chains + // default: TxSubmitterType.JSON_RPC + const signerContextManager = signerStrategy.createContextManager( + chains, + strategyConfig, + argv, + ); - // Configure signers - await strategy.configureSigners(argv, multiProvider, contextManager); + // Configure the signers using the selected strategy, multiProvider, and context manager + await signerStrategy.configureSigners( + argv, + multiProvider, + signerContextManager, + ); - return argv; + return { ...argv, strategy: strategyUrl }; } /** @@ -101,6 +106,7 @@ export async function getContext({ signers, }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy); + const multiProvider = await getMultiProvider(registry); return { diff --git a/typescript/cli/src/context/manager/ContextManager.ts b/typescript/cli/src/context/manager/ContextManager.ts index baa68e5ee7b..4cb3f464502 100644 --- a/typescript/cli/src/context/manager/ContextManager.ts +++ b/typescript/cli/src/context/manager/ContextManager.ts @@ -2,36 +2,58 @@ import { Signer } from 'ethers'; import { ChainName, TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ENV } from '../../utils/env.js'; import { ISubmitterStrategy } from '../strategies/submitter/SubmitterStrategy.js'; import { SubmitterStrategyFactory } from '../strategies/submitter/SubmitterStrategyFactory.js'; +/** + * @title ContextManager + * @dev Manages the context for transaction submitters, including retrieving chain keys and signers. + */ export class ContextManager { private strategy: ISubmitterStrategy; + /** + * @param strategyConfig Configuration for the submitter strategy. + * @param chains Array of chain names to manage. + * @param submitterType Type of transaction submitter to use. + */ constructor( - defaultStrategy: any, + strategyConfig: any, private chains: ChainName[], submitterType: TxSubmitterType, + private argv?: any, ) { this.strategy = SubmitterStrategyFactory.createStrategy( submitterType, - defaultStrategy, + strategyConfig, ); } + /** + * @dev Retrieves the private keys for the specified chains. + * @return An array of objects containing chain names and their corresponding private keys. + */ async getChainKeys(): Promise< Array<{ chainName: ChainName; privateKey: string }> > { const chainKeys = await Promise.all( this.chains.map(async (chain) => ({ chainName: chain, - privateKey: await this.strategy.getPrivateKey(chain), + privateKey: + this.argv.key || // argv.key overrides strategy key + (await this.strategy.getPrivateKey(chain)) || + ENV.HYP_KEY, // argv.key and ENV.HYP_KEY for backwards compatibility })), ); return chainKeys; } + /** + * @dev Retrieves signers for the specified chains using their private keys. + * @return A record mapping chain names to their corresponding Signer objects. + */ async getSigners(): Promise> { const chainKeys = await this.getChainKeys(); return Object.fromEntries( diff --git a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts new file mode 100644 index 00000000000..ce441206f9f --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts @@ -0,0 +1,62 @@ +import { + ChainName, + ChainSubmissionStrategy, + MultiProvider, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; + +import { runSingleChainSelectionStep } from '../../../utils/chains.js'; +import { ContextManager } from '../../manager/ContextManager.js'; + +import { SignerStrategy } from './SignerStrategy.js'; + +export class OriginDestinationSignerStrategy implements SignerStrategy { + async determineChains(argv: Record): Promise { + const { context } = argv; + let origin = argv.origin; + let destination = argv.destination; + + if (!origin) { + origin = await runSingleChainSelectionStep( + context.chainMetadata, + 'Select the origin chain', + ); + } + + if (!destination) { + destination = await runSingleChainSelectionStep( + context.chainMetadata, + 'Select the destination chain', + ); + } + const chains = [origin, destination]; + argv.chains = chains; + argv.origin = origin; + argv.destination = origin; + return chains; // Explicitly return as single-item array + } + + createContextManager( + chains: ChainName[], + strategyConfig: ChainSubmissionStrategy, + argv?: any, + ): ContextManager { + return new ContextManager( + strategyConfig, + chains, + TxSubmitterType.JSON_RPC, + argv, + ); + } + + async configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise { + const signers = await contextManager.getSigners(); + multiProvider.setSigners(signers); + argv.context.multiProvider = multiProvider; + argv.contextManager = contextManager; + } +} diff --git a/typescript/cli/src/context/strategies/signer/SignerStrategy.ts b/typescript/cli/src/context/strategies/signer/SignerStrategy.ts new file mode 100644 index 00000000000..459796876aa --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/SignerStrategy.ts @@ -0,0 +1,45 @@ +import { + ChainName, + ChainSubmissionStrategy, + MultiProvider, +} from '@hyperlane-xyz/sdk'; + +import { ContextManager } from '../../manager/ContextManager.js'; + +export interface WarpDeployContextResult { + warpRouteConfig: Record; + chains: ChainName[]; +} + +export interface SignerStrategy { + /** + * Determines the chains to be used for signing + * @param argv Command arguments + * @returns Array of chain names + */ + determineChains(argv: Record): Promise; + + /** + * Creates a context manager for the selected chains + * @param chains Selected chains + * @param strategyConfig Default strategy configuration + * @returns ContextManager instance + */ + createContextManager( + chains: ChainName[], + strategyConfig: ChainSubmissionStrategy, + argv?: any, + ): ContextManager; + + /** + * Configures signers for the multi-provider + * @param argv Command arguments + * @param multiProvider MultiProvider instance + * @param contextManager ContextManager instance + */ + configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise; +} diff --git a/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts b/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts new file mode 100644 index 00000000000..8b259098f59 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts @@ -0,0 +1,23 @@ +import { OriginDestinationSignerStrategy } from './OriginDestinationSignerStrategy.js'; +import { SignerStrategy } from './SignerStrategy.js'; +import { SingleChainSignerStrategy } from './SingleChainSignerStrategy.js'; +import { WarpDeploySignerStrategy } from './WarpDeploySignerStrategy.js'; + +export class SignerStrategyFactory { + static createStrategy(argv: Record): SignerStrategy { + const strategyMap: Record SignerStrategy> = { + 'core:apply': () => new SingleChainSignerStrategy(), + 'warp:deploy': () => new WarpDeploySignerStrategy(), + 'warp:send': () => new WarpDeploySignerStrategy(), // Assuming same strategy for 'send' + 'warp:apply': () => new WarpDeploySignerStrategy(), // Assuming same strategy for 'appl' + 'send:message': () => new OriginDestinationSignerStrategy(), + }; + + const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim(); + + const createStrategy = + strategyMap[commandKey] || (() => new SingleChainSignerStrategy()); + + return createStrategy(); + } +} diff --git a/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts new file mode 100644 index 00000000000..a7c9b154a21 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts @@ -0,0 +1,49 @@ +import { + ChainName, + ChainSubmissionStrategy, + MultiProvider, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; + +import { runSingleChainSelectionStep } from '../../../utils/chains.js'; +import { ContextManager } from '../../manager/ContextManager.js'; + +import { SignerStrategy } from './SignerStrategy.js'; + +export class SingleChainSignerStrategy implements SignerStrategy { + async determineChains(argv: Record): Promise { + const chain: ChainName = + argv.chain || + (await runSingleChainSelectionStep( + argv.context.chainMetadata, + 'Select chain to connect:', + )); + + argv.chain = chain; + return [chain]; // Explicitly return as single-item array + } + + createContextManager( + chains: ChainName[], + strategyConfig: ChainSubmissionStrategy, + argv?: any, + ): ContextManager { + return new ContextManager( + strategyConfig, + chains, + TxSubmitterType.JSON_RPC, + argv, + ); + } + + async configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise { + const signers = await contextManager.getSigners(); + multiProvider.setSigners(signers); + argv.context.multiProvider = multiProvider; + argv.contextManager = contextManager; + } +} diff --git a/typescript/cli/src/context/strategies/signer/WarpDeploySignerStrategy.ts b/typescript/cli/src/context/strategies/signer/WarpDeploySignerStrategy.ts new file mode 100644 index 00000000000..48a6e346074 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/WarpDeploySignerStrategy.ts @@ -0,0 +1,117 @@ +import { confirm } from '@inquirer/prompts'; + +import { + ChainName, + ChainSubmissionStrategy, + MultiProvider, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; + +import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; +import { + isFile, + readYamlOrJson, + runFileSelectionStep, +} from '../../../utils/files.js'; +import { ContextManager } from '../../manager/ContextManager.js'; + +import { SignerStrategy } from './SignerStrategy.js'; + +export interface WarpDeployContextResult { + warpRouteConfig: Record; + chains: ChainName[]; +} + +export class WarpDeploySignerStrategy implements SignerStrategy { + async determineChains(argv: Record): Promise { + const configPath = argv.wd || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; + const { chains } = await getWarpDeployContext({ + configPath, + skipConfirmation: argv.skipConfirmation, + }); + + argv.context.config = configPath; + argv.context.chains = chains; + return chains; + } + + createContextManager( + chains: ChainName[], + strategyConfig: ChainSubmissionStrategy, + argv: any, + ): ContextManager { + return new ContextManager( + strategyConfig, + chains, + TxSubmitterType.JSON_RPC, + argv, + ); + } + + async configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise { + const signers = await contextManager.getSigners(); + multiProvider.setSigners(signers); + argv.context.multiProvider = multiProvider; + argv.contextManager = contextManager; + } +} + +export async function getWarpDeployContext({ + configPath = DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, + skipConfirmation = false, +}: { + configPath?: string; + skipConfirmation?: boolean; +}): Promise { + // Validate config path + if (!configPath || !isFile(configPath)) { + if (skipConfirmation) { + throw new Error('Warp route deployment config is required'); + } + + // Interactive file selection if no path provided + configPath = await runFileSelectionStep( + './configs', + 'Warp route deployment config', + 'warp', + ); + } else { + console.log(`Using warp route deployment config at ${configPath}`); + } + + // Read warp route deployment configuration + let warpRouteConfig = readYamlOrJson(configPath); + if (!warpRouteConfig) + throw new Error(`No warp route deploy config found at ${configPath}`); + + // Extract chains from configuration + const chains = Object.keys(warpRouteConfig) as ChainName[]; + + // Validate chains + if (chains.length === 0) { + throw new Error('No chains found in warp route deployment config'); + } + + // Optional: Confirm multi-chain deployment + if (!skipConfirmation && chains.length > 1) { + const confirmMultiChain = await confirm({ + message: `Deploy warp route across ${chains.length} chains: ${chains.join( + ', ', + )}?`, + default: true, + }); + + if (!confirmMultiChain) { + throw new Error('Deployment cancelled by user'); + } + } + + return { + warpRouteConfig, + chains, + }; +} diff --git a/typescript/cli/src/context/strategies/signer/signer.ts b/typescript/cli/src/context/strategies/signer/signer.ts deleted file mode 100644 index 3da8bf9c709..00000000000 --- a/typescript/cli/src/context/strategies/signer/signer.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { confirm } from '@inquirer/prompts'; - -import { ChainName, MultiProvider, TxSubmitterType } from '@hyperlane-xyz/sdk'; - -import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; -import { readWarpRouteDeployConfig } from '../../../config/warp.js'; -import { runSingleChainSelectionStep } from '../../../utils/chains.js'; -import { isFile, runFileSelectionStep } from '../../../utils/files.js'; -import { ContextManager } from '../../manager/ContextManager.js'; - -export interface WarpDeployContextResult { - warpRouteConfig: Record; - chains: ChainName[]; -} - -export interface SignerStrategy { - /** - * Determines the chains to be used for signing - * @param argv Command arguments - * @returns Array of chain names - */ - determineChains(argv: Record): Promise; - - /** - * Creates a context manager for the selected chains - * @param chains Selected chains - * @param defaultStrategy Default strategy configuration - * @returns ContextManager instance - */ - createContextManager( - chains: ChainName[], - defaultStrategy: any, - ): ContextManager; - - /** - * Configures signers for the multi-provider - * @param argv Command arguments - * @param multiProvider MultiProvider instance - * @param contextManager ContextManager instance - */ - configureSigners( - argv: Record, - multiProvider: MultiProvider, - contextManager: ContextManager, - ): Promise; -} - -export class SingleChainSignerStrategy implements SignerStrategy { - async determineChains(argv: Record): Promise { - const chain: ChainName = - argv.chain || - (await runSingleChainSelectionStep( - argv.context.chainMetadata, - 'Select chain to connect:', - )); - - argv.chain = chain; - return [chain]; // Explicitly return as single-item array - } - - createContextManager( - chains: ChainName[], - defaultStrategy: any, - ): ContextManager { - return new ContextManager( - defaultStrategy, - chains, - TxSubmitterType.JSON_RPC, - ); - } - - async configureSigners( - argv: Record, - multiProvider: MultiProvider, - contextManager: ContextManager, - ): Promise { - const signers = await contextManager.getSigners(); - multiProvider.setSigners(signers); - argv.context.multiProvider = multiProvider; - argv.contextManager = contextManager; - } -} - -export class WarpDeploySignerStrategy implements SignerStrategy { - async determineChains(argv: Record): Promise { - const { warpRouteConfig, chains } = await getWarpDeployContext({ - configPath: argv.wd || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, - skipConfirmation: argv.skipConfirmation, - context: argv.context, - }); - - argv.context.warpRouteConfig = warpRouteConfig; - argv.context.chains = chains; - return chains; - } - - createContextManager( - chains: ChainName[], - defaultStrategy: any, - ): ContextManager { - return new ContextManager( - defaultStrategy, - chains, - TxSubmitterType.JSON_RPC, - ); - } - - async configureSigners( - argv: Record, - multiProvider: MultiProvider, - contextManager: ContextManager, - ): Promise { - const signers = await contextManager.getSigners(); - multiProvider.setSigners(signers); - argv.context.multiProvider = multiProvider; - argv.contextManager = contextManager; - } -} - -export class SignerStrategyFactory { - static createStrategy(argv: Record): SignerStrategy { - if ( - argv._[0] === 'warp' && - (argv._[1] === 'deploy' || argv._[1] === 'send') - ) { - return new WarpDeploySignerStrategy(); - } - - if (argv._[0] === 'send') { - // You might want to create a specific multi-chain send strategy - return new WarpDeploySignerStrategy(); - } - - return new SingleChainSignerStrategy(); - } -} - -export async function getWarpDeployContext({ - configPath = DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, - skipConfirmation = false, - context, -}: { - configPath?: string; - skipConfirmation?: boolean; - context: any; -}): Promise { - // Validate config path - if (!configPath || !isFile(configPath)) { - if (skipConfirmation) { - throw new Error('Warp route deployment config is required'); - } - - // Interactive file selection if no path provided - configPath = await runFileSelectionStep( - './configs', - 'Warp route deployment config', - 'warp', - ); - } else { - console.log(`Using warp route deployment config at ${configPath}`); - } - - // Read warp route deployment configuration - const warpRouteConfig = await readWarpRouteDeployConfig(configPath, context); - - // Extract chains from configuration - const chains = Object.keys(warpRouteConfig) as ChainName[]; - - // Validate chains - if (chains.length === 0) { - throw new Error('No chains found in warp route deployment config'); - } - - // Optional: Confirm multi-chain deployment - if (!skipConfirmation && chains.length > 1) { - const confirmMultiChain = await confirm({ - message: `Deploy warp route across ${chains.length} chains: ${chains.join( - ', ', - )}?`, - default: true, - }); - - if (!confirmMultiChain) { - throw new Error('Deployment cancelled by user'); - } - } - - return { - warpRouteConfig, - chains, - }; -} diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index e3ada01b2b6..eb573e84dcc 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -30,8 +30,6 @@ export interface CommandContext { skipConfirmation: boolean; key?: string; signer?: ethers.Signer; - signers?: ethers.Signer[]; - chain?: ChainName; chains?: ChainName[]; warpRouteConfig?: WarpRouteDeployConfig; } diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 71eed010c5d..de50ac34a7f 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -62,6 +62,7 @@ import { retryAsync, } from '@hyperlane-xyz/utils'; +import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { getOrRequestApiKeys } from '../context/context.js'; import { WriteCommandContext } from '../context/types.js'; @@ -69,7 +70,9 @@ import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; import { getSubmitterBuilder } from '../submit/submit.js'; import { indentYamlOrJson, + isFile, readYamlOrJson, + runFileSelectionStep, writeYamlOrJson, } from '../utils/files.js'; @@ -92,15 +95,33 @@ interface WarpApplyParams extends DeployParams { export async function runWarpRouteDeploy({ context, + warpRouteDeploymentConfigPath, }: { context: WriteCommandContext; + warpRouteDeploymentConfigPath?: string; }) { - const { - skipConfirmation, - chainMetadata, - warpRouteConfig, - chains: contextChains, - } = context; + const { skipConfirmation, chainMetadata, chains: contextChains } = context; + + if ( + !warpRouteDeploymentConfigPath || + !isFile(warpRouteDeploymentConfigPath) + ) { + if (skipConfirmation) + throw new Error('Warp route deployment config required'); + warpRouteDeploymentConfigPath = await runFileSelectionStep( + './configs', + 'Warp route deployment config', + 'warp', + ); + } else { + log( + `Using warp route deployment config at ${warpRouteDeploymentConfigPath}`, + ); + } + const warpRouteConfig = await readWarpRouteDeployConfig( + warpRouteDeploymentConfigPath, + context, + ); const chains = contextChains!; let apiKeys: ChainMap = {}; From af3766b5141b58a7c858ba6180b46deeb647b793 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:12:28 +0330 Subject: [PATCH 025/132] temp: starknet reader classes --- typescript/sdk/src/core/StarknetCoreModule.ts | 40 ++-- typescript/sdk/src/core/StarknetCoreReader.ts | 43 ++++ typescript/sdk/src/hook/StarknetHookReader.ts | 65 ++++++ typescript/sdk/src/ism/StarknetIsmReader.ts | 214 ++++++++++++++++++ 4 files changed, 340 insertions(+), 22 deletions(-) create mode 100644 typescript/sdk/src/core/StarknetCoreReader.ts create mode 100644 typescript/sdk/src/hook/StarknetHookReader.ts create mode 100644 typescript/sdk/src/ism/StarknetIsmReader.ts diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 247a7e6ebfd..913dd5d0e9b 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -1,4 +1,4 @@ -import { Account, Contract, num } from 'starknet'; +import { Account, Contract } from 'starknet'; import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; import { assert, rootLogger } from '@hyperlane-xyz/utils'; @@ -7,16 +7,28 @@ import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; import { HookType } from '../hook/types.js'; import { ChainNameOrId } from '../types.js'; +import { StarknetCoreReader } from './StarknetCoreReader.js'; import { CoreConfig } from './types.js'; export class StarknetCoreModule { protected logger = rootLogger.child({ module: 'StarknetCoreModule' }); protected deployer: StarknetDeployer; + protected coreReader: StarknetCoreReader; + constructor( protected readonly signer: Account, protected readonly domainId: number, ) { this.deployer = new StarknetDeployer(signer); + this.coreReader = new StarknetCoreReader(signer); + } + + /** + * Reads the core configuration from the mailbox address + * @returns The core config. + */ + public async read(mailboxContract: Contract): Promise { + return this.coreReader.deriveCoreConfig(mailboxContract.address); } async deploy(params: { @@ -37,9 +49,9 @@ export class StarknetCoreModule { const defaultHook = await this.deployer.deployContract('hook', []); const protocolFee = await this.deployer.deployContract('protocol_fee', [ - config.requiredHook.maxProtocolFee || '1000000000000000000', + '1000000000000000000', '0', - config.requiredHook.protocolFee || '10000000000000000', + '10000000000000000', '0', config.requiredHook.beneficiary, config.owner, @@ -103,7 +115,7 @@ export class StarknetCoreModule { owner?: string; } = {}; - const actualDefaultIsmConfig = await this.read(args.mailboxContract); + const actualConfig = await this.read(args.mailboxContract); if (expectedConfig.defaultIsm) { const defaultIsm = await this.deployer.deployIsm({ @@ -143,10 +155,7 @@ export class StarknetCoreModule { result.requiredHook = merkleTreeHook; } - if ( - expectedConfig.owner && - actualDefaultIsmConfig.owner !== expectedConfig.owner - ) { + if (expectedConfig.owner && actualConfig.owner !== expectedConfig.owner) { this.logger.trace(`Updating mailbox owner ${expectedConfig.owner}..`); const { transaction_hash: transferOwnershipTxHash } = await args.mailboxContract.invoke('transfer_ownership', [ @@ -155,7 +164,7 @@ export class StarknetCoreModule { await this.signer.waitForTransaction(transferOwnershipTxHash); this.logger.trace( - `Transaction hash for updated required hook: ${transferOwnershipTxHash}`, + `Transaction hash for updated owner: ${transferOwnershipTxHash}`, ); result.owner = expectedConfig.owner; @@ -163,17 +172,4 @@ export class StarknetCoreModule { return result; } - - //TODO: return CoreConfig by fetching each contract details - async read(mailboxContract: Contract) { - const [defaultIsm, defaultHook, requiredHook, owner] = ( - await Promise.all([ - mailboxContract.call('get_default_ism'), - mailboxContract.call('get_default_hook'), - mailboxContract.call('get_required_hook'), - mailboxContract.call('owner'), - ]) - ).map((res) => num.toHex64(res.toString())); - return { defaultIsm, defaultHook, requiredHook, owner }; - } } diff --git a/typescript/sdk/src/core/StarknetCoreReader.ts b/typescript/sdk/src/core/StarknetCoreReader.ts new file mode 100644 index 00000000000..1106d152adb --- /dev/null +++ b/typescript/sdk/src/core/StarknetCoreReader.ts @@ -0,0 +1,43 @@ +import { Account, Contract, num } from 'starknet'; + +import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; +import { Address, rootLogger } from '@hyperlane-xyz/utils'; + +import { StarknetHookReader } from '../hook/StarknetHookReader.js'; +import { StarknetIsmReader } from '../ism/StarknetIsmReader.js'; + +import { CoreConfig } from './types.js'; + +export class StarknetCoreReader { + protected readonly logger = rootLogger.child({ + module: 'StarknetCoreReader', + }); + protected ismReader: StarknetIsmReader; + protected hookReader: StarknetHookReader; + + constructor(protected readonly signer: Account) { + this.ismReader = new StarknetIsmReader(this.signer); + this.hookReader = new StarknetHookReader(this.signer); + } + + async deriveCoreConfig(mailboxAddress: Address): Promise { + const { abi } = getCompiledContract('mailbox'); + const mailbox = new Contract(abi, mailboxAddress, this.signer); + + const [defaultIsm, defaultHook, requiredHook, owner] = ( + await Promise.all([ + mailbox.get_default_ism(), + mailbox.get_default_hook(), + mailbox.get_required_hook(), + mailbox.owner(), + ]) + ).map((res) => num.toHex64(res.toString())); + + return { + owner, + defaultIsm: await this.ismReader.deriveIsmConfig(defaultIsm), + defaultHook: await this.hookReader.deriveHookConfig(defaultHook), + requiredHook: await this.hookReader.deriveHookConfig(requiredHook), + }; + } +} diff --git a/typescript/sdk/src/hook/StarknetHookReader.ts b/typescript/sdk/src/hook/StarknetHookReader.ts new file mode 100644 index 00000000000..2707df234c9 --- /dev/null +++ b/typescript/sdk/src/hook/StarknetHookReader.ts @@ -0,0 +1,65 @@ +import { Account, Contract, num } from 'starknet'; + +import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; +import { Address, rootLogger } from '@hyperlane-xyz/utils'; + +import { HookType } from './types.js'; + +export class StarknetHookReader { + protected readonly logger = rootLogger.child({ + module: 'StarknetHookReader', + }); + + constructor(protected readonly signer: Account) {} + + async deriveHookConfig(address: Address): Promise { + try { + const { abi } = getCompiledContract('hook'); + const hook = new Contract(abi, address, this.signer); + + const hookType = (await hook.hook_type()).toString(); + + switch (hookType) { + case HookType.MERKLE_TREE: // MERKLE_TREE + return this.deriveMerkleTreeConfig(address); + case HookType.PROTOCOL_FEE: // PROTOCOL_FEE + return this.deriveProtocolFeeConfig(address); + default: + throw Error; + } + } catch (error) { + this.logger.error(`Failed to derive Hook config for ${address}`, error); + throw error; + } + } + + private async deriveMerkleTreeConfig(address: Address) { + return { + type: HookType.MERKLE_TREE, + address, + }; + } + + private async deriveProtocolFeeConfig(address: Address) { + const { abi } = getCompiledContract('protocol_fee'); + const hook = new Contract(abi, address, this.signer); + + const [owner, maxProtocolFee, protocolFee, beneficiary] = await Promise.all( + [ + hook.owner(), + hook.get_max_protocol_fee(), + hook.get_protocol_fee(), + hook.get_beneficiary(), + ], + ); + + return { + type: HookType.PROTOCOL_FEE, + address, + owner: num.toHex64(owner.toString()), + maxProtocolFee: maxProtocolFee.toString(), + protocolFee: protocolFee.toString(), + beneficiary: num.toHex64(beneficiary.toString()), + }; + } +} diff --git a/typescript/sdk/src/ism/StarknetIsmReader.ts b/typescript/sdk/src/ism/StarknetIsmReader.ts new file mode 100644 index 00000000000..d6c63c7f81b --- /dev/null +++ b/typescript/sdk/src/ism/StarknetIsmReader.ts @@ -0,0 +1,214 @@ +import { Account, Contract, num } from 'starknet'; + +import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; +import { Address, rootLogger } from '@hyperlane-xyz/utils'; + +import { StarknetIsmContractName } from './starknet-utils.js'; +import { IsmType } from './types.js'; + +export class StarknetIsmReader { + protected readonly logger = rootLogger.child({ module: 'StarknetIsmReader' }); + + constructor(protected readonly signer: Account) {} + + private getContractAbi(ismType: keyof typeof StarknetIsmContractName) { + return getCompiledContract(StarknetIsmContractName[ismType]).abi; + } + + async deriveIsmConfig(address: Address): Promise { + try { + const ism = new Contract( + this.getContractAbi(IsmType.ROUTING), + address, + this.signer, + ); + const moduleType = (await ism.module_type()).toString(); + + switch (moduleType) { + case IsmType.PAUSABLE: + case IsmType.TRUSTED_RELAYER: // NULL (Pausable ISM & Trusted Relayer ISM) + return this.deriveNullConfig(address); + case IsmType.MESSAGE_ID_MULTISIG: + return this.deriveMessageIdMultisigConfig(address); + case IsmType.MERKLE_ROOT_MULTISIG: + return this.deriveMerkleRootMultisigConfig(address); + case IsmType.ROUTING: + return this.deriveRoutingConfig(address); + case IsmType.FALLBACK_ROUTING: + return this.deriveFallbackRoutingConfig(address); + case IsmType.AGGREGATION: + return this.deriveAggregationConfig(address); + default: + return { + type: IsmType.TEST_ISM, + address, + }; + } + } catch (error) { + this.logger.error(`Failed to derive ISM config for ${address}`, error); + throw error; + } + } + + private async deriveNullConfig(address: Address) { + try { + const ism = new Contract( + this.getContractAbi(IsmType.PAUSABLE), + address, + this.signer, + ); + await ism.paused(); // Will succeed for pausable ISM + return { + type: IsmType.PAUSABLE, + address, + }; + } catch { + return { + type: IsmType.TRUSTED_RELAYER, + address, + }; + } + } + + private async deriveMessageIdMultisigConfig(address: Address) { + const ism = new Contract( + this.getContractAbi(IsmType.MESSAGE_ID_MULTISIG), + address, + this.signer, + ); + + const [validators, threshold] = await Promise.all([ + ism.get_validators(), + ism.get_threshold(), + ]); + + return { + type: IsmType.MESSAGE_ID_MULTISIG, + address, + validators: validators.map((v: any) => num.toHex64(v.toString())), + threshold: threshold.toString(), + }; + } + + private async deriveMerkleRootMultisigConfig(address: Address) { + const ism = new Contract( + this.getContractAbi(IsmType.MERKLE_ROOT_MULTISIG), + address, + this.signer, + ); + + const [validators, threshold] = await Promise.all([ + ism.get_validators(), + ism.get_threshold(), + ]); + + return { + type: IsmType.MERKLE_ROOT_MULTISIG, + address, + validators: validators.map((v: any) => num.toHex64(v.toString())), + threshold: threshold.toString(), + }; + } + + private async deriveRoutingConfig(address: Address) { + const ism = new Contract( + this.getContractAbi(IsmType.ROUTING), + address, + this.signer, + ); + + const domains = await ism.domains(); + const domainConfigs: Record = {}; + + for (const domain of domains) { + try { + const module = await ism.module(domain); + const moduleConfig = await this.deriveIsmConfig( + num.toHex64(module.toString()), + ); + domainConfigs[domain.toString()] = moduleConfig; + } catch (error) { + this.logger.error( + `Failed to derive config for domain ${domain}`, + error, + ); + } + } + + return { + type: IsmType.ROUTING, + address, + domains: domainConfigs, + }; + } + + private async deriveFallbackRoutingConfig(address: Address) { + const ism = new Contract( + this.getContractAbi(IsmType.FALLBACK_ROUTING), + address, + this.signer, + ); + + const domains = await ism.domains(); + const domainConfigs: Record = {}; + const mailbox = await ism.mailbox(); + + for (const domain of domains) { + try { + const module = await ism.module(domain); + const moduleConfig = await this.deriveIsmConfig( + num.toHex64(module.toString()), + ); + domainConfigs[domain.toString()] = moduleConfig; + } catch (error) { + this.logger.error( + `Failed to derive config for domain ${domain}`, + error, + ); + } + } + + return { + type: IsmType.FALLBACK_ROUTING, + address, + domains: domainConfigs, + mailbox: num.toHex64(mailbox.toString()), + }; + } + + private async deriveAggregationConfig(address: Address) { + const ism = new Contract( + this.getContractAbi(IsmType.AGGREGATION), + address, + this.signer, + ); + + const [modules, threshold] = await Promise.all([ + ism.get_modules(), + ism.get_threshold(), + ]); + + const moduleConfigs = await Promise.all( + modules.map(async (moduleAddress: any) => { + try { + return await this.deriveIsmConfig( + num.toHex64(moduleAddress.toString()), + ); + } catch (error) { + this.logger.error( + `Failed to derive config for module ${moduleAddress}`, + error, + ); + return null; + } + }), + ); + + return { + type: IsmType.AGGREGATION, + address, + modules: moduleConfigs.filter(Boolean), + threshold: threshold.toString(), + }; + } +} From bbfd317cbd5127fab518e3c2093713041bfa4002 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:49:58 +0330 Subject: [PATCH 026/132] feat: register starknet chains --- typescript/cli/src/config/chain.ts | 80 ++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index e710b6cf007..9d3245992b7 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -1,5 +1,9 @@ import { confirm, input, select } from '@inquirer/prompts'; import { ethers } from 'ethers'; +import { + Provider as StarknetProvider, + provider as starknetProvider, +} from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { @@ -9,7 +13,7 @@ import { ExplorerFamily, ZChainName, } from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; @@ -48,16 +52,23 @@ export async function createChainConfig({ }) { logBlue('Creating a new chain config'); + const protocol = (await select({ + message: 'Select the chain protocol type:', + choices: Object.entries(ProtocolType).map(([_, value]) => ({ value })), + pageSize: Object.entries(ProtocolType).length, + })) as ProtocolType; + + assert( + protocol === ProtocolType.Ethereum || protocol === ProtocolType.Starknet, + 'Protocol type not supported yet!', + ); + const rpcUrl = await detectAndConfirmOrPrompt( - async () => { - await new ethers.providers.JsonRpcProvider().getNetwork(); - return ethers.providers.JsonRpcProvider.defaultUrl(); - }, + createProtocolDefaultProviderDetector(protocol), 'Enter http or https', 'rpc url', 'JSON RPC provider', ); - const provider = new ethers.providers.JsonRpcProvider(rpcUrl); const name = await input({ message: 'Enter chain name (one word, lower case)', @@ -69,17 +80,14 @@ export async function createChainConfig({ default: name[0].toUpperCase() + name.slice(1), }); - const chainId = parseInt( + const chainId = formatChainIdBasedOnProtocol( await detectAndConfirmOrPrompt( - async () => { - const network = await provider.getNetwork(); - return network.chainId.toString(); - }, - 'Enter a (number)', + createProtocolChainIdDetector(protocol, rpcUrl), + protocol === ProtocolType.Starknet ? 'Enter a (hex)' : 'Enter a (number)', 'chain id', 'JSON RPC provider', ), - 10, + protocol, ); const isTestnet = await confirm({ @@ -91,8 +99,12 @@ export async function createChainConfig({ name, displayName, chainId, - domainId: chainId, - protocol: ProtocolType.Ethereum, + // TODO: Agree on a uniqe way to generate number domain id for starknet chains + domainId: + typeof chainId === 'string' + ? parseInt((parseInt(chainId.slice(-4)) / 10 ** 18).toString()) + : chainId, + protocol: protocol, rpcUrls: [{ http: rpcUrl }], isTestnet, }; @@ -265,3 +277,41 @@ async function addNativeTokenConfig(metadata: ChainMetadata): Promise { decimals: decimals ? parseInt(decimals, 10) : 18, }; } + +function createProtocolDefaultProviderDetector( + protocol: ProtocolType.Ethereum | ProtocolType.Starknet, +) { + switch (protocol) { + case ProtocolType.Ethereum: + return async () => { + return ethers.providers.JsonRpcProvider.defaultUrl(); + }; + case ProtocolType.Starknet: + return async () => { + return starknetProvider.getDefaultNodeUrl(); + }; + } +} + +function createProtocolChainIdDetector( + protocol: ProtocolType.Ethereum | ProtocolType.Starknet, + rpcUrl: string, +) { + return async () => { + switch (protocol) { + case ProtocolType.Ethereum: { + const network = await new ethers.providers.JsonRpcProvider( + rpcUrl, + ).getNetwork(); + return network.chainId.toString(); + } + case ProtocolType.Starknet: + return new StarknetProvider({ nodeUrl: rpcUrl }).getChainId(); + } + }; +} + +function formatChainIdBasedOnProtocol(chainId: string, protocol: ProtocolType) { + if (protocol === ProtocolType.Starknet) return chainId; + return parseInt(chainId, 10); +} From 9c43d1c27cceab8343764db15db43f5f475013d6 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 11 Nov 2024 15:50:50 +0100 Subject: [PATCH 027/132] fix: e2e test working --- typescript/cli/src/commands/send.ts | 4 +-- typescript/cli/src/commands/signCommands.ts | 9 ++++- typescript/cli/src/commands/strategy.ts | 2 +- typescript/cli/src/commands/warp.ts | 1 + .../signer/OriginDestinationSignerStrategy.ts | 7 ++-- .../strategies/signer/SignerStrategy.ts | 5 --- .../signer/SignerStrategyFactory.ts | 35 +++++++++++------- ...trategy.ts => WarpConfigSignerStrategy.ts} | 36 ++++--------------- .../strategies/submitter/JsonRpcStrategy.ts | 2 +- typescript/cli/src/send/transfer.ts | 21 ++--------- typescript/cli/src/tests/commands/helpers.ts | 8 +++++ 11 files changed, 54 insertions(+), 76 deletions(-) rename typescript/cli/src/context/strategies/signer/{WarpDeploySignerStrategy.ts => WarpConfigSignerStrategy.ts} (72%) diff --git a/typescript/cli/src/commands/send.ts b/typescript/cli/src/commands/send.ts index 1167b3b5599..27f59a52dc1 100644 --- a/typescript/cli/src/commands/send.ts +++ b/typescript/cli/src/commands/send.ts @@ -46,8 +46,8 @@ export const messageOptions: { [k: string]: Options } = { }; export interface MessageOptionsArgTypes { - origin?: string; - destination?: string; + origin: string; + destination: string; timeout: number; quick: boolean; relay: boolean; diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index e420677958c..7cf23d129a5 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -1,7 +1,14 @@ // Commands that send tx and require a key to sign. // It's useful to have this listed here so the context // middleware can request keys up front when required. -export const SIGN_COMMANDS = ['deploy', 'send', 'status', 'submit', 'apply']; +export const SIGN_COMMANDS = [ + 'deploy', + 'send', + 'status', + 'submit', + 'apply', + 'read', +]; export function isSignCommand(argv: any): boolean { return ( diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index 8bc92a43657..687deb7e9be 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -51,7 +51,7 @@ export const init: CommandModuleWithWriteContext<{ config: outputFileCommandOption( DEFAULT_STRATEGY_CONFIG_PATH, false, - 'The path to output a Key Config JSON or YAML file.', + 'The path to output a Strategy Config JSON or YAML file.', ), type: { type: 'string', diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index 388c478b032..b7fb4562430 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -138,6 +138,7 @@ export const deploy: CommandModuleWithWriteContext<{ try { await runWarpRouteDeploy({ context, + warpRouteDeploymentConfigPath: config, }); } catch (error: any) { evaluateIfDryRunFailure(error, dryRun); diff --git a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts index ce441206f9f..85e63e2e25d 100644 --- a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts @@ -29,11 +29,10 @@ export class OriginDestinationSignerStrategy implements SignerStrategy { 'Select the destination chain', ); } - const chains = [origin, destination]; - argv.chains = chains; + argv.origin = origin; - argv.destination = origin; - return chains; // Explicitly return as single-item array + argv.destination = destination; + return [origin, destination]; // Explicitly return as single-item array } createContextManager( diff --git a/typescript/cli/src/context/strategies/signer/SignerStrategy.ts b/typescript/cli/src/context/strategies/signer/SignerStrategy.ts index 459796876aa..ddb92f2a7b8 100644 --- a/typescript/cli/src/context/strategies/signer/SignerStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/SignerStrategy.ts @@ -6,11 +6,6 @@ import { import { ContextManager } from '../../manager/ContextManager.js'; -export interface WarpDeployContextResult { - warpRouteConfig: Record; - chains: ChainName[]; -} - export interface SignerStrategy { /** * Determines the chains to be used for signing diff --git a/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts b/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts index 8b259098f59..ae4c00eeefd 100644 --- a/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts +++ b/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts @@ -1,23 +1,32 @@ import { OriginDestinationSignerStrategy } from './OriginDestinationSignerStrategy.js'; import { SignerStrategy } from './SignerStrategy.js'; import { SingleChainSignerStrategy } from './SingleChainSignerStrategy.js'; -import { WarpDeploySignerStrategy } from './WarpDeploySignerStrategy.js'; +import { WarpConfigSignerStrategy } from './WarpConfigSignerStrategy.js'; -export class SignerStrategyFactory { - static createStrategy(argv: Record): SignerStrategy { - const strategyMap: Record SignerStrategy> = { - 'core:apply': () => new SingleChainSignerStrategy(), - 'warp:deploy': () => new WarpDeploySignerStrategy(), - 'warp:send': () => new WarpDeploySignerStrategy(), // Assuming same strategy for 'send' - 'warp:apply': () => new WarpDeploySignerStrategy(), // Assuming same strategy for 'appl' - 'send:message': () => new OriginDestinationSignerStrategy(), - }; +enum CommandType { + CORE_APPLY = 'core:apply', + WARP_DEPLOY = 'warp:deploy', + WARP_SEND = 'warp:send', + WARP_APPLY = 'warp:apply', + WARP_READ = 'warp:read', + WARP_MESSAGE = 'send:message', +} - const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim(); +export class SignerStrategyFactory { + private static strategyMap: Map SignerStrategy> = new Map([ + [CommandType.CORE_APPLY, () => new SingleChainSignerStrategy()], + [CommandType.WARP_DEPLOY, () => new WarpConfigSignerStrategy()], + [CommandType.WARP_SEND, () => new OriginDestinationSignerStrategy()], + [CommandType.WARP_APPLY, () => new WarpConfigSignerStrategy()], + [CommandType.WARP_READ, () => new SingleChainSignerStrategy()], + [CommandType.WARP_MESSAGE, () => new OriginDestinationSignerStrategy()], + ]); + static createStrategy(argv: Record): SignerStrategy { + const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; const createStrategy = - strategyMap[commandKey] || (() => new SingleChainSignerStrategy()); - + this.strategyMap.get(commandKey) || + (() => new SingleChainSignerStrategy()); return createStrategy(); } } diff --git a/typescript/cli/src/context/strategies/signer/WarpDeploySignerStrategy.ts b/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts similarity index 72% rename from typescript/cli/src/context/strategies/signer/WarpDeploySignerStrategy.ts rename to typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts index 48a6e346074..88a6aea2145 100644 --- a/typescript/cli/src/context/strategies/signer/WarpDeploySignerStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts @@ -1,5 +1,3 @@ -import { confirm } from '@inquirer/prompts'; - import { ChainName, ChainSubmissionStrategy, @@ -17,15 +15,10 @@ import { ContextManager } from '../../manager/ContextManager.js'; import { SignerStrategy } from './SignerStrategy.js'; -export interface WarpDeployContextResult { - warpRouteConfig: Record; - chains: ChainName[]; -} - -export class WarpDeploySignerStrategy implements SignerStrategy { +export class WarpConfigSignerStrategy implements SignerStrategy { async determineChains(argv: Record): Promise { - const configPath = argv.wd || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; - const { chains } = await getWarpDeployContext({ + const configPath = argv.config || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; + const { chains } = await getWarpConfigChains({ configPath, skipConfirmation: argv.skipConfirmation, }); @@ -60,13 +53,13 @@ export class WarpDeploySignerStrategy implements SignerStrategy { } } -export async function getWarpDeployContext({ +export async function getWarpConfigChains({ configPath = DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, skipConfirmation = false, }: { configPath?: string; skipConfirmation?: boolean; -}): Promise { +}): Promise<{ chains: ChainName[] }> { // Validate config path if (!configPath || !isFile(configPath)) { if (skipConfirmation) { @@ -96,22 +89,5 @@ export async function getWarpDeployContext({ throw new Error('No chains found in warp route deployment config'); } - // Optional: Confirm multi-chain deployment - if (!skipConfirmation && chains.length > 1) { - const confirmMultiChain = await confirm({ - message: `Deploy warp route across ${chains.length} chains: ${chains.join( - ', ', - )}?`, - default: true, - }); - - if (!confirmMultiChain) { - throw new Error('Deployment cancelled by user'); - } - } - - return { - warpRouteConfig, - chains, - }; + return { chains }; } diff --git a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts index 55ca6369312..ad9adeda18c 100644 --- a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts +++ b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts @@ -7,7 +7,7 @@ import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; export class JsonRpcStrategy extends BaseSubmitterStrategy { async getPrivateKey(chain: ChainName): Promise { - let pk = this.config[chain]?.submitter?.privateKey; + let pk = this.config[chain]?.submitter?.privateKey; // HYP_KEY for backwards compatibility if (!pk) { pk = await password({ diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index 2e94fdd53fe..75205a7c57d 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -18,7 +18,6 @@ import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { WriteCommandContext } from '../context/types.js'; import { runPreflightChecksForChains } from '../deploy/utils.js'; import { log, logBlue, logGreen, logRed } from '../logger.js'; -import { runSingleChainSelectionStep } from '../utils/chains.js'; import { indentYamlOrJson } from '../utils/files.js'; import { stubMerkleTreeConfig } from '../utils/relay.js'; import { runTokenSelectionStep } from '../utils/tokens.js'; @@ -40,30 +39,14 @@ export async function sendTestTransfer({ }: { context: WriteCommandContext; warpCoreConfig: WarpCoreConfig; - origin?: ChainName; - destination?: ChainName; + origin: ChainName; + destination: ChainName; amount: string; recipient?: string; timeoutSec: number; skipWaitForDelivery: boolean; selfRelay?: boolean; }) { - const { chainMetadata } = context; - - if (!origin) { - origin = await runSingleChainSelectionStep( - chainMetadata, - 'Select the origin chain', - ); - } - - if (!destination) { - destination = await runSingleChainSelectionStep( - chainMetadata, - 'Select the destination chain', - ); - } - await runPreflightChecksForChains({ context, chains: [origin, destination], diff --git a/typescript/cli/src/tests/commands/helpers.ts b/typescript/cli/src/tests/commands/helpers.ts index c4ad0365194..f689d8c61ae 100644 --- a/typescript/cli/src/tests/commands/helpers.ts +++ b/typescript/cli/src/tests/commands/helpers.ts @@ -1,3 +1,5 @@ +import { ethers } from 'ethers'; + import { ERC20Test__factory, ERC4626Test__factory } from '@hyperlane-xyz/core'; import { ChainAddresses } from '@hyperlane-xyz/registry'; import { @@ -136,6 +138,9 @@ export async function deployToken(privateKey: string, chain: string) { key: privateKey, }); + // Future works: make signer compatible with protocol/chain stack + multiProvider.setSigner(chain, new ethers.Wallet(privateKey)); + const token = await new ERC20Test__factory( multiProvider.getSigner(chain), ).deploy('token', 'token', '100000000000000000000', 18); @@ -155,6 +160,9 @@ export async function deploy4626Vault( key: privateKey, }); + // Future works: make signer compatible with protocol/chain stack + multiProvider.setSigner(chain, new ethers.Wallet(privateKey)); + const vault = await new ERC4626Test__factory( multiProvider.getSigner(chain), ).deploy(tokenAddress, 'VAULT', 'VAULT'); From 3602abd9177e7ccfaf5d21a6e899ad752c2b8606 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 11 Nov 2024 17:39:18 +0100 Subject: [PATCH 028/132] chore: add comments & minor refactoring --- typescript/cli/src/config/strategy.ts | 4 +- typescript/cli/src/context/context.ts | 11 ++-- .../signer/OriginDestinationSignerStrategy.ts | 32 +++++++++--- .../strategies/signer/SignerStrategy.ts | 14 +++--- .../signer/SingleChainSignerStrategy.ts | 34 ++++++++++--- .../signer/WarpConfigSignerStrategy.ts | 50 +++++++++++++------ .../submitter/GnosisSafeStrategy.ts | 2 +- .../submitter/SubmitterContext.ts} | 23 +++++---- typescript/cli/src/send/message.ts | 21 +------- 9 files changed, 116 insertions(+), 75 deletions(-) rename typescript/cli/src/context/{manager/ContextManager.ts => strategies/submitter/SubmitterContext.ts} (76%) diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index 36925250a67..547d71a49c3 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -2,6 +2,7 @@ import { ChainSubmissionStrategy, ChainSubmissionStrategySchema, } from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; import { readYamlOrJson } from '../utils/files.js'; @@ -9,8 +10,7 @@ export async function readDefaultStrategyConfig( filePath: string, ): Promise { let config = readYamlOrJson(filePath); - if (!config) - throw new Error(`No default strategy config found at ${filePath}`); + assert(config, `No default strategy config found at ${filePath}`); return ChainSubmissionStrategySchema.parse(config); } diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index bae7754b840..4c1d5ce901d 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -71,22 +71,23 @@ export async function signerMiddleware(argv: Record) { const signerStrategy = SignerStrategyFactory.createStrategy(argv); // Determine the chains that will be used for signing based on the selected strategy - // e.g. SingleChainSignerStrategy extracts jsonRpc private key from strategyConfig else prompts user PK input + // e.g. SingleChainSignerStrategy extracts jsonRpc private key from strategyConfig else prompts user private key input const chains = await signerStrategy.determineChains(argv); - // Create a context manager for the signer, which will manage the signing context for the specified chains + // Creates a submitter context for the signer, which manages the signing context for the specified chains // default: TxSubmitterType.JSON_RPC - const signerContextManager = signerStrategy.createContextManager( + const signerSubmitterContext = signerStrategy.createSubmitterContext( chains, strategyConfig, argv, ); - // Configure the signers using the selected strategy, multiProvider, and context manager + // Configure the signers using the selected strategy, multiProvider, and submitter context + // manipulates argv values await signerStrategy.configureSigners( argv, multiProvider, - signerContextManager, + signerSubmitterContext, ); return { ...argv, strategy: strategyUrl }; diff --git a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts index 85e63e2e25d..80a50804264 100644 --- a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts @@ -6,11 +6,20 @@ import { } from '@hyperlane-xyz/sdk'; import { runSingleChainSelectionStep } from '../../../utils/chains.js'; -import { ContextManager } from '../../manager/ContextManager.js'; +import { SubmitterContext } from '../submitter/SubmitterContext.js'; import { SignerStrategy } from './SignerStrategy.js'; +/** + * @title OriginDestinationSignerStrategy + * @notice Strategy implementation for managing multiVM operations requiring both origin and destination chains + * @dev This strategy is used by the SignerStrategyFactory for sending messages and tokens across chains + */ export class OriginDestinationSignerStrategy implements SignerStrategy { + /** + * @notice Determines and validates the origin and destination chains + * @dev If origin or destination are not provided in argv, prompts user for interactive selection + */ async determineChains(argv: Record): Promise { const { context } = argv; let origin = argv.origin; @@ -35,12 +44,15 @@ export class OriginDestinationSignerStrategy implements SignerStrategy { return [origin, destination]; // Explicitly return as single-item array } - createContextManager( + /** + * @dev Hardcoded: JSON_RPC as the transaction submitter type + */ + createSubmitterContext( chains: ChainName[], strategyConfig: ChainSubmissionStrategy, - argv?: any, - ): ContextManager { - return new ContextManager( + argv?: Record, + ): SubmitterContext { + return new SubmitterContext( strategyConfig, chains, TxSubmitterType.JSON_RPC, @@ -48,14 +60,18 @@ export class OriginDestinationSignerStrategy implements SignerStrategy { ); } + /** + * @notice Configures signers for both origin and destination chains + * @dev Sets up signers in the MultiProvider and updates the context with necessary references + */ async configureSigners( argv: Record, multiProvider: MultiProvider, - contextManager: ContextManager, + submitterContext: SubmitterContext, ): Promise { - const signers = await contextManager.getSigners(); + const signers = await submitterContext.getSigners(); multiProvider.setSigners(signers); argv.context.multiProvider = multiProvider; - argv.contextManager = contextManager; + argv.submitterContext = submitterContext; } } diff --git a/typescript/cli/src/context/strategies/signer/SignerStrategy.ts b/typescript/cli/src/context/strategies/signer/SignerStrategy.ts index ddb92f2a7b8..1b802831d12 100644 --- a/typescript/cli/src/context/strategies/signer/SignerStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/SignerStrategy.ts @@ -4,7 +4,7 @@ import { MultiProvider, } from '@hyperlane-xyz/sdk'; -import { ContextManager } from '../../manager/ContextManager.js'; +import { SubmitterContext } from '../submitter/SubmitterContext.js'; export interface SignerStrategy { /** @@ -18,23 +18,23 @@ export interface SignerStrategy { * Creates a context manager for the selected chains * @param chains Selected chains * @param strategyConfig Default strategy configuration - * @returns ContextManager instance + * @returns SubmitterContext instance */ - createContextManager( + createSubmitterContext( chains: ChainName[], strategyConfig: ChainSubmissionStrategy, - argv?: any, - ): ContextManager; + argv?: Record, + ): SubmitterContext; /** * Configures signers for the multi-provider * @param argv Command arguments * @param multiProvider MultiProvider instance - * @param contextManager ContextManager instance + * @param submitterContext SubmitterContext instance */ configureSigners( argv: Record, multiProvider: MultiProvider, - contextManager: ContextManager, + submitterContext: SubmitterContext, ): Promise; } diff --git a/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts index a7c9b154a21..26bad17c713 100644 --- a/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts @@ -6,11 +6,22 @@ import { } from '@hyperlane-xyz/sdk'; import { runSingleChainSelectionStep } from '../../../utils/chains.js'; -import { ContextManager } from '../../manager/ContextManager.js'; +import { SubmitterContext } from '../submitter/SubmitterContext.js'; import { SignerStrategy } from './SignerStrategy.js'; +/** + * @title SingleChainSignerStrategy + * @notice Strategy implementation for managing single-chain operations + * @dev This strategy is used by commands that operate on a single blockchain + * It implements the SignerStrategy interface and is primarily used for + * operations like 'core:apply' and 'warp:read' (see SignerStrategyFactory) + */ export class SingleChainSignerStrategy implements SignerStrategy { + /** + * @notice Determines the chain to be used for signing operations + * @dev Either uses the chain specified in argv or prompts for interactive selection + */ async determineChains(argv: Record): Promise { const chain: ChainName = argv.chain || @@ -23,12 +34,15 @@ export class SingleChainSignerStrategy implements SignerStrategy { return [chain]; // Explicitly return as single-item array } - createContextManager( + /** + * @dev Hardcoded: JSON_RPC as the transaction submitter type + */ + createSubmitterContext( chains: ChainName[], strategyConfig: ChainSubmissionStrategy, - argv?: any, - ): ContextManager { - return new ContextManager( + argv?: Record, + ): SubmitterContext { + return new SubmitterContext( strategyConfig, chains, TxSubmitterType.JSON_RPC, @@ -36,14 +50,18 @@ export class SingleChainSignerStrategy implements SignerStrategy { ); } + /** + * @notice Sets up signers for the specified chain in the MultiProvider + * @dev Sets up signers for single chain + */ async configureSigners( argv: Record, multiProvider: MultiProvider, - contextManager: ContextManager, + submitterContext: SubmitterContext, ): Promise { - const signers = await contextManager.getSigners(); + const signers = await submitterContext.getSigners(); multiProvider.setSigners(signers); argv.context.multiProvider = multiProvider; - argv.contextManager = contextManager; + argv.submitterContext = submitterContext; } } diff --git a/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts index 88a6aea2145..ad06bec157d 100644 --- a/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts @@ -4,6 +4,7 @@ import { MultiProvider, TxSubmitterType, } from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; import { @@ -11,11 +12,21 @@ import { readYamlOrJson, runFileSelectionStep, } from '../../../utils/files.js'; -import { ContextManager } from '../../manager/ContextManager.js'; +import { SubmitterContext } from '../submitter/SubmitterContext.js'; import { SignerStrategy } from './SignerStrategy.js'; +/** + * @title WarpConfigSignerStrategy + * @notice Strategy implementation for managing Warp route deployments and configurations + * @dev This strategy is used by commands like 'warp:deploy' and 'warp:apply' + */ export class WarpConfigSignerStrategy implements SignerStrategy { + /** + * @notice Determines the chains to be used based on the Warp configuration file + * @dev Reads and validates a YAML/JSON config file to extract chain information + * If no config is provided, prompts for interactive file selection + */ async determineChains(argv: Record): Promise { const configPath = argv.config || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; const { chains } = await getWarpConfigChains({ @@ -28,12 +39,12 @@ export class WarpConfigSignerStrategy implements SignerStrategy { return chains; } - createContextManager( + createSubmitterContext( chains: ChainName[], strategyConfig: ChainSubmissionStrategy, - argv: any, - ): ContextManager { - return new ContextManager( + argv: Record, + ): SubmitterContext { + return new SubmitterContext( strategyConfig, chains, TxSubmitterType.JSON_RPC, @@ -41,18 +52,25 @@ export class WarpConfigSignerStrategy implements SignerStrategy { ); } + /** + * @dev Sets up signers for all chains [can be one or more] specified in the Warp config + */ async configureSigners( argv: Record, multiProvider: MultiProvider, - contextManager: ContextManager, + submitterContext: SubmitterContext, ): Promise { - const signers = await contextManager.getSigners(); + const signers = await submitterContext.getSigners(); multiProvider.setSigners(signers); argv.context.multiProvider = multiProvider; - argv.contextManager = contextManager; + argv.submitterContext = submitterContext; } } +/** + * @notice Helper function to extract and validate chains from a Warp config file + * @dev Supports both YAML and JSON config formats + */ export async function getWarpConfigChains({ configPath = DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, skipConfirmation = false, @@ -61,10 +79,9 @@ export async function getWarpConfigChains({ skipConfirmation?: boolean; }): Promise<{ chains: ChainName[] }> { // Validate config path + if (!configPath || !isFile(configPath)) { - if (skipConfirmation) { - throw new Error('Warp route deployment config is required'); - } + assert(!skipConfirmation, 'Warp route deployment config is required'); // Interactive file selection if no path provided configPath = await runFileSelectionStep( @@ -78,16 +95,17 @@ export async function getWarpConfigChains({ // Read warp route deployment configuration let warpRouteConfig = readYamlOrJson(configPath); - if (!warpRouteConfig) - throw new Error(`No warp route deploy config found at ${configPath}`); + + assert(warpRouteConfig, `No warp route deploy config found at ${configPath}`); // Extract chains from configuration const chains = Object.keys(warpRouteConfig) as ChainName[]; // Validate chains - if (chains.length === 0) { - throw new Error('No chains found in warp route deployment config'); - } + assert( + chains.length !== 0, + 'No chains found in warp route deployment config', + ); return { chains }; } diff --git a/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts b/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts index 8369be9b00e..44f3686ca24 100644 --- a/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts +++ b/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts @@ -5,7 +5,7 @@ import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; export class GnosisSafeStrategy extends BaseSubmitterStrategy { async getPrivateKey(chain: ChainName): Promise { - // Implement Gnosis Safe specific logic + // Future works: Implement Gnosis Safe specific logic throw new Error('Not implemented'); } diff --git a/typescript/cli/src/context/manager/ContextManager.ts b/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts similarity index 76% rename from typescript/cli/src/context/manager/ContextManager.ts rename to typescript/cli/src/context/strategies/submitter/SubmitterContext.ts index 4cb3f464502..cf09e92d214 100644 --- a/typescript/cli/src/context/manager/ContextManager.ts +++ b/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts @@ -1,16 +1,21 @@ import { Signer } from 'ethers'; -import { ChainName, TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { + ChainName, + ChainSubmissionStrategy, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; -import { ENV } from '../../utils/env.js'; -import { ISubmitterStrategy } from '../strategies/submitter/SubmitterStrategy.js'; -import { SubmitterStrategyFactory } from '../strategies/submitter/SubmitterStrategyFactory.js'; +import { ENV } from '../../../utils/env.js'; + +import { ISubmitterStrategy } from './SubmitterStrategy.js'; +import { SubmitterStrategyFactory } from './SubmitterStrategyFactory.js'; /** - * @title ContextManager + * @title SubmitterContext * @dev Manages the context for transaction submitters, including retrieving chain keys and signers. */ -export class ContextManager { +export class SubmitterContext { private strategy: ISubmitterStrategy; /** @@ -19,10 +24,10 @@ export class ContextManager { * @param submitterType Type of transaction submitter to use. */ constructor( - strategyConfig: any, + strategyConfig: ChainSubmissionStrategy, private chains: ChainName[], submitterType: TxSubmitterType, - private argv?: any, + private argv?: Record, ) { this.strategy = SubmitterStrategyFactory.createStrategy( submitterType, @@ -41,7 +46,7 @@ export class ContextManager { this.chains.map(async (chain) => ({ chainName: chain, privateKey: - this.argv.key || // argv.key overrides strategy key + this.argv?.key || // argv.key overrides strategy key (await this.strategy.getPrivateKey(chain)) || ENV.HYP_KEY, // argv.key and ENV.HYP_KEY for backwards compatibility })), diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index 430d3b7bcfc..5ece5529f9b 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -7,7 +7,6 @@ import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { CommandContext, WriteCommandContext } from '../context/types.js'; import { runPreflightChecksForChains } from '../deploy/utils.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; -import { runSingleChainSelectionStep } from '../utils/chains.js'; import { indentYamlOrJson } from '../utils/files.js'; import { stubMerkleTreeConfig } from '../utils/relay.js'; @@ -21,29 +20,13 @@ export async function sendTestMessage({ selfRelay, }: { context: WriteCommandContext; - origin?: ChainName; - destination?: ChainName; + origin: ChainName; + destination: ChainName; messageBody: string; timeoutSec: number; skipWaitForDelivery: boolean; selfRelay?: boolean; }) { - const { chainMetadata } = context; - - if (!origin) { - origin = await runSingleChainSelectionStep( - chainMetadata, - 'Select the origin chain', - ); - } - - if (!destination) { - destination = await runSingleChainSelectionStep( - chainMetadata, - 'Select the destination chain', - ); - } - await runPreflightChecksForChains({ context, chains: [origin, destination], From ce81b48f89bc415ea659115be17d2d4fae024088 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Tue, 12 Nov 2024 10:01:38 +0100 Subject: [PATCH 029/132] chore: minor fixes --- .../signer/OriginDestinationSignerStrategy.ts | 18 ++++++++---------- .../strategies/signer/SignerStrategyFactory.ts | 4 ++-- typescript/cli/src/context/types.ts | 2 -- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts index 80a50804264..aea8cb1d45e 100644 --- a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts @@ -22,22 +22,20 @@ export class OriginDestinationSignerStrategy implements SignerStrategy { */ async determineChains(argv: Record): Promise { const { context } = argv; - let origin = argv.origin; - let destination = argv.destination; - if (!origin) { - origin = await runSingleChainSelectionStep( + let origin = + argv.origin ?? + (await runSingleChainSelectionStep( context.chainMetadata, 'Select the origin chain', - ); - } + )); - if (!destination) { - destination = await runSingleChainSelectionStep( + let destination = + argv.destination ?? + (await runSingleChainSelectionStep( context.chainMetadata, 'Select the destination chain', - ); - } + )); argv.origin = origin; argv.destination = destination; diff --git a/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts b/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts index ae4c00eeefd..399ea61e655 100644 --- a/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts +++ b/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts @@ -9,7 +9,7 @@ enum CommandType { WARP_SEND = 'warp:send', WARP_APPLY = 'warp:apply', WARP_READ = 'warp:read', - WARP_MESSAGE = 'send:message', + SEND_MESSAGE = 'send:message', } export class SignerStrategyFactory { @@ -19,7 +19,7 @@ export class SignerStrategyFactory { [CommandType.WARP_SEND, () => new OriginDestinationSignerStrategy()], [CommandType.WARP_APPLY, () => new WarpConfigSignerStrategy()], [CommandType.WARP_READ, () => new SingleChainSignerStrategy()], - [CommandType.WARP_MESSAGE, () => new OriginDestinationSignerStrategy()], + [CommandType.SEND_MESSAGE, () => new OriginDestinationSignerStrategy()], ]); static createStrategy(argv: Record): SignerStrategy { diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index eb573e84dcc..79499bd987e 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -10,8 +10,6 @@ import type { WarpRouteDeployConfig, } from '@hyperlane-xyz/sdk'; -// TODO: revisit ContextSettings & CommandContext for improvements - export interface ContextSettings { registryUri: string; registryOverrideUri: string; From 63c4865ae497ff051cc76facd7e9c145514341b2 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:17:24 +0330 Subject: [PATCH 030/132] fix: ism and hook redeader --- typescript/sdk/src/hook/StarknetHookReader.ts | 35 ++++++++++--------- typescript/sdk/src/ism/StarknetIsmReader.ts | 20 +++++------ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/typescript/sdk/src/hook/StarknetHookReader.ts b/typescript/sdk/src/hook/StarknetHookReader.ts index 2707df234c9..6881e5c455d 100644 --- a/typescript/sdk/src/hook/StarknetHookReader.ts +++ b/typescript/sdk/src/hook/StarknetHookReader.ts @@ -1,4 +1,4 @@ -import { Account, Contract, num } from 'starknet'; +import { Account, CairoCustomEnum, Contract, num } from 'starknet'; import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; import { Address, rootLogger } from '@hyperlane-xyz/utils'; @@ -17,12 +17,13 @@ export class StarknetHookReader { const { abi } = getCompiledContract('hook'); const hook = new Contract(abi, address, this.signer); - const hookType = (await hook.hook_type()).toString(); - - switch (hookType) { - case HookType.MERKLE_TREE: // MERKLE_TREE + const hookType: CairoCustomEnum = await hook.hook_type(); + switch (hookType.activeVariant()) { + case 'UNUSED': + return this.deriveUnusedConfig(address); + case 'MERKLE_TREE': return this.deriveMerkleTreeConfig(address); - case HookType.PROTOCOL_FEE: // PROTOCOL_FEE + case 'PROTOCOL_FEE': return this.deriveProtocolFeeConfig(address); default: throw Error; @@ -44,22 +45,24 @@ export class StarknetHookReader { const { abi } = getCompiledContract('protocol_fee'); const hook = new Contract(abi, address, this.signer); - const [owner, maxProtocolFee, protocolFee, beneficiary] = await Promise.all( - [ - hook.owner(), - hook.get_max_protocol_fee(), - hook.get_protocol_fee(), - hook.get_beneficiary(), - ], - ); - + const [owner, protocolFee, beneficiary] = await Promise.all([ + hook.owner(), + hook.get_protocol_fee(), + hook.get_beneficiary(), + ]); return { type: HookType.PROTOCOL_FEE, address, owner: num.toHex64(owner.toString()), - maxProtocolFee: maxProtocolFee.toString(), protocolFee: protocolFee.toString(), beneficiary: num.toHex64(beneficiary.toString()), }; } + + private async deriveUnusedConfig(address: Address) { + return { + type: HookType.MERKLE_TREE, + address, + }; + } } diff --git a/typescript/sdk/src/ism/StarknetIsmReader.ts b/typescript/sdk/src/ism/StarknetIsmReader.ts index d6c63c7f81b..2151b262b57 100644 --- a/typescript/sdk/src/ism/StarknetIsmReader.ts +++ b/typescript/sdk/src/ism/StarknetIsmReader.ts @@ -1,4 +1,4 @@ -import { Account, Contract, num } from 'starknet'; +import { Account, CairoCustomEnum, Contract, num } from 'starknet'; import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; import { Address, rootLogger } from '@hyperlane-xyz/utils'; @@ -22,21 +22,19 @@ export class StarknetIsmReader { address, this.signer, ); - const moduleType = (await ism.module_type()).toString(); - - switch (moduleType) { - case IsmType.PAUSABLE: - case IsmType.TRUSTED_RELAYER: // NULL (Pausable ISM & Trusted Relayer ISM) + const moduleType: CairoCustomEnum = await ism.module_type(); + switch (moduleType.activeVariant()) { + case 'NULL': return this.deriveNullConfig(address); - case IsmType.MESSAGE_ID_MULTISIG: + case 'MESSAGE_ID_MULTISIG': return this.deriveMessageIdMultisigConfig(address); - case IsmType.MERKLE_ROOT_MULTISIG: + case 'MERKLE_ROOT_MULTISIG': return this.deriveMerkleRootMultisigConfig(address); - case IsmType.ROUTING: + case 'ROUTING': return this.deriveRoutingConfig(address); - case IsmType.FALLBACK_ROUTING: + case 'FALLBACK_ROUTING': return this.deriveFallbackRoutingConfig(address); - case IsmType.AGGREGATION: + case 'AGGREGATION': return this.deriveAggregationConfig(address); default: return { From e4839a135ff3e6c97856d93073b9d0e5b096acda Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Tue, 12 Nov 2024 16:42:02 +0100 Subject: [PATCH 031/132] perf: removed unused code and refactoring --- typescript/cli/src/commands/options.ts | 7 + typescript/cli/src/commands/signCommands.ts | 9 +- typescript/cli/src/commands/strategy.ts | 32 ++-- typescript/cli/src/config/strategy.ts | 2 +- typescript/cli/src/context/context.ts | 28 +-- .../strategies/chain/ChainCommandHandler.ts | 34 ++++ .../strategies/chain/MultiChainHandler.ts | 166 ++++++++++++++++++ .../SingleChainHandler.ts} | 15 +- .../SignerStrategy.ts => chain/types.ts} | 2 +- .../signer/OriginDestinationSignerStrategy.ts | 75 -------- .../signer/SignerStrategyFactory.ts | 32 ---- .../signer/WarpConfigSignerStrategy.ts | 111 ------------ .../submitter/GnosisSafeStrategy.ts | 15 -- .../strategies/submitter/JsonRpcStrategy.ts | 16 +- .../strategies/submitter/SubmitterContext.ts | 22 ++- .../strategies/submitter/SubmitterStrategy.ts | 4 +- .../submitter/SubmitterStrategyFactory.ts | 10 +- typescript/cli/src/deploy/agent.ts | 1 + .../submitter/ethersV5/schemas.ts | 1 + 19 files changed, 278 insertions(+), 304 deletions(-) create mode 100644 typescript/cli/src/context/strategies/chain/ChainCommandHandler.ts create mode 100644 typescript/cli/src/context/strategies/chain/MultiChainHandler.ts rename typescript/cli/src/context/strategies/{signer/SingleChainSignerStrategy.ts => chain/SingleChainHandler.ts} (79%) rename typescript/cli/src/context/strategies/{signer/SignerStrategy.ts => chain/types.ts} (96%) delete mode 100644 typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts delete mode 100644 typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts delete mode 100644 typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts delete mode 100644 typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 7f34ac8cb01..96be4c77a85 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -128,6 +128,13 @@ export const chainTargetsCommandOption: Options = { alias: 'c', }; +export const strategyConfigUrlCommandOption: Options = { + type: 'string', + description: 'A path to a JSON or YAML file with a strategy config.', + default: DEFAULT_STRATEGY_CONFIG_PATH, + alias: 's', +}; + export const outputFileCommandOption = ( defaultPath?: string, demandOption = false, diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 7cf23d129a5..d80dc9c0f59 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -1,14 +1,7 @@ // Commands that send tx and require a key to sign. // It's useful to have this listed here so the context // middleware can request keys up front when required. -export const SIGN_COMMANDS = [ - 'deploy', - 'send', - 'status', - 'submit', - 'apply', - 'read', -]; +export const SIGN_COMMANDS = ['deploy', 'send', 'status', 'apply']; export function isSignCommand(argv: any): boolean { return ( diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index 687deb7e9be..b6456465400 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -101,22 +101,22 @@ export const init: CommandModuleWithWriteContext<{ })), })); - let submitter: any = { + const submitter: any = { type: type, }; // Configure submitter based on type switch (type) { case TxSubmitterType.JSON_RPC: - const privateKey = await input({ + submitter.privateKey = await input({ message: 'Enter your private key', validate: (pk) => isValidPrivateKey(pk), }); - submitter.privateKey = privateKey; + submitter.chain = chain; break; case TxSubmitterType.IMPERSONATED_ACCOUNT: - const userAddress = + submitter.userAddress = inputUserAddress || (await input({ message: 'Enter the user address to impersonate', @@ -131,15 +131,14 @@ export const init: CommandModuleWithWriteContext<{ }, })); assert( - userAddress, + submitter.userAddress, 'User address is required for impersonated account', ); - submitter.userAddress = userAddress; break; case TxSubmitterType.GNOSIS_SAFE: case TxSubmitterType.GNOSIS_TX_BUILDER: - const safeAddress = + submitter.safeAddress = inputSafeAddress || (await input({ message: 'Enter the Safe address', @@ -153,20 +152,17 @@ export const init: CommandModuleWithWriteContext<{ } }, })); - assert(safeAddress, 'Safe address is required for Gnosis Safe'); - - submitter = { - type: type, - chain: chain, - safeAddress: safeAddress, - }; + assert( + submitter.safeAddress, + 'Safe address is required for Gnosis Safe', + ); + submitter.chain = chain; if (type === TxSubmitterType.GNOSIS_TX_BUILDER) { - const version = await input({ + submitter.version = await input({ message: 'Enter the Safe version (default: 1.0)', default: '1.0', }); - submitter.version = version; } break; @@ -174,8 +170,8 @@ export const init: CommandModuleWithWriteContext<{ throw new Error(`Unsupported submitter type: ${type}`); } - let result: ChainSubmissionStrategy = { - ...defaultStrategy, + const result: ChainSubmissionStrategy = { + ...defaultStrategy, // if there are changes in ChainSubmissionStrategy, the defaultStrategy may no longer be compatible [chain]: { submitter: submitter, }, diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index 547d71a49c3..48c668993f8 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -9,7 +9,7 @@ import { readYamlOrJson } from '../utils/files.js'; export async function readDefaultStrategyConfig( filePath: string, ): Promise { - let config = readYamlOrJson(filePath); + const config = readYamlOrJson(filePath); assert(config, `No default strategy config found at ${filePath}`); return ChainSubmissionStrategySchema.parse(config); diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 3545972a5f5..8417bd123d2 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -16,9 +16,9 @@ import { } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; -import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; import { isSignCommand } from '../commands/signCommands.js'; import { readDefaultStrategyConfig } from '../config/strategy.js'; +// import { readDefaultStrategyConfig } from '../config/strategy.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; @@ -26,7 +26,7 @@ import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner } from '../utils/keys.js'; -import { SignerStrategyFactory } from './strategies/signer/SignerStrategyFactory.js'; +import { ChainCommandHandler } from './strategies/chain/ChainCommandHandler.js'; import { CommandContext, ContextSettings, @@ -63,20 +63,24 @@ export async function signerMiddleware(argv: Record) { if (!requiresKey) return argv; - const strategyUrl = argv.strategy || DEFAULT_STRATEGY_CONFIG_PATH; - const strategyConfig = await readDefaultStrategyConfig(strategyUrl); - // Select the appropriate signing strategy based on the provided hyperlane command - // e.g command `core deploy` uses SingleChainSignerStrategy - const signerStrategy = SignerStrategyFactory.createStrategy(argv); + // e.g command `core deploy` uses SingleChainHandler + const chainHandler = ChainCommandHandler.getHandler(argv); // Determine the chains that will be used for signing based on the selected strategy - // e.g. SingleChainSignerStrategy extracts jsonRpc private key from strategyConfig else prompts user private key input - const chains = await signerStrategy.determineChains(argv); + // e.g. SingleChainHandler extracts jsonRpc private key from strategyConfig else prompts user private key input + const chains = await chainHandler.determineChains(argv); + + let strategyConfig = {}; + try { + strategyConfig = await readDefaultStrategyConfig(argv.strategy); + } catch (error) { + strategyConfig = {}; + } // Creates a submitter context for the signer, which manages the signing context for the specified chains // default: TxSubmitterType.JSON_RPC - const signerSubmitterContext = signerStrategy.createSubmitterContext( + const signerSubmitterContext = chainHandler.createSubmitterContext( chains, strategyConfig, argv, @@ -84,13 +88,13 @@ export async function signerMiddleware(argv: Record) { // Configure the signers using the selected strategy, multiProvider, and submitter context // manipulates argv values - await signerStrategy.configureSigners( + await chainHandler.configureSigners( argv, multiProvider, signerSubmitterContext, ); - return { ...argv, strategy: strategyUrl }; + return argv; } /** diff --git a/typescript/cli/src/context/strategies/chain/ChainCommandHandler.ts b/typescript/cli/src/context/strategies/chain/ChainCommandHandler.ts new file mode 100644 index 00000000000..595282326b1 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/ChainCommandHandler.ts @@ -0,0 +1,34 @@ +import { MultiChainHandler } from './MultiChainHandler.js'; +import { SingleChainHandler } from './SingleChainHandler.js'; +import { ChainHandler } from './types.js'; + +enum CommandType { + CORE_APPLY = 'core:apply', + WARP_DEPLOY = 'warp:deploy', + WARP_SEND = 'warp:send', + WARP_APPLY = 'warp:apply', + WARP_READ = 'warp:read', + SEND_MESSAGE = 'send:message', + AGENT_KURTOSIS = 'deploy:kurtosis-agents', + STATUS = 'status:', +} + +export class ChainCommandHandler { + private static strategyMap: Map ChainHandler> = new Map([ + [CommandType.CORE_APPLY, () => new SingleChainHandler()], + [CommandType.WARP_DEPLOY, () => MultiChainHandler.forWarpConfig()], + [CommandType.WARP_SEND, () => MultiChainHandler.forOriginDestination()], + [CommandType.WARP_APPLY, () => MultiChainHandler.forWarpConfig()], + [CommandType.WARP_READ, () => new SingleChainHandler()], + [CommandType.SEND_MESSAGE, () => MultiChainHandler.forOriginDestination()], + [CommandType.AGENT_KURTOSIS, () => MultiChainHandler.forAgentKurtosis()], + [CommandType.STATUS, () => MultiChainHandler.forOriginDestination()], + ]); + + static getHandler(argv: Record): ChainHandler { + const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; + const createStrategy = + this.strategyMap.get(commandKey) || (() => new SingleChainHandler()); + return createStrategy(); + } +} diff --git a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts new file mode 100644 index 00000000000..ac9381abb66 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts @@ -0,0 +1,166 @@ +import { + ChainName, + ChainSubmissionStrategy, + MultiProvider, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; + +import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; +import { + runMultiChainSelectionStep, + runSingleChainSelectionStep, +} from '../../../utils/chains.js'; +import { + isFile, + readYamlOrJson, + runFileSelectionStep, +} from '../../../utils/files.js'; +import { SubmitterContext } from '../submitter/SubmitterContext.js'; + +import { ChainHandler } from './types.js'; + +enum ChainSelectionMode { + ORIGIN_DESTINATION, + AGENT_KURTOSIS, + WARP_CONFIG, +} + +export class MultiChainHandler implements ChainHandler { + constructor(private mode: ChainSelectionMode) {} + + async determineChains(argv: Record): Promise { + const { context } = argv; + + switch (this.mode) { + case ChainSelectionMode.WARP_CONFIG: + return this.determineWarpConfigChains(argv); + case ChainSelectionMode.AGENT_KURTOSIS: + return this.determineAgentChains(argv, context); + case ChainSelectionMode.ORIGIN_DESTINATION: + default: + return this.determineOriginDestinationChains(argv, context); + } + } + + private async determineWarpConfigChains( + argv: Record, + ): Promise { + argv.config = argv.config || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; + argv.context.chains = await this.getWarpConfigChains( + argv.config, + argv.skipConfirmation, + ); + return argv.context.chains; + } + + private async determineAgentChains( + argv: Record, + context: any, + ): Promise { + argv.origin = + argv.origin ?? + (await runSingleChainSelectionStep( + context.chainMetadata, + 'Select the origin chain', + )); + + if (!argv.targets) { + const selectedRelayChains = await runMultiChainSelectionStep({ + chainMetadata: context.chainMetadata, + message: 'Select chains to relay between', + requireNumber: 2, + }); + argv.targets = selectedRelayChains.join(','); + } + + return [argv.origin, ...argv.targets]; + } + + private async determineOriginDestinationChains( + argv: Record, + context: any, + ): Promise { + argv.origin = + argv.origin ?? + (await runSingleChainSelectionStep( + context.chainMetadata, + 'Select the origin chain', + )); + + argv.destination = + argv.destination ?? + (await runSingleChainSelectionStep( + context.chainMetadata, + 'Select the destination chain', + )); + + return [argv.origin, argv.destination]; + } + + private async getWarpConfigChains( + configPath: string, + skipConfirmation: boolean, + ): Promise { + if (!configPath || !isFile(configPath)) { + assert(!skipConfirmation, 'Warp route deployment config is required'); + configPath = await runFileSelectionStep( + './configs', + 'Warp route deployment config', + 'warp', + ); + } else { + console.log(`Using warp route deployment config at ${configPath}`); + } + + const warpRouteConfig = readYamlOrJson(configPath); + assert( + warpRouteConfig, + `No warp route deploy config found at ${configPath}`, + ); + + const chains = Object.keys(warpRouteConfig) as ChainName[]; + assert( + chains.length !== 0, + 'No chains found in warp route deployment config', + ); + + return chains; + } + + createSubmitterContext( + chains: ChainName[], + strategyConfig: ChainSubmissionStrategy, + argv?: Record, + ): SubmitterContext { + return new SubmitterContext( + strategyConfig, + chains, + TxSubmitterType.JSON_RPC, + argv, + ); + } + + async configureSigners( + argv: Record, + multiProvider: MultiProvider, + submitterContext: SubmitterContext, + ): Promise { + const signers = await submitterContext.getSigners(); + multiProvider.setSigners(signers); + argv.context.multiProvider = multiProvider; + argv.submitterContext = submitterContext; + } + + static forOriginDestination(): MultiChainHandler { + return new MultiChainHandler(ChainSelectionMode.ORIGIN_DESTINATION); + } + + static forAgentKurtosis(): MultiChainHandler { + return new MultiChainHandler(ChainSelectionMode.AGENT_KURTOSIS); + } + + static forWarpConfig(): MultiChainHandler { + return new MultiChainHandler(ChainSelectionMode.WARP_CONFIG); + } +} diff --git a/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts b/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts similarity index 79% rename from typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts rename to typescript/cli/src/context/strategies/chain/SingleChainHandler.ts index 26bad17c713..37d6c36c691 100644 --- a/typescript/cli/src/context/strategies/signer/SingleChainSignerStrategy.ts +++ b/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts @@ -8,30 +8,29 @@ import { import { runSingleChainSelectionStep } from '../../../utils/chains.js'; import { SubmitterContext } from '../submitter/SubmitterContext.js'; -import { SignerStrategy } from './SignerStrategy.js'; +import { ChainHandler } from './types.js'; /** - * @title SingleChainSignerStrategy + * @title SingleChainHandler * @notice Strategy implementation for managing single-chain operations * @dev This strategy is used by commands that operate on a single blockchain - * It implements the SignerStrategy interface and is primarily used for - * operations like 'core:apply' and 'warp:read' (see SignerStrategyFactory) + * It implements the ChainHandler interface and is primarily used for + * operations like 'core:apply' and 'warp:read' */ -export class SingleChainSignerStrategy implements SignerStrategy { +export class SingleChainHandler implements ChainHandler { /** * @notice Determines the chain to be used for signing operations * @dev Either uses the chain specified in argv or prompts for interactive selection */ async determineChains(argv: Record): Promise { - const chain: ChainName = + argv.chain = argv.chain || (await runSingleChainSelectionStep( argv.context.chainMetadata, 'Select chain to connect:', )); - argv.chain = chain; - return [chain]; // Explicitly return as single-item array + return [argv.chain]; // Explicitly return as single-item array } /** diff --git a/typescript/cli/src/context/strategies/signer/SignerStrategy.ts b/typescript/cli/src/context/strategies/chain/types.ts similarity index 96% rename from typescript/cli/src/context/strategies/signer/SignerStrategy.ts rename to typescript/cli/src/context/strategies/chain/types.ts index 1b802831d12..5b429739c96 100644 --- a/typescript/cli/src/context/strategies/signer/SignerStrategy.ts +++ b/typescript/cli/src/context/strategies/chain/types.ts @@ -6,7 +6,7 @@ import { import { SubmitterContext } from '../submitter/SubmitterContext.js'; -export interface SignerStrategy { +export interface ChainHandler { /** * Determines the chains to be used for signing * @param argv Command arguments diff --git a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts deleted file mode 100644 index aea8cb1d45e..00000000000 --- a/typescript/cli/src/context/strategies/signer/OriginDestinationSignerStrategy.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - ChainName, - ChainSubmissionStrategy, - MultiProvider, - TxSubmitterType, -} from '@hyperlane-xyz/sdk'; - -import { runSingleChainSelectionStep } from '../../../utils/chains.js'; -import { SubmitterContext } from '../submitter/SubmitterContext.js'; - -import { SignerStrategy } from './SignerStrategy.js'; - -/** - * @title OriginDestinationSignerStrategy - * @notice Strategy implementation for managing multiVM operations requiring both origin and destination chains - * @dev This strategy is used by the SignerStrategyFactory for sending messages and tokens across chains - */ -export class OriginDestinationSignerStrategy implements SignerStrategy { - /** - * @notice Determines and validates the origin and destination chains - * @dev If origin or destination are not provided in argv, prompts user for interactive selection - */ - async determineChains(argv: Record): Promise { - const { context } = argv; - - let origin = - argv.origin ?? - (await runSingleChainSelectionStep( - context.chainMetadata, - 'Select the origin chain', - )); - - let destination = - argv.destination ?? - (await runSingleChainSelectionStep( - context.chainMetadata, - 'Select the destination chain', - )); - - argv.origin = origin; - argv.destination = destination; - return [origin, destination]; // Explicitly return as single-item array - } - - /** - * @dev Hardcoded: JSON_RPC as the transaction submitter type - */ - createSubmitterContext( - chains: ChainName[], - strategyConfig: ChainSubmissionStrategy, - argv?: Record, - ): SubmitterContext { - return new SubmitterContext( - strategyConfig, - chains, - TxSubmitterType.JSON_RPC, - argv, - ); - } - - /** - * @notice Configures signers for both origin and destination chains - * @dev Sets up signers in the MultiProvider and updates the context with necessary references - */ - async configureSigners( - argv: Record, - multiProvider: MultiProvider, - submitterContext: SubmitterContext, - ): Promise { - const signers = await submitterContext.getSigners(); - multiProvider.setSigners(signers); - argv.context.multiProvider = multiProvider; - argv.submitterContext = submitterContext; - } -} diff --git a/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts b/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts deleted file mode 100644 index 399ea61e655..00000000000 --- a/typescript/cli/src/context/strategies/signer/SignerStrategyFactory.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { OriginDestinationSignerStrategy } from './OriginDestinationSignerStrategy.js'; -import { SignerStrategy } from './SignerStrategy.js'; -import { SingleChainSignerStrategy } from './SingleChainSignerStrategy.js'; -import { WarpConfigSignerStrategy } from './WarpConfigSignerStrategy.js'; - -enum CommandType { - CORE_APPLY = 'core:apply', - WARP_DEPLOY = 'warp:deploy', - WARP_SEND = 'warp:send', - WARP_APPLY = 'warp:apply', - WARP_READ = 'warp:read', - SEND_MESSAGE = 'send:message', -} - -export class SignerStrategyFactory { - private static strategyMap: Map SignerStrategy> = new Map([ - [CommandType.CORE_APPLY, () => new SingleChainSignerStrategy()], - [CommandType.WARP_DEPLOY, () => new WarpConfigSignerStrategy()], - [CommandType.WARP_SEND, () => new OriginDestinationSignerStrategy()], - [CommandType.WARP_APPLY, () => new WarpConfigSignerStrategy()], - [CommandType.WARP_READ, () => new SingleChainSignerStrategy()], - [CommandType.SEND_MESSAGE, () => new OriginDestinationSignerStrategy()], - ]); - - static createStrategy(argv: Record): SignerStrategy { - const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; - const createStrategy = - this.strategyMap.get(commandKey) || - (() => new SingleChainSignerStrategy()); - return createStrategy(); - } -} diff --git a/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts b/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts deleted file mode 100644 index ad06bec157d..00000000000 --- a/typescript/cli/src/context/strategies/signer/WarpConfigSignerStrategy.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - ChainName, - ChainSubmissionStrategy, - MultiProvider, - TxSubmitterType, -} from '@hyperlane-xyz/sdk'; -import { assert } from '@hyperlane-xyz/utils'; - -import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; -import { - isFile, - readYamlOrJson, - runFileSelectionStep, -} from '../../../utils/files.js'; -import { SubmitterContext } from '../submitter/SubmitterContext.js'; - -import { SignerStrategy } from './SignerStrategy.js'; - -/** - * @title WarpConfigSignerStrategy - * @notice Strategy implementation for managing Warp route deployments and configurations - * @dev This strategy is used by commands like 'warp:deploy' and 'warp:apply' - */ -export class WarpConfigSignerStrategy implements SignerStrategy { - /** - * @notice Determines the chains to be used based on the Warp configuration file - * @dev Reads and validates a YAML/JSON config file to extract chain information - * If no config is provided, prompts for interactive file selection - */ - async determineChains(argv: Record): Promise { - const configPath = argv.config || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; - const { chains } = await getWarpConfigChains({ - configPath, - skipConfirmation: argv.skipConfirmation, - }); - - argv.context.config = configPath; - argv.context.chains = chains; - return chains; - } - - createSubmitterContext( - chains: ChainName[], - strategyConfig: ChainSubmissionStrategy, - argv: Record, - ): SubmitterContext { - return new SubmitterContext( - strategyConfig, - chains, - TxSubmitterType.JSON_RPC, - argv, - ); - } - - /** - * @dev Sets up signers for all chains [can be one or more] specified in the Warp config - */ - async configureSigners( - argv: Record, - multiProvider: MultiProvider, - submitterContext: SubmitterContext, - ): Promise { - const signers = await submitterContext.getSigners(); - multiProvider.setSigners(signers); - argv.context.multiProvider = multiProvider; - argv.submitterContext = submitterContext; - } -} - -/** - * @notice Helper function to extract and validate chains from a Warp config file - * @dev Supports both YAML and JSON config formats - */ -export async function getWarpConfigChains({ - configPath = DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, - skipConfirmation = false, -}: { - configPath?: string; - skipConfirmation?: boolean; -}): Promise<{ chains: ChainName[] }> { - // Validate config path - - if (!configPath || !isFile(configPath)) { - assert(!skipConfirmation, 'Warp route deployment config is required'); - - // Interactive file selection if no path provided - configPath = await runFileSelectionStep( - './configs', - 'Warp route deployment config', - 'warp', - ); - } else { - console.log(`Using warp route deployment config at ${configPath}`); - } - - // Read warp route deployment configuration - let warpRouteConfig = readYamlOrJson(configPath); - - assert(warpRouteConfig, `No warp route deploy config found at ${configPath}`); - - // Extract chains from configuration - const chains = Object.keys(warpRouteConfig) as ChainName[]; - - // Validate chains - assert( - chains.length !== 0, - 'No chains found in warp route deployment config', - ); - - return { chains }; -} diff --git a/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts b/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts deleted file mode 100644 index 44f3686ca24..00000000000 --- a/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TxSubmitterType } from '@hyperlane-xyz/sdk'; -import { ChainName } from '@hyperlane-xyz/sdk'; - -import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; - -export class GnosisSafeStrategy extends BaseSubmitterStrategy { - async getPrivateKey(chain: ChainName): Promise { - // Future works: Implement Gnosis Safe specific logic - throw new Error('Not implemented'); - } - - getType(): TxSubmitterType { - return TxSubmitterType.GNOSIS_SAFE; - } -} diff --git a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts index ad9adeda18c..66d466e89c6 100644 --- a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts +++ b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts @@ -7,15 +7,17 @@ import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; export class JsonRpcStrategy extends BaseSubmitterStrategy { async getPrivateKey(chain: ChainName): Promise { - let pk = this.config[chain]?.submitter?.privateKey; // HYP_KEY for backwards compatibility + const submitter = this.config[chain]?.submitter as { + type: TxSubmitterType.JSON_RPC; + privateKey?: string; + }; - if (!pk) { - pk = await password({ + return ( + submitter?.privateKey ?? + (await password({ message: `Please enter the private key for chain ${chain}`, - }); - } - - return pk; + })) + ); } getType(): TxSubmitterType { diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts b/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts index cf09e92d214..97571941212 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts +++ b/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts @@ -39,18 +39,22 @@ export class SubmitterContext { * @dev Retrieves the private keys for the specified chains. * @return An array of objects containing chain names and their corresponding private keys. */ - async getChainKeys(): Promise< + private async getChainKeys(): Promise< Array<{ chainName: ChainName; privateKey: string }> > { - const chainKeys = await Promise.all( - this.chains.map(async (chain) => ({ + const chainKeys = []; + + for (const chain of this.chains) { + const privateKey = + this.argv?.key ?? // argv.key overrides strategy private key + (await this.strategy.getPrivateKey(chain)) ?? + ENV.HYP_KEY; // argv.key and ENV.HYP_KEY for backwards compatibility + + chainKeys.push({ chainName: chain, - privateKey: - this.argv?.key || // argv.key overrides strategy key - (await this.strategy.getPrivateKey(chain)) || - ENV.HYP_KEY, // argv.key and ENV.HYP_KEY for backwards compatibility - })), - ); + privateKey: privateKey, + }); + } return chainKeys; } diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts index 10bc3f3123b..fa623515ff2 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts +++ b/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts @@ -1,6 +1,6 @@ import { ethers } from 'ethers'; -import { TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ChainSubmissionStrategy, TxSubmitterType } from '@hyperlane-xyz/sdk'; import { ChainName } from '@hyperlane-xyz/sdk'; export interface ISubmitterStrategy { @@ -10,7 +10,7 @@ export interface ISubmitterStrategy { } export abstract class BaseSubmitterStrategy implements ISubmitterStrategy { - constructor(protected config: any) {} + constructor(protected config: ChainSubmissionStrategy) {} abstract getPrivateKey(chain: ChainName): Promise; diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts index 9c32f992c7d..4efc173760b 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts +++ b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts @@ -1,19 +1,19 @@ -import { TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ChainSubmissionStrategy, TxSubmitterType } from '@hyperlane-xyz/sdk'; -import { GnosisSafeStrategy } from './GnosisSafeStrategy.js'; import { JsonRpcStrategy } from './JsonRpcStrategy.js'; import { ISubmitterStrategy } from './SubmitterStrategy.js'; export class SubmitterStrategyFactory { static createStrategy( type: TxSubmitterType, - config: any, + config: ChainSubmissionStrategy, ): ISubmitterStrategy { switch (type) { case TxSubmitterType.JSON_RPC: return new JsonRpcStrategy(config); - case TxSubmitterType.GNOSIS_SAFE: - return new GnosisSafeStrategy(config); + // TO BE IMPLEMENTED! + // case TxSubmitterType.STARKNET_JSON_RPC: + // return new StarknetJsonRpcStrategy(config); default: throw new Error(`Unsupported submitter type: ${type}`); } diff --git a/typescript/cli/src/deploy/agent.ts b/typescript/cli/src/deploy/agent.ts index ca490fc5fbe..88730629949 100644 --- a/typescript/cli/src/deploy/agent.ts +++ b/typescript/cli/src/deploy/agent.ts @@ -21,6 +21,7 @@ export async function runKurtosisAgentDeploy({ relayChains?: string; agentConfigurationPath?: string; }) { + // TODO: decide what to do with this, since its handled in MultiChainHandler - AGENT_KURTOSIS mode if (!originChain) { originChain = await runSingleChainSelectionStep( context.chainMetadata, diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts index 1586ec6b28b..c5898573705 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts @@ -15,6 +15,7 @@ export const EV5GnosisSafeTxBuilderPropsSchema = z.object({ export const EV5JsonRpcTxSubmitterPropsSchema = z.object({ chain: ZChainName, + privateKey: ZHash.optional(), }); export const EV5ImpersonatedAccountTxSubmitterPropsSchema = From 4abd806989e31ef48dd53cbfe70024e4b7d034ce Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Wed, 13 Nov 2024 15:48:08 +0100 Subject: [PATCH 032/132] feat: masked pk, refactoring --- typescript/cli/cli.ts | 4 +- typescript/cli/src/commands/config.ts | 15 ++ typescript/cli/src/commands/options.ts | 8 +- typescript/cli/src/commands/strategy.ts | 209 +++--------------- typescript/cli/src/config/strategy.ts | 200 ++++++++++++++++- typescript/cli/src/context/context.ts | 5 +- .../strategies/chain/MultiChainHandler.ts | 16 +- typescript/cli/src/context/types.ts | 2 - typescript/cli/src/deploy/utils.ts | 10 +- .../submitter/ethersV5/schemas.ts | 1 + typescript/utils/src/addresses.ts | 6 +- typescript/utils/src/index.ts | 1 + 12 files changed, 267 insertions(+), 210 deletions(-) diff --git a/typescript/cli/cli.ts b/typescript/cli/cli.ts index 672d84247dd..45cad33bb88 100644 --- a/typescript/cli/cli.ts +++ b/typescript/cli/cli.ts @@ -19,6 +19,7 @@ import { overrideRegistryUriCommandOption, registryUriCommandOption, skipConfirmationOption, + strategyCommandOption, } from './src/commands/options.js'; import { registryCommand } from './src/commands/registry.js'; import { relayerCommand } from './src/commands/relayer.js'; @@ -50,6 +51,7 @@ try { .option('key', keyCommandOption) .option('disableProxy', disableProxyCommandOption) .option('yes', skipConfirmationOption) + .option('strategy', strategyCommandOption) .global(['log', 'verbosity', 'registry', 'overrides', 'yes']) .middleware([ (argv) => { @@ -68,10 +70,10 @@ try { .command(relayerCommand) .command(sendCommand) .command(statusCommand) + .command(strategyCommand) .command(submitCommand) .command(validatorCommand) .command(warpCommand) - .command(strategyCommand) .version(VERSION) .demandCommand() .strict() diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index e72b72452a5..acebf77d18c 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -3,6 +3,7 @@ import { CommandModule } from 'yargs'; import { readChainConfigs } from '../config/chain.js'; import { readIsmConfig } from '../config/ism.js'; import { readMultisigConfig } from '../config/multisig.js'; +import { readStrategyConfig } from '../config/strategy.js'; import { readWarpRouteDeployConfig } from '../config/warp.js'; import { CommandModuleWithContext } from '../context/types.js'; import { log, logGreen } from '../logger.js'; @@ -31,6 +32,7 @@ const validateCommand: CommandModule = { .command(validateChainCommand) .command(validateIsmCommand) .command(validateIsmAdvancedCommand) + .command(validateStrategyCommand) .command(validateWarpCommand) .version(false) .demandCommand(), @@ -76,6 +78,19 @@ const validateIsmAdvancedCommand: CommandModuleWithContext<{ path: string }> = { }, }; +const validateStrategyCommand: CommandModuleWithContext<{ path: string }> = { + command: 'strategy', + describe: 'Validate a Strategy config file', + builder: { + path: inputFileCommandOption(), + }, + handler: async ({ path }) => { + await readStrategyConfig(path); + logGreen('Config is valid'); + process.exit(0); + }, +}; + const validateWarpCommand: CommandModuleWithContext<{ path: string }> = { command: 'warp', describe: 'Validate a Warp Route deployment config file', diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 96be4c77a85..9b4d1d19e80 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -128,13 +128,6 @@ export const chainTargetsCommandOption: Options = { alias: 'c', }; -export const strategyConfigUrlCommandOption: Options = { - type: 'string', - description: 'A path to a JSON or YAML file with a strategy config.', - default: DEFAULT_STRATEGY_CONFIG_PATH, - alias: 's', -}; - export const outputFileCommandOption = ( defaultPath?: string, demandOption = false, @@ -205,6 +198,7 @@ export const strategyCommandOption: Options = { type: 'string', description: 'The submission strategy input file path.', alias: 's', + default: DEFAULT_STRATEGY_CONFIG_PATH, demandOption: true, }; diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index b6456465400..0dae34292e9 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -1,34 +1,19 @@ -// import { input, select } from '@inquirer/prompts'; -import { input, select } from '@inquirer/prompts'; -import { ethers } from 'ethers'; import { stringify as yamlStringify } from 'yaml'; import { CommandModule } from 'yargs'; import { - ChainSubmissionStrategy, - ChainSubmissionStrategySchema, - TxSubmitterType, -} from '@hyperlane-xyz/sdk'; -import { ProtocolType, assert } from '@hyperlane-xyz/utils'; - + createStrategyConfig, + maskSensitiveData, + readStrategyConfig, +} from '../config/strategy.js'; import { CommandModuleWithWriteContext } from '../context/types.js'; -import { - errorRed, - log, - logBlue, - logCommandHeader, - logGreen, -} from '../logger.js'; -import { runSingleChainSelectionStep } from '../utils/chains.js'; -import { - indentYamlOrJson, - readYamlOrJson, - writeYamlOrJson, -} from '../utils/files.js'; +import { log, logCommandHeader } from '../logger.js'; +import { indentYamlOrJson } from '../utils/files.js'; import { DEFAULT_STRATEGY_CONFIG_PATH, outputFileCommandOption, + strategyCommandOption, } from './options.js'; /** @@ -37,171 +22,45 @@ import { export const strategyCommand: CommandModule = { command: 'strategy', describe: 'Manage Hyperlane deployment strategies', - builder: (yargs) => yargs.command(init).version(false).demandCommand(), + builder: (yargs) => + yargs.command(init).command(read).version(false).demandCommand(), handler: () => log('Command required'), }; export const init: CommandModuleWithWriteContext<{ - chain: string; - config: string; + out: string; }> = { command: 'init', - describe: 'Initiates strategy', + describe: 'Initiates default strategy configuration', builder: { - config: outputFileCommandOption( - DEFAULT_STRATEGY_CONFIG_PATH, - false, - 'The path to output a Strategy Config JSON or YAML file.', - ), - type: { - type: 'string', - description: - 'Type of submitter (jsonRpc, impersonatedAccount, gnosisSafe, gnosisSafeTxBuilder)', - }, - safeAddress: { - type: 'string', - description: - 'Safe address (required for gnosisSafe and gnosisSafeTxBuilder types)', - }, - userAddress: { - type: 'string', - description: 'User address (required for impersonatedAccount type)', - }, + out: outputFileCommandOption(DEFAULT_STRATEGY_CONFIG_PATH), }, - handler: async ({ - context, - type: inputType, - safeAddress: inputSafeAddress, - userAddress: inputUserAddress, - }) => { - logCommandHeader(`Hyperlane Key Init`); - let defaultStrategy; - try { - defaultStrategy = await readYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH); - } catch (e) { - defaultStrategy = writeYamlOrJson( - DEFAULT_STRATEGY_CONFIG_PATH, - {}, - 'yaml', - ); - } - - const chain = await runSingleChainSelectionStep(context.chainMetadata); - const chainProtocol = context.chainMetadata[chain].protocol; - assert(chainProtocol === ProtocolType.Ethereum, 'Incompatible protocol'); - - // If type wasn't provided via command line, prompt for it - const type = - inputType || - (await select({ - message: 'Enter the type of submitter', - choices: Object.values(TxSubmitterType).map((value) => ({ - name: value, - value: value, - })), - })); - - const submitter: any = { - type: type, - }; - - // Configure submitter based on type - switch (type) { - case TxSubmitterType.JSON_RPC: - submitter.privateKey = await input({ - message: 'Enter your private key', - validate: (pk) => isValidPrivateKey(pk), - }); - submitter.chain = chain; - break; - - case TxSubmitterType.IMPERSONATED_ACCOUNT: - submitter.userAddress = - inputUserAddress || - (await input({ - message: 'Enter the user address to impersonate', - validate: (address) => { - try { - return ethers.utils.isAddress(address) - ? true - : 'Invalid Ethereum address'; - } catch { - return 'Invalid Ethereum address'; - } - }, - })); - assert( - submitter.userAddress, - 'User address is required for impersonated account', - ); - break; - - case TxSubmitterType.GNOSIS_SAFE: - case TxSubmitterType.GNOSIS_TX_BUILDER: - submitter.safeAddress = - inputSafeAddress || - (await input({ - message: 'Enter the Safe address', - validate: (address) => { - try { - return ethers.utils.isAddress(address) - ? true - : 'Invalid Safe address'; - } catch { - return 'Invalid Safe address'; - } - }, - })); - assert( - submitter.safeAddress, - 'Safe address is required for Gnosis Safe', - ); - submitter.chain = chain; + handler: async ({ context, out }) => { + logCommandHeader(`Hyperlane Strategy Init`); - if (type === TxSubmitterType.GNOSIS_TX_BUILDER) { - submitter.version = await input({ - message: 'Enter the Safe version (default: 1.0)', - default: '1.0', - }); - } - break; - - default: - throw new Error(`Unsupported submitter type: ${type}`); - } + await createStrategyConfig({ + context, + outPath: out, + }); + process.exit(0); + }, +}; - const result: ChainSubmissionStrategy = { - ...defaultStrategy, // if there are changes in ChainSubmissionStrategy, the defaultStrategy may no longer be compatible - [chain]: { - submitter: submitter, - }, - }; +export const read: CommandModuleWithWriteContext<{ + strategy: string; +}> = { + command: 'read', + describe: 'Reads strategy configuration', + builder: { + strategy: { ...strategyCommandOption, demandOption: true }, + }, + handler: async ({ strategy: strategyUrl }) => { + logCommandHeader(`Hyperlane Strategy Read`); - try { - const strategyConfig = ChainSubmissionStrategySchema.parse(result); - logBlue( - `Strategy config is valid, writing to file ${DEFAULT_STRATEGY_CONFIG_PATH}:\n`, - ); - log(indentYamlOrJson(yamlStringify(strategyConfig, null, 2), 4)); + const strategy = await readStrategyConfig(strategyUrl); + const maskedConfig = maskSensitiveData(strategy); + log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); - writeYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH, strategyConfig); - logGreen('✅ Successfully created new key config.'); - } catch (e) { - errorRed( - `Key config is invalid, please check the submitter configuration.`, - ); - throw e; - } process.exit(0); }, }; - -function isValidPrivateKey(privateKey: string): boolean { - try { - // Attempt to create a Wallet instance with the private key - const wallet = new ethers.Wallet(privateKey); - return wallet.privateKey === privateKey; - } catch (error) { - return false; - } -} diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index 48c668993f8..548f49e1bf8 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -1,16 +1,204 @@ +import { confirm, input, password, select } from '@inquirer/prompts'; +import { Wallet } from 'ethers'; +import { stringify as yamlStringify } from 'yaml'; + import { ChainSubmissionStrategy, ChainSubmissionStrategySchema, + TxSubmitterType, } from '@hyperlane-xyz/sdk'; -import { assert } from '@hyperlane-xyz/utils'; +import { + ProtocolType, + assert, + isAddress, + isPrivateKeyEvm, +} from '@hyperlane-xyz/utils'; -import { readYamlOrJson } from '../utils/files.js'; +import { CommandContext } from '../context/types.js'; +import { errorRed, log, logBlue, logGreen } from '../logger.js'; +import { runSingleChainSelectionStep } from '../utils/chains.js'; +import { + indentYamlOrJson, + readYamlOrJson, + writeYamlOrJson, +} from '../utils/files.js'; -export async function readDefaultStrategyConfig( +export async function readStrategyConfig( filePath: string, ): Promise { - const config = readYamlOrJson(filePath); - assert(config, `No default strategy config found at ${filePath}`); + log(`Reading file configs in ${filePath}`); + + const strategyConfig = readYamlOrJson(filePath); + + if ( + !strategyConfig || + typeof strategyConfig !== 'object' || + !Object.keys(strategyConfig).length + ) { + errorRed(`No strategy configs found in ${filePath}`); + process.exit(1); + } + + const parseResult = ChainSubmissionStrategySchema.safeParse(strategyConfig); + + if (!parseResult.success) { + errorRed(`Strategy config for ${filePath} is invalid!`); + errorRed(JSON.stringify(parseResult.error.errors)); + process.exit(1); + } + return strategyConfig; +} + +export async function createStrategyConfig({ + context, + outPath, +}: { + context: CommandContext; + outPath: string; +}) { + let strategy; + try { + // the output strategy might contain submitters for other chain we don't want to overwrite + strategy = await readYamlOrJson(outPath); + } catch (e) { + strategy = writeYamlOrJson(outPath, {}, 'yaml'); + } + const chain = await runSingleChainSelectionStep(context.chainMetadata); + const chainProtocol = context.chainMetadata[chain].protocol; + assert(chainProtocol === ProtocolType.Ethereum, 'Incompatible protocol'); + + if ( + !context.skipConfirmation && + Object.prototype.hasOwnProperty.call(strategy, chain) + ) { + const isConfirmed = await confirm({ + message: `Default strategy for chain ${chain} already exists. Are you sure you want to overwrite existing strategy config?`, + default: false, + }); + + if (!isConfirmed) { + throw Error('Strategy init cancelled'); + } + } + + const type = await select({ + message: 'Enter the type of submitter', + choices: Object.values(TxSubmitterType).map((value) => ({ + name: value, + value: value, + })), + }); + + const submitter: any = { + type: type, + }; + + // Configure submitter based on type + switch (type) { + case TxSubmitterType.JSON_RPC: + submitter.privateKey = await password({ + message: 'Enter your private key', + validate: (pk) => isPrivateKeyEvm(pk), + }); + + submitter.userAddress = await new Wallet( + submitter.privateKey, + ).getAddress(); + + submitter.chain = chain; + break; + + case TxSubmitterType.IMPERSONATED_ACCOUNT: + submitter.userAddress = await input({ + message: 'Enter the user address to impersonate', + validate: (address) => { + try { + return isAddress(address) ? true : 'Invalid Ethereum address'; + } catch { + return 'Invalid Ethereum address'; + } + }, + }); + assert( + submitter.userAddress, + 'User address is required for impersonated account', + ); + break; + + case TxSubmitterType.GNOSIS_SAFE: + case TxSubmitterType.GNOSIS_TX_BUILDER: + submitter.safeAddress = await input({ + message: 'Enter the Safe address', + validate: (address) => { + try { + return isAddress(address) ? true : 'Invalid Safe address'; + } catch { + return 'Invalid Safe address'; + } + }, + }); + + submitter.chain = chain; + + if (type === TxSubmitterType.GNOSIS_TX_BUILDER) { + submitter.version = await input({ + message: 'Enter the Safe version (default: 1.0)', + default: '1.0', + }); + } + break; + + default: + throw new Error(`Unsupported submitter type: ${type}`); + } + + const strategyResult: ChainSubmissionStrategy = { + ...strategy, // if there are changes in ChainSubmissionStrategy, the strategy may no longer be compatible + [chain]: { + submitter: submitter, + }, + }; + + try { + const strategyConfig = ChainSubmissionStrategySchema.parse(strategyResult); + logBlue(`Strategy config is valid, writing to file ${outPath}:\n`); + + // Mask sensitive data before logging + const maskedConfig = maskSensitiveData(strategyConfig); + log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); + + // Write the original unmasked config to file + writeYamlOrJson(outPath, strategyConfig); + logGreen('✅ Successfully created new key config.'); + } catch (e) { + errorRed( + `Key config is invalid, please check the submitter configuration.`, + ); + } +} + +// New utility function to mask sensitive data +export function maskPrivateKey(key: string): string { + if (!key) return key; + const middle = '•'.repeat(key.length); + return `${middle}`; +} + +// Function to recursively mask private keys in an object +export function maskSensitiveData(obj: any): any { + if (!obj) return obj; + + if (typeof obj === 'object') { + const masked = { ...obj }; + for (const [key, value] of Object.entries(masked)) { + if (key === 'privateKey' && typeof value === 'string') { + masked[key] = maskPrivateKey(value); + } else if (typeof value === 'object') { + masked[key] = maskSensitiveData(value); + } + } + return masked; + } - return ChainSubmissionStrategySchema.parse(config); + return obj; } diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 8417bd123d2..f6c04d1fb44 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -17,8 +17,7 @@ import { import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; import { isSignCommand } from '../commands/signCommands.js'; -import { readDefaultStrategyConfig } from '../config/strategy.js'; -// import { readDefaultStrategyConfig } from '../config/strategy.js'; +import { readStrategyConfig } from '../config/strategy.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; @@ -73,7 +72,7 @@ export async function signerMiddleware(argv: Record) { let strategyConfig = {}; try { - strategyConfig = await readDefaultStrategyConfig(argv.strategy); + strategyConfig = await readStrategyConfig(argv.strategy); } catch (error) { strategyConfig = {}; } diff --git a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts index ac9381abb66..ed1727e5133 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts @@ -7,15 +7,13 @@ import { import { assert } from '@hyperlane-xyz/utils'; import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; +import { readWarpRouteDeployConfig } from '../../../config/warp.js'; +import { logRed } from '../../../logger.js'; import { runMultiChainSelectionStep, runSingleChainSelectionStep, } from '../../../utils/chains.js'; -import { - isFile, - readYamlOrJson, - runFileSelectionStep, -} from '../../../utils/files.js'; +import { isFile, runFileSelectionStep } from '../../../utils/files.js'; import { SubmitterContext } from '../submitter/SubmitterContext.js'; import { ChainHandler } from './types.js'; @@ -110,14 +108,10 @@ export class MultiChainHandler implements ChainHandler { 'warp', ); } else { - console.log(`Using warp route deployment config at ${configPath}`); + logRed(`Using warp route deployment config at ${configPath}`); } - const warpRouteConfig = readYamlOrJson(configPath); - assert( - warpRouteConfig, - `No warp route deploy config found at ${configPath}`, - ); + const warpRouteConfig = await readWarpRouteDeployConfig(configPath); const chains = Object.keys(warpRouteConfig) as ChainName[]; assert( diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 79499bd987e..84dcc65ba0d 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -7,7 +7,6 @@ import type { ChainMetadata, ChainName, MultiProvider, - WarpRouteDeployConfig, } from '@hyperlane-xyz/sdk'; export interface ContextSettings { @@ -29,7 +28,6 @@ export interface CommandContext { key?: string; signer?: ethers.Signer; chains?: ChainName[]; - warpRouteConfig?: WarpRouteDeployConfig; } export interface WriteCommandContext extends CommandContext { diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index 71ea6cbb650..c03cc761c34 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -139,8 +139,9 @@ export async function prepareDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); - const userAddress = await multiProvider.getSigner(chain).getAddress(); - const currentBalance = await provider.getBalance(userAddress); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); initialBalances[chain] = currentBalance; }), ); @@ -160,8 +161,9 @@ export async function completeDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); - const userAddress = await multiProvider.getSigner(chain).getAddress(); - const currentBalance = await provider.getBalance(userAddress); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); const balanceDelta = initialBalances[chain].sub(currentBalance); if (isDryRun && balanceDelta.lt(0)) break; logPink( diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts index c5898573705..cef774e738e 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts @@ -15,6 +15,7 @@ export const EV5GnosisSafeTxBuilderPropsSchema = z.object({ export const EV5JsonRpcTxSubmitterPropsSchema = z.object({ chain: ZChainName, + userAddress: ZHash.optional(), privateKey: ZHash.optional(), }); diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index 01f9fdb1072..035d1207e6e 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -1,6 +1,6 @@ import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding'; import { PublicKey } from '@solana/web3.js'; -import { utils as ethersUtils } from 'ethers'; +import { Wallet, utils as ethersUtils } from 'ethers'; import { isNullish } from './typeof.js'; import { Address, HexString, ProtocolType } from './types.js'; @@ -380,3 +380,7 @@ export function ensure0x(hexstr: string) { export function strip0x(hexstr: string) { return hexstr.startsWith('0x') ? hexstr.slice(2) : hexstr; } + +export function isPrivateKeyEvm(privateKey: string): boolean { + return new Wallet(privateKey).privateKey === privateKey; +} diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index c4a86928338..22910e42e1d 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -26,6 +26,7 @@ export { isValidAddressCosmos, isValidAddressEvm, isValidAddressSealevel, + isPrivateKeyEvm, isValidTransactionHash, isValidTransactionHashCosmos, isValidTransactionHashEvm, From 188dced1d481212a37c43a502ac06a491fc7b12e Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 14 Nov 2024 14:21:19 +0100 Subject: [PATCH 033/132] feat: MultiChainHandler support to extract chains from WarpCoreConfig file --- typescript/cli/src/commands/config.ts | 2 +- typescript/cli/src/commands/signCommands.ts | 9 +- typescript/cli/src/commands/strategy.ts | 2 +- typescript/cli/src/config/strategy.ts | 45 +++--- typescript/cli/src/context/context.ts | 59 ++++---- ...nCommandHandler.ts => ChainInterceptor.ts} | 12 +- .../strategies/chain/MultiChainHandler.ts | 128 ++++++++++++------ .../strategies/chain/SingleChainHandler.ts | 39 +----- .../cli/src/context/strategies/chain/types.ts | 32 +---- .../strategies/submitter/JsonRpcStrategy.ts | 3 +- .../strategies/submitter/SubmitterContext.ts | 36 ++++- .../strategies/submitter/SubmitterStrategy.ts | 8 +- typescript/cli/src/context/types.ts | 2 + typescript/cli/src/read/warp.ts | 12 +- typescript/cli/src/send/transfer.ts | 10 +- 15 files changed, 219 insertions(+), 180 deletions(-) rename typescript/cli/src/context/strategies/chain/{ChainCommandHandler.ts => ChainInterceptor.ts} (73%) diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index acebf77d18c..51d00057748 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -80,7 +80,7 @@ const validateIsmAdvancedCommand: CommandModuleWithContext<{ path: string }> = { const validateStrategyCommand: CommandModuleWithContext<{ path: string }> = { command: 'strategy', - describe: 'Validate a Strategy config file', + describe: 'Validates a Strategy config file', builder: { path: inputFileCommandOption(), }, diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index d80dc9c0f59..f32f6c580ad 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -1,7 +1,14 @@ // Commands that send tx and require a key to sign. // It's useful to have this listed here so the context // middleware can request keys up front when required. -export const SIGN_COMMANDS = ['deploy', 'send', 'status', 'apply']; +export const SIGN_COMMANDS = [ + 'apply', + 'deploy', + 'send', + 'status', + 'submit', + 'read', +]; export function isSignCommand(argv: any): boolean { return ( diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index 0dae34292e9..1d62a030d56 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -31,7 +31,7 @@ export const init: CommandModuleWithWriteContext<{ out: string; }> = { command: 'init', - describe: 'Initiates default strategy configuration', + describe: 'Creates strategy configuration', builder: { out: outputFileCommandOption(DEFAULT_STRATEGY_CONFIG_PATH), }, diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index 548f49e1bf8..d4773349172 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -26,27 +26,35 @@ import { export async function readStrategyConfig( filePath: string, ): Promise { - log(`Reading file configs in ${filePath}`); - - const strategyConfig = readYamlOrJson(filePath); + try { + log(`Reading file configs in ${filePath}`); + const strategyConfig = readYamlOrJson(filePath); - if ( - !strategyConfig || - typeof strategyConfig !== 'object' || - !Object.keys(strategyConfig).length - ) { - errorRed(`No strategy configs found in ${filePath}`); - process.exit(1); - } + // Check if config exists and is a non-empty object + if (!strategyConfig || typeof strategyConfig !== 'object') { + logBlue( + `No strategy config found in ${filePath}, returning empty config`, + ); + return {}; + } - const parseResult = ChainSubmissionStrategySchema.safeParse(strategyConfig); + // Validate against schema + const parseResult = ChainSubmissionStrategySchema.safeParse(strategyConfig); + if (!parseResult.success) { + errorRed(`Strategy config validation failed for ${filePath}`); + errorRed(JSON.stringify(parseResult.error.errors, null, 2)); + throw new Error('Invalid strategy configuration'); + } - if (!parseResult.success) { - errorRed(`Strategy config for ${filePath} is invalid!`); - errorRed(JSON.stringify(parseResult.error.errors)); - process.exit(1); + return strategyConfig; + } catch (error) { + if (error instanceof Error) { + errorRed(`Error reading strategy config: ${error.message}`); + } else { + errorRed('Unknown error reading strategy config'); + } + throw error; // Re-throw to let caller handle the error } - return strategyConfig; } export async function createStrategyConfig({ @@ -103,7 +111,7 @@ export async function createStrategyConfig({ submitter.userAddress = await new Wallet( submitter.privateKey, - ).getAddress(); + ).getAddress(); // EVM submitter.chain = chain; break; @@ -177,6 +185,7 @@ export async function createStrategyConfig({ } } +// TODO: put in utils // New utility function to mask sensitive data export function maskPrivateKey(key: string): string { if (!key) return key; diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index f6c04d1fb44..d607d3d078a 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -12,7 +12,9 @@ import { ChainMap, ChainMetadata, ChainName, + ChainSubmissionStrategy, MultiProvider, + TxSubmitterType, } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; @@ -25,7 +27,8 @@ import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner } from '../utils/keys.js'; -import { ChainCommandHandler } from './strategies/chain/ChainCommandHandler.js'; +import { ChainInterceptor } from './strategies/chain/ChainInterceptor.js'; +import { SubmitterContext } from './strategies/submitter/SubmitterContext.js'; import { CommandContext, ContextSettings, @@ -58,41 +61,49 @@ export async function contextMiddleware(argv: Record) { } export async function signerMiddleware(argv: Record) { - const { requiresKey, multiProvider } = argv.context; + const { key, context } = argv; + const { requiresKey, multiProvider } = context; if (!requiresKey) return argv; - // Select the appropriate signing strategy based on the provided hyperlane command - // e.g command `core deploy` uses SingleChainHandler - const chainHandler = ChainCommandHandler.getHandler(argv); - - // Determine the chains that will be used for signing based on the selected strategy - // e.g. SingleChainHandler extracts jsonRpc private key from strategyConfig else prompts user private key input - const chains = await chainHandler.determineChains(argv); - - let strategyConfig = {}; + let strategyConfig: ChainSubmissionStrategy = {}; try { strategyConfig = await readStrategyConfig(argv.strategy); - } catch (error) { + } catch (e) { strategyConfig = {}; } - // Creates a submitter context for the signer, which manages the signing context for the specified chains - // default: TxSubmitterType.JSON_RPC - const signerSubmitterContext = chainHandler.createSubmitterContext( - chains, - strategyConfig, - argv, - ); + /** + * @notice Select the appropriate chain strategy based on the hyperlane command + * @dev e.g command `core deploy` uses SingleChainHandler + */ + const chainStrategy = ChainInterceptor.getStrategy(argv); + + /** + * @notice Determines chains that are used in createSignerContext based on the chain strategy + * @dev e.g. SingleChainHandler extracts chains from CLI or prompts the user to select the chain + * @dev e.g. MultiChainHandler.forOriginDestination() extracts origin/destination from CLI or prompts the user to select origin/destination + */ + const chains = await chainStrategy.determineChains(argv); - // Configure the signers using the selected strategy, multiProvider, and submitter context - // manipulates argv values - await chainHandler.configureSigners( - argv, + /** + * @notice Extracts private keys from strategyConfig else prompts user private key input + */ + + const signerStrategy = new SubmitterContext( + strategyConfig, + chains, + TxSubmitterType.JSON_RPC, multiProvider, - signerSubmitterContext, + key, ); + /** + * @notice Configure the signers using the selected strategy, multiProvider, and signer context + */ + const MultiProviderWithSigners = await signerStrategy.configureSigners(); + argv.multiProvider = MultiProviderWithSigners; + return argv; } diff --git a/typescript/cli/src/context/strategies/chain/ChainCommandHandler.ts b/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts similarity index 73% rename from typescript/cli/src/context/strategies/chain/ChainCommandHandler.ts rename to typescript/cli/src/context/strategies/chain/ChainInterceptor.ts index 595282326b1..ba10f5c5583 100644 --- a/typescript/cli/src/context/strategies/chain/ChainCommandHandler.ts +++ b/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts @@ -11,21 +11,23 @@ enum CommandType { SEND_MESSAGE = 'send:message', AGENT_KURTOSIS = 'deploy:kurtosis-agents', STATUS = 'status:', + SUBMIT = 'submit:', } -export class ChainCommandHandler { +export class ChainInterceptor { private static strategyMap: Map ChainHandler> = new Map([ [CommandType.CORE_APPLY, () => new SingleChainHandler()], - [CommandType.WARP_DEPLOY, () => MultiChainHandler.forWarpConfig()], + [CommandType.WARP_DEPLOY, () => MultiChainHandler.forWarpRouteConfig()], [CommandType.WARP_SEND, () => MultiChainHandler.forOriginDestination()], - [CommandType.WARP_APPLY, () => MultiChainHandler.forWarpConfig()], - [CommandType.WARP_READ, () => new SingleChainHandler()], + [CommandType.WARP_APPLY, () => MultiChainHandler.forWarpRouteConfig()], + [CommandType.WARP_READ, () => MultiChainHandler.forWarpCoreConfig()], [CommandType.SEND_MESSAGE, () => MultiChainHandler.forOriginDestination()], [CommandType.AGENT_KURTOSIS, () => MultiChainHandler.forAgentKurtosis()], [CommandType.STATUS, () => MultiChainHandler.forOriginDestination()], + [CommandType.SUBMIT, () => MultiChainHandler.forStrategyConfig()], ]); - static getHandler(argv: Record): ChainHandler { + static getStrategy(argv: Record): ChainHandler { const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; const createStrategy = this.strategyMap.get(commandKey) || (() => new SingleChainHandler()); diff --git a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts index ed1727e5133..d11740c76cc 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts @@ -1,12 +1,8 @@ -import { - ChainName, - ChainSubmissionStrategy, - MultiProvider, - TxSubmitterType, -} from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; import { assert } from '@hyperlane-xyz/utils'; import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; +import { readStrategyConfig } from '../../../config/strategy.js'; import { readWarpRouteDeployConfig } from '../../../config/warp.js'; import { logRed } from '../../../logger.js'; import { @@ -14,7 +10,7 @@ import { runSingleChainSelectionStep, } from '../../../utils/chains.js'; import { isFile, runFileSelectionStep } from '../../../utils/files.js'; -import { SubmitterContext } from '../submitter/SubmitterContext.js'; +import { getWarpCoreConfigOrExit } from '../../../utils/input.js'; import { ChainHandler } from './types.js'; @@ -22,26 +18,30 @@ enum ChainSelectionMode { ORIGIN_DESTINATION, AGENT_KURTOSIS, WARP_CONFIG, + WARP_READ, + STRATEGY, } export class MultiChainHandler implements ChainHandler { constructor(private mode: ChainSelectionMode) {} async determineChains(argv: Record): Promise { - const { context } = argv; - switch (this.mode) { case ChainSelectionMode.WARP_CONFIG: - return this.determineWarpConfigChains(argv); + return this.determineWarpRouteConfigChains(argv); + case ChainSelectionMode.WARP_READ: + return this.determineWarpCoreConfigChains(argv); case ChainSelectionMode.AGENT_KURTOSIS: - return this.determineAgentChains(argv, context); + return this.determineAgentChains(argv); + case ChainSelectionMode.STRATEGY: + return this.determineStrategyChains(argv); case ChainSelectionMode.ORIGIN_DESTINATION: default: - return this.determineOriginDestinationChains(argv, context); + return this.determineOriginDestinationChains(argv); } } - private async determineWarpConfigChains( + private async determineWarpRouteConfigChains( argv: Record, ): Promise { argv.config = argv.config || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; @@ -51,21 +51,41 @@ export class MultiChainHandler implements ChainHandler { ); return argv.context.chains; } + private async determineWarpCoreConfigChains( + argv: Record, + ): Promise { + if (argv.symbol || argv.warp) { + const warpCoreConfig = await getWarpCoreConfigOrExit({ + context: argv.context, + warp: argv.warp, + symbol: argv.symbol, + }); + argv.context.warpCoreConfig = warpCoreConfig; + const chains = extractChainValues(warpCoreConfig); + return chains; + } else if (argv.chain) { + return [argv.chain]; + } else { + throw new Error( + `Please specify either a symbol, chain and address or warp file`, + ); + } + } private async determineAgentChains( argv: Record, - context: any, ): Promise { + const { chainMetadata } = argv.context; argv.origin = argv.origin ?? (await runSingleChainSelectionStep( - context.chainMetadata, + chainMetadata, 'Select the origin chain', )); if (!argv.targets) { const selectedRelayChains = await runMultiChainSelectionStep({ - chainMetadata: context.chainMetadata, + chainMetadata: chainMetadata, message: 'Select chains to relay between', requireNumber: 2, }); @@ -77,24 +97,31 @@ export class MultiChainHandler implements ChainHandler { private async determineOriginDestinationChains( argv: Record, - context: any, ): Promise { + const { chainMetadata } = argv.context; + argv.origin = argv.origin ?? (await runSingleChainSelectionStep( - context.chainMetadata, + chainMetadata, 'Select the origin chain', )); argv.destination = argv.destination ?? (await runSingleChainSelectionStep( - context.chainMetadata, + chainMetadata, 'Select the destination chain', )); return [argv.origin, argv.destination]; } + private async determineStrategyChains( + argv: Record, + ): Promise { + const strategy = await readStrategyConfig(argv.strategy); + return extractChainValues(strategy); + } private async getWarpConfigChains( configPath: string, @@ -122,30 +149,6 @@ export class MultiChainHandler implements ChainHandler { return chains; } - createSubmitterContext( - chains: ChainName[], - strategyConfig: ChainSubmissionStrategy, - argv?: Record, - ): SubmitterContext { - return new SubmitterContext( - strategyConfig, - chains, - TxSubmitterType.JSON_RPC, - argv, - ); - } - - async configureSigners( - argv: Record, - multiProvider: MultiProvider, - submitterContext: SubmitterContext, - ): Promise { - const signers = await submitterContext.getSigners(); - multiProvider.setSigners(signers); - argv.context.multiProvider = multiProvider; - argv.submitterContext = submitterContext; - } - static forOriginDestination(): MultiChainHandler { return new MultiChainHandler(ChainSelectionMode.ORIGIN_DESTINATION); } @@ -154,7 +157,44 @@ export class MultiChainHandler implements ChainHandler { return new MultiChainHandler(ChainSelectionMode.AGENT_KURTOSIS); } - static forWarpConfig(): MultiChainHandler { + static forWarpRouteConfig(): MultiChainHandler { return new MultiChainHandler(ChainSelectionMode.WARP_CONFIG); } + static forWarpCoreConfig(): MultiChainHandler { + return new MultiChainHandler(ChainSelectionMode.WARP_READ); + } + static forStrategyConfig(): MultiChainHandler { + return new MultiChainHandler(ChainSelectionMode.STRATEGY); + } +} + +// TODO: Put in helpers +function extractChainValues(config: Record): string[] { + const chains: string[] = []; + + // Function to recursively search for chain fields + function findChainFields(obj: any) { + // Return if value is null or not an object/array + if (obj === null || typeof obj !== 'object') return; + + // Handle arrays + if (Array.isArray(obj)) { + obj.forEach((item) => findChainFields(item)); + return; + } + + // Check for chain fields + if ('chain' in obj) { + chains.push(obj.chain); + } + if ('chainName' in obj) { + chains.push(obj.chainName); + } + + // Recursively search in all object values + Object.values(obj).forEach((value) => findChainFields(value)); + } + + findChainFields(config); + return chains; } diff --git a/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts b/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts index 37d6c36c691..491d585f1bd 100644 --- a/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts @@ -1,12 +1,6 @@ -import { - ChainName, - ChainSubmissionStrategy, - MultiProvider, - TxSubmitterType, -} from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; import { runSingleChainSelectionStep } from '../../../utils/chains.js'; -import { SubmitterContext } from '../submitter/SubmitterContext.js'; import { ChainHandler } from './types.js'; @@ -32,35 +26,4 @@ export class SingleChainHandler implements ChainHandler { return [argv.chain]; // Explicitly return as single-item array } - - /** - * @dev Hardcoded: JSON_RPC as the transaction submitter type - */ - createSubmitterContext( - chains: ChainName[], - strategyConfig: ChainSubmissionStrategy, - argv?: Record, - ): SubmitterContext { - return new SubmitterContext( - strategyConfig, - chains, - TxSubmitterType.JSON_RPC, - argv, - ); - } - - /** - * @notice Sets up signers for the specified chain in the MultiProvider - * @dev Sets up signers for single chain - */ - async configureSigners( - argv: Record, - multiProvider: MultiProvider, - submitterContext: SubmitterContext, - ): Promise { - const signers = await submitterContext.getSigners(); - multiProvider.setSigners(signers); - argv.context.multiProvider = multiProvider; - argv.submitterContext = submitterContext; - } } diff --git a/typescript/cli/src/context/strategies/chain/types.ts b/typescript/cli/src/context/strategies/chain/types.ts index 5b429739c96..f7919152f2b 100644 --- a/typescript/cli/src/context/strategies/chain/types.ts +++ b/typescript/cli/src/context/strategies/chain/types.ts @@ -1,10 +1,4 @@ -import { - ChainName, - ChainSubmissionStrategy, - MultiProvider, -} from '@hyperlane-xyz/sdk'; - -import { SubmitterContext } from '../submitter/SubmitterContext.js'; +import { ChainName } from '@hyperlane-xyz/sdk'; export interface ChainHandler { /** @@ -13,28 +7,4 @@ export interface ChainHandler { * @returns Array of chain names */ determineChains(argv: Record): Promise; - - /** - * Creates a context manager for the selected chains - * @param chains Selected chains - * @param strategyConfig Default strategy configuration - * @returns SubmitterContext instance - */ - createSubmitterContext( - chains: ChainName[], - strategyConfig: ChainSubmissionStrategy, - argv?: Record, - ): SubmitterContext; - - /** - * Configures signers for the multi-provider - * @param argv Command arguments - * @param multiProvider MultiProvider instance - * @param submitterContext SubmitterContext instance - */ - configureSigners( - argv: Record, - multiProvider: MultiProvider, - submitterContext: SubmitterContext, - ): Promise; } diff --git a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts index 66d466e89c6..5129fc123ca 100644 --- a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts +++ b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts @@ -1,7 +1,6 @@ import { password } from '@inquirer/prompts'; -import { TxSubmitterType } from '@hyperlane-xyz/sdk'; -import { ChainName } from '@hyperlane-xyz/sdk'; +import { ChainName, TxSubmitterType } from '@hyperlane-xyz/sdk'; import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts b/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts index 97571941212..670ef94d989 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts +++ b/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts @@ -1,10 +1,12 @@ -import { Signer } from 'ethers'; +import { Signer, Wallet } from 'ethers'; import { ChainName, ChainSubmissionStrategy, + MultiProvider, TxSubmitterType, } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; import { ENV } from '../../../utils/env.js'; @@ -27,7 +29,8 @@ export class SubmitterContext { strategyConfig: ChainSubmissionStrategy, private chains: ChainName[], submitterType: TxSubmitterType, - private argv?: Record, + private multiProvider: MultiProvider, + private key?: string, ) { this.strategy = SubmitterStrategyFactory.createStrategy( submitterType, @@ -46,7 +49,7 @@ export class SubmitterContext { for (const chain of this.chains) { const privateKey = - this.argv?.key ?? // argv.key overrides strategy private key + this?.key ?? // argv.key overrides strategy private key (await this.strategy.getPrivateKey(chain)) ?? ENV.HYP_KEY; // argv.key and ENV.HYP_KEY for backwards compatibility @@ -64,6 +67,7 @@ export class SubmitterContext { * @return A record mapping chain names to their corresponding Signer objects. */ async getSigners(): Promise> { + this.strategy; const chainKeys = await this.getChainKeys(); return Object.fromEntries( chainKeys.map(({ chainName, privateKey }) => [ @@ -72,4 +76,30 @@ export class SubmitterContext { ]), ); } + + async configureSigners(): Promise { + for (const chain of this.chains) { + const protocol = this.multiProvider.getChainMetadata(chain).protocol; + const signer = await this.getSignerForChain(chain, protocol); + this.multiProvider.setSigner(chain, signer); + } + + return this.multiProvider; + } + + async getSignerForChain( + chain: ChainName, + protocol: ProtocolType, + ): Promise { + const privateKey = + this?.key ?? // argv.key overrides strategy private key + ENV.HYP_KEY ?? // ENV.HYP_KEY overrides strategy/prompt to enter pk + (await this.strategy.getPrivateKey(chain)); + switch (protocol) { + case ProtocolType.Ethereum: + return new Wallet(privateKey); + default: + throw new Error(`Unsupported protocol: ${protocol}`); + } + } } diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts index fa623515ff2..6035bc27992 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts +++ b/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts @@ -1,11 +1,11 @@ -import { ethers } from 'ethers'; +import { Signer, Wallet } from 'ethers'; import { ChainSubmissionStrategy, TxSubmitterType } from '@hyperlane-xyz/sdk'; import { ChainName } from '@hyperlane-xyz/sdk'; export interface ISubmitterStrategy { getPrivateKey(chain: ChainName): Promise; - getSigner(privateKey: string): ethers.Signer; + getSigner(privateKey: string): Signer; getType(): TxSubmitterType; } @@ -14,8 +14,8 @@ export abstract class BaseSubmitterStrategy implements ISubmitterStrategy { abstract getPrivateKey(chain: ChainName): Promise; - getSigner(privateKey: string): ethers.Signer { - return new ethers.Wallet(privateKey); + getSigner(privateKey: string): Signer { + return new Wallet(privateKey); } abstract getType(): TxSubmitterType; diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 84dcc65ba0d..d12e9dea539 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -7,6 +7,7 @@ import type { ChainMetadata, ChainName, MultiProvider, + WarpCoreConfig, } from '@hyperlane-xyz/sdk'; export interface ContextSettings { @@ -28,6 +29,7 @@ export interface CommandContext { key?: string; signer?: ethers.Signer; chains?: ChainName[]; + warpCoreConfig?: WarpCoreConfig; } export interface WriteCommandContext extends CommandContext { diff --git a/typescript/cli/src/read/warp.ts b/typescript/cli/src/read/warp.ts index 9139d890c25..3cd6c193526 100644 --- a/typescript/cli/src/read/warp.ts +++ b/typescript/cli/src/read/warp.ts @@ -34,11 +34,13 @@ export async function runWarpRouteRead({ let addresses: ChainMap; if (symbol || warp) { - const warpCoreConfig = await getWarpCoreConfigOrExit({ - context, - warp, - symbol, - }); + const warpCoreConfig = + context.warpCoreConfig ?? // this case is be handled by MultiChainHandler.forWarpCoreConfig() interceptor + (await getWarpCoreConfigOrExit({ + context, + warp, + symbol, + })); // TODO: merge with XERC20TokenAdapter and WarpRouteReader const xerc20Limits = await Promise.all( diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index 75205a7c57d..cd75ffc7171 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -92,8 +92,12 @@ async function executeDelivery({ const { multiProvider, registry } = context; const signer = multiProvider.getSigner(origin); + const recipientSigner = multiProvider.getSigner(destination); + + const recipientAddress = await recipientSigner.getAddress(); const signerAddress = await signer.getAddress(); - recipient ||= signerAddress; + + recipient ||= recipientAddress; const chainAddresses = await registry.getAddresses(); @@ -124,7 +128,7 @@ async function executeDelivery({ const errors = await warpCore.validateTransfer({ originTokenAmount: token.amount(amount), destination, - recipient: recipient ?? signerAddress, + recipient, sender: signerAddress, }); if (errors) { @@ -137,7 +141,7 @@ async function executeDelivery({ originTokenAmount: new TokenAmount(amount, token), destination, sender: signerAddress, - recipient: recipient ?? signerAddress, + recipient, }); const txReceipts = []; From 0b92c681af195fd63e7230c291055b67b73ecc90 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 15 Nov 2024 10:46:17 +0100 Subject: [PATCH 034/132] chore: refactoring & adding comments --- typescript/cli/src/commands/config.ts | 4 +-- typescript/cli/src/commands/strategy.ts | 4 +-- typescript/cli/src/config/strategy.ts | 15 +++++++-- typescript/cli/src/context/context.ts | 11 ++----- .../strategies/chain/MultiChainHandler.ts | 8 ++--- .../strategies/submitter/SubmitterContext.ts | 31 +++++++++++++++---- .../submitter/SubmitterStrategyFactory.ts | 2 +- 7 files changed, 49 insertions(+), 26 deletions(-) diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index 51d00057748..4a5c6b580a5 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -3,7 +3,7 @@ import { CommandModule } from 'yargs'; import { readChainConfigs } from '../config/chain.js'; import { readIsmConfig } from '../config/ism.js'; import { readMultisigConfig } from '../config/multisig.js'; -import { readStrategyConfig } from '../config/strategy.js'; +import { readChainSubmissionStrategyConfig } from '../config/strategy.js'; import { readWarpRouteDeployConfig } from '../config/warp.js'; import { CommandModuleWithContext } from '../context/types.js'; import { log, logGreen } from '../logger.js'; @@ -85,7 +85,7 @@ const validateStrategyCommand: CommandModuleWithContext<{ path: string }> = { path: inputFileCommandOption(), }, handler: async ({ path }) => { - await readStrategyConfig(path); + await readChainSubmissionStrategyConfig(path); logGreen('Config is valid'); process.exit(0); }, diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index 1d62a030d56..6bd83ec8af6 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -4,7 +4,7 @@ import { CommandModule } from 'yargs'; import { createStrategyConfig, maskSensitiveData, - readStrategyConfig, + readChainSubmissionStrategyConfig, } from '../config/strategy.js'; import { CommandModuleWithWriteContext } from '../context/types.js'; import { log, logCommandHeader } from '../logger.js'; @@ -57,7 +57,7 @@ export const read: CommandModuleWithWriteContext<{ handler: async ({ strategy: strategyUrl }) => { logCommandHeader(`Hyperlane Strategy Read`); - const strategy = await readStrategyConfig(strategyUrl); + const strategy = await readChainSubmissionStrategyConfig(strategyUrl); const maskedConfig = maskSensitiveData(strategy); log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index d4773349172..f47ceba11a9 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -19,16 +19,27 @@ import { errorRed, log, logBlue, logGreen } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; import { indentYamlOrJson, + isFile, readYamlOrJson, writeYamlOrJson, } from '../utils/files.js'; -export async function readStrategyConfig( +export async function readChainSubmissionStrategyConfig( filePath: string, ): Promise { try { log(`Reading file configs in ${filePath}`); - const strategyConfig = readYamlOrJson(filePath); + + if (!isFile(filePath.trim())) { + logBlue( + `No strategy config found in ${filePath}, returning empty config`, + ); + return {}; + } + + const strategyConfig = readYamlOrJson( + filePath.trim(), + ); // Check if config exists and is a non-empty object if (!strategyConfig || typeof strategyConfig !== 'object') { diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index d607d3d078a..79a7413c07a 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -12,14 +12,13 @@ import { ChainMap, ChainMetadata, ChainName, - ChainSubmissionStrategy, MultiProvider, TxSubmitterType, } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; import { isSignCommand } from '../commands/signCommands.js'; -import { readStrategyConfig } from '../config/strategy.js'; +import { readChainSubmissionStrategyConfig } from '../config/strategy.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; @@ -66,12 +65,7 @@ export async function signerMiddleware(argv: Record) { if (!requiresKey) return argv; - let strategyConfig: ChainSubmissionStrategy = {}; - try { - strategyConfig = await readStrategyConfig(argv.strategy); - } catch (e) { - strategyConfig = {}; - } + const strategyConfig = await readChainSubmissionStrategyConfig(argv.strategy); /** * @notice Select the appropriate chain strategy based on the hyperlane command @@ -89,7 +83,6 @@ export async function signerMiddleware(argv: Record) { /** * @notice Extracts private keys from strategyConfig else prompts user private key input */ - const signerStrategy = new SubmitterContext( strategyConfig, chains, diff --git a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts index d11740c76cc..41c513ed540 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts @@ -2,7 +2,7 @@ import { ChainName } from '@hyperlane-xyz/sdk'; import { assert } from '@hyperlane-xyz/utils'; import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; -import { readStrategyConfig } from '../../../config/strategy.js'; +import { readChainSubmissionStrategyConfig } from '../../../config/strategy.js'; import { readWarpRouteDeployConfig } from '../../../config/warp.js'; import { logRed } from '../../../logger.js'; import { @@ -44,9 +44,9 @@ export class MultiChainHandler implements ChainHandler { private async determineWarpRouteConfigChains( argv: Record, ): Promise { - argv.config = argv.config || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; + argv.config ||= DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; argv.context.chains = await this.getWarpConfigChains( - argv.config, + argv.config.trim(), argv.skipConfirmation, ); return argv.context.chains; @@ -119,7 +119,7 @@ export class MultiChainHandler implements ChainHandler { private async determineStrategyChains( argv: Record, ): Promise { - const strategy = await readStrategyConfig(argv.strategy); + const strategy = await readChainSubmissionStrategyConfig(argv.strategy); return extractChainValues(strategy); } diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts b/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts index 670ef94d989..54b86b2c054 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts +++ b/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts @@ -24,6 +24,8 @@ export class SubmitterContext { * @param strategyConfig Configuration for the submitter strategy. * @param chains Array of chain names to manage. * @param submitterType Type of transaction submitter to use. + * @param multiProvider MultiProvider instance for managing multiple chains. + * @param key Optional private key for overriding strategy private key. */ constructor( strategyConfig: ChainSubmissionStrategy, @@ -41,6 +43,7 @@ export class SubmitterContext { /** * @dev Retrieves the private keys for the specified chains. * @return An array of objects containing chain names and their corresponding private keys. + * @notice This function retrieves private keys from the strategy or falls back to the environment variable. */ private async getChainKeys(): Promise< Array<{ chainName: ChainName; privateKey: string }> @@ -77,26 +80,42 @@ export class SubmitterContext { ); } + /** + * @dev Configures signers for all specified chains in the MultiProvider. + * @return The updated MultiProvider instance. + * @notice This function sets the signer for each chain based on its protocol. + */ async configureSigners(): Promise { for (const chain of this.chains) { - const protocol = this.multiProvider.getChainMetadata(chain).protocol; - const signer = await this.getSignerForChain(chain, protocol); + const signer = await this.getSignerForChain(chain); this.multiProvider.setSigner(chain, signer); } return this.multiProvider; } - async getSignerForChain( - chain: ChainName, - protocol: ProtocolType, - ): Promise { + /** + * @dev Retrieves a signer for a specific chain based on its protocol. + * @param chain The name of the chain for which to retrieve the signer. + * @param protocol The protocol type of the chain. + * @return A Promise that resolves to the Signer instance for the specified chain. + * @throws Error if the protocol is unsupported. + */ + async getSignerForChain(chain: ChainName): Promise { + const { protocol } = this.multiProvider.getChainMetadata(chain); + const privateKey = this?.key ?? // argv.key overrides strategy private key ENV.HYP_KEY ?? // ENV.HYP_KEY overrides strategy/prompt to enter pk (await this.strategy.getPrivateKey(chain)); + + // If protocol is starknet, prompt for address input + switch (protocol) { case ProtocolType.Ethereum: + // Example for ZKSync + // if (technicalStack === ChainTechnicalStack.ZkSync) + // return new ZKSyncWallet(privateKey); return new Wallet(privateKey); default: throw new Error(`Unsupported protocol: ${protocol}`); diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts index 4efc173760b..87db792f5b3 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts +++ b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts @@ -11,7 +11,7 @@ export class SubmitterStrategyFactory { switch (type) { case TxSubmitterType.JSON_RPC: return new JsonRpcStrategy(config); - // TO BE IMPLEMENTED! + // TODO: TO BE IMPLEMENTED! // case TxSubmitterType.STARKNET_JSON_RPC: // return new StarknetJsonRpcStrategy(config); default: From 4360874682df66dbaf30492345ea424e422418dc Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 15 Nov 2024 18:20:13 +0330 Subject: [PATCH 035/132] fix: handle numeric domainId while we have string chainID --- typescript/cli/src/config/chain.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index 9d3245992b7..fbb10e7f0a4 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -1,5 +1,6 @@ import { confirm, input, select } from '@inquirer/prompts'; import { ethers } from 'ethers'; +import { keccak256 } from 'ethers/lib/utils.js'; import { Provider as StarknetProvider, provider as starknetProvider, @@ -99,11 +100,8 @@ export async function createChainConfig({ name, displayName, chainId, - // TODO: Agree on a uniqe way to generate number domain id for starknet chains domainId: - typeof chainId === 'string' - ? parseInt((parseInt(chainId.slice(-4)) / 10 ** 18).toString()) - : chainId, + typeof chainId === 'string' ? stringChainIdToDomainId(chainId) : chainId, protocol: protocol, rpcUrls: [{ http: rpcUrl }], isTestnet, @@ -315,3 +313,8 @@ function formatChainIdBasedOnProtocol(chainId: string, protocol: ProtocolType) { if (protocol === ProtocolType.Starknet) return chainId; return parseInt(chainId, 10); } + +//TODO: move this to somewhere else +function stringChainIdToDomainId(chainId: string): number { + return parseInt(keccak256(chainId).slice(0, 12)); +} From ac6d5c1f2ec1b11fbc00c29dfb99c70a93040671 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 15 Nov 2024 16:18:16 +0100 Subject: [PATCH 036/132] feat: relay command chain resolver strategy & refactoring --- .vscode/launch.json | 16 ----------- typescript/cli/src/commands/options.ts | 5 ++-- typescript/cli/src/context/context.ts | 9 ++++-- .../strategies/chain/ChainInterceptor.ts | 2 ++ .../strategies/chain/MultiChainHandler.ts | 28 +++++++++++++++---- .../BaseMultiChainSigner.ts} | 4 +-- .../JsonRpcSigner.ts} | 11 ++++---- .../MultiChainSignerContext.ts} | 14 +++++----- .../signer/MultiChainSignerFactory.ts | 19 +++++++++++++ .../submitter/SubmitterStrategyFactory.ts | 21 -------------- typescript/cli/src/tests/commands/core.ts | 1 + 11 files changed, 68 insertions(+), 62 deletions(-) delete mode 100644 .vscode/launch.json rename typescript/cli/src/context/strategies/{submitter/SubmitterStrategy.ts => signer/BaseMultiChainSigner.ts} (82%) rename typescript/cli/src/context/strategies/{submitter/JsonRpcStrategy.ts => signer/JsonRpcSigner.ts} (73%) rename typescript/cli/src/context/strategies/{submitter/SubmitterContext.ts => signer/MultiChainSignerContext.ts} (90%) create mode 100644 typescript/cli/src/context/strategies/signer/MultiChainSignerFactory.ts delete mode 100644 typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 37fcc6042b0..00000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "skipFiles": ["/**"], - "program": "$${workspaceFolder}/**/*.ts", - "outFiles": ["${workspaceFolder}/**/*.js"] - } - ] -} diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 9b4d1d19e80..baf0fa84721 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -197,9 +197,8 @@ export const transactionsCommandOption: Options = { export const strategyCommandOption: Options = { type: 'string', description: 'The submission strategy input file path.', - alias: 's', - default: DEFAULT_STRATEGY_CONFIG_PATH, - demandOption: true, + alias: ['s', 'strategy'], + demandOption: false, }; export const addressCommandOption = ( diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 79a7413c07a..886ef7aca60 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -17,6 +17,7 @@ import { } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; +import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; import { isSignCommand } from '../commands/signCommands.js'; import { readChainSubmissionStrategyConfig } from '../config/strategy.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; @@ -27,7 +28,7 @@ import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner } from '../utils/keys.js'; import { ChainInterceptor } from './strategies/chain/ChainInterceptor.js'; -import { SubmitterContext } from './strategies/submitter/SubmitterContext.js'; +import { MultiChainSignerContext } from './strategies/signer/MultiChainSignerContext.js'; import { CommandContext, ContextSettings, @@ -65,7 +66,9 @@ export async function signerMiddleware(argv: Record) { if (!requiresKey) return argv; - const strategyConfig = await readChainSubmissionStrategyConfig(argv.strategy); + const strategyConfig = await readChainSubmissionStrategyConfig( + argv.strategy ?? DEFAULT_STRATEGY_CONFIG_PATH, + ); /** * @notice Select the appropriate chain strategy based on the hyperlane command @@ -83,7 +86,7 @@ export async function signerMiddleware(argv: Record) { /** * @notice Extracts private keys from strategyConfig else prompts user private key input */ - const signerStrategy = new SubmitterContext( + const signerStrategy = new MultiChainSignerContext( strategyConfig, chains, TxSubmitterType.JSON_RPC, diff --git a/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts b/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts index ba10f5c5583..b46b35b7394 100644 --- a/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts +++ b/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts @@ -12,6 +12,7 @@ enum CommandType { AGENT_KURTOSIS = 'deploy:kurtosis-agents', STATUS = 'status:', SUBMIT = 'submit:', + RELAYER = 'relayer:', } export class ChainInterceptor { @@ -25,6 +26,7 @@ export class ChainInterceptor { [CommandType.AGENT_KURTOSIS, () => MultiChainHandler.forAgentKurtosis()], [CommandType.STATUS, () => MultiChainHandler.forOriginDestination()], [CommandType.SUBMIT, () => MultiChainHandler.forStrategyConfig()], + [CommandType.RELAYER, () => MultiChainHandler.forRelayer()], ]); static getStrategy(argv: Record): ChainHandler { diff --git a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts index 41c513ed540..1cdabc33eed 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts @@ -3,13 +3,16 @@ import { assert } from '@hyperlane-xyz/utils'; import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; import { readChainSubmissionStrategyConfig } from '../../../config/strategy.js'; -import { readWarpRouteDeployConfig } from '../../../config/warp.js'; import { logRed } from '../../../logger.js'; import { runMultiChainSelectionStep, runSingleChainSelectionStep, } from '../../../utils/chains.js'; -import { isFile, runFileSelectionStep } from '../../../utils/files.js'; +import { + isFile, + readYamlOrJson, + runFileSelectionStep, +} from '../../../utils/files.js'; import { getWarpCoreConfigOrExit } from '../../../utils/input.js'; import { ChainHandler } from './types.js'; @@ -20,6 +23,7 @@ enum ChainSelectionMode { WARP_CONFIG, WARP_READ, STRATEGY, + RELAYER, } export class MultiChainHandler implements ChainHandler { @@ -35,6 +39,8 @@ export class MultiChainHandler implements ChainHandler { return this.determineAgentChains(argv); case ChainSelectionMode.STRATEGY: return this.determineStrategyChains(argv); + case ChainSelectionMode.RELAYER: + return this.determineRelayerChains(argv); case ChainSelectionMode.ORIGIN_DESTINATION: default: return this.determineOriginDestinationChains(argv); @@ -45,7 +51,7 @@ export class MultiChainHandler implements ChainHandler { argv: Record, ): Promise { argv.config ||= DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; - argv.context.chains = await this.getWarpConfigChains( + argv.context.chains = await this.getWarpRouteConfigChains( argv.config.trim(), argv.skipConfirmation, ); @@ -122,8 +128,13 @@ export class MultiChainHandler implements ChainHandler { const strategy = await readChainSubmissionStrategyConfig(argv.strategy); return extractChainValues(strategy); } + private async determineRelayerChains( + argv: Record, + ): Promise { + return argv.chains.split(',').map((item: string) => item.trim()); + } - private async getWarpConfigChains( + private async getWarpRouteConfigChains( configPath: string, skipConfirmation: boolean, ): Promise { @@ -138,7 +149,11 @@ export class MultiChainHandler implements ChainHandler { logRed(`Using warp route deployment config at ${configPath}`); } - const warpRouteConfig = await readWarpRouteDeployConfig(configPath); + // @dev instead of using readWarpRouteDeployConfig, which uses context to get the signer to fill defaults and make file pass zod validation + const warpRouteConfig = (await readYamlOrJson(configPath)) as Record< + string, + any + >; const chains = Object.keys(warpRouteConfig) as ChainName[]; assert( @@ -166,6 +181,9 @@ export class MultiChainHandler implements ChainHandler { static forStrategyConfig(): MultiChainHandler { return new MultiChainHandler(ChainSelectionMode.STRATEGY); } + static forRelayer(): MultiChainHandler { + return new MultiChainHandler(ChainSelectionMode.RELAYER); + } } // TODO: Put in helpers diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts b/typescript/cli/src/context/strategies/signer/BaseMultiChainSigner.ts similarity index 82% rename from typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts rename to typescript/cli/src/context/strategies/signer/BaseMultiChainSigner.ts index 6035bc27992..81c10b0e63b 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/BaseMultiChainSigner.ts @@ -3,13 +3,13 @@ import { Signer, Wallet } from 'ethers'; import { ChainSubmissionStrategy, TxSubmitterType } from '@hyperlane-xyz/sdk'; import { ChainName } from '@hyperlane-xyz/sdk'; -export interface ISubmitterStrategy { +export interface IMultiChainSigner { getPrivateKey(chain: ChainName): Promise; getSigner(privateKey: string): Signer; getType(): TxSubmitterType; } -export abstract class BaseSubmitterStrategy implements ISubmitterStrategy { +export abstract class BaseMultiChainSigner implements IMultiChainSigner { constructor(protected config: ChainSubmissionStrategy) {} abstract getPrivateKey(chain: ChainName): Promise; diff --git a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts b/typescript/cli/src/context/strategies/signer/JsonRpcSigner.ts similarity index 73% rename from typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts rename to typescript/cli/src/context/strategies/signer/JsonRpcSigner.ts index 5129fc123ca..f8e7f68126d 100644 --- a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts +++ b/typescript/cli/src/context/strategies/signer/JsonRpcSigner.ts @@ -2,21 +2,22 @@ import { password } from '@inquirer/prompts'; import { ChainName, TxSubmitterType } from '@hyperlane-xyz/sdk'; -import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; +import { BaseMultiChainSigner } from './BaseMultiChainSigner.js'; -export class JsonRpcStrategy extends BaseSubmitterStrategy { +export class JsonRpcSigner extends BaseMultiChainSigner { async getPrivateKey(chain: ChainName): Promise { const submitter = this.config[chain]?.submitter as { type: TxSubmitterType.JSON_RPC; privateKey?: string; }; - return ( + const privateKey = submitter?.privateKey ?? (await password({ message: `Please enter the private key for chain ${chain}`, - })) - ); + })); + + return privateKey; } getType(): TxSubmitterType { diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts b/typescript/cli/src/context/strategies/signer/MultiChainSignerContext.ts similarity index 90% rename from typescript/cli/src/context/strategies/submitter/SubmitterContext.ts rename to typescript/cli/src/context/strategies/signer/MultiChainSignerContext.ts index 54b86b2c054..c75eae0b2a7 100644 --- a/typescript/cli/src/context/strategies/submitter/SubmitterContext.ts +++ b/typescript/cli/src/context/strategies/signer/MultiChainSignerContext.ts @@ -10,15 +10,15 @@ import { ProtocolType } from '@hyperlane-xyz/utils'; import { ENV } from '../../../utils/env.js'; -import { ISubmitterStrategy } from './SubmitterStrategy.js'; -import { SubmitterStrategyFactory } from './SubmitterStrategyFactory.js'; +import { IMultiChainSigner } from './BaseMultiChainSigner.js'; +import { MultiChainSignerFactory } from './MultiChainSignerFactory.js'; /** - * @title SubmitterContext + * @title MultiChainSignerContext * @dev Manages the context for transaction submitters, including retrieving chain keys and signers. */ -export class SubmitterContext { - private strategy: ISubmitterStrategy; +export class MultiChainSignerContext { + private strategy: IMultiChainSigner; /** * @param strategyConfig Configuration for the submitter strategy. @@ -34,7 +34,7 @@ export class SubmitterContext { private multiProvider: MultiProvider, private key?: string, ) { - this.strategy = SubmitterStrategyFactory.createStrategy( + this.strategy = MultiChainSignerFactory.getSignerStrategy( submitterType, strategyConfig, ); @@ -101,7 +101,7 @@ export class SubmitterContext { * @return A Promise that resolves to the Signer instance for the specified chain. * @throws Error if the protocol is unsupported. */ - async getSignerForChain(chain: ChainName): Promise { + async getSignerForChain(chain: ChainName): Promise { const { protocol } = this.multiProvider.getChainMetadata(chain); const privateKey = diff --git a/typescript/cli/src/context/strategies/signer/MultiChainSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiChainSignerFactory.ts new file mode 100644 index 00000000000..9fc2bc320ac --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/MultiChainSignerFactory.ts @@ -0,0 +1,19 @@ +import { ChainSubmissionStrategy, TxSubmitterType } from '@hyperlane-xyz/sdk'; + +import { IMultiChainSigner } from './BaseMultiChainSigner.js'; +import { JsonRpcSigner } from './JsonRpcSigner.js'; + +export class MultiChainSignerFactory { + static getSignerStrategy( + type: TxSubmitterType, + config: ChainSubmissionStrategy, + ): IMultiChainSigner { + switch (type) { + case TxSubmitterType.JSON_RPC: + return new JsonRpcSigner(config); + // Future works: TO BE IMPLEMENTED! + default: + throw new Error(`Unsupported submitter type: ${type}`); + } + } +} diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts deleted file mode 100644 index 87db792f5b3..00000000000 --- a/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ChainSubmissionStrategy, TxSubmitterType } from '@hyperlane-xyz/sdk'; - -import { JsonRpcStrategy } from './JsonRpcStrategy.js'; -import { ISubmitterStrategy } from './SubmitterStrategy.js'; - -export class SubmitterStrategyFactory { - static createStrategy( - type: TxSubmitterType, - config: ChainSubmissionStrategy, - ): ISubmitterStrategy { - switch (type) { - case TxSubmitterType.JSON_RPC: - return new JsonRpcStrategy(config); - // TODO: TO BE IMPLEMENTED! - // case TxSubmitterType.STARKNET_JSON_RPC: - // return new StarknetJsonRpcStrategy(config); - default: - throw new Error(`Unsupported submitter type: ${type}`); - } - } -} diff --git a/typescript/cli/src/tests/commands/core.ts b/typescript/cli/src/tests/commands/core.ts index 24c91097443..51e44716939 100644 --- a/typescript/cli/src/tests/commands/core.ts +++ b/typescript/cli/src/tests/commands/core.ts @@ -30,6 +30,7 @@ export async function hyperlaneCoreRead(chain: string, coreOutputPath: string) { --registry ${REGISTRY_PATH} \ --config ${coreOutputPath} \ --chain ${chain} \ + --key ${ANVIL_KEY} \ --verbosity debug \ --yes`; } From eeec6c9e0b77472237c04fe68a33df4f3eaf6c01 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:06:11 +0330 Subject: [PATCH 037/132] fix: change lenght of starknet domainId --- typescript/cli/src/config/chain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index fbb10e7f0a4..bd0064e59ea 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -316,5 +316,5 @@ function formatChainIdBasedOnProtocol(chainId: string, protocol: ProtocolType) { //TODO: move this to somewhere else function stringChainIdToDomainId(chainId: string): number { - return parseInt(keccak256(chainId).slice(0, 12)); + return parseInt(keccak256(chainId).slice(0, 8)); } From 925621c0c6440d840a0f0363e35bf511034ba6cc Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 18 Nov 2024 13:57:55 +0100 Subject: [PATCH 038/132] feat: Signer strategy based on chain's protocol/tech stack --- typescript/cli/src/config/strategy.ts | 8 +- typescript/cli/src/context/context.ts | 26 ++-- .../strategies/chain/MultiChainHandler.ts | 26 ++-- .../strategies/chain/SingleChainHandler.ts | 12 +- .../cli/src/context/strategies/chain/types.ts | 2 +- .../strategies/signer/BaseMultiChainSigner.ts | 22 --- .../signer/BaseMultiProtocolSigner.ts | 27 ++++ .../strategies/signer/JsonRpcSigner.ts | 26 ---- .../signer/MultiChainSignerContext.ts | 124 ----------------- .../signer/MultiChainSignerFactory.ts | 19 --- .../signer/MultiProtocolSignerContext.ts | 128 ++++++++++++++++++ .../signer/MultiProtocolSignerFactory.ts | 79 +++++++++++ 12 files changed, 270 insertions(+), 229 deletions(-) delete mode 100644 typescript/cli/src/context/strategies/signer/BaseMultiChainSigner.ts create mode 100644 typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts delete mode 100644 typescript/cli/src/context/strategies/signer/JsonRpcSigner.ts delete mode 100644 typescript/cli/src/context/strategies/signer/MultiChainSignerContext.ts delete mode 100644 typescript/cli/src/context/strategies/signer/MultiChainSignerFactory.ts create mode 100644 typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts create mode 100644 typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index f47ceba11a9..66eb0c5d0df 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -75,19 +75,21 @@ export async function createStrategyConfig({ context: CommandContext; outPath: string; }) { - let strategy; + let strategy: ChainSubmissionStrategy; try { // the output strategy might contain submitters for other chain we don't want to overwrite - strategy = await readYamlOrJson(outPath); + const strategyObj = await readYamlOrJson(outPath); + strategy = ChainSubmissionStrategySchema.parse(strategyObj); } catch (e) { strategy = writeYamlOrJson(outPath, {}, 'yaml'); } const chain = await runSingleChainSelectionStep(context.chainMetadata); const chainProtocol = context.chainMetadata[chain].protocol; - assert(chainProtocol === ProtocolType.Ethereum, 'Incompatible protocol'); + assert(chainProtocol === ProtocolType.Ethereum, 'Incompatible protocol'); // Needs to be compatible with MultiProvider - ethers.Signer if ( !context.skipConfirmation && + strategy && Object.prototype.hasOwnProperty.call(strategy, chain) ) { const isConfirmed = await confirm({ diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 886ef7aca60..2b211583d88 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -13,7 +13,6 @@ import { ChainMetadata, ChainName, MultiProvider, - TxSubmitterType, } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; @@ -28,7 +27,7 @@ import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner } from '../utils/keys.js'; import { ChainInterceptor } from './strategies/chain/ChainInterceptor.js'; -import { MultiChainSignerContext } from './strategies/signer/MultiChainSignerContext.js'; +import { MultiProtocolSignerContext } from './strategies/signer/MultiProtocolSignerContext.js'; import { CommandContext, ContextSettings, @@ -71,34 +70,33 @@ export async function signerMiddleware(argv: Record) { ); /** - * @notice Select the appropriate chain strategy based on the hyperlane command - * @dev e.g command `core deploy` uses SingleChainHandler + * @notice Intercepts Hyperlane command to determine chains + * @dev For example, command `core deploy` uses SingleChainHandler */ const chainStrategy = ChainInterceptor.getStrategy(argv); /** - * @notice Determines chains that are used in createSignerContext based on the chain strategy - * @dev e.g. SingleChainHandler extracts chains from CLI or prompts the user to select the chain - * @dev e.g. MultiChainHandler.forOriginDestination() extracts origin/destination from CLI or prompts the user to select origin/destination + * @notice Resolves chains that are used in MultiProtocolSignerContext based on the chain strategy + * @dev For example: + * - SingleChainHandler extracts chains from CLI or prompts the user to select the chain + * - MultiChainHandler.forOriginDestination() extracts origin/destination from CLI or prompts the user to select origin/destination */ - const chains = await chainStrategy.determineChains(argv); + const chains = await chainStrategy.resolveChains(argv); /** - * @notice Extracts private keys from strategyConfig else prompts user private key input + * @notice Extracts signer config - private keys from strategyConfig or prompts user for private key input */ - const signerStrategy = new MultiChainSignerContext( + const multiProtocolSigner = new MultiProtocolSignerContext( strategyConfig, chains, - TxSubmitterType.JSON_RPC, multiProvider, key, ); /** - * @notice Configure the signers using the selected strategy, multiProvider, and signer context + * @notice Attaches signers to MultiProvider and assigns it to argv.multiProvider */ - const MultiProviderWithSigners = await signerStrategy.configureSigners(); - argv.multiProvider = MultiProviderWithSigners; + argv.multiProvider = await multiProtocolSigner.attachSignersToMp(); return argv; } diff --git a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts index 1cdabc33eed..c839c02c158 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts @@ -29,25 +29,25 @@ enum ChainSelectionMode { export class MultiChainHandler implements ChainHandler { constructor(private mode: ChainSelectionMode) {} - async determineChains(argv: Record): Promise { + async resolveChains(argv: Record): Promise { switch (this.mode) { case ChainSelectionMode.WARP_CONFIG: - return this.determineWarpRouteConfigChains(argv); + return this.resolveWarpRouteConfigChains(argv); case ChainSelectionMode.WARP_READ: - return this.determineWarpCoreConfigChains(argv); + return this.resolveWarpCoreConfigChains(argv); case ChainSelectionMode.AGENT_KURTOSIS: - return this.determineAgentChains(argv); + return this.resolveAgentChains(argv); case ChainSelectionMode.STRATEGY: - return this.determineStrategyChains(argv); + return this.resolveStrategyChains(argv); case ChainSelectionMode.RELAYER: - return this.determineRelayerChains(argv); + return this.resolveRelayerChains(argv); case ChainSelectionMode.ORIGIN_DESTINATION: default: - return this.determineOriginDestinationChains(argv); + return this.resolveOriginDestinationChains(argv); } } - private async determineWarpRouteConfigChains( + private async resolveWarpRouteConfigChains( argv: Record, ): Promise { argv.config ||= DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; @@ -57,7 +57,7 @@ export class MultiChainHandler implements ChainHandler { ); return argv.context.chains; } - private async determineWarpCoreConfigChains( + private async resolveWarpCoreConfigChains( argv: Record, ): Promise { if (argv.symbol || argv.warp) { @@ -78,7 +78,7 @@ export class MultiChainHandler implements ChainHandler { } } - private async determineAgentChains( + private async resolveAgentChains( argv: Record, ): Promise { const { chainMetadata } = argv.context; @@ -101,7 +101,7 @@ export class MultiChainHandler implements ChainHandler { return [argv.origin, ...argv.targets]; } - private async determineOriginDestinationChains( + private async resolveOriginDestinationChains( argv: Record, ): Promise { const { chainMetadata } = argv.context; @@ -122,13 +122,13 @@ export class MultiChainHandler implements ChainHandler { return [argv.origin, argv.destination]; } - private async determineStrategyChains( + private async resolveStrategyChains( argv: Record, ): Promise { const strategy = await readChainSubmissionStrategyConfig(argv.strategy); return extractChainValues(strategy); } - private async determineRelayerChains( + private async resolveRelayerChains( argv: Record, ): Promise { return argv.chains.split(',').map((item: string) => item.trim()); diff --git a/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts b/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts index 491d585f1bd..620df06d756 100644 --- a/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts @@ -16,13 +16,11 @@ export class SingleChainHandler implements ChainHandler { * @notice Determines the chain to be used for signing operations * @dev Either uses the chain specified in argv or prompts for interactive selection */ - async determineChains(argv: Record): Promise { - argv.chain = - argv.chain || - (await runSingleChainSelectionStep( - argv.context.chainMetadata, - 'Select chain to connect:', - )); + async resolveChains(argv: Record): Promise { + argv.chain ||= await runSingleChainSelectionStep( + argv.context.chainMetadata, + 'Select chain to connect:', + ); return [argv.chain]; // Explicitly return as single-item array } diff --git a/typescript/cli/src/context/strategies/chain/types.ts b/typescript/cli/src/context/strategies/chain/types.ts index f7919152f2b..b2055df38ca 100644 --- a/typescript/cli/src/context/strategies/chain/types.ts +++ b/typescript/cli/src/context/strategies/chain/types.ts @@ -6,5 +6,5 @@ export interface ChainHandler { * @param argv Command arguments * @returns Array of chain names */ - determineChains(argv: Record): Promise; + resolveChains(argv: Record): Promise; } diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiChainSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiChainSigner.ts deleted file mode 100644 index 81c10b0e63b..00000000000 --- a/typescript/cli/src/context/strategies/signer/BaseMultiChainSigner.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Signer, Wallet } from 'ethers'; - -import { ChainSubmissionStrategy, TxSubmitterType } from '@hyperlane-xyz/sdk'; -import { ChainName } from '@hyperlane-xyz/sdk'; - -export interface IMultiChainSigner { - getPrivateKey(chain: ChainName): Promise; - getSigner(privateKey: string): Signer; - getType(): TxSubmitterType; -} - -export abstract class BaseMultiChainSigner implements IMultiChainSigner { - constructor(protected config: ChainSubmissionStrategy) {} - - abstract getPrivateKey(chain: ChainName): Promise; - - getSigner(privateKey: string): Signer { - return new Wallet(privateKey); - } - - abstract getType(): TxSubmitterType; -} diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts new file mode 100644 index 00000000000..9f87f6eb219 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts @@ -0,0 +1,27 @@ +import { Signer, Wallet } from 'ethers'; + +import { ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; + +export interface SignerConfig { + privateKey: string; + address?: string; // For chains like StarkNet that require address + extraParams?: Record; // For any additional chain-specific params +} + +export interface IMultiProtocolSigner { + getSignerConfig(chain: ChainName): Promise | SignerConfig; + getSigner(config: SignerConfig): Signer; +} + +export abstract class BaseMultiProtocolSigner implements IMultiProtocolSigner { + constructor(protected config: ChainSubmissionStrategy) {} + + abstract getSignerConfig( + chain: ChainName, + ): Promise | SignerConfig; + + getSigner(config: SignerConfig): Signer { + return new Wallet(config.privateKey); + } +} diff --git a/typescript/cli/src/context/strategies/signer/JsonRpcSigner.ts b/typescript/cli/src/context/strategies/signer/JsonRpcSigner.ts deleted file mode 100644 index f8e7f68126d..00000000000 --- a/typescript/cli/src/context/strategies/signer/JsonRpcSigner.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { password } from '@inquirer/prompts'; - -import { ChainName, TxSubmitterType } from '@hyperlane-xyz/sdk'; - -import { BaseMultiChainSigner } from './BaseMultiChainSigner.js'; - -export class JsonRpcSigner extends BaseMultiChainSigner { - async getPrivateKey(chain: ChainName): Promise { - const submitter = this.config[chain]?.submitter as { - type: TxSubmitterType.JSON_RPC; - privateKey?: string; - }; - - const privateKey = - submitter?.privateKey ?? - (await password({ - message: `Please enter the private key for chain ${chain}`, - })); - - return privateKey; - } - - getType(): TxSubmitterType { - return TxSubmitterType.JSON_RPC; - } -} diff --git a/typescript/cli/src/context/strategies/signer/MultiChainSignerContext.ts b/typescript/cli/src/context/strategies/signer/MultiChainSignerContext.ts deleted file mode 100644 index c75eae0b2a7..00000000000 --- a/typescript/cli/src/context/strategies/signer/MultiChainSignerContext.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Signer, Wallet } from 'ethers'; - -import { - ChainName, - ChainSubmissionStrategy, - MultiProvider, - TxSubmitterType, -} from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; - -import { ENV } from '../../../utils/env.js'; - -import { IMultiChainSigner } from './BaseMultiChainSigner.js'; -import { MultiChainSignerFactory } from './MultiChainSignerFactory.js'; - -/** - * @title MultiChainSignerContext - * @dev Manages the context for transaction submitters, including retrieving chain keys and signers. - */ -export class MultiChainSignerContext { - private strategy: IMultiChainSigner; - - /** - * @param strategyConfig Configuration for the submitter strategy. - * @param chains Array of chain names to manage. - * @param submitterType Type of transaction submitter to use. - * @param multiProvider MultiProvider instance for managing multiple chains. - * @param key Optional private key for overriding strategy private key. - */ - constructor( - strategyConfig: ChainSubmissionStrategy, - private chains: ChainName[], - submitterType: TxSubmitterType, - private multiProvider: MultiProvider, - private key?: string, - ) { - this.strategy = MultiChainSignerFactory.getSignerStrategy( - submitterType, - strategyConfig, - ); - } - - /** - * @dev Retrieves the private keys for the specified chains. - * @return An array of objects containing chain names and their corresponding private keys. - * @notice This function retrieves private keys from the strategy or falls back to the environment variable. - */ - private async getChainKeys(): Promise< - Array<{ chainName: ChainName; privateKey: string }> - > { - const chainKeys = []; - - for (const chain of this.chains) { - const privateKey = - this?.key ?? // argv.key overrides strategy private key - (await this.strategy.getPrivateKey(chain)) ?? - ENV.HYP_KEY; // argv.key and ENV.HYP_KEY for backwards compatibility - - chainKeys.push({ - chainName: chain, - privateKey: privateKey, - }); - } - - return chainKeys; - } - - /** - * @dev Retrieves signers for the specified chains using their private keys. - * @return A record mapping chain names to their corresponding Signer objects. - */ - async getSigners(): Promise> { - this.strategy; - const chainKeys = await this.getChainKeys(); - return Object.fromEntries( - chainKeys.map(({ chainName, privateKey }) => [ - chainName, - this.strategy.getSigner(privateKey), - ]), - ); - } - - /** - * @dev Configures signers for all specified chains in the MultiProvider. - * @return The updated MultiProvider instance. - * @notice This function sets the signer for each chain based on its protocol. - */ - async configureSigners(): Promise { - for (const chain of this.chains) { - const signer = await this.getSignerForChain(chain); - this.multiProvider.setSigner(chain, signer); - } - - return this.multiProvider; - } - - /** - * @dev Retrieves a signer for a specific chain based on its protocol. - * @param chain The name of the chain for which to retrieve the signer. - * @param protocol The protocol type of the chain. - * @return A Promise that resolves to the Signer instance for the specified chain. - * @throws Error if the protocol is unsupported. - */ - async getSignerForChain(chain: ChainName): Promise { - const { protocol } = this.multiProvider.getChainMetadata(chain); - - const privateKey = - this?.key ?? // argv.key overrides strategy private key - ENV.HYP_KEY ?? // ENV.HYP_KEY overrides strategy/prompt to enter pk - (await this.strategy.getPrivateKey(chain)); - - // If protocol is starknet, prompt for address input - - switch (protocol) { - case ProtocolType.Ethereum: - // Example for ZKSync - // if (technicalStack === ChainTechnicalStack.ZkSync) - // return new ZKSyncWallet(privateKey); - return new Wallet(privateKey); - default: - throw new Error(`Unsupported protocol: ${protocol}`); - } - } -} diff --git a/typescript/cli/src/context/strategies/signer/MultiChainSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiChainSignerFactory.ts deleted file mode 100644 index 9fc2bc320ac..00000000000 --- a/typescript/cli/src/context/strategies/signer/MultiChainSignerFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChainSubmissionStrategy, TxSubmitterType } from '@hyperlane-xyz/sdk'; - -import { IMultiChainSigner } from './BaseMultiChainSigner.js'; -import { JsonRpcSigner } from './JsonRpcSigner.js'; - -export class MultiChainSignerFactory { - static getSignerStrategy( - type: TxSubmitterType, - config: ChainSubmissionStrategy, - ): IMultiChainSigner { - switch (type) { - case TxSubmitterType.JSON_RPC: - return new JsonRpcSigner(config); - // Future works: TO BE IMPLEMENTED! - default: - throw new Error(`Unsupported submitter type: ${type}`); - } - } -} diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts new file mode 100644 index 00000000000..311812d2e0b --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts @@ -0,0 +1,128 @@ +import { Signer } from 'ethers'; + +import { + ChainName, + ChainSubmissionStrategy, + MultiProvider, +} from '@hyperlane-xyz/sdk'; + +import { ENV } from '../../../utils/env.js'; + +import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js'; +import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js'; + +/** + * @title MultiProtocolSignerContext + * @dev Manages the context for transaction submitters, including retrieving signers config and signers. + */ +export class MultiProtocolSignerContext { + private signerStrategies: Map = new Map(); + + constructor( + strategyConfig: ChainSubmissionStrategy, + private chains: ChainName[], + private multiProvider: MultiProvider, + private key?: string, + ) { + // Initialize chain-specific strategies + for (const chain of chains) { + const strategy = MultiProtocolSignerFactory.getSignerStrategy( + chain, + strategyConfig, + multiProvider, + ); + this.signerStrategies.set(chain, strategy); + } + } + + /** + * @dev Retrieves the signers config for the specified chains. + * @return An array of objects containing chain names and their corresponding signers config. + */ + private async getSignersConfig(): Promise< + Array<{ chain: ChainName; privateKey: string }> + > { + return Promise.all( + this.chains.map((chain) => this.getSignerConfigForChain(chain)), + ); + } + + /** + * @notice This function retrieves private key from the strategy or falls back to the environment variable. + */ + private async getSignerConfigForChain( + chain: ChainName, + ): Promise<{ chain: ChainName; privateKey: string }> { + const signerStrategy = this.signerStrategies.get(chain); + if (!signerStrategy) { + throw new Error(`No signer strategy found for chain ${chain}`); + } + + // Determine private key with clear precedence + let privateKey: string; + if (this.key) { + privateKey = this.key; + } else if (ENV.HYP_KEY) { + privateKey = ENV.HYP_KEY; + } else { + const strategyConfig = await signerStrategy.getSignerConfig(chain); + if (!strategyConfig?.privateKey) { + throw new Error(`No private key found for chain ${chain}`); + } + privateKey = strategyConfig.privateKey; + } + + return { + chain, + privateKey, + }; + } + + /** + * @dev Retrieves a signer for a specific chain based on its protocol. + * @param chain The name of the chain for which to retrieve the signer. + * @return A Promise that resolves to the Signer instance for the specified chain. + * @throws Error if the protocol is unsupported. + */ + async getSigner(chain: ChainName): Promise { + const { privateKey } = await this.getSignerConfigForChain(chain); + + const signerStrategy = this.signerStrategies.get(chain); + if (!signerStrategy) { + throw new Error(`No signer strategy found for chain ${chain}`); + } + return signerStrategy.getSigner({ privateKey }); + } + + /** + * @dev Retrieves signers for the specified chains using their signers config. + * @return A record mapping chain names to their corresponding Signer objects. + */ + async getSigners(): Promise> { + const signerConfigs = await this.getSignersConfig(); + const result: Record = {}; + + for (const { chain, privateKey } of signerConfigs) { + const signerStrategy = this.signerStrategies.get(chain); + if (signerStrategy) { + result[chain] = signerStrategy.getSigner({ privateKey }); + } + } + + return result; + } + + /** + * @dev Configures signers for all specified chains in the MultiProvider. + * @return The updated MultiProvider instance. + * @notice This function sets the signer for each chain based on its protocol. + */ + async attachSignersToMp(): Promise { + for (const chain of this.chains) { + const signer = await this.getSigner(chain); + this.multiProvider.setSigner(chain, signer); + } + + return this.multiProvider; + } +} diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts new file mode 100644 index 00000000000..29e48318e75 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -0,0 +1,79 @@ +import { password } from '@inquirer/prompts'; +import { Signer, Wallet } from 'ethers'; + +import { + ChainName, + ChainSubmissionStrategy, + ChainTechnicalStack, + MultiProvider, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { + BaseMultiProtocolSigner, + IMultiProtocolSigner, + SignerConfig, +} from './BaseMultiProtocolSigner.js'; + +export class MultiProtocolSignerFactory { + static getSignerStrategy( + chain: ChainName, + strategyConfig: ChainSubmissionStrategy, + multiProvider: MultiProvider, + ): IMultiProtocolSigner { + const { protocol, technicalStack } = multiProvider.getChainMetadata(chain); + + switch (protocol) { + case ProtocolType.Ethereum: + if (technicalStack === ChainTechnicalStack.ZkSync) + return new ZKSyncSignerStrategy(strategyConfig); + return new EthereumSignerStrategy(strategyConfig); + default: + throw new Error(`Unsupported protocol: ${protocol}`); + } + } +} + +class EthereumSignerStrategy extends BaseMultiProtocolSigner { + async getSignerConfig(chain: ChainName): Promise { + const submitter = this.config[chain]?.submitter as { + type: TxSubmitterType.JSON_RPC; + privateKey?: string; + }; + + const privateKey = + submitter?.privateKey ?? + (await password({ + message: `Please enter the private key for chain ${chain}`, + })); + + return { privateKey }; + } + + getSigner(config: SignerConfig): Signer { + return new Wallet(config.privateKey); + } +} + +// 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean +class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { + async getSignerConfig(chain: ChainName): Promise { + const submitter = this.config[chain]?.submitter as { + type: TxSubmitterType.JSON_RPC; + privateKey?: string; + }; + + const privateKey = + submitter?.privateKey ?? + (await password({ + message: `Please enter the private key for chain ${chain}`, + })); + + return { privateKey }; + } + + getSigner(config: SignerConfig): Signer { + return new Wallet(config.privateKey); + } +} From a0ea88bfd4b6e37aef54ea7c6d8f42b1b8b381cd Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 18 Nov 2024 14:12:47 +0100 Subject: [PATCH 039/132] chore: minor refactoring --- typescript/cli/src/commands/strategy.ts | 2 +- typescript/cli/src/config/strategy.ts | 30 +-------- .../strategies/chain/MultiChainHandler.ts | 36 +--------- typescript/cli/src/deploy/agent.ts | 2 +- typescript/cli/src/utils/output.ts | 67 +++++++++++++++++++ 5 files changed, 74 insertions(+), 63 deletions(-) diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index 6bd83ec8af6..63a66b117bb 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -3,12 +3,12 @@ import { CommandModule } from 'yargs'; import { createStrategyConfig, - maskSensitiveData, readChainSubmissionStrategyConfig, } from '../config/strategy.js'; import { CommandModuleWithWriteContext } from '../context/types.js'; import { log, logCommandHeader } from '../logger.js'; import { indentYamlOrJson } from '../utils/files.js'; +import { maskSensitiveData } from '../utils/output.js'; import { DEFAULT_STRATEGY_CONFIG_PATH, diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index 66eb0c5d0df..c90615a3d04 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -23,6 +23,7 @@ import { readYamlOrJson, writeYamlOrJson, } from '../utils/files.js'; +import { maskSensitiveData } from '../utils/output.js'; export async function readChainSubmissionStrategyConfig( filePath: string, @@ -174,7 +175,7 @@ export async function createStrategyConfig({ } const strategyResult: ChainSubmissionStrategy = { - ...strategy, // if there are changes in ChainSubmissionStrategy, the strategy may no longer be compatible + ...strategy, // if there are changes in ChainSubmissionStrategy, the loaded strategy may no longer be compatible [chain]: { submitter: submitter, }, @@ -197,30 +198,3 @@ export async function createStrategyConfig({ ); } } - -// TODO: put in utils -// New utility function to mask sensitive data -export function maskPrivateKey(key: string): string { - if (!key) return key; - const middle = '•'.repeat(key.length); - return `${middle}`; -} - -// Function to recursively mask private keys in an object -export function maskSensitiveData(obj: any): any { - if (!obj) return obj; - - if (typeof obj === 'object') { - const masked = { ...obj }; - for (const [key, value] of Object.entries(masked)) { - if (key === 'privateKey' && typeof value === 'string') { - masked[key] = maskPrivateKey(value); - } else if (typeof value === 'object') { - masked[key] = maskSensitiveData(value); - } - } - return masked; - } - - return obj; -} diff --git a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts index c839c02c158..f9f84715ea5 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts @@ -14,6 +14,7 @@ import { runFileSelectionStep, } from '../../../utils/files.js'; import { getWarpCoreConfigOrExit } from '../../../utils/input.js'; +import { extractChainsFromObj } from '../../../utils/output.js'; import { ChainHandler } from './types.js'; @@ -67,7 +68,7 @@ export class MultiChainHandler implements ChainHandler { symbol: argv.symbol, }); argv.context.warpCoreConfig = warpCoreConfig; - const chains = extractChainValues(warpCoreConfig); + const chains = extractChainsFromObj(warpCoreConfig); return chains; } else if (argv.chain) { return [argv.chain]; @@ -126,7 +127,7 @@ export class MultiChainHandler implements ChainHandler { argv: Record, ): Promise { const strategy = await readChainSubmissionStrategyConfig(argv.strategy); - return extractChainValues(strategy); + return extractChainsFromObj(strategy); } private async resolveRelayerChains( argv: Record, @@ -185,34 +186,3 @@ export class MultiChainHandler implements ChainHandler { return new MultiChainHandler(ChainSelectionMode.RELAYER); } } - -// TODO: Put in helpers -function extractChainValues(config: Record): string[] { - const chains: string[] = []; - - // Function to recursively search for chain fields - function findChainFields(obj: any) { - // Return if value is null or not an object/array - if (obj === null || typeof obj !== 'object') return; - - // Handle arrays - if (Array.isArray(obj)) { - obj.forEach((item) => findChainFields(item)); - return; - } - - // Check for chain fields - if ('chain' in obj) { - chains.push(obj.chain); - } - if ('chainName' in obj) { - chains.push(obj.chainName); - } - - // Recursively search in all object values - Object.values(obj).forEach((value) => findChainFields(value)); - } - - findChainFields(config); - return chains; -} diff --git a/typescript/cli/src/deploy/agent.ts b/typescript/cli/src/deploy/agent.ts index 88730629949..180e42a4bd1 100644 --- a/typescript/cli/src/deploy/agent.ts +++ b/typescript/cli/src/deploy/agent.ts @@ -21,7 +21,7 @@ export async function runKurtosisAgentDeploy({ relayChains?: string; agentConfigurationPath?: string; }) { - // TODO: decide what to do with this, since its handled in MultiChainHandler - AGENT_KURTOSIS mode + // Future works: decide what to do with this, since its handled in MultiChainHandler - AGENT_KURTOSIS mode if (!originChain) { originChain = await runSingleChainSelectionStep( context.chainMetadata, diff --git a/typescript/cli/src/utils/output.ts b/typescript/cli/src/utils/output.ts index 442b8a09069..bee37be859d 100644 --- a/typescript/cli/src/utils/output.ts +++ b/typescript/cli/src/utils/output.ts @@ -54,3 +54,70 @@ export function formatYamlViolationsOutput( return highlightedLines.join('\n'); } + +/** + * @notice Masks private key with dots + * @param key Private key to mask + * @return Masked key + */ +export function maskPrivateKey(key: string): string { + if (!key) return key; + const middle = '•'.repeat(key.length); + return `${middle}`; +} + +/** + * @notice Recursively masks sensitive data in objects + * @param obj Object with potential sensitive data + * @return Object with masked sensitive data + */ +export function maskSensitiveData(obj: any): any { + if (!obj) return obj; + + if (typeof obj === 'object') { + const masked = { ...obj }; + for (const [key, value] of Object.entries(masked)) { + if (key === 'privateKey' && typeof value === 'string') { + masked[key] = maskPrivateKey(value); + } else if (typeof value === 'object') { + masked[key] = maskSensitiveData(value); + } + } + return masked; + } + + return obj; +} + +/** + * @notice Extracts chain names from a nested configuration object + * @param config Object to search for chain names + * @return Array of discovered chain names + */ +export function extractChainsFromObj(config: Record): string[] { + const chains: string[] = []; + + // Recursively search for chain/chainName fields + function findChainFields(obj: any) { + if (obj === null || typeof obj !== 'object') return; + + if (Array.isArray(obj)) { + obj.forEach((item) => findChainFields(item)); + return; + } + + if ('chain' in obj) { + chains.push(obj.chain); + } + + if ('chainName' in obj) { + chains.push(obj.chainName); + } + + // Recursively search in all nested values + Object.values(obj).forEach((value) => findChainFields(value)); + } + + findChainFields(config); + return chains; +} From 52efa8179e124bcdcb46b6ff46accf2507634340 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 18 Nov 2024 14:43:01 +0100 Subject: [PATCH 040/132] add: strategy types for submitter --- typescript/cli/src/config/strategy.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index c90615a3d04..a91599542f3 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -80,6 +80,7 @@ export async function createStrategyConfig({ try { // the output strategy might contain submitters for other chain we don't want to overwrite const strategyObj = await readYamlOrJson(outPath); + // if there are changes in ChainSubmissionStrategy, the existing strategy may no longer be compatible strategy = ChainSubmissionStrategySchema.parse(strategyObj); } catch (e) { strategy = writeYamlOrJson(outPath, {}, 'yaml'); @@ -103,7 +104,7 @@ export async function createStrategyConfig({ } } - const type = await select({ + const submitterType = await select({ message: 'Enter the type of submitter', choices: Object.values(TxSubmitterType).map((value) => ({ name: value, @@ -111,12 +112,12 @@ export async function createStrategyConfig({ })), }); - const submitter: any = { - type: type, + const submitter: Record = { + type: submitterType, }; - // Configure submitter based on type - switch (type) { + // Configure submitter based on submitterType + switch (submitterType) { case TxSubmitterType.JSON_RPC: submitter.privateKey = await password({ message: 'Enter your private key', @@ -162,7 +163,7 @@ export async function createStrategyConfig({ submitter.chain = chain; - if (type === TxSubmitterType.GNOSIS_TX_BUILDER) { + if (submitterType === TxSubmitterType.GNOSIS_TX_BUILDER) { submitter.version = await input({ message: 'Enter the Safe version (default: 1.0)', default: '1.0', @@ -171,13 +172,13 @@ export async function createStrategyConfig({ break; default: - throw new Error(`Unsupported submitter type: ${type}`); + throw new Error(`Unsupported submitter type: ${submitterType}`); } const strategyResult: ChainSubmissionStrategy = { - ...strategy, // if there are changes in ChainSubmissionStrategy, the loaded strategy may no longer be compatible + ...strategy, [chain]: { - submitter: submitter, + submitter: submitter as ChainSubmissionStrategy[string]['submitter'], }, }; From 12679af3ce419545167352afbde2f0761c8fb167 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 18 Nov 2024 15:34:40 +0100 Subject: [PATCH 041/132] chore: following naming conventions & comments --- typescript/cli/src/context/context.ts | 19 ++------ .../strategies/chain/ChainInterceptor.ts | 38 --------------- .../strategies/chain/ChainResolverFactory.ts | 46 +++++++++++++++++++ ...iChainHandler.ts => MultiChainResolver.ts} | 42 +++++++++++------ ...ChainHandler.ts => SingleChainResolver.ts} | 10 ++-- .../cli/src/context/strategies/chain/types.ts | 2 +- .../signer/MultiProtocolSignerContext.ts | 19 +++----- .../signer/MultiProtocolSignerFactory.ts | 1 + typescript/cli/src/context/types.ts | 3 -- typescript/cli/src/deploy/agent.ts | 2 +- typescript/cli/src/deploy/warp.ts | 12 ++--- typescript/cli/src/send/transfer.ts | 21 ++++++++- 12 files changed, 114 insertions(+), 101 deletions(-) delete mode 100644 typescript/cli/src/context/strategies/chain/ChainInterceptor.ts create mode 100644 typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts rename typescript/cli/src/context/strategies/chain/{MultiChainHandler.ts => MultiChainResolver.ts} (81%) rename typescript/cli/src/context/strategies/chain/{SingleChainHandler.ts => SingleChainResolver.ts} (65%) diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 2b211583d88..e23a6564624 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -26,7 +26,7 @@ import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner } from '../utils/keys.js'; -import { ChainInterceptor } from './strategies/chain/ChainInterceptor.js'; +import { ChainResolverFactory } from './strategies/chain/ChainResolverFactory.js'; import { MultiProtocolSignerContext } from './strategies/signer/MultiProtocolSignerContext.js'; import { CommandContext, @@ -36,7 +36,6 @@ import { export async function contextMiddleware(argv: Record) { const isDryRun = !isNullish(argv.dryRun); - const requiresKey = isSignCommand(argv); const settings: ContextSettings = { registryUri: argv.registry, @@ -47,12 +46,10 @@ export async function contextMiddleware(argv: Record) { disableProxy: argv.disableProxy, skipConfirmation: argv.yes, }; - if (!isDryRun && settings.fromAddress) throw new Error( "'--from-address' or '-f' should only be used for dry-runs", ); - const context = isDryRun ? await getDryRunContext(settings, argv.dryRun) : await getContext(settings); @@ -70,21 +67,17 @@ export async function signerMiddleware(argv: Record) { ); /** - * @notice Intercepts Hyperlane command to determine chains - * @dev For example, command `core deploy` uses SingleChainHandler + * Intercepts Hyperlane command to determine chains. */ - const chainStrategy = ChainInterceptor.getStrategy(argv); + const chainStrategy = ChainResolverFactory.getStrategy(argv); /** - * @notice Resolves chains that are used in MultiProtocolSignerContext based on the chain strategy - * @dev For example: - * - SingleChainHandler extracts chains from CLI or prompts the user to select the chain - * - MultiChainHandler.forOriginDestination() extracts origin/destination from CLI or prompts the user to select origin/destination + * Resolves chains based on the chain strategy. */ const chains = await chainStrategy.resolveChains(argv); /** - * @notice Extracts signer config - private keys from strategyConfig or prompts user for private key input + * Extracts signer config */ const multiProtocolSigner = new MultiProtocolSignerContext( strategyConfig, @@ -112,7 +105,6 @@ export async function getContext({ requiresKey, skipConfirmation, disableProxy = false, - signers, }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy); @@ -124,7 +116,6 @@ export async function getContext({ chainMetadata: multiProvider.metadata, multiProvider, key, - signers, skipConfirmation: !!skipConfirmation, } as CommandContext; } diff --git a/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts b/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts deleted file mode 100644 index b46b35b7394..00000000000 --- a/typescript/cli/src/context/strategies/chain/ChainInterceptor.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { MultiChainHandler } from './MultiChainHandler.js'; -import { SingleChainHandler } from './SingleChainHandler.js'; -import { ChainHandler } from './types.js'; - -enum CommandType { - CORE_APPLY = 'core:apply', - WARP_DEPLOY = 'warp:deploy', - WARP_SEND = 'warp:send', - WARP_APPLY = 'warp:apply', - WARP_READ = 'warp:read', - SEND_MESSAGE = 'send:message', - AGENT_KURTOSIS = 'deploy:kurtosis-agents', - STATUS = 'status:', - SUBMIT = 'submit:', - RELAYER = 'relayer:', -} - -export class ChainInterceptor { - private static strategyMap: Map ChainHandler> = new Map([ - [CommandType.CORE_APPLY, () => new SingleChainHandler()], - [CommandType.WARP_DEPLOY, () => MultiChainHandler.forWarpRouteConfig()], - [CommandType.WARP_SEND, () => MultiChainHandler.forOriginDestination()], - [CommandType.WARP_APPLY, () => MultiChainHandler.forWarpRouteConfig()], - [CommandType.WARP_READ, () => MultiChainHandler.forWarpCoreConfig()], - [CommandType.SEND_MESSAGE, () => MultiChainHandler.forOriginDestination()], - [CommandType.AGENT_KURTOSIS, () => MultiChainHandler.forAgentKurtosis()], - [CommandType.STATUS, () => MultiChainHandler.forOriginDestination()], - [CommandType.SUBMIT, () => MultiChainHandler.forStrategyConfig()], - [CommandType.RELAYER, () => MultiChainHandler.forRelayer()], - ]); - - static getStrategy(argv: Record): ChainHandler { - const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; - const createStrategy = - this.strategyMap.get(commandKey) || (() => new SingleChainHandler()); - return createStrategy(); - } -} diff --git a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts new file mode 100644 index 00000000000..4b14d77d992 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts @@ -0,0 +1,46 @@ +import { MultiChainResolver } from './MultiChainResolver.js'; +import { SingleChainResolver } from './SingleChainResolver.js'; +import { ChainResolver } from './types.js'; + +enum CommandType { + CORE_APPLY = 'core:apply', + WARP_DEPLOY = 'warp:deploy', + WARP_SEND = 'warp:send', + WARP_APPLY = 'warp:apply', + WARP_READ = 'warp:read', + SEND_MESSAGE = 'send:message', + AGENT_KURTOSIS = 'deploy:kurtosis-agents', + STATUS = 'status:', + SUBMIT = 'submit:', + RELAYER = 'relayer:', +} + +/** + * @class ChainResolverFactory + * @description Intercepts commands to determine the appropriate chain resolver strategy based on command type. + */ +export class ChainResolverFactory { + private static strategyMap: Map ChainResolver> = new Map([ + [CommandType.CORE_APPLY, () => new SingleChainResolver()], + [CommandType.WARP_DEPLOY, () => MultiChainResolver.forWarpRouteConfig()], + [CommandType.WARP_SEND, () => MultiChainResolver.forOriginDestination()], + [CommandType.WARP_APPLY, () => MultiChainResolver.forWarpRouteConfig()], + [CommandType.WARP_READ, () => MultiChainResolver.forWarpCoreConfig()], + [CommandType.SEND_MESSAGE, () => MultiChainResolver.forOriginDestination()], + [CommandType.AGENT_KURTOSIS, () => MultiChainResolver.forAgentKurtosis()], + [CommandType.STATUS, () => MultiChainResolver.forOriginDestination()], + [CommandType.SUBMIT, () => MultiChainResolver.forStrategyConfig()], + [CommandType.RELAYER, () => MultiChainResolver.forRelayer()], + ]); + + /** + * @param argv - Command line arguments. + * @returns ChainResolver - The appropriate chain resolver strategy based on the command type. + */ + static getStrategy(argv: Record): ChainResolver { + const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; + const createStrategy = + this.strategyMap.get(commandKey) || (() => new SingleChainResolver()); + return createStrategy(); + } +} diff --git a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts similarity index 81% rename from typescript/cli/src/context/strategies/chain/MultiChainHandler.ts rename to typescript/cli/src/context/strategies/chain/MultiChainResolver.ts index f9f84715ea5..5d582e80df9 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -16,7 +16,7 @@ import { import { getWarpCoreConfigOrExit } from '../../../utils/input.js'; import { extractChainsFromObj } from '../../../utils/output.js'; -import { ChainHandler } from './types.js'; +import { ChainResolver } from './types.js'; enum ChainSelectionMode { ORIGIN_DESTINATION, @@ -27,7 +27,13 @@ enum ChainSelectionMode { RELAYER, } -export class MultiChainHandler implements ChainHandler { +// This class could be broken down into multiple strategies + +/** + * @title MultiChainResolver + * @notice Resolves chains based on the specified selection mode. + */ +export class MultiChainResolver implements ChainResolver { constructor(private mode: ChainSelectionMode) {} async resolveChains(argv: Record): Promise { @@ -58,6 +64,7 @@ export class MultiChainHandler implements ChainHandler { ); return argv.context.chains; } + private async resolveWarpCoreConfigChains( argv: Record, ): Promise { @@ -123,12 +130,14 @@ export class MultiChainHandler implements ChainHandler { return [argv.origin, argv.destination]; } + private async resolveStrategyChains( argv: Record, ): Promise { const strategy = await readChainSubmissionStrategyConfig(argv.strategy); return extractChainsFromObj(strategy); } + private async resolveRelayerChains( argv: Record, ): Promise { @@ -150,7 +159,7 @@ export class MultiChainHandler implements ChainHandler { logRed(`Using warp route deployment config at ${configPath}`); } - // @dev instead of using readWarpRouteDeployConfig, which uses context to get the signer to fill defaults and make file pass zod validation + // Alternative to readWarpRouteDeployConfig that doesn't use context for signer and zod validation const warpRouteConfig = (await readYamlOrJson(configPath)) as Record< string, any @@ -165,24 +174,27 @@ export class MultiChainHandler implements ChainHandler { return chains; } - static forOriginDestination(): MultiChainHandler { - return new MultiChainHandler(ChainSelectionMode.ORIGIN_DESTINATION); + static forAgentKurtosis(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.AGENT_KURTOSIS); } - static forAgentKurtosis(): MultiChainHandler { - return new MultiChainHandler(ChainSelectionMode.AGENT_KURTOSIS); + static forOriginDestination(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.ORIGIN_DESTINATION); } - static forWarpRouteConfig(): MultiChainHandler { - return new MultiChainHandler(ChainSelectionMode.WARP_CONFIG); + static forRelayer(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.RELAYER); } - static forWarpCoreConfig(): MultiChainHandler { - return new MultiChainHandler(ChainSelectionMode.WARP_READ); + + static forStrategyConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.STRATEGY); } - static forStrategyConfig(): MultiChainHandler { - return new MultiChainHandler(ChainSelectionMode.STRATEGY); + + static forWarpRouteConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.WARP_CONFIG); } - static forRelayer(): MultiChainHandler { - return new MultiChainHandler(ChainSelectionMode.RELAYER); + + static forWarpCoreConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.WARP_READ); } } diff --git a/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts b/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts similarity index 65% rename from typescript/cli/src/context/strategies/chain/SingleChainHandler.ts rename to typescript/cli/src/context/strategies/chain/SingleChainResolver.ts index 620df06d756..dd46cba5192 100644 --- a/typescript/cli/src/context/strategies/chain/SingleChainHandler.ts +++ b/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts @@ -2,16 +2,14 @@ import { ChainName } from '@hyperlane-xyz/sdk'; import { runSingleChainSelectionStep } from '../../../utils/chains.js'; -import { ChainHandler } from './types.js'; +import { ChainResolver } from './types.js'; /** - * @title SingleChainHandler + * @title SingleChainResolver * @notice Strategy implementation for managing single-chain operations - * @dev This strategy is used by commands that operate on a single blockchain - * It implements the ChainHandler interface and is primarily used for - * operations like 'core:apply' and 'warp:read' + * @dev Primarily used for operations like 'core:apply' and 'warp:read' */ -export class SingleChainHandler implements ChainHandler { +export class SingleChainResolver implements ChainResolver { /** * @notice Determines the chain to be used for signing operations * @dev Either uses the chain specified in argv or prompts for interactive selection diff --git a/typescript/cli/src/context/strategies/chain/types.ts b/typescript/cli/src/context/strategies/chain/types.ts index b2055df38ca..ab42c81acda 100644 --- a/typescript/cli/src/context/strategies/chain/types.ts +++ b/typescript/cli/src/context/strategies/chain/types.ts @@ -1,6 +1,6 @@ import { ChainName } from '@hyperlane-xyz/sdk'; -export interface ChainHandler { +export interface ChainResolver { /** * Determines the chains to be used for signing * @param argv Command arguments diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts index 311812d2e0b..e69017cb872 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts @@ -13,7 +13,7 @@ import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js'; /** * @title MultiProtocolSignerContext - * @dev Manages the context for transaction submitters, including retrieving signers config and signers. + * @dev Context manager for signers across multiple protocols */ export class MultiProtocolSignerContext { private signerStrategies: Map = new Map(); @@ -36,8 +36,7 @@ export class MultiProtocolSignerContext { } /** - * @dev Retrieves the signers config for the specified chains. - * @return An array of objects containing chain names and their corresponding signers config. + * @dev Gets signers config for specified chains */ private async getSignersConfig(): Promise< Array<{ chain: ChainName; privateKey: string }> @@ -48,7 +47,7 @@ export class MultiProtocolSignerContext { } /** - * @notice This function retrieves private key from the strategy or falls back to the environment variable. + * @dev Gets private key from strategy or environment fallback */ private async getSignerConfigForChain( chain: ChainName, @@ -79,10 +78,7 @@ export class MultiProtocolSignerContext { } /** - * @dev Retrieves a signer for a specific chain based on its protocol. - * @param chain The name of the chain for which to retrieve the signer. - * @return A Promise that resolves to the Signer instance for the specified chain. - * @throws Error if the protocol is unsupported. + * @dev Gets protocol-specific signer for a chain */ async getSigner(chain: ChainName): Promise { const { privateKey } = await this.getSignerConfigForChain(chain); @@ -95,8 +91,7 @@ export class MultiProtocolSignerContext { } /** - * @dev Retrieves signers for the specified chains using their signers config. - * @return A record mapping chain names to their corresponding Signer objects. + * @dev Gets signers for all specified chains */ async getSigners(): Promise> { const signerConfigs = await this.getSignersConfig(); @@ -113,9 +108,7 @@ export class MultiProtocolSignerContext { } /** - * @dev Configures signers for all specified chains in the MultiProvider. - * @return The updated MultiProvider instance. - * @notice This function sets the signer for each chain based on its protocol. + * @dev Configures signers for chains in MultiProvider */ async attachSignersToMp(): Promise { for (const chain of this.chains) { diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index 29e48318e75..5f31a0f0740 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -57,6 +57,7 @@ class EthereumSignerStrategy extends BaseMultiProtocolSigner { } // 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean +// TODO: use ZKSync suitable signer class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { async getSignerConfig(chain: ChainName): Promise { const submitter = this.config[chain]?.submitter as { diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index d12e9dea539..eef1ad2bbfb 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -5,7 +5,6 @@ import type { IRegistry } from '@hyperlane-xyz/registry'; import type { ChainMap, ChainMetadata, - ChainName, MultiProvider, WarpCoreConfig, } from '@hyperlane-xyz/sdk'; @@ -18,7 +17,6 @@ export interface ContextSettings { requiresKey?: boolean; disableProxy?: boolean; skipConfirmation?: boolean; - signers?: any; } export interface CommandContext { @@ -28,7 +26,6 @@ export interface CommandContext { skipConfirmation: boolean; key?: string; signer?: ethers.Signer; - chains?: ChainName[]; warpCoreConfig?: WarpCoreConfig; } diff --git a/typescript/cli/src/deploy/agent.ts b/typescript/cli/src/deploy/agent.ts index 180e42a4bd1..a36955a3f3d 100644 --- a/typescript/cli/src/deploy/agent.ts +++ b/typescript/cli/src/deploy/agent.ts @@ -21,7 +21,7 @@ export async function runKurtosisAgentDeploy({ relayChains?: string; agentConfigurationPath?: string; }) { - // Future works: decide what to do with this, since its handled in MultiChainHandler - AGENT_KURTOSIS mode + // Future works: decide what to do with this, since its handled in MultiChainResolver - AGENT_KURTOSIS mode if (!originChain) { originChain = await runSingleChainSelectionStep( context.chainMetadata, diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index b5eb04e40dc..9e8d7268eed 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -100,12 +100,7 @@ export async function runWarpRouteDeploy({ context: WriteCommandContext; warpRouteDeploymentConfigPath?: string; }) { - const { - skipConfirmation, - chainMetadata, - registry, - chains: contextChains, - } = context; + const { skipConfirmation, chainMetadata, registry } = context; if ( !warpRouteDeploymentConfigPath || @@ -128,14 +123,15 @@ export async function runWarpRouteDeploy({ context, ); - const chains = contextChains!; + const chains = Object.keys(warpRouteConfig); + let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await requestAndSaveApiKeys(chains, chainMetadata, registry); const deploymentParams = { context, - warpDeployConfig: warpRouteConfig!, + warpDeployConfig: warpRouteConfig, }; await runDeployPlanStep(deploymentParams); diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index cd75ffc7171..dc4b8ca96b4 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -18,6 +18,7 @@ import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { WriteCommandContext } from '../context/types.js'; import { runPreflightChecksForChains } from '../deploy/utils.js'; import { log, logBlue, logGreen, logRed } from '../logger.js'; +import { runSingleChainSelectionStep } from '../utils/chains.js'; import { indentYamlOrJson } from '../utils/files.js'; import { stubMerkleTreeConfig } from '../utils/relay.js'; import { runTokenSelectionStep } from '../utils/tokens.js'; @@ -39,14 +40,30 @@ export async function sendTestTransfer({ }: { context: WriteCommandContext; warpCoreConfig: WarpCoreConfig; - origin: ChainName; - destination: ChainName; + origin?: ChainName; // resolved in signerMiddleware + destination?: ChainName; // resolved in signerMiddleware amount: string; recipient?: string; timeoutSec: number; skipWaitForDelivery: boolean; selfRelay?: boolean; }) { + const { chainMetadata } = context; + + if (!origin) { + origin = await runSingleChainSelectionStep( + chainMetadata, + 'Select the origin chain', + ); + } + + if (!destination) { + destination = await runSingleChainSelectionStep( + chainMetadata, + 'Select the destination chain', + ); + } + await runPreflightChecksForChains({ context, chains: [origin, destination], From 294c17cfd757efcab43065727f6d4b5cf9756825 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:41:43 +0330 Subject: [PATCH 042/132] feat: manage warp deploy based on protocol --- typescript/cli/src/deploy/warp.ts | 112 ++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 15 deletions(-) diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 508e18f69ec..9f55ea87c25 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -100,7 +100,8 @@ export async function runWarpRouteDeploy({ context: WriteCommandContext; warpRouteDeploymentConfigPath?: string; }) { - const { signer, skipConfirmation, chainMetadata, registry } = context; + const { signer, skipConfirmation, chainMetadata, registry, multiProvider } = + context; if ( !warpRouteDeploymentConfigPath || @@ -136,26 +137,75 @@ export async function runWarpRouteDeploy({ await runDeployPlanStep(deploymentParams); - await runPreflightChecksForChains({ - context, - chains, - minGas: MINIMUM_WARP_DEPLOY_GAS, - }); + // Group chains string list by protocol + const chainsByProtocol = chains.reduce>( + (acc, chain) => { + const protocol = multiProvider.tryGetProtocol(chain); + assert(protocol, `protocol is not provided for chain: ${chain}`); + acc[protocol] = acc[protocol] || []; + acc[protocol].push(chain); + return acc; + }, + {} as Record, + ); + console.log({ chainsByProtocol }); + const deployments: WarpCoreConfig = { tokens: [] }; + + // Execute deployments for each protocol + for (const protocol of Object.keys(chainsByProtocol) as ProtocolType[]) { + const protocolChains = chainsByProtocol[protocol]; + const protocolDeploymentParams = { + context: deploymentParams.context, + warpDeployConfig: Object.fromEntries( + Object.entries(deploymentParams.warpDeployConfig).filter(([chain]) => + protocolChains.includes(chain), + ), + ), + }; - const userAddress = await signer.getAddress(); + switch (protocol) { + case ProtocolType.Ethereum: + { + const userAddress = await signer.getAddress(); - const initialBalances = await prepareDeploy(context, userAddress, chains); + await runPreflightChecksForChains({ + context, + chains: protocolChains, + minGas: MINIMUM_WARP_DEPLOY_GAS, + }); - const deployedContracts = await executeDeploy(deploymentParams, apiKeys); + await prepareDeploy(context, userAddress, protocolChains); - const warpCoreConfig = await getWarpCoreConfig( - deploymentParams, - deployedContracts, - ); + const deployedContracts = await executeDeploy( + protocolDeploymentParams, + apiKeys, + ); + + const warpCoreConfig = await getWarpCoreConfig( + deploymentParams, + deployedContracts, + ); + deployments.tokens = [ + ...deployments.tokens, + ...warpCoreConfig.tokens, + ]; + deployments.options = { + ...deployments.options, + ...warpCoreConfig.options, + }; + } + break; - await writeDeploymentArtifacts(warpCoreConfig, context); + case ProtocolType.Starknet: + break; - await completeDeploy(context, 'warp', initialBalances, userAddress, chains); + default: + throw new Error(`Unsupported protocol type: ${protocol}`); + } + } + + await writeDeploymentArtifacts(deployments, context); + await completeDeploy(context, 'warp', {}, '', chains); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { @@ -954,3 +1004,35 @@ async function getWarpApplySubmitter({ multiProvider, }); } + +// async function runStarknetDeployments({ +// context, +// warpRouteConfig, +// chains, +// }: { +// context: WriteCommandContext; +// warpRouteConfig: WarpRouteDeployConfig; +// chains: ChainName[]; +// }): Promise> { +// if (chains.length === 0) { +// return {}; +// } + +// logBlue('Deploying Starknet contracts...'); + +// const deployedContracts: HyperlaneContractsMap = {}; + +// for (const chain of chains) { +// const config = warpRouteConfig[chain]; + +// // TODO: Implement Starknet-specific deployment logic: +// // 1. Set up Starknet provider and account similar to core.ts +// // 2. Create Starknet token deployer (similar to HypERC20Deployer/HypERC721Deployer) +// // 3. Deploy ISM if specified in config +// // 4. Deploy token contracts +// // 5. Store deployed addresses in deployedContracts map +// } + +// logGreen('✅ Starknet contract deployments complete'); +// return deployedContracts; +// } From b7d439bb1ad5fe03f08e5f680bdef416424298be Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Tue, 19 Nov 2024 11:36:19 +0100 Subject: [PATCH 043/132] feat: requiresKey options on strategy `read` command --- typescript/cli/src/commands/strategy.ts | 8 +++++++- typescript/cli/src/config/strategy.ts | 5 +++-- typescript/cli/src/context/context.ts | 6 +++--- .../strategies/signer/BaseMultiProtocolSigner.ts | 11 +++-------- .../strategies/signer/MultiProtocolSignerFactory.ts | 3 +-- ...SignerContext.ts => MultiProtocolSignerManager.ts} | 8 ++++---- 6 files changed, 21 insertions(+), 20 deletions(-) rename typescript/cli/src/context/strategies/signer/{MultiProtocolSignerContext.ts => MultiProtocolSignerManager.ts} (95%) diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index 63a66b117bb..6f85dcb885e 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -48,11 +48,17 @@ export const init: CommandModuleWithWriteContext<{ export const read: CommandModuleWithWriteContext<{ strategy: string; + requiresKey: boolean; }> = { command: 'read', describe: 'Reads strategy configuration', builder: { - strategy: { ...strategyCommandOption, demandOption: true }, + strategy: { + ...strategyCommandOption, + demandOption: true, + default: DEFAULT_STRATEGY_CONFIG_PATH, + }, + requiresKey: { demandOption: false, default: false, hidden: true }, }, handler: async ({ strategy: strategyUrl }) => { logCommandHeader(`Hyperlane Strategy Read`); diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index a91599542f3..7aac3ff083c 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -50,10 +50,11 @@ export async function readChainSubmissionStrategyConfig( return {}; } - // Validate against schema const parseResult = ChainSubmissionStrategySchema.safeParse(strategyConfig); if (!parseResult.success) { - errorRed(`Strategy config validation failed for ${filePath}`); + errorRed( + `Strategy config validation using ChainSubmissionStrategySchema failed for ${filePath}`, + ); errorRed(JSON.stringify(parseResult.error.errors, null, 2)); throw new Error('Invalid strategy configuration'); } diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index e23a6564624..3de5f7f0cf4 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -27,7 +27,7 @@ import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner } from '../utils/keys.js'; import { ChainResolverFactory } from './strategies/chain/ChainResolverFactory.js'; -import { MultiProtocolSignerContext } from './strategies/signer/MultiProtocolSignerContext.js'; +import { MultiProtocolSignerManager } from './strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext, ContextSettings, @@ -36,7 +36,7 @@ import { export async function contextMiddleware(argv: Record) { const isDryRun = !isNullish(argv.dryRun); - const requiresKey = isSignCommand(argv); + const requiresKey = argv.requiresKey ?? isSignCommand(argv); const settings: ContextSettings = { registryUri: argv.registry, registryOverrideUri: argv.overrides, @@ -79,7 +79,7 @@ export async function signerMiddleware(argv: Record) { /** * Extracts signer config */ - const multiProtocolSigner = new MultiProtocolSignerContext( + const multiProtocolSigner = new MultiProtocolSignerManager( strategyConfig, chains, multiProvider, diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts index 9f87f6eb219..4aadcdf81f1 100644 --- a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts +++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts @@ -1,4 +1,4 @@ -import { Signer, Wallet } from 'ethers'; +import { Signer } from 'ethers'; import { ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; import { ChainName } from '@hyperlane-xyz/sdk'; @@ -17,11 +17,6 @@ export interface IMultiProtocolSigner { export abstract class BaseMultiProtocolSigner implements IMultiProtocolSigner { constructor(protected config: ChainSubmissionStrategy) {} - abstract getSignerConfig( - chain: ChainName, - ): Promise | SignerConfig; - - getSigner(config: SignerConfig): Signer { - return new Wallet(config.privateKey); - } + abstract getSignerConfig(chain: ChainName): Promise; + abstract getSigner(config: SignerConfig): Signer; } diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index 5f31a0f0740..030f11b5f4e 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -57,11 +57,10 @@ class EthereumSignerStrategy extends BaseMultiProtocolSigner { } // 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean -// TODO: use ZKSync suitable signer +// TODO: import ZKSync signer class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { async getSignerConfig(chain: ChainName): Promise { const submitter = this.config[chain]?.submitter as { - type: TxSubmitterType.JSON_RPC; privateKey?: string; }; diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts similarity index 95% rename from typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts rename to typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts index e69017cb872..60fa136827a 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerContext.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -12,14 +12,14 @@ import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js'; import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js'; /** - * @title MultiProtocolSignerContext + * @title MultiProtocolSignerManager * @dev Context manager for signers across multiple protocols */ -export class MultiProtocolSignerContext { +export class MultiProtocolSignerManager { private signerStrategies: Map = new Map(); constructor( - strategyConfig: ChainSubmissionStrategy, + submissionStrategy: ChainSubmissionStrategy, private chains: ChainName[], private multiProvider: MultiProvider, private key?: string, @@ -28,7 +28,7 @@ export class MultiProtocolSignerContext { for (const chain of chains) { const strategy = MultiProtocolSignerFactory.getSignerStrategy( chain, - strategyConfig, + submissionStrategy, multiProvider, ); this.signerStrategies.set(chain, strategy); From 24b9d7a57d72802e8072ef4916ffa54f640f6ae6 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Tue, 19 Nov 2024 11:46:21 +0100 Subject: [PATCH 044/132] chore: revert MessageOptionsArgTypes origin & destination optionality --- typescript/cli/src/commands/send.ts | 4 ++-- typescript/cli/src/send/message.ts | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/typescript/cli/src/commands/send.ts b/typescript/cli/src/commands/send.ts index 27f59a52dc1..1167b3b5599 100644 --- a/typescript/cli/src/commands/send.ts +++ b/typescript/cli/src/commands/send.ts @@ -46,8 +46,8 @@ export const messageOptions: { [k: string]: Options } = { }; export interface MessageOptionsArgTypes { - origin: string; - destination: string; + origin?: string; + destination?: string; timeout: number; quick: boolean; relay: boolean; diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index 5ece5529f9b..430d3b7bcfc 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -7,6 +7,7 @@ import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { CommandContext, WriteCommandContext } from '../context/types.js'; import { runPreflightChecksForChains } from '../deploy/utils.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; +import { runSingleChainSelectionStep } from '../utils/chains.js'; import { indentYamlOrJson } from '../utils/files.js'; import { stubMerkleTreeConfig } from '../utils/relay.js'; @@ -20,13 +21,29 @@ export async function sendTestMessage({ selfRelay, }: { context: WriteCommandContext; - origin: ChainName; - destination: ChainName; + origin?: ChainName; + destination?: ChainName; messageBody: string; timeoutSec: number; skipWaitForDelivery: boolean; selfRelay?: boolean; }) { + const { chainMetadata } = context; + + if (!origin) { + origin = await runSingleChainSelectionStep( + chainMetadata, + 'Select the origin chain', + ); + } + + if (!destination) { + destination = await runSingleChainSelectionStep( + chainMetadata, + 'Select the destination chain', + ); + } + await runPreflightChecksForChains({ context, chains: [origin, destination], From 60a2ae27ea4d167f45368770f82e05ca6f79d8e8 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Wed, 20 Nov 2024 12:33:39 +0100 Subject: [PATCH 045/132] feat: strategy init chain agnostic --- typescript/cli/src/config/strategy.ts | 52 +++++++++++++-------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index 7aac3ff083c..f2b33119617 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -29,7 +29,7 @@ export async function readChainSubmissionStrategyConfig( filePath: string, ): Promise { try { - log(`Reading file configs in ${filePath}`); + log(`Reading submission strategy in ${filePath}`); if (!isFile(filePath.trim())) { logBlue( @@ -79,16 +79,14 @@ export async function createStrategyConfig({ }) { let strategy: ChainSubmissionStrategy; try { - // the output strategy might contain submitters for other chain we don't want to overwrite const strategyObj = await readYamlOrJson(outPath); - // if there are changes in ChainSubmissionStrategy, the existing strategy may no longer be compatible strategy = ChainSubmissionStrategySchema.parse(strategyObj); } catch (e) { strategy = writeYamlOrJson(outPath, {}, 'yaml'); } + const chain = await runSingleChainSelectionStep(context.chainMetadata); const chainProtocol = context.chainMetadata[chain].protocol; - assert(chainProtocol === ProtocolType.Ethereum, 'Incompatible protocol'); // Needs to be compatible with MultiProvider - ethers.Signer if ( !context.skipConfirmation && @@ -100,34 +98,34 @@ export async function createStrategyConfig({ default: false, }); - if (!isConfirmed) { - throw Error('Strategy init cancelled'); - } + assert(isConfirmed, 'Strategy initialization cancelled by user.'); } - const submitterType = await select({ - message: 'Enter the type of submitter', - choices: Object.values(TxSubmitterType).map((value) => ({ - name: value, - value: value, - })), - }); + const isEthereum = chainProtocol === ProtocolType.Ethereum; + const submitterType = isEthereum + ? await select({ + message: 'Select the submitter type', + choices: Object.values(TxSubmitterType).map((value) => ({ + name: value, + value: value, + })), + }) + : TxSubmitterType.JSON_RPC; // Do other non-evm chains support gnosis and account impersonation? - const submitter: Record = { - type: submitterType, - }; + const submitter: Record = { type: submitterType }; - // Configure submitter based on submitterType switch (submitterType) { case TxSubmitterType.JSON_RPC: submitter.privateKey = await password({ - message: 'Enter your private key', - validate: (pk) => isPrivateKeyEvm(pk), + message: 'Enter the private key for JSON-RPC submission:', + validate: (pk) => (isEthereum ? isPrivateKeyEvm(pk) : true), }); - submitter.userAddress = await new Wallet( - submitter.privateKey, - ).getAddress(); // EVM + submitter.userAddress = isEthereum + ? await new Wallet(submitter.privateKey).getAddress() + : await input({ + message: 'Enter the user address for JSON-RPC submission:', + }); submitter.chain = chain; break; @@ -185,18 +183,16 @@ export async function createStrategyConfig({ try { const strategyConfig = ChainSubmissionStrategySchema.parse(strategyResult); - logBlue(`Strategy config is valid, writing to file ${outPath}:\n`); + logBlue(`Strategy configuration is valid. Writing to file ${outPath}:\n`); - // Mask sensitive data before logging const maskedConfig = maskSensitiveData(strategyConfig); log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); - // Write the original unmasked config to file writeYamlOrJson(outPath, strategyConfig); - logGreen('✅ Successfully created new key config.'); + logGreen('✅ Successfully created a new strategy configuration.'); } catch (e) { errorRed( - `Key config is invalid, please check the submitter configuration.`, + `The strategy configuration is invalid. Please review the submitter settings.`, ); } } From 884db145eb5de13ae38fd92ad553ef54a25d0f10 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Wed, 20 Nov 2024 12:42:31 +0100 Subject: [PATCH 046/132] feat: logging private key source --- .../context/strategies/signer/MultiProtocolSignerManager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts index 60fa136827a..3e27be20e0c 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -6,6 +6,7 @@ import { MultiProvider, } from '@hyperlane-xyz/sdk'; +import { logBlue } from '../../../logger.js'; import { ENV } from '../../../utils/env.js'; import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js'; @@ -60,14 +61,18 @@ export class MultiProtocolSignerManager { // Determine private key with clear precedence let privateKey: string; if (this.key) { + logBlue('Using private key passed via CLI --key flag'); privateKey = this.key; } else if (ENV.HYP_KEY) { + logBlue('Using private key from .env'); privateKey = ENV.HYP_KEY; } else { const strategyConfig = await signerStrategy.getSignerConfig(chain); if (!strategyConfig?.privateKey) { throw new Error(`No private key found for chain ${chain}`); } + logBlue('Extracting private key from strategy config/user prompt'); + privateKey = strategyConfig.privateKey; } From 7a5fc2c797ee63d3574af9c9d5e92f9978d94ab5 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:45:59 +0330 Subject: [PATCH 047/132] feat: support loading token contract files on @hyperlane-xyz/starknet --- starknet/src/index.ts | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/starknet/src/index.ts b/starknet/src/index.ts index eb1798f02fd..e29dbe78136 100644 --- a/starknet/src/index.ts +++ b/starknet/src/index.ts @@ -61,31 +61,46 @@ export const getCompiledContractCasm = (name: string): CairoAssembly => { }; /** - * @notice Locates a contract file with the specified suffix - * @dev Combines the target path with contract name and suffix, validates file existence - * @param name The name of the contract to find - * @param suffix The suffix type from CONFIG.SUFFIXES to append to the filename - * @returns The full path to the contract file - * @throws {ContractError} If the file is not found or name is invalid + * @notice Finds the path to a contract file based on predefined patterns + * @dev Searches for contract files in multiple predefined locations: + * - contracts_{name}{suffix} + * - token_{name}{suffix} + * @param name The base name of the contract to find + * @param suffix The type of contract file to look for (from CONFIG.SUFFIXES) + * @returns {string} The full path to the first matching contract file + * @throws {ContractError} If no matching file is found or the contract name is invalid */ function findContractFile( name: string, suffix: keyof typeof CONFIG.SUFFIXES, ): string { assertValidContractName(name); - const mainPath = `${TARGET_DEV_PATH}/contracts_${name}${CONFIG.SUFFIXES[suffix]}`; - if (!existsSync(mainPath)) { + const suffixPath = CONFIG.SUFFIXES[suffix]; + const possiblePaths = [ + { + type: 'contracts', + path: `${TARGET_DEV_PATH}/contracts_${name}${suffixPath}`, + }, + { + type: 'token', + path: `${TARGET_DEV_PATH}/token_${name}${suffixPath}`, + }, + ]; + + const existingPath = possiblePaths.find(({ path }) => existsSync(path)); + + if (!existingPath) { throw new ContractError( ErrorMessages[CONFIG.ERROR_CODES.FILE_NOT_FOUND], CONFIG.ERROR_CODES.FILE_NOT_FOUND, { name, suffix, - path: mainPath, + path: possiblePaths[0].path, }, ); } - return mainPath; + return existingPath.path; } From 2cc7ca4486a209748f4be6ad8a6529aa48c0f9f1 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Wed, 20 Nov 2024 16:07:56 +0100 Subject: [PATCH 048/132] chore: MultiProtocolSignerOptions refactor --- typescript/cli/src/context/context.ts | 4 +- .../signer/MultiProtocolSignerManager.ts | 163 ++++++++++-------- 2 files changed, 97 insertions(+), 70 deletions(-) diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 3de5f7f0cf4..900752cd2a2 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -83,13 +83,13 @@ export async function signerMiddleware(argv: Record) { strategyConfig, chains, multiProvider, - key, + { key }, ); /** * @notice Attaches signers to MultiProvider and assigns it to argv.multiProvider */ - argv.multiProvider = await multiProtocolSigner.attachSignersToMp(); + argv.multiProvider = await multiProtocolSigner.setupMultiProvider(); return argv; } diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts index 3e27be20e0c..3a8605fe741 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -1,126 +1,153 @@ import { Signer } from 'ethers'; +import { Logger } from 'pino'; import { ChainName, ChainSubmissionStrategy, MultiProvider, } from '@hyperlane-xyz/sdk'; +import { assert, rootLogger } from '@hyperlane-xyz/utils'; -import { logBlue } from '../../../logger.js'; import { ENV } from '../../../utils/env.js'; import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js'; import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js'; +export interface MultiProtocolSignerOptions { + logger?: Logger; + key?: string; +} + /** * @title MultiProtocolSignerManager * @dev Context manager for signers across multiple protocols */ export class MultiProtocolSignerManager { - private signerStrategies: Map = new Map(); + protected readonly signerStrategies: Map; + protected readonly signers: Map; + public readonly logger: Logger; constructor( - submissionStrategy: ChainSubmissionStrategy, - private chains: ChainName[], - private multiProvider: MultiProvider, - private key?: string, + protected readonly submissionStrategy: ChainSubmissionStrategy, + protected readonly chains: ChainName[], + protected readonly multiProvider: MultiProvider, + protected readonly options: MultiProtocolSignerOptions = {}, ) { - // Initialize chain-specific strategies - for (const chain of chains) { + this.logger = + options?.logger || + rootLogger.child({ + module: 'MultiProtocolSignerManager', + }); + this.signerStrategies = new Map(); + this.signers = new Map(); + this.initializeStrategies(); + } + + /** + * @notice Sets up chain-specific signer strategies + */ + protected initializeStrategies(): void { + for (const chain of this.chains) { const strategy = MultiProtocolSignerFactory.getSignerStrategy( chain, - submissionStrategy, - multiProvider, + this.submissionStrategy, + this.multiProvider, ); this.signerStrategies.set(chain, strategy); } } /** - * @dev Gets signers config for specified chains + * @dev Configures signers for EVM chains in MultiProvider */ - private async getSignersConfig(): Promise< - Array<{ chain: ChainName; privateKey: string }> - > { - return Promise.all( - this.chains.map((chain) => this.getSignerConfigForChain(chain)), - ); - } - - /** - * @dev Gets private key from strategy or environment fallback - */ - private async getSignerConfigForChain( - chain: ChainName, - ): Promise<{ chain: ChainName; privateKey: string }> { - const signerStrategy = this.signerStrategies.get(chain); - if (!signerStrategy) { - throw new Error(`No signer strategy found for chain ${chain}`); - } - - // Determine private key with clear precedence - let privateKey: string; - if (this.key) { - logBlue('Using private key passed via CLI --key flag'); - privateKey = this.key; - } else if (ENV.HYP_KEY) { - logBlue('Using private key from .env'); - privateKey = ENV.HYP_KEY; - } else { - const strategyConfig = await signerStrategy.getSignerConfig(chain); - if (!strategyConfig?.privateKey) { - throw new Error(`No private key found for chain ${chain}`); - } - logBlue('Extracting private key from strategy config/user prompt'); - - privateKey = strategyConfig.privateKey; + async setupMultiProvider(): Promise { + for (const chain of this.chains) { + const signer = await this.initSigner(chain); + this.multiProvider.setSigner(chain, signer); } - return { - chain, - privateKey, - }; + return this.multiProvider; } /** - * @dev Gets protocol-specific signer for a chain + * @notice Creates signer for specific chain */ - async getSigner(chain: ChainName): Promise { - const { privateKey } = await this.getSignerConfigForChain(chain); + async initSigner(chain: ChainName): Promise { + const { privateKey } = await this.resolveConfig(chain); const signerStrategy = this.signerStrategies.get(chain); - if (!signerStrategy) { - throw new Error(`No signer strategy found for chain ${chain}`); - } + assert(signerStrategy, `No signer strategy found for chain ${chain}`); + return signerStrategy.getSigner({ privateKey }); } /** - * @dev Gets signers for all specified chains + * @notice Creates signers for all chains */ - async getSigners(): Promise> { - const signerConfigs = await this.getSignersConfig(); - const result: Record = {}; + async initAllSigners(): Promise { + const signerConfigs = await this.resolveAllConfigs(); for (const { chain, privateKey } of signerConfigs) { const signerStrategy = this.signerStrategies.get(chain); if (signerStrategy) { - result[chain] = signerStrategy.getSigner({ privateKey }); + this.signers.set(chain, signerStrategy.getSigner({ privateKey })); } } - return result; + return this.signers; } /** - * @dev Configures signers for chains in MultiProvider + * @notice Resolves all chain configurations */ - async attachSignersToMp(): Promise { - for (const chain of this.chains) { - const signer = await this.getSigner(chain); - this.multiProvider.setSigner(chain, signer); + private async resolveAllConfigs(): Promise< + Array<{ chain: ChainName; privateKey: string }> + > { + return Promise.all(this.chains.map((chain) => this.resolveConfig(chain))); + } + + /** + * @notice Resolves single chain configuration + */ + private async resolveConfig( + chain: ChainName, + ): Promise<{ chain: ChainName; privateKey: string }> { + const signerStrategy = this.signerStrategies.get(chain); + assert(signerStrategy, `No signer strategy found for chain ${chain}`); + + let privateKey: string; + + if (this.options.key) { + this.logger.info( + `Using private key passed via CLI --key flag for chain ${chain}`, + ); + privateKey = this.options.key; + } else if (ENV.HYP_KEY) { + this.logger.info(`Using private key from .env for chain ${chain}`); + privateKey = ENV.HYP_KEY; + } else { + privateKey = await this.extractPrivateKey(chain, signerStrategy); } - return this.multiProvider; + return { chain, privateKey }; + } + + /** + * @notice Gets private key from strategy + */ + private async extractPrivateKey( + chain: ChainName, + signerStrategy: IMultiProtocolSigner, + ): Promise { + const strategyConfig = await signerStrategy.getSignerConfig(chain); + assert( + strategyConfig.privateKey, + `No private key found for chain ${chain}`, + ); + + this.logger.info( + `Extracting private key from strategy config/user prompt for chain ${chain}`, + ); + return strategyConfig.privateKey; } } From d7326a696f2c58d1bac62766694b60758aa2bb23 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Wed, 20 Nov 2024 16:20:44 +0100 Subject: [PATCH 049/132] docs(changeset): Added strategy management CLI commands and MultiProtocolSigner implementation for flexible cross-chain signer configuration and management --- .changeset/spicy-gifts-hear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spicy-gifts-hear.md diff --git a/.changeset/spicy-gifts-hear.md b/.changeset/spicy-gifts-hear.md new file mode 100644 index 00000000000..c157be1cfd7 --- /dev/null +++ b/.changeset/spicy-gifts-hear.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': major +--- + +Added strategy management CLI commands and MultiProtocolSigner implementation for flexible cross-chain signer configuration and management From a9df9e8b7b71c8bf0e4754e6629dec104eef9283 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 21 Nov 2024 15:12:38 +0100 Subject: [PATCH 050/132] feat: integrate zksync-ethers wallet for ZKSync signer strategy --- typescript/cli/package.json | 3 ++- .../signer/MultiProtocolSignerFactory.ts | 5 ++--- yarn.lock | 14 +++++++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/typescript/cli/package.json b/typescript/cli/package.json index df2acaeb16e..7369dbd74ab 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -40,7 +40,8 @@ "eslint-config-prettier": "^9.1.0", "mocha": "^10.2.0", "prettier": "^2.8.8", - "typescript": "5.3.3" + "typescript": "5.3.3", + "zksync-ethers": "^5.10.0" }, "scripts": { "hyperlane": "node ./dist/cli.js", diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index 030f11b5f4e..557e90fc3c0 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -1,5 +1,6 @@ import { password } from '@inquirer/prompts'; import { Signer, Wallet } from 'ethers'; +import { Wallet as ZKSyncWallet } from 'zksync-ethers'; import { ChainName, @@ -56,8 +57,6 @@ class EthereumSignerStrategy extends BaseMultiProtocolSigner { } } -// 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean -// TODO: import ZKSync signer class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { async getSignerConfig(chain: ChainName): Promise { const submitter = this.config[chain]?.submitter as { @@ -74,6 +73,6 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { } getSigner(config: SignerConfig): Signer { - return new Wallet(config.privateKey); + return new ZKSyncWallet(config.privateKey); } } diff --git a/yarn.lock b/yarn.lock index 9ba2eef3e18..a9f8c878a63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7852,6 +7852,7 @@ __metadata: typescript: "npm:5.3.3" yaml: "npm:2.4.5" yargs: "npm:^17.7.2" + zksync-ethers: "npm:^5.10.0" zod: "npm:^3.21.2" zod-validation-error: "npm:^3.3.0" zx: "npm:^8.1.4" @@ -19643,7 +19644,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:5.7.2, ethers@npm:^5.1.0, ethers@npm:^5.3.1, ethers@npm:^5.7.0, ethers@npm:^5.7.1, ethers@npm:^5.7.2": +"ethers@npm:5.7.2, ethers@npm:^5.1.0, ethers@npm:^5.3.1, ethers@npm:^5.7.0, ethers@npm:^5.7.1, ethers@npm:^5.7.2, ethers@npm:~5.7.0": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -33425,6 +33426,17 @@ __metadata: languageName: node linkType: hard +"zksync-ethers@npm:^5.10.0": + version: 5.10.0 + resolution: "zksync-ethers@npm:5.10.0" + dependencies: + ethers: "npm:~5.7.0" + peerDependencies: + ethers: ~5.7.0 + checksum: 10/826719e2e40731e1104cf8a0c16c758526de6ca9e907d0483eb5bd80b635f02e3cce012115b75d68976a8dd746d63d4f83d576cc3bddc18a02a49d2bc023347f + languageName: node + linkType: hard + "zksync-web3@npm:^0.14.3": version: 0.14.4 resolution: "zksync-web3@npm:0.14.4" From 81a6ba7ee0a95b1a28018ffe9a45b4ae05fc882b Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:22:04 +0330 Subject: [PATCH 051/132] feat: support erc20 deployment on both starknet and evm chains --- typescript/cli/src/deploy/warp.ts | 121 +++++++++--------- typescript/sdk/src/index.ts | 1 + .../sdk/src/token/StarknetERC20WarpModule.ts | 69 ++++++++++ 3 files changed, 129 insertions(+), 62 deletions(-) create mode 100644 typescript/sdk/src/token/StarknetERC20WarpModule.ts diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 9f55ea87c25..34e67720975 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -1,5 +1,6 @@ import { confirm } from '@inquirer/prompts'; import { groupBy } from 'lodash-es'; +import { Account, RpcProvider } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; @@ -33,6 +34,7 @@ import { ProxyFactoryFactoriesAddresses, RemoteRouters, RoutingIsmConfig, + StarknetERC20WarpModule, SubmissionStrategy, TOKEN_TYPE_TO_STANDARD, TokenFactories, @@ -130,59 +132,34 @@ export async function runWarpRouteDeploy({ if (!skipConfirmation) apiKeys = await requestAndSaveApiKeys(chains, chainMetadata, registry); - const deploymentParams = { + await runDeployPlanStep({ context, warpDeployConfig: warpRouteConfig, - }; - - await runDeployPlanStep(deploymentParams); + }); - // Group chains string list by protocol - const chainsByProtocol = chains.reduce>( - (acc, chain) => { - const protocol = multiProvider.tryGetProtocol(chain); - assert(protocol, `protocol is not provided for chain: ${chain}`); - acc[protocol] = acc[protocol] || []; - acc[protocol].push(chain); - return acc; - }, - {} as Record, - ); - console.log({ chainsByProtocol }); + const chainsByProtocol = groupChainsByProtocol(chains, multiProvider); const deployments: WarpCoreConfig = { tokens: [] }; // Execute deployments for each protocol for (const protocol of Object.keys(chainsByProtocol) as ProtocolType[]) { const protocolChains = chainsByProtocol[protocol]; - const protocolDeploymentParams = { - context: deploymentParams.context, - warpDeployConfig: Object.fromEntries( - Object.entries(deploymentParams.warpDeployConfig).filter(([chain]) => - protocolChains.includes(chain), - ), - ), - }; - switch (protocol) { case ProtocolType.Ethereum: { const userAddress = await signer.getAddress(); - await runPreflightChecksForChains({ context, chains: protocolChains, minGas: MINIMUM_WARP_DEPLOY_GAS, }); - await prepareDeploy(context, userAddress, protocolChains); - const deployedContracts = await executeDeploy( - protocolDeploymentParams, + { context, warpDeployConfig: warpRouteConfig }, apiKeys, ); const warpCoreConfig = await getWarpCoreConfig( - deploymentParams, + { context, warpDeployConfig: warpRouteConfig }, deployedContracts, ); deployments.tokens = [ @@ -197,13 +174,17 @@ export async function runWarpRouteDeploy({ break; case ProtocolType.Starknet: + await executeStarknetDeployments({ + warpRouteConfig, + context, + }); break; default: throw new Error(`Unsupported protocol type: ${protocol}`); } } - + return; await writeDeploymentArtifacts(deployments, context); await completeDeploy(context, 'warp', {}, '', chains); } @@ -290,6 +271,9 @@ async function deployAndResolveWarpIsm( return promiseObjAll( objMap(warpConfig, async (chain, config) => { if ( + // for non-evm chains just return config as it is + multiProvider.getChainMetadata(chain).protocol !== + ProtocolType.Ethereum || !config.interchainSecurityModule || typeof config.interchainSecurityModule === 'string' ) { @@ -1005,34 +989,47 @@ async function getWarpApplySubmitter({ }); } -// async function runStarknetDeployments({ -// context, -// warpRouteConfig, -// chains, -// }: { -// context: WriteCommandContext; -// warpRouteConfig: WarpRouteDeployConfig; -// chains: ChainName[]; -// }): Promise> { -// if (chains.length === 0) { -// return {}; -// } - -// logBlue('Deploying Starknet contracts...'); - -// const deployedContracts: HyperlaneContractsMap = {}; - -// for (const chain of chains) { -// const config = warpRouteConfig[chain]; - -// // TODO: Implement Starknet-specific deployment logic: -// // 1. Set up Starknet provider and account similar to core.ts -// // 2. Create Starknet token deployer (similar to HypERC20Deployer/HypERC721Deployer) -// // 3. Deploy ISM if specified in config -// // 4. Deploy token contracts -// // 5. Store deployed addresses in deployedContracts map -// } - -// logGreen('✅ Starknet contract deployments complete'); -// return deployedContracts; -// } +function groupChainsByProtocol( + chains: ChainName[], + multiProvider: MultiProvider, +): Record { + return chains.reduce((protocolMap, chainName) => { + const protocolType = multiProvider.tryGetProtocol(chainName); + assert(protocolType, `Protocol not found for chain: ${chainName}`); + + if (!protocolMap[protocolType]) { + protocolMap[protocolType] = []; + } + + protocolMap[protocolType].push(chainName); + return protocolMap; + }, {} as Record); +} + +async function executeStarknetDeployments({ + warpRouteConfig, + context, +}: { + warpRouteConfig: WarpRouteDeployConfig; + context: WriteCommandContext; +}): Promise> { + const provider = new RpcProvider({ + nodeUrl: 'http://127.0.0.1:5050', + }); + const account = new Account( + provider, + '0x4acc9b79dae485fb71f309f5b62501a1329789f4418bb4c25353ad5617be4d4', + '0x000000000000000000000000000000002f663fafebbee32e0698f7e13f886c73', + ); + logBlue('🚀 Beginning Starknet warp deployments...'); + + assert(!warpRouteConfig.isNft, 'NFT routes not supported yet!'); + + const starknetDeployer = new StarknetERC20WarpModule( + account, + warpRouteConfig, + context.multiProvider, + ); + await starknetDeployer.deployToken(); + return {}; +} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 559fb7e5b10..00b032bf815 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -568,3 +568,4 @@ export { export { EvmIsmModule } from './ism/EvmIsmModule.js'; export { AnnotatedEV5Transaction } from './providers/ProviderType.js'; export { EvmERC20WarpModule } from './token/EvmERC20WarpModule.js'; +export { StarknetERC20WarpModule } from './token/StarknetERC20WarpModule.js'; diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts new file mode 100644 index 00000000000..c487ed825c2 --- /dev/null +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -0,0 +1,69 @@ +import { Account, byteArray, getChecksumAddress } from 'starknet'; + +import { ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; + +import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; +import { IsmConfig } from '../ism/types.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; + +import { WarpRouteDeployConfig } from './types.js'; + +export class StarknetERC20WarpModule { + protected logger = rootLogger.child({ module: 'StarknetERC20WarpModule' }); + protected deployer: StarknetDeployer; + + constructor( + protected readonly signer: Account, + protected readonly config: WarpRouteDeployConfig, + protected readonly multiProvider: MultiProvider, + ) { + this.deployer = new StarknetDeployer(signer); + } + + public async deployToken() { + for (const [chain, chainConfig] of Object.entries(this.config)) { + //Ignore non-starknet chains + if ( + this.multiProvider.getChainMetadata(chain).protocol !== + ProtocolType.Starknet + ) + continue; + + let ismAddress = await this.getStarknetDeploymentISMAddress({ + ismConfig: chainConfig.interchainSecurityModule, + mailbox: chainConfig.mailbox, + chain, + }); + + const tokenAddress = await this.deployer.deployContract('HypErc20', { + decimals: 18, + mailbox: chainConfig.mailbox, + total_supply: 0, + name: [byteArray.byteArrayFromString('etherum')], + symbol: [byteArray.byteArrayFromString('ETH')], + hook: getChecksumAddress(0), + interchain_security_module: ismAddress, + owner: this.signer.address, + }); + console.log({ tokenAddress }); + } + } + + async getStarknetDeploymentISMAddress({ + ismConfig, + chain, + mailbox, + }: { + ismConfig?: IsmConfig; + chain: string; + mailbox: string; + }): Promise { + if (!ismConfig) return getChecksumAddress(0); + if (typeof ismConfig === 'string') return ismConfig; + return await this.deployer.deployIsm({ + chain, + ismConfig, + mailbox, + }); + } +} From 78d1089c51e7af103dbcfba0356ccba8832509e0 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 21 Nov 2024 16:13:15 +0100 Subject: [PATCH 052/132] feat: integrate MultiProtocolProvider into signer middleware and manager --- typescript/cli/src/context/context.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 900752cd2a2..e2bd55773df 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -12,6 +12,7 @@ import { ChainMap, ChainMetadata, ChainName, + MultiProtocolProvider, MultiProvider, } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; @@ -58,8 +59,10 @@ export async function contextMiddleware(argv: Record) { export async function signerMiddleware(argv: Record) { const { key, context } = argv; - const { requiresKey, multiProvider } = context; + const { requiresKey, multiProvider, chainMetadata } = context; + const multiProtocolProvider = new MultiProtocolProvider(chainMetadata); + argv.multiProtocolProvider = multiProtocolProvider; if (!requiresKey) return argv; const strategyConfig = await readChainSubmissionStrategyConfig( @@ -83,6 +86,7 @@ export async function signerMiddleware(argv: Record) { strategyConfig, chains, multiProvider, + multiProtocolProvider, { key }, ); From f5dd6eb174ba1593c35e385758f6df082d200e3a Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 21 Nov 2024 16:14:02 +0100 Subject: [PATCH 053/132] feat: import MultiProtocolProvider into MultiProtocolSignerManager --- .../src/context/strategies/signer/MultiProtocolSignerManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts index 3a8605fe741..e612940630f 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -4,6 +4,7 @@ import { Logger } from 'pino'; import { ChainName, ChainSubmissionStrategy, + MultiProtocolProvider, MultiProvider, } from '@hyperlane-xyz/sdk'; import { assert, rootLogger } from '@hyperlane-xyz/utils'; @@ -31,6 +32,7 @@ export class MultiProtocolSignerManager { protected readonly submissionStrategy: ChainSubmissionStrategy, protected readonly chains: ChainName[], protected readonly multiProvider: MultiProvider, + private multiProtocolProvider: MultiProtocolProvider, protected readonly options: MultiProtocolSignerOptions = {}, ) { this.logger = From 182fc36c215d24581ff9c88b17c76db0cf6e7ca4 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 21 Nov 2024 16:54:57 +0100 Subject: [PATCH 054/132] feat: add Starknet protocol support to SDK --- typescript/sdk/package.json | 1 + .../src/providers/MultiProtocolProvider.ts | 10 + typescript/sdk/src/providers/ProviderType.ts | 50 ++++- .../sdk/src/providers/explorerHealthTest.ts | 2 + .../sdk/src/providers/providerBuilders.ts | 13 ++ typescript/sdk/src/token/TokenStandard.ts | 1 + typescript/utils/src/types.ts | 2 + yarn.lock | 206 +++++++++++++++++- 8 files changed, 279 insertions(+), 6 deletions(-) diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 9f4fd061598..656da21d736 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -20,6 +20,7 @@ "cross-fetch": "^3.1.5", "ethers": "^5.7.2", "pino": "^8.19.0", + "starknet": "6.17.0", "viem": "^2.21.45", "zod": "^3.21.2" }, diff --git a/typescript/sdk/src/providers/MultiProtocolProvider.ts b/typescript/sdk/src/providers/MultiProtocolProvider.ts index 6148f76a000..28b7f7f2030 100644 --- a/typescript/sdk/src/providers/MultiProtocolProvider.ts +++ b/typescript/sdk/src/providers/MultiProtocolProvider.ts @@ -24,6 +24,7 @@ import { ProviderMap, ProviderType, SolanaWeb3Provider, + StarknetJsProvider, TypedProvider, TypedTransaction, ViemProvider, @@ -205,6 +206,15 @@ export class MultiProtocolProvider< ); } + getStarknetProvider( + chainNameOrId: ChainNameOrId, + ): StarknetJsProvider['provider'] { + return this.getSpecificProvider( + chainNameOrId, + ProviderType.Starknet, + ); + } + setProvider( chainNameOrId: ChainNameOrId, provider: TypedProvider, diff --git a/typescript/sdk/src/providers/ProviderType.ts b/typescript/sdk/src/providers/ProviderType.ts index ce1740873d8..a0f9a006364 100644 --- a/typescript/sdk/src/providers/ProviderType.ts +++ b/typescript/sdk/src/providers/ProviderType.ts @@ -15,6 +15,12 @@ import type { providers as EV5Providers, PopulatedTransaction as EV5Transaction, } from 'ethers'; +import { + Contract as StarknetContract, + RpcProvider as StarknetProvider, + ReceiptTx as StarknetReceiptTx, + V3TransactionDetails as StarknetTransaction, +} from 'starknet'; import type { GetContractReturnType, PublicClient, @@ -31,6 +37,7 @@ export enum ProviderType { CosmJs = 'cosmjs', CosmJsWasm = 'cosmjs-wasm', GnosisTxBuilder = 'gnosis-txBuilder', + Starknet = 'starknet', } export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< @@ -40,6 +47,7 @@ export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< [ProtocolType.Ethereum]: ProviderType.EthersV5, [ProtocolType.Sealevel]: ProviderType.SolanaWeb3, [ProtocolType.Cosmos]: ProviderType.CosmJsWasm, + [ProtocolType.Starknet]: ProviderType.Starknet, }; export type ProviderMap = Partial>; @@ -63,6 +71,12 @@ type ProtocolTypesMapping = { contract: CosmJsWasmContract; receipt: CosmJsWasmTransactionReceipt; }; + [ProtocolType.Starknet]: { + transaction: StarknetJsTransaction; + provider: StarknetJsProvider; + contract: StarknetJsContract; + receipt: StarknetJsTransactionReceipt; + }; }; type ProtocolTyped< @@ -124,13 +138,20 @@ export interface CosmJsWasmProvider provider: Promise; } +export interface StarknetJsProvider + extends TypedProviderBase { + type: ProviderType.Starknet; + provider: StarknetProvider; +} + export type TypedProvider = | EthersV5Provider // | EthersV6Provider | ViemProvider | SolanaWeb3Provider | CosmJsProvider - | CosmJsWasmProvider; + | CosmJsWasmProvider + | StarknetJsProvider; /** * Contracts with discriminated union of provider type @@ -169,13 +190,20 @@ export interface CosmJsWasmContract contract: CosmWasmContract; } +export interface StarknetJsContract + extends TypedContractBase { + type: ProviderType.Starknet; + contract: StarknetContract; +} + export type TypedContract = | EthersV5Contract // | EthersV6Contract | ViemContract | SolanaWeb3Contract | CosmJsContract - | CosmJsWasmContract; + | CosmJsWasmContract + | StarknetJsContract; /** * Transactions with discriminated union of provider type @@ -216,13 +244,20 @@ export interface CosmJsWasmTransaction transaction: ExecuteInstruction; } +export interface StarknetJsTransaction + extends TypedTransactionBase { + type: ProviderType.Starknet; + transaction: StarknetTransaction; +} + export type TypedTransaction = | EthersV5Transaction // | EthersV6Transaction | ViemTransaction | SolanaWeb3Transaction | CosmJsTransaction - | CosmJsWasmTransaction; + | CosmJsWasmTransaction + | StarknetJsTransaction; /** * Transaction receipt/response with discriminated union of provider type @@ -263,9 +298,16 @@ export interface CosmJsWasmTransactionReceipt receipt: DeliverTxResponse; } +export interface StarknetJsTransactionReceipt + extends TypedTransactionReceiptBase { + type: ProviderType.Starknet; + receipt: StarknetReceiptTx; +} + export type TypedTransactionReceipt = | EthersV5TransactionReceipt | ViemTransactionReceipt | SolanaWeb3TransactionReceipt | CosmJsTransactionReceipt - | CosmJsWasmTransactionReceipt; + | CosmJsWasmTransactionReceipt + | StarknetJsTransactionReceipt; diff --git a/typescript/sdk/src/providers/explorerHealthTest.ts b/typescript/sdk/src/providers/explorerHealthTest.ts index 4e6d8be3ff8..e17494a25e2 100644 --- a/typescript/sdk/src/providers/explorerHealthTest.ts +++ b/typescript/sdk/src/providers/explorerHealthTest.ts @@ -11,6 +11,8 @@ const PROTOCOL_TO_ADDRESS: Record = { [ProtocolType.Ethereum]: '0x0000000000000000000000000000000000000000', [ProtocolType.Sealevel]: '11111111111111111111111111111111', [ProtocolType.Cosmos]: 'cosmos100000000000000000000000000000000000000', + [ProtocolType.Starknet]: + '0x0000000000000000000000000000000000000000000000000000000000000000', }; const PROTOCOL_TO_TX_HASH: Partial> = { diff --git a/typescript/sdk/src/providers/providerBuilders.ts b/typescript/sdk/src/providers/providerBuilders.ts index bc8051b1baa..ba05f2d5b93 100644 --- a/typescript/sdk/src/providers/providerBuilders.ts +++ b/typescript/sdk/src/providers/providerBuilders.ts @@ -2,6 +2,7 @@ import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'; import { StargateClient } from '@cosmjs/stargate'; import { Connection } from '@solana/web3.js'; import { providers } from 'ethers'; +import { RpcProvider as StarknetRpcProvider } from 'starknet'; import { createPublicClient, http } from 'viem'; import { ProtocolType, isNumeric } from '@hyperlane-xyz/utils'; @@ -14,6 +15,7 @@ import { EthersV5Provider, ProviderType, SolanaWeb3Provider, + StarknetJsProvider, TypedProvider, ViemProvider, } from './ProviderType.js'; @@ -109,6 +111,15 @@ export function defaultCosmJsWasmProviderBuilder( }; } +export function defaultStarknetJsProviderBuilder( + rpcUrls: RpcUrl[], +): StarknetJsProvider { + const provider = new StarknetRpcProvider({ + nodeUrl: rpcUrls[0].http, + }); + return { provider, type: ProviderType.Starknet }; +} + // Kept for backwards compatibility export function defaultProviderBuilder( rpcUrls: RpcUrl[], @@ -128,6 +139,7 @@ export const defaultProviderBuilderMap: ProviderBuilderMap = { [ProviderType.SolanaWeb3]: defaultSolProviderBuilder, [ProviderType.CosmJs]: defaultCosmJsProviderBuilder, [ProviderType.CosmJsWasm]: defaultCosmJsWasmProviderBuilder, + [ProviderType.Starknet]: defaultStarknetJsProviderBuilder, }; export const protocolToDefaultProviderBuilder: Record< @@ -137,4 +149,5 @@ export const protocolToDefaultProviderBuilder: Record< [ProtocolType.Ethereum]: defaultEthersV5ProviderBuilder, [ProtocolType.Sealevel]: defaultSolProviderBuilder, [ProtocolType.Cosmos]: defaultCosmJsWasmProviderBuilder, + [ProtocolType.Starknet]: defaultStarknetJsProviderBuilder, }; diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 690096a43df..2a8ec8c5aac 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -171,4 +171,5 @@ export const PROTOCOL_TO_NATIVE_STANDARD: Record = [ProtocolType.Ethereum]: TokenStandard.EvmNative, [ProtocolType.Cosmos]: TokenStandard.CosmosNative, [ProtocolType.Sealevel]: TokenStandard.SealevelNative, + [ProtocolType.Starknet]: TokenStandard.EvmNative, // TODO: define starknet token types based on cairo contracts }; diff --git a/typescript/utils/src/types.ts b/typescript/utils/src/types.ts index c2a3a1fcfd2..5faaf3a6a49 100644 --- a/typescript/utils/src/types.ts +++ b/typescript/utils/src/types.ts @@ -5,6 +5,7 @@ export enum ProtocolType { Ethereum = 'ethereum', Sealevel = 'sealevel', Cosmos = 'cosmos', + Starknet = 'starknet', } // A type that also allows for literal values of the enum export type ProtocolTypeValue = `${ProtocolType}`; @@ -13,6 +14,7 @@ export const ProtocolSmallestUnit = { [ProtocolType.Ethereum]: 'wei', [ProtocolType.Sealevel]: 'lamports', [ProtocolType.Cosmos]: 'uATOM', + [ProtocolType.Starknet]: 'wei', }; /********* BASIC TYPES *********/ diff --git a/yarn.lock b/yarn.lock index 1a658284c0d..1a5d31057e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8693,6 +8693,7 @@ __metadata: pino: "npm:^8.19.0" prettier: "npm:^2.8.8" sinon: "npm:^13.0.2" + starknet: "npm:6.17.0" ts-node: "npm:^10.8.0" tsx: "npm:^4.7.1" typescript: "npm:5.3.3" @@ -10309,6 +10310,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:~1.3.0": + version: 1.3.0 + resolution: "@noble/curves@npm:1.3.0" + dependencies: + "@noble/hashes": "npm:1.3.3" + checksum: 10/f3cbdd1af00179e30146eac5539e6df290228fb857a7a8ba36d1a772cbe59288a2ca83d06f175d3446ef00db3a80d7fd8b8347f7de9c2d4d5bf3865d8bb78252 + languageName: node + linkType: hard + "@noble/hashes@npm:1.0.0, @noble/hashes@npm:~1.0.0": version: 1.0.0 resolution: "@noble/hashes@npm:1.0.0" @@ -10323,6 +10333,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.3.3, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.3": + version: 1.3.3 + resolution: "@noble/hashes@npm:1.3.3" + checksum: 10/1025ddde4d24630e95c0818e63d2d54ee131b980fe113312d17ed7468bc18f54486ac86c907685759f8a7e13c2f9b9e83ec7b67d1cc20836f36b5e4a65bb102d + languageName: node + linkType: hard + "@noble/hashes@npm:1.4.0, @noble/hashes@npm:~1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" @@ -13939,7 +13956,7 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.6, @scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": +"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.3, @scure/base@npm:~1.1.6, @scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" checksum: 10/f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb @@ -14016,6 +14033,16 @@ __metadata: languageName: node linkType: hard +"@scure/starknet@npm:~1.0.0": + version: 1.0.0 + resolution: "@scure/starknet@npm:1.0.0" + dependencies: + "@noble/curves": "npm:~1.3.0" + "@noble/hashes": "npm:~1.3.3" + checksum: 10/0f7a627cfa3cf5f679adc805e63c3d087f2e2501347a73d5d9cf72d7e54f788bc27f228901c75818e0fcc82b4d847527b958f2a192e8572e88526c4fca93085c + languageName: node + linkType: hard + "@sentry/core@npm:5.30.0": version: 5.30.0 resolution: "@sentry/core@npm:5.30.0" @@ -18339,6 +18366,20 @@ __metadata: languageName: node linkType: hard +"abi-wan-kanabi@npm:^2.2.3": + version: 2.2.3 + resolution: "abi-wan-kanabi@npm:2.2.3" + dependencies: + ansicolors: "npm:^0.3.2" + cardinal: "npm:^2.1.1" + fs-extra: "npm:^10.0.0" + yargs: "npm:^17.7.2" + bin: + generate: dist/generate.js + checksum: 10/112ff2ee880ada687e033be1de3680a6ee51d411e0c13a7aa1160349ecc8ec0b79d2b11f352b8049db7aa45e8d10090bd99123905007423ee1ace9fb89f740de + languageName: node + linkType: hard + "abitype@npm:1.0.6, abitype@npm:^1.0.6": version: 1.0.6 resolution: "abitype@npm:1.0.6" @@ -18761,6 +18802,13 @@ __metadata: languageName: node linkType: hard +"ansicolors@npm:^0.3.2, ansicolors@npm:~0.3.2": + version: 0.3.2 + resolution: "ansicolors@npm:0.3.2" + checksum: 10/0704d1485d84d65a47aacd3d2d26f501f21aeeb509922c8f2496d0ec5d346dc948efa64f3151aef0571d73e5c44eb10fd02f27f59762e9292fe123bb1ea9ff7d + languageName: node + linkType: hard + "antlr4@npm:^4.13.1-patch-1": version: 4.13.1 resolution: "antlr4@npm:4.13.1" @@ -20336,6 +20384,18 @@ __metadata: languageName: node linkType: hard +"cardinal@npm:^2.1.1": + version: 2.1.1 + resolution: "cardinal@npm:2.1.1" + dependencies: + ansicolors: "npm:~0.3.2" + redeyed: "npm:~2.1.0" + bin: + cdl: ./bin/cdl.js + checksum: 10/caf0d34739ef7b1d80e1753311f889997b62c4490906819eb5da5bd46e7f5e5caba7a8a96ca401190c7d9c18443a7749e5338630f7f9a1ae98d60cac49b9008e + languageName: node + linkType: hard + "case@npm:^1.6.3": version: 1.6.3 resolution: "case@npm:1.6.3" @@ -24380,6 +24440,16 @@ __metadata: languageName: node linkType: hard +"fetch-cookie@npm:^3.0.0": + version: 3.0.1 + resolution: "fetch-cookie@npm:3.0.1" + dependencies: + set-cookie-parser: "npm:^2.4.8" + tough-cookie: "npm:^4.0.0" + checksum: 10/7ca3b56e71564e51a292f2590b1103b9223137f1a5163127c00339a6b02b3f3a216e0072f554e8f93c93899c52da72939b9d9bc72e76c8060f10c2b4867cfc85 + languageName: node + linkType: hard + "fetch-retry@npm:^5.0.2": version: 5.0.6 resolution: "fetch-retry@npm:5.0.6" @@ -27359,6 +27429,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^3.0.0": + version: 3.0.0 + resolution: "isomorphic-fetch@npm:3.0.0" + dependencies: + node-fetch: "npm:^2.6.1" + whatwg-fetch: "npm:^3.4.1" + checksum: 10/568fe0307528c63405c44dd3873b7b6c96c0d19ff795cb15846e728b6823bdbc68cc8c97ac23324509661316f12f551e43dac2929bc7030b8bc4d6aa1158b857 + languageName: node + linkType: hard + "isomorphic-unfetch@npm:^3.0.0": version: 3.1.0 resolution: "isomorphic-unfetch@npm:3.1.0" @@ -29041,6 +29121,13 @@ __metadata: languageName: node linkType: hard +"lossless-json@npm:^4.0.1": + version: 4.0.2 + resolution: "lossless-json@npm:4.0.2" + checksum: 10/76de08676c94e3fa31f04fbb2be0a9c0096ff9f68dbb8275d777d55f2123754501b626b5f665d0ff0aabaf56e827b3f21b355cc81c4eac554080876623318b55 + languageName: node + linkType: hard + "loupe@npm:^2.3.1": version: 2.3.4 resolution: "loupe@npm:2.3.4" @@ -31217,7 +31304,7 @@ __metadata: languageName: node linkType: hard -"pako@npm:^2.0.2": +"pako@npm:^2.0.2, pako@npm:^2.0.4": version: 2.1.0 resolution: "pako@npm:2.1.0" checksum: 10/38a04991d0ec4f4b92794a68b8c92bf7340692c5d980255c92148da96eb3e550df7a86a7128b5ac0c65ecddfe5ef3bbe9c6dab13e1bc315086e759b18f7c1401 @@ -32162,6 +32249,15 @@ __metadata: languageName: node linkType: hard +"psl@npm:^1.1.33": + version: 1.11.0 + resolution: "psl@npm:1.11.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10/ceb32d9152dfa323bf30d7f4f343305f906591cfea08725627c98317a9b46b5f1a1e545e52ffd4e03e2808dde36dc626e77d31e6c38ef851611031c936319af0 + languageName: node + linkType: hard + "pstree.remy@npm:^1.1.8": version: 1.1.8 resolution: "pstree.remy@npm:1.1.8" @@ -32221,6 +32317,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 + languageName: node + linkType: hard + "puppeteer-core@npm:^2.1.1": version: 2.1.1 resolution: "puppeteer-core@npm:2.1.1" @@ -32396,6 +32499,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 10/46ab16f252fd892fc29d6af60966d338cdfeea68a231e9457631ffd22d67cec1e00141e0a5236a2eb16c0d7d74175d9ec1d6f963660c6f2b1c2fc85b194c5680 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2, queue-microtask@npm:^1.2.3": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -33027,6 +33137,15 @@ __metadata: languageName: node linkType: hard +"redeyed@npm:~2.1.0": + version: 2.1.1 + resolution: "redeyed@npm:2.1.1" + dependencies: + esprima: "npm:~4.0.0" + checksum: 10/86880f97d54bb55bbf1c338e27fe28f18f52afc2f5afa808354a09a3777aa79b4f04e04844350d7fec80aa2d299196bde256b21f586e7e5d9b63494bd4a9db27 + languageName: node + linkType: hard + "reduce-flatten@npm:^2.0.0": version: 2.0.0 resolution: "reduce-flatten@npm:2.0.0" @@ -33287,6 +33406,13 @@ __metadata: languageName: node linkType: hard +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: 10/878880ee78ccdce372784f62f52a272048e2d0827c29ae31e7f99da18b62a2b9463ea03a75f277352f4697c100183debb0532371ad515a2d49d4bfe596dd4c20 + languageName: node + linkType: hard + "resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" @@ -34153,6 +34279,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.4.8": + version: 2.7.1 + resolution: "set-cookie-parser@npm:2.7.1" + checksum: 10/c92b1130032693342bca13ea1b1bc93967ab37deec4387fcd8c2a843c0ef2fd9a9f3df25aea5bb3976cd05a91c2cf4632dd6164d6e1814208fb7d7e14edd42b4 + languageName: node + linkType: hard + "set-function-length@npm:^1.1.1": version: 1.1.1 resolution: "set-function-length@npm:1.1.1" @@ -34980,6 +35113,32 @@ __metadata: languageName: node linkType: hard +"starknet-types-07@npm:@starknet-io/types-js@^0.7.7": + version: 0.7.7 + resolution: "@starknet-io/types-js@npm:0.7.7" + checksum: 10/12c80c34c167d51ebe2cbd210703749aa74faf341cefb5cc44b118418279e095efe93f3ebc90f8b828888f66a6f230522a22c87bfed53a49ddf515637c0b9b5f + languageName: node + linkType: hard + +"starknet@npm:6.17.0": + version: 6.17.0 + resolution: "starknet@npm:6.17.0" + dependencies: + "@noble/curves": "npm:~1.3.0" + "@noble/hashes": "npm:~1.3.0" + "@scure/base": "npm:~1.1.3" + "@scure/starknet": "npm:~1.0.0" + abi-wan-kanabi: "npm:^2.2.3" + fetch-cookie: "npm:^3.0.0" + isomorphic-fetch: "npm:^3.0.0" + lossless-json: "npm:^4.0.1" + pako: "npm:^2.0.4" + starknet-types-07: "npm:@starknet-io/types-js@^0.7.7" + ts-mixer: "npm:^6.0.3" + checksum: 10/709c44f933ea20b7c925c493b1dcc8e1cd3c6b3b5776fd726a8fc56e3bb514187cd81e4899f4e3999231dcf389ed591a8d0515ce4a27f0378b874e556b539f79 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -36059,6 +36218,18 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^4.0.0": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" + dependencies: + psl: "npm:^1.1.33" + punycode: "npm:^2.1.1" + universalify: "npm:^0.2.0" + url-parse: "npm:^1.5.3" + checksum: 10/75663f4e2cd085f16af0b217e4218772adf0617fb3227171102618a54ce0187a164e505d61f773ed7d65988f8ff8a8f935d381f87da981752c1171b076b4afac + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -36185,6 +36356,13 @@ __metadata: languageName: node linkType: hard +"ts-mixer@npm:^6.0.3": + version: 6.0.4 + resolution: "ts-mixer@npm:6.0.4" + checksum: 10/f20571a4a4ff7b5e1a2ff659208c1ea9d4180dda932b71d289edc99e25a2948c9048e2e676b930302ac0f8e88279e0da6022823183e67de3906a3f3a8b72ea80 + languageName: node + linkType: hard + "ts-node@npm:^10.8.0": version: 10.9.2 resolution: "ts-node@npm:10.9.2" @@ -36919,6 +37097,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^0.2.0": + version: 0.2.0 + resolution: "universalify@npm:0.2.0" + checksum: 10/e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5 + languageName: node + linkType: hard + "universalify@npm:^2.0.0": version: 2.0.0 resolution: "universalify@npm:2.0.0" @@ -37077,6 +37262,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:^1.5.3": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: 10/c9e96bc8c5b34e9f05ddfeffc12f6aadecbb0d971b3cc26015b58d5b44676a99f50d5aeb1e5c9e61fa4d49961ae3ab1ae997369ed44da51b2f5ac010d188e6ad + languageName: node + linkType: hard + "url-set-query@npm:^1.0.0": version: 1.0.0 resolution: "url-set-query@npm:1.0.0" @@ -38037,6 +38232,13 @@ __metadata: languageName: node linkType: hard +"whatwg-fetch@npm:^3.4.1": + version: 3.6.20 + resolution: "whatwg-fetch@npm:3.6.20" + checksum: 10/2b4ed92acd6a7ad4f626a6cb18b14ec982bbcaf1093e6fe903b131a9c6decd14d7f9c9ca3532663c2759d1bdf01d004c77a0adfb2716a5105465c20755a8c57c + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" From e233bfa15f019f606ee007c6da8fc08451aa2f00 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:45:58 +0330 Subject: [PATCH 055/132] feat: read token metadata on starknte erc20 deployer module --- .../sdk/src/token/StarknetERC20WarpModule.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index c487ed825c2..36833567b67 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -1,11 +1,12 @@ import { Account, byteArray, getChecksumAddress } from 'starknet'; -import { ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert, rootLogger } from '@hyperlane-xyz/utils'; import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; import { IsmConfig } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; +import { HypERC20Deployer } from './deploy.js'; import { WarpRouteDeployConfig } from './types.js'; export class StarknetERC20WarpModule { @@ -21,6 +22,15 @@ export class StarknetERC20WarpModule { } public async deployToken() { + // TODO: manage this in a multi-protocol way, for now works as we just support native-synthetic pair + const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( + this.multiProvider, + this.config, + ); + assert( + tokenMetadata && tokenMetadata.decimals, + "Token metadata can't be extracted", + ); for (const [chain, chainConfig] of Object.entries(this.config)) { //Ignore non-starknet chains if ( @@ -36,14 +46,14 @@ export class StarknetERC20WarpModule { }); const tokenAddress = await this.deployer.deployContract('HypErc20', { - decimals: 18, + decimals: tokenMetadata.decimals, mailbox: chainConfig.mailbox, - total_supply: 0, - name: [byteArray.byteArrayFromString('etherum')], - symbol: [byteArray.byteArrayFromString('ETH')], + total_supply: tokenMetadata.totalSupply, + name: [byteArray.byteArrayFromString(tokenMetadata.name)], + symbol: [byteArray.byteArrayFromString(tokenMetadata.symbol)], hook: getChecksumAddress(0), interchain_security_module: ismAddress, - owner: this.signer.address, + owner: this.signer.address, //TODO: use config.owner, and in warp init ask for starknet owner }); console.log({ tokenAddress }); } From 65615046412abb8d2aedcf0f9835b863a0f18977 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 21 Nov 2024 18:45:35 +0100 Subject: [PATCH 056/132] refactor: improve signer management in warp route deploy config creation --- typescript/cli/src/config/warp.ts | 31 +++++++++++++++++++++------ typescript/cli/src/context/context.ts | 6 +++--- typescript/cli/src/context/types.ts | 2 ++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 1174d0156b1..5f628a1677b 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -21,6 +21,8 @@ import { promiseObjAll, } from '@hyperlane-xyz/utils'; +import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; +import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; import { runMultiChainSelectionStep } from '../utils/chains.js'; @@ -35,6 +37,7 @@ import { } from '../utils/input.js'; import { createAdvancedIsmConfig } from './ism.js'; +import { readChainSubmissionStrategyConfig } from './strategy.js'; const TYPE_DESCRIPTIONS: Record = { [TokenType.synthetic]: 'A new ERC20 with remote transfer functionality', @@ -122,13 +125,6 @@ export async function createWarpRouteDeployConfig({ }) { logBlue('Creating a new warp route deployment config...'); - const owner = await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), - 'Enter the desired', - 'owner address', - 'signer', - ); - const warpChains = await runMultiChainSelectionStep({ chainMetadata: context.chainMetadata, message: 'Select chains to connect', @@ -138,11 +134,32 @@ export async function createWarpRouteDeployConfig({ requiresConfirmation: !context.skipConfirmation, }); + const strategyConfig = await readChainSubmissionStrategyConfig( + context.strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, + ); + + const multiProtocolSigner = new MultiProtocolSignerManager( + strategyConfig, + warpChains, + context.multiProvider, + { key: context.key }, + ); + + const multiProviderWithSigners = + await multiProtocolSigner.setupMultiProvider(); + const result: WarpRouteDeployConfig = {}; let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); + const owner = await detectAndConfirmOrPrompt( + async () => await multiProviderWithSigners.getSigner(chain).getAddress(), + 'Enter the desired', + 'owner address', + 'signer', + ); + // default to the mailbox from the registry and if not found ask to the user to submit one const chainAddresses = await context.registry.getChainAddresses(chain); diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 900752cd2a2..8fa69303bd4 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -45,6 +45,7 @@ export async function contextMiddleware(argv: Record) { requiresKey, disableProxy: argv.disableProxy, skipConfirmation: argv.yes, + strategyPath: argv.strategy, }; if (!isDryRun && settings.fromAddress) throw new Error( @@ -57,13 +58,12 @@ export async function contextMiddleware(argv: Record) { } export async function signerMiddleware(argv: Record) { - const { key, context } = argv; - const { requiresKey, multiProvider } = context; + const { key, requiresKey, multiProvider, strategyPath } = argv.context; if (!requiresKey) return argv; const strategyConfig = await readChainSubmissionStrategyConfig( - argv.strategy ?? DEFAULT_STRATEGY_CONFIG_PATH, + strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, ); /** diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index eef1ad2bbfb..7837972893d 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -17,6 +17,7 @@ export interface ContextSettings { requiresKey?: boolean; disableProxy?: boolean; skipConfirmation?: boolean; + strategyPath?: string; } export interface CommandContext { @@ -27,6 +28,7 @@ export interface CommandContext { key?: string; signer?: ethers.Signer; warpCoreConfig?: WarpCoreConfig; + strategyPath?: string; } export interface WriteCommandContext extends CommandContext { From e532b1ad6f153d5ddd97fccbc3436a72eded8795 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 22 Nov 2024 11:33:11 +0100 Subject: [PATCH 057/132] refactor: improve strategy config handling and type safety & sensitive key masking --- typescript/cli/src/commands/signCommands.ts | 1 - typescript/cli/src/commands/strategy.ts | 2 - typescript/cli/src/config/strategy.ts | 63 +++++++++---------- typescript/cli/src/config/warp.ts | 2 +- typescript/cli/src/context/context.ts | 6 +- .../strategies/chain/MultiChainResolver.ts | 4 +- .../strategies/chain/SingleChainResolver.ts | 4 +- .../cli/src/context/strategies/chain/types.ts | 4 +- typescript/cli/src/deploy/utils.ts | 1 - typescript/cli/src/send/transfer.ts | 1 - typescript/cli/src/tests/commands/core.ts | 1 - typescript/cli/src/utils/balances.ts | 1 - typescript/cli/src/utils/output.ts | 15 ++++- typescript/utils/src/addresses.ts | 6 +- 14 files changed, 59 insertions(+), 52 deletions(-) diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 07504a8b2c4..58afa1c04f1 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -8,7 +8,6 @@ export const SIGN_COMMANDS = [ 'status', 'submit', 'relayer', - 'read', ]; export function isSignCommand(argv: any): boolean { diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts index 6f85dcb885e..414a3d48ee3 100644 --- a/typescript/cli/src/commands/strategy.ts +++ b/typescript/cli/src/commands/strategy.ts @@ -48,7 +48,6 @@ export const init: CommandModuleWithWriteContext<{ export const read: CommandModuleWithWriteContext<{ strategy: string; - requiresKey: boolean; }> = { command: 'read', describe: 'Reads strategy configuration', @@ -58,7 +57,6 @@ export const read: CommandModuleWithWriteContext<{ demandOption: true, default: DEFAULT_STRATEGY_CONFIG_PATH, }, - requiresKey: { demandOption: false, default: false, hidden: true }, }, handler: async ({ strategy: strategyUrl }) => { logCommandHeader(`Hyperlane Strategy Read`); diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index f2b33119617..ce9fd9b41de 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -10,12 +10,13 @@ import { import { ProtocolType, assert, + errorToString, isAddress, isPrivateKeyEvm, } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; -import { errorRed, log, logBlue, logGreen } from '../logger.js'; +import { errorRed, log, logBlue, logGreen, logRed } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; import { indentYamlOrJson, @@ -25,48 +26,44 @@ import { } from '../utils/files.js'; import { maskSensitiveData } from '../utils/output.js'; +/** + * Reads and validates a chain submission strategy configuration from a file + */ export async function readChainSubmissionStrategyConfig( filePath: string, ): Promise { + log(`Reading submission strategy in ${filePath}`); try { - log(`Reading submission strategy in ${filePath}`); + const strategyConfig = readYamlOrJson(filePath); - if (!isFile(filePath.trim())) { - logBlue( - `No strategy config found in ${filePath}, returning empty config`, - ); - return {}; - } + const parseResult = ChainSubmissionStrategySchema.parse(strategyConfig); - const strategyConfig = readYamlOrJson( - filePath.trim(), - ); + return parseResult; + } catch (error) { + logRed(`⛔️ Error reading strategy config:`, errorToString(error)); + throw error; // Re-throw to let caller handle the error + } +} - // Check if config exists and is a non-empty object - if (!strategyConfig || typeof strategyConfig !== 'object') { - logBlue( - `No strategy config found in ${filePath}, returning empty config`, - ); +/** + * Safely reads chain submission strategy config, returns empty object if any errors occur + */ +export async function safeReadChainSubmissionStrategyConfig( + filePath: string, +): Promise { + try { + const trimmedFilePath = filePath.trim(); + if (!isFile(trimmedFilePath)) { + logBlue(`File ${trimmedFilePath} does not exist, returning empty config`); return {}; } - - const parseResult = ChainSubmissionStrategySchema.safeParse(strategyConfig); - if (!parseResult.success) { - errorRed( - `Strategy config validation using ChainSubmissionStrategySchema failed for ${filePath}`, - ); - errorRed(JSON.stringify(parseResult.error.errors, null, 2)); - throw new Error('Invalid strategy configuration'); - } - - return strategyConfig; + return await readChainSubmissionStrategyConfig(trimmedFilePath); } catch (error) { - if (error instanceof Error) { - errorRed(`Error reading strategy config: ${error.message}`); - } else { - errorRed('Unknown error reading strategy config'); - } - throw error; // Re-throw to let caller handle the error + logRed( + `Failed to read strategy config, defaulting to empty config:`, + errorToString(error), + ); + return {}; } } diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 5f628a1677b..b75058b4f24 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -154,7 +154,7 @@ export async function createWarpRouteDeployConfig({ logBlue(`${chain}: Configuring warp route...`); const owner = await detectAndConfirmOrPrompt( - async () => await multiProviderWithSigners.getSigner(chain).getAddress(), + async () => multiProviderWithSigners.getSigner(chain).getAddress(), 'Enter the desired', 'owner address', 'signer', diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 8fa69303bd4..f679b69be0e 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -18,7 +18,7 @@ import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; import { isSignCommand } from '../commands/signCommands.js'; -import { readChainSubmissionStrategyConfig } from '../config/strategy.js'; +import { safeReadChainSubmissionStrategyConfig } from '../config/strategy.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; @@ -36,7 +36,7 @@ import { export async function contextMiddleware(argv: Record) { const isDryRun = !isNullish(argv.dryRun); - const requiresKey = argv.requiresKey ?? isSignCommand(argv); + const requiresKey = isSignCommand(argv); const settings: ContextSettings = { registryUri: argv.registry, registryOverrideUri: argv.overrides, @@ -62,7 +62,7 @@ export async function signerMiddleware(argv: Record) { if (!requiresKey) return argv; - const strategyConfig = await readChainSubmissionStrategyConfig( + const strategyConfig = await safeReadChainSubmissionStrategyConfig( strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, ); diff --git a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts index 5d582e80df9..87e74752f33 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -1,4 +1,4 @@ -import { ChainName } from '@hyperlane-xyz/sdk'; +import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; import { assert } from '@hyperlane-xyz/utils'; import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; @@ -36,7 +36,7 @@ enum ChainSelectionMode { export class MultiChainResolver implements ChainResolver { constructor(private mode: ChainSelectionMode) {} - async resolveChains(argv: Record): Promise { + async resolveChains(argv: ChainMap): Promise { switch (this.mode) { case ChainSelectionMode.WARP_CONFIG: return this.resolveWarpRouteConfigChains(argv); diff --git a/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts b/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts index dd46cba5192..8dddaf3c4a4 100644 --- a/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts +++ b/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts @@ -1,4 +1,4 @@ -import { ChainName } from '@hyperlane-xyz/sdk'; +import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; import { runSingleChainSelectionStep } from '../../../utils/chains.js'; @@ -14,7 +14,7 @@ export class SingleChainResolver implements ChainResolver { * @notice Determines the chain to be used for signing operations * @dev Either uses the chain specified in argv or prompts for interactive selection */ - async resolveChains(argv: Record): Promise { + async resolveChains(argv: ChainMap): Promise { argv.chain ||= await runSingleChainSelectionStep( argv.context.chainMetadata, 'Select chain to connect:', diff --git a/typescript/cli/src/context/strategies/chain/types.ts b/typescript/cli/src/context/strategies/chain/types.ts index ab42c81acda..9318bed8c26 100644 --- a/typescript/cli/src/context/strategies/chain/types.ts +++ b/typescript/cli/src/context/strategies/chain/types.ts @@ -1,4 +1,4 @@ -import { ChainName } from '@hyperlane-xyz/sdk'; +import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; export interface ChainResolver { /** @@ -6,5 +6,5 @@ export interface ChainResolver { * @param argv Command arguments * @returns Array of chain names */ - resolveChains(argv: Record): Promise; + resolveChains(argv: ChainMap): Promise; } diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index c03cc761c34..e5dc8e0857d 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -57,7 +57,6 @@ export async function runPreflightChecksForChains({ await nativeBalancesAreSufficient( multiProvider, - null, chainsToGasCheck ?? chains, minGas, ); diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index dc4b8ca96b4..2929b09c6e8 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -141,7 +141,6 @@ async function executeDelivery({ token = warpCore.findToken(origin, routerAddress)!; } - // const senderAddress = await multiProvider.getSigner(origin).getAddress(); const errors = await warpCore.validateTransfer({ originTokenAmount: token.amount(amount), destination, diff --git a/typescript/cli/src/tests/commands/core.ts b/typescript/cli/src/tests/commands/core.ts index 51e44716939..24c91097443 100644 --- a/typescript/cli/src/tests/commands/core.ts +++ b/typescript/cli/src/tests/commands/core.ts @@ -30,7 +30,6 @@ export async function hyperlaneCoreRead(chain: string, coreOutputPath: string) { --registry ${REGISTRY_PATH} \ --config ${coreOutputPath} \ --chain ${chain} \ - --key ${ANVIL_KEY} \ --verbosity debug \ --yes`; } diff --git a/typescript/cli/src/utils/balances.ts b/typescript/cli/src/utils/balances.ts index ef497e62612..10df193b9c5 100644 --- a/typescript/cli/src/utils/balances.ts +++ b/typescript/cli/src/utils/balances.ts @@ -7,7 +7,6 @@ import { logGreen, logRed } from '../logger.js'; export async function nativeBalancesAreSufficient( multiProvider: MultiProvider, - signer: ethers.Signer | null, chains: ChainName[], minGas: string, ) { diff --git a/typescript/cli/src/utils/output.ts b/typescript/cli/src/utils/output.ts index bee37be859d..a9f512504da 100644 --- a/typescript/cli/src/utils/output.ts +++ b/typescript/cli/src/utils/output.ts @@ -66,6 +66,19 @@ export function maskPrivateKey(key: string): string { return `${middle}`; } +const SENSITIVE_PATTERNS = [ + 'privatekey', + 'key', + 'secret', + 'secretkey', + 'password', +]; + +const isSensitiveKey = (key: string) => { + const lowerKey = key.toLowerCase(); + return SENSITIVE_PATTERNS.some((pattern) => lowerKey.includes(pattern)); +}; + /** * @notice Recursively masks sensitive data in objects * @param obj Object with potential sensitive data @@ -77,7 +90,7 @@ export function maskSensitiveData(obj: any): any { if (typeof obj === 'object') { const masked = { ...obj }; for (const [key, value] of Object.entries(masked)) { - if (key === 'privateKey' && typeof value === 'string') { + if (isSensitiveKey(key) && typeof value === 'string') { masked[key] = maskPrivateKey(value); } else if (typeof value === 'object') { masked[key] = maskSensitiveData(value); diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index 035d1207e6e..b43d22d96ee 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -382,5 +382,9 @@ export function strip0x(hexstr: string) { } export function isPrivateKeyEvm(privateKey: string): boolean { - return new Wallet(privateKey).privateKey === privateKey; + try { + return new Wallet(privateKey).privateKey === privateKey; + } catch (e) { + throw new Error('Provided Private Key is not EVM compatible!'); + } } From afb3b21e5dcd85deca902f108ce0506d10a8097b Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 22 Nov 2024 11:33:39 +0100 Subject: [PATCH 058/132] refactor: sensitive key function name --- typescript/cli/src/utils/output.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typescript/cli/src/utils/output.ts b/typescript/cli/src/utils/output.ts index a9f512504da..bcb794c6808 100644 --- a/typescript/cli/src/utils/output.ts +++ b/typescript/cli/src/utils/output.ts @@ -56,11 +56,11 @@ export function formatYamlViolationsOutput( } /** - * @notice Masks private key with dots - * @param key Private key to mask + * @notice Masks sensitive key with dots + * @param key Sensitive key to mask * @return Masked key */ -export function maskPrivateKey(key: string): string { +export function maskSensitiveKey(key: string): string { if (!key) return key; const middle = '•'.repeat(key.length); return `${middle}`; @@ -91,7 +91,7 @@ export function maskSensitiveData(obj: any): any { const masked = { ...obj }; for (const [key, value] of Object.entries(masked)) { if (isSensitiveKey(key) && typeof value === 'string') { - masked[key] = maskPrivateKey(value); + masked[key] = maskSensitiveKey(value); } else if (typeof value === 'object') { masked[key] = maskSensitiveData(value); } From ef16d8ec0f3674103c952b2cc3890e9d9b987f79 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:18:46 +0330 Subject: [PATCH 059/132] feat: make warp deploy work on starknet for native/synthetic tokens beside evm chains --- typescript/cli/src/deploy/warp.ts | 78 ++++++++++++++++--- typescript/sdk/package.json | 2 +- .../sdk/src/token/StarknetERC20WarpModule.ts | 55 +++++++++---- 3 files changed, 107 insertions(+), 28 deletions(-) diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 34e67720975..6993d1f3692 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -78,11 +78,7 @@ import { writeYamlOrJson, } from '../utils/files.js'; -import { - completeDeploy, - prepareDeploy, - runPreflightChecksForChains, -} from './utils.js'; +import { prepareDeploy, runPreflightChecksForChains } from './utils.js'; interface DeployParams { context: WriteCommandContext; @@ -174,19 +170,22 @@ export async function runWarpRouteDeploy({ break; case ProtocolType.Starknet: - await executeStarknetDeployments({ + const addresses = await executeStarknetDeployments({ warpRouteConfig, context, }); + const warpCoreConfig = await getWarpCoreConfigForStarknet( + { context, warpDeployConfig: warpRouteConfig }, + addresses, + ); + deployments.tokens = [...deployments.tokens, ...warpCoreConfig.tokens]; break; default: throw new Error(`Unsupported protocol type: ${protocol}`); } } - return; await writeDeploymentArtifacts(deployments, context); - await completeDeploy(context, 'warp', {}, '', chains); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { @@ -1012,7 +1011,7 @@ async function executeStarknetDeployments({ }: { warpRouteConfig: WarpRouteDeployConfig; context: WriteCommandContext; -}): Promise> { +}): Promise> { const provider = new RpcProvider({ nodeUrl: 'http://127.0.0.1:5050', }); @@ -1030,6 +1029,63 @@ async function executeStarknetDeployments({ warpRouteConfig, context.multiProvider, ); - await starknetDeployer.deployToken(); - return {}; + + return await starknetDeployer.deployToken(); +} + +async function getWarpCoreConfigForStarknet( + { warpDeployConfig, context }: DeployParams, + contracts: ChainMap, +): Promise { + const warpCoreConfig: WarpCoreConfig = { tokens: [] }; + + // TODO: replace with warp read + const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( + context.multiProvider, + warpDeployConfig, + ); + assert( + tokenMetadata && isTokenMetadata(tokenMetadata), + 'Missing required token metadata', + ); + const { decimals, symbol, name } = tokenMetadata; + assert(decimals, 'Missing decimals on token metadata'); + + generateTokenConfigsForStarknet( + warpCoreConfig, + warpDeployConfig, + contracts, + symbol, + name, + decimals, + ); + + fullyConnectTokens(warpCoreConfig); + + return warpCoreConfig; +} + +function generateTokenConfigsForStarknet( + warpCoreConfig: WarpCoreConfig, + warpDeployConfig: WarpRouteDeployConfig, + contracts: ChainMap, + symbol: string, + name: string, + decimals: number, +): void { + for (const [chainName, contract] of Object.entries(contracts)) { + const config = warpDeployConfig[chainName]; + const collateralAddressOrDenom = isCollateralConfig(config) + ? config.token // gets set in the above deriveTokenMetadata() + : undefined; + warpCoreConfig.tokens.push({ + chainName, + standard: TOKEN_TYPE_TO_STANDARD[config.type], + decimals, + symbol, + name, + addressOrDenom: contract, + collateralAddressOrDenom, + }); + } } diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index cd9073f6c64..a1fbacc3fea 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -7,8 +7,8 @@ "@aws-sdk/client-s3": "^3.74.0", "@cosmjs/cosmwasm-stargate": "^0.32.4", "@cosmjs/stargate": "^0.32.4", - "@hyperlane-xyz/starknet-core": "1.0.0", "@hyperlane-xyz/core": "5.8.0", + "@hyperlane-xyz/starknet-core": "1.0.0", "@hyperlane-xyz/utils": "7.0.0", "@safe-global/api-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0", diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index 36833567b67..9470c8cec82 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -1,10 +1,12 @@ import { Account, byteArray, getChecksumAddress } from 'starknet'; +import { TokenType } from '@hyperlane-xyz/sdk'; import { ProtocolType, assert, rootLogger } from '@hyperlane-xyz/utils'; import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; import { IsmConfig } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; +import { ChainMap } from '../types.js'; import { HypERC20Deployer } from './deploy.js'; import { WarpRouteDeployConfig } from './types.js'; @@ -21,7 +23,7 @@ export class StarknetERC20WarpModule { this.deployer = new StarknetDeployer(signer); } - public async deployToken() { + public async deployToken(): Promise> { // TODO: manage this in a multi-protocol way, for now works as we just support native-synthetic pair const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( this.multiProvider, @@ -31,7 +33,11 @@ export class StarknetERC20WarpModule { tokenMetadata && tokenMetadata.decimals, "Token metadata can't be extracted", ); - for (const [chain, chainConfig] of Object.entries(this.config)) { + const addresses: ChainMap = {}; + for (const [ + chain, + { mailbox, interchainSecurityModule, type }, + ] of Object.entries(this.config)) { //Ignore non-starknet chains if ( this.multiProvider.getChainMetadata(chain).protocol !== @@ -40,23 +46,40 @@ export class StarknetERC20WarpModule { continue; let ismAddress = await this.getStarknetDeploymentISMAddress({ - ismConfig: chainConfig.interchainSecurityModule, - mailbox: chainConfig.mailbox, + ismConfig: interchainSecurityModule, + mailbox: mailbox, chain, }); - - const tokenAddress = await this.deployer.deployContract('HypErc20', { - decimals: tokenMetadata.decimals, - mailbox: chainConfig.mailbox, - total_supply: tokenMetadata.totalSupply, - name: [byteArray.byteArrayFromString(tokenMetadata.name)], - symbol: [byteArray.byteArrayFromString(tokenMetadata.symbol)], - hook: getChecksumAddress(0), - interchain_security_module: ismAddress, - owner: this.signer.address, //TODO: use config.owner, and in warp init ask for starknet owner - }); - console.log({ tokenAddress }); + switch (type) { + case TokenType.synthetic: { + const tokenAddress = await this.deployer.deployContract('HypErc20', { + decimals: tokenMetadata.decimals, + mailbox: mailbox, + total_supply: tokenMetadata.totalSupply, + name: [byteArray.byteArrayFromString(tokenMetadata.name)], + symbol: [byteArray.byteArrayFromString(tokenMetadata.symbol)], + hook: getChecksumAddress(0), + interchain_security_module: ismAddress, + owner: this.signer.address, //TODO: use config.owner, and in warp init ask for starknet owner + }); + addresses[chain] = tokenAddress; + break; + } + case TokenType.native: { + const tokenAddress = await this.deployer.deployContract('HypNative', { + mailbox: mailbox, + hook: getChecksumAddress(0), + interchain_security_module: ismAddress, + owner: this.signer.address, //TODO: use config.owner, and in warp init ask for starknet owner + }); + addresses[chain] = tokenAddress; + break; + } + default: + throw Error('Token type is not supported on starknet'); + } } + return addresses; } async getStarknetDeploymentISMAddress({ From fc1188a2f61e06d5ec64921948e7fe5818a9e5f3 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 22 Nov 2024 14:21:00 +0100 Subject: [PATCH 060/132] docs(changeset): Added `isPrivateKeyEvm` function for validating EVM private keys --- .changeset/chilly-balloons-rule.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilly-balloons-rule.md diff --git a/.changeset/chilly-balloons-rule.md b/.changeset/chilly-balloons-rule.md new file mode 100644 index 00000000000..b339b75699b --- /dev/null +++ b/.changeset/chilly-balloons-rule.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/utils': minor +--- + +Added `isPrivateKeyEvm` function for validating EVM private keys From 366aab58c03f01112f5bdaafe45b851d1e2ee4be Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 22 Nov 2024 16:05:58 +0100 Subject: [PATCH 061/132] refactor: simplify address validation and reorganize chain utils --- typescript/cli/src/config/strategy.ts | 19 +++-------- typescript/cli/src/config/warp.ts | 3 +- typescript/cli/src/context/context.ts | 3 +- .../strategies/chain/ChainResolverFactory.ts | 2 -- .../strategies/chain/MultiChainResolver.ts | 2 +- .../signer/BaseMultiProtocolSigner.ts | 3 +- .../signer/MultiProtocolSignerManager.ts | 2 +- typescript/cli/src/utils/chains.ts | 33 +++++++++++++++++++ typescript/cli/src/utils/output.ts | 33 ------------------- 9 files changed, 44 insertions(+), 56 deletions(-) diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index ce9fd9b41de..d9cee7101d1 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -130,13 +130,8 @@ export async function createStrategyConfig({ case TxSubmitterType.IMPERSONATED_ACCOUNT: submitter.userAddress = await input({ message: 'Enter the user address to impersonate', - validate: (address) => { - try { - return isAddress(address) ? true : 'Invalid Ethereum address'; - } catch { - return 'Invalid Ethereum address'; - } - }, + validate: (address) => + isAddress(address) ? true : 'Invalid Ethereum address', }); assert( submitter.userAddress, @@ -148,13 +143,8 @@ export async function createStrategyConfig({ case TxSubmitterType.GNOSIS_TX_BUILDER: submitter.safeAddress = await input({ message: 'Enter the Safe address', - validate: (address) => { - try { - return isAddress(address) ? true : 'Invalid Safe address'; - } catch { - return 'Invalid Safe address'; - } - }, + validate: (address) => + isAddress(address) ? true : 'Invalid Safe address', }); submitter.chain = chain; @@ -188,6 +178,7 @@ export async function createStrategyConfig({ writeYamlOrJson(outPath, strategyConfig); logGreen('✅ Successfully created a new strategy configuration.'); } catch (e) { + // don't log error since it may contain sensitive data errorRed( `The strategy configuration is invalid. Please review the submitter settings.`, ); diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index b75058b4f24..a2cd19d57ca 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -145,8 +145,7 @@ export async function createWarpRouteDeployConfig({ { key: context.key }, ); - const multiProviderWithSigners = - await multiProtocolSigner.setupMultiProvider(); + const multiProviderWithSigners = await multiProtocolSigner.getMultiProvider(); const result: WarpRouteDeployConfig = {}; let typeChoices = TYPE_CHOICES; diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index f679b69be0e..6f46be30619 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -89,7 +89,8 @@ export async function signerMiddleware(argv: Record) { /** * @notice Attaches signers to MultiProvider and assigns it to argv.multiProvider */ - argv.multiProvider = await multiProtocolSigner.setupMultiProvider(); + argv.multiProvider = await multiProtocolSigner.getMultiProvider(); + argv.multiProtocolSigner = multiProtocolSigner; return argv; } diff --git a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts index 4b14d77d992..24260c19823 100644 --- a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts +++ b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts @@ -3,7 +3,6 @@ import { SingleChainResolver } from './SingleChainResolver.js'; import { ChainResolver } from './types.js'; enum CommandType { - CORE_APPLY = 'core:apply', WARP_DEPLOY = 'warp:deploy', WARP_SEND = 'warp:send', WARP_APPLY = 'warp:apply', @@ -21,7 +20,6 @@ enum CommandType { */ export class ChainResolverFactory { private static strategyMap: Map ChainResolver> = new Map([ - [CommandType.CORE_APPLY, () => new SingleChainResolver()], [CommandType.WARP_DEPLOY, () => MultiChainResolver.forWarpRouteConfig()], [CommandType.WARP_SEND, () => MultiChainResolver.forOriginDestination()], [CommandType.WARP_APPLY, () => MultiChainResolver.forWarpRouteConfig()], diff --git a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts index 87e74752f33..ec0d6e3e477 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -5,6 +5,7 @@ import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/opt import { readChainSubmissionStrategyConfig } from '../../../config/strategy.js'; import { logRed } from '../../../logger.js'; import { + extractChainsFromObj, runMultiChainSelectionStep, runSingleChainSelectionStep, } from '../../../utils/chains.js'; @@ -14,7 +15,6 @@ import { runFileSelectionStep, } from '../../../utils/files.js'; import { getWarpCoreConfigOrExit } from '../../../utils/input.js'; -import { extractChainsFromObj } from '../../../utils/output.js'; import { ChainResolver } from './types.js'; diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts index 4aadcdf81f1..a48d1ef89fc 100644 --- a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts +++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts @@ -1,7 +1,6 @@ import { Signer } from 'ethers'; -import { ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; -import { ChainName } from '@hyperlane-xyz/sdk'; +import { ChainName, ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; export interface SignerConfig { privateKey: string; diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts index 3a8605fe741..12f9c0f8192 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -60,7 +60,7 @@ export class MultiProtocolSignerManager { /** * @dev Configures signers for EVM chains in MultiProvider */ - async setupMultiProvider(): Promise { + async getMultiProvider(): Promise { for (const chain of this.chains) { const signer = await this.initSigner(chain); this.multiProvider.setSigner(chain, signer); diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts index add11203d0c..7e2eaccd0a5 100644 --- a/typescript/cli/src/utils/chains.ts +++ b/typescript/cli/src/utils/chains.ts @@ -171,3 +171,36 @@ function handleNewChain(chainNames: string[]) { process.exit(0); } } + +/** + * @notice Extracts chain names from a nested configuration object + * @param config Object to search for chain names + * @return Array of discovered chain names + */ +export function extractChainsFromObj(config: Record): string[] { + const chains: string[] = []; + + // Recursively search for chain/chainName fields + function findChainFields(obj: any) { + if (obj === null || typeof obj !== 'object') return; + + if (Array.isArray(obj)) { + obj.forEach((item) => findChainFields(item)); + return; + } + + if ('chain' in obj) { + chains.push(obj.chain); + } + + if ('chainName' in obj) { + chains.push(obj.chainName); + } + + // Recursively search in all nested values + Object.values(obj).forEach((value) => findChainFields(value)); + } + + findChainFields(config); + return chains; +} diff --git a/typescript/cli/src/utils/output.ts b/typescript/cli/src/utils/output.ts index bcb794c6808..2e1acfdf41c 100644 --- a/typescript/cli/src/utils/output.ts +++ b/typescript/cli/src/utils/output.ts @@ -101,36 +101,3 @@ export function maskSensitiveData(obj: any): any { return obj; } - -/** - * @notice Extracts chain names from a nested configuration object - * @param config Object to search for chain names - * @return Array of discovered chain names - */ -export function extractChainsFromObj(config: Record): string[] { - const chains: string[] = []; - - // Recursively search for chain/chainName fields - function findChainFields(obj: any) { - if (obj === null || typeof obj !== 'object') return; - - if (Array.isArray(obj)) { - obj.forEach((item) => findChainFields(item)); - return; - } - - if ('chain' in obj) { - chains.push(obj.chain); - } - - if ('chainName' in obj) { - chains.push(obj.chainName); - } - - // Recursively search in all nested values - Object.values(obj).forEach((value) => findChainFields(value)); - } - - findChainFields(config); - return chains; -} From 0b793c7e89ce5dec75629246803b4a50607a03fb Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 22 Nov 2024 16:13:46 +0100 Subject: [PATCH 062/132] chore: change cli changeset to minor --- .changeset/spicy-gifts-hear.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/spicy-gifts-hear.md b/.changeset/spicy-gifts-hear.md index c157be1cfd7..37d4efa28da 100644 --- a/.changeset/spicy-gifts-hear.md +++ b/.changeset/spicy-gifts-hear.md @@ -1,5 +1,5 @@ --- -'@hyperlane-xyz/cli': major +'@hyperlane-xyz/cli': minor --- Added strategy management CLI commands and MultiProtocolSigner implementation for flexible cross-chain signer configuration and management From 24b0c553d68ae04700b5d0050e66ce619b4c797a Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 25 Nov 2024 10:46:42 +0100 Subject: [PATCH 063/132] refactor: update SignerConfig address type to use Address from hyperlane-utils --- .../src/context/strategies/signer/BaseMultiProtocolSigner.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts index a48d1ef89fc..b91242b42df 100644 --- a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts +++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts @@ -1,10 +1,11 @@ import { Signer } from 'ethers'; import { ChainName, ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; export interface SignerConfig { privateKey: string; - address?: string; // For chains like StarkNet that require address + address?: Address; // For chains like StarkNet that require address extraParams?: Record; // For any additional chain-specific params } From 2076ef6b31ee50bf7d7a77cfe4761d47c5fd636d Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 25 Nov 2024 11:31:28 +0100 Subject: [PATCH 064/132] refactor: move CommandType enum to signCommands.ts --- typescript/cli/src/commands/signCommands.ts | 12 ++++++++++++ .../strategies/chain/ChainResolverFactory.ts | 14 ++------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 58afa1c04f1..6bd617302b7 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -16,3 +16,15 @@ export function isSignCommand(argv: any): boolean { (argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1])) ); } + +export enum CommandType { + WARP_DEPLOY = 'warp:deploy', + WARP_SEND = 'warp:send', + WARP_APPLY = 'warp:apply', + WARP_READ = 'warp:read', + SEND_MESSAGE = 'send:message', + AGENT_KURTOSIS = 'deploy:kurtosis-agents', + STATUS = 'status:', + SUBMIT = 'submit:', + RELAYER = 'relayer:', +} diff --git a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts index 24260c19823..eb9fa135aa2 100644 --- a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts +++ b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts @@ -1,19 +1,9 @@ +import { CommandType } from '../../../commands/signCommands.js'; + import { MultiChainResolver } from './MultiChainResolver.js'; import { SingleChainResolver } from './SingleChainResolver.js'; import { ChainResolver } from './types.js'; -enum CommandType { - WARP_DEPLOY = 'warp:deploy', - WARP_SEND = 'warp:send', - WARP_APPLY = 'warp:apply', - WARP_READ = 'warp:read', - SEND_MESSAGE = 'send:message', - AGENT_KURTOSIS = 'deploy:kurtosis-agents', - STATUS = 'status:', - SUBMIT = 'submit:', - RELAYER = 'relayer:', -} - /** * @class ChainResolverFactory * @description Intercepts commands to determine the appropriate chain resolver strategy based on command type. From e61baff3fc3d9235a99b3b4d3a443a316bfc2b07 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 25 Nov 2024 17:53:54 +0100 Subject: [PATCH 065/132] fix: passed key used for setting signer --- typescript/cli/src/context/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 6f46be30619..0d1084fcc6d 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -60,7 +60,7 @@ export async function contextMiddleware(argv: Record) { export async function signerMiddleware(argv: Record) { const { key, requiresKey, multiProvider, strategyPath } = argv.context; - if (!requiresKey) return argv; + if (!requiresKey && !key) return argv; const strategyConfig = await safeReadChainSubmissionStrategyConfig( strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, From 9e8a2aa0a85f721bda14a0d670a869f2da2d40bd Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:08:16 +0330 Subject: [PATCH 066/132] feat: support starknet signer by implementing starknet signer strategy --- typescript/cli/src/config/warp.ts | 2 + typescript/cli/src/context/context.ts | 5 +- .../signer/BaseMultiProtocolSigner.ts | 7 +- .../signer/MultiProtocolSignerFactory.ts | 40 +++++- .../signer/MultiProtocolSignerManager.ts | 126 +++++++++++++----- 5 files changed, 141 insertions(+), 39 deletions(-) diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index a2cd19d57ca..f9bbc2c01f5 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -7,6 +7,7 @@ import { IsmConfig, IsmType, MailboxClientConfig, + MultiProtocolProvider, TokenType, WarpCoreConfig, WarpCoreConfigSchema, @@ -142,6 +143,7 @@ export async function createWarpRouteDeployConfig({ strategyConfig, warpChains, context.multiProvider, + new MultiProtocolProvider(context.chainMetadata), { key: context.key }, ); diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 6f46be30619..f267f03c904 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -12,6 +12,7 @@ import { ChainMap, ChainMetadata, ChainName, + MultiProtocolProvider, MultiProvider, } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; @@ -58,7 +59,8 @@ export async function contextMiddleware(argv: Record) { } export async function signerMiddleware(argv: Record) { - const { key, requiresKey, multiProvider, strategyPath } = argv.context; + const { key, requiresKey, multiProvider, strategyPath, chainMetadata } = + argv.context; if (!requiresKey) return argv; @@ -83,6 +85,7 @@ export async function signerMiddleware(argv: Record) { strategyConfig, chains, multiProvider, + new MultiProtocolProvider(chainMetadata), { key }, ); diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts index b91242b42df..ad122fb67b4 100644 --- a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts +++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts @@ -1,8 +1,11 @@ import { Signer } from 'ethers'; +import { Account as StarknetAccount } from 'starknet'; import { ChainName, ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; +export type TypedSigner = Signer | StarknetAccount; + export interface SignerConfig { privateKey: string; address?: Address; // For chains like StarkNet that require address @@ -11,12 +14,12 @@ export interface SignerConfig { export interface IMultiProtocolSigner { getSignerConfig(chain: ChainName): Promise | SignerConfig; - getSigner(config: SignerConfig): Signer; + getSigner(config: SignerConfig): TypedSigner; } export abstract class BaseMultiProtocolSigner implements IMultiProtocolSigner { constructor(protected config: ChainSubmissionStrategy) {} abstract getSignerConfig(chain: ChainName): Promise; - abstract getSigner(config: SignerConfig): Signer; + abstract getSigner(config: SignerConfig): TypedSigner; } diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index 030f11b5f4e..f10b82b663c 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -1,5 +1,6 @@ import { password } from '@inquirer/prompts'; import { Signer, Wallet } from 'ethers'; +import { Account as StarknetAccount } from 'starknet'; import { ChainName, @@ -8,7 +9,7 @@ import { MultiProvider, TxSubmitterType, } from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert } from '@hyperlane-xyz/utils'; import { BaseMultiProtocolSigner, @@ -29,6 +30,8 @@ export class MultiProtocolSignerFactory { if (technicalStack === ChainTechnicalStack.ZkSync) return new ZKSyncSignerStrategy(strategyConfig); return new EthereumSignerStrategy(strategyConfig); + case ProtocolType.Starknet: + return new StarknetSignerStrategy(strategyConfig); default: throw new Error(`Unsupported protocol: ${protocol}`); } @@ -77,3 +80,38 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { return new Wallet(config.privateKey); } } + +class StarknetSignerStrategy extends BaseMultiProtocolSigner { + async getSignerConfig(chain: ChainName): Promise { + const submitter = this.config[chain]?.submitter as { + privateKey?: string; + address?: string; + }; + + const privateKey = + submitter?.privateKey ?? + (await password({ + message: `Please enter the private key for chain ${chain}`, + })); + + const address = + submitter?.address ?? + (await password({ + message: `Please enter the signer address for chain ${chain}`, + })); + + return { privateKey, address }; + } + + getSigner({ + privateKey, + address, + extraParams, + }: SignerConfig): StarknetAccount { + assert( + address && extraParams?.provider, + 'Missing StarknetAccount arguments', + ); + return new StarknetAccount(extraParams.provider, address, privateKey); + } +} diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts index 12f9c0f8192..1d4af9ed57e 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -4,13 +4,18 @@ import { Logger } from 'pino'; import { ChainName, ChainSubmissionStrategy, + MultiProtocolProvider, MultiProvider, } from '@hyperlane-xyz/sdk'; -import { assert, rootLogger } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert, rootLogger } from '@hyperlane-xyz/utils'; import { ENV } from '../../../utils/env.js'; -import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js'; +import { + IMultiProtocolSigner, + SignerConfig, + TypedSigner, +} from './BaseMultiProtocolSigner.js'; import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js'; export interface MultiProtocolSignerOptions { @@ -24,13 +29,14 @@ export interface MultiProtocolSignerOptions { */ export class MultiProtocolSignerManager { protected readonly signerStrategies: Map; - protected readonly signers: Map; + protected readonly signers: Map; public readonly logger: Logger; constructor( protected readonly submissionStrategy: ChainSubmissionStrategy, protected readonly chains: ChainName[], protected readonly multiProvider: MultiProvider, + private multiProtocolProvider: MultiProtocolProvider, protected readonly options: MultiProtocolSignerOptions = {}, ) { this.logger = @@ -62,8 +68,15 @@ export class MultiProtocolSignerManager { */ async getMultiProvider(): Promise { for (const chain of this.chains) { - const signer = await this.initSigner(chain); - this.multiProvider.setSigner(chain, signer); + // multiProvider is only compatible with evm chains + if ( + this.multiProvider.getChainMetadata(chain).protocol === + ProtocolType.Ethereum + ) { + const signer = await this.initSigner(chain); + if (signer instanceof Signer) + this.multiProvider.setSigner(chain, signer); + } } return this.multiProvider; @@ -72,13 +85,10 @@ export class MultiProtocolSignerManager { /** * @notice Creates signer for specific chain */ - async initSigner(chain: ChainName): Promise { - const { privateKey } = await this.resolveConfig(chain); - - const signerStrategy = this.signerStrategies.get(chain); - assert(signerStrategy, `No signer strategy found for chain ${chain}`); - - return signerStrategy.getSigner({ privateKey }); + async initSigner(chain: ChainName): Promise { + const config = await this.resolveConfig(chain); + const signerStrategy = this.getSignerStrategyOrFail(chain); + return signerStrategy.getSigner(config); } /** @@ -87,10 +97,24 @@ export class MultiProtocolSignerManager { async initAllSigners(): Promise { const signerConfigs = await this.resolveAllConfigs(); - for (const { chain, privateKey } of signerConfigs) { + for (const { chain, privateKey, address } of signerConfigs) { const signerStrategy = this.signerStrategies.get(chain); if (signerStrategy) { - this.signers.set(chain, signerStrategy.getSigner({ privateKey })); + const { protocol } = this.multiProvider.getChainMetadata(chain); + if (protocol === ProtocolType.Starknet) { + const provider = + this.multiProtocolProvider?.getStarknetProvider(chain); + this.signers.set( + chain, + signerStrategy.getSigner({ + privateKey, + address, + extraParams: { provider }, + }), + ); + } else { + this.signers.set(chain, signerStrategy.getSigner({ privateKey })); + } } } @@ -101,7 +125,7 @@ export class MultiProtocolSignerManager { * @notice Resolves all chain configurations */ private async resolveAllConfigs(): Promise< - Array<{ chain: ChainName; privateKey: string }> + Array<{ chain: ChainName } & SignerConfig> > { return Promise.all(this.chains.map((chain) => this.resolveConfig(chain))); } @@ -111,43 +135,75 @@ export class MultiProtocolSignerManager { */ private async resolveConfig( chain: ChainName, - ): Promise<{ chain: ChainName; privateKey: string }> { - const signerStrategy = this.signerStrategies.get(chain); - assert(signerStrategy, `No signer strategy found for chain ${chain}`); + ): Promise<{ chain: ChainName } & SignerConfig> { + const { protocol } = this.multiProvider.getChainMetadata(chain); - let privateKey: string; + // For Starknet, we must use strategy config + if (protocol === ProtocolType.Starknet) { + return this.resolveStarknetConfig(chain); + } + + // For other protocols, try CLI/ENV keys first, then fallback to strategy + const config = await this.extractPrivateKey(chain); + return { chain, ...config }; + } + /** + * @notice Gets private key from strategy + */ + private async extractPrivateKey(chain: ChainName): Promise { if (this.options.key) { this.logger.info( `Using private key passed via CLI --key flag for chain ${chain}`, ); - privateKey = this.options.key; - } else if (ENV.HYP_KEY) { + return { privateKey: this.options.key }; + } + + if (ENV.HYP_KEY) { this.logger.info(`Using private key from .env for chain ${chain}`); - privateKey = ENV.HYP_KEY; - } else { - privateKey = await this.extractPrivateKey(chain, signerStrategy); + return { privateKey: ENV.HYP_KEY }; } - return { chain, privateKey }; + const signerStrategy = this.getSignerStrategyOrFail(chain); + const strategyConfig = await signerStrategy.getSignerConfig(chain); + assert( + strategyConfig.privateKey, + `No private key found for chain ${chain}`, + ); + this.logger.info( + `Extracting private key from strategy config/user prompt for chain ${chain}`, + ); + + return { privateKey: strategyConfig.privateKey }; } - /** - * @notice Gets private key from strategy - */ - private async extractPrivateKey( + private async resolveStarknetConfig( chain: ChainName, - signerStrategy: IMultiProtocolSigner, - ): Promise { + ): Promise<{ chain: ChainName } & SignerConfig> { + const signerStrategy = this.getSignerStrategyOrFail(chain); const strategyConfig = await signerStrategy.getSignerConfig(chain); + const provider = this.multiProtocolProvider.getStarknetProvider(chain); + assert( strategyConfig.privateKey, `No private key found for chain ${chain}`, ); + assert(strategyConfig.address, 'No Starknet Address found'); + assert(provider, 'No Starknet Provider found'); - this.logger.info( - `Extracting private key from strategy config/user prompt for chain ${chain}`, - ); - return strategyConfig.privateKey; + this.logger.info(`Using strategy config for Starknet chain ${chain}`); + + return { + chain, + privateKey: strategyConfig.privateKey, + address: strategyConfig.address, + extraParams: { provider }, + }; + } + + private getSignerStrategyOrFail(chain: ChainName): IMultiProtocolSigner { + const strategy = this.signerStrategies.get(chain); + assert(strategy, `No signer strategy found for chain ${chain}`); + return strategy; } } From 2d08d1ec6576370fe8885562b8c89c9da89381ec Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:19:51 +0330 Subject: [PATCH 067/132] Merge remote-tracking branch 'origin/main' into feat/key-command --- .changeset/bright-students-exist.md | 5 + .changeset/empty-dodos-clap.md | 6 + .changeset/funny-elephants-pretend.md | 5 - .changeset/mighty-panthers-play.md | 5 - .changeset/nice-elephants-heal.md | 5 - .changeset/odd-frogs-move.md | 5 - .changeset/polite-kings-warn.md | 5 - .changeset/rare-llamas-float.md | 7 - .changeset/real-phones-bake.md | 7 - .changeset/shaggy-countries-kick.md | 5 - .changeset/spotty-pumpkins-buy.md | 5 + .changeset/strange-poems-build.md | 6 - .changeset/yellow-baboons-kneel.md | 5 + .codespell/ignore.txt | 1 + .eslintignore | 4 - .eslintrc | 65 - .github/workflows/test.yml | 36 +- .registryrc | 2 +- .syncpackrc | 3 + eslint.config.mjs | 115 + package.json | 13 +- rust/main/Cargo.lock | 53 +- rust/main/Cargo.toml | 36 +- rust/main/agents/relayer/src/relayer.rs | 3 + rust/main/agents/scraper/src/agent.rs | 3 + rust/main/agents/validator/src/validator.rs | 1 + .../hyperlane-sealevel/src/interchain_gas.rs | 46 +- .../chains/hyperlane-sealevel/src/mailbox.rs | 82 +- .../hyperlane-sealevel/src/rpc/client.rs | 7 +- .../src/rpc/client/tests.rs | 14 + rust/main/config/mainnet_config.json | 406 +- rust/main/hyperlane-base/src/settings/base.rs | 15 +- .../hyperlane-base/src/settings/chains.rs | 72 +- .../hyperlane-base/src/settings/signers.rs | 7 +- rust/rust-toolchain | 3 - rust/sealevel/Cargo.lock | 121 +- rust/sealevel/Cargo.toml | 35 +- rust/sealevel/client/src/cmd_utils.rs | 21 +- rust/sealevel/client/src/core.rs | 22 +- .../apxETH-eclipse-ethereum/program-ids.json | 10 + .../apxETH-eclipse-ethereum/token-config.json | 17 + .../ezSOL-eclipse-solana/program-ids.json | 10 + .../ezSOL-eclipse-solana/token-config.json | 17 + rust/sealevel/libraries/test-utils/src/igp.rs | 2 +- rust/sealevel/rust-toolchain | 2 +- solidity/CHANGELOG.md | 7 + solidity/contracts/PackageVersioned.sol | 2 +- solidity/package.json | 8 +- typescript/ccip-server/.eslintrc | 6 - typescript/ccip-server/CHANGELOG.md | 2 + typescript/ccip-server/eslint.config.mjs | 17 + typescript/ccip-server/package.json | 11 +- typescript/ccip-server/src/server.ts | 14 +- .../src/services/LightClientService.ts | 4 +- .../ccip-server/src/services/ProofsService.ts | 8 +- typescript/ccip-server/tsconfig.json | 9 + typescript/cli/.eslintignore | 2 - typescript/cli/.eslintrc | 6 - typescript/cli/.mocharc-e2e.json | 1 - typescript/cli/CHANGELOG.md | 18 + typescript/cli/eslint.config.mjs | 20 + typescript/cli/package.json | 23 +- typescript/cli/scripts/run-e2e-test.sh | 7 +- typescript/cli/src/avs/check.ts | 2 +- typescript/cli/src/check/warp.ts | 1 - typescript/cli/src/commands/relayer.ts | 2 +- typescript/cli/src/commands/warp.ts | 2 +- typescript/cli/src/config/agent.ts | 2 +- typescript/cli/src/config/submit.ts | 9 +- typescript/cli/src/deploy/core.ts | 2 +- typescript/cli/src/deploy/dry-run.ts | 3 +- typescript/cli/src/deploy/utils.ts | 4 - typescript/cli/src/deploy/warp.ts | 32 +- typescript/cli/src/read/warp.ts | 2 +- typescript/cli/src/status/message.ts | 2 +- typescript/cli/src/utils/balances.ts | 8 +- typescript/cli/src/utils/env.ts | 2 +- typescript/cli/src/utils/files.ts | 6 +- typescript/cli/src/utils/input.ts | 50 +- typescript/cli/src/utils/warp.ts | 35 + .../cli/src/validator/preFlightCheck.ts | 4 +- typescript/cli/src/version.ts | 2 +- typescript/github-proxy/CHANGELOG.md | 2 + typescript/github-proxy/package.json | 4 +- typescript/helloworld/.eslintignore | 5 - typescript/helloworld/.eslintrc | 39 - typescript/helloworld/CHANGELOG.md | 13 + typescript/helloworld/eslint.config.mjs | 17 + typescript/helloworld/package.json | 23 +- typescript/infra/CHANGELOG.md | 19 + .../config/environments/mainnet3/agent.ts | 50 +- .../mainnet3/aw-validators/hyperlane.json | 18 + .../mainnet3/core/verification.json | 420 + .../config/environments/mainnet3/funding.ts | 9 +- .../environments/mainnet3/gasPrices.json | 64 +- .../mainnet3/ism/verification.json | 516 + .../middleware/accounts/verification.json | 126 + .../misc-artifacts/aave-sender-addresses.json | 38 + .../merkly-erc20-addresses.json | 0 .../merkly-eth-addresses.json | 0 .../merkly-nft-addresses.json | 0 .../velo-message-module-addresses.json | 14 + .../velo-token-bridge-addresses.json | 14 + .../mainnet3/supportedChainNames.ts | 6 + .../environments/mainnet3/tokenPrices.json | 174 +- .../environments/mainnet3/validators.ts | 62 + .../getAncient8EthereumUSDCWarpConfig.ts | 16 +- ...bitrumEthereumZircuitAmphrETHWarpConfig.ts | 27 +- .../getArbitrumNeutronEclipWarpConfig.ts | 20 +- .../getArbitrumNeutronTiaWarpConfig.ts | 14 +- .../getEclipseEthereumApxETHWarpConfig.ts | 46 + .../getEclipseEthereumSolanaUSDCWarpConfig.ts | 3 +- .../getEclipseEthereumSolanaUSDTWarpConfig.ts | 15 +- .../getEclipseEthereumTETHWarpConfig.ts | 4 +- .../getEclipseEthereumWBTCWarpConfig.ts | 15 +- .../getEclipseEthereumWeETHsWarpConfig.ts | 28 +- .../getEclipseStrideSTTIAWarpConfig.ts | 16 +- .../getEclipseStrideTIAWarpConfig.ts | 16 +- .../getEthereumBscLumiaLUMIAWarpConfig.ts | 51 +- .../getEthereumFlowCbBTCWarpConfig.ts | 47 + .../getEthereumInevmUSDCWarpConfig.ts | 12 +- .../getEthereumInevmUSDTWarpConfig.ts | 12 +- .../getEthereumSeiFastUSDWarpConfig.ts | 22 +- .../getEthereumVictionETHWarpConfig.ts | 9 +- .../getEthereumVictionUSDCWarpConfig.ts | 12 +- .../getEthereumVictionUSDTWarpConfig.ts | 12 +- .../getInevmInjectiveINJWarpConfig.ts | 11 +- .../getMantapacificNeutronTiaWarpConfig.ts | 8 +- .../environments/mainnet3/warp/consts.ts | 2 + .../environments/mainnet3/warp/warpIds.ts | 4 + typescript/infra/config/warp.ts | 44 +- .../helm/warp-routes/templates/_helpers.tpl | 2 +- typescript/infra/package.json | 28 +- typescript/infra/scripts/agent-utils.ts | 41 +- typescript/infra/scripts/agents/utils.ts | 10 +- typescript/infra/scripts/check/check-utils.ts | 7 +- .../infra/scripts/check/check-warp-deploy.ts | 7 + .../funding/fund-keys-from-deployer.ts | 6 +- .../infra/scripts/{ => keys}/create-keys.ts | 5 +- .../infra/scripts/{ => keys}/delete-keys.ts | 5 +- .../scripts/{ => keys}/get-key-addresses.ts | 7 +- typescript/infra/scripts/keys/get-key.ts | 38 + .../infra/scripts/{ => keys}/get-owner-ica.ts | 9 +- .../infra/scripts/{ => keys}/rotate-key.ts | 2 +- .../infra/scripts/{ => keys}/update-key.ts | 2 +- .../infra/scripts/safes/get-pending-txs.ts | 202 + typescript/infra/scripts/safes/parse-txs.ts | 68 + .../warp-routes/deploy-warp-monitor.ts | 17 +- .../warp-routes/generate-warp-config.ts | 2 +- .../scripts/warp-routes/monitor/metrics.ts | 22 +- .../monitor/monitor-warp-route-balances.ts | 17 +- typescript/infra/src/config/agent/relayer.ts | 12 + typescript/infra/src/config/environment.ts | 10 + typescript/infra/src/config/warp.ts | 15 +- .../infra/src/tx/govern-transaction-reader.ts | 531 + typescript/infra/src/utils/safe.ts | 81 +- typescript/infra/src/warp/helm.ts | 2 +- typescript/infra/test/warpIds.test.ts | 6 +- typescript/sdk/.eslintrc | 7 - typescript/sdk/CHANGELOG.md | 17 + typescript/sdk/eslint.config.mjs | 25 + typescript/sdk/package.json | 27 +- typescript/sdk/src/consts/.eslintrc | 5 - typescript/sdk/src/consts/multisigIsm.ts | 30 + .../sdk/src/consts/multisigIsmVerifyCosts.ts | 1 - .../sdk/src/core/CoreDeployer.hardhat-test.ts | 4 +- typescript/sdk/src/core/EvmCoreModule.ts | 5 +- typescript/sdk/src/core/HyperlaneRelayer.ts | 8 +- typescript/sdk/src/core/schemas.ts | 4 +- .../sdk/src/deploy/HyperlaneDeployer.ts | 2 +- typescript/sdk/src/deploy/verify/.eslintrc | 5 - .../sdk/src/deploy/verify/ContractVerifier.ts | 2 +- .../sdk/src/gas/adapters/serialization.ts | 1 - typescript/sdk/src/gas/types.ts | 2 +- .../src/hook/EvmHookModule.hardhat-test.ts | 1 - typescript/sdk/src/hook/EvmHookModule.ts | 2 +- typescript/sdk/src/hook/EvmHookReader.test.ts | 1 - typescript/sdk/src/hook/EvmHookReader.ts | 4 +- typescript/sdk/src/hook/schemas.ts | 96 - typescript/sdk/src/hook/types.ts | 97 +- typescript/sdk/src/index.ts | 15 +- .../sdk/src/ism/EvmIsmModule.hardhat-test.ts | 1 - typescript/sdk/src/ism/EvmIsmModule.ts | 2 +- typescript/sdk/src/ism/EvmIsmReader.ts | 8 +- typescript/sdk/src/ism/HyperlaneIsmFactory.ts | 2 +- .../sdk/src/ism/metadata/aggregation.ts | 7 +- .../ism/metadata/arbL2ToL1.hardhat-test.ts | 10 +- typescript/sdk/src/ism/metadata/arbL2ToL1.ts | 2 +- .../src/ism/metadata/builder.hardhat-test.ts | 6 +- typescript/sdk/src/ism/metadata/builder.ts | 86 +- typescript/sdk/src/ism/metadata/decode.ts | 41 + typescript/sdk/src/ism/metadata/multisig.ts | 2 +- typescript/sdk/src/ism/metadata/null.ts | 2 +- typescript/sdk/src/ism/metadata/routing.ts | 9 +- typescript/sdk/src/ism/metadata/types.ts | 32 + typescript/sdk/src/ism/schemas.ts | 103 - .../ism/{schemas.test.ts => types.test.ts} | 3 +- typescript/sdk/src/ism/types.ts | 110 +- typescript/sdk/src/ism/utils.ts | 2 +- .../src/metadata/chainMetadataConversion.ts | 98 + .../src/middleware/liquidity-layer/.eslintrc | 5 - .../liquidity-layer/LiquidityLayerApp.ts | 27 +- .../providers/SmartProvider/SmartProvider.ts | 4 +- .../submitter/builder/TxSubmitterBuilder.ts | 3 +- typescript/sdk/src/router/schemas.ts | 45 - typescript/sdk/src/router/types.ts | 49 +- .../token/EvmERC20WarpModule.hardhat-test.ts | 19 +- .../sdk/src/token/EvmERC20WarpModule.ts | 85 +- .../sdk/src/token/EvmERC20WarpRouteReader.ts | 2 +- typescript/sdk/src/token/Token.ts | 6 +- .../adapters/CosmWasmTokenAdapter.test.ts | 2 - .../sdk/src/token/adapters/EvmTokenAdapter.ts | 19 +- typescript/sdk/src/token/checker.ts | 2 +- typescript/sdk/src/token/deploy.ts | 1 - typescript/sdk/src/token/schemas.ts | 2 +- typescript/sdk/src/utils/.eslintrc | 5 - typescript/sdk/src/utils/gnosisSafe.js | 2 +- typescript/sdk/src/utils/ism.ts | 2 +- typescript/sdk/src/utils/logUtils.ts | 2 +- .../sdk/src/utils/sealevelSerialization.ts | 1 - typescript/sdk/src/utils/viem.ts | 29 - typescript/utils/CHANGELOG.md | 6 + typescript/utils/eslint.config.mjs | 3 + typescript/utils/package.json | 12 +- typescript/utils/src/addresses.ts | 12 +- typescript/utils/src/amount.ts | 2 +- typescript/utils/src/base64.ts | 4 +- typescript/utils/src/big-numbers.ts | 4 +- typescript/utils/src/env.ts | 2 +- typescript/utils/src/index.ts | 2 + typescript/utils/src/strings.ts | 4 + typescript/utils/src/url.ts | 4 +- typescript/widgets/.eslintignore | 6 - typescript/widgets/.eslintrc | 16 - typescript/widgets/.storybook/main.ts | 5 + typescript/widgets/CHANGELOG.md | 25 + typescript/widgets/eslint.config.mjs | 40 + typescript/widgets/mg.eslint.config.mjs | 38 + typescript/widgets/package.json | 39 +- .../widgets/src/chains/ChainAddMenu.tsx | 5 +- .../widgets/src/chains/ChainDetailsMenu.tsx | 2 +- typescript/widgets/src/chains/ChainLogo.tsx | 5 +- .../widgets/src/chains/ChainSearchMenu.tsx | 14 +- typescript/widgets/src/components/Button.tsx | 2 +- .../widgets/src/components/ErrorBoundary.tsx | 3 +- .../widgets/src/components/IconButton.tsx | 2 +- .../widgets/src/components/LinkButton.tsx | 2 +- .../widgets/src/components/SearchMenu.tsx | 11 +- .../widgets/src/components/TextInput.tsx | 4 +- typescript/widgets/src/icons/Web.tsx | 4 +- typescript/widgets/src/index.ts | 66 +- .../widgets/src/layout/DropdownMenu.tsx | 2 +- typescript/widgets/src/layout/Modal.tsx | 2 +- typescript/widgets/src/layout/Popover.tsx | 2 +- typescript/widgets/src/logger.ts | 3 + typescript/widgets/src/logos/Cosmos.tsx | 40 + typescript/widgets/src/logos/Ethereum.tsx | 27 + typescript/widgets/src/logos/Solana.tsx | 63 + .../widgets/src/logos/WalletConnect.tsx | 33 + typescript/widgets/src/logos/protocols.ts | 16 + typescript/widgets/src/messages/useMessage.ts | 11 +- .../widgets/src/messages/useMessageStage.ts | 15 +- .../MultiProtocolWalletModal.stories.tsx | 124 + typescript/widgets/src/utils/clipboard.ts | 6 +- typescript/widgets/src/utils/explorers.ts | 6 +- .../src/utils/useChainConnectionTest.ts | 2 +- .../src/walletIntegrations/AccountList.tsx | 143 + .../ConnectWalletButton.tsx | 115 + .../MultiProtocolWalletModal.tsx | 86 + .../src/walletIntegrations/WalletLogo.tsx | 24 + .../widgets/src/walletIntegrations/cosmos.ts | 208 + .../src/walletIntegrations/ethereum.ts | 169 + .../src/walletIntegrations/multiProtocol.tsx | 256 + .../widgets/src/walletIntegrations/solana.ts | 138 + .../widgets/src/walletIntegrations/types.ts | 48 + .../widgets/src/walletIntegrations/utils.ts | 56 + typescript/widgets/tailwind.config.cjs | 1 + yarn.lock | 8625 +++++++++++++---- 278 files changed, 13111 insertions(+), 3556 deletions(-) create mode 100644 .changeset/bright-students-exist.md create mode 100644 .changeset/empty-dodos-clap.md delete mode 100644 .changeset/funny-elephants-pretend.md delete mode 100644 .changeset/mighty-panthers-play.md delete mode 100644 .changeset/nice-elephants-heal.md delete mode 100644 .changeset/odd-frogs-move.md delete mode 100644 .changeset/polite-kings-warn.md delete mode 100644 .changeset/rare-llamas-float.md delete mode 100644 .changeset/real-phones-bake.md delete mode 100644 .changeset/shaggy-countries-kick.md create mode 100644 .changeset/spotty-pumpkins-buy.md delete mode 100644 .changeset/strange-poems-build.md create mode 100644 .changeset/yellow-baboons-kneel.md delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 .syncpackrc create mode 100644 eslint.config.mjs create mode 100644 rust/main/chains/hyperlane-sealevel/src/rpc/client/tests.rs delete mode 100644 rust/rust-toolchain create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/apxETH-eclipse-ethereum/program-ids.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/apxETH-eclipse-ethereum/token-config.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/ezSOL-eclipse-solana/program-ids.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/ezSOL-eclipse-solana/token-config.json delete mode 100644 typescript/ccip-server/.eslintrc create mode 100644 typescript/ccip-server/eslint.config.mjs create mode 100644 typescript/ccip-server/tsconfig.json delete mode 100644 typescript/cli/.eslintignore delete mode 100644 typescript/cli/.eslintrc create mode 100644 typescript/cli/eslint.config.mjs create mode 100644 typescript/cli/src/utils/warp.ts delete mode 100644 typescript/helloworld/.eslintignore delete mode 100644 typescript/helloworld/.eslintrc create mode 100644 typescript/helloworld/eslint.config.mjs create mode 100644 typescript/infra/config/environments/mainnet3/misc-artifacts/aave-sender-addresses.json rename typescript/infra/config/environments/mainnet3/{warp/artifacts => misc-artifacts}/merkly-erc20-addresses.json (100%) rename typescript/infra/config/environments/mainnet3/{warp/artifacts => misc-artifacts}/merkly-eth-addresses.json (100%) rename typescript/infra/config/environments/mainnet3/{warp/artifacts => misc-artifacts}/merkly-nft-addresses.json (100%) create mode 100644 typescript/infra/config/environments/mainnet3/misc-artifacts/velo-message-module-addresses.json create mode 100644 typescript/infra/config/environments/mainnet3/misc-artifacts/velo-token-bridge-addresses.json create mode 100644 typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumApxETHWarpConfig.ts create mode 100644 typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumFlowCbBTCWarpConfig.ts create mode 100644 typescript/infra/config/environments/mainnet3/warp/consts.ts rename typescript/infra/scripts/{ => keys}/create-keys.ts (61%) rename typescript/infra/scripts/{ => keys}/delete-keys.ts (58%) rename typescript/infra/scripts/{ => keys}/get-key-addresses.ts (80%) create mode 100644 typescript/infra/scripts/keys/get-key.ts rename typescript/infra/scripts/{ => keys}/get-owner-ica.ts (92%) rename typescript/infra/scripts/{ => keys}/rotate-key.ts (97%) rename typescript/infra/scripts/{ => keys}/update-key.ts (97%) create mode 100644 typescript/infra/scripts/safes/get-pending-txs.ts create mode 100644 typescript/infra/scripts/safes/parse-txs.ts create mode 100644 typescript/infra/src/tx/govern-transaction-reader.ts delete mode 100644 typescript/sdk/.eslintrc create mode 100644 typescript/sdk/eslint.config.mjs delete mode 100644 typescript/sdk/src/consts/.eslintrc delete mode 100644 typescript/sdk/src/deploy/verify/.eslintrc delete mode 100644 typescript/sdk/src/hook/schemas.ts create mode 100644 typescript/sdk/src/ism/metadata/decode.ts create mode 100644 typescript/sdk/src/ism/metadata/types.ts delete mode 100644 typescript/sdk/src/ism/schemas.ts rename typescript/sdk/src/ism/{schemas.test.ts => types.test.ts} (85%) create mode 100644 typescript/sdk/src/metadata/chainMetadataConversion.ts delete mode 100644 typescript/sdk/src/middleware/liquidity-layer/.eslintrc delete mode 100644 typescript/sdk/src/router/schemas.ts delete mode 100644 typescript/sdk/src/utils/.eslintrc delete mode 100644 typescript/sdk/src/utils/viem.ts create mode 100644 typescript/utils/eslint.config.mjs delete mode 100644 typescript/widgets/.eslintignore delete mode 100644 typescript/widgets/.eslintrc create mode 100644 typescript/widgets/eslint.config.mjs create mode 100644 typescript/widgets/mg.eslint.config.mjs create mode 100644 typescript/widgets/src/logger.ts create mode 100644 typescript/widgets/src/logos/Cosmos.tsx create mode 100644 typescript/widgets/src/logos/Ethereum.tsx create mode 100644 typescript/widgets/src/logos/Solana.tsx create mode 100644 typescript/widgets/src/logos/WalletConnect.tsx create mode 100644 typescript/widgets/src/logos/protocols.ts create mode 100644 typescript/widgets/src/stories/MultiProtocolWalletModal.stories.tsx create mode 100644 typescript/widgets/src/walletIntegrations/AccountList.tsx create mode 100644 typescript/widgets/src/walletIntegrations/ConnectWalletButton.tsx create mode 100644 typescript/widgets/src/walletIntegrations/MultiProtocolWalletModal.tsx create mode 100644 typescript/widgets/src/walletIntegrations/WalletLogo.tsx create mode 100644 typescript/widgets/src/walletIntegrations/cosmos.ts create mode 100644 typescript/widgets/src/walletIntegrations/ethereum.ts create mode 100644 typescript/widgets/src/walletIntegrations/multiProtocol.tsx create mode 100644 typescript/widgets/src/walletIntegrations/solana.ts create mode 100644 typescript/widgets/src/walletIntegrations/types.ts create mode 100644 typescript/widgets/src/walletIntegrations/utils.ts diff --git a/.changeset/bright-students-exist.md b/.changeset/bright-students-exist.md new file mode 100644 index 00000000000..84833ecaea9 --- /dev/null +++ b/.changeset/bright-students-exist.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Support using the CLI to deploy warp routes that involve foreign deployments diff --git a/.changeset/empty-dodos-clap.md b/.changeset/empty-dodos-clap.md new file mode 100644 index 00000000000..cd2826f7183 --- /dev/null +++ b/.changeset/empty-dodos-clap.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Remove ismFactoryAddresses from warpConfig diff --git a/.changeset/funny-elephants-pretend.md b/.changeset/funny-elephants-pretend.md deleted file mode 100644 index 9de7ff40bee..00000000000 --- a/.changeset/funny-elephants-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Updated Fraxtal set to include Superlane validators, updated Flow set diff --git a/.changeset/mighty-panthers-play.md b/.changeset/mighty-panthers-play.md deleted file mode 100644 index ef054787bf2..00000000000 --- a/.changeset/mighty-panthers-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Enroll new validators for alephzeroevmmainnet, chilizmainnet, flowmainnet, immutablezkevmmainnet, metal, polynomialfi, rarichain, rootstockmainnet, superpositionmainnet, flame, prom, inevm. diff --git a/.changeset/nice-elephants-heal.md b/.changeset/nice-elephants-heal.md deleted file mode 100644 index d186955c49f..00000000000 --- a/.changeset/nice-elephants-heal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/utils': minor ---- - -Add an isRelativeUrl function diff --git a/.changeset/odd-frogs-move.md b/.changeset/odd-frogs-move.md deleted file mode 100644 index d99e99107bb..00000000000 --- a/.changeset/odd-frogs-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Added helpers to Token and token adapters to get bridged supply of tokens" diff --git a/.changeset/polite-kings-warn.md b/.changeset/polite-kings-warn.md deleted file mode 100644 index 0a32d1ca161..00000000000 --- a/.changeset/polite-kings-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/sdk': minor ---- - -Add a validateZodResult util function diff --git a/.changeset/rare-llamas-float.md b/.changeset/rare-llamas-float.md deleted file mode 100644 index 337cdd5ebcd..00000000000 --- a/.changeset/rare-llamas-float.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@hyperlane-xyz/widgets': minor ---- - -New Icons -Updated modal with new props -Updated storybook for modal and icon list diff --git a/.changeset/real-phones-bake.md b/.changeset/real-phones-bake.md deleted file mode 100644 index 589c4e012f6..00000000000 --- a/.changeset/real-phones-bake.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@hyperlane-xyz/infra': minor -'@hyperlane-xyz/cli': minor -'@hyperlane-xyz/sdk': minor ---- - -Implements persistent relayer for use in CLI diff --git a/.changeset/shaggy-countries-kick.md b/.changeset/shaggy-countries-kick.md deleted file mode 100644 index 3d9a740629d..00000000000 --- a/.changeset/shaggy-countries-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/widgets': minor ---- - -Add various utility hooks: useIsSsr, useTimeout, useDebounce, useInterval diff --git a/.changeset/spotty-pumpkins-buy.md b/.changeset/spotty-pumpkins-buy.md new file mode 100644 index 00000000000..2d8f0fcb3ac --- /dev/null +++ b/.changeset/spotty-pumpkins-buy.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/utils': minor +--- + +Add toUpperCamelCase and deepFind functionss diff --git a/.changeset/strange-poems-build.md b/.changeset/strange-poems-build.md deleted file mode 100644 index ffb1f8f2385..00000000000 --- a/.changeset/strange-poems-build.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@hyperlane-xyz/widgets': minor ---- - -Props and style update: IconButton and Tooltip -New Icons: XCircleIcon and SwapIcon diff --git a/.changeset/yellow-baboons-kneel.md b/.changeset/yellow-baboons-kneel.md new file mode 100644 index 00000000000..7a7ab645783 --- /dev/null +++ b/.changeset/yellow-baboons-kneel.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Add decodeIsmMetadata function diff --git a/.codespell/ignore.txt b/.codespell/ignore.txt index 85cd6f56a94..ec0437cb91b 100644 --- a/.codespell/ignore.txt +++ b/.codespell/ignore.txt @@ -5,3 +5,4 @@ receivedFrom ser readded re-use +superseed diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 4d284e827e5..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -coverage -*.cts \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a23e3c2f937..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,65 +0,0 @@ -{ - "env": { - "node": true, - "browser": true, - "es2021": true - }, - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module", - "project": "./tsconfig.json" - }, - "plugins": ["@typescript-eslint","jest"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "rules": { - "no-console": ["error"], - "no-eval": ["error"], - "no-extra-boolean-cast": ["error"], - "no-ex-assign": ["error"], - "no-constant-condition": ["off"], - "no-return-await": ["error"], - "no-restricted-imports": ["error", { - "name": "console", - "message": "Please use a logger and/or the utils' package assert" - }, { - "name": "fs", - "message": "Avoid use of node-specific libraries" - }], - "guard-for-in": ["error"], - "@typescript-eslint/ban-ts-comment": ["off"], - "@typescript-eslint/explicit-module-boundary-types": ["off"], - "@typescript-eslint/no-explicit-any": ["off"], - "@typescript-eslint/no-floating-promises": ["error"], - "@typescript-eslint/no-non-null-assertion": ["off"], - "@typescript-eslint/no-require-imports": ["warn"], - "@typescript-eslint/no-unused-vars": [ - "error", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } - ], - "@typescript-eslint/ban-types": [ - "error", - { - "types": { - // Unban the {} type which is a useful shorthand for non-nullish value - "{}": false - }, - "extendDefaults": true - } - ], - "jest/no-disabled-tests": "warn", - "jest/no-focused-tests": "error", - "jest/no-identical-title": "error", - "jest/prefer-to-have-length": "warn", - "jest/valid-expect": "error" - } -} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b780d6585d6..664f93612af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,6 +54,10 @@ jobs: exit 1 fi + # Check for mismatched dep versions across the monorepo + - name: syncpack + run: yarn syncpack list-mismatches + lint-prettier: runs-on: ubuntu-latest needs: [yarn-install] @@ -72,6 +76,12 @@ jobs: .yarn key: ${{ runner.os }}-yarn-4.5.1-cache-${{ hashFiles('./yarn.lock') }} fail-on-cache-miss: true + + # Build required before linting or the intra-monorepo package cycle checking won't work + - name: yarn-build + uses: ./.github/actions/yarn-build-with-cache + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: lint run: yarn lint @@ -109,9 +119,18 @@ jobs: - name: Unit Tests run: yarn test:ci - cli-e2e: + cli-e2e-matrix: runs-on: ubuntu-latest needs: [yarn-install] + strategy: + fail-fast: false + matrix: + test: + - core + - relay + - warp-read + - warp-apply + - warp-deploy steps: - uses: actions/checkout@v4 with: @@ -130,8 +149,21 @@ jobs: - name: Checkout registry uses: ./.github/actions/checkout-registry - - name: CLI e2e tests + - name: CLI e2e tests (${{ matrix.test }}) run: yarn --cwd typescript/cli test:e2e + env: + CLI_E2E_TEST: ${{ matrix.test }} + + cli-e2e: + runs-on: ubuntu-latest + needs: cli-e2e-matrix + if: always() + steps: + - name: Check cli-e2e matrix status + if: ${{ needs.cli-e2e-matrix.result != 'success' }} + run: | + echo "CLI E2E tests failed" + exit 1 agent-configs: runs-on: ubuntu-latest diff --git a/.registryrc b/.registryrc index 9f4316497a7..01f6449ff08 100644 --- a/.registryrc +++ b/.registryrc @@ -1 +1 @@ -3e366eae1d49da4270695b157d7af00bb761156a +385b83950adba6f033be836b627bab7d89aae38d diff --git a/.syncpackrc b/.syncpackrc new file mode 100644 index 00000000000..7c4fd6c9d44 --- /dev/null +++ b/.syncpackrc @@ -0,0 +1,3 @@ +{ + "dependencyTypes": ["prod", "dev"] +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000000..3be951d4500 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,115 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import importPlugin from 'eslint-plugin-import'; +import jest from 'eslint-plugin-jest'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +export const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: [ + '**/node_modules', + '**/dist', + '**/coverage', + '**/*.cjs', + '**/*.cts', + '**/*.mjs', + 'jest.config.js', + ], + }, + ...compat.extends( + 'eslint:recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ), + { + plugins: { + import: importPlugin, + '@typescript-eslint': typescriptEslint, + jest, + }, + + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + }, + + parser: tsParser, + ecmaVersion: 12, + sourceType: 'module', + + parserOptions: { + project: './tsconfig.json', + }, + }, + + settings: { + 'import/resolver': { + typescript: true, + node: true, + }, + }, + + rules: { + 'guard-for-in': ['error'], + 'import/no-cycle': ['error'], + 'import/no-self-import': ['error'], + 'import/no-named-as-default-member': ['off'], + 'no-console': ['error'], + 'no-eval': ['error'], + 'no-extra-boolean-cast': ['error'], + 'no-ex-assign': ['error'], + 'no-constant-condition': ['off'], + 'no-return-await': ['error'], + + 'no-restricted-imports': [ + 'error', + { + name: 'console', + message: 'Please use a logger and/or the utils package assert', + }, + { + name: 'fs', + message: 'Avoid use of node-specific libraries', + }, + ], + + '@typescript-eslint/ban-ts-comment': ['off'], + '@typescript-eslint/explicit-module-boundary-types': ['off'], + '@typescript-eslint/no-explicit-any': ['off'], + '@typescript-eslint/no-floating-promises': ['error'], + '@typescript-eslint/no-non-null-assertion': ['off'], + '@typescript-eslint/no-require-imports': ['warn'], + '@typescript-eslint/no-unused-expressions': ['off'], + '@typescript-eslint/no-empty-object-type': ['off'], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + + 'jest/no-disabled-tests': 'warn', + 'jest/no-focused-tests': 'error', + 'jest/no-identical-title': 'error', + 'jest/prefer-to-have-length': 'warn', + 'jest/valid-expect': 'error', + }, + }, +]; diff --git a/package.json b/package.json index 2a2400e9654..92d5b4b5d2b 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,20 @@ "description": "A yarn workspace of core Hyperlane packages", "version": "0.0.0", "devDependencies": { + "@eslint/js": "^9.15.0", "@trivago/prettier-plugin-sort-imports": "^4.2.1", - "@typescript-eslint/eslint-plugin": "^7.4.0", - "@typescript-eslint/parser": "^7.4.0", - "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.1.6", + "@typescript-eslint/parser": "^8.1.6", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.2.0", "husky": "^8.0.0", "lint-staged": "^12.4.3", "prettier": "^2.8.8", - "tsx": "^4.7.1" + "syncpack": "^13.0.0", + "tsx": "^4.19.1" }, "dependencies": { "@changesets/cli": "^2.26.2" @@ -41,6 +45,7 @@ "async": "^2.6.4", "fetch-ponyfill": "^7.1", "flat": "^5.0.2", + "globals": "^14.0.0", "lodash": "^4.17.21", "recursive-readdir": "^2.2.3", "underscore": "^1.13", diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index 6dfcb67ceee..28f0f74c2c8 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -2564,7 +2564,8 @@ dependencies = [ [[package]] name = "ed25519-dalek" version = "1.0.1" -source = "git+https://github.com/Eclipse-Laboratories-Inc/ed25519-dalek?branch=main#7529d65506147b6cb24ca6d8f4fc062cac33b395" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.2", "ed25519 1.5.3", @@ -8353,7 +8354,7 @@ dependencies = [ [[package]] name = "solana-account-decoder" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "Inflector", "base64 0.13.1", @@ -8377,7 +8378,7 @@ dependencies = [ [[package]] name = "solana-address-lookup-table-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "bytemuck", @@ -8397,7 +8398,7 @@ dependencies = [ [[package]] name = "solana-clap-utils" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "chrono", "clap 2.34.0", @@ -8414,7 +8415,7 @@ dependencies = [ [[package]] name = "solana-cli-config" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "dirs-next", "lazy_static", @@ -8429,7 +8430,7 @@ dependencies = [ [[package]] name = "solana-client" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "async-mutex", "async-trait", @@ -8482,7 +8483,7 @@ dependencies = [ [[package]] name = "solana-config-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "chrono", @@ -8495,7 +8496,7 @@ dependencies = [ [[package]] name = "solana-faucet" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "byteorder", @@ -8518,7 +8519,7 @@ dependencies = [ [[package]] name = "solana-frozen-abi" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "ahash 0.7.8", "blake3", @@ -8551,7 +8552,7 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", @@ -8562,7 +8563,7 @@ dependencies = [ [[package]] name = "solana-logger" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "env_logger", "lazy_static", @@ -8572,7 +8573,7 @@ dependencies = [ [[package]] name = "solana-measure" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "log", "solana-sdk", @@ -8581,7 +8582,7 @@ dependencies = [ [[package]] name = "solana-metrics" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "crossbeam-channel", "gethostname", @@ -8594,7 +8595,7 @@ dependencies = [ [[package]] name = "solana-net-utils" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "clap 3.2.25", @@ -8615,7 +8616,7 @@ dependencies = [ [[package]] name = "solana-perf" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "ahash 0.7.8", "bincode", @@ -8641,7 +8642,7 @@ dependencies = [ [[package]] name = "solana-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "base64 0.13.1", "bincode", @@ -8689,7 +8690,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "base64 0.13.1", "bincode", @@ -8715,7 +8716,7 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "lazy_static", "num_cpus", @@ -8724,7 +8725,7 @@ dependencies = [ [[package]] name = "solana-remote-wallet" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "console", "dialoguer", @@ -8742,7 +8743,7 @@ dependencies = [ [[package]] name = "solana-sdk" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "assert_matches", "base64 0.13.1", @@ -8792,7 +8793,7 @@ dependencies = [ [[package]] name = "solana-sdk-macro" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bs58 0.4.0", "proc-macro2 1.0.86", @@ -8804,7 +8805,7 @@ dependencies = [ [[package]] name = "solana-streamer" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "crossbeam-channel", "futures-util", @@ -8832,7 +8833,7 @@ dependencies = [ [[package]] name = "solana-transaction-status" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "Inflector", "base64 0.13.1", @@ -8860,7 +8861,7 @@ dependencies = [ [[package]] name = "solana-version" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "log", "rustc_version", @@ -8875,7 +8876,7 @@ dependencies = [ [[package]] name = "solana-vote-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "log", @@ -8895,7 +8896,7 @@ dependencies = [ [[package]] name = "solana-zk-token-sdk" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "aes-gcm-siv", "arrayref", diff --git a/rust/main/Cargo.toml b/rust/main/Cargo.toml index e9192b7f0fa..0059867e133 100644 --- a/rust/main/Cargo.toml +++ b/rust/main/Cargo.toml @@ -126,24 +126,9 @@ sha2 = { version = "0.10.6", default-features = false } sha256 = "1.1.4" sha3 = "0.10" solana-account-decoder = "=1.14.13" -solana-banks-client = "=1.14.13" -solana-banks-interface = "=1.14.13" -solana-banks-server = "=1.14.13" -solana-clap-utils = "=1.14.13" -solana-cli-config = "=1.14.13" solana-client = "=1.14.13" -solana-program = "=1.14.13" -solana-program-test = "=1.14.13" solana-sdk = "=1.14.13" solana-transaction-status = "=1.14.13" -solana-zk-token-sdk = "=1.14.13" -spl-associated-token-account = { version = "=1.1.2", features = [ - "no-entrypoint", -] } -spl-noop = { version = "=0.1.3", features = ["no-entrypoint"] } -spl-token = { version = "=3.5.0", features = ["no-entrypoint"] } -spl-token-2022 = { version = "=0.5.0", features = ["no-entrypoint"] } -spl-type-length-value = "=0.1.0" static_assertions = "1.1" strum = "0.26.2" strum_macros = "0.26.2" @@ -239,11 +224,6 @@ branch = "v3.2.2-relax-zeroize" git = "https://github.com/Eclipse-Laboratories-Inc/curve25519-dalek" version = "3.2.2" -[patch.crates-io.ed25519-dalek] -branch = "main" -git = "https://github.com/Eclipse-Laboratories-Inc/ed25519-dalek" -version = "1.0.1" - [patch.crates-io.primitive-types] branch = "hyperlane" git = "https://github.com/hyperlane-xyz/parity-common.git" @@ -256,42 +236,42 @@ version = "=0.5.2" [patch.crates-io.solana-account-decoder] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-clap-utils] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-cli-config] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-client] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-program] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-sdk] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-transaction-status] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-zk-token-sdk] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.spl-associated-token-account] diff --git a/rust/main/agents/relayer/src/relayer.rs b/rust/main/agents/relayer/src/relayer.rs index 1ac619792c4..6bd1a63a8d7 100644 --- a/rust/main/agents/relayer/src/relayer.rs +++ b/rust/main/agents/relayer/src/relayer.rs @@ -152,6 +152,7 @@ impl BaseAgent for Relayer { dbs.iter() .map(|(d, db)| (d.clone(), Arc::new(db.clone()))) .collect(), + false, ) .await? .into_iter() @@ -166,6 +167,7 @@ impl BaseAgent for Relayer { dbs.iter() .map(|(d, db)| (d.clone(), Arc::new(db.clone()))) .collect(), + false, ) .await? .into_iter() @@ -180,6 +182,7 @@ impl BaseAgent for Relayer { dbs.iter() .map(|(d, db)| (d.clone(), Arc::new(db.clone()))) .collect(), + false, ) .await? .into_iter() diff --git a/rust/main/agents/scraper/src/agent.rs b/rust/main/agents/scraper/src/agent.rs index 97f6bd3db19..443d1c7fac2 100644 --- a/rust/main/agents/scraper/src/agent.rs +++ b/rust/main/agents/scraper/src/agent.rs @@ -198,6 +198,7 @@ impl Scraper { &metrics.clone(), &contract_sync_metrics.clone(), store.into(), + true, ) .await .unwrap(); @@ -229,6 +230,7 @@ impl Scraper { &metrics.clone(), &contract_sync_metrics.clone(), Arc::new(store.clone()) as _, + true, ) .await .unwrap(); @@ -261,6 +263,7 @@ impl Scraper { &metrics.clone(), &contract_sync_metrics.clone(), Arc::new(store.clone()), + true, ) .await .unwrap(); diff --git a/rust/main/agents/validator/src/validator.rs b/rust/main/agents/validator/src/validator.rs index 2d09bd93ffa..f7a8b43f596 100644 --- a/rust/main/agents/validator/src/validator.rs +++ b/rust/main/agents/validator/src/validator.rs @@ -109,6 +109,7 @@ impl BaseAgent for Validator { &metrics, &contract_sync_metrics, msg_db.clone().into(), + false, ) .await?; diff --git a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs index e415e9bc766..4d5e819de30 100644 --- a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs +++ b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs @@ -91,7 +91,8 @@ impl InterchainGasPaymaster for SealevelInterchainGasPaymaster {} pub struct SealevelInterchainGasPaymasterIndexer { rpc_client: SealevelRpcClient, igp: SealevelInterchainGasPaymaster, - _log_meta_composer: LogMetaComposer, + log_meta_composer: LogMetaComposer, + advanced_log_meta: bool, } /// IGP payment data on Sealevel @@ -107,6 +108,7 @@ impl SealevelInterchainGasPaymasterIndexer { pub async fn new( conf: &ConnectionConf, igp_account_locator: ContractLocator<'_>, + advanced_log_meta: bool, ) -> ChainResult { // Set the `processed` commitment at rpc level let rpc_client = SealevelRpcClient::new(conf.url.to_string()); @@ -122,7 +124,8 @@ impl SealevelInterchainGasPaymasterIndexer { Ok(Self { rpc_client, igp, - _log_meta_composer: log_meta_composer, + log_meta_composer, + advanced_log_meta, }) } @@ -168,23 +171,24 @@ impl SealevelInterchainGasPaymasterIndexer { gas_amount: gas_payment_account.gas_amount.into(), }; - // let log_meta = self - // .interchain_payment_log_meta( - // U256::from(sequence_number), - // &valid_payment_pda_pubkey, - // &gas_payment_account.slot, - // ) - // .await?; - - let log_meta = LogMeta { - address: self.igp.program_id.to_bytes().into(), - block_number: gas_payment_account.slot, - // TODO: get these when building out scraper support. - // It's inconvenient to get these :| - block_hash: H256::zero(), - transaction_id: H512::zero(), - transaction_index: 0, - log_index: sequence_number.into(), + let log_meta = if self.advanced_log_meta { + self.interchain_payment_log_meta( + U256::from(sequence_number), + &valid_payment_pda_pubkey, + &gas_payment_account.slot, + ) + .await? + } else { + LogMeta { + address: self.igp.program_id.to_bytes().into(), + block_number: gas_payment_account.slot, + // TODO: get these when building out scraper support. + // It's inconvenient to get these :| + block_hash: H256::zero(), + transaction_id: H512::zero(), + transaction_index: 0, + log_index: sequence_number.into(), + } }; Ok(SealevelGasPayment::new( @@ -212,7 +216,7 @@ impl SealevelInterchainGasPaymasterIndexer { Ok(expected_pubkey) } - async fn _interchain_payment_log_meta( + async fn interchain_payment_log_meta( &self, log_index: U256, payment_pda_pubkey: &Pubkey, @@ -220,7 +224,7 @@ impl SealevelInterchainGasPaymasterIndexer { ) -> ChainResult { let block = self.rpc_client.get_block(*payment_pda_slot).await?; - self._log_meta_composer + self.log_meta_composer .log_meta(block, log_index, payment_pda_pubkey, payment_pda_slot) .map_err(Into::::into) } diff --git a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs index d376801fe07..cc88da72f0a 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs @@ -648,10 +648,15 @@ pub struct SealevelMailboxIndexer { program_id: Pubkey, dispatch_message_log_meta_composer: LogMetaComposer, delivery_message_log_meta_composer: LogMetaComposer, + advanced_log_meta: bool, } impl SealevelMailboxIndexer { - pub fn new(conf: &ConnectionConf, locator: ContractLocator) -> ChainResult { + pub fn new( + conf: &ConnectionConf, + locator: ContractLocator, + advanced_log_meta: bool, + ) -> ChainResult { let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); let mailbox = SealevelMailbox::new(conf, locator, None)?; @@ -672,6 +677,7 @@ impl SealevelMailboxIndexer { mailbox, dispatch_message_log_meta_composer, delivery_message_log_meta_composer, + advanced_log_meta, }) } @@ -712,23 +718,24 @@ impl SealevelMailboxIndexer { let hyperlane_message = HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?; - // let log_meta = self - // .dispatch_message_log_meta( - // U256::from(nonce), - // &valid_message_storage_pda_pubkey, - // &dispatched_message_account.slot, - // ) - // .await?; - - let log_meta = LogMeta { - address: self.program_id.to_bytes().into(), - block_number: dispatched_message_account.slot, - // TODO: get these when building out scraper support. - // It's inconvenient to get these :| - block_hash: H256::zero(), - transaction_id: H512::zero(), - transaction_index: 0, - log_index: U256::zero(), + let log_meta = if self.advanced_log_meta { + self.dispatch_message_log_meta( + U256::from(nonce), + &valid_message_storage_pda_pubkey, + &dispatched_message_account.slot, + ) + .await? + } else { + LogMeta { + address: self.program_id.to_bytes().into(), + block_number: dispatched_message_account.slot, + // TODO: get these when building out scraper support. + // It's inconvenient to get these :| + block_hash: H256::zero(), + transaction_id: H512::zero(), + transaction_index: 0, + log_index: U256::zero(), + } }; Ok((hyperlane_message.into(), log_meta)) @@ -748,7 +755,7 @@ impl SealevelMailboxIndexer { Ok(expected_pubkey) } - async fn _dispatch_message_log_meta( + async fn dispatch_message_log_meta( &self, log_index: U256, message_storage_pda_pubkey: &Pubkey, @@ -805,23 +812,24 @@ impl SealevelMailboxIndexer { .into_inner(); let message_id = delivered_message_account.message_id; - // let log_meta = self - // .delivered_message_log_meta( - // U256::from(nonce), - // &valid_message_storage_pda_pubkey, - // &delivered_message_account.slot, - // ) - // .await?; - - let log_meta = LogMeta { - address: self.program_id.to_bytes().into(), - block_number: delivered_message_account.slot, - // TODO: get these when building out scraper support. - // It's inconvenient to get these :| - block_hash: H256::zero(), - transaction_id: H512::zero(), - transaction_index: 0, - log_index: U256::zero(), + let log_meta = if self.advanced_log_meta { + self.delivered_message_log_meta( + U256::from(nonce), + &valid_message_storage_pda_pubkey, + &delivered_message_account.slot, + ) + .await? + } else { + LogMeta { + address: self.program_id.to_bytes().into(), + block_number: delivered_message_account.slot, + // TODO: get these when building out scraper support. + // It's inconvenient to get these :| + block_hash: H256::zero(), + transaction_id: H512::zero(), + transaction_index: 0, + log_index: U256::zero(), + } }; Ok((message_id.into(), log_meta)) @@ -839,7 +847,7 @@ impl SealevelMailboxIndexer { Ok(expected_pubkey) } - async fn _delivered_message_log_meta( + async fn delivered_message_log_meta( &self, log_index: U256, message_storage_pda_pubkey: &Pubkey, diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs index 6186bf31ee0..4d557c0d167 100644 --- a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs @@ -112,14 +112,14 @@ impl SealevelRpcClient { Ok(balance.into()) } - pub async fn get_block(&self, height: u64) -> ChainResult { + pub async fn get_block(&self, slot: u64) -> ChainResult { let config = RpcBlockConfig { commitment: Some(CommitmentConfig::finalized()), max_supported_transaction_version: Some(0), ..Default::default() }; self.0 - .get_block_with_config(height, config) + .get_block_with_config(slot, config) .await .map_err(HyperlaneSealevelError::ClientError) .map_err(Into::into) @@ -273,3 +273,6 @@ impl std::fmt::Debug for SealevelRpcClient { f.write_str("RpcClient { ... }") } } + +#[cfg(test)] +mod tests; diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/client/tests.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/client/tests.rs new file mode 100644 index 00000000000..81859559c23 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/client/tests.rs @@ -0,0 +1,14 @@ +use crate::SealevelRpcClient; + +//#[tokio::test] +async fn _test_get_block() { + // given + let client = SealevelRpcClient::new("".to_string()); + + // when + let slot = 301337842; // block which requires latest version of solana-client + let result = client.get_block(slot).await; + + // then + assert!(result.is_ok()); +} diff --git a/rust/main/config/mainnet_config.json b/rust/main/config/mainnet_config.json index f0ea1a9ddd9..284abb82338 100644 --- a/rust/main/config/mainnet_config.json +++ b/rust/main/config/mainnet_config.json @@ -1231,7 +1231,7 @@ "bech32Prefix": "inj", "blockExplorers": [ { - "apiUrl": "https://www.mintscan.io/injective", + "apiUrl": "https://apis.mintscan.io/v1/injective", "family": "other", "name": "Mintscan", "url": "https://www.mintscan.io/injective" @@ -1839,7 +1839,7 @@ "aggregationHook": "0xF6C1769d5390Be0f77080eF7791fBbA7eF4D5659", "blockExplorers": [ { - "apiUrl": "https://explorer.mintchain.io/api/eth-rpc", + "apiUrl": "https://explorer.mintchain.io/api", "family": "blockscout", "name": "Mint Explorer", "url": "https://explorer.mintchain.io" @@ -2042,7 +2042,7 @@ "bech32Prefix": "neutron", "blockExplorers": [ { - "apiUrl": "https://www.mintscan.io/neutron", + "apiUrl": "https://apis.mintscan.io/v1/neutron", "family": "other", "name": "Mintscan", "url": "https://www.mintscan.io/neutron" @@ -2180,7 +2180,7 @@ "bech32Prefix": "osmo", "blockExplorers": [ { - "apiUrl": "https://www.mintscan.io/osmosis", + "apiUrl": "https://apis.mintscan.io/v1/osmosis", "family": "other", "name": "Mintscan", "url": "https://www.mintscan.io/osmosis" @@ -4111,7 +4111,7 @@ "bech32Prefix": "stride", "blockExplorers": [ { - "apiUrl": "https://www.mintscan.io/stride", + "apiUrl": "https://apis.mintscan.io/v1/stride", "family": "other", "name": "Mintscan", "url": "https://www.mintscan.io/stride" @@ -5731,6 +5731,402 @@ "testRecipient": "0x545E289B88c6d97b74eC0B96e308cae46Bf5f832", "timelockController": "0x0000000000000000000000000000000000000000", "validatorAnnounce": "0x26A29486480BD74f9B830a9B8dB33cb43C40f496" + }, + "boba": { + "blockExplorers": [ + { + "apiUrl": "https://api.routescan.io/v2/network/mainnet/evm/288/etherscan/api", + "family": "routescan", + "name": "bobascan", + "url": "https://bobascan.com" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 2, + "reorgPeriod": 5 + }, + "chainId": 288, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Boba Mainnet", + "domainId": 288, + "gasCurrencyCoinGeckoId": "ethereum", + "name": "boba", + "nativeToken": { + "decimals": 18, + "name": "Ether", + "symbol": "ETH" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://mainnet.boba.network" + } + ], + "technicalStack": "opstack", + "aggregationHook": "0x4533B9ff84bE4f4009409414aF8b8e9c3f4F52a8", + "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", + "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "fallbackRoutingHook": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "interchainSecurityModule": "0x9c582a96B7350eEd313560Aeb9aBDff047aeaD36", + "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "pausableIsm": "0xc5D6aCaafBCcEC6D7fD7d92F4509befce641c563", + "protocolFee": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "proxyAdmin": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "staticAggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "staticAggregationIsm": "0x46De8b87577624b9ce63201238982b95ad0d7Ea4", + "staticAggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "staticMerkleRootWeightedMultisigIsmFactory": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "staticMessageIdWeightedMultisigIsmFactory": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "storageGasOracle": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "testRecipient": "0xbB88a31E4b709b645c06825c0E0b5CAC906d97DE", + "timelockController": "0x0000000000000000000000000000000000000000", + "validatorAnnounce": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "index": { + "from": 10598389 + } + }, + "duckchain": { + "blockExplorers": [ + { + "apiUrl": "https://scan.duckchain.io/api", + "family": "blockscout", + "name": "DuckChain Explorer", + "url": "https://scan.duckchain.io" + } + ], + "blocks": { + "confirmations": 3, + "estimateBlockTime": 1, + "reorgPeriod": 5 + }, + "chainId": 5545, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "DuckChain", + "domainId": 5545, + "gasCurrencyCoinGeckoId": "the-open-network", + "index": { + "from": 1149918 + }, + "name": "duckchain", + "nativeToken": { + "decimals": 18, + "name": "Toncoin", + "symbol": "TON" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://rpc.duckchain.io" + }, + { + "http": "https://rpc-hk.duckchain.io" + } + ], + "technicalStack": "arbitrumnitro", + "aggregationHook": "0x4533B9ff84bE4f4009409414aF8b8e9c3f4F52a8", + "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", + "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "fallbackRoutingHook": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "interchainSecurityModule": "0x739800B825916456b55CF832A535eE253bC1f358", + "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "pausableIsm": "0xc5D6aCaafBCcEC6D7fD7d92F4509befce641c563", + "protocolFee": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "proxyAdmin": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "staticAggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "staticAggregationIsm": "0x46De8b87577624b9ce63201238982b95ad0d7Ea4", + "staticAggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "staticMerkleRootWeightedMultisigIsmFactory": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "staticMessageIdWeightedMultisigIsmFactory": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "storageGasOracle": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "testRecipient": "0xbB88a31E4b709b645c06825c0E0b5CAC906d97DE", + "timelockController": "0x0000000000000000000000000000000000000000", + "validatorAnnounce": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e" + }, + "superseed": { + "blockExplorers": [ + { + "apiUrl": "https://explorer.superseed.xyz/api", + "family": "blockscout", + "name": "Superseed Explorer", + "url": "https://explorer.superseed.xyz" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 2, + "reorgPeriod": 5 + }, + "chainId": 5330, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Superseed", + "domainId": 5330, + "gasCurrencyCoinGeckoId": "ethereum", + "name": "superseed", + "nativeToken": { + "decimals": 18, + "name": "Ether", + "symbol": "ETH" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://mainnet.superseed.xyz" + } + ], + "technicalStack": "opstack", + "aggregationHook": "0x4533B9ff84bE4f4009409414aF8b8e9c3f4F52a8", + "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", + "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "fallbackRoutingHook": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "interchainSecurityModule": "0x9bdafD7aEd501B30f72b24Ce85d423eB77f51ba0", + "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "pausableIsm": "0xc5D6aCaafBCcEC6D7fD7d92F4509befce641c563", + "protocolFee": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "proxyAdmin": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "staticAggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "staticAggregationIsm": "0x46De8b87577624b9ce63201238982b95ad0d7Ea4", + "staticAggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "staticMerkleRootWeightedMultisigIsmFactory": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "staticMessageIdWeightedMultisigIsmFactory": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "storageGasOracle": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "testRecipient": "0xbB88a31E4b709b645c06825c0E0b5CAC906d97DE", + "timelockController": "0x0000000000000000000000000000000000000000", + "validatorAnnounce": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "index": { + "from": 3010957 + } + }, + "unichain": { + "blockExplorers": [ + { + "apiUrl": "https://unichain.blockscout.com/api", + "family": "blockscout", + "name": "Unichain Explorer", + "url": "https://unichain.blockscout.com" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 1, + "reorgPeriod": 5 + }, + "chainId": 130, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Unichain", + "domainId": 130, + "gasCurrencyCoinGeckoId": "ethereum", + "name": "unichain", + "nativeToken": { + "decimals": 18, + "name": "Ether", + "symbol": "ETH" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://mainnet.unichain.org" + } + ], + "technicalStack": "opstack", + "aggregationHook": "0x4533B9ff84bE4f4009409414aF8b8e9c3f4F52a8", + "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", + "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "fallbackRoutingHook": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "interchainSecurityModule": "0xce5718713f019b71F7d37058cD75d12e01dE2611", + "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "pausableIsm": "0xc5D6aCaafBCcEC6D7fD7d92F4509befce641c563", + "protocolFee": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "proxyAdmin": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "staticAggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "staticAggregationIsm": "0x46De8b87577624b9ce63201238982b95ad0d7Ea4", + "staticAggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "staticMerkleRootWeightedMultisigIsmFactory": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "staticMessageIdWeightedMultisigIsmFactory": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "storageGasOracle": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "testRecipient": "0xbB88a31E4b709b645c06825c0E0b5CAC906d97DE", + "timelockController": "0x0000000000000000000000000000000000000000", + "validatorAnnounce": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "index": { + "from": 1453239 + } + }, + "vana": { + "blockExplorers": [ + { + "apiUrl": "https://vanascan.io/api/eth-rpc", + "family": "blockscout", + "name": "Vana Explorer", + "url": "https://vanascan.io" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 6, + "reorgPeriod": 5 + }, + "chainId": 1480, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Vana", + "domainId": 1480, + "gasCurrencyCoinGeckoId": "vana", + "name": "vana", + "nativeToken": { + "decimals": 18, + "name": "Vana", + "symbol": "VANA" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://rpc.vana.org" + } + ], + "technicalStack": "other", + "aggregationHook": "0x4533B9ff84bE4f4009409414aF8b8e9c3f4F52a8", + "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", + "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "fallbackRoutingHook": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "interchainSecurityModule": "0xce5718713f019b71F7d37058cD75d12e01dE2611", + "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "pausableIsm": "0xc5D6aCaafBCcEC6D7fD7d92F4509befce641c563", + "protocolFee": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "proxyAdmin": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "staticAggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "staticAggregationIsm": "0x46De8b87577624b9ce63201238982b95ad0d7Ea4", + "staticAggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "staticMerkleRootWeightedMultisigIsmFactory": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "staticMessageIdWeightedMultisigIsmFactory": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "storageGasOracle": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "testRecipient": "0xbB88a31E4b709b645c06825c0E0b5CAC906d97DE", + "timelockController": "0x0000000000000000000000000000000000000000", + "validatorAnnounce": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "index": { + "from": 629917 + } + }, + "bsquared": { + "blockExplorers": [ + { + "apiUrl": "https://explorer.bsquared.network/api", + "family": "etherscan", + "name": "B² Network Explorer", + "url": "https://explorer.bsquared.network" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 3, + "reorgPeriod": 5 + }, + "chainId": 223, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "B² Network", + "domainId": 223, + "gasCurrencyCoinGeckoId": "bitcoin", + "name": "bsquared", + "nativeToken": { + "decimals": 18, + "name": "Bitcoin", + "symbol": "BTC" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://rpc.bsquared.network" + }, + { + "http": "https://rpc.ankr.com/b2" + }, + { + "http": "https://mainnet.b2-rpc.com" + }, + { + "http": "https://b2-mainnet.alt.technology" + } + ], + "technicalStack": "other", + "aggregationHook": "0x03D610d916D5D274e4A31c026fd6884281A35d9C", + "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", + "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "fallbackRoutingHook": "0x60bB6D060393D3C206719A7bD61844cC82891cfB", + "interchainAccountIsm": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "interchainAccountRouter": "0x4D50044335dc1d4D26c343AdeDf6E47808475Deb", + "interchainGasPaymaster": "0x70EbA87Cd15616f32C736B3f3BdCfaeD0713a82B", + "interchainSecurityModule": "0x7dBb82188F553161d4B4ac3a2362Bff3a57e21D2", + "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "merkleTreeHook": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "pausableHook": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de", + "pausableIsm": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "protocolFee": "0xbB88a31E4b709b645c06825c0E0b5CAC906d97DE", + "proxyAdmin": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "staticAggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "staticAggregationIsm": "0x7dBb82188F553161d4B4ac3a2362Bff3a57e21D2", + "staticAggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "staticMerkleRootWeightedMultisigIsmFactory": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "staticMessageIdWeightedMultisigIsmFactory": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "storageGasOracle": "0x451dF8AB0936D85526D816f0b4dCaDD934A034A4", + "testRecipient": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597", + "timelockController": "0x0000000000000000000000000000000000000000", + "validatorAnnounce": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "index": { + "from": 9687363 + } } }, "defaultRpcConsensusType": "fallback" diff --git a/rust/main/hyperlane-base/src/settings/base.rs b/rust/main/hyperlane-base/src/settings/base.rs index 0320e4eb710..e32771b0e2f 100644 --- a/rust/main/hyperlane-base/src/settings/base.rs +++ b/rust/main/hyperlane-base/src/settings/base.rs @@ -151,13 +151,14 @@ impl Settings { build_contract_fns!(build_validator_announce, build_validator_announces -> dyn ValidatorAnnounce); build_contract_fns!(build_provider, build_providers -> dyn HyperlaneProvider); - /// Build a contract sync for type `T` using log store `D` + /// Build a contract sync for type `T` using log store `S` pub async fn sequenced_contract_sync( &self, domain: &HyperlaneDomain, metrics: &CoreMetrics, sync_metrics: &ContractSyncMetrics, store: Arc, + advanced_log_meta: bool, ) -> eyre::Result>> where T: Indexable + Debug, @@ -166,7 +167,8 @@ impl Settings { { let setup = self.chain_setup(domain)?; // Currently, all indexers are of the `SequenceIndexer` type - let indexer = SequenceIndexer::::try_from_with_metrics(setup, metrics).await?; + let indexer = + SequenceIndexer::::try_from_with_metrics(setup, metrics, advanced_log_meta).await?; Ok(Arc::new(ContractSync::new( domain.clone(), store.clone() as SequenceAwareLogStore<_>, @@ -175,13 +177,14 @@ impl Settings { ))) } - /// Build a contract sync for type `T` using log store `D` + /// Build a contract sync for type `T` using log store `S` pub async fn watermark_contract_sync( &self, domain: &HyperlaneDomain, metrics: &CoreMetrics, sync_metrics: &ContractSyncMetrics, store: Arc, + advanced_log_meta: bool, ) -> eyre::Result>> where T: Indexable + Debug, @@ -190,7 +193,8 @@ impl Settings { { let setup = self.chain_setup(domain)?; // Currently, all indexers are of the `SequenceIndexer` type - let indexer = SequenceIndexer::::try_from_with_metrics(setup, metrics).await?; + let indexer = + SequenceIndexer::::try_from_with_metrics(setup, metrics, advanced_log_meta).await?; Ok(Arc::new(ContractSync::new( domain.clone(), store.clone() as WatermarkLogStore<_>, @@ -208,6 +212,7 @@ impl Settings { metrics: &CoreMetrics, sync_metrics: &ContractSyncMetrics, stores: HashMap>, + advanced_log_meta: bool, ) -> Result>>> where T: Indexable + Debug + Send + Sync + Clone + Eq + Hash + 'static, @@ -227,6 +232,7 @@ impl Settings { metrics, sync_metrics, stores.get(domain).unwrap().clone(), + advanced_log_meta, ) .await .map(|r| r as Arc>)?, @@ -236,6 +242,7 @@ impl Settings { metrics, sync_metrics, stores.get(domain).unwrap().clone(), + advanced_log_meta, ) .await .map(|r| r as Arc>)?, diff --git a/rust/main/hyperlane-base/src/settings/chains.rs b/rust/main/hyperlane-base/src/settings/chains.rs index 0e08b2d1568..33600418bb0 100644 --- a/rust/main/hyperlane-base/src/settings/chains.rs +++ b/rust/main/hyperlane-base/src/settings/chains.rs @@ -33,7 +33,11 @@ use super::ChainSigner; #[async_trait] pub trait TryFromWithMetrics: Sized { /// Try to convert the chain configuration into the type - async fn try_from_with_metrics(conf: &ChainConf, metrics: &CoreMetrics) -> Result; + async fn try_from_with_metrics( + conf: &ChainConf, + metrics: &CoreMetrics, + advanced_log_meta: bool, + ) -> Result; } /// A chain setup is a domain ID, an address on that chain (where the mailbox is @@ -72,22 +76,38 @@ pub type MerkleTreeHookIndexer = Arc for MessageIndexer { - async fn try_from_with_metrics(conf: &ChainConf, metrics: &CoreMetrics) -> Result { - conf.build_message_indexer(metrics).await.map(Into::into) + async fn try_from_with_metrics( + conf: &ChainConf, + metrics: &CoreMetrics, + advanced_log_meta: bool, + ) -> Result { + conf.build_message_indexer(metrics, advanced_log_meta) + .await + .map(Into::into) } } #[async_trait] impl TryFromWithMetrics for DeliveryIndexer { - async fn try_from_with_metrics(conf: &ChainConf, metrics: &CoreMetrics) -> Result { - conf.build_delivery_indexer(metrics).await.map(Into::into) + async fn try_from_with_metrics( + conf: &ChainConf, + metrics: &CoreMetrics, + advanced_log_meta: bool, + ) -> Result { + conf.build_delivery_indexer(metrics, advanced_log_meta) + .await + .map(Into::into) } } #[async_trait] impl TryFromWithMetrics for IgpIndexer { - async fn try_from_with_metrics(conf: &ChainConf, metrics: &CoreMetrics) -> Result { - conf.build_interchain_gas_payment_indexer(metrics) + async fn try_from_with_metrics( + conf: &ChainConf, + metrics: &CoreMetrics, + advanced_log_meta: bool, + ) -> Result { + conf.build_interchain_gas_payment_indexer(metrics, advanced_log_meta) .await .map(Into::into) } @@ -95,8 +115,12 @@ impl TryFromWithMetrics for IgpIndexer { #[async_trait] impl TryFromWithMetrics for MerkleTreeHookIndexer { - async fn try_from_with_metrics(conf: &ChainConf, metrics: &CoreMetrics) -> Result { - conf.build_merkle_tree_hook_indexer(metrics) + async fn try_from_with_metrics( + conf: &ChainConf, + metrics: &CoreMetrics, + advanced_log_meta: bool, + ) -> Result { + conf.build_merkle_tree_hook_indexer(metrics, advanced_log_meta) .await .map(Into::into) } @@ -266,6 +290,7 @@ impl ChainConf { pub async fn build_message_indexer( &self, metrics: &CoreMetrics, + advanced_log_meta: bool, ) -> Result>> { let ctx = "Building delivery indexer"; let locator = self.locator(self.addresses.mailbox); @@ -284,7 +309,11 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { - let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new(conf, locator)?); + let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new( + conf, + locator, + advanced_log_meta, + )?); Ok(indexer as Box>) } ChainConnectionConf::Cosmos(conf) => { @@ -306,6 +335,7 @@ impl ChainConf { pub async fn build_delivery_indexer( &self, metrics: &CoreMetrics, + advanced_log_meta: bool, ) -> Result>> { let ctx = "Building delivery indexer"; let locator = self.locator(self.addresses.mailbox); @@ -324,7 +354,11 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { - let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new(conf, locator)?); + let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new( + conf, + locator, + advanced_log_meta, + )?); Ok(indexer as Box>) } ChainConnectionConf::Cosmos(conf) => { @@ -385,6 +419,7 @@ impl ChainConf { pub async fn build_interchain_gas_payment_indexer( &self, metrics: &CoreMetrics, + advanced_log_meta: bool, ) -> Result>> { let ctx = "Building IGP indexer"; let locator = self.locator(self.addresses.interchain_gas_paymaster); @@ -407,7 +442,12 @@ impl ChainConf { ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { let indexer = Box::new( - h_sealevel::SealevelInterchainGasPaymasterIndexer::new(conf, locator).await?, + h_sealevel::SealevelInterchainGasPaymasterIndexer::new( + conf, + locator, + advanced_log_meta, + ) + .await?, ); Ok(indexer as Box>) } @@ -428,6 +468,7 @@ impl ChainConf { pub async fn build_merkle_tree_hook_indexer( &self, metrics: &CoreMetrics, + advanced_log_meta: bool, ) -> Result>> { let ctx = "Building merkle tree hook indexer"; let locator = self.locator(self.addresses.merkle_tree_hook); @@ -446,8 +487,11 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { - let mailbox_indexer = - Box::new(h_sealevel::SealevelMailboxIndexer::new(conf, locator)?); + let mailbox_indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new( + conf, + locator, + advanced_log_meta, + )?); let indexer = Box::new(h_sealevel::SealevelMerkleTreeHookIndexer::new( *mailbox_indexer, )); diff --git a/rust/main/hyperlane-base/src/settings/signers.rs b/rust/main/hyperlane-base/src/settings/signers.rs index 459a6337bea..4233a5c6926 100644 --- a/rust/main/hyperlane-base/src/settings/signers.rs +++ b/rust/main/hyperlane-base/src/settings/signers.rs @@ -126,10 +126,9 @@ impl BuildableWithSignerConf for Keypair { if let SignerConf::HexKey { key } = conf { let secret = SecretKey::from_bytes(key.as_bytes()) .context("Invalid sealevel ed25519 secret key")?; - Ok( - Keypair::from_bytes(&ed25519_dalek::Keypair::from(secret).to_bytes()) - .context("Unable to create Keypair")?, - ) + let public = ed25519_dalek::PublicKey::from(&secret); + let dalek = ed25519_dalek::Keypair { secret, public }; + Ok(Keypair::from_bytes(&dalek.to_bytes()).context("Unable to create Keypair")?) } else { bail!(format!("{conf:?} key is not supported by sealevel")); } diff --git a/rust/rust-toolchain b/rust/rust-toolchain deleted file mode 100644 index 83e24cc5665..00000000000 --- a/rust/rust-toolchain +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "1.72.1" -profile = "default" diff --git a/rust/sealevel/Cargo.lock b/rust/sealevel/Cargo.lock index 81beeb35b38..7462ad6c98b 100644 --- a/rust/sealevel/Cargo.lock +++ b/rust/sealevel/Cargo.lock @@ -743,9 +743,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceed8ef69d8518a5dda55c07425450b58a4e1946f4951eab6d7191ee86c2443d" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" dependencies = [ "serde", ] @@ -1254,6 +1254,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + [[package]] name = "dialoguer" version = "0.10.4" @@ -1405,7 +1425,8 @@ dependencies = [ [[package]] name = "ed25519-dalek" version = "1.0.1" -source = "git+https://github.com/Eclipse-Laboratories-Inc/ed25519-dalek?branch=main#7529d65506147b6cb24ca6d8f4fc062cac33b395" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", @@ -2385,7 +2406,7 @@ dependencies = [ "bytes", "convert_case 0.6.0", "derive-new", - "derive_more", + "derive_more 0.99.17", "eyre", "fixed-hash 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "getrandom 0.2.15", @@ -4538,26 +4559,26 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.11.3" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca070c12893629e2cc820a9761bedf6ce1dcddc9852984d1dc734b8bd9bd024" +checksum = "1aa7ffc1c0ef49b0452c6e2986abf2b07743320641ffd5fc63d552458e3b779b" dependencies = [ "cfg-if", - "derive_more", + "derive_more 1.0.0", "parity-scale-codec", "scale-info-derive", ] [[package]] name = "scale-info-derive" -version = "2.11.3" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d35494501194174bda522a32605929eefc9ecf7e0a326c26db1fdd85881eb62" +checksum = "46385cc24172cf615450267463f937c10072516359b3ff1cb24228a4a08bf951" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] @@ -4903,7 +4924,7 @@ dependencies = [ [[package]] name = "solana-account-decoder" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "Inflector", "base64 0.13.1", @@ -4927,7 +4948,7 @@ dependencies = [ [[package]] name = "solana-address-lookup-table-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "bytemuck", @@ -4947,7 +4968,7 @@ dependencies = [ [[package]] name = "solana-banks-client" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "borsh", "futures", @@ -4963,7 +4984,7 @@ dependencies = [ [[package]] name = "solana-banks-interface" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "serde", "solana-sdk", @@ -4973,7 +4994,7 @@ dependencies = [ [[package]] name = "solana-banks-server" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "crossbeam-channel", @@ -4992,7 +5013,7 @@ dependencies = [ [[package]] name = "solana-bpf-loader-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "byteorder", @@ -5010,7 +5031,7 @@ dependencies = [ [[package]] name = "solana-bucket-map" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "log", "memmap2", @@ -5024,7 +5045,7 @@ dependencies = [ [[package]] name = "solana-clap-utils" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "chrono", "clap 2.34.0", @@ -5041,7 +5062,7 @@ dependencies = [ [[package]] name = "solana-cli-config" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "dirs-next", "lazy_static", @@ -5056,7 +5077,7 @@ dependencies = [ [[package]] name = "solana-client" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "async-mutex", "async-trait", @@ -5109,7 +5130,7 @@ dependencies = [ [[package]] name = "solana-compute-budget-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "solana-program-runtime", "solana-sdk", @@ -5118,7 +5139,7 @@ dependencies = [ [[package]] name = "solana-config-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "chrono", @@ -5131,7 +5152,7 @@ dependencies = [ [[package]] name = "solana-faucet" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "byteorder", @@ -5154,7 +5175,7 @@ dependencies = [ [[package]] name = "solana-frozen-abi" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "ahash", "blake3", @@ -5187,7 +5208,7 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", @@ -5198,7 +5219,7 @@ dependencies = [ [[package]] name = "solana-logger" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "env_logger 0.9.3", "lazy_static", @@ -5208,7 +5229,7 @@ dependencies = [ [[package]] name = "solana-measure" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "log", "solana-sdk", @@ -5217,7 +5238,7 @@ dependencies = [ [[package]] name = "solana-metrics" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "crossbeam-channel", "gethostname", @@ -5230,7 +5251,7 @@ dependencies = [ [[package]] name = "solana-net-utils" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "clap 3.2.25", @@ -5251,7 +5272,7 @@ dependencies = [ [[package]] name = "solana-perf" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "ahash", "bincode", @@ -5277,7 +5298,7 @@ dependencies = [ [[package]] name = "solana-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "base64 0.13.1", "bincode", @@ -5325,7 +5346,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "base64 0.13.1", "bincode", @@ -5351,7 +5372,7 @@ dependencies = [ [[package]] name = "solana-program-test" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "assert_matches", "async-trait", @@ -5375,7 +5396,7 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "lazy_static", "num_cpus", @@ -5384,7 +5405,7 @@ dependencies = [ [[package]] name = "solana-remote-wallet" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "console", "dialoguer", @@ -5402,7 +5423,7 @@ dependencies = [ [[package]] name = "solana-runtime" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "arrayref", "bincode", @@ -5462,7 +5483,7 @@ dependencies = [ [[package]] name = "solana-sdk" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "assert_matches", "base64 0.13.1", @@ -5512,7 +5533,7 @@ dependencies = [ [[package]] name = "solana-sdk-macro" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bs58 0.4.0", "proc-macro2 1.0.86", @@ -5524,7 +5545,7 @@ dependencies = [ [[package]] name = "solana-send-transaction-service" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "crossbeam-channel", "log", @@ -5538,7 +5559,7 @@ dependencies = [ [[package]] name = "solana-stake-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "log", @@ -5560,7 +5581,7 @@ dependencies = [ [[package]] name = "solana-streamer" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "crossbeam-channel", "futures-util", @@ -5588,7 +5609,7 @@ dependencies = [ [[package]] name = "solana-transaction-status" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "Inflector", "base64 0.13.1", @@ -5616,7 +5637,7 @@ dependencies = [ [[package]] name = "solana-version" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "log", "rustc_version", @@ -5631,7 +5652,7 @@ dependencies = [ [[package]] name = "solana-vote-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bincode", "log", @@ -5651,7 +5672,7 @@ dependencies = [ [[package]] name = "solana-zk-token-proof-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "bytemuck", "getrandom 0.1.16", @@ -5665,7 +5686,7 @@ dependencies = [ [[package]] name = "solana-zk-token-sdk" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2023-07-04#62a6421cab862c77b9ac7a8d93f54f8b5b223af7" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" dependencies = [ "aes-gcm-siv", "arrayref", @@ -6304,9 +6325,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap 2.5.0", "toml_datetime", @@ -6943,9 +6964,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/rust/sealevel/Cargo.toml b/rust/sealevel/Cargo.toml index 5887dc8d174..145b9bfcc2b 100644 --- a/rust/sealevel/Cargo.toml +++ b/rust/sealevel/Cargo.toml @@ -256,11 +256,6 @@ branch = "v3.2.2-relax-zeroize" git = "https://github.com/Eclipse-Laboratories-Inc/curve25519-dalek" version = "3.2.2" -[patch.crates-io.ed25519-dalek] -branch = "main" -git = "https://github.com/Eclipse-Laboratories-Inc/ed25519-dalek" -version = "1.0.1" - [patch.crates-io.primitive-types] branch = "hyperlane" git = "https://github.com/hyperlane-xyz/parity-common.git" @@ -273,62 +268,52 @@ version = "=0.5.2" [patch.crates-io.solana-account-decoder] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-banks-client] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" -version = "=1.14.13" - -[patch.crates-io.solana-banks-interface] -git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" -version = "=1.14.13" - -[patch.crates-io.solana-banks-server] -git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-clap-utils] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-cli-config] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-client] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-program] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-program-test] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-sdk] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-transaction-status] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.solana-zk-token-sdk] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2023-07-04" +tag = "hyperlane-1.14.13-2024-11-20" version = "=1.14.13" [patch.crates-io.spl-associated-token-account] diff --git a/rust/sealevel/client/src/cmd_utils.rs b/rust/sealevel/client/src/cmd_utils.rs index e0cd3ff6866..f387d3c4fdf 100644 --- a/rust/sealevel/client/src/cmd_utils.rs +++ b/rust/sealevel/client/src/cmd_utils.rs @@ -17,6 +17,23 @@ use solana_sdk::{ const SOLANA_DOMAIN: u32 = 1399811149; +pub(crate) fn get_compute_unit_price_micro_lamports_for_id(domain: u32) -> u64 { + get_compute_unit_price(domain == SOLANA_DOMAIN) +} + +pub(crate) fn get_compute_unit_price_micro_lamports_for_chain_name(chain_name: &str) -> u64 { + get_compute_unit_price(chain_name == "solanamainnet") +} + +fn get_compute_unit_price(is_solanamainnet: bool) -> u64 { + if is_solanamainnet { + // Generally taking a low/medium value from https://www.quicknode.com/gas-tracker/solana + 500_000 + } else { + 0 + } +} + pub(crate) fn account_exists(client: &RpcClient, account: &Pubkey) -> Result { // Using `get_account_with_commitment` instead of `get_account` so we get Ok(None) when the account // doesn't exist, rather than an error @@ -73,9 +90,9 @@ pub(crate) fn deploy_program( program_keypair_path, ]; + let compute_unit_price = get_compute_unit_price_micro_lamports_for_id(local_domain).to_string(); if local_domain.eq(&SOLANA_DOMAIN) { - // May need tweaking depending on gas prices / available balance - command.append(&mut vec!["--with-compute-unit-price", "550000"]); + command.extend(vec!["--with-compute-unit-price", &compute_unit_price]); } build_cmd(command.as_slice(), None, None); diff --git a/rust/sealevel/client/src/core.rs b/rust/sealevel/client/src/core.rs index 44cf9cb5290..e3abedf0352 100644 --- a/rust/sealevel/client/src/core.rs +++ b/rust/sealevel/client/src/core.rs @@ -9,26 +9,21 @@ use solana_sdk::{compute_budget, compute_budget::ComputeBudgetInstruction}; use std::collections::HashMap; use std::{fs::File, path::Path}; +use crate::cmd_utils::get_compute_unit_price_micro_lamports_for_chain_name; +use crate::ONE_SOL_IN_LAMPORTS; use crate::{ artifacts::{read_json, write_json}, cmd_utils::{create_and_write_keypair, create_new_directory, deploy_program}, multisig_ism::deploy_multisig_ism_message_id, Context, CoreCmd, CoreDeploy, CoreSubCmd, }; -use crate::{DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, ONE_SOL_IN_LAMPORTS}; use hyperlane_core::H256; use hyperlane_sealevel_igp::accounts::{SOL_DECIMALS, TOKEN_EXCHANGE_RATE_SCALE}; pub(crate) fn adjust_gas_price_if_needed(chain_name: &str, ctx: &mut Context) { if chain_name.eq("solanamainnet") { + let compute_unit_price = get_compute_unit_price_micro_lamports_for_chain_name(chain_name); let mut initial_instructions = ctx.initial_instructions.borrow_mut(); - const PROCESS_DESIRED_PRIORITIZATION_FEE_LAMPORTS_PER_TX: u64 = 50_000_000; - const MICRO_LAMPORT_FEE_PER_LIMIT: u64 = - // Convert to micro-lamports - (PROCESS_DESIRED_PRIORITIZATION_FEE_LAMPORTS_PER_TX * 1_000_000) - // Divide by the max compute units - / DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64; - for i in initial_instructions.iter_mut() { if i.instruction.program_id != compute_budget::id() { continue; @@ -41,9 +36,8 @@ pub(crate) fn adjust_gas_price_if_needed(chain_name: &str, ctx: &mut Context) { ComputeBudgetInstruction::SetComputeUnitPrice { .. } ) { // The compute unit price has already been set, so we override it and return early - i.instruction = ComputeBudgetInstruction::set_compute_unit_price( - MICRO_LAMPORT_FEE_PER_LIMIT, - ); + i.instruction = + ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price); return; } } @@ -51,10 +45,10 @@ pub(crate) fn adjust_gas_price_if_needed(chain_name: &str, ctx: &mut Context) { initial_instructions.push( ( - ComputeBudgetInstruction::set_compute_unit_price(MICRO_LAMPORT_FEE_PER_LIMIT), + ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price), Some(format!( - "Set compute unit price to {}", - MICRO_LAMPORT_FEE_PER_LIMIT + "Set compute unit price to {} micro-lamports", + compute_unit_price )), ) .into(), diff --git a/rust/sealevel/environments/mainnet3/warp-routes/apxETH-eclipse-ethereum/program-ids.json b/rust/sealevel/environments/mainnet3/warp-routes/apxETH-eclipse-ethereum/program-ids.json new file mode 100644 index 00000000000..683a275abb9 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/apxETH-eclipse-ethereum/program-ids.json @@ -0,0 +1,10 @@ +{ + "eclipsemainnet": { + "hex": "0x82f7445ccda6396092998c8f841f0d4eb63cca29ba23cfd2609d283f3ee9d13f", + "base58": "9pEgj7m2VkwLtJHPtTw5d8vbB7kfjzcXXCRgdwruW7C2" + }, + "ethereum": { + "hex": "0x000000000000000000000000d34fe1685c28a68bb4b8faaadcb2769962ae737c", + "base58": "1111111111113wkPRLXCJuj9539UEURsN3qhoaYb" + } +} diff --git a/rust/sealevel/environments/mainnet3/warp-routes/apxETH-eclipse-ethereum/token-config.json b/rust/sealevel/environments/mainnet3/warp-routes/apxETH-eclipse-ethereum/token-config.json new file mode 100644 index 00000000000..1634a20d35e --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/apxETH-eclipse-ethereum/token-config.json @@ -0,0 +1,17 @@ +{ + "eclipsemainnet": { + "type": "synthetic", + "decimals": 9, + "remoteDecimals": 18, + "name": "Autocompounding Pirex Ether", + "symbol": "apxETH", + "uri": "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/ae7df1bc00af19f8ba692c14e4df3acdbf30497d/deployments/warp_routes/APXETH/metadata.json", + "interchainGasPaymaster": "3Wp4qKkgf4tjXz1soGyTSndCgBPLZFSrZkiDZ8Qp9EEj" + }, + "ethereum": { + "type": "collateral", + "decimals": 18, + "token": "0x9ba021b0a9b958b5e75ce9f6dff97c7ee52cb3e6", + "foreignDeployment": "0xd34FE1685c28A68Bb4B8fAaadCb2769962AE737c" + } +} diff --git a/rust/sealevel/environments/mainnet3/warp-routes/ezSOL-eclipse-solana/program-ids.json b/rust/sealevel/environments/mainnet3/warp-routes/ezSOL-eclipse-solana/program-ids.json new file mode 100644 index 00000000000..5a914d4f2c4 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/ezSOL-eclipse-solana/program-ids.json @@ -0,0 +1,10 @@ +{ + "eclipsemainnet": { + "hex": "0xb06d58417c929a624e9b689e604e6d60ca652168ee76b9a290bd5b974b22b306", + "base58": "CshTfxXWMvnRAwBTCjQ4577bkP5po5ZuNG1QTuQxA5Au" + }, + "solanamainnet": { + "hex": "0x08bb318b88b38cc6f450b185e51a9c42402dc9d36fa6741c19c2aa62464a5eb3", + "base58": "b5pMgizA9vrGRt3hVqnU7vUVGBQUnLpwPzcJhG1ucyQ" + } +} diff --git a/rust/sealevel/environments/mainnet3/warp-routes/ezSOL-eclipse-solana/token-config.json b/rust/sealevel/environments/mainnet3/warp-routes/ezSOL-eclipse-solana/token-config.json new file mode 100644 index 00000000000..eef5c8349be --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/ezSOL-eclipse-solana/token-config.json @@ -0,0 +1,17 @@ +{ + "solanamainnet": { + "type": "collateral", + "decimals": 9, + "interchainGasPaymaster": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF", + "token": "ezSoL6fY1PVdJcJsUpe5CM3xkfmy3zoVCABybm5WtiC", + "splTokenProgram": "token" + }, + "eclipsemainnet": { + "type": "synthetic", + "decimals": 9, + "name": "Renzo Restaked SOL", + "symbol": "ezSOL", + "uri": "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/ae7df1bc00af19f8ba692c14e4df3acdbf30497d/deployments/warp_routes/EZSOL/metadata.json", + "interchainGasPaymaster": "3Wp4qKkgf4tjXz1soGyTSndCgBPLZFSrZkiDZ8Qp9EEj" + } +} diff --git a/rust/sealevel/libraries/test-utils/src/igp.rs b/rust/sealevel/libraries/test-utils/src/igp.rs index 70e51000679..3ff20da1b37 100644 --- a/rust/sealevel/libraries/test-utils/src/igp.rs +++ b/rust/sealevel/libraries/test-utils/src/igp.rs @@ -49,7 +49,7 @@ pub async fn initialize_igp_accounts( GasOracle::RemoteGasData(RemoteGasData { token_exchange_rate: TOKEN_EXCHANGE_RATE_SCALE, gas_price: 1u128, - /// The number of decimals for the remote token. + // The number of decimals for the remote token. token_decimals: SOL_DECIMALS, }), None, diff --git a/rust/sealevel/rust-toolchain b/rust/sealevel/rust-toolchain index 83e24cc5665..1c8cfba9f59 100644 --- a/rust/sealevel/rust-toolchain +++ b/rust/sealevel/rust-toolchain @@ -1,3 +1,3 @@ [toolchain] -channel = "1.72.1" +channel = "1.75.0" profile = "default" diff --git a/solidity/CHANGELOG.md b/solidity/CHANGELOG.md index 6edb35de625..0ba5c201ecb 100644 --- a/solidity/CHANGELOG.md +++ b/solidity/CHANGELOG.md @@ -1,5 +1,12 @@ # @hyperlane-xyz/core +## 5.8.1 + +### Patch Changes + +- Updated dependencies [0e285a443] + - @hyperlane-xyz/utils@7.1.0 + ## 5.8.0 ### Minor Changes diff --git a/solidity/contracts/PackageVersioned.sol b/solidity/contracts/PackageVersioned.sol index 3bd056b570e..476985336d1 100644 --- a/solidity/contracts/PackageVersioned.sol +++ b/solidity/contracts/PackageVersioned.sol @@ -7,5 +7,5 @@ pragma solidity >=0.6.11; **/ abstract contract PackageVersioned { // GENERATED CODE - DO NOT EDIT - string public constant PACKAGE_VERSION = "5.8.0"; + string public constant PACKAGE_VERSION = "5.8.1"; } diff --git a/solidity/package.json b/solidity/package.json index bcdc8cb4f95..d56a148a320 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -1,14 +1,14 @@ { "name": "@hyperlane-xyz/core", "description": "Core solidity contracts for Hyperlane", - "version": "5.8.0", + "version": "5.8.1", "dependencies": { "@arbitrum/nitro-contracts": "^1.2.1", "@eth-optimism/contracts": "^0.6.0", - "@hyperlane-xyz/utils": "7.0.0", + "@hyperlane-xyz/utils": "7.1.0", "@layerzerolabs/lz-evm-oapp-v2": "2.0.2", "@openzeppelin/contracts": "^4.9.3", - "@openzeppelin/contracts-upgradeable": "^v4.9.3", + "@openzeppelin/contracts-upgradeable": "^4.9.3", "fx-portal": "^1.0.3" }, "devDependencies": { @@ -19,7 +19,7 @@ "@typechain/ethers-v6": "^0.5.1", "@typechain/hardhat": "^9.1.0", "@types/node": "^18.14.5", - "chai": "4.5.0", + "chai": "^4.5.0", "ethereum-waffle": "^4.0.10", "ethers": "^5.7.2", "hardhat": "^2.22.2", diff --git a/typescript/ccip-server/.eslintrc b/typescript/ccip-server/.eslintrc deleted file mode 100644 index 05936cd4e4e..00000000000 --- a/typescript/ccip-server/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-console": ["off"] - } - } - \ No newline at end of file diff --git a/typescript/ccip-server/CHANGELOG.md b/typescript/ccip-server/CHANGELOG.md index a6a35b3683c..5bfabb5d48f 100644 --- a/typescript/ccip-server/CHANGELOG.md +++ b/typescript/ccip-server/CHANGELOG.md @@ -1,5 +1,7 @@ # @hyperlane-xyz/ccip-server +## 7.1.0 + ## 7.0.0 ## 6.0.0 diff --git a/typescript/ccip-server/eslint.config.mjs b/typescript/ccip-server/eslint.config.mjs new file mode 100644 index 00000000000..17cd27a740d --- /dev/null +++ b/typescript/ccip-server/eslint.config.mjs @@ -0,0 +1,17 @@ +import MonorepoDefaults from '../../eslint.config.mjs'; + +export default [ + ...MonorepoDefaults, + { + files: ['./src/**/*.ts'], + }, + { + rules: { + 'no-console': ['off'], + 'no-restricted-imports': ['off'], + }, + }, + { + ignores: ['**/__mocks__/*','**/tests/*',] + } +]; diff --git a/typescript/ccip-server/package.json b/typescript/ccip-server/package.json index d93fca2c6a2..58e7a3e5522 100644 --- a/typescript/ccip-server/package.json +++ b/typescript/ccip-server/package.json @@ -1,6 +1,6 @@ { "name": "@hyperlane-xyz/ccip-server", - "version": "7.0.0", + "version": "7.1.0", "description": "CCIP server", "typings": "dist/index.d.ts", "typedocMain": "src/index.ts", @@ -12,27 +12,30 @@ "node": ">=16" }, "scripts": { + "build": "tsc -p tsconfig.json", "start": "tsx src/server.ts", "dev": "nodemon src/server.ts", "test": "jest", + "lint": "eslint -c ./eslint.config.mjs", "prettier": "prettier --write ./src/* ./tests/" }, "author": "brolee", "license": "Apache-2.0", "devDependencies": { "@jest/globals": "^29.7.0", - "@types/node": "^16.9.1", + "@types/node": "^18.14.5", + "eslint": "^9.15.0", "jest": "^29.7.0", "nodemon": "^3.0.3", "prettier": "^2.8.8", "ts-jest": "^29.1.2", "ts-node": "^10.8.0", - "tsx": "^4.7.1", + "tsx": "^4.19.1", "typescript": "5.3.3" }, "dependencies": { "@chainlink/ccip-read-server": "^0.2.1", "dotenv-flow": "^4.1.0", - "ethers": "5.7.2" + "ethers": "^5.7.2" } } diff --git a/typescript/ccip-server/src/server.ts b/typescript/ccip-server/src/server.ts index 0f69c499f47..4151b986442 100644 --- a/typescript/ccip-server/src/server.ts +++ b/typescript/ccip-server/src/server.ts @@ -6,12 +6,14 @@ import { ProofsService } from './services/ProofsService'; // Initialize Services const proofsService = new ProofsService( - config.LIGHT_CLIENT_ADDR, - config.RPC_ADDRESS, - config.STEP_FN_ID, - config.CHAIN_ID, - config.SUCCINCT_PLATFORM_URL, - config.SUCCINCT_API_KEY, + { + lightClientAddress: config.LIGHT_CLIENT_ADDR, + stepFunctionId: config.STEP_FN_ID, + platformUrl: config.SUCCINCT_PLATFORM_URL, + apiKey: config.SUCCINCT_API_KEY, + }, + { url: config.RPC_ADDRESS, chainId: config.CHAIN_ID }, + { url: `${config.SERVER_URL_PREFIX}:${config.SERVER_PORT}` }, ); // Initialize Server and add Service handlers diff --git a/typescript/ccip-server/src/services/LightClientService.ts b/typescript/ccip-server/src/services/LightClientService.ts index f3aae5b0970..4511229669f 100644 --- a/typescript/ccip-server/src/services/LightClientService.ts +++ b/typescript/ccip-server/src/services/LightClientService.ts @@ -27,8 +27,8 @@ class LightClientService { * @param slot * @returns */ - async getSyncCommitteePoseidons(slot: bigint): Promise { - return await this.lightClientContract.syncCommitteePoseidons( + getSyncCommitteePoseidons(slot: bigint): Promise { + return this.lightClientContract.syncCommitteePoseidons( this.getSyncCommitteePeriod(slot), ); } diff --git a/typescript/ccip-server/src/services/ProofsService.ts b/typescript/ccip-server/src/services/ProofsService.ts index 1d05e7a3f73..5db40c56486 100644 --- a/typescript/ccip-server/src/services/ProofsService.ts +++ b/typescript/ccip-server/src/services/ProofsService.ts @@ -4,8 +4,7 @@ import { TelepathyCcipReadIsmAbi } from '../abis/TelepathyCcipReadIsmAbi'; import { HyperlaneService } from './HyperlaneService'; import { LightClientService, SuccinctConfig } from './LightClientService'; -import { RPCService } from './RPCService'; -import { ProofResult } from './RPCService'; +import { ProofResult, RPCService } from './RPCService'; import { ProofStatus } from './common/ProofStatusEnum'; type RPCConfig = { @@ -100,10 +99,7 @@ class ProofsService { ); const slot = await this.lightClientService.calculateSlot(BigInt(timestamp)); const syncCommitteePoseidon = ''; // TODO get from LC - return await this.lightClientService.requestProof( - syncCommitteePoseidon, - slot, - ); + return this.lightClientService.requestProof(syncCommitteePoseidon, slot); } /** diff --git a/typescript/ccip-server/tsconfig.json b/typescript/ccip-server/tsconfig.json new file mode 100644 index 00000000000..793d16b8e49 --- /dev/null +++ b/typescript/ccip-server/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist/", + "rootDir": "./src" + }, + "exclude": ["./node_modules/", "./dist/"], + "include": ["./src/*.ts"] +} diff --git a/typescript/cli/.eslintignore b/typescript/cli/.eslintignore deleted file mode 100644 index 76add878f8d..00000000000 --- a/typescript/cli/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist \ No newline at end of file diff --git a/typescript/cli/.eslintrc b/typescript/cli/.eslintrc deleted file mode 100644 index 4d2a6fe74fc..00000000000 --- a/typescript/cli/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-console": ["off"], - "no-restricted-imports": ["off"] - } -} diff --git a/typescript/cli/.mocharc-e2e.json b/typescript/cli/.mocharc-e2e.json index ecabac62f32..0f8803e7adb 100644 --- a/typescript/cli/.mocharc-e2e.json +++ b/typescript/cli/.mocharc-e2e.json @@ -1,6 +1,5 @@ { "extensions": ["ts"], - "spec": ["src/**/*.e2e-test.ts"], "node-option": [ "experimental-specifier-resolution=node", "loader=ts-node/esm" diff --git a/typescript/cli/CHANGELOG.md b/typescript/cli/CHANGELOG.md index 3dd7232495e..9e82984e479 100644 --- a/typescript/cli/CHANGELOG.md +++ b/typescript/cli/CHANGELOG.md @@ -1,5 +1,23 @@ # @hyperlane-xyz/cli +## 7.1.0 + +### Minor Changes + +- 5db46bd31: Implements persistent relayer for use in CLI + +### Patch Changes + +- Updated dependencies [6f2d50fbd] +- Updated dependencies [1159e0f4b] +- Updated dependencies [0e285a443] +- Updated dependencies [ff2b4e2fb] +- Updated dependencies [0e285a443] +- Updated dependencies [5db46bd31] +- Updated dependencies [0cd65c571] + - @hyperlane-xyz/sdk@7.1.0 + - @hyperlane-xyz/utils@7.1.0 + ## 7.0.0 ### Minor Changes diff --git a/typescript/cli/eslint.config.mjs b/typescript/cli/eslint.config.mjs new file mode 100644 index 00000000000..30f5895c6a8 --- /dev/null +++ b/typescript/cli/eslint.config.mjs @@ -0,0 +1,20 @@ +import MonorepoDefaults from '../../eslint.config.mjs'; + +export default [ + ...MonorepoDefaults, + { + files: ['./src/**/*.ts', './cli.ts', './env.ts'], + }, + { + rules: { + 'no-console': ['off'], + 'no-restricted-imports': ['off'], + }, + }, + { + ignores: ['./src/tests/**/*.ts'], + rules: { + 'import/no-cycle': ['off'], + }, + }, +]; diff --git a/typescript/cli/package.json b/typescript/cli/package.json index df2acaeb16e..5b6765de316 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -1,16 +1,16 @@ { "name": "@hyperlane-xyz/cli", - "version": "7.0.0", + "version": "7.1.0", "description": "A command-line utility for common Hyperlane operations", "dependencies": { "@aws-sdk/client-kms": "^3.577.0", "@aws-sdk/client-s3": "^3.577.0", - "@hyperlane-xyz/registry": "4.7.0", - "@hyperlane-xyz/sdk": "7.0.0", - "@hyperlane-xyz/utils": "7.0.0", + "@hyperlane-xyz/registry": "6.1.0", + "@hyperlane-xyz/sdk": "7.1.0", + "@hyperlane-xyz/utils": "7.1.0", "@inquirer/core": "9.0.10", "@inquirer/figures": "1.0.5", - "@inquirer/prompts": "^3.0.0", + "@inquirer/prompts": "3.3.2", "ansi-escapes": "^7.0.0", "asn1.js": "^5.4.1", "bignumber.js": "^9.1.1", @@ -18,7 +18,7 @@ "ethers": "^5.7.2", "latest-version": "^8.0.0", "terminal-link": "^3.0.0", - "tsx": "^4.7.1", + "tsx": "^4.19.1", "yaml": "2.4.5", "yargs": "^17.7.2", "zod": "^3.21.2", @@ -26,18 +26,21 @@ "zx": "^8.1.4" }, "devDependencies": { + "@eslint/js": "^9.15.0", "@ethersproject/abi": "*", "@ethersproject/providers": "*", "@types/chai-as-promised": "^8", "@types/mocha": "^10.0.1", "@types/node": "^18.14.5", "@types/yargs": "^17.0.24", - "@typescript-eslint/eslint-plugin": "^7.4.0", - "@typescript-eslint/parser": "^7.4.0", + "@typescript-eslint/eslint-plugin": "^8.1.6", + "@typescript-eslint/parser": "^8.1.6", "chai": "^4.5.0", "chai-as-promised": "^8.0.0", - "eslint": "^8.57.0", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", "mocha": "^10.2.0", "prettier": "^2.8.8", "typescript": "5.3.3" @@ -47,7 +50,7 @@ "build": "yarn version:update && tsc", "dev": "yarn version:update && tsc --watch", "clean": "rm -rf ./dist", - "lint": "eslint . --ext .ts", + "lint": "eslint -c ./eslint.config.mjs", "prettier": "prettier --write ./src ./examples", "test:ci": "yarn mocha --config .mocharc.json", "test:e2e": "./scripts/run-e2e-test.sh", diff --git a/typescript/cli/scripts/run-e2e-test.sh b/typescript/cli/scripts/run-e2e-test.sh index bfa89a0b31f..c98fc9158f6 100755 --- a/typescript/cli/scripts/run-e2e-test.sh +++ b/typescript/cli/scripts/run-e2e-test.sh @@ -16,7 +16,12 @@ anvil --chain-id 31338 -p 8555 --state /tmp/anvil2/state --gas-price 1 > /dev/nu anvil --chain-id 31347 -p 8600 --state /tmp/anvil3/state --gas-price 1 > /dev/null & echo "Running E2E tests" -yarn mocha --config .mocharc-e2e.json +if [ -n "${CLI_E2E_TEST}" ]; then + echo "Running only ${CLI_E2E_TEST} test" + yarn mocha --config .mocharc-e2e.json "src/**/${CLI_E2E_TEST}.e2e-test.ts" +else + yarn mocha --config .mocharc-e2e.json "src/**/*.e2e-test.ts" +fi cleanup diff --git a/typescript/cli/src/avs/check.ts b/typescript/cli/src/avs/check.ts index 6730463e62c..27055c68485 100644 --- a/typescript/cli/src/avs/check.ts +++ b/typescript/cli/src/avs/check.ts @@ -429,7 +429,7 @@ const getEcdsaStakeRegistryAddress = ( ): Address | undefined => { try { return avsAddresses[chain]['ecdsaStakeRegistry']; - } catch (err) { + } catch { topLevelErrors.push( `❗️ EcdsaStakeRegistry address not found for ${chain}`, ); diff --git a/typescript/cli/src/check/warp.ts b/typescript/cli/src/check/warp.ts index a31fac62e48..f0d147a468c 100644 --- a/typescript/cli/src/check/warp.ts +++ b/typescript/cli/src/check/warp.ts @@ -4,7 +4,6 @@ import { WarpRouteDeployConfig, normalizeConfig } from '@hyperlane-xyz/sdk'; import { ObjectDiff, diffObjMerge } from '@hyperlane-xyz/utils'; import { log, logGreen } from '../logger.js'; -import '../utils/output.js'; import { formatYamlViolationsOutput } from '../utils/output.js'; export async function runWarpRouteCheck({ diff --git a/typescript/cli/src/commands/relayer.ts b/typescript/cli/src/commands/relayer.ts index 0d7672e7342..c07a4d41207 100644 --- a/typescript/cli/src/commands/relayer.ts +++ b/typescript/cli/src/commands/relayer.ts @@ -9,7 +9,7 @@ import { Address } from '@hyperlane-xyz/utils'; import { CommandModuleWithContext } from '../context/types.js'; import { log } from '../logger.js'; import { tryReadJson, writeJson } from '../utils/files.js'; -import { getWarpCoreConfigOrExit } from '../utils/input.js'; +import { getWarpCoreConfigOrExit } from '../utils/warp.js'; import { agentTargetsCommandOption, diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index c2f4916b015..3cd99fe6a0d 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -23,8 +23,8 @@ import { removeEndingSlash, writeYamlOrJson, } from '../utils/files.js'; -import { getWarpCoreConfigOrExit } from '../utils/input.js'; import { selectRegistryWarpRoute } from '../utils/tokens.js'; +import { getWarpCoreConfigOrExit } from '../utils/warp.js'; import { runVerifyWarpRoute } from '../verify/warp.js'; import { diff --git a/typescript/cli/src/config/agent.ts b/typescript/cli/src/config/agent.ts index 05fa165596b..a176a6f040e 100644 --- a/typescript/cli/src/config/agent.ts +++ b/typescript/cli/src/config/agent.ts @@ -99,7 +99,7 @@ async function getStartBlocks( try { const deployedBlock = await mailbox.deployedBlock(); return deployedBlock.toNumber(); - } catch (err) { + } catch { errorRed( `❌ Failed to get deployed block to set an index for ${chain}, this is potentially an issue with rpc provider or a misconfiguration`, ); diff --git a/typescript/cli/src/config/submit.ts b/typescript/cli/src/config/submit.ts index 3b7e3c59e71..6ce067a8a0b 100644 --- a/typescript/cli/src/config/submit.ts +++ b/typescript/cli/src/config/submit.ts @@ -3,9 +3,8 @@ import { stringify as yamlStringify } from 'yaml'; import { AnnotatedEV5Transaction, SubmissionStrategy, - getChainIdFromTxs, } from '@hyperlane-xyz/sdk'; -import { assert, errorToString } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert, errorToString } from '@hyperlane-xyz/utils'; import { WriteCommandContext } from '../context/types.js'; import { logGray, logRed } from '../logger.js'; @@ -27,17 +26,15 @@ export async function runSubmit({ receiptsFilepath: string; submissionStrategy: SubmissionStrategy; }) { - const { chainMetadata, multiProvider } = context; + const { multiProvider } = context; assert( submissionStrategy, 'Submission strategy required to submit transactions.\nPlease create a submission strategy. See examples in cli/examples/submit/strategy/*.', ); const transactions = getTransactions(transactionsFilepath); - const chainId = getChainIdFromTxs(transactions); - const protocol = chainMetadata[chainId].protocol; - const submitterBuilder = await getSubmitterBuilder({ + const submitterBuilder = await getSubmitterBuilder({ submissionStrategy, multiProvider, }); diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index e8175e14833..afd586646a0 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -1,12 +1,12 @@ import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; -import { DeployedCoreAddresses } from '@hyperlane-xyz/sdk'; import { ChainMap, ChainName, ContractVerifier, CoreConfig, + DeployedCoreAddresses, EvmCoreModule, ExplorerLicenseType, } from '@hyperlane-xyz/sdk'; diff --git a/typescript/cli/src/deploy/dry-run.ts b/typescript/cli/src/deploy/dry-run.ts index f821dc179f8..fa0c3097252 100644 --- a/typescript/cli/src/deploy/dry-run.ts +++ b/typescript/cli/src/deploy/dry-run.ts @@ -5,12 +5,11 @@ import { resetFork, setFork, } from '@hyperlane-xyz/sdk'; +import { toUpperCamelCase } from '@hyperlane-xyz/utils'; import { logGray, logGreen, warnYellow } from '../logger.js'; import { ENV } from '../utils/env.js'; -import { toUpperCamelCase } from './utils.js'; - /** * Forks a provided network onto MultiProvider * @param multiProvider the MultiProvider to be prepared diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index e5dc8e0857d..f5ac01a1752 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -177,10 +177,6 @@ export async function completeDeploy( if (isDryRun) await completeDryRun(command); } -export function toUpperCamelCase(string: string) { - return string.charAt(0).toUpperCase() + string.slice(1); -} - function transformChainMetadataForDisplay(chainMetadata: ChainMetadata) { return { Name: chainMetadata.name, diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 9e8d7268eed..a7827b5dcaf 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -136,13 +136,18 @@ export async function runWarpRouteDeploy({ await runDeployPlanStep(deploymentParams); + // Some of the below functions throw if passed non-EVM chains + const ethereumChains = chains.filter( + (chain) => chainMetadata[chain].protocol === ProtocolType.Ethereum, + ); + await runPreflightChecksForChains({ context, - chains, + chains: ethereumChains, minGas: MINIMUM_WARP_DEPLOY_GAS, }); - const initialBalances = await prepareDeploy(context, null, chains); + const initialBalances = await prepareDeploy(context, null, ethereumChains); const deployedContracts = await executeDeploy(deploymentParams, apiKeys); @@ -153,7 +158,7 @@ export async function runWarpRouteDeploy({ await writeDeploymentArtifacts(warpCoreConfig, context); - await completeDeploy(context, 'warp', initialBalances, null, chains!); + await completeDeploy(context, 'warp', initialBalances, null, ethereumChains!); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { @@ -532,7 +537,8 @@ async function updateExistingWarpRoute( ) { logBlue('Updating deployed Warp Routes'); const { multiProvider, registry } = params.context; - const addresses = await registry.getAddresses(); + const registryAddresses = + (await registry.getAddresses()) as ChainMap; const contractVerifier = new ContractVerifier( multiProvider, apiKeys, @@ -551,15 +557,13 @@ async function updateExistingWarpRoute( `Missing artifacts for ${chain}. Probably new deployment. Skipping update...`, ); - config.ismFactoryAddresses = addresses[ - chain - ] as ProxyFactoryFactoriesAddresses; const evmERC20WarpModule = new EvmERC20WarpModule( multiProvider, { config, chain, addresses: { + ...registryAddresses[chain], deployedTokenRoute: deployedConfig.addressOrDenom!, }, }, @@ -644,7 +648,9 @@ async function enrollRemoteRouters( deployedContractsMap: HyperlaneContractsMap, ): Promise { logBlue(`Enrolling deployed routers with each other...`); - const { multiProvider } = params.context; + const { multiProvider, registry } = params.context; + const registryAddresses = + (await registry.getAddresses()) as ChainMap; const deployedRoutersAddresses: ChainMap
= objMap( deployedContractsMap, (_, contracts) => getRouter(contracts).address, @@ -672,7 +678,10 @@ async function enrollRemoteRouters( const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { config: mutatedWarpRouteConfig, chain, - addresses: { deployedTokenRoute: router.address }, + addresses: { + ...registryAddresses[chain], + deployedTokenRoute: router.address, + }, }); const otherChains = multiProvider @@ -935,7 +944,7 @@ async function getWarpApplySubmitter({ context: WriteCommandContext; strategyUrl?: string; }): Promise> { - const { chainMetadata, multiProvider } = context; + const { multiProvider } = context; const submissionStrategy: SubmissionStrategy = strategyUrl ? readChainSubmissionStrategy(strategyUrl)[chain] @@ -946,8 +955,7 @@ async function getWarpApplySubmitter({ }, }; - const protocol = chainMetadata[chain].protocol; - return getSubmitterBuilder({ + return getSubmitterBuilder({ submissionStrategy, multiProvider, }); diff --git a/typescript/cli/src/read/warp.ts b/typescript/cli/src/read/warp.ts index 3cd6c193526..169593c5e94 100644 --- a/typescript/cli/src/read/warp.ts +++ b/typescript/cli/src/read/warp.ts @@ -15,7 +15,7 @@ import { isAddressEvm, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { logGray, logRed, logTable } from '../logger.js'; -import { getWarpCoreConfigOrExit } from '../utils/input.js'; +import { getWarpCoreConfigOrExit } from '../utils/warp.js'; export async function runWarpRouteRead({ context, diff --git a/typescript/cli/src/status/message.ts b/typescript/cli/src/status/message.ts index 2c1e9af96c8..df3ff1d9832 100644 --- a/typescript/cli/src/status/message.ts +++ b/typescript/cli/src/status/message.ts @@ -52,7 +52,7 @@ export async function checkMessageStatus({ } else { try { dispatchedReceipt = await core.getDispatchTx(origin, messageId); - } catch (e) { + } catch { logRed(`Failed to infer dispatch transaction for message ${messageId}`); dispatchTx = await input({ diff --git a/typescript/cli/src/utils/balances.ts b/typescript/cli/src/utils/balances.ts index 10df193b9c5..2a6e6fcb8a6 100644 --- a/typescript/cli/src/utils/balances.ts +++ b/typescript/cli/src/utils/balances.ts @@ -2,8 +2,9 @@ import { confirm } from '@inquirer/prompts'; import { ethers } from 'ethers'; import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; -import { logGreen, logRed } from '../logger.js'; +import { logGray, logGreen, logRed } from '../logger.js'; export async function nativeBalancesAreSufficient( multiProvider: MultiProvider, @@ -12,6 +13,11 @@ export async function nativeBalancesAreSufficient( ) { const sufficientBalances: boolean[] = []; for (const chain of chains) { + // Only Ethereum chains are supported + if (multiProvider.getProtocol(chain) !== ProtocolType.Ethereum) { + logGray(`Skipping balance check for non-EVM chain: ${chain}`); + continue; + } const address = multiProvider.getSigner(chain).getAddress(); const provider = multiProvider.getProvider(chain); const gasPrice = await provider.getGasPrice(); diff --git a/typescript/cli/src/utils/env.ts b/typescript/cli/src/utils/env.ts index 51ab1ce3522..a0a5b232f12 100644 --- a/typescript/cli/src/utils/env.ts +++ b/typescript/cli/src/utils/env.ts @@ -1,4 +1,4 @@ -import z from 'zod'; +import { z } from 'zod'; const envScheme = z.object({ HYP_KEY: z.string().optional(), diff --git a/typescript/cli/src/utils/files.ts b/typescript/cli/src/utils/files.ts index 9c7cb6216e9..50f56d1b779 100644 --- a/typescript/cli/src/utils/files.ts +++ b/typescript/cli/src/utils/files.ts @@ -42,7 +42,7 @@ export function isFile(filepath: string) { if (!filepath) return false; try { return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile(); - } catch (error) { + } catch { log(`Error checking for file: ${filepath}`); return false; } @@ -70,7 +70,7 @@ export function readJson(filepath: string): T { export function tryReadJson(filepath: string): T | null { try { return readJson(filepath) as T; - } catch (error) { + } catch { return null; } } @@ -98,7 +98,7 @@ export function readYaml(filepath: string): T { export function tryReadYamlAtPath(filepath: string): T | null { try { return readYaml(filepath); - } catch (error) { + } catch { return null; } } diff --git a/typescript/cli/src/utils/input.ts b/typescript/cli/src/utils/input.ts index 2ccef32db89..eb197c1acdc 100644 --- a/typescript/cli/src/utils/input.ts +++ b/typescript/cli/src/utils/input.ts @@ -14,24 +14,18 @@ import { } from '@inquirer/core'; import figures from '@inquirer/figures'; import { KeypressEvent, confirm, input, isSpaceKey } from '@inquirer/prompts'; -import type { PartialDeep } from '@inquirer/type'; +import type { PartialDeep, Prompt } from '@inquirer/type'; import ansiEscapes from 'ansi-escapes'; import chalk from 'chalk'; import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; -import { - ChainName, - DeployedOwnableConfig, - WarpCoreConfig, -} from '@hyperlane-xyz/sdk'; +import { ChainName, DeployedOwnableConfig } from '@hyperlane-xyz/sdk'; import { Address, isAddress, rootLogger } from '@hyperlane-xyz/utils'; -import { readWarpCoreConfig } from '../config/warp.js'; import { CommandContext } from '../context/types.js'; -import { logGray, logRed } from '../logger.js'; +import { logGray } from '../logger.js'; import { indentYamlOrJson } from './files.js'; -import { selectRegistryWarpRoute } from './tokens.js'; export async function detectAndConfirmOrPrompt( detect: () => Promise, @@ -52,8 +46,9 @@ export async function detectAndConfirmOrPrompt( return detectedValue; } } - // eslint-disable-next-line no-empty - } catch (e) {} + } catch { + // Fallback to input prompt + } return input({ message: `${prompt} ${label}:`, default: detectedValue }); } @@ -136,34 +131,6 @@ export async function setProxyAdminConfig( } } -/** - * Gets a {@link WarpCoreConfig} based on the provided path or prompts the user to choose one: - * - if `symbol` is provided the user will have to select one of the available warp routes. - * - if `warp` is provided the config will be read by the provided file path. - * - if none is provided the CLI will exit. - */ -export async function getWarpCoreConfigOrExit({ - context, - symbol, - warp, -}: { - context: CommandContext; - symbol?: string; - warp?: string; -}): Promise { - let warpCoreConfig: WarpCoreConfig; - if (symbol) { - warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol); - } else if (warp) { - warpCoreConfig = readWarpCoreConfig(warp); - } else { - logRed(`Please specify either a symbol or warp config`); - process.exit(0); - } - - return warpCoreConfig; -} - /** * Searchable checkbox code * @@ -497,7 +464,10 @@ function isDownKey(key: KeypressEvent): boolean { return key.name === 'down'; } -export const searchableCheckBox = createPrompt( +export const searchableCheckBox: Prompt< + any, + SearchableCheckboxConfig +> = createPrompt( ( config: SearchableCheckboxConfig, done: (value: Array) => void, diff --git a/typescript/cli/src/utils/warp.ts b/typescript/cli/src/utils/warp.ts new file mode 100644 index 00000000000..08888628e5d --- /dev/null +++ b/typescript/cli/src/utils/warp.ts @@ -0,0 +1,35 @@ +import { WarpCoreConfig } from '@hyperlane-xyz/sdk'; + +import { readWarpCoreConfig } from '../config/warp.js'; +import { CommandContext } from '../context/types.js'; +import { logRed } from '../logger.js'; + +import { selectRegistryWarpRoute } from './tokens.js'; + +/** + * Gets a {@link WarpCoreConfig} based on the provided path or prompts the user to choose one: + * - if `symbol` is provided the user will have to select one of the available warp routes. + * - if `warp` is provided the config will be read by the provided file path. + * - if none is provided the CLI will exit. + */ +export async function getWarpCoreConfigOrExit({ + context, + symbol, + warp, +}: { + context: CommandContext; + symbol?: string; + warp?: string; +}): Promise { + let warpCoreConfig: WarpCoreConfig; + if (symbol) { + warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol); + } else if (warp) { + warpCoreConfig = readWarpCoreConfig(warp); + } else { + logRed(`Please specify either a symbol or warp config`); + process.exit(0); + } + + return warpCoreConfig; +} diff --git a/typescript/cli/src/validator/preFlightCheck.ts b/typescript/cli/src/validator/preFlightCheck.ts index ca0c4d8504a..ba674b48b9d 100644 --- a/typescript/cli/src/validator/preFlightCheck.ts +++ b/typescript/cli/src/validator/preFlightCheck.ts @@ -44,7 +44,7 @@ export const checkValidatorSetup = async ( try { validatorStorageLocations = await validatorAnnounce.getAnnouncedStorageLocations(validatorsArray); - } catch (e) { + } catch { errorSet.add('Failed to read announced storage locations on chain.'); } @@ -64,7 +64,7 @@ export const checkValidatorSetup = async ( let s3Validator: S3Validator; try { s3Validator = await S3Validator.fromStorageLocation(s3StorageLocation); - } catch (e) { + } catch { errorRed( `❌ Failed to fetch storage locations for validator ${validator}, this may be due to the storage location not being an S3 bucket\n\n`, ); diff --git a/typescript/cli/src/version.ts b/typescript/cli/src/version.ts index 82bc6e87d8f..e882c3c6fe4 100644 --- a/typescript/cli/src/version.ts +++ b/typescript/cli/src/version.ts @@ -1 +1 @@ -export const VERSION = '7.0.0'; +export const VERSION = '7.1.0'; diff --git a/typescript/github-proxy/CHANGELOG.md b/typescript/github-proxy/CHANGELOG.md index fc84e244648..a522bb6d5ca 100644 --- a/typescript/github-proxy/CHANGELOG.md +++ b/typescript/github-proxy/CHANGELOG.md @@ -1,5 +1,7 @@ # @hyperlane-xyz/github-proxy +## 7.1.0 + ## 7.0.0 ## 6.0.0 diff --git a/typescript/github-proxy/package.json b/typescript/github-proxy/package.json index 34ff9cdb522..b6604be0ee0 100644 --- a/typescript/github-proxy/package.json +++ b/typescript/github-proxy/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/github-proxy", "description": "Github proxy that adds the API key to requests", - "version": "7.0.0", + "version": "7.1.0", "private": true, "scripts": { "deploy": "wrangler deploy", @@ -27,7 +27,7 @@ "@cloudflare/vitest-pool-workers": "^0.4.5", "@cloudflare/workers-types": "^4.20240821.1", "@faker-js/faker": "^8.4.1", - "chai": "4.5.0", + "chai": "^4.5.0", "prettier": "^2.8.8", "typescript": "5.3.3", "vitest": "1.4.0", diff --git a/typescript/helloworld/.eslintignore b/typescript/helloworld/.eslintignore deleted file mode 100644 index d461f0fa89e..00000000000 --- a/typescript/helloworld/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -dist -coverage -src/types -hardhat.config.ts \ No newline at end of file diff --git a/typescript/helloworld/.eslintrc b/typescript/helloworld/.eslintrc deleted file mode 100644 index 446616f52f7..00000000000 --- a/typescript/helloworld/.eslintrc +++ /dev/null @@ -1,39 +0,0 @@ -{ - "env": { - "node": true, - "browser": true, - "es2021": true - }, - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module", - "project": "./tsconfig.json" - }, - "plugins": ["@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "rules": { - "no-eval": ["error"], - "no-ex-assign": ["error"], - "no-constant-condition": ["off"], - "@typescript-eslint/ban-ts-comment": ["off"], - "@typescript-eslint/explicit-module-boundary-types": ["off"], - "@typescript-eslint/no-explicit-any": ["off"], - "@typescript-eslint/no-floating-promises": ["error"], - "@typescript-eslint/no-non-null-assertion": ["off"], - "@typescript-eslint/no-require-imports": ["warn"], - "@typescript-eslint/no-unused-vars": [ - "error", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } - ] - } -} diff --git a/typescript/helloworld/CHANGELOG.md b/typescript/helloworld/CHANGELOG.md index 7b34ab8a5ce..fe9c0b3da11 100644 --- a/typescript/helloworld/CHANGELOG.md +++ b/typescript/helloworld/CHANGELOG.md @@ -1,5 +1,18 @@ # @hyperlane-xyz/helloworld +## 7.1.0 + +### Patch Changes + +- Updated dependencies [6f2d50fbd] +- Updated dependencies [1159e0f4b] +- Updated dependencies [ff2b4e2fb] +- Updated dependencies [0e285a443] +- Updated dependencies [5db46bd31] +- Updated dependencies [0cd65c571] + - @hyperlane-xyz/sdk@7.1.0 + - @hyperlane-xyz/core@5.8.1 + ## 7.0.0 ### Patch Changes diff --git a/typescript/helloworld/eslint.config.mjs b/typescript/helloworld/eslint.config.mjs new file mode 100644 index 00000000000..f88d2081577 --- /dev/null +++ b/typescript/helloworld/eslint.config.mjs @@ -0,0 +1,17 @@ +import MonorepoDefaults from '../../eslint.config.mjs'; + +export default [ + ...MonorepoDefaults, + { + files: ['./src/**/*.ts'], + }, + { + ignores: ["**/src/types/*"], + }, + { + ignores: ['./src/scripts'], + rules: { + 'no-console': ['off'], + }, + }, +]; \ No newline at end of file diff --git a/typescript/helloworld/package.json b/typescript/helloworld/package.json index 3d3307a672b..96479537071 100644 --- a/typescript/helloworld/package.json +++ b/typescript/helloworld/package.json @@ -1,26 +1,29 @@ { "name": "@hyperlane-xyz/helloworld", "description": "A basic skeleton of an Hyperlane app", - "version": "7.0.0", + "version": "7.1.0", "dependencies": { - "@hyperlane-xyz/core": "5.8.0", - "@hyperlane-xyz/registry": "4.7.0", - "@hyperlane-xyz/sdk": "7.0.0", + "@hyperlane-xyz/core": "5.8.1", + "@hyperlane-xyz/registry": "6.1.0", + "@hyperlane-xyz/sdk": "7.1.0", "@openzeppelin/contracts-upgradeable": "^4.9.3", "ethers": "^5.7.2" }, "devDependencies": { + "@eslint/js": "^9.15.0", "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-waffle": "^2.0.6", "@trivago/prettier-plugin-sort-imports": "^4.2.1", "@typechain/ethers-v5": "^11.1.2", "@typechain/ethers-v6": "^0.5.1", "@typechain/hardhat": "^9.1.0", - "@typescript-eslint/eslint-plugin": "^7.4.0", - "@typescript-eslint/parser": "^7.4.0", - "chai": "4.5.0", - "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.1.6", + "@typescript-eslint/parser": "^8.1.6", + "chai": "^4.5.0", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", "ethereum-waffle": "^4.0.10", "hardhat": "^2.22.2", "hardhat-gas-reporter": "^1.0.9", @@ -56,7 +59,9 @@ "build": "yarn hardhat-esm compile && tsc", "clean": "yarn hardhat-esm clean && rm -rf dist cache src/types", "coverage": "yarn hardhat-esm coverage", - "lint": "solhint contracts/**/*.sol && eslint . --ext .ts", + "lint": "yarn lint:sol && yarn lint:ts", + "lint:sol": "solhint contracts/**/*.sol", + "lint:ts": "eslint -c ./eslint.config.mjs", "hardhat-esm": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only --no-warnings=ExperimentalWarning' hardhat --config hardhat.config.cts", "prettier": "prettier --write ./contracts ./src", "test": "yarn hardhat-esm test ./src/test/**/*.test.ts", diff --git a/typescript/infra/CHANGELOG.md b/typescript/infra/CHANGELOG.md index 08389aa78a3..d533e66b324 100644 --- a/typescript/infra/CHANGELOG.md +++ b/typescript/infra/CHANGELOG.md @@ -1,5 +1,24 @@ # @hyperlane-xyz/infra +## 7.1.0 + +### Minor Changes + +- 5db46bd31: Implements persistent relayer for use in CLI + +### Patch Changes + +- Updated dependencies [6f2d50fbd] +- Updated dependencies [1159e0f4b] +- Updated dependencies [0e285a443] +- Updated dependencies [ff2b4e2fb] +- Updated dependencies [0e285a443] +- Updated dependencies [5db46bd31] +- Updated dependencies [0cd65c571] + - @hyperlane-xyz/sdk@7.1.0 + - @hyperlane-xyz/utils@7.1.0 + - @hyperlane-xyz/helloworld@7.1.0 + ## 7.0.0 ### Minor Changes diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 8a2ecd7a1f3..46ba1a4d7cc 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -12,6 +12,7 @@ import { import { MetricAppContext, routerMatchingList, + senderMatchingList, warpRouteMatchingList, } from '../../../src/config/agent/relayer.js'; import { ALL_KEY_ROLES, Role } from '../../../src/roles.js'; @@ -20,14 +21,17 @@ import { getDomainId } from '../../registry.js'; import { environment } from './chains.js'; import { helloWorld } from './helloworld.js'; +import aaveSenderAddresses from './misc-artifacts/aave-sender-addresses.json'; +import merklyEthAddresses from './misc-artifacts/merkly-eth-addresses.json'; +import merklyNftAddresses from './misc-artifacts/merkly-eth-addresses.json'; +import merklyErc20Addresses from './misc-artifacts/merkly-eth-addresses.json'; +import veloMessageModuleAddresses from './misc-artifacts/velo-message-module-addresses.json'; +import veloTokenBridgeAddresses from './misc-artifacts/velo-token-bridge-addresses.json'; import { mainnet3SupportedChainNames, supportedChainNames, } from './supportedChainNames.js'; import { validatorChainConfig } from './validators.js'; -import merklyEthAddresses from './warp/artifacts/merkly-eth-addresses.json'; -import merklyNftAddresses from './warp/artifacts/merkly-eth-addresses.json'; -import merklyErc20Addresses from './warp/artifacts/merkly-eth-addresses.json'; import { WarpRouteIds } from './warp/warpIds.js'; // const releaseCandidateHelloworldMatchingList = routerMatchingList( @@ -60,7 +64,9 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< bitlayer: true, blast: true, bob: true, + boba: true, bsc: true, + bsquared: true, celo: true, cheesechain: true, chilizmainnet: true, @@ -68,6 +74,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< cyber: true, degenchain: true, dogechain: true, + duckchain: true, eclipsemainnet: true, endurance: true, ethereum: true, @@ -120,9 +127,12 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< snaxchain: true, solanamainnet: true, stride: false, + superseed: true, superpositionmainnet: true, taiko: true, tangle: true, + unichain: true, + vana: true, viction: true, worldchain: true, xai: true, @@ -148,7 +158,9 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< bitlayer: true, blast: true, bob: true, + boba: true, bsc: true, + bsquared: true, celo: true, cheesechain: true, chilizmainnet: true, @@ -156,6 +168,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< cyber: true, degenchain: true, dogechain: true, + duckchain: true, eclipsemainnet: true, endurance: true, ethereum: true, @@ -209,9 +222,12 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< snaxchain: true, solanamainnet: true, stride: true, + superseed: true, superpositionmainnet: true, taiko: true, tangle: true, + unichain: true, + vana: true, viction: true, worldchain: true, xai: true, @@ -237,7 +253,9 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< bitlayer: true, blast: true, bob: true, + boba: true, bsc: true, + bsquared: true, celo: true, cheesechain: true, chilizmainnet: true, @@ -245,6 +263,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< cyber: true, degenchain: true, dogechain: true, + duckchain: true, // Cannot scrape Sealevel chains eclipsemainnet: false, endurance: true, @@ -299,9 +318,12 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< // Cannot scrape Sealevel chains solanamainnet: false, stride: true, + superseed: true, superpositionmainnet: true, taiko: true, tangle: true, + unichain: true, + vana: true, // Has RPC non-compliance that breaks scraping. viction: false, worldchain: true, @@ -375,6 +397,22 @@ const metricAppContextsGetter = (): MetricAppContext[] => { name: 'merkly_nft', matchingList: routerMatchingList(merklyNftAddresses), }, + { + name: 'velo_message_module', + matchingList: routerMatchingList(veloMessageModuleAddresses), + }, + { + name: 'velo_token_bridge', + matchingList: routerMatchingList(veloTokenBridgeAddresses), + }, + { + // https://github.com/bgd-labs/aave-delivery-infrastructure?tab=readme-ov-file#deployed-addresses + // We match on senders because the sender is always the same and + // well documented, while the recipient may be switched out and is + // more poorly documented. + name: 'aave', + matchingList: senderMatchingList(aaveSenderAddresses), + }, ]; }; @@ -409,7 +447,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '25a927d-20241114-171323', + tag: 'd834d81-20241125-135658', }, gasPaymentEnforcement: gasPaymentEnforcement, metricAppContextsGetter, @@ -418,7 +456,7 @@ const hyperlane: RootAgentConfig = { validators: { docker: { repo, - tag: '75d62ae-20241107-060707', + tag: 'd834d81-20241125-135658', }, rpcConsensusType: RpcConsensusType.Quorum, chains: validatorChainConfig(Contexts.Hyperlane), @@ -428,7 +466,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '75d62ae-20241107-060707', + tag: 'd834d81-20241125-135658', }, resources: scraperResources, }, diff --git a/typescript/infra/config/environments/mainnet3/aw-validators/hyperlane.json b/typescript/infra/config/environments/mainnet3/aw-validators/hyperlane.json index fddbdc762eb..c6946b97224 100644 --- a/typescript/infra/config/environments/mainnet3/aw-validators/hyperlane.json +++ b/typescript/infra/config/environments/mainnet3/aw-validators/hyperlane.json @@ -53,6 +53,9 @@ "bob": { "validators": ["0x20f283be1eb0e81e22f51705dcb79883cfdd34aa"] }, + "boba": { + "validators": ["0xebeb92c94ca8408e73aa16fd554cb3a7df075c59"] + }, "bsc": { "validators": [ "0x570af9b7b36568c8877eebba6c6727aa9dab7268", @@ -60,6 +63,9 @@ "0x03047213365800f065356b4a2fe97c3c3a52296a" ] }, + "bsquared": { + "validators": ["0xcadc90933c9fbe843358a4e70e46ad2db78e28aa"] + }, "celo": { "validators": [ "0x63478422679303c3e4fc611b771fa4a707ef7f4a", @@ -85,6 +91,9 @@ "dogechain": { "validators": ["0xe43f742c37858746e6d7e458bc591180d0cba440"] }, + "duckchain": { + "validators": ["0x91d55fe6dac596a6735d96365e21ce4bca21d83c"] + }, "eclipsemainnet": { "validators": ["0xebb52d7eaa3ff7a5a6260bfe5111ce52d57401d0"] }, @@ -278,6 +287,9 @@ "solanamainnet": { "validators": ["0x28464752829b3ea59a497fca0bdff575c534c3ff"] }, + "superseed": { + "validators": ["0xdc2b87cb555411bb138d3a4e5f7832c87fae2b88"] + }, "superpositionmainnet": { "validators": ["0x3f489acdd341c6b4dd86293fa2cc5ecc8ccf4f84"] }, @@ -287,6 +299,12 @@ "tangle": { "validators": ["0x1ee52cbbfacd7dcb0ba4e91efaa6fbc61602b15b"] }, + "unichain": { + "validators": ["0x9773a382342ebf604a2e5de0a1f462fb499e28b1"] + }, + "vana": { + "validators": ["0xfdf3b0dfd4b822d10cacb15c8ae945ea269e7534"] + }, "viction": { "validators": ["0x1f87c368f8e05a85ef9126d984a980a20930cb9c"] }, diff --git a/typescript/infra/config/environments/mainnet3/core/verification.json b/typescript/infra/config/environments/mainnet3/core/verification.json index 0a9ab38bb52..62ee7d5f81a 100644 --- a/typescript/infra/config/environments/mainnet3/core/verification.json +++ b/typescript/infra/config/environments/mainnet3/core/verification.json @@ -5376,5 +5376,425 @@ "constructorArguments": "00000000000000000000000096d51cc3f7500d501baeb1a2a62bb96fa03532f8", "isProxy": false } + ], + "boba": [ + { + "name": "ProxyAdmin", + "address": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "Mailbox", + "address": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", + "constructorArguments": "0000000000000000000000000000000000000000000000000000000000000120", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "constructorArguments": "000000000000000000000000ea87ae93fa0019a82a727bfd3ebd1cfca8f64f1d0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D" + }, + { + "name": "MerkleTreeHook", + "address": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "FallbackRoutingHook", + "address": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba0000000000000000000000009eaac366bfd70430cfee6e70265fefff1cfc9e47", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "constructorArguments": "00000000000000000000000083475ca5beb2eaa59a2ff48a0544ebaa4a32c2de0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de" + }, + { + "name": "ProtocolFee", + "address": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "constructorArguments": "000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba", + "isProxy": false + }, + { + "name": "ValidatorAnnounce", + "address": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + } + ], + "superseed": [ + { + "name": "ProxyAdmin", + "address": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "Mailbox", + "address": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", + "constructorArguments": "00000000000000000000000000000000000000000000000000000000000014d2", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "constructorArguments": "000000000000000000000000ea87ae93fa0019a82a727bfd3ebd1cfca8f64f1d0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D" + }, + { + "name": "MerkleTreeHook", + "address": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "FallbackRoutingHook", + "address": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba0000000000000000000000009eaac366bfd70430cfee6e70265fefff1cfc9e47", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "constructorArguments": "00000000000000000000000083475ca5beb2eaa59a2ff48a0544ebaa4a32c2de0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de" + }, + { + "name": "ProtocolFee", + "address": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "constructorArguments": "000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba", + "isProxy": false + }, + { + "name": "ValidatorAnnounce", + "address": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + } + ], + "unichain": [ + { + "name": "ProxyAdmin", + "address": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "Mailbox", + "address": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", + "constructorArguments": "0000000000000000000000000000000000000000000000000000000000000082", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "constructorArguments": "000000000000000000000000ea87ae93fa0019a82a727bfd3ebd1cfca8f64f1d0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D" + }, + { + "name": "MerkleTreeHook", + "address": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "FallbackRoutingHook", + "address": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba0000000000000000000000009eaac366bfd70430cfee6e70265fefff1cfc9e47", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "constructorArguments": "00000000000000000000000083475ca5beb2eaa59a2ff48a0544ebaa4a32c2de0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de" + }, + { + "name": "ProtocolFee", + "address": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "constructorArguments": "000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba", + "isProxy": false + }, + { + "name": "ValidatorAnnounce", + "address": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + } + ], + "duckchain": [ + { + "name": "ProxyAdmin", + "address": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "Mailbox", + "address": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", + "constructorArguments": "00000000000000000000000000000000000000000000000000000000000015a9", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "constructorArguments": "000000000000000000000000ea87ae93fa0019a82a727bfd3ebd1cfca8f64f1d0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D" + }, + { + "name": "MerkleTreeHook", + "address": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "FallbackRoutingHook", + "address": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba0000000000000000000000009eaac366bfd70430cfee6e70265fefff1cfc9e47", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "constructorArguments": "00000000000000000000000083475ca5beb2eaa59a2ff48a0544ebaa4a32c2de0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de" + }, + { + "name": "ProtocolFee", + "address": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "constructorArguments": "000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba", + "isProxy": false + }, + { + "name": "ValidatorAnnounce", + "address": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + } + ], + "vana": [ + { + "name": "ProxyAdmin", + "address": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "Mailbox", + "address": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", + "constructorArguments": "00000000000000000000000000000000000000000000000000000000000005c8", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "constructorArguments": "000000000000000000000000ea87ae93fa0019a82a727bfd3ebd1cfca8f64f1d0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D" + }, + { + "name": "MerkleTreeHook", + "address": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "FallbackRoutingHook", + "address": "0x0D3bD9F1bcDA82bD1682b2C895a907d7aaE45849", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba0000000000000000000000009eaac366bfd70430cfee6e70265fefff1cfc9e47", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", + "constructorArguments": "00000000000000000000000083475ca5beb2eaa59a2ff48a0544ebaa4a32c2de0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de" + }, + { + "name": "ProtocolFee", + "address": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", + "constructorArguments": "000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba", + "isProxy": false + }, + { + "name": "ValidatorAnnounce", + "address": "0xD743801ABB6c7664B623D8534C0f5AF8cD2F1C5e", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + } + ], + "bsquared": [ + { + "name": "ProxyAdmin", + "address": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "Mailbox", + "address": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", + "constructorArguments": "00000000000000000000000000000000000000000000000000000000000000df", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", + "constructorArguments": "000000000000000000000000ea87ae93fa0019a82a727bfd3ebd1cfca8f64f1d0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D" + }, + { + "name": "MerkleTreeHook", + "address": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "FallbackRoutingHook", + "address": "0x60bB6D060393D3C206719A7bD61844cC82891cfB", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000bb0ae51bca526cf313b6a95bfab020794af6c394", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0x451dF8AB0936D85526D816f0b4dCaDD934A034A4", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x81EbEdfc1220BE33C3B9c5E09c1FCab849a392A6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x70EbA87Cd15616f32C736B3f3BdCfaeD0713a82B", + "constructorArguments": "00000000000000000000000081ebedfc1220be33c3b9c5e09c1fcab849a392a60000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x81EbEdfc1220BE33C3B9c5E09c1FCab849a392A6" + }, + { + "name": "ProtocolFee", + "address": "0xbB88a31E4b709b645c06825c0E0b5CAC906d97DE", + "constructorArguments": "000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba", + "isProxy": false + }, + { + "name": "ValidatorAnnounce", + "address": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + } ] } diff --git a/typescript/infra/config/environments/mainnet3/funding.ts b/typescript/infra/config/environments/mainnet3/funding.ts index 03803fefa26..95ac6e84f84 100644 --- a/typescript/infra/config/environments/mainnet3/funding.ts +++ b/typescript/infra/config/environments/mainnet3/funding.ts @@ -10,7 +10,7 @@ export const keyFunderConfig: KeyFunderConfig< > = { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '1ed620c-20241107-052148', + tag: 'd834d81-20241125-135642', }, // We're currently using the same deployer/key funder key as mainnet2. // To minimize nonce clobbering we offset the key funder cron @@ -40,7 +40,9 @@ export const keyFunderConfig: KeyFunderConfig< bitlayer: '0.002', blast: '0.2', bob: '0.2', + boba: '0.05', bsc: '5', + bsquared: '0.002', celo: '3', cheesechain: '50', chilizmainnet: '200', @@ -48,6 +50,7 @@ export const keyFunderConfig: KeyFunderConfig< cyber: '0.05', degenchain: '100', dogechain: '100', + duckchain: '5', endurance: '20', ethereum: '0.5', everclear: '0.05', @@ -96,9 +99,13 @@ export const keyFunderConfig: KeyFunderConfig< snaxchain: '0.05', // ignore non-evm chains stride: '0', + superseed: '0.05', superpositionmainnet: '0.05', taiko: '0.2', tangle: '2', + unichain: '0.05', + // temporarily low until we're able to fund more + vana: '0.001', viction: '3', worldchain: '0.2', xai: '20', diff --git a/typescript/infra/config/environments/mainnet3/gasPrices.json b/typescript/infra/config/environments/mainnet3/gasPrices.json index e0eb0309331..45daad3a3ee 100644 --- a/typescript/infra/config/environments/mainnet3/gasPrices.json +++ b/typescript/infra/config/environments/mainnet3/gasPrices.json @@ -12,7 +12,7 @@ "decimals": 9 }, "arbitrum": { - "amount": "0.01", + "amount": "0.123544", "decimals": 9 }, "arbitrumnova": { @@ -24,7 +24,7 @@ "decimals": 9 }, "astarzkevm": { - "amount": "0.078", + "amount": "0.24", "decimals": 9 }, "flame": { @@ -36,11 +36,11 @@ "decimals": 9 }, "b3": { - "amount": "0.001000254", + "amount": "0.001000252", "decimals": 9 }, "base": { - "amount": "0.004893247", + "amount": "0.024525952", "decimals": 9 }, "bitlayer": { @@ -48,17 +48,25 @@ "decimals": 9 }, "blast": { - "amount": "0.005018176", + "amount": "0.005892268", "decimals": 9 }, "bob": { "amount": "0.001000252", "decimals": 9 }, + "boba": { + "amount": "0.001000047", + "decimals": 9 + }, "bsc": { "amount": "1.0", "decimals": 9 }, + "bsquared": { + "amount": "0.001000252", + "decimals": 9 + }, "celo": { "amount": "10.0", "decimals": 9 @@ -87,6 +95,10 @@ "amount": "250.0", "decimals": 9 }, + "duckchain": { + "amount": "10.0", + "decimals": 9 + }, "eclipsemainnet": { "amount": "0.0000001", "decimals": 1 @@ -96,7 +108,7 @@ "decimals": 9 }, "ethereum": { - "amount": "15.0", + "amount": "30.088451558", "decimals": 9 }, "everclear": { @@ -124,7 +136,7 @@ "decimals": 9 }, "gnosis": { - "amount": "1.500000007", + "amount": "1.500000008", "decimals": 9 }, "gravity": { @@ -136,7 +148,7 @@ "decimals": 9 }, "immutablezkevmmainnet": { - "amount": "10.000000056", + "amount": "10.00000005", "decimals": 9 }, "inevm": { @@ -156,7 +168,7 @@ "decimals": 9 }, "linea": { - "amount": "0.160485013", + "amount": "0.548523195", "decimals": 9 }, "lisk": { @@ -172,7 +184,7 @@ "decimals": 9 }, "mantapacific": { - "amount": "0.003001158", + "amount": "0.0030005", "decimals": 9 }, "mantle": { @@ -208,7 +220,7 @@ "decimals": 9 }, "morph": { - "amount": "0.002", + "amount": "0.014614332", "decimals": 9 }, "neutron": { @@ -220,7 +232,7 @@ "decimals": 9 }, "optimism": { - "amount": "0.001010111", + "amount": "0.001065045", "decimals": 9 }, "orderly": { @@ -232,11 +244,11 @@ "decimals": 1 }, "polygon": { - "amount": "47.07124319", + "amount": "260.197309239", "decimals": 9 }, "polygonzkevm": { - "amount": "0.146", + "amount": "0.451", "decimals": 9 }, "polynomialfi": { @@ -244,7 +256,7 @@ "decimals": 9 }, "prom": { - "amount": "13.1", + "amount": "75.2", "decimals": 9 }, "proofofplay": { @@ -280,7 +292,7 @@ "decimals": 9 }, "shibarium": { - "amount": "0.542811448", + "amount": "3.89327567", "decimals": 9 }, "snaxchain": { @@ -295,24 +307,36 @@ "amount": "0.005", "decimals": 1 }, + "superseed": { + "amount": "0.001000252", + "decimals": 9 + }, "superpositionmainnet": { "amount": "0.01", "decimals": 9 }, "taiko": { - "amount": "0.1323", + "amount": "0.1243284", "decimals": 9 }, "tangle": { "amount": "1.0", "decimals": 9 }, + "unichain": { + "amount": "0.001000252", + "decimals": 9 + }, + "vana": { + "amount": "0.001000007", + "decimals": 9 + }, "viction": { "amount": "0.25", "decimals": 9 }, "worldchain": { - "amount": "0.00100025", + "amount": "0.00100026", "decimals": 9 }, "xai": { @@ -320,7 +344,7 @@ "decimals": 9 }, "xlayer": { - "amount": "7.04", + "amount": "23.05", "decimals": 9 }, "zeronetwork": { @@ -340,7 +364,7 @@ "decimals": 9 }, "zoramainnet": { - "amount": "0.001000269", + "amount": "0.001000252", "decimals": 9 } } diff --git a/typescript/infra/config/environments/mainnet3/ism/verification.json b/typescript/infra/config/environments/mainnet3/ism/verification.json index 928bceb8b55..6f9366f4e49 100644 --- a/typescript/infra/config/environments/mainnet3/ism/verification.json +++ b/typescript/infra/config/environments/mainnet3/ism/verification.json @@ -6916,5 +6916,521 @@ "constructorArguments": "", "isProxy": true } + ], + "superseed": [ + { + "name": "StaticMerkleRootMultisigIsmFactory", + "address": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootMultisigIsm", + "address": "0x4725F7b8037513915aAf6D6CBDE2920E28540dDc", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdMultisigIsmFactory", + "address": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdMultisigIsm", + "address": "0xAF03386044373E2fe26C5b1dCedF5a7e854a7a3F", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationIsmFactory", + "address": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationIsm", + "address": "0x882CD0C5D50b6dD74b36Da4BDb059507fddEDdf2", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationHookFactory", + "address": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationHook", + "address": "0x19930232E9aFC4f4F09d09fe2375680fAc2100D0", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "DomainRoutingIsmFactory", + "address": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "DomaingRoutingIsm", + "address": "0x12Ed1BbA182CbC63692F813651BD493B7445C874", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMerkleRootWeightedMultisigIsmFactory", + "address": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootWeightedMultisigIsm", + "address": "0x3b9f24fD2ecfed0d3A88fa7f0E4e5747671981D7", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdWeightedMultisigIsmFactory", + "address": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdWeightedMultisigIsm", + "address": "0x71DCcD21B912F7d4f636af0C9eA5DC0C10617354", + "constructorArguments": "", + "isProxy": true + } + ], + "unichain": [ + { + "name": "StaticMerkleRootMultisigIsmFactory", + "address": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootMultisigIsm", + "address": "0x4725F7b8037513915aAf6D6CBDE2920E28540dDc", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdMultisigIsmFactory", + "address": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdMultisigIsm", + "address": "0xAF03386044373E2fe26C5b1dCedF5a7e854a7a3F", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationIsmFactory", + "address": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationIsm", + "address": "0x882CD0C5D50b6dD74b36Da4BDb059507fddEDdf2", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationHookFactory", + "address": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationHook", + "address": "0x19930232E9aFC4f4F09d09fe2375680fAc2100D0", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "DomainRoutingIsmFactory", + "address": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "DomaingRoutingIsm", + "address": "0x12Ed1BbA182CbC63692F813651BD493B7445C874", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMerkleRootWeightedMultisigIsmFactory", + "address": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootWeightedMultisigIsm", + "address": "0x3b9f24fD2ecfed0d3A88fa7f0E4e5747671981D7", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdWeightedMultisigIsmFactory", + "address": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdWeightedMultisigIsm", + "address": "0x71DCcD21B912F7d4f636af0C9eA5DC0C10617354", + "constructorArguments": "", + "isProxy": true + } + ], + "duckchain": [ + { + "name": "StaticMerkleRootMultisigIsmFactory", + "address": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootMultisigIsm", + "address": "0x4725F7b8037513915aAf6D6CBDE2920E28540dDc", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdMultisigIsmFactory", + "address": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdMultisigIsm", + "address": "0xAF03386044373E2fe26C5b1dCedF5a7e854a7a3F", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationIsmFactory", + "address": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationIsm", + "address": "0x882CD0C5D50b6dD74b36Da4BDb059507fddEDdf2", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationHookFactory", + "address": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationHook", + "address": "0x19930232E9aFC4f4F09d09fe2375680fAc2100D0", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "DomainRoutingIsmFactory", + "address": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "DomaingRoutingIsm", + "address": "0x12Ed1BbA182CbC63692F813651BD493B7445C874", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMerkleRootWeightedMultisigIsmFactory", + "address": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootWeightedMultisigIsm", + "address": "0x3b9f24fD2ecfed0d3A88fa7f0E4e5747671981D7", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdWeightedMultisigIsmFactory", + "address": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdWeightedMultisigIsm", + "address": "0x71DCcD21B912F7d4f636af0C9eA5DC0C10617354", + "constructorArguments": "", + "isProxy": true + } + ], + "vana": [ + { + "name": "StaticMerkleRootMultisigIsmFactory", + "address": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootMultisigIsm", + "address": "0x4725F7b8037513915aAf6D6CBDE2920E28540dDc", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdMultisigIsmFactory", + "address": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdMultisigIsm", + "address": "0xAF03386044373E2fe26C5b1dCedF5a7e854a7a3F", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationIsmFactory", + "address": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationIsm", + "address": "0x882CD0C5D50b6dD74b36Da4BDb059507fddEDdf2", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationHookFactory", + "address": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationHook", + "address": "0x19930232E9aFC4f4F09d09fe2375680fAc2100D0", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "DomainRoutingIsmFactory", + "address": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "DomaingRoutingIsm", + "address": "0x12Ed1BbA182CbC63692F813651BD493B7445C874", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMerkleRootWeightedMultisigIsmFactory", + "address": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootWeightedMultisigIsm", + "address": "0x3b9f24fD2ecfed0d3A88fa7f0E4e5747671981D7", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdWeightedMultisigIsmFactory", + "address": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdWeightedMultisigIsm", + "address": "0x71DCcD21B912F7d4f636af0C9eA5DC0C10617354", + "constructorArguments": "", + "isProxy": true + } + ], + "boba": [ + { + "name": "StaticMerkleRootMultisigIsmFactory", + "address": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootMultisigIsm", + "address": "0x4725F7b8037513915aAf6D6CBDE2920E28540dDc", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdMultisigIsmFactory", + "address": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdMultisigIsm", + "address": "0xAF03386044373E2fe26C5b1dCedF5a7e854a7a3F", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationIsmFactory", + "address": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationIsm", + "address": "0x882CD0C5D50b6dD74b36Da4BDb059507fddEDdf2", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationHookFactory", + "address": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationHook", + "address": "0x19930232E9aFC4f4F09d09fe2375680fAc2100D0", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "DomainRoutingIsmFactory", + "address": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "DomaingRoutingIsm", + "address": "0x12Ed1BbA182CbC63692F813651BD493B7445C874", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMerkleRootWeightedMultisigIsmFactory", + "address": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootWeightedMultisigIsm", + "address": "0x3b9f24fD2ecfed0d3A88fa7f0E4e5747671981D7", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdWeightedMultisigIsmFactory", + "address": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdWeightedMultisigIsm", + "address": "0x71DCcD21B912F7d4f636af0C9eA5DC0C10617354", + "constructorArguments": "", + "isProxy": true + } + ], + "bsquared": [ + { + "name": "StaticMerkleRootMultisigIsmFactory", + "address": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootMultisigIsm", + "address": "0x4725F7b8037513915aAf6D6CBDE2920E28540dDc", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdMultisigIsmFactory", + "address": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdMultisigIsm", + "address": "0xAF03386044373E2fe26C5b1dCedF5a7e854a7a3F", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationIsmFactory", + "address": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationIsm", + "address": "0x882CD0C5D50b6dD74b36Da4BDb059507fddEDdf2", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticAggregationHookFactory", + "address": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationHook", + "address": "0x19930232E9aFC4f4F09d09fe2375680fAc2100D0", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "DomainRoutingIsmFactory", + "address": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "DomaingRoutingIsm", + "address": "0x12Ed1BbA182CbC63692F813651BD493B7445C874", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMerkleRootWeightedMultisigIsmFactory", + "address": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootWeightedMultisigIsm", + "address": "0x3b9f24fD2ecfed0d3A88fa7f0E4e5747671981D7", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "StaticMessageIdWeightedMultisigIsmFactory", + "address": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdWeightedMultisigIsm", + "address": "0x71DCcD21B912F7d4f636af0C9eA5DC0C10617354", + "constructorArguments": "", + "isProxy": true + } ] } diff --git a/typescript/infra/config/environments/mainnet3/middleware/accounts/verification.json b/typescript/infra/config/environments/mainnet3/middleware/accounts/verification.json index b940665f2c2..d6349dce988 100644 --- a/typescript/infra/config/environments/mainnet3/middleware/accounts/verification.json +++ b/typescript/infra/config/environments/mainnet3/middleware/accounts/verification.json @@ -1680,5 +1680,131 @@ "isProxy": true, "expectedimplementation": "0x28846fCb579747E8ddad9E93b55BE51b0A1Bf1f3" } + ], + "superseed": [ + { + "name": "InterchainAccountIsm", + "address": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "InterchainAccountRouter", + "address": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "constructorArguments": "0000000000000000000000000f9d4704e1fb25e416042524e594f1ceac6ff5970000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064c0c53b8b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025eac2007b0d40e3f0af112fd346412321038719000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597" + } + ], + "boba": [ + { + "name": "InterchainAccountIsm", + "address": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "InterchainAccountRouter", + "address": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "constructorArguments": "0000000000000000000000000f9d4704e1fb25e416042524e594f1ceac6ff5970000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064c0c53b8b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025eac2007b0d40e3f0af112fd346412321038719000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597" + } + ], + "unichain": [ + { + "name": "InterchainAccountIsm", + "address": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "InterchainAccountRouter", + "address": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "constructorArguments": "0000000000000000000000000f9d4704e1fb25e416042524e594f1ceac6ff5970000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064c0c53b8b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025eac2007b0d40e3f0af112fd346412321038719000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597" + } + ], + "duckchain": [ + { + "name": "InterchainAccountIsm", + "address": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "InterchainAccountRouter", + "address": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "constructorArguments": "0000000000000000000000000f9d4704e1fb25e416042524e594f1ceac6ff5970000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064c0c53b8b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025eac2007b0d40e3f0af112fd346412321038719000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597" + } + ], + "vana": [ + { + "name": "InterchainAccountIsm", + "address": "0x25EAC2007b0D40E3f0AF112FD346412321038719", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "InterchainAccountRouter", + "address": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "constructorArguments": "0000000000000000000000000f9d4704e1fb25e416042524e594f1ceac6ff5970000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064c0c53b8b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025eac2007b0d40e3f0af112fd346412321038719000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x0F9d4704E1Fb25e416042524e594F1cEac6fF597" + } + ], + "bsquared": [ + { + "name": "InterchainAccountIsm", + "address": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "InterchainAccountRouter", + "address": "0x7CE76f5f0C469bBB4cd7Ea6EbabB54437A093127", + "constructorArguments": "0000000000000000000000003a464f746d23ab22155710f44db16dca53e0775e", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x4D50044335dc1d4D26c343AdeDf6E47808475Deb", + "constructorArguments": "0000000000000000000000007ce76f5f0c469bbb4cd7ea6ebabb54437a0931270000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064c0c53b8b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff26696dcdb6bbfd27e959b847d4f1399d5bcf64000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true, + "expectedimplementation": "0x7CE76f5f0C469bBB4cd7Ea6EbabB54437A093127" + } ] } diff --git a/typescript/infra/config/environments/mainnet3/misc-artifacts/aave-sender-addresses.json b/typescript/infra/config/environments/mainnet3/misc-artifacts/aave-sender-addresses.json new file mode 100644 index 00000000000..21cfa5eccfc --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/misc-artifacts/aave-sender-addresses.json @@ -0,0 +1,38 @@ +{ + "ethereum": { + "sender": "0xEd42a7D8559a463722Ca4beD50E0Cc05a386b0e1" + }, + "polygon": { + "sender": "0xF6B99959F0b5e79E1CC7062E12aF632CEb18eF0d" + }, + "avalanche": { + "sender": "0x27FC7D54C893dA63C0AE6d57e1B2B13A70690928" + }, + "arbitrum": { + "sender": "0xCbFB78a3Eeaa611b826E37c80E4126c8787D29f0" + }, + "optimism": { + "sender": "0x48A9FE90bce5EEd790f3F4Ce192d1C0B351fd4Ca" + }, + "bsc": { + "sender": "0x9d33ee6543C9b2C8c183b8fb58fB089266cffA19" + }, + "base": { + "sender": "0x529467C76f234F2bD359d7ecF7c660A2846b04e2" + }, + "metis": { + "sender": "0x6fDaFb26915ABD6065a1E1501a37Ac438D877f70" + }, + "gnosis": { + "sender": "0x8Dc5310fc9D3D7D1Bb3D1F686899c8F082316c9F" + }, + "scroll": { + "sender": "0x03073D3F4769f6b6604d616238fD6c636C99AD0A" + }, + "polygonzkevm": { + "sender": "0xed7e0874526B9BB9E36C7e9472ed7ed324CEeE3B" + }, + "celo": { + "sender": "0x4A5f4b29C0407E5Feb323305e121f563c7bC4d79" + } +} diff --git a/typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-erc20-addresses.json b/typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-erc20-addresses.json similarity index 100% rename from typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-erc20-addresses.json rename to typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-erc20-addresses.json diff --git a/typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-eth-addresses.json b/typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-eth-addresses.json similarity index 100% rename from typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-eth-addresses.json rename to typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-eth-addresses.json diff --git a/typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-nft-addresses.json b/typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-nft-addresses.json similarity index 100% rename from typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-nft-addresses.json rename to typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-nft-addresses.json diff --git a/typescript/infra/config/environments/mainnet3/misc-artifacts/velo-message-module-addresses.json b/typescript/infra/config/environments/mainnet3/misc-artifacts/velo-message-module-addresses.json new file mode 100644 index 00000000000..9e2cc555b65 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/misc-artifacts/velo-message-module-addresses.json @@ -0,0 +1,14 @@ +{ + "optimism": { + "router": "0xF385603a12Be8b7B885222329c581FDD1C30071D" + }, + "mode": { + "router": "0xF385603a12Be8b7B885222329c581FDD1C30071D" + }, + "lisk": { + "router": "0xF385603a12Be8b7B885222329c581FDD1C30071D" + }, + "fraxtal": { + "router": "0xF385603a12Be8b7B885222329c581FDD1C30071D" + } +} diff --git a/typescript/infra/config/environments/mainnet3/misc-artifacts/velo-token-bridge-addresses.json b/typescript/infra/config/environments/mainnet3/misc-artifacts/velo-token-bridge-addresses.json new file mode 100644 index 00000000000..cd9fe511490 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/misc-artifacts/velo-token-bridge-addresses.json @@ -0,0 +1,14 @@ +{ + "optimism": { + "router": "0xA7287a56C01ac8Baaf8e7B662bDB41b10889C7A6" + }, + "mode": { + "router": "0xA7287a56C01ac8Baaf8e7B662bDB41b10889C7A6" + }, + "lisk": { + "router": "0xA7287a56C01ac8Baaf8e7B662bDB41b10889C7A6" + }, + "fraxtal": { + "router": "0xA7287a56C01ac8Baaf8e7B662bDB41b10889C7A6" + } +} diff --git a/typescript/infra/config/environments/mainnet3/supportedChainNames.ts b/typescript/infra/config/environments/mainnet3/supportedChainNames.ts index dddc793123c..f2f609fc236 100644 --- a/typescript/infra/config/environments/mainnet3/supportedChainNames.ts +++ b/typescript/infra/config/environments/mainnet3/supportedChainNames.ts @@ -15,7 +15,9 @@ export const mainnet3SupportedChainNames = [ 'bitlayer', 'blast', 'bob', + 'boba', 'bsc', + 'bsquared', 'celo', 'cheesechain', 'chilizmainnet', @@ -23,6 +25,7 @@ export const mainnet3SupportedChainNames = [ 'cyber', 'degenchain', 'dogechain', + 'duckchain', 'eclipsemainnet', 'endurance', 'ethereum', @@ -75,9 +78,12 @@ export const mainnet3SupportedChainNames = [ 'snaxchain', 'solanamainnet', 'stride', + 'superseed', 'superpositionmainnet', 'taiko', 'tangle', + 'unichain', + 'vana', 'viction', 'worldchain', 'xai', diff --git a/typescript/infra/config/environments/mainnet3/tokenPrices.json b/typescript/infra/config/environments/mainnet3/tokenPrices.json index 106c5831faf..ef91796ec26 100644 --- a/typescript/infra/config/environments/mainnet3/tokenPrices.json +++ b/typescript/infra/config/environments/mainnet3/tokenPrices.json @@ -1,88 +1,94 @@ { - "ancient8": "2901.97", - "alephzeroevmmainnet": "0.352558", - "apechain": "1.021", - "arbitrum": "2901.97", - "arbitrumnova": "2901.97", - "astar": "0.05516", - "astarzkevm": "2901.97", - "flame": "5.14", - "avalanche": "27.43", - "b3": "2901.97", - "base": "2901.97", - "bitlayer": "75858", - "blast": "2901.97", - "bob": "2901.97", - "bsc": "596.14", - "celo": "0.63787", - "cheesechain": "0.00211859", - "chilizmainnet": "0.061174", - "coredao": "0.886582", - "cyber": "2901.97", - "degenchain": "0.00776785", - "dogechain": "0.197113", - "eclipsemainnet": "2901.97", - "endurance": "2.02", - "ethereum": "2901.97", - "everclear": "2901.97", - "fantom": "0.708456", - "flare": "0.01303515", - "flowmainnet": "0.5289", - "fraxtal": "2892.2", - "fusemainnet": "0.02930522", - "gnosis": "1.001", - "gravity": "0.02864739", - "harmony": "0.01246587", - "immutablezkevmmainnet": "1.18", - "inevm": "21.99", - "injective": "21.99", - "kaia": "0.121813", - "kroma": "2901.97", - "linea": "2901.97", - "lisk": "2901.97", - "lukso": "1.48", - "lumia": "1.059", - "mantapacific": "2901.97", - "mantle": "0.651888", - "merlin": "75853", - "metal": "2901.97", - "metis": "44.6", - "mint": "2901.97", - "mode": "2901.97", - "molten": "0.25598", - "moonbeam": "0.169922", - "morph": "2901.97", - "neutron": "0.374494", - "oortmainnet": "0.092246", - "optimism": "2901.97", - "orderly": "2901.97", - "osmosis": "0.43997", - "polygon": "0.343626", - "polygonzkevm": "2901.97", - "polynomialfi": "2901.97", - "prom": "5.38", - "proofofplay": "2901.97", - "rarichain": "2901.97", + "ancient8": "3339.7", + "alephzeroevmmainnet": "0.323087", + "apechain": "1.15", + "arbitrum": "3339.7", + "arbitrumnova": "3339.7", + "astar": "0.065841", + "astarzkevm": "3339.7", + "flame": "5.32", + "avalanche": "35.04", + "b3": "3339.7", + "base": "3339.7", + "bitlayer": "97008", + "blast": "3339.7", + "bob": "3339.7", + "boba": "3339.7", + "bsc": "622.77", + "bsquared": "98146", + "celo": "0.69886", + "cheesechain": "0.00180143", + "chilizmainnet": "0.074224", + "coredao": "0.954893", + "cyber": "3339.7", + "degenchain": "0.02076785", + "dogechain": "0.386856", + "duckchain": "5.5", + "eclipsemainnet": "3339.7", + "endurance": "2.34", + "ethereum": "3339.7", + "everclear": "3339.7", + "fantom": "0.718323", + "flare": "0.01997817", + "flowmainnet": "0.699342", + "fraxtal": "3330.82", + "fusemainnet": "0.03172781", + "gnosis": "0.995976", + "gravity": "0.03027443", + "harmony": "0.01537487", + "immutablezkevmmainnet": "1.35", + "inevm": "24.39", + "injective": "24.39", + "kaia": "0.148169", + "kroma": "3339.7", + "linea": "3339.7", + "lisk": "3339.7", + "lukso": "1.28", + "lumia": "1.27", + "mantapacific": "3339.7", + "mantle": "0.797424", + "merlin": "97247", + "metal": "3339.7", + "metis": "49.54", + "mint": "3339.7", + "mode": "3339.7", + "molten": "0.255266", + "moonbeam": "0.216341", + "morph": "3339.7", + "neutron": "0.440864", + "oortmainnet": "0.084942", + "optimism": "3339.7", + "orderly": "3339.7", + "osmosis": "0.543778", + "polygon": "0.449905", + "polygonzkevm": "3339.7", + "polynomialfi": "3339.7", + "prom": "5.54", + "proofofplay": "3339.7", + "rarichain": "3339.7", "real": "1", - "redstone": "2901.97", - "rootstockmainnet": "75541", - "sanko": "53.25", - "scroll": "2901.97", - "sei": "0.404683", - "shibarium": "0.407901", - "snaxchain": "2901.97", - "solanamainnet": "199.51", - "stride": "0.583853", - "superpositionmainnet": "2901.97", - "taiko": "2901.97", + "redstone": "3339.7", + "rootstockmainnet": "97164", + "sanko": "48.68", + "scroll": "3339.7", + "sei": "0.488479", + "shibarium": "0.512057", + "snaxchain": "3339.7", + "solanamainnet": "244.09", + "stride": "0.613675", + "superseed": "3339.7", + "superpositionmainnet": "3339.7", + "taiko": "3339.7", "tangle": "1", - "viction": "0.340844", - "worldchain": "2901.97", - "xai": "0.2107", - "xlayer": "40.21", - "zeronetwork": "2901.97", - "zetachain": "0.680925", - "zircuit": "2901.97", - "zksync": "2901.97", - "zoramainnet": "2901.97" + "unichain": "3339.7", + "vana": "1", + "viction": "0.399341", + "worldchain": "3339.7", + "xai": "0.241575", + "xlayer": "45.13", + "zeronetwork": "3339.7", + "zetachain": "0.66842", + "zircuit": "3339.7", + "zksync": "3339.7", + "zoramainnet": "3339.7" } diff --git a/typescript/infra/config/environments/mainnet3/validators.ts b/typescript/infra/config/environments/mainnet3/validators.ts index 05673abf099..beb35ee7feb 100644 --- a/typescript/infra/config/environments/mainnet3/validators.ts +++ b/typescript/infra/config/environments/mainnet3/validators.ts @@ -1140,5 +1140,67 @@ export const validatorChainConfig = ( 'prom', ), }, + + boba: { + interval: 5, + reorgPeriod: getReorgPeriod('boba'), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0xebeb92c94ca8408e73aa16fd554cb3a7df075c59'], + }, + 'boba', + ), + }, + duckchain: { + interval: 5, + reorgPeriod: getReorgPeriod('duckchain'), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0x91d55fe6dac596a6735d96365e21ce4bca21d83c'], + }, + 'duckchain', + ), + }, + superseed: { + interval: 5, + reorgPeriod: getReorgPeriod('superseed'), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0xdc2b87cb555411bb138d3a4e5f7832c87fae2b88'], + }, + 'superseed', + ), + }, + unichain: { + interval: 5, + reorgPeriod: getReorgPeriod('unichain'), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0x9773a382342ebf604a2e5de0a1f462fb499e28b1'], + }, + 'unichain', + ), + }, + vana: { + interval: 5, + reorgPeriod: getReorgPeriod('vana'), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0xfdf3b0dfd4b822d10cacb15c8ae945ea269e7534'], + }, + 'vana', + ), + }, + + bsquared: { + interval: 5, + reorgPeriod: getReorgPeriod('bsquared'), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0xcadc90933c9fbe843358a4e70e46ad2db78e28aa'], + }, + 'bsquared', + ), + }, }; }; diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getAncient8EthereumUSDCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getAncient8EthereumUSDCWarpConfig.ts index 939bcc1ab2b..58f60211dcb 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getAncient8EthereumUSDCWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getAncient8EthereumUSDCWarpConfig.ts @@ -2,17 +2,21 @@ import { ethers } from 'ethers'; import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, buildAggregationIsmConfigs, defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; export const getAncient8EthereumUSDCWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const ismConfig = buildAggregationIsmConfigs( 'ethereum', @@ -22,6 +26,7 @@ export const getAncient8EthereumUSDCWarpConfig = async ( const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.collateral, token: tokens.ethereum.USDC, interchainSecurityModule: ismConfig, @@ -31,12 +36,9 @@ export const getAncient8EthereumUSDCWarpConfig = async ( hook: '0x19b2cF952b70b217c90FC408714Fbc1acD29A6A8', }; - // @ts-ignore - The types as they stand require a synthetic to specify - // TokenMetadata, but in practice these are actually inferred from a - // collateral config. To avoid needing to specify the TokenMetadata, just - // ts-ignore for synthetic tokens. const ancient8: TokenRouterConfig = { ...routerConfig.ancient8, + ...abacusWorksEnvOwnerConfig.ancient8, type: TokenType.synthetic, // Uses the default ISM interchainSecurityModule: ethers.constants.AddressZero, diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumEthereumZircuitAmphrETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumEthereumZircuitAmphrETHWarpConfig.ts index 1b81e811030..9633354b561 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumEthereumZircuitAmphrETHWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumEthereumZircuitAmphrETHWarpConfig.ts @@ -2,49 +2,46 @@ import { ethers } from 'ethers'; import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; +// MEV Capital const arbitrumOwner = '0x008615770B588633265cB01Abd19740fAe67d0B9'; const ethereumOwner = '0x008615770B588633265cB01Abd19740fAe67d0B9'; const zircuitOwner = '0xD0673e7F3FB4037CA79F53d2d311D0e017d39963'; export const getArbitrumEthereumZircuitAmphrETHWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + _abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const arbitrum: TokenRouterConfig = { ...routerConfig.arbitrum, + ...getOwnerConfigForAddress(arbitrumOwner), type: TokenType.synthetic, interchainSecurityModule: ethers.constants.AddressZero, - owner: arbitrumOwner, - ownerOverrides: { - proxyAdmin: arbitrumOwner, - }, }; const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...getOwnerConfigForAddress(ethereumOwner), type: TokenType.collateral, token: tokens.ethereum.amphrETH, - owner: ethereumOwner, interchainSecurityModule: ethers.constants.AddressZero, - ownerOverrides: { - proxyAdmin: ethereumOwner, - }, }; const zircuit: TokenRouterConfig = { ...routerConfig.zircuit, + ...getOwnerConfigForAddress(zircuitOwner), type: TokenType.synthetic, interchainSecurityModule: ethers.constants.AddressZero, - owner: zircuitOwner, - ownerOverrides: { - proxyAdmin: zircuitOwner, - }, }; return { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumNeutronEclipWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumNeutronEclipWarpConfig.ts index daebd6c026c..2dae8e447be 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumNeutronEclipWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumNeutronEclipWarpConfig.ts @@ -1,23 +1,36 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; + +// Eclipse Fi team +const arbitrumOwner = '0xfF07222cb0AC905304d6586Aabf13f497C07F0C8'; +const neutronOwner = + 'neutron1aud8lty0wwmyc86ugkzqrusnrku0ckm0ym62v4ve0jjjyepjjg6spssrwj'; + export const getArbitrumNeutronEclipWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + _abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const neutronRouter = '6b04c49fcfd98bc4ea9c05cd5790462a39537c00028333474aebe6ddf20b73a3'; - // @ts-ignore - foreignDeployment configs dont conform to the TokenRouterConfig const neutron: TokenRouterConfig = { + ...routerConfig.neutron, + ...getOwnerConfigForAddress(neutronOwner), + type: TokenType.collateral, + token: 'factory/neutron10sr06r3qkhn7xzpw3339wuj77hu06mzna6uht0/eclip', foreignDeployment: neutronRouter, }; const arbitrum: TokenRouterConfig = { ...routerConfig.arbitrum, + ...getOwnerConfigForAddress(arbitrumOwner), type: TokenType.synthetic, name: 'Eclipse Fi', symbol: 'ECLIP', @@ -25,7 +38,6 @@ export const getArbitrumNeutronEclipWarpConfig = async ( totalSupply: 0, gas: 600_000, interchainSecurityModule: '0x676151bFB8D29690a359F99AE764860595504689', // This has diverged from the default ism on neutron, we cannot change as it is owned by the Eclip team - owner: '0xfF07222cb0AC905304d6586Aabf13f497C07F0C8', // Eclip team }; return { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumNeutronTiaWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumNeutronTiaWarpConfig.ts index ce601ffc167..66a6d9e116f 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumNeutronTiaWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumNeutronTiaWarpConfig.ts @@ -1,23 +1,31 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; + export const getArbitrumNeutronTiaWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const neutronRouter = '910926c4cf95d107237a9cf0b3305fe9c81351ebcba3d218ceb0e4935d92ceac'; - // @ts-ignore - foreignDeployment configs dont conform to the TokenRouterConfig const neutron: TokenRouterConfig = { + ...routerConfig.neutron, + ...abacusWorksEnvOwnerConfig.neutron, + type: TokenType.collateral, + token: + 'ibc/773B4D0A3CD667B2275D5A4A7A2F0909C0BA0F4059C0B9181E680DDF4965DCC7', foreignDeployment: neutronRouter, }; const arbitrum: TokenRouterConfig = { ...routerConfig.arbitrum, + ...abacusWorksEnvOwnerConfig.arbitrum, type: TokenType.synthetic, name: 'TIA', symbol: 'TIA.n', diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumApxETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumApxETHWarpConfig.ts new file mode 100644 index 00000000000..517836b52c0 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumApxETHWarpConfig.ts @@ -0,0 +1,46 @@ +import { ethers } from 'ethers'; + +import { + ChainMap, + OwnableConfig, + TokenRouterConfig, + TokenType, +} from '@hyperlane-xyz/sdk'; + +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; +import { DEPLOYER } from '../../owners.js'; +import { SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT } from '../consts.js'; + +const ethereumOwner = DEPLOYER; +const eclipseOwner = '9bRSUPjfS3xS6n5EfkJzHFTRDa4AHLda8BU2pP4HoWnf'; + +export async function getEclipseEthereumApxEthWarpConfig( + routerConfig: ChainMap, + _abacusWorksEnvOwnerConfig: ChainMap, +): Promise> { + const eclipsemainnet: TokenRouterConfig = { + ...routerConfig.eclipsemainnet, + ...getOwnerConfigForAddress(eclipseOwner), + type: TokenType.synthetic, + foreignDeployment: '9pEgj7m2VkwLtJHPtTw5d8vbB7kfjzcXXCRgdwruW7C2', + gas: SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT, + interchainSecurityModule: ethers.constants.AddressZero, + }; + + let ethereum: TokenRouterConfig = { + ...routerConfig.ethereum, + ...getOwnerConfigForAddress(ethereumOwner), + type: TokenType.collateral, + token: tokens.ethereum.apxETH, + interchainSecurityModule: ethers.constants.AddressZero, + }; + + return { + eclipsemainnet, + ethereum, + }; +} diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDCWarpConfig.ts index 078c90776ee..eaaf5749c6e 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDCWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDCWarpConfig.ts @@ -8,6 +8,7 @@ import { } from '@hyperlane-xyz/sdk'; import { tokens } from '../../../../../src/config/warp.js'; +import { SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT } from '../consts.js'; export const getEclipseEthereumSolanaUSDCWarpConfig = async ( routerConfig: ChainMap, @@ -16,7 +17,7 @@ export const getEclipseEthereumSolanaUSDCWarpConfig = async ( ...routerConfig.eclipsemainnet, type: TokenType.synthetic, foreignDeployment: 'D6k6T3G74ij6atCtBiWBs5TbFa1hFVcrFUSGZHuV7q3Z', - gas: 300_000, + gas: SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT, }; const ethereum: TokenRouterConfig = { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDTWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDTWarpConfig.ts index 781b3caa983..bbabe4f5dcd 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDTWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDTWarpConfig.ts @@ -2,25 +2,32 @@ import { ethers } from 'ethers'; import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; +import { SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT } from '../consts.js'; export const getEclipseEthereumSolanaUSDTWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const eclipsemainnet: TokenRouterConfig = { ...routerConfig.eclipsemainnet, + ...abacusWorksEnvOwnerConfig.eclipsemainnet, type: TokenType.synthetic, foreignDeployment: '5g5ujyYUNvdydwyDVCpZwPpgYRqH5RYJRi156cxyE3me', - gas: 300_000, + gas: SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT, interchainSecurityModule: ethers.constants.AddressZero, }; let ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.collateral, token: tokens.ethereum.USDT, interchainSecurityModule: ethers.constants.AddressZero, diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumTETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumTETHWarpConfig.ts index db149da15c5..ca6138df1d6 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumTETHWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumTETHWarpConfig.ts @@ -7,6 +7,8 @@ import { TokenType, } from '@hyperlane-xyz/sdk'; +import { SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT } from '../consts.js'; + export const getEthereumEclipseTETHWarpConfig = async ( routerConfig: ChainMap, ): Promise> => { @@ -14,7 +16,7 @@ export const getEthereumEclipseTETHWarpConfig = async ( ...routerConfig.eclipsemainnet, type: TokenType.synthetic, foreignDeployment: 'BJa3fPvvjKx8gRCWunoSrWBbsmieub37gsGpjp4BfTfW', - gas: 300_000, + gas: SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT, }; const ethereum: TokenRouterConfig = { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumWBTCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumWBTCWarpConfig.ts index 03bdd55b7dd..0b1fd234317 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumWBTCWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumWBTCWarpConfig.ts @@ -2,26 +2,33 @@ import { ethers } from 'ethers'; import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; +import { SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT } from '../consts.js'; export const getEclipseEthereumWBTCWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const eclipsemainnet: TokenRouterConfig = { ...routerConfig.eclipsemainnet, + ...abacusWorksEnvOwnerConfig.eclipsemainnet, type: TokenType.synthetic, foreignDeployment: 'A7EGCDYFw5R7Jfm6cYtKvY8dmkrYMgwRCJFkyQwpHTYu', - gas: 300_000, + gas: SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT, interchainSecurityModule: ethers.constants.AddressZero, }; let ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.collateral, token: tokens.ethereum.WBTC, interchainSecurityModule: ethers.constants.AddressZero, diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumWeETHsWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumWeETHsWarpConfig.ts index 34da322ad75..7df68ccbac6 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumWeETHsWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseEthereumWeETHsWarpConfig.ts @@ -2,35 +2,47 @@ import { ethers } from 'ethers'; import { ChainMap, + OwnableConfig, RouterConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; -import { DEPLOYER } from '../../owners.js'; +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; +import { SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT } from '../consts.js'; + +// Safe owned by Veda +const ethereumOwner = '0xCEA8039076E35a825854c5C2f85659430b06ec96'; +// Vault owned by Veda +const eclipseOwner = '4Cj1s2ipALjJk9foQV4oDaZYCZwSsVkAShQL1KFVJG9b'; -export const getEclipseEthereumWeEthsWarpConfig = async ( - routerConfig: ChainMap, -): Promise> => { +export async function getEclipseEthereumWeEthsWarpConfig( + routerConfig: ChainMap, + _abacusWorksEnvOwnerConfig: ChainMap, +): Promise> { const eclipsemainnet: TokenRouterConfig = { ...routerConfig.eclipsemainnet, + ...getOwnerConfigForAddress(eclipseOwner), type: TokenType.synthetic, foreignDeployment: '7Zx4wU1QAw98MfvnPFqRh1oyumek7G5VAX6TKB3U1tcn', - gas: 300_000, + gas: SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT, interchainSecurityModule: ethers.constants.AddressZero, }; let ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...getOwnerConfigForAddress(ethereumOwner), type: TokenType.collateral, token: tokens.ethereum.weETHs, interchainSecurityModule: ethers.constants.AddressZero, - owner: DEPLOYER, }; return { eclipsemainnet, ethereum, }; -}; +} diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseStrideSTTIAWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseStrideSTTIAWarpConfig.ts index 4d8db22a7b5..158e6e367f6 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseStrideSTTIAWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseStrideSTTIAWarpConfig.ts @@ -1,22 +1,32 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; +import { SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT } from '../consts.js'; + +// Stride team +const strideOwner = 'stride1k8c2m5cn322akk5wy8lpt87dd2f4yh9azg7jlh'; + export const getEclipseStrideTiaWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const eclipsemainnet: TokenRouterConfig = { ...routerConfig.eclipsemainnet, + ...abacusWorksEnvOwnerConfig.eclipsemainnet, type: TokenType.synthetic, foreignDeployment: 'BpXHAiktwjx7fN6M9ST9wr6qKAsH27wZFhdHEhReJsR6', - gas: 300_000, + gas: SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT, }; const stride: TokenRouterConfig = { ...routerConfig.stride, + ...getOwnerConfigForAddress(strideOwner), type: TokenType.collateral, foreignDeployment: 'stride1pvtesu3ve7qn7ctll2x495mrqf2ysp6fws68grvcu6f7n2ajghgsh2jdj6', diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseStrideTIAWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseStrideTIAWarpConfig.ts index 9d5bdf57866..93640265992 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseStrideTIAWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseStrideTIAWarpConfig.ts @@ -1,22 +1,32 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; +import { SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT } from '../consts.js'; + +// Stride team +const strideOwner = 'stride1k8c2m5cn322akk5wy8lpt87dd2f4yh9azg7jlh'; + export const getEclipseStrideStTiaWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const eclipsemainnet: TokenRouterConfig = { ...routerConfig.eclipsemainnet, + ...abacusWorksEnvOwnerConfig.eclipsemainnet, type: TokenType.synthetic, foreignDeployment: 'tKUHyJ5NxhnwU94JUmzh1ekukDcHHX8mZF6fqxbMwX6', - gas: 300_000, + gas: SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT, }; const stride: TokenRouterConfig = { ...routerConfig.stride, + ...getOwnerConfigForAddress(strideOwner), type: TokenType.collateral, foreignDeployment: 'stride134axwdlam929m3mar3wv95nvkyep7mr87ravkqcpf8dfe3v0pjlqwrw6ee', diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumBscLumiaLUMIAWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumBscLumiaLUMIAWarpConfig.ts index 3ce50742a99..1afa97f4f95 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumBscLumiaLUMIAWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumBscLumiaLUMIAWarpConfig.ts @@ -1,59 +1,50 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; -import { objMap } from '@hyperlane-xyz/utils'; + +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; // Lumia Team const owner = '0x8bBA07Ddc72455b55530C17e6f6223EF6E156863'; - -const ownerConfig = { - owner, - // The proxyAdmins are warp-route specific - ownerOverrides: { - proxyAdmin: owner, - }, -}; +const ownerConfig = getOwnerConfigForAddress(owner); export const getEthereumBscLUMIAWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + _abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { - const ethereum = { + const ethereum: TokenRouterConfig = { + ...routerConfig.ethereum, + ...ownerConfig, type: TokenType.collateral, token: '0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7', - ownerOverrides: { - proxyAdmin: owner, - }, }; - const bsc = { + const bsc: TokenRouterConfig = { + ...routerConfig.bsc, + ...ownerConfig, type: TokenType.synthetic, - ownerOverrides: { - proxyAdmin: owner, - }, }; - const lumia = { + const lumia: TokenRouterConfig = { + ...routerConfig.lumia, + ...ownerConfig, type: TokenType.native, // As this has been removed from the registry in https://github.com/hyperlane-xyz/hyperlane-registry/pull/348, // we must specify this explicitly. mailbox: '0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7', - proxyAdmin: '0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D', + proxyAdmin: { + owner: owner, + address: '0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D', + }, }; - const configMap = { + return { ethereum, bsc, lumia, }; - - const merged = objMap(configMap, (chain, config) => ({ - ...routerConfig[chain], - ...config, - ...ownerConfig, - })); - - return merged as ChainMap; }; diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumFlowCbBTCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumFlowCbBTCWarpConfig.ts new file mode 100644 index 00000000000..3c33919e1c0 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumFlowCbBTCWarpConfig.ts @@ -0,0 +1,47 @@ +import { ethers } from 'ethers'; + +import { + ChainMap, + OwnableConfig, + TokenRouterConfig, + TokenType, +} from '@hyperlane-xyz/sdk'; + +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; + +// Flow team Safe +const ethereumOwner = '0x58C3FB862a4F5f038C24F8506BE378e9415c5B6C'; +const ethereumOwnerConfig = getOwnerConfigForAddress(ethereumOwner); + +// Flow team Safe +const flowOwner = '0xa507DFccA02727B46cBdC600C57E89b2b55E5330'; +const flowOwnerConfig = getOwnerConfigForAddress(flowOwner); + +export const getEthereumFlowCbBTCWarpConfig = async ( + routerConfig: ChainMap, + _abacusWorksEnvOwnerConfig: ChainMap, +): Promise> => { + const ethereum: TokenRouterConfig = { + ...routerConfig.ethereum, + ...ethereumOwnerConfig, + type: TokenType.collateral, + token: tokens.ethereum.cbBTC, + interchainSecurityModule: ethers.constants.AddressZero, + }; + + const flowmainnet: TokenRouterConfig = { + ...routerConfig.flowmainnet, + ...flowOwnerConfig, + type: TokenType.synthetic, + interchainSecurityModule: ethers.constants.AddressZero, + }; + + return { + ethereum, + flowmainnet, + }; +}; diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumInevmUSDCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumInevmUSDCWarpConfig.ts index aa4b19054f5..cb5ba7ada2a 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumInevmUSDCWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumInevmUSDCWarpConfig.ts @@ -2,18 +2,23 @@ import { ethers } from 'ethers'; import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; export const getEthereumInevmUSDCWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.collateral, token: tokens.ethereum.USDC, hook: '0xb87AC8EA4533AE017604E44470F7c1E550AC6F10', // aggregation of IGP and Merkle, arbitrary config not supported for now, TODO: may want to move to zero address in future @@ -21,6 +26,7 @@ export const getEthereumInevmUSDCWarpConfig = async ( const inevm: TokenRouterConfig = { ...routerConfig.inevm, + ...abacusWorksEnvOwnerConfig.inevm, type: TokenType.synthetic, interchainSecurityModule: ethers.constants.AddressZero, }; diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumInevmUSDTWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumInevmUSDTWarpConfig.ts index abe8fd14c57..b861444f1f3 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumInevmUSDTWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumInevmUSDTWarpConfig.ts @@ -2,18 +2,23 @@ import { ethers } from 'ethers'; import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; export const getEthereumInevmUSDTWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.collateral, token: tokens.ethereum.USDT, hook: '0xb87AC8EA4533AE017604E44470F7c1E550AC6F10', // aggregation of IGP and Merkle, arbitrary config not supported for now, TODO: may want to move to zero address in future @@ -21,6 +26,7 @@ export const getEthereumInevmUSDTWarpConfig = async ( const inevm: TokenRouterConfig = { ...routerConfig.inevm, + ...abacusWorksEnvOwnerConfig.inevm, type: TokenType.synthetic, interchainSecurityModule: ethers.constants.AddressZero, }; diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.ts index 5bd4938d203..1eb7f4a58c5 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.ts @@ -2,42 +2,42 @@ import { ethers } from 'ethers'; import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { getOwnerConfigForAddress } from '../../../../../src/config/environment.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; // Elixir const owner = '0x00000000F51340906F767C6999Fe512b1275955C'; +const ownerConfig = getOwnerConfigForAddress(owner); export const getEthereumSeiFastUSDWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + _abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const sei: TokenRouterConfig = { ...routerConfig.viction, + ...ownerConfig, type: TokenType.XERC20, name: 'fastUSD', symbol: 'fastUSD', decimals: 18, token: tokens.sei.fastUSD, interchainSecurityModule: ethers.constants.AddressZero, - owner, - ownerOverrides: { - proxyAdmin: owner, - }, }; const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...ownerConfig, type: TokenType.collateral, token: tokens.ethereum.deUSD, - owner, interchainSecurityModule: ethers.constants.AddressZero, - ownerOverrides: { - proxyAdmin: owner, - }, }; return { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts index 975483bc5ae..a932b181b24 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts @@ -1,14 +1,17 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, buildAggregationIsmConfigs, defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; + export const getEthereumVictionETHWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const ismConfig = buildAggregationIsmConfigs( 'ethereum', @@ -18,6 +21,7 @@ export const getEthereumVictionETHWarpConfig = async ( const viction: TokenRouterConfig = { ...routerConfig.viction, + ...abacusWorksEnvOwnerConfig.viction, type: TokenType.synthetic, name: 'ETH', symbol: 'ETH', @@ -28,6 +32,7 @@ export const getEthereumVictionETHWarpConfig = async ( const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.native, gas: 65_000, interchainSecurityModule: ismConfig, diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts index ae8ed549461..c046753c911 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts @@ -1,16 +1,20 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, buildAggregationIsmConfigs, defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; export const getEthereumVictionUSDCWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { // commit that the config was copied from https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3067/commits/7ed5b460034ea5e140c6ff86bcd6baf6ebb824c4#diff-fab5dd1a27c76e4310699c57ccf92ab6274ef0acf17e079b17270cedf4057775R109 const ismConfig = buildAggregationIsmConfigs( @@ -21,6 +25,7 @@ export const getEthereumVictionUSDCWarpConfig = async ( const viction: TokenRouterConfig = { ...routerConfig.viction, + ...abacusWorksEnvOwnerConfig.viction, type: TokenType.synthetic, name: 'USDC', symbol: 'USDC', @@ -31,6 +36,7 @@ export const getEthereumVictionUSDCWarpConfig = async ( const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.collateral, token: tokens.ethereum.USDC, gas: 65_000, diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts index 85abe405d59..5835f963222 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts @@ -1,16 +1,20 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, buildAggregationIsmConfigs, defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; -import { tokens } from '../../../../../src/config/warp.js'; +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; export const getEthereumVictionUSDTWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const ismConfig = buildAggregationIsmConfigs( 'ethereum', @@ -20,6 +24,7 @@ export const getEthereumVictionUSDTWarpConfig = async ( const viction: TokenRouterConfig = { ...routerConfig.viction, + ...abacusWorksEnvOwnerConfig.viction, type: TokenType.synthetic, name: 'USDT', symbol: 'USDT', @@ -30,6 +35,7 @@ export const getEthereumVictionUSDTWarpConfig = async ( const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, + ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.collateral, token: tokens.ethereum.USDT, gas: 65_000, diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getInevmInjectiveINJWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getInevmInjectiveINJWarpConfig.ts index 438fe23e94f..fd1b5c2b1a5 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getInevmInjectiveINJWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getInevmInjectiveINJWarpConfig.ts @@ -1,23 +1,28 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; + export const getInevmInjectiveINJWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const injectiveRouter = 'inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k'; - // @ts-ignore - foreignDeployment configs don't conform to the TokenRouterConfig const injective: TokenRouterConfig = { + ...routerConfig.injective, + ...abacusWorksEnvOwnerConfig.injective, type: TokenType.native, foreignDeployment: injectiveRouter, }; const inevm: TokenRouterConfig = { ...routerConfig.inevm, + ...abacusWorksEnvOwnerConfig.inevm, type: TokenType.native, }; diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getMantapacificNeutronTiaWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getMantapacificNeutronTiaWarpConfig.ts index e157552e62a..a603ec139c1 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getMantapacificNeutronTiaWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getMantapacificNeutronTiaWarpConfig.ts @@ -1,12 +1,15 @@ import { ChainMap, - RouterConfig, + OwnableConfig, TokenRouterConfig, TokenType, } from '@hyperlane-xyz/sdk'; +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; + export const getMantapacificNeutronTiaWarpConfig = async ( - routerConfig: ChainMap, + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { const neutronRouter = '0xc5fc6899019cb4a7649981d89eb7b1a0929d0a85b2d41802f3315129ad4b581a'; @@ -18,6 +21,7 @@ export const getMantapacificNeutronTiaWarpConfig = async ( const mantapacific: TokenRouterConfig = { ...routerConfig.mantapacific, + ...abacusWorksEnvOwnerConfig.mantapacific, type: TokenType.synthetic, name: 'TIA', symbol: 'TIA', diff --git a/typescript/infra/config/environments/mainnet3/warp/consts.ts b/typescript/infra/config/environments/mainnet3/warp/consts.ts new file mode 100644 index 00000000000..f201c1784e8 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/consts.ts @@ -0,0 +1,2 @@ +// The amount of gas to pay for when performing a transferRemote to a Sealevel chain. +export const SEALEVEL_WARP_ROUTE_HANDLER_GAS_AMOUNT = 300_000; diff --git a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts index a68ed19c8fd..679d5313195 100644 --- a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts +++ b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts @@ -1,19 +1,23 @@ export enum WarpRouteIds { Ancient8EthereumUSDC = 'USDC/ancient8-ethereum', ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismSeiTaikoZircuitEZETH = 'EZETH/arbitrum-base-blast-bsc-ethereum-fraxtal-linea-mode-optimism-sei-taiko-zircuit', + ArbitrumBaseEnduranceUSDC = 'USDC/arbitrum-base-endurance', ArbitrumEthereumZircuitAMPHRETH = 'AMPHRETH/arbitrum-ethereum-zircuit', ArbitrumNeutronEclip = 'ECLIP/arbitrum-neutron', ArbitrumNeutronTIA = 'TIA/arbitrum-neutron', + EclipseEthereumApxEth = 'APXETH/eclipsemainnet-ethereum', EclipseEthereumSolanaUSDC = 'USDC/eclipsemainnet-ethereum-solanamainnet', EclipseEthereumSolanaUSDT = 'USDT/eclipsemainnet-ethereum-solanamainnet', EclipseEthereumTETH = 'tETH/eclipsemainnet-ethereum', EclipseEthereumWBTC = 'WBTC/eclipsemainnet-ethereum', EclipseEthereumWeETHs = 'weETHs/eclipsemainnet-ethereum', + EclipseSolanaEzSOL = 'EZSOL/eclipsemainnet-solanamainnet', EclipseSolanaORCA = 'ORCA/eclipsemainnet-solanamainnet', EclipseSolanaSOL = 'SOL/eclipsemainnet-solanamainnet', EclipseSolanaWIF = 'WIF/eclipsemainnet-solanamainnet', EclipseStrideSTTIA = 'stTIA/eclipsemainnet-stride', EclipseStrideTIA = 'TIA/eclipsemainnet-stride', + EthereumFlowCbBTC = 'CBBTC/ethereum-flowmainnet', EthereumInevmUSDC = 'USDC/ethereum-inevm', EthereumInevmUSDT = 'USDT/ethereum-inevm', EthereumSeiFastUSD = 'FASTUSD/ethereum-sei', diff --git a/typescript/infra/config/warp.ts b/typescript/infra/config/warp.ts index 3a9bf87e0b5..a32dc4228b6 100644 --- a/typescript/infra/config/warp.ts +++ b/typescript/infra/config/warp.ts @@ -1,26 +1,29 @@ import { ChainMap, MultiProvider, - RouterConfig, + OwnableConfig, TokenRouterConfig, } from '@hyperlane-xyz/sdk'; +import { objMap } from '@hyperlane-xyz/utils'; -import { getHyperlaneCore } from '../scripts/core-utils.js'; import { EnvironmentConfig, getRouterConfigsForAllVms, } from '../src/config/environment.js'; +import { RouterConfigWithoutOwner } from '../src/config/warp.js'; import { getAncient8EthereumUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getAncient8EthereumUSDCWarpConfig.js'; import { getArbitrumEthereumZircuitAmphrETHWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumEthereumZircuitAmphrETHWarpConfig.js'; import { getArbitrumNeutronEclipWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumNeutronEclipWarpConfig.js'; import { getArbitrumNeutronTiaWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumNeutronTiaWarpConfig.js'; +import { getEclipseEthereumApxEthWarpConfig } from './environments/mainnet3/warp/configGetters/getEclipseEthereumApxETHWarpConfig.js'; import { getEclipseEthereumSolanaUSDTWarpConfig } from './environments/mainnet3/warp/configGetters/getEclipseEthereumSolanaUSDTWarpConfig.js'; import { getEclipseEthereumWBTCWarpConfig } from './environments/mainnet3/warp/configGetters/getEclipseEthereumWBTCWarpConfig.js'; import { getEclipseEthereumWeEthsWarpConfig } from './environments/mainnet3/warp/configGetters/getEclipseEthereumWeETHsWarpConfig.js'; import { getEclipseStrideTiaWarpConfig } from './environments/mainnet3/warp/configGetters/getEclipseStrideSTTIAWarpConfig.js'; import { getEclipseStrideStTiaWarpConfig } from './environments/mainnet3/warp/configGetters/getEclipseStrideTIAWarpConfig.js'; import { getEthereumBscLUMIAWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumBscLumiaLUMIAWarpConfig.js'; +import { getEthereumFlowCbBTCWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumFlowCbBTCWarpConfig.js'; import { getEthereumInevmUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumInevmUSDCWarpConfig.js'; import { getEthereumInevmUSDTWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumInevmUSDTWarpConfig.js'; import { getEthereumSeiFastUSDWarpConfig } from './environments/mainnet3/warp/configGetters/getEthereumSeiFastUSDWarpConfig.js'; @@ -33,16 +36,12 @@ import { getRenzoEZETHWarpConfig } from './environments/mainnet3/warp/configGett import { getRenzoPZETHWarpConfig } from './environments/mainnet3/warp/configGetters/getRenzoPZETHWarpConfig.js'; import { WarpRouteIds } from './environments/mainnet3/warp/warpIds.js'; -type WarpConfigGetterWithConfig = ( - routerConfig: ChainMap, +type WarpConfigGetter = ( + routerConfig: ChainMap, + abacusWorksEnvOwnerConfig: ChainMap, ) => Promise>; -type WarpConfigGetterWithoutConfig = () => Promise>; - -export const warpConfigGetterMap: Record< - string, - WarpConfigGetterWithConfig | WarpConfigGetterWithoutConfig -> = { +export const warpConfigGetterMap: Record = { [WarpRouteIds.Ancient8EthereumUSDC]: getAncient8EthereumUSDCWarpConfig, [WarpRouteIds.ArbitrumEthereumZircuitAMPHRETH]: getArbitrumEthereumZircuitAmphrETHWarpConfig, @@ -53,6 +52,7 @@ export const warpConfigGetterMap: Record< [WarpRouteIds.ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismSeiTaikoZircuitEZETH]: getRenzoEZETHWarpConfig, [WarpRouteIds.InevmInjectiveINJ]: getInevmInjectiveINJWarpConfig, + [WarpRouteIds.EthereumFlowCbBTC]: getEthereumFlowCbBTCWarpConfig, [WarpRouteIds.EthereumSeiFastUSD]: getEthereumSeiFastUSDWarpConfig, [WarpRouteIds.EthereumVictionETH]: getEthereumVictionETHWarpConfig, [WarpRouteIds.EthereumVictionUSDC]: getEthereumVictionUSDCWarpConfig, @@ -60,12 +60,13 @@ export const warpConfigGetterMap: Record< [WarpRouteIds.EthereumZircuitPZETH]: getRenzoPZETHWarpConfig, [WarpRouteIds.EthereumBscLumiaLUMIA]: getEthereumBscLUMIAWarpConfig, [WarpRouteIds.MantapacificNeutronTIA]: getMantapacificNeutronTiaWarpConfig, - [WarpRouteIds.EclipseStrideTIA]: getEclipseStrideTiaWarpConfig, - [WarpRouteIds.EclipseStrideSTTIA]: getEclipseStrideStTiaWarpConfig, + [WarpRouteIds.EclipseEthereumApxEth]: getEclipseEthereumApxEthWarpConfig, [WarpRouteIds.EclipseEthereumSolanaUSDT]: getEclipseEthereumSolanaUSDTWarpConfig, [WarpRouteIds.EclipseEthereumWBTC]: getEclipseEthereumWBTCWarpConfig, [WarpRouteIds.EclipseEthereumWeETHs]: getEclipseEthereumWeEthsWarpConfig, + [WarpRouteIds.EclipseStrideTIA]: getEclipseStrideTiaWarpConfig, + [WarpRouteIds.EclipseStrideSTTIA]: getEclipseStrideStTiaWarpConfig, }; export async function getWarpConfig( @@ -77,6 +78,19 @@ export async function getWarpConfig( envConfig, multiProvider, ); + // Strip the owners from the router config + const routerConfigWithoutOwner = objMap(routerConfig, (_chain, config) => { + const { owner, ownerOverrides, ...configWithoutOwner } = config; + return configWithoutOwner; + }); + // Isolate the owners from the router config + const abacusWorksEnvOwnerConfig = objMap(routerConfig, (_chain, config) => { + const { owner, ownerOverrides } = config; + return { + owner, + ownerOverrides, + }; + }); const warpConfigGetter = warpConfigGetterMap[warpRouteId]; if (!warpConfigGetter) { @@ -87,9 +101,5 @@ export async function getWarpConfig( ); } - if (warpConfigGetter.length === 1) { - return warpConfigGetter(routerConfig); - } else { - return (warpConfigGetter as WarpConfigGetterWithoutConfig)(); - } + return warpConfigGetter(routerConfigWithoutOwner, abacusWorksEnvOwnerConfig); } diff --git a/typescript/infra/helm/warp-routes/templates/_helpers.tpl b/typescript/infra/helm/warp-routes/templates/_helpers.tpl index bc732a5a378..2a9424b0a95 100644 --- a/typescript/infra/helm/warp-routes/templates/_helpers.tpl +++ b/typescript/infra/helm/warp-routes/templates/_helpers.tpl @@ -69,7 +69,7 @@ The warp-routes container imagePullPolicy: IfNotPresent env: - name: LOG_FORMAT - value: pretty + value: json command: - ./node_modules/.bin/tsx - ./typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts diff --git a/typescript/infra/package.json b/typescript/infra/package.json index c62699a6629..400bb11546c 100644 --- a/typescript/infra/package.json +++ b/typescript/infra/package.json @@ -1,29 +1,29 @@ { "name": "@hyperlane-xyz/infra", "description": "Infrastructure utilities for the Hyperlane Network", - "version": "7.0.0", + "version": "7.1.0", "dependencies": { - "@arbitrum/sdk": "^3.0.0", + "@arbitrum/sdk": "^4.0.0", "@aws-sdk/client-iam": "^3.74.0", - "@aws-sdk/client-kms": "3.48.0", - "@aws-sdk/client-s3": "^3.74.0", + "@aws-sdk/client-kms": "^3.577.0", + "@aws-sdk/client-s3": "^3.577.0", "@cosmjs/amino": "^0.32.4", "@eth-optimism/sdk": "^3.1.6", "@ethersproject/experimental": "^5.7.0", "@ethersproject/hardware-wallets": "^5.7.0", - "@ethersproject/providers": "^5.7.2", + "@ethersproject/providers": "*", "@google-cloud/secret-manager": "^5.5.0", - "@hyperlane-xyz/helloworld": "7.0.0", - "@hyperlane-xyz/registry": "4.10.0", - "@hyperlane-xyz/sdk": "7.0.0", - "@hyperlane-xyz/utils": "7.0.0", - "@inquirer/prompts": "^5.3.8", + "@hyperlane-xyz/helloworld": "7.1.0", + "@hyperlane-xyz/registry": "6.1.0", + "@hyperlane-xyz/sdk": "7.1.0", + "@hyperlane-xyz/utils": "7.1.0", + "@inquirer/prompts": "3.3.2", "@nomiclabs/hardhat-etherscan": "^3.0.3", "@safe-global/api-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0", "@safe-global/safe-core-sdk-types": "2.3.0", "@solana/web3.js": "^1.95.4", - "asn1.js": "5.4.1", + "asn1.js": "^5.4.1", "aws-kms-ethers-signer": "^0.1.3", "deep-object-diff": "^1.1.9", "dotenv": "^10.0.0", @@ -39,17 +39,17 @@ "@types/chai": "^4.2.21", "@types/json-stable-stringify": "^1.0.36", "@types/mocha": "^10.0.1", - "@types/node": "^16.9.1", + "@types/node": "^18.14.5", "@types/prompts": "^2.0.14", "@types/sinon-chai": "^3.2.12", "@types/yargs": "^17.0.24", - "chai": "4.5.0", + "chai": "^4.5.0", "ethereum-waffle": "^4.0.10", "ethers": "^5.7.2", "hardhat": "^2.22.2", "mocha": "^10.2.0", "prettier": "^2.8.8", - "tsx": "^4.7.1", + "tsx": "^4.19.1", "typescript": "5.3.3" }, "private": true, diff --git a/typescript/infra/scripts/agent-utils.ts b/typescript/infra/scripts/agent-utils.ts index 4fd4c8ec3dd..f47d13f8793 100644 --- a/typescript/infra/scripts/agent-utils.ts +++ b/typescript/infra/scripts/agent-utils.ts @@ -1,11 +1,7 @@ import path, { join } from 'path'; import yargs, { Argv } from 'yargs'; -import { - ChainAddresses, - IRegistry, - warpConfigToWarpAddresses, -} from '@hyperlane-xyz/registry'; +import { ChainAddresses, IRegistry } from '@hyperlane-xyz/registry'; import { ChainMap, ChainMetadata, @@ -157,20 +153,26 @@ export function withChain(args: Argv) { .alias('c', 'chain'); } -export function withChains(args: Argv) { +export function withChains(args: Argv, chainOptions?: ChainName[]) { return ( args .describe('chains', 'Set of chains to perform actions on.') .array('chains') - .choices('chains', getChains()) + .choices( + 'chains', + !chainOptions || chainOptions.length === 0 ? getChains() : chainOptions, + ) // Ensure chains are unique .coerce('chains', (chains: string[]) => Array.from(new Set(chains))) .alias('c', 'chains') ); } -export function withChainsRequired(args: Argv) { - return withChains(args).demandOption('chains'); +export function withChainsRequired( + args: Argv, + chainOptions?: ChainName[], +) { + return withChains(args, chainOptions).demandOption('chains'); } export function withWarpRouteId(args: Argv) { @@ -191,9 +193,8 @@ export function withProtocol(args: Argv) { export function withAgentRole(args: Argv) { return args - .describe('role', 'agent roles') - .array('role') - .coerce('role', (role: string[]): Role[] => role.map(assertRole)) + .describe('role', 'agent role') + .coerce('role', (role: string): Role => assertRole(role)) .demandOption('role') .alias('r', 'role'); } @@ -206,11 +207,16 @@ export function withAgentRoles(args: Argv) { .coerce('roles', (role: string[]): Role[] => role.map(assertRole)) .choices('roles', Object.values(Role)) // Ensure roles are unique - .coerce('roles', (roles: string[]) => Array.from(new Set(roles))) + .coerce('roles', (roles: Role[]) => Array.from(new Set(roles))) .alias('r', 'roles') + .alias('role', 'roles') ); } +export function withAgentRolesRequired(args: Argv) { + return withAgentRoles(args).demandOption('roles'); +} + export function withKeyRoleAndChain(args: Argv) { return args .describe('role', 'key role') @@ -264,6 +270,15 @@ export function withRpcUrls(args: Argv) { .alias('r', 'rpcUrls'); } +export function withTxHashes(args: Argv) { + return args + .describe('txHashes', 'transaction hash') + .string('txHashes') + .array('txHashes') + .demandOption('txHashes') + .alias('t', 'txHashes'); +} + // not requiring to build coreConfig to get agentConfig export async function getAgentConfigsBasedOnArgs(argv?: { environment: DeployEnvironment; diff --git a/typescript/infra/scripts/agents/utils.ts b/typescript/infra/scripts/agents/utils.ts index 84fe60f5977..fa3a1da4ce7 100644 --- a/typescript/infra/scripts/agents/utils.ts +++ b/typescript/infra/scripts/agents/utils.ts @@ -11,7 +11,7 @@ import { HelmCommand } from '../../src/utils/helm.js'; import { assertCorrectKubeContext, getArgs, - withAgentRole, + withAgentRolesRequired, withChains, withContext, } from '../agent-utils.js'; @@ -70,21 +70,23 @@ export class AgentCli { protected async init() { if (this.initialized) return; - const argv = await withChains(withAgentRole(withContext(getArgs()))) + const argv = await withChains( + withAgentRolesRequired(withContext(getArgs())), + ) .describe('dry-run', 'Run through the steps without making any changes') .boolean('dry-run').argv; if ( argv.chains && argv.chains.length > 0 && - !argv.role.includes(Role.Validator) + !argv.roles.includes(Role.Validator) ) { console.warn('Chain argument applies to validator role only. Ignoring.'); } const { envConfig, agentConfig } = await getConfigsBasedOnArgs(argv); await assertCorrectKubeContext(envConfig); - this.roles = argv.role; + this.roles = argv.roles; this.envConfig = envConfig; this.agentConfig = agentConfig; this.dryRun = argv.dryRun || false; diff --git a/typescript/infra/scripts/check/check-utils.ts b/typescript/infra/scripts/check/check-utils.ts index 5925832fbd8..bac0229f57d 100644 --- a/typescript/infra/scripts/check/check-utils.ts +++ b/typescript/infra/scripts/check/check-utils.ts @@ -14,6 +14,7 @@ import { InterchainAccountConfig, InterchainQuery, InterchainQueryChecker, + MultiProvider, attachContractsMapAndGetForeignDeployments, hypERC20factories, proxiedFactories, @@ -72,9 +73,13 @@ export async function getGovernor( chains?: string[], fork?: string, govern?: boolean, + multiProvider: MultiProvider | undefined = undefined, ) { const envConfig = getEnvironmentConfig(environment); - let multiProvider = await envConfig.getMultiProvider(); + // If the multiProvider is not passed in, get it from the environment + if (!multiProvider) { + multiProvider = await envConfig.getMultiProvider(); + } // must rotate to forked provider before building core contracts if (fork) { diff --git a/typescript/infra/scripts/check/check-warp-deploy.ts b/typescript/infra/scripts/check/check-warp-deploy.ts index aa51c70165a..f32d53c2b02 100644 --- a/typescript/infra/scripts/check/check-warp-deploy.ts +++ b/typescript/infra/scripts/check/check-warp-deploy.ts @@ -4,6 +4,7 @@ import { Gauge, Registry } from 'prom-client'; import { warpConfigGetterMap } from '../../config/warp.js'; import { submitMetrics } from '../../src/utils/metrics.js'; import { Modules } from '../agent-utils.js'; +import { getEnvironmentConfig } from '../core-utils.js'; import { getCheckWarpDeployArgs, @@ -16,6 +17,10 @@ async function main() { const { environment, asDeployer, chains, fork, context, pushMetrics } = await getCheckWarpDeployArgs().argv; + const envConfig = getEnvironmentConfig(environment); + // Get the multiprovider once to avoid recreating it for each warp route + const multiProvider = await envConfig.getMultiProvider(); + const metricsRegister = new Registry(); const checkerViolationsGauge = new Gauge( getCheckerViolationsGaugeObj(metricsRegister), @@ -38,6 +43,8 @@ async function main() { warpRouteId, chains, fork, + false, + multiProvider, ); await governor.check(); diff --git a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts index 4601dc702f1..dabb1fbd27e 100644 --- a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts +++ b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts @@ -1,4 +1,4 @@ -import { EthBridger, getL2Network } from '@arbitrum/sdk'; +import { EthBridger, getArbitrumNetwork } from '@arbitrum/sdk'; import { CrossChainMessenger } from '@eth-optimism/sdk'; import { Connection, PublicKey } from '@solana/web3.js'; import { BigNumber, ethers } from 'ethers'; @@ -841,13 +841,13 @@ class ContextFunder { private async bridgeToArbitrum(l2Chain: ChainName, amount: BigNumber) { const l1Chain = L2ToL1[l2Chain]; - const l2Network = await getL2Network( + const l2Network = await getArbitrumNetwork( this.multiProvider.getEvmChainId(l2Chain), ); const ethBridger = new EthBridger(l2Network); return ethBridger.deposit({ amount, - l1Signer: this.multiProvider.getSigner(l1Chain), + parentSigner: this.multiProvider.getSigner(l1Chain), overrides: this.multiProvider.getTransactionOverrides(l1Chain), }); } diff --git a/typescript/infra/scripts/create-keys.ts b/typescript/infra/scripts/keys/create-keys.ts similarity index 61% rename from typescript/infra/scripts/create-keys.ts rename to typescript/infra/scripts/keys/create-keys.ts index c5798679aa3..43b7694b0b9 100644 --- a/typescript/infra/scripts/create-keys.ts +++ b/typescript/infra/scripts/keys/create-keys.ts @@ -1,6 +1,5 @@ -import { createAgentKeysIfNotExists } from '../src/agents/key-utils.js'; - -import { getAgentConfigsBasedOnArgs } from './agent-utils.js'; +import { createAgentKeysIfNotExists } from '../../src/agents/key-utils.js'; +import { getAgentConfigsBasedOnArgs } from '../agent-utils.js'; async function main() { const { agentConfig } = await getAgentConfigsBasedOnArgs(); diff --git a/typescript/infra/scripts/delete-keys.ts b/typescript/infra/scripts/keys/delete-keys.ts similarity index 58% rename from typescript/infra/scripts/delete-keys.ts rename to typescript/infra/scripts/keys/delete-keys.ts index 934733cfdaf..12a0f1bb781 100644 --- a/typescript/infra/scripts/delete-keys.ts +++ b/typescript/infra/scripts/keys/delete-keys.ts @@ -1,6 +1,5 @@ -import { deleteAgentKeys } from '../src/agents/key-utils.js'; - -import { getAgentConfigsBasedOnArgs } from './agent-utils.js'; +import { deleteAgentKeys } from '../../src/agents/key-utils.js'; +import { getAgentConfigsBasedOnArgs } from '../agent-utils.js'; async function main() { const { agentConfig } = await getAgentConfigsBasedOnArgs(); diff --git a/typescript/infra/scripts/get-key-addresses.ts b/typescript/infra/scripts/keys/get-key-addresses.ts similarity index 80% rename from typescript/infra/scripts/get-key-addresses.ts rename to typescript/infra/scripts/keys/get-key-addresses.ts index b35c38cd22c..9cd7df379ac 100644 --- a/typescript/infra/scripts/get-key-addresses.ts +++ b/typescript/infra/scripts/keys/get-key-addresses.ts @@ -1,7 +1,6 @@ -import { getAllCloudAgentKeys } from '../src/agents/key-utils.js'; - -import { getArgs, withContext, withProtocol } from './agent-utils.js'; -import { getConfigsBasedOnArgs } from './core-utils.js'; +import { getAllCloudAgentKeys } from '../../src/agents/key-utils.js'; +import { getArgs, withContext, withProtocol } from '../agent-utils.js'; +import { getConfigsBasedOnArgs } from '../core-utils.js'; async function main() { const argv = await withProtocol(withContext(getArgs())).argv; diff --git a/typescript/infra/scripts/keys/get-key.ts b/typescript/infra/scripts/keys/get-key.ts new file mode 100644 index 00000000000..60338717de7 --- /dev/null +++ b/typescript/infra/scripts/keys/get-key.ts @@ -0,0 +1,38 @@ +import { getCloudAgentKey } from '../../src/agents/key-utils.js'; +import { + getArgs, + withAgentRole, + withContext, + withProtocol, +} from '../agent-utils.js'; +import { getConfigsBasedOnArgs } from '../core-utils.js'; + +async function main() { + const argv = await withAgentRole(withContext(getArgs())).argv; + + const { agentConfig } = await getConfigsBasedOnArgs(argv); + + // As a (very rudimentary) security precaution, we don't print the private key directly to + // the console if this script is ran directly. + // We only write the private key to the console if it is not a tty, e.g. if + // this is being called in a subshell or piped to another command. + // + // E.g. this will print the private key: + // $ echo `yarn tsx infra/scripts/keys/get-key.ts -e mainnet3 --role deployer` + // or this too: + // $ echo $(yarn tsx infra/scripts/keys/get-key.ts -e mainnet3 --role deployer) + // and even this: + // $ yarn tsx infra/scripts/keys/get-key.ts -e mainnet3 --role deployer | cat + // + // But this will not print the private key directly to the shell: + // $ yarn tsx infra/scripts/keys/get-key.ts -e mainnet3 --role deployer + if (process.stdout.isTTY) { + console.log(''); + } else { + const key = getCloudAgentKey(agentConfig, argv.role); + await key.fetch(); + console.log(key.privateKey); + } +} + +main().catch(console.error); diff --git a/typescript/infra/scripts/get-owner-ica.ts b/typescript/infra/scripts/keys/get-owner-ica.ts similarity index 92% rename from typescript/infra/scripts/get-owner-ica.ts rename to typescript/infra/scripts/keys/get-owner-ica.ts index 16c6db99e3e..b4d42c860e3 100644 --- a/typescript/infra/scripts/get-owner-ica.ts +++ b/typescript/infra/scripts/keys/get-owner-ica.ts @@ -1,11 +1,10 @@ import { AccountConfig, InterchainAccount } from '@hyperlane-xyz/sdk'; import { Address, eqAddress, isZeroishAddress } from '@hyperlane-xyz/utils'; -import { chainsToSkip } from '../src/config/chain.js'; -import { isEthereumProtocolChain } from '../src/utils/utils.js'; - -import { getArgs as getEnvArgs, withChains } from './agent-utils.js'; -import { getEnvironmentConfig, getHyperlaneCore } from './core-utils.js'; +import { chainsToSkip } from '../../src/config/chain.js'; +import { isEthereumProtocolChain } from '../../src/utils/utils.js'; +import { getArgs as getEnvArgs, withChains } from '../agent-utils.js'; +import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js'; function getArgs() { return withChains(getEnvArgs()) diff --git a/typescript/infra/scripts/rotate-key.ts b/typescript/infra/scripts/keys/rotate-key.ts similarity index 97% rename from typescript/infra/scripts/rotate-key.ts rename to typescript/infra/scripts/keys/rotate-key.ts index c7a9fe5597b..bd01f6071cc 100644 --- a/typescript/infra/scripts/rotate-key.ts +++ b/typescript/infra/scripts/keys/rotate-key.ts @@ -3,7 +3,7 @@ import { getArgs, withContext, withKeyRoleAndChain, -} from './agent-utils.js'; +} from '../agent-utils.js'; async function rotateKey() { const argv = await withContext(withKeyRoleAndChain(getArgs())).argv; diff --git a/typescript/infra/scripts/update-key.ts b/typescript/infra/scripts/keys/update-key.ts similarity index 97% rename from typescript/infra/scripts/update-key.ts rename to typescript/infra/scripts/keys/update-key.ts index 862b8c8bc62..da27c410bd7 100644 --- a/typescript/infra/scripts/update-key.ts +++ b/typescript/infra/scripts/keys/update-key.ts @@ -3,7 +3,7 @@ import { getArgs, withContext, withKeyRoleAndChain, -} from './agent-utils.js'; +} from '../agent-utils.js'; async function rotateKey() { const argv = await withKeyRoleAndChain(withContext(getArgs())).argv; diff --git a/typescript/infra/scripts/safes/get-pending-txs.ts b/typescript/infra/scripts/safes/get-pending-txs.ts new file mode 100644 index 00000000000..0c7fc708427 --- /dev/null +++ b/typescript/infra/scripts/safes/get-pending-txs.ts @@ -0,0 +1,202 @@ +import { confirm } from '@inquirer/prompts'; +import chalk from 'chalk'; +import yargs from 'yargs'; + +import { MultiProvider } from '@hyperlane-xyz/sdk'; + +import { Contexts } from '../../config/contexts.js'; +import { safes } from '../../config/environments/mainnet3/owners.js'; +import { Role } from '../../src/roles.js'; +import { executeTx, getSafeAndService } from '../../src/utils/safe.js'; +import { withChains } from '../agent-utils.js'; +import { getEnvironmentConfig } from '../core-utils.js'; + +export enum SafeTxStatus { + NO_CONFIRMATIONS = '🔴', + PENDING = '🟡', + ONE_AWAY = '🔵', + READY_TO_EXECUTE = '🟢', +} + +type SafeStatus = { + chain: string; + nonce: number; + submissionDate: string; + shortTxHash: string; + fullTxHash: string; + confs: number; + threshold: number; + status: string; +}; + +export async function getPendingTxsForChains( + chains: string[], + multiProvider: MultiProvider, +): Promise { + const txs: SafeStatus[] = []; + await Promise.all( + chains.map(async (chain) => { + if (!safes[chain]) { + console.error(chalk.red.bold(`No safe found for ${chain}`)); + return; + } + + if (chain === 'endurance') { + console.info( + chalk.gray.italic( + `Skipping chain ${chain} as it does not have a functional safe API`, + ), + ); + return; + } + + let safeSdk, safeService; + try { + ({ safeSdk, safeService } = await getSafeAndService( + chain, + multiProvider, + safes[chain], + )); + } catch (error) { + console.warn( + chalk.yellow( + `Skipping chain ${chain} as there was an error getting the safe service: ${error}`, + ), + ); + return; + } + + const threshold = await safeSdk.getThreshold(); + const pendingTxs = await safeService.getPendingTransactions(safes[chain]); + if (pendingTxs.results.length === 0) { + return; + } + + pendingTxs.results.forEach( + ({ nonce, submissionDate, safeTxHash, confirmations }) => { + const confs = confirmations?.length ?? 0; + const status = + confs >= threshold + ? SafeTxStatus.READY_TO_EXECUTE + : confs === 0 + ? SafeTxStatus.NO_CONFIRMATIONS + : threshold - confs + ? SafeTxStatus.ONE_AWAY + : SafeTxStatus.PENDING; + + txs.push({ + chain, + nonce, + submissionDate: new Date(submissionDate).toDateString(), + shortTxHash: `${safeTxHash.slice(0, 6)}...${safeTxHash.slice(-4)}`, + fullTxHash: safeTxHash, + confs, + threshold, + status, + }); + }, + ); + }), + ); + return txs.sort( + (a, b) => a.chain.localeCompare(b.chain) || a.nonce - b.nonce, + ); +} + +async function main() { + const safeChains = Object.keys(safes); + const { chains, fullTxHash, execute } = await withChains( + yargs(process.argv.slice(2)), + safeChains, + ) + .describe( + 'fullTxHash', + 'If enabled, include the full tx hash in the output', + ) + .boolean('fullTxHash') + .default('fullTxHash', false) + .describe( + 'execute', + 'If enabled, execute transactions that have enough confirmations', + ) + .boolean('execute') + .default('execute', false).argv; + + const chainsToCheck = chains || safeChains; + if (chainsToCheck.length === 0) { + console.error('No chains provided'); + process.exit(1); + } + + const envConfig = getEnvironmentConfig('mainnet3'); + const multiProvider = await envConfig.getMultiProvider( + Contexts.Hyperlane, + Role.Deployer, + true, + chainsToCheck, + ); + + const pendingTxs = await getPendingTxsForChains(chainsToCheck, multiProvider); + if (pendingTxs.length === 0) { + console.info(chalk.green('No pending transactions found!')); + process.exit(0); + } + console.table(pendingTxs, [ + 'chain', + 'nonce', + 'submissionDate', + fullTxHash ? 'fullTxHash' : 'shortTxHash', + 'confs', + 'threshold', + 'status', + ]); + + const executableTxs = pendingTxs.filter( + (tx) => tx.status === SafeTxStatus.READY_TO_EXECUTE, + ); + if ( + executableTxs.length === 0 || + !execute || + !(await confirm({ + message: 'Execute transactions?', + default: execute, + })) + ) { + console.info(chalk.green('No transactions to execute!')); + process.exit(0); + } else { + console.info(chalk.blueBright('Executing transactions...')); + } + + for (const tx of executableTxs) { + const confirmExecuteTx = await confirm({ + message: `Execute transaction ${tx.shortTxHash} on chain ${tx.chain}?`, + default: execute, + }); + if (confirmExecuteTx) { + console.log( + `Executing transaction ${tx.shortTxHash} on chain ${tx.chain}`, + ); + try { + await executeTx( + tx.chain, + multiProvider, + safes[tx.chain], + tx.fullTxHash, + ); + } catch (error) { + console.error(chalk.red(`Error executing transaction: ${error}`)); + return; + } + } + } + + process.exit(0); +} + +main() + .then() + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/typescript/infra/scripts/safes/parse-txs.ts b/typescript/infra/scripts/safes/parse-txs.ts new file mode 100644 index 00000000000..0d4a2947e30 --- /dev/null +++ b/typescript/infra/scripts/safes/parse-txs.ts @@ -0,0 +1,68 @@ +import { BigNumber } from 'ethers'; + +import { AnnotatedEV5Transaction } from '@hyperlane-xyz/sdk'; +import { stringifyObject } from '@hyperlane-xyz/utils'; + +import { GovernTransactionReader } from '../../src/tx/govern-transaction-reader.js'; +import { getSafeTx } from '../../src/utils/safe.js'; +import { + getArgs, + withChainRequired, + withChainsRequired, + withTxHashes, +} from '../agent-utils.js'; +import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js'; + +async function main() { + const { environment, chains, txHashes } = await withTxHashes( + withChainsRequired(getArgs()), + ).argv; + + const config = getEnvironmentConfig(environment); + const multiProvider = await config.getMultiProvider(); + const { chainAddresses } = await getHyperlaneCore(environment, multiProvider); + + const reader = new GovernTransactionReader( + environment, + multiProvider, + chainAddresses, + config.core, + ); + + const chainResultEntries = await Promise.all( + chains.map(async (chain, chainIndex) => { + const txHash = txHashes[chainIndex]; + console.log(`Reading tx ${txHash} on ${chain}`); + const safeTx = await getSafeTx(chain, multiProvider, txHash); + const tx: AnnotatedEV5Transaction = { + to: safeTx.to, + data: safeTx.data, + value: BigNumber.from(safeTx.value), + }; + + try { + const results = await reader.read(chain, tx); + console.log(`Finished reading tx ${txHash} on ${chain}`); + return [chain, results]; + } catch (err) { + console.error('Error reading transaction', err, chain, tx); + process.exit(1); + } + }), + ); + + const chainResults = Object.fromEntries(chainResultEntries); + console.log(stringifyObject(chainResults, 'yaml', 2)); + + if (reader.errors.length) { + console.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌'); + console.log(stringifyObject(reader.errors, 'yaml', 2)); + console.error('❌❌❌❌❌ Encountered fatal errors ❌❌❌❌❌'); + process.exit(1); + } +} + +main().catch((err) => { + console.error('Error:', err); + process.exit(1); +}); diff --git a/typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts b/typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts index bc19f434f3f..e7714778cac 100644 --- a/typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts +++ b/typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts @@ -46,11 +46,18 @@ async function getWarpRouteIdsInteractive() { value: id, })); - const selection = await checkbox({ - message: 'Select Warp Route IDs to deploy', - choices, - pageSize: 30, - }); + let selection: WarpRouteIds[] = []; + + while (!selection.length) { + selection = await checkbox({ + message: 'Select Warp Route IDs to deploy', + choices, + pageSize: 30, + }); + if (!selection.length) { + console.log('Please select at least one Warp Route ID'); + } + } return selection; } diff --git a/typescript/infra/scripts/warp-routes/generate-warp-config.ts b/typescript/infra/scripts/warp-routes/generate-warp-config.ts index 61f1cd46f68..077cef0d4ca 100644 --- a/typescript/infra/scripts/warp-routes/generate-warp-config.ts +++ b/typescript/infra/scripts/warp-routes/generate-warp-config.ts @@ -34,4 +34,4 @@ async function main() { } } -main().catch(console.error).then(console.log); +main().catch((err) => console.error('Error:', err)); diff --git a/typescript/infra/scripts/warp-routes/monitor/metrics.ts b/typescript/infra/scripts/warp-routes/monitor/metrics.ts index 7f832b512fe..eec6738ab18 100644 --- a/typescript/infra/scripts/warp-routes/monitor/metrics.ts +++ b/typescript/infra/scripts/warp-routes/monitor/metrics.ts @@ -74,17 +74,23 @@ export function updateTokenBalanceMetrics( }; warpRouteTokenBalance.labels(metrics).set(balanceInfo.balance); - logger.info('Wallet balance updated for token', { - labels: metrics, - balance: balanceInfo.balance, - }); + logger.info( + { + labels: metrics, + balance: balanceInfo.balance, + }, + 'Wallet balance updated for token', + ); if (balanceInfo.valueUSD) { warpRouteCollateralValue.labels(metrics).set(balanceInfo.valueUSD); - logger.info('Wallet balance updated for token', { - labels: metrics, - valueUSD: balanceInfo.valueUSD, - }); + logger.info( + { + labels: metrics, + valueUSD: balanceInfo.valueUSD, + }, + 'Wallet value updated for token', + ); } } diff --git a/typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts b/typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts index f9af0039d38..7a8fdb645a8 100644 --- a/typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts @@ -13,7 +13,7 @@ import { TokenStandard, WarpCore, } from '@hyperlane-xyz/sdk'; -import { ProtocolType, objMap, objMerge } from '@hyperlane-xyz/utils'; +import { ProtocolType, objMap, objMerge, sleep } from '@hyperlane-xyz/utils'; import { getWarpCoreConfig } from '../../../config/registry.js'; import { @@ -78,7 +78,7 @@ async function pollAndUpdateWarpRouteMetrics( apiKey: await getCoinGeckoApiKey(), }); - setInterval(async () => { + while (true) { await tryFn(async () => { await Promise.all( warpCore.tokens.map((token) => @@ -86,7 +86,8 @@ async function pollAndUpdateWarpRouteMetrics( ), ); }, 'Updating warp route metrics'); - }, checkFrequency); + await sleep(checkFrequency); + } } // Updates the metrics for a single token in a warp route. @@ -246,9 +247,7 @@ async function getCoinGeckoApiKey(): Promise { return apiKey; } -main() - .then(logger.info) - .catch((err) => { - logger.error('Error in main', err); - process.exit(1); - }); +main().catch((err) => { + logger.error('Error in main:', err); + process.exit(1); +}); diff --git a/typescript/infra/src/config/agent/relayer.ts b/typescript/infra/src/config/agent/relayer.ts index 648ab81aca9..a7500ec74a3 100644 --- a/typescript/infra/src/config/agent/relayer.ts +++ b/typescript/infra/src/config/agent/relayer.ts @@ -242,6 +242,18 @@ export function routerMatchingList( return matchingList(routers); } +// Create a matching list for the given senders to any destination or recipient +export function senderMatchingList( + senders: ChainMap<{ sender: Address }>, +): MatchingList { + return Object.entries(senders).map(([chain, { sender }]) => ({ + originDomain: getDomainId(chain), + senderAddress: addressToBytes32(sender), + destinationDomain: '*', + recipientAddress: '*', + })); +} + // Create a matching list for the given contract addresses export function matchingList( addressesMap: HyperlaneAddressesMap, diff --git a/typescript/infra/src/config/environment.ts b/typescript/infra/src/config/environment.ts index 25b17abff79..90761e1adc2 100644 --- a/typescript/infra/src/config/environment.ts +++ b/typescript/infra/src/config/environment.ts @@ -104,3 +104,13 @@ export async function getRouterConfigsForAllVms( // Merge, giving evmRouterConfig precedence return objMerge(allRouterConfigs, evmRouterConfig); } + +export function getOwnerConfigForAddress(owner: string): OwnableConfig { + return { + owner, + // To ensure that any other overrides aren't applied + ownerOverrides: { + proxyAdmin: owner, + }, + }; +} diff --git a/typescript/infra/src/config/warp.ts b/typescript/infra/src/config/warp.ts index 27f159079bc..e42cd1f03d1 100644 --- a/typescript/infra/src/config/warp.ts +++ b/typescript/infra/src/config/warp.ts @@ -1,13 +1,20 @@ -import { ChainMap } from '@hyperlane-xyz/sdk'; +import { + ChainMap, + OwnableConfig, + RouterConfig, + TokenRouterConfig, +} from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; // Common collateral tokens to be used by warp route deployments. export const tokens: ChainMap> = { ethereum: { + amphrETH: '0x5fD13359Ba15A84B76f7F87568309040176167cd', + apxETH: '0x9ba021b0a9b958b5e75ce9f6dff97c7ee52cb3e6', + cbBTC: '0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf', + deUSD: '0x15700B564Ca08D9439C58cA5053166E8317aa138', USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', USDT: '0xdac17f958d2ee523a2206206994597c13d831ec7', - deUSD: '0x15700B564Ca08D9439C58cA5053166E8317aa138', - amphrETH: '0x5fD13359Ba15A84B76f7F87568309040176167cd', WBTC: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', weETHs: '0x917cee801a67f933f2e6b33fc0cd1ed2d5909d88', }, @@ -15,3 +22,5 @@ export const tokens: ChainMap> = { fastUSD: '0x37a4dD9CED2b19Cfe8FAC251cd727b5787E45269', }, }; + +export type RouterConfigWithoutOwner = Omit; diff --git a/typescript/infra/src/tx/govern-transaction-reader.ts b/typescript/infra/src/tx/govern-transaction-reader.ts new file mode 100644 index 00000000000..6b0327bba15 --- /dev/null +++ b/typescript/infra/src/tx/govern-transaction-reader.ts @@ -0,0 +1,531 @@ +import { Result } from '@ethersproject/abi'; +import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils/index.js'; +import { + MetaTransactionData, + OperationType, +} from '@safe-global/safe-core-sdk-types'; +import { BigNumber, ethers } from 'ethers'; + +import { + AnnotatedEV5Transaction, + ChainMap, + ChainName, + CoreConfig, + DerivedIsmConfig, + EvmIsmReader, + InterchainAccount, + MultiProvider, + coreFactories, + interchainAccountFactories, + normalizeConfig, +} from '@hyperlane-xyz/sdk'; +import { + addressToBytes32, + bytes32ToAddress, + deepEquals, + eqAddress, + retryAsync, +} from '@hyperlane-xyz/utils'; + +import { + icaOwnerChain, + icas, + safes, +} from '../../config/environments/mainnet3/owners.js'; +import { DeployEnvironment } from '../config/environment.js'; +import { getSafeAndService } from '../utils/safe.js'; + +interface GovernTransaction extends Record { + chain: ChainName; +} + +interface MultiSendTransaction { + index: number; + value: string; + operation: string; + decoded: GovernTransaction; +} + +interface MultiSendGovernTransactions extends GovernTransaction { + multisends: MultiSendTransaction[]; +} + +interface SetDefaultIsmInsight { + module: string; + insight: string; +} + +interface IcaRemoteCallInsight { + destination: { + domain: number; + chain: ChainName; + }; + router: { + address: string; + insight: string; + }; + ism: { + address: string; + insight: string; + }; + destinationIca: { + address: string; + insight: string; + }; + calls: GovernTransaction[]; +} + +export class GovernTransactionReader { + errors: any[] = []; + + constructor( + readonly environment: DeployEnvironment, + readonly multiProvider: MultiProvider, + readonly chainAddresses: ChainMap>, + readonly coreConfig: ChainMap, + ) {} + + async read( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + // If it's to an ICA + if (this.isIcaTransaction(chain, tx)) { + return this.readIcaTransaction(chain, tx); + } + + // If it's to a Mailbox + if (this.isMailboxTransaction(chain, tx)) { + return this.readMailboxTransaction(chain, tx); + } + + if (await this.isMultisendTransaction(chain, tx)) { + return this.readMultisendTransaction(chain, tx); + } + + const insight = '⚠️ Unknown transaction type'; + // If we get here, it's an unknown transaction + this.errors.push({ + chain: chain, + tx, + info: insight, + }); + + return { + chain, + insight, + tx, + }; + } + + private async readIcaTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (!tx.data) { + throw new Error('No data in ICA transaction'); + } + const { symbol } = await this.multiProvider.getNativeToken(chain); + const icaInterface = + interchainAccountFactories.interchainAccountRouter.interface; + const decoded = icaInterface.parseTransaction({ + data: tx.data, + value: tx.value, + }); + + const args = formatFunctionFragmentArgs( + decoded.args, + decoded.functionFragment, + ); + let prettyArgs = args; + + if ( + decoded.functionFragment.name === + icaInterface.functions['enrollRemoteRouter(uint32,bytes32)'].name + ) { + prettyArgs = await this.formatRouterEnrollments( + chain, + 'interchainAccountRouter', + args, + ); + } else if ( + decoded.functionFragment.name === + icaInterface.functions[ + 'callRemoteWithOverrides(uint32,bytes32,bytes32,(bytes32,uint256,bytes)[])' + ].name + ) { + prettyArgs = await this.readIcaRemoteCall(chain, args); + } + + return { + to: `ICA Router (${chain} ${this.chainAddresses[chain].interchainAccountRouter})`, + value: `${ethers.utils.formatEther(decoded.value)} ${symbol}`, + signature: decoded.signature, + args: prettyArgs, + chain, + }; + } + + private async formatRouterEnrollments( + chain: ChainName, + routerName: string, + args: Record, + ): Promise { + const { _domains: domains, _addresses: addresses } = args; + return domains.map((domain: number, index: number) => { + const remoteChainName = this.multiProvider.getChainName(domain); + const expectedRouter = this.chainAddresses[remoteChainName][routerName]; + const routerToBeEnrolled = addresses[index]; + const matchesExpectedRouter = + eqAddress(expectedRouter, bytes32ToAddress(routerToBeEnrolled)) && + // Poor man's check that the 12 byte padding is all zeroes + addressToBytes32(bytes32ToAddress(routerToBeEnrolled)) === + routerToBeEnrolled; + + let insight = '✅ matches expected router from artifacts'; + if (!matchesExpectedRouter) { + insight = `❌ fatal mismatch, expected ${expectedRouter}`; + this.errors.push({ + chain: chain, + remoteDomain: domain, + remoteChain: remoteChainName, + router: routerToBeEnrolled, + expected: expectedRouter, + info: 'Incorrect router getting enrolled', + }); + } + + return { + domain: domain, + chainName: remoteChainName, + router: routerToBeEnrolled, + insight, + }; + }); + } + + private async readMailboxTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (!tx.data) { + throw new Error('⚠️ No data in mailbox transaction'); + } + const mailboxInterface = coreFactories.mailbox.interface; + const decoded = mailboxInterface.parseTransaction({ + data: tx.data, + value: tx.value, + }); + + const args = formatFunctionFragmentArgs( + decoded.args, + decoded.functionFragment, + ); + let prettyArgs = args; + if ( + decoded.functionFragment.name === + mailboxInterface.functions['setDefaultIsm(address)'].name + ) { + prettyArgs = await this.formatMailboxSetDefaultIsm(chain, args); + } + + return { + chain, + to: `Mailbox (${chain} ${this.chainAddresses[chain].mailbox})`, + signature: decoded.signature, + args: prettyArgs, + }; + } + + private ismDerivationsInProgress: ChainMap = {}; + + private async deriveIsmConfig( + chain: string, + module: string, + ): Promise { + const reader = new EvmIsmReader(this.multiProvider, chain); + + // Start recording some info about the deriving + const startTime = Date.now(); + console.log('Deriving ISM config...', chain); + this.ismDerivationsInProgress[chain] = true; + + const derivedConfig = await reader.deriveIsmConfig(module); + + // Deriving is done, remove from in progress + delete this.ismDerivationsInProgress[chain]; + console.log( + 'Finished deriving ISM config', + chain, + 'in', + (Date.now() - startTime) / (1000 * 60), + 'mins', + ); + const remainingInProgress = Object.keys(this.ismDerivationsInProgress); + console.log( + 'Remaining derivations in progress:', + remainingInProgress.length, + 'chains', + remainingInProgress, + ); + + return derivedConfig; + } + + private async formatMailboxSetDefaultIsm( + chain: ChainName, + args: Record, + ): Promise { + const { _module: module } = args; + + const derivedConfig = this.deriveIsmConfig(chain, module); + const expectedIsmConfig = this.coreConfig[chain].defaultIsm; + + let insight = '✅ matches expected ISM config'; + const normalizedDerived = normalizeConfig(derivedConfig); + const normalizedExpected = normalizeConfig(expectedIsmConfig); + if (!deepEquals(normalizedDerived, normalizedExpected)) { + this.errors.push({ + chain: chain, + module, + derivedConfig, + expectedIsmConfig, + info: 'Incorrect default ISM being set', + }); + insight = `❌ fatal mismatch of ISM config`; + console.log( + 'Mismatch of ISM config', + chain, + JSON.stringify(normalizedDerived), + JSON.stringify(normalizedExpected), + ); + } + + return { + module, + insight, + }; + } + + private async readIcaRemoteCall( + chain: ChainName, + args: Record, + ): Promise { + const { + _destination: destination, + _router: router, + _ism: ism, + _calls: calls, + } = args; + const remoteChainName = this.multiProvider.getChainName(destination); + + const expectedRouter = + this.chainAddresses[remoteChainName].interchainAccountRouter; + const matchesExpectedRouter = + eqAddress(expectedRouter, bytes32ToAddress(router)) && + // Poor man's check that the 12 byte padding is all zeroes + addressToBytes32(bytes32ToAddress(router)) === router; + let routerInsight = '✅ matches expected router from artifacts'; + if (!matchesExpectedRouter) { + this.errors.push({ + chain: chain, + remoteDomain: destination, + remoteChain: remoteChainName, + router: router, + expected: expectedRouter, + info: 'Incorrect router in ICA call', + }); + routerInsight = `❌ fatal mismatch, expected ${expectedRouter}`; + } + + let ismInsight = '✅ matches expected ISM'; + if (ism !== ethers.constants.HashZero) { + this.errors.push({ + chain: chain, + remoteDomain: destination, + remoteChain: remoteChainName, + ism, + info: 'Incorrect ISM in ICA call, expected zero hash', + }); + ismInsight = `❌ fatal mismatch, expected zero hash`; + } + + const remoteIcaAddress = await InterchainAccount.fromAddressesMap( + this.chainAddresses, + this.multiProvider, + ).getAccount(remoteChainName, { + owner: safes[icaOwnerChain], + origin: icaOwnerChain, + routerOverride: router, + ismOverride: ism, + }); + const expectedRemoteIcaAddress = icas[remoteChainName as keyof typeof icas]; + let remoteIcaInsight = '✅ matches expected ICA'; + if ( + !expectedRemoteIcaAddress || + !eqAddress(remoteIcaAddress, expectedRemoteIcaAddress) + ) { + this.errors.push({ + chain: chain, + remoteDomain: destination, + remoteChain: remoteChainName, + ica: remoteIcaAddress, + expected: expectedRemoteIcaAddress, + info: 'Incorrect destination ICA in ICA call', + }); + remoteIcaInsight = `❌ fatal mismatch, expected ${remoteIcaAddress}`; + } + + const decodedCalls = await Promise.all( + calls.map((call: any) => { + const icaCallAsTx = { + to: bytes32ToAddress(call[0]), + value: BigNumber.from(call[1]), + data: call[2], + }; + return this.read(remoteChainName, icaCallAsTx); + }), + ); + + return { + destination: { + domain: destination, + chain: remoteChainName, + }, + router: { + address: router, + insight: routerInsight, + }, + ism: { + address: ism, + insight: ismInsight, + }, + destinationIca: { + address: remoteIcaAddress, + insight: remoteIcaInsight, + }, + calls: decodedCalls, + }; + } + + private async readMultisendTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (!tx.data) { + throw new Error('No data in multisend transaction'); + } + const multisendDatas = decodeMultiSendData(tx.data); + + const { symbol } = await this.multiProvider.getNativeToken(chain); + + const multisends = await Promise.all( + multisendDatas.map(async (multisend, index) => { + const decoded = await this.read( + chain, + metaTransactionDataToEV5Transaction(multisend), + ); + return { + chain, + index, + value: `${ethers.utils.formatEther(multisend.value)} ${symbol}`, + operation: formatOperationType(multisend.operation), + decoded, + }; + }), + ); + + return { + chain, + multisends, + }; + } + + isIcaTransaction(chain: ChainName, tx: AnnotatedEV5Transaction): boolean { + return ( + tx.to !== undefined && + eqAddress(tx.to, this.chainAddresses[chain].interchainAccountRouter) + ); + } + + isMailboxTransaction(chain: ChainName, tx: AnnotatedEV5Transaction): boolean { + return ( + tx.to !== undefined && + eqAddress(tx.to, this.chainAddresses[chain].mailbox) + ); + } + + async isMultisendTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (tx.to === undefined) { + return false; + } + const multiSendCallOnlyAddress = await this.getMultiSendCallOnlyAddress( + chain, + ); + if (!multiSendCallOnlyAddress) { + return false; + } + + return eqAddress(multiSendCallOnlyAddress, tx.to); + } + + private multiSendCallOnlyAddressCache: ChainMap = {}; + + async getMultiSendCallOnlyAddress( + chain: ChainName, + ): Promise { + if (this.multiSendCallOnlyAddressCache[chain]) { + return this.multiSendCallOnlyAddressCache[chain]; + } + + const safe = safes[chain]; + if (!safe) { + return undefined; + } + + const { safeSdk } = await getSafeAndService( + chain, + this.multiProvider, + safe, + ); + + this.multiSendCallOnlyAddressCache[chain] = + safeSdk.getMultiSendCallOnlyAddress(); + return this.multiSendCallOnlyAddressCache[chain]; + } +} + +function metaTransactionDataToEV5Transaction( + metaTransactionData: MetaTransactionData, +): AnnotatedEV5Transaction { + return { + to: metaTransactionData.to, + value: BigNumber.from(metaTransactionData.value), + data: metaTransactionData.data, + }; +} + +function formatFunctionFragmentArgs( + args: Result, + fragment: ethers.utils.FunctionFragment, +): Record { + const accumulator: Record = {}; + return fragment.inputs.reduce((acc, input, index) => { + acc[input.name] = args[index]; + return acc; + }, accumulator); +} + +function formatOperationType(operation: OperationType | undefined): string { + switch (operation) { + case OperationType.Call: + return 'Call'; + case OperationType.DelegateCall: + return 'Delegate Call'; + default: + return '⚠️ Unknown ⚠️'; + } +} diff --git a/typescript/infra/src/utils/safe.ts b/typescript/infra/src/utils/safe.ts index 397e746c07b..95e27c50e3e 100644 --- a/typescript/infra/src/utils/safe.ts +++ b/typescript/infra/src/utils/safe.ts @@ -14,7 +14,7 @@ import { getSafe, getSafeService, } from '@hyperlane-xyz/sdk'; -import { Address, CallData, eqAddress } from '@hyperlane-xyz/utils'; +import { Address, CallData, eqAddress, retryAsync } from '@hyperlane-xyz/utils'; import safeSigners from '../../config/environments/mainnet3/safe/safeSigners.json' assert { type: 'json' }; import { AnnotatedCallData } from '../govern/HyperlaneAppGovernor.js'; @@ -24,10 +24,10 @@ export async function getSafeAndService( multiProvider: MultiProvider, safeAddress: Address, ) { - const safeSdk: Safe.default = await getSafe( - chain, - multiProvider, - safeAddress, + const safeSdk: Safe.default = await retryAsync( + () => getSafe(chain, multiProvider, safeAddress), + 5, + 1000, ); const safeService: SafeApiKit.default = getSafeService(chain, multiProvider); return { safeSdk, safeService }; @@ -41,6 +41,52 @@ export function createSafeTransactionData(call: CallData): MetaTransactionData { }; } +export async function executeTx( + chain: ChainNameOrId, + multiProvider: MultiProvider, + safeAddress: Address, + safeTxHash: string, +): Promise { + const { safeSdk, safeService } = await getSafeAndService( + chain, + multiProvider, + safeAddress, + ); + const safeTransaction = await safeService.getTransaction(safeTxHash); + if (!safeTransaction) { + throw new Error(`Failed to fetch transaction details for ${safeTxHash}`); + } + + // Throw if the safe doesn't have enough balance to cover the gas + let estimate; + try { + estimate = await safeService.estimateSafeTransaction( + safeAddress, + safeTransaction, + ); + } catch (error) { + throw new Error( + `Failed to estimate gas for Safe transaction ${safeTxHash} on chain ${chain}: ${error}`, + ); + } + const balance = await multiProvider + .getProvider(chain) + .getBalance(safeAddress); + if (balance.lt(estimate.safeTxGas)) { + throw new Error( + `Safe ${safeAddress} on ${chain} has insufficient balance (${balance.toString()}) for estimated gas (${ + estimate.safeTxGas + })`, + ); + } + + await safeSdk.executeTransaction(safeTransaction); + + console.log( + chalk.green.bold(`Executed transaction ${safeTxHash} on ${chain}`), + ); +} + export async function createSafeTransaction( safeSdk: Safe.default, safeService: SafeApiKit.default, @@ -115,6 +161,31 @@ export async function deleteAllPendingSafeTxs( ); } +export async function getSafeTx( + chain: ChainNameOrId, + multiProvider: MultiProvider, + safeTxHash: string, +): Promise { + const txServiceUrl = + multiProvider.getChainMetadata(chain).gnosisSafeTransactionServiceUrl; + + // Fetch the transaction details to get the proposer + const txDetailsUrl = `${txServiceUrl}/api/v1/multisig-transactions/${safeTxHash}/`; + const txDetailsResponse = await fetch(txDetailsUrl, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!txDetailsResponse.ok) { + console.error( + chalk.red(`Failed to fetch transaction details for ${safeTxHash}`), + ); + return; + } + + return txDetailsResponse.json(); +} + export async function deleteSafeTx( chain: ChainNameOrId, multiProvider: MultiProvider, diff --git a/typescript/infra/src/warp/helm.ts b/typescript/infra/src/warp/helm.ts index 02b8d9dc995..fb9db10eed1 100644 --- a/typescript/infra/src/warp/helm.ts +++ b/typescript/infra/src/warp/helm.ts @@ -22,7 +22,7 @@ export class WarpRouteMonitorHelmManager extends HelmManager { return { image: { repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '6cd61f1-20241112-111341', + tag: '4d0de30-20241119-171012', }, warpRouteId: this.warpRouteId, fullnameOverride: this.helmReleaseName, diff --git a/typescript/infra/test/warpIds.test.ts b/typescript/infra/test/warpIds.test.ts index ba171c5bab8..11f8a2dd159 100644 --- a/typescript/infra/test/warpIds.test.ts +++ b/typescript/infra/test/warpIds.test.ts @@ -8,8 +8,10 @@ describe('Warp IDs', () => { const registry = getRegistry(); for (const warpId of Object.values(WarpRouteIds)) { // That's a long sentence! - expect(registry.getWarpRoute(warpId), `Warp ID ${warpId} not in registry`) - .to.not.be.null.and.not.be.undefined; + expect( + registry.getWarpRoute(warpId), + `Warp ID ${warpId} not in registry, the .registryrc or your local registry may be out of date`, + ).to.not.be.null.and.not.be.undefined; } }); }); diff --git a/typescript/sdk/.eslintrc b/typescript/sdk/.eslintrc deleted file mode 100644 index a0a68426786..00000000000 --- a/typescript/sdk/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rules": { - "@typescript-eslint/explicit-module-boundary-types": ["warn", { - "allowArgumentsExplicitlyTypedAsAny": true - }] - } -} diff --git a/typescript/sdk/CHANGELOG.md b/typescript/sdk/CHANGELOG.md index 028f546b2a8..2395e197fec 100644 --- a/typescript/sdk/CHANGELOG.md +++ b/typescript/sdk/CHANGELOG.md @@ -1,5 +1,22 @@ # @hyperlane-xyz/sdk +## 7.1.0 + +### Minor Changes + +- 6f2d50fbd: Updated Fraxtal set to include Superlane validators, updated Flow set +- 1159e0f4b: Enroll new validators for alephzeroevmmainnet, chilizmainnet, flowmainnet, immutablezkevmmainnet, metal, polynomialfi, rarichain, rootstockmainnet, superpositionmainnet, flame, prom, inevm. +- ff2b4e2fb: Added helpers to Token and token adapters to get bridged supply of tokens" +- 0e285a443: Add a validateZodResult util function +- 5db46bd31: Implements persistent relayer for use in CLI +- 0cd65c571: Add chainMetadataToCosmosChain function + +### Patch Changes + +- Updated dependencies [0e285a443] + - @hyperlane-xyz/utils@7.1.0 + - @hyperlane-xyz/core@5.8.1 + ## 7.0.0 ### Major Changes diff --git a/typescript/sdk/eslint.config.mjs b/typescript/sdk/eslint.config.mjs new file mode 100644 index 00000000000..2855488799e --- /dev/null +++ b/typescript/sdk/eslint.config.mjs @@ -0,0 +1,25 @@ +import MonorepoDefaults from '../../eslint.config.mjs'; + +export default [ + ...MonorepoDefaults, + { + files: ['./src/**/*.ts'], + rules: { + '@typescript-eslint/explicit-module-boundary-types': [ + 'warn', + { + allowArgumentsExplicitlyTypedAsAny: true, + }, + ], + }, + }, + { + ignores: ['./src/ism/metadata/**/*.ts'], + rules: { + 'import/no-cycle': ['off'], + }, + }, + { + ignores: ['src/**/*.js'], + }, +]; diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 0918fb6839d..0c621a763c5 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -1,14 +1,15 @@ { "name": "@hyperlane-xyz/sdk", "description": "The official SDK for the Hyperlane Network", - "version": "7.0.0", + "version": "7.1.0", "dependencies": { "@arbitrum/sdk": "^4.0.0", - "@aws-sdk/client-s3": "^3.74.0", + "@aws-sdk/client-s3": "^3.577.0", + "@chain-registry/types": "^0.50.14", "@cosmjs/cosmwasm-stargate": "^0.32.4", "@cosmjs/stargate": "^0.32.4", - "@hyperlane-xyz/core": "5.8.0", - "@hyperlane-xyz/utils": "7.0.0", + "@hyperlane-xyz/core": "5.8.1", + "@hyperlane-xyz/utils": "7.1.0", "@safe-global/api-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0", "@safe-global/safe-deployments": "1.37.8", @@ -19,27 +20,33 @@ "cross-fetch": "^3.1.5", "ethers": "^5.7.2", "pino": "^8.19.0", - "viem": "^2.21.40", + "viem": "^2.21.45", "zod": "^3.21.2" }, "devDependencies": { + "@eslint/js": "^9.15.0", "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-waffle": "^2.0.6", "@types/mocha": "^10.0.1", - "@types/node": "^16.9.1", + "@types/node": "^18.14.5", "@types/sinon": "^17.0.1", "@types/sinon-chai": "^3.2.12", "@types/ws": "^8.5.5", - "chai": "4.5.0", + "@typescript-eslint/eslint-plugin": "^8.1.6", + "@typescript-eslint/parser": "^8.1.6", + "chai": "^4.5.0", "dotenv": "^10.0.0", - "eslint": "^8.57.0", + "eslint": "^9.15.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", "ethereum-waffle": "^4.0.10", "hardhat": "^2.22.2", "mocha": "^10.2.0", "prettier": "^2.8.8", "sinon": "^13.0.2", "ts-node": "^10.8.0", - "tsx": "^4.7.1", + "tsx": "^4.19.1", "typescript": "5.3.3", "yaml": "2.4.5" }, @@ -69,7 +76,7 @@ "dev": "tsc --watch", "check": "tsc --noEmit", "clean": "rm -rf ./dist ./cache", - "lint": "eslint src --ext .ts", + "lint": "eslint -c ./eslint.config.mjs", "prepublishOnly": "yarn build", "prettier": "prettier --write ./src", "test": "yarn test:unit && yarn test:hardhat && yarn test:foundry", diff --git a/typescript/sdk/src/consts/.eslintrc b/typescript/sdk/src/consts/.eslintrc deleted file mode 100644 index 7242f124127..00000000000 --- a/typescript/sdk/src/consts/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "sort-keys": ["error"] - } -} diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index 5f80ff91abc..32bd501f357 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -163,6 +163,11 @@ export const defaultMultisigConfigs: ChainMap = { ], }, + boba: { + threshold: 1, + validators: ['0xebeb92c94ca8408e73aa16fd554cb3a7df075c59'], + }, + bsc: { threshold: 3, validators: [ @@ -182,6 +187,11 @@ export const defaultMultisigConfigs: ChainMap = { ], }, + bsquared: { + threshold: 1, + validators: ['0xcadc90933c9fbe843358a4e70e46ad2db78e28aa'], + }, + camptestnet: { threshold: 1, validators: ['0x238f40f055a7ff697ea6dbff3ae943c9eae7a38e'], @@ -271,6 +281,11 @@ export const defaultMultisigConfigs: ChainMap = { ], }, + duckchain: { + threshold: 1, + validators: ['0x91d55fe6dac596a6735d96365e21ce4bca21d83c'], + }, + eclipsemainnet: { threshold: 3, validators: [ @@ -905,6 +920,11 @@ export const defaultMultisigConfigs: ChainMap = { validators: ['0x1d3168504b23b73cdf9c27f13bb0a595d7f1a96a'], }, + superseed: { + threshold: 1, + validators: ['0xdc2b87cb555411bb138d3a4e5f7832c87fae2b88'], + }, + taiko: { threshold: 3, validators: [ @@ -929,11 +949,21 @@ export const defaultMultisigConfigs: ChainMap = { validators: ['0x9750849beda0a7870462d4685f953fe39033a5ae'], }, + unichain: { + threshold: 1, + validators: ['0x9773a382342ebf604a2e5de0a1f462fb499e28b1'], + }, + unichaintestnet: { threshold: 1, validators: ['0x5e99961cf71918308c3b17ef21b5f515a4f86fe5'], }, + vana: { + threshold: 1, + validators: ['0xfdf3b0dfd4b822d10cacb15c8ae945ea269e7534'], + }, + viction: { threshold: 2, validators: [ diff --git a/typescript/sdk/src/consts/multisigIsmVerifyCosts.ts b/typescript/sdk/src/consts/multisigIsmVerifyCosts.ts index 2891527b2c6..16be1a19518 100644 --- a/typescript/sdk/src/consts/multisigIsmVerifyCosts.ts +++ b/typescript/sdk/src/consts/multisigIsmVerifyCosts.ts @@ -1,4 +1,3 @@ -/* eslint-disable sort-keys */ export const multisigIsmVerifyCosts = { '1': { '1': 151966, diff --git a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts index 525ca4acd0b..665133ed0f2 100644 --- a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts +++ b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts @@ -195,7 +195,7 @@ describe('core', async () => { try { await deployer.deploy(coreConfig); // eslint-disable-next-line no-empty - } catch (e: any) {} + } catch {} }); afterEach(async () => { @@ -252,7 +252,7 @@ describe('core', async () => { deployer.chainTimeoutMs = 1; try { await deployer.deploy(coreConfig); - } catch (e: any) { + } catch { // TODO: figure out how to test specific error case // expect(e.message).to.include('Timed out in 1ms'); } diff --git a/typescript/sdk/src/core/EvmCoreModule.ts b/typescript/sdk/src/core/EvmCoreModule.ts index a14cdc5fe95..0d783aa341e 100644 --- a/typescript/sdk/src/core/EvmCoreModule.ts +++ b/typescript/sdk/src/core/EvmCoreModule.ts @@ -21,8 +21,6 @@ import { HyperlaneAddresses, HyperlaneContractsMap, } from '../contracts/types.js'; -import { DeployedCoreAddresses } from '../core/schemas.js'; -import { CoreConfig } from '../core/types.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { ProxyFactoryFactories, @@ -47,7 +45,8 @@ import { EvmCoreReader } from './EvmCoreReader.js'; import { EvmIcaModule } from './EvmIcaModule.js'; import { HyperlaneCoreDeployer } from './HyperlaneCoreDeployer.js'; import { CoreFactories } from './contracts.js'; -import { CoreConfigSchema } from './schemas.js'; +import { CoreConfigSchema, DeployedCoreAddresses } from './schemas.js'; +import { CoreConfig } from './types.js'; export class EvmCoreModule extends HyperlaneModule< ProtocolType.Ethereum, diff --git a/typescript/sdk/src/core/HyperlaneRelayer.ts b/typescript/sdk/src/core/HyperlaneRelayer.ts index 0a1a451ebfa..48ddfd32344 100644 --- a/typescript/sdk/src/core/HyperlaneRelayer.ts +++ b/typescript/sdk/src/core/HyperlaneRelayer.ts @@ -17,10 +17,10 @@ import { } from '@hyperlane-xyz/utils'; import { DerivedHookConfig, EvmHookReader } from '../hook/EvmHookReader.js'; -import { HookConfigSchema } from '../hook/schemas.js'; +import { HookConfigSchema } from '../hook/types.js'; import { DerivedIsmConfig, EvmIsmReader } from '../ism/EvmIsmReader.js'; import { BaseMetadataBuilder } from '../ism/metadata/builder.js'; -import { IsmConfigSchema } from '../ism/schemas.js'; +import { IsmConfigSchema } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; @@ -307,7 +307,7 @@ export class HyperlaneRelayer { // TODO: handle batching await this.relayMessage(dispatchReceipt, undefined, dispatchMsg); - } catch (error) { + } catch { this.logger.error( `Failed to relay message ${id} (attempt #${attempts + 1})`, ); @@ -320,7 +320,7 @@ export class HyperlaneRelayer { } } - protected whitelistChains() { + protected whitelistChains(): string[] | undefined { return this.whitelist ? Object.keys(this.whitelist) : undefined; } diff --git a/typescript/sdk/src/core/schemas.ts b/typescript/sdk/src/core/schemas.ts index 470df95ab08..9959c302426 100644 --- a/typescript/sdk/src/core/schemas.ts +++ b/typescript/sdk/src/core/schemas.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; import { ProxyFactoryFactoriesSchema } from '../deploy/schemas.js'; -import { HookConfigSchema } from '../hook/schemas.js'; -import { IsmConfigSchema } from '../ism/schemas.js'; +import { HookConfigSchema } from '../hook/types.js'; +import { IsmConfigSchema } from '../ism/types.js'; import { DeployedOwnableSchema, OwnableSchema } from '../schemas.js'; export const CoreConfigSchema = OwnableSchema.extend({ diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index c6cd2048c60..4deee15ac6c 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -29,7 +29,7 @@ import { HyperlaneFactories, } from '../contracts/types.js'; import { HookConfig } from '../hook/types.js'; -import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; +import type { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { IsmConfig } from '../ism/types.js'; import { moduleMatchesConfig } from '../ism/utils.js'; import { InterchainAccount } from '../middleware/account/InterchainAccount.js'; diff --git a/typescript/sdk/src/deploy/verify/.eslintrc b/typescript/sdk/src/deploy/verify/.eslintrc deleted file mode 100644 index e3f712414b8..00000000000 --- a/typescript/sdk/src/deploy/verify/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-console": ["off"] - } -} diff --git a/typescript/sdk/src/deploy/verify/ContractVerifier.ts b/typescript/sdk/src/deploy/verify/ContractVerifier.ts index 992d2de3b95..49ecd30980f 100644 --- a/typescript/sdk/src/deploy/verify/ContractVerifier.ts +++ b/typescript/sdk/src/deploy/verify/ContractVerifier.ts @@ -183,7 +183,7 @@ export class ContractVerifier { 'Parsing response from explorer...', ); responseJson = JSON.parse(responseTextString); - } catch (error) { + } catch { verificationLogger.trace( { failure: response.statusText, diff --git a/typescript/sdk/src/gas/adapters/serialization.ts b/typescript/sdk/src/gas/adapters/serialization.ts index 70cd8082591..e7d34a47903 100644 --- a/typescript/sdk/src/gas/adapters/serialization.ts +++ b/typescript/sdk/src/gas/adapters/serialization.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { PublicKey } from '@solana/web3.js'; import { Domain } from '@hyperlane-xyz/utils'; diff --git a/typescript/sdk/src/gas/types.ts b/typescript/sdk/src/gas/types.ts index 55114478d26..6cb46630ebb 100644 --- a/typescript/sdk/src/gas/types.ts +++ b/typescript/sdk/src/gas/types.ts @@ -5,7 +5,7 @@ import { InterchainGasPaymaster } from '@hyperlane-xyz/core'; import type { Address } from '@hyperlane-xyz/utils'; import type { CheckerViolation } from '../deploy/types.js'; -import { IgpSchema } from '../hook/schemas.js'; +import { IgpSchema } from '../hook/types.js'; import { ChainMap } from '../types.js'; export type IgpConfig = z.infer; diff --git a/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts b/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts index aa4c9d559db..ae66d7adbf6 100644 --- a/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts +++ b/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { expect } from 'chai'; import { Signer } from 'ethers'; import hre from 'hardhat'; diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts index 5f6a64e3441..a1e82177a97 100644 --- a/typescript/sdk/src/hook/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -56,13 +56,13 @@ import { normalizeConfig } from '../utils/ism.js'; import { EvmHookReader } from './EvmHookReader.js'; import { DeployedHook, HookFactories, hookFactories } from './contracts.js'; -import { HookConfigSchema } from './schemas.js'; import { AggregationHookConfig, ArbL2ToL1HookConfig, DomainRoutingHookConfig, FallbackRoutingHookConfig, HookConfig, + HookConfigSchema, HookType, IgpHookConfig, MUTABLE_HOOK_TYPE, diff --git a/typescript/sdk/src/hook/EvmHookReader.test.ts b/typescript/sdk/src/hook/EvmHookReader.test.ts index befd73a431b..3a8bb457638 100644 --- a/typescript/sdk/src/hook/EvmHookReader.test.ts +++ b/typescript/sdk/src/hook/EvmHookReader.test.ts @@ -148,7 +148,6 @@ describe('EvmHookReader', () => { expect(config).to.deep.equal(hookConfig); }); - // eslint-disable-next-line @typescript-eslint/no-empty-function it('should derive op stack config correctly', async () => { const mockAddress = randomAddress(); const mockOwner = randomAddress(); diff --git a/typescript/sdk/src/hook/EvmHookReader.ts b/typescript/sdk/src/hook/EvmHookReader.ts index 9ebcd8ae03e..dae0f9d5898 100644 --- a/typescript/sdk/src/hook/EvmHookReader.ts +++ b/typescript/sdk/src/hook/EvmHookReader.ts @@ -269,7 +269,7 @@ export class EvmHookReader extends HyperlaneReader implements HookReader { this.provider, ); return oracle.owner(); - } catch (error) { + } catch { this.logger.debug( 'Domain not configured on IGP Hook', domainId, @@ -451,7 +451,7 @@ export class EvmHookReader extends HyperlaneReader implements HookReader { if (domainHook !== ethers.constants.AddressZero) { domainHooks[chainName] = await this.deriveHookConfig(domainHook); } - } catch (error) { + } catch { this.logger.debug( `Domain not configured on ${hook.constructor.name}`, domainId, diff --git a/typescript/sdk/src/hook/schemas.ts b/typescript/sdk/src/hook/schemas.ts deleted file mode 100644 index 16bd01b27f7..00000000000 --- a/typescript/sdk/src/hook/schemas.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { z } from 'zod'; - -import { StorageGasOracleConfigSchema } from '../gas/oracle/types.js'; -import { ZHash } from '../metadata/customZodTypes.js'; -import { OwnableSchema, PausableSchema } from '../schemas.js'; - -import { - AggregationHookConfig, - DomainRoutingHookConfig, - FallbackRoutingHookConfig, - HookType, -} from './types.js'; - -export const ProtocolFeeSchema = OwnableSchema.extend({ - type: z.literal(HookType.PROTOCOL_FEE), - beneficiary: z.string(), - maxProtocolFee: z.string(), - protocolFee: z.string(), -}); - -export const MerkleTreeSchema = z.object({ - type: z.literal(HookType.MERKLE_TREE), -}); - -export const PausableHookSchema = PausableSchema.extend({ - type: z.literal(HookType.PAUSABLE), -}); - -export const OpStackHookSchema = OwnableSchema.extend({ - type: z.literal(HookType.OP_STACK), - nativeBridge: z.string(), - destinationChain: z.string(), -}); - -export const ArbL2ToL1HookSchema = z.object({ - type: z.literal(HookType.ARB_L2_TO_L1), - arbSys: z - .string() - .describe( - 'precompile for sending messages to L1, interface here: https://github.com/OffchainLabs/nitro-contracts/blob/90037b996509312ef1addb3f9352457b8a99d6a6/src/precompiles/ArbSys.sol#L12', - ), - bridge: z - .string() - .optional() - .describe( - 'address of the bridge contract on L1, optional only needed for non @arbitrum/sdk chains', - ), - destinationChain: z.string(), - childHook: z.lazy((): z.ZodSchema => HookConfigSchema), -}); - -export const IgpSchema = OwnableSchema.extend({ - type: z.literal(HookType.INTERCHAIN_GAS_PAYMASTER), - beneficiary: z.string(), - oracleKey: z.string(), - overhead: z.record(z.number()), - oracleConfig: z.record(StorageGasOracleConfigSchema), -}); - -export const DomainRoutingHookConfigSchema: z.ZodSchema = - z.lazy(() => - OwnableSchema.extend({ - type: z.literal(HookType.ROUTING), - domains: z.record(HookConfigSchema), - }), - ); - -export const FallbackRoutingHookConfigSchema: z.ZodSchema = - z.lazy(() => - OwnableSchema.extend({ - type: z.literal(HookType.FALLBACK_ROUTING), - domains: z.record(HookConfigSchema), - fallback: HookConfigSchema, - }), - ); - -export const AggregationHookConfigSchema: z.ZodSchema = - z.lazy(() => - z.object({ - type: z.literal(HookType.AGGREGATION), - hooks: z.array(HookConfigSchema), - }), - ); - -export const HookConfigSchema = z.union([ - ZHash, - ProtocolFeeSchema, - PausableHookSchema, - OpStackHookSchema, - MerkleTreeSchema, - IgpSchema, - DomainRoutingHookConfigSchema, - FallbackRoutingHookConfigSchema, - AggregationHookConfigSchema, - ArbL2ToL1HookSchema, -]); diff --git a/typescript/sdk/src/hook/types.ts b/typescript/sdk/src/hook/types.ts index 01d5b0df746..3cfd824b7a9 100644 --- a/typescript/sdk/src/hook/types.ts +++ b/typescript/sdk/src/hook/types.ts @@ -1,18 +1,11 @@ import { z } from 'zod'; import { OwnableConfig } from '../deploy/types.js'; +import { StorageGasOracleConfigSchema } from '../gas/oracle/types.js'; +import { ZHash } from '../metadata/customZodTypes.js'; +import { OwnableSchema, PausableSchema } from '../schemas.js'; import { ChainMap } from '../types.js'; -import { - ArbL2ToL1HookSchema, - HookConfigSchema, - IgpSchema, - MerkleTreeSchema, - OpStackHookSchema, - PausableHookSchema, - ProtocolFeeSchema, -} from './schemas.js'; - // As found in IPostDispatchHook.sol export enum OnchainHookType { UNUSED, @@ -75,3 +68,87 @@ export const MUTABLE_HOOK_TYPE = [ HookType.FALLBACK_ROUTING, HookType.PAUSABLE, ]; + +export const ProtocolFeeSchema = OwnableSchema.extend({ + type: z.literal(HookType.PROTOCOL_FEE), + beneficiary: z.string(), + maxProtocolFee: z.string(), + protocolFee: z.string(), +}); + +export const MerkleTreeSchema = z.object({ + type: z.literal(HookType.MERKLE_TREE), +}); + +export const PausableHookSchema = PausableSchema.extend({ + type: z.literal(HookType.PAUSABLE), +}); + +export const OpStackHookSchema = OwnableSchema.extend({ + type: z.literal(HookType.OP_STACK), + nativeBridge: z.string(), + destinationChain: z.string(), +}); + +export const ArbL2ToL1HookSchema = z.object({ + type: z.literal(HookType.ARB_L2_TO_L1), + arbSys: z + .string() + .describe( + 'precompile for sending messages to L1, interface here: https://github.com/OffchainLabs/nitro-contracts/blob/90037b996509312ef1addb3f9352457b8a99d6a6/src/precompiles/ArbSys.sol#L12', + ), + bridge: z + .string() + .optional() + .describe( + 'address of the bridge contract on L1, optional only needed for non @arbitrum/sdk chains', + ), + destinationChain: z.string(), + childHook: z.lazy((): z.ZodSchema => HookConfigSchema), +}); + +export const IgpSchema = OwnableSchema.extend({ + type: z.literal(HookType.INTERCHAIN_GAS_PAYMASTER), + beneficiary: z.string(), + oracleKey: z.string(), + overhead: z.record(z.number()), + oracleConfig: z.record(StorageGasOracleConfigSchema), +}); + +export const DomainRoutingHookConfigSchema: z.ZodSchema = + z.lazy(() => + OwnableSchema.extend({ + type: z.literal(HookType.ROUTING), + domains: z.record(HookConfigSchema), + }), + ); + +export const FallbackRoutingHookConfigSchema: z.ZodSchema = + z.lazy(() => + OwnableSchema.extend({ + type: z.literal(HookType.FALLBACK_ROUTING), + domains: z.record(HookConfigSchema), + fallback: HookConfigSchema, + }), + ); + +export const AggregationHookConfigSchema: z.ZodSchema = + z.lazy(() => + z.object({ + type: z.literal(HookType.AGGREGATION), + hooks: z.array(HookConfigSchema), + }), + ); + +export const HookConfigSchema = z.union([ + ZHash, + ProtocolFeeSchema, + PausableHookSchema, + OpStackHookSchema, + MerkleTreeSchema, + IgpSchema, + DomainRoutingHookConfigSchema, + FallbackRoutingHookConfigSchema, + AggregationHookConfigSchema, + ArbL2ToL1HookSchema, +]); diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 68e474d1882..61495f53c52 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -135,12 +135,12 @@ export { } from './gas/types.js'; export { EvmHookReader } from './hook/EvmHookReader.js'; export { HyperlaneHookDeployer } from './hook/HyperlaneHookDeployer.js'; -export { HookConfigSchema } from './hook/schemas.js'; export { AggregationHookConfig, DomainRoutingHookConfig, FallbackRoutingHookConfig, HookConfig, + HookConfigSchema, HookType, IgpHookConfig, MerkleTreeHookConfig, @@ -148,8 +148,9 @@ export { PausableHookConfig, ProtocolFeeHookConfig, } from './hook/types.js'; -export { EvmIsmReader } from './ism/EvmIsmReader.js'; +export { DerivedIsmConfig, EvmIsmReader } from './ism/EvmIsmReader.js'; export { HyperlaneIsmFactory } from './ism/HyperlaneIsmFactory.js'; +export { decodeIsmMetadata } from './ism/metadata/decode.js'; export { buildAggregationIsmConfigs, buildMultisigIsmConfigs, @@ -370,6 +371,10 @@ export { EV5TxTransformerInterface } from './providers/transactions/transformer/ export { EV5InterchainAccountTxTransformerPropsSchema } from './providers/transactions/transformer/ethersV5/schemas.js'; export { EV5InterchainAccountTxTransformerProps } from './providers/transactions/transformer/ethersV5/types.js'; +export { + chainMetadataToCosmosChain, + chainMetadataToViemChain, +} from './metadata/chainMetadataConversion.js'; export { EvmGasRouterAdapter, EvmRouterAdapter, @@ -496,6 +501,7 @@ export { stopImpersonatingAccount, } from './utils/fork.js'; export { multisigIsmVerificationCost, normalizeConfig } from './utils/ism.js'; +export { HyperlaneReader } from './utils/HyperlaneReader.js'; export { MultiGeneric } from './utils/MultiGeneric.js'; export { SealevelAccountDataWrapper, @@ -503,7 +509,6 @@ export { getSealevelAccountDataSchema, } from './utils/sealevelSerialization.js'; export { getChainIdFromTxs } from './utils/transactions.js'; -export { chainMetadataToViemChain } from './utils/viem.js'; export { FeeConstantConfig, RouteBlacklist, @@ -518,8 +523,8 @@ export { AggregationIsmConfigSchema, IsmConfigSchema, MultisigIsmConfigSchema, -} from './ism/schemas.js'; -export { MailboxClientConfigSchema as mailboxClientConfigSchema } from './router/schemas.js'; +} from './ism/types.js'; +export { MailboxClientConfigSchema as mailboxClientConfigSchema } from './router/types.js'; export { CollateralConfig, NativeConfig, diff --git a/typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts b/typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts index 00c31fc715c..4f815138cc3 100644 --- a/typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts +++ b/typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import assert from 'assert'; import { expect } from 'chai'; import { Signer } from 'ethers'; diff --git a/typescript/sdk/src/ism/EvmIsmModule.ts b/typescript/sdk/src/ism/EvmIsmModule.ts index 2f4e1ee1d85..6cb0593542f 100644 --- a/typescript/sdk/src/ism/EvmIsmModule.ts +++ b/typescript/sdk/src/ism/EvmIsmModule.ts @@ -28,10 +28,10 @@ import { normalizeConfig } from '../utils/ism.js'; import { EvmIsmReader } from './EvmIsmReader.js'; import { HyperlaneIsmFactory } from './HyperlaneIsmFactory.js'; -import { IsmConfigSchema } from './schemas.js'; import { DeployedIsm, IsmConfig, + IsmConfigSchema, IsmType, MUTABLE_ISM_TYPE, RoutingIsmConfig, diff --git a/typescript/sdk/src/ism/EvmIsmReader.ts b/typescript/sdk/src/ism/EvmIsmReader.ts index 06493cb450e..6257e7a6e23 100644 --- a/typescript/sdk/src/ism/EvmIsmReader.ts +++ b/typescript/sdk/src/ism/EvmIsmReader.ts @@ -158,7 +158,7 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader { let ismType = IsmType.FALLBACK_ROUTING; try { await ism.mailbox(); - } catch (error) { + } catch { ismType = IsmType.ROUTING; this.logger.debug( 'Error accessing mailbox property, implying this is not a fallback routing ISM.', @@ -248,7 +248,7 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader { relayer, type: IsmType.TRUSTED_RELAYER, }; - } catch (error) { + } catch { this.logger.debug( 'Error accessing "trustedRelayer" property, implying this is not a Trusted Relayer ISM.', address, @@ -266,7 +266,7 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader { type: IsmType.PAUSABLE, paused, }; - } catch (error) { + } catch { this.logger.debug( 'Error accessing "paused" property, implying this is not a Pausable ISM.', address, @@ -283,7 +283,7 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader { origin: address, nativeBridge: '', // no way to extract native bridge from the ism }; - } catch (error) { + } catch { this.logger.debug( 'Error accessing "VERIFIED_MASK_INDEX" property, implying this is not an OP Stack ISM.', address, diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts index 2835e4a351f..246f3d27f62 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts @@ -466,7 +466,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { .map((log) => { try { return domainRoutingIsmFactory.interface.parseLog(log); - } catch (e) { + } catch { return undefined; } }) diff --git a/typescript/sdk/src/ism/metadata/aggregation.ts b/typescript/sdk/src/ism/metadata/aggregation.ts index 5c347a60070..0f35e7e2cf4 100644 --- a/typescript/sdk/src/ism/metadata/aggregation.ts +++ b/typescript/sdk/src/ism/metadata/aggregation.ts @@ -10,12 +10,13 @@ import { import { DerivedIsmConfig } from '../EvmIsmReader.js'; import { AggregationIsmConfig, IsmType } from '../types.js'; +import type { BaseMetadataBuilder } from './builder.js'; +import { decodeIsmMetadata } from './decode.js'; import { - BaseMetadataBuilder, MetadataBuilder, MetadataContext, StructuredMetadata, -} from './builder.js'; +} from './types.js'; // null indicates that metadata is NOT INCLUDED for this submodule // empty or 0x string indicates that metadata is INCLUDED but NULL @@ -137,7 +138,7 @@ export class AggregationMetadataBuilder implements MetadataBuilder { const range = this.metadataRange(metadata, index); if (range.start == 0) return null; if (typeof ism === 'string') return range.encoded; - return BaseMetadataBuilder.decode(range.encoded, { + return decodeIsmMetadata(range.encoded, { ...context, ism: ism as DerivedIsmConfig, }); diff --git a/typescript/sdk/src/ism/metadata/arbL2ToL1.hardhat-test.ts b/typescript/sdk/src/ism/metadata/arbL2ToL1.hardhat-test.ts index f2b66d9cb9c..e186da5c9a0 100644 --- a/typescript/sdk/src/ism/metadata/arbL2ToL1.hardhat-test.ts +++ b/typescript/sdk/src/ism/metadata/arbL2ToL1.hardhat-test.ts @@ -16,8 +16,12 @@ import { MockArbSys__factory, TestRecipient, } from '@hyperlane-xyz/core'; -import { Address, WithAddress, objMap } from '@hyperlane-xyz/utils'; -import { bytes32ToAddress } from '@hyperlane-xyz/utils'; +import { + Address, + WithAddress, + bytes32ToAddress, + objMap, +} from '@hyperlane-xyz/utils'; import { testChains } from '../../consts/testChains.js'; import { @@ -38,7 +42,7 @@ import { HyperlaneIsmFactory } from '../HyperlaneIsmFactory.js'; import { ArbL2ToL1IsmConfig } from '../types.js'; import { ArbL2ToL1MetadataBuilder } from './arbL2ToL1.js'; -import { MetadataContext } from './builder.js'; +import { MetadataContext } from './types.js'; describe('ArbL2ToL1MetadataBuilder', () => { const origin: ChainName = 'test4'; diff --git a/typescript/sdk/src/ism/metadata/arbL2ToL1.ts b/typescript/sdk/src/ism/metadata/arbL2ToL1.ts index 9b12010eb7e..5279f1dafef 100644 --- a/typescript/sdk/src/ism/metadata/arbL2ToL1.ts +++ b/typescript/sdk/src/ism/metadata/arbL2ToL1.ts @@ -20,7 +20,7 @@ import { ArbL2ToL1HookConfig } from '../../hook/types.js'; import { findMatchingLogEvents } from '../../utils/logUtils.js'; import { ArbL2ToL1IsmConfig, IsmType } from '../types.js'; -import { MetadataBuilder, MetadataContext } from './builder.js'; +import type { MetadataBuilder, MetadataContext } from './types.js'; export type NitroChildToParentTransactionEvent = EventArgs; export type ArbL2ToL1Metadata = Omit< diff --git a/typescript/sdk/src/ism/metadata/builder.hardhat-test.ts b/typescript/sdk/src/ism/metadata/builder.hardhat-test.ts index fbda40a5b50..53fc89d3d95 100644 --- a/typescript/sdk/src/ism/metadata/builder.hardhat-test.ts +++ b/typescript/sdk/src/ism/metadata/builder.hardhat-test.ts @@ -39,7 +39,9 @@ import { EvmIsmReader } from '../EvmIsmReader.js'; import { randomIsmConfig } from '../HyperlaneIsmFactory.hardhat-test.js'; import { HyperlaneIsmFactory } from '../HyperlaneIsmFactory.js'; -import { BaseMetadataBuilder, MetadataContext } from './builder.js'; +import { BaseMetadataBuilder } from './builder.js'; +import { decodeIsmMetadata } from './decode.js'; +import { MetadataContext } from './types.js'; const MAX_ISM_DEPTH = 5; const MAX_NUM_VALIDATORS = 10; @@ -198,7 +200,7 @@ describe('BaseMetadataBuilder', () => { }); it(`should decode metadata for random ism config (${i})`, async () => { - BaseMetadataBuilder.decode(metadata, context); + decodeIsmMetadata(metadata, context); }); } }); diff --git a/typescript/sdk/src/ism/metadata/builder.ts b/typescript/sdk/src/ism/metadata/builder.ts index e290f390f7d..c316c20dbf7 100644 --- a/typescript/sdk/src/ism/metadata/builder.ts +++ b/typescript/sdk/src/ism/metadata/builder.ts @@ -1,53 +1,30 @@ -/* eslint-disable no-case-declarations */ -import { TransactionReceipt } from '@ethersproject/providers'; - -import { WithAddress, assert, rootLogger } from '@hyperlane-xyz/utils'; +import { + WithAddress, + assert, + deepFind, + rootLogger, +} from '@hyperlane-xyz/utils'; -import { deepFind } from '../../../../utils/dist/objects.js'; import { HyperlaneCore } from '../../core/HyperlaneCore.js'; -import { DispatchedMessage } from '../../core/types.js'; -import { DerivedHookConfig } from '../../hook/EvmHookReader.js'; import { ArbL2ToL1HookConfig, HookType, MerkleTreeHookConfig, } from '../../hook/types.js'; import { MultiProvider } from '../../providers/MultiProvider.js'; -import { DerivedIsmConfig } from '../EvmIsmReader.js'; import { IsmType } from '../types.js'; -import { - AggregationMetadata, - AggregationMetadataBuilder, -} from './aggregation.js'; -import { ArbL2ToL1Metadata, ArbL2ToL1MetadataBuilder } from './arbL2ToL1.js'; -import { MultisigMetadata, MultisigMetadataBuilder } from './multisig.js'; -import { NullMetadata, NullMetadataBuilder } from './null.js'; -import { - DefaultFallbackRoutingMetadataBuilder, - RoutingMetadata, -} from './routing.js'; - -export type StructuredMetadata = - | NullMetadata - | MultisigMetadata - | ArbL2ToL1Metadata - | AggregationMetadata - | RoutingMetadata; - -export interface MetadataContext< - IsmContext = DerivedIsmConfig, - HookContext = DerivedHookConfig, -> { - message: DispatchedMessage; - dispatchTx: TransactionReceipt; - ism: IsmContext; - hook: HookContext; -} - -export interface MetadataBuilder { - build(context: MetadataContext): Promise; -} +import { AggregationMetadataBuilder } from './aggregation.js'; +import { ArbL2ToL1MetadataBuilder } from './arbL2ToL1.js'; +import { decodeIsmMetadata } from './decode.js'; +import { MultisigMetadataBuilder } from './multisig.js'; +import { NullMetadataBuilder } from './null.js'; +import { DefaultFallbackRoutingMetadataBuilder } from './routing.js'; +import type { + MetadataBuilder, + MetadataContext, + StructuredMetadata, +} from './types.js'; export class BaseMetadataBuilder implements MetadataBuilder { public nullMetadataBuilder: NullMetadataBuilder; @@ -91,6 +68,7 @@ export class BaseMetadataBuilder implements MetadataBuilder { if (typeof hook === 'string') { throw new Error('Hook context must be an object (for multisig ISM)'); } + // eslint-disable-next-line no-case-declarations const merkleTreeHook = deepFind( hook, (v): v is WithAddress => @@ -137,32 +115,6 @@ export class BaseMetadataBuilder implements MetadataBuilder { metadata: string, context: MetadataContext, ): StructuredMetadata { - const { ism } = context; - switch (ism.type) { - case IsmType.TRUSTED_RELAYER: - return NullMetadataBuilder.decode(ism); - - case IsmType.MERKLE_ROOT_MULTISIG: - case IsmType.MESSAGE_ID_MULTISIG: - return MultisigMetadataBuilder.decode(metadata, ism.type); - - case IsmType.AGGREGATION: - return AggregationMetadataBuilder.decode(metadata, { ...context, ism }); - - case IsmType.ROUTING: - return DefaultFallbackRoutingMetadataBuilder.decode(metadata, { - ...context, - ism, - }); - - case IsmType.ARB_L2_TO_L1: - return ArbL2ToL1MetadataBuilder.decode(metadata, { - ...context, - ism, - }); - - default: - throw new Error(`Unsupported ISM type: ${ism.type}`); - } + return decodeIsmMetadata(metadata, context); } } diff --git a/typescript/sdk/src/ism/metadata/decode.ts b/typescript/sdk/src/ism/metadata/decode.ts new file mode 100644 index 00000000000..cdd4c6194d3 --- /dev/null +++ b/typescript/sdk/src/ism/metadata/decode.ts @@ -0,0 +1,41 @@ +import { IsmType } from '../types.js'; + +import { AggregationMetadataBuilder } from './aggregation.js'; +import { ArbL2ToL1MetadataBuilder } from './arbL2ToL1.js'; +import { MultisigMetadataBuilder } from './multisig.js'; +import { NullMetadataBuilder } from './null.js'; +import { DefaultFallbackRoutingMetadataBuilder } from './routing.js'; +import { MetadataContext, StructuredMetadata } from './types.js'; + +export function decodeIsmMetadata( + metadata: string, + context: MetadataContext, +): StructuredMetadata { + const { ism } = context; + switch (ism.type) { + case IsmType.TRUSTED_RELAYER: + return NullMetadataBuilder.decode(ism); + + case IsmType.MERKLE_ROOT_MULTISIG: + case IsmType.MESSAGE_ID_MULTISIG: + return MultisigMetadataBuilder.decode(metadata, ism.type); + + case IsmType.AGGREGATION: + return AggregationMetadataBuilder.decode(metadata, { ...context, ism }); + + case IsmType.ROUTING: + return DefaultFallbackRoutingMetadataBuilder.decode(metadata, { + ...context, + ism, + }); + + case IsmType.ARB_L2_TO_L1: + return ArbL2ToL1MetadataBuilder.decode(metadata, { + ...context, + ism, + }); + + default: + throw new Error(`Unsupported ISM type: ${ism.type}`); + } +} diff --git a/typescript/sdk/src/ism/metadata/multisig.ts b/typescript/sdk/src/ism/metadata/multisig.ts index 1de554fa5d2..74e50e355b1 100644 --- a/typescript/sdk/src/ism/metadata/multisig.ts +++ b/typescript/sdk/src/ism/metadata/multisig.ts @@ -26,7 +26,7 @@ import { MerkleTreeHookConfig } from '../../hook/types.js'; import { ChainName } from '../../types.js'; import { IsmType, MultisigIsmConfig } from '../types.js'; -import { MetadataBuilder, MetadataContext } from './builder.js'; +import type { MetadataBuilder, MetadataContext } from './types.js'; interface MessageIdMultisigMetadata { type: IsmType.MESSAGE_ID_MULTISIG; diff --git a/typescript/sdk/src/ism/metadata/null.ts b/typescript/sdk/src/ism/metadata/null.ts index ed66277ce43..e4be778b737 100644 --- a/typescript/sdk/src/ism/metadata/null.ts +++ b/typescript/sdk/src/ism/metadata/null.ts @@ -3,7 +3,7 @@ import { WithAddress, assert, eqAddress } from '@hyperlane-xyz/utils'; import { MultiProvider } from '../../providers/MultiProvider.js'; import { IsmType, NullIsmConfig } from '../types.js'; -import { MetadataBuilder, MetadataContext } from './builder.js'; +import type { MetadataBuilder, MetadataContext } from './types.js'; export const NULL_METADATA = '0x'; diff --git a/typescript/sdk/src/ism/metadata/routing.ts b/typescript/sdk/src/ism/metadata/routing.ts index 36fd69e6057..6a2e2168c4d 100644 --- a/typescript/sdk/src/ism/metadata/routing.ts +++ b/typescript/sdk/src/ism/metadata/routing.ts @@ -5,12 +5,13 @@ import { ChainName } from '../../types.js'; import { DerivedIsmConfig, EvmIsmReader } from '../EvmIsmReader.js'; import { IsmType, RoutingIsmConfig } from '../types.js'; -import { - BaseMetadataBuilder, +import type { BaseMetadataBuilder } from './builder.js'; +import { decodeIsmMetadata } from './decode.js'; +import type { MetadataBuilder, MetadataContext, StructuredMetadata, -} from './builder.js'; +} from './types.js'; export type RoutingMetadata = { type: IsmType.ROUTING; @@ -45,7 +46,7 @@ export class RoutingMetadataBuilder implements MetadataBuilder { const originMetadata = typeof ism === 'string' ? metadata - : BaseMetadataBuilder.decode(metadata, { + : decodeIsmMetadata(metadata, { ...context, ism: ism as DerivedIsmConfig, }); diff --git a/typescript/sdk/src/ism/metadata/types.ts b/typescript/sdk/src/ism/metadata/types.ts new file mode 100644 index 00000000000..104ef5757d3 --- /dev/null +++ b/typescript/sdk/src/ism/metadata/types.ts @@ -0,0 +1,32 @@ +import type { providers } from 'ethers'; + +import type { DispatchedMessage } from '../../core/types.js'; +import type { DerivedHookConfig } from '../../hook/EvmHookReader.js'; +import type { DerivedIsmConfig } from '../EvmIsmReader.js'; + +import type { AggregationMetadata } from './aggregation.js'; +import type { ArbL2ToL1Metadata } from './arbL2ToL1.js'; +import type { MultisigMetadata } from './multisig.js'; +import type { NullMetadata } from './null.js'; +import type { RoutingMetadata } from './routing.js'; + +export type StructuredMetadata = + | NullMetadata + | MultisigMetadata + | ArbL2ToL1Metadata + | AggregationMetadata + | RoutingMetadata; + +export interface MetadataContext< + IsmContext = DerivedIsmConfig, + HookContext = DerivedHookConfig, +> { + message: DispatchedMessage; + dispatchTx: providers.TransactionReceipt; + ism: IsmContext; + hook: HookContext; +} + +export interface MetadataBuilder { + build(context: MetadataContext): Promise; +} diff --git a/typescript/sdk/src/ism/schemas.ts b/typescript/sdk/src/ism/schemas.ts deleted file mode 100644 index e9d2e1cda81..00000000000 --- a/typescript/sdk/src/ism/schemas.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { z } from 'zod'; - -import { ZHash } from '../metadata/customZodTypes.js'; -import { OwnableSchema, PausableSchema } from '../schemas.js'; - -import { AggregationIsmConfig, IsmType, RoutingIsmConfig } from './types.js'; - -const ValidatorInfoSchema = z.object({ - signingAddress: ZHash, - weight: z.number(), -}); - -export const TestIsmConfigSchema = z.object({ - type: z.literal(IsmType.TEST_ISM), -}); - -export const MultisigConfigSchema = z.object({ - validators: z.array(ZHash), - threshold: z.number(), -}); - -export const WeightedMultisigConfigSchema = z.object({ - validators: z.array(ValidatorInfoSchema), - thresholdWeight: z.number(), -}); - -export const TrustedRelayerIsmConfigSchema = z.object({ - type: z.literal(IsmType.TRUSTED_RELAYER), - relayer: z.string(), -}); - -export const OpStackIsmConfigSchema = z.object({ - type: z.literal(IsmType.OP_STACK), - origin: z.string(), - nativeBridge: z.string(), -}); - -export const ArbL2ToL1IsmConfigSchema = z.object({ - type: z.literal(IsmType.ARB_L2_TO_L1), - bridge: z.string(), -}); - -export const PausableIsmConfigSchema = PausableSchema.and( - z.object({ - type: z.literal(IsmType.PAUSABLE), - }), -); - -export const MultisigIsmConfigSchema = MultisigConfigSchema.and( - z.object({ - type: z.union([ - z.literal(IsmType.MERKLE_ROOT_MULTISIG), - z.literal(IsmType.MESSAGE_ID_MULTISIG), - z.literal(IsmType.STORAGE_MERKLE_ROOT_MULTISIG), - z.literal(IsmType.STORAGE_MESSAGE_ID_MULTISIG), - ]), - }), -); - -export const WeightedMultisigIsmConfigSchema = WeightedMultisigConfigSchema.and( - z.object({ - type: z.union([ - z.literal(IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG), - z.literal(IsmType.WEIGHTED_MESSAGE_ID_MULTISIG), - ]), - }), -); - -export const RoutingIsmConfigSchema: z.ZodSchema = z.lazy( - () => - OwnableSchema.extend({ - type: z.union([ - z.literal(IsmType.ROUTING), - z.literal(IsmType.FALLBACK_ROUTING), - ]), - domains: z.record(IsmConfigSchema), - }), -); - -export const AggregationIsmConfigSchema: z.ZodSchema = z - .lazy(() => - z.object({ - type: z.literal(IsmType.AGGREGATION), - modules: z.array(IsmConfigSchema), - threshold: z.number(), - }), - ) - .refine((data) => data.threshold <= data.modules.length, { - message: 'Threshold must be less than or equal to the number of modules', - }); - -export const IsmConfigSchema = z.union([ - ZHash, - TestIsmConfigSchema, - OpStackIsmConfigSchema, - PausableIsmConfigSchema, - TrustedRelayerIsmConfigSchema, - MultisigIsmConfigSchema, - WeightedMultisigIsmConfigSchema, - RoutingIsmConfigSchema, - AggregationIsmConfigSchema, - ArbL2ToL1IsmConfigSchema, -]); diff --git a/typescript/sdk/src/ism/schemas.test.ts b/typescript/sdk/src/ism/types.test.ts similarity index 85% rename from typescript/sdk/src/ism/schemas.test.ts rename to typescript/sdk/src/ism/types.test.ts index 7605382c244..9c57ebf042b 100644 --- a/typescript/sdk/src/ism/schemas.test.ts +++ b/typescript/sdk/src/ism/types.test.ts @@ -1,8 +1,7 @@ import { expect } from 'chai'; import { ethers } from 'ethers'; -import { AggregationIsmConfigSchema } from './schemas.js'; -import { IsmType } from './types.js'; +import { AggregationIsmConfigSchema, IsmType } from './types.js'; const SOME_ADDRESS = ethers.Wallet.createRandom().address; describe('AggregationIsmConfigSchema refine', () => { diff --git a/typescript/sdk/src/ism/types.ts b/typescript/sdk/src/ism/types.ts index 82e025a0491..c215c346daf 100644 --- a/typescript/sdk/src/ism/types.ts +++ b/typescript/sdk/src/ism/types.ts @@ -15,19 +15,10 @@ import { import type { Address, Domain, ValueOf } from '@hyperlane-xyz/utils'; import { OwnableConfig } from '../deploy/types.js'; +import { ZHash } from '../metadata/customZodTypes.js'; +import { OwnableSchema, PausableSchema } from '../schemas.js'; import { ChainMap } from '../types.js'; -import { - ArbL2ToL1IsmConfigSchema, - IsmConfigSchema, - MultisigIsmConfigSchema, - OpStackIsmConfigSchema, - PausableIsmConfigSchema, - TestIsmConfigSchema, - TrustedRelayerIsmConfigSchema, - WeightedMultisigIsmConfigSchema, -} from './schemas.js'; - // this enum should match the IInterchainSecurityModule.sol enum // meant for the relayer export enum ModuleType { @@ -167,3 +158,100 @@ export type RoutingIsmDelta = { owner?: Address; // is the owner different mailbox?: Address; // is the mailbox different (only for fallback routing) }; + +const ValidatorInfoSchema = z.object({ + signingAddress: ZHash, + weight: z.number(), +}); + +export const TestIsmConfigSchema = z.object({ + type: z.literal(IsmType.TEST_ISM), +}); + +export const MultisigConfigSchema = z.object({ + validators: z.array(ZHash), + threshold: z.number(), +}); + +export const WeightedMultisigConfigSchema = z.object({ + validators: z.array(ValidatorInfoSchema), + thresholdWeight: z.number(), +}); + +export const TrustedRelayerIsmConfigSchema = z.object({ + type: z.literal(IsmType.TRUSTED_RELAYER), + relayer: z.string(), +}); + +export const OpStackIsmConfigSchema = z.object({ + type: z.literal(IsmType.OP_STACK), + origin: z.string(), + nativeBridge: z.string(), +}); + +export const ArbL2ToL1IsmConfigSchema = z.object({ + type: z.literal(IsmType.ARB_L2_TO_L1), + bridge: z.string(), +}); + +export const PausableIsmConfigSchema = PausableSchema.and( + z.object({ + type: z.literal(IsmType.PAUSABLE), + }), +); + +export const MultisigIsmConfigSchema = MultisigConfigSchema.and( + z.object({ + type: z.union([ + z.literal(IsmType.MERKLE_ROOT_MULTISIG), + z.literal(IsmType.MESSAGE_ID_MULTISIG), + z.literal(IsmType.STORAGE_MERKLE_ROOT_MULTISIG), + z.literal(IsmType.STORAGE_MESSAGE_ID_MULTISIG), + ]), + }), +); + +export const WeightedMultisigIsmConfigSchema = WeightedMultisigConfigSchema.and( + z.object({ + type: z.union([ + z.literal(IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG), + z.literal(IsmType.WEIGHTED_MESSAGE_ID_MULTISIG), + ]), + }), +); + +export const RoutingIsmConfigSchema: z.ZodSchema = z.lazy( + () => + OwnableSchema.extend({ + type: z.union([ + z.literal(IsmType.ROUTING), + z.literal(IsmType.FALLBACK_ROUTING), + ]), + domains: z.record(IsmConfigSchema), + }), +); + +export const AggregationIsmConfigSchema: z.ZodSchema = z + .lazy(() => + z.object({ + type: z.literal(IsmType.AGGREGATION), + modules: z.array(IsmConfigSchema), + threshold: z.number(), + }), + ) + .refine((data) => data.threshold <= data.modules.length, { + message: 'Threshold must be less than or equal to the number of modules', + }); + +export const IsmConfigSchema = z.union([ + ZHash, + TestIsmConfigSchema, + OpStackIsmConfigSchema, + PausableIsmConfigSchema, + TrustedRelayerIsmConfigSchema, + MultisigIsmConfigSchema, + WeightedMultisigIsmConfigSchema, + RoutingIsmConfigSchema, + AggregationIsmConfigSchema, + ArbL2ToL1IsmConfigSchema, +]); diff --git a/typescript/sdk/src/ism/utils.ts b/typescript/sdk/src/ism/utils.ts index 7c28d757a12..4ad26579811 100644 --- a/typescript/sdk/src/ism/utils.ts +++ b/typescript/sdk/src/ism/utils.ts @@ -270,7 +270,7 @@ export async function moduleMatchesConfig( let mailboxAddress; try { mailboxAddress = await client.mailbox(); - } catch (error) { + } catch { matches = false; break; } diff --git a/typescript/sdk/src/metadata/chainMetadataConversion.ts b/typescript/sdk/src/metadata/chainMetadataConversion.ts new file mode 100644 index 00000000000..d0574e3017c --- /dev/null +++ b/typescript/sdk/src/metadata/chainMetadataConversion.ts @@ -0,0 +1,98 @@ +import type { AssetList, Chain as CosmosChain } from '@chain-registry/types'; +import { Chain, defineChain } from 'viem'; + +import { test1 } from '../consts/testChains.js'; +import { + ChainMetadata, + getChainIdNumber, +} from '../metadata/chainMetadataTypes.js'; + +export function chainMetadataToViemChain(metadata: ChainMetadata): Chain { + return defineChain({ + id: getChainIdNumber(metadata), + name: metadata.displayName || metadata.name, + network: metadata.name, + nativeCurrency: metadata.nativeToken || test1.nativeToken!, + rpcUrls: { + public: { http: [metadata.rpcUrls[0].http] }, + default: { http: [metadata.rpcUrls[0].http] }, + }, + blockExplorers: metadata.blockExplorers?.length + ? { + default: { + name: metadata.blockExplorers[0].name, + url: metadata.blockExplorers[0].url, + }, + } + : undefined, + testnet: !!metadata.isTestnet, + }); +} + +export function chainMetadataToCosmosChain(metadata: ChainMetadata): { + chain: CosmosChain; + assets: AssetList; +} { + const { + name, + displayName, + chainId, + rpcUrls, + restUrls, + isTestnet, + nativeToken, + bech32Prefix, + slip44, + } = metadata; + + if (!nativeToken) throw new Error(`Missing native token for ${name}`); + + const chain: CosmosChain = { + chain_name: name, + chain_type: 'cosmos', + status: 'live', + network_type: isTestnet ? 'testnet' : 'mainnet', + pretty_name: displayName || name, + chain_id: chainId as string, + bech32_prefix: bech32Prefix!, + slip44: slip44!, + apis: { + rpc: [{ address: rpcUrls[0].http, provider: displayName || name }], + rest: restUrls + ? [{ address: restUrls[0].http, provider: displayName || name }] + : [], + }, + fees: { + fee_tokens: [{ denom: 'token' }], + }, + staking: { + staking_tokens: [{ denom: 'stake' }], + }, + }; + + const assets: AssetList = { + chain_name: name, + assets: [ + { + description: `The native token of ${displayName || name} chain.`, + denom_units: [{ denom: 'token', exponent: nativeToken.decimals }], + base: 'token', + name: 'token', + display: 'token', + symbol: 'token', + type_asset: 'sdk.coin', + }, + { + description: `The native token of ${displayName || name} chain.`, + denom_units: [{ denom: 'token', exponent: nativeToken.decimals }], + base: 'stake', + name: 'stake', + display: 'stake', + symbol: 'stake', + type_asset: 'sdk.coin', + }, + ], + }; + + return { chain, assets }; +} diff --git a/typescript/sdk/src/middleware/liquidity-layer/.eslintrc b/typescript/sdk/src/middleware/liquidity-layer/.eslintrc deleted file mode 100644 index e3f712414b8..00000000000 --- a/typescript/sdk/src/middleware/liquidity-layer/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-console": ["off"] - } -} diff --git a/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerApp.ts b/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerApp.ts index 93e324a24dc..6e26989348c 100644 --- a/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerApp.ts +++ b/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerApp.ts @@ -11,6 +11,7 @@ import { addressToBytes32, ensure0x, eqAddress, + rootLogger, strip0x, } from '@hyperlane-xyz/utils'; @@ -23,6 +24,8 @@ import { fetchWithTimeout } from '../../utils/fetch.js'; import { BridgeAdapterConfig } from './LiquidityLayerRouterDeployer.js'; import { liquidityLayerFactories } from './contracts.js'; +const logger = rootLogger.child({ module: 'LiquidityLayerApp' }); + const PORTAL_VAA_SERVICE_TESTNET_BASE_URL = 'https://wormhole-v2-testnet-api.certus.one/v1/signed_vaa/'; const CIRCLE_ATTESTATIONS_TESTNET_BASE_URL = @@ -77,7 +80,7 @@ export class LiquidityLayerApp extends HyperlaneApp< } async fetchCircleMessageTransactions(chain: ChainName): Promise { - console.log(`Fetch circle messages for ${chain}`); + logger.info(`Fetch circle messages for ${chain}`); const url = new URL(this.multiProvider.getExplorerApiUrl(chain)); url.searchParams.set('module', 'logs'); url.searchParams.set('action', 'getLogs'); @@ -140,7 +143,7 @@ export class LiquidityLayerApp extends HyperlaneApp< chain: ChainName, txHash: string, ): Promise { - console.debug(`Parse Circle messages for chain ${chain} ${txHash}`); + logger.debug(`Parse Circle messages for chain ${chain} ${txHash}`); const provider = this.multiProvider.getProvider(chain); const receipt = await provider.getTransactionReceipt(txHash); const matchingLogs = receipt.logs @@ -207,7 +210,7 @@ export class LiquidityLayerApp extends HyperlaneApp< await destinationPortalAdapter.portalTransfersProcessed(transferId); if (!eqAddress(transferTokenAddress, ethers.constants.AddressZero)) { - console.log( + logger.info( `Transfer with nonce ${message.nonce} from ${message.origin} to ${message.destination} already processed`, ); return; @@ -229,11 +232,11 @@ export class LiquidityLayerApp extends HyperlaneApp< ).then((response) => response.json()); if (vaa.code && vaa.code === PORTAL_VAA_SERVICE_SUCCESS_CODE) { - console.log(`VAA not yet found for nonce ${message.nonce}`); + logger.info(`VAA not yet found for nonce ${message.nonce}`); return; } - console.debug( + logger.debug( `Complete portal transfer for nonce ${message.nonce} on ${message.destination}`, ); @@ -246,10 +249,10 @@ export class LiquidityLayerApp extends HyperlaneApp< ); } catch (error: any) { if (error?.error?.reason?.includes('no wrapper for this token')) { - console.log( + logger.info( 'No wrapper for this token, you should register the token at https://wormhole-foundation.github.io/example-token-bridge-ui/#/register', ); - console.log(message); + logger.info(message); return; } throw error; @@ -268,11 +271,11 @@ export class LiquidityLayerApp extends HyperlaneApp< const alreadyProcessed = await transmitter.usedNonces(message.nonceHash); if (alreadyProcessed) { - console.log(`Message sent on ${message.txHash} was already processed`); + logger.info(`Message sent on ${message.txHash} was already processed`); return; } - console.log(`Attempt Circle message delivery`, JSON.stringify(message)); + logger.info(`Attempt Circle message delivery`, JSON.stringify(message)); const messageHash = ethers.utils.keccak256(message.message); const baseurl = this.multiProvider.getChainMetadata(message.chain).isTestnet @@ -282,19 +285,19 @@ export class LiquidityLayerApp extends HyperlaneApp< const attestations = await attestationsB.json(); if (attestations.status !== 'complete') { - console.log( + logger.info( `Attestations not available for message nonce ${message.nonce} on ${message.txHash}`, ); return; } - console.log(`Ready to submit attestations for message ${message.nonce}`); + logger.info(`Ready to submit attestations for message ${message.nonce}`); const tx = await transmitter.receiveMessage( message.message, attestations.attestation, ); - console.log( + logger.info( `Submitted attestations in ${this.multiProvider.tryGetExplorerTxUrl( message.remoteChain, tx, diff --git a/typescript/sdk/src/providers/SmartProvider/SmartProvider.ts b/typescript/sdk/src/providers/SmartProvider/SmartProvider.ts index 58ee3e7ef1f..d30f9ddd11e 100644 --- a/typescript/sdk/src/providers/SmartProvider/SmartProvider.ts +++ b/typescript/sdk/src/providers/SmartProvider/SmartProvider.ts @@ -1,5 +1,5 @@ import { BigNumber, errors as EthersError, providers, utils } from 'ethers'; -import pino, { Logger } from 'pino'; +import { Logger, pino } from 'pino'; import { raceWithContext, @@ -126,7 +126,7 @@ export class HyperlaneSmartProvider async getPriorityFee(): Promise { try { return BigNumber.from(await this.perform('maxPriorityFeePerGas', {})); - } catch (error) { + } catch { return BigNumber.from('1500000000'); } } diff --git a/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts b/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts index 53d1b44524f..61616ff3c07 100644 --- a/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts +++ b/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts @@ -1,7 +1,6 @@ import { Logger } from 'pino'; -import { Annotated, rootLogger } from '@hyperlane-xyz/utils'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { Annotated, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; import { ProtocolTypedReceipt, diff --git a/typescript/sdk/src/router/schemas.ts b/typescript/sdk/src/router/schemas.ts deleted file mode 100644 index 571cb0d1f5a..00000000000 --- a/typescript/sdk/src/router/schemas.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { z } from 'zod'; - -import { ProxyFactoryFactoriesSchema } from '../deploy/schemas.js'; -import { HookConfigSchema } from '../hook/schemas.js'; -import { IsmConfigSchema } from '../ism/schemas.js'; -import { ZHash } from '../metadata/customZodTypes.js'; -import { DeployedOwnableSchema, OwnableSchema } from '../schemas.js'; - -export const MailboxClientConfigSchema = OwnableSchema.extend({ - mailbox: ZHash, - hook: HookConfigSchema.optional(), - interchainSecurityModule: IsmConfigSchema.optional(), - ismFactoryAddresses: ProxyFactoryFactoriesSchema.optional(), -}); - -export const ForeignDeploymentConfigSchema = z.object({ - foreignDeployment: z.string().optional(), -}); - -const RemoteRouterDomain = z.string(); -const RemoteRouterRouter = z.string().startsWith('0x'); -export const RemoteRoutersSchema = z.record( - RemoteRouterDomain, - RemoteRouterRouter, -); - -export const RouterConfigSchema = MailboxClientConfigSchema.merge( - ForeignDeploymentConfigSchema, -).merge( - z.object({ - remoteRouters: RemoteRoutersSchema.optional(), - proxyAdmin: DeployedOwnableSchema.optional(), - }), -); - -const DestinationGasDomain = z.string(); -const DestinationGasAmount = z.string(); // This must be a string type to match Ether's type -export const DestinationGasSchema = z.record( - DestinationGasDomain, - DestinationGasAmount, -); -export const GasRouterConfigSchema = RouterConfigSchema.extend({ - gas: z.number().optional(), - destinationGas: DestinationGasSchema.optional(), -}); diff --git a/typescript/sdk/src/router/types.ts b/typescript/sdk/src/router/types.ts index 17e36ead667..d950e43a30a 100644 --- a/typescript/sdk/src/router/types.ts +++ b/typescript/sdk/src/router/types.ts @@ -11,16 +11,12 @@ import { Address, AddressBytes32 } from '@hyperlane-xyz/utils'; import { HyperlaneFactories } from '../contracts/types.js'; import { UpgradeConfig } from '../deploy/proxy.js'; import { CheckerViolation } from '../deploy/types.js'; +import { HookConfigSchema } from '../hook/types.js'; +import { IsmConfigSchema } from '../ism/types.js'; +import { ZHash } from '../metadata/customZodTypes.js'; +import { DeployedOwnableSchema, OwnableSchema } from '../schemas.js'; import { ChainMap } from '../types.js'; -import { - DestinationGasSchema, - GasRouterConfigSchema, - MailboxClientConfigSchema, - RemoteRoutersSchema, - RouterConfigSchema, -} from './schemas.js'; - export type RouterAddress = { router: Address; }; @@ -68,3 +64,40 @@ export interface RouterViolation extends CheckerViolation { export type RemoteRouters = z.infer; export type DestinationGas = z.infer; + +export const MailboxClientConfigSchema = OwnableSchema.extend({ + mailbox: ZHash, + hook: HookConfigSchema.optional(), + interchainSecurityModule: IsmConfigSchema.optional(), +}); + +export const ForeignDeploymentConfigSchema = z.object({ + foreignDeployment: z.string().optional(), +}); + +const RemoteRouterDomain = z.string(); +const RemoteRouterRouter = z.string().startsWith('0x'); +export const RemoteRoutersSchema = z.record( + RemoteRouterDomain, + RemoteRouterRouter, +); + +export const RouterConfigSchema = MailboxClientConfigSchema.merge( + ForeignDeploymentConfigSchema, +).merge( + z.object({ + remoteRouters: RemoteRoutersSchema.optional(), + proxyAdmin: DeployedOwnableSchema.optional(), + }), +); + +const DestinationGasDomain = z.string(); +const DestinationGasAmount = z.string(); // This must be a string type to match Ether's type +export const DestinationGasSchema = z.record( + DestinationGasDomain, + DestinationGasAmount, +); +export const GasRouterConfigSchema = RouterConfigSchema.extend({ + gas: z.number().optional(), + destinationGas: DestinationGasSchema.optional(), +}); diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts index 5895d783fc3..7ed443eb6c4 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -123,6 +123,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { chain, config, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); // Let's derive it's onchain token type @@ -151,6 +152,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { chain, config, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); // Let's derive it's onchain token type @@ -187,6 +189,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { chain, config, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); // Let's derive it's onchain token type @@ -219,6 +222,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { chain, config, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); // Let's derive it's onchain token type @@ -249,6 +253,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { chain, config, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); const { remoteRouters } = await evmERC20WarpModule.read(); expect(Object.keys(remoteRouters!).length).to.equal(numOfRouters); @@ -285,13 +290,14 @@ describe('EvmERC20WarpHyperlaneModule', async () => { chain, config, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); for (const interchainSecurityModule of ismConfigToUpdate) { const expectedConfig: TokenRouterConfig = { ...actualConfig, - ismFactoryAddresses, + interchainSecurityModule, }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); @@ -316,6 +322,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { chain, config, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); @@ -327,7 +334,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }; const expectedConfig: TokenRouterConfig = { ...actualConfig, - ismFactoryAddresses, interchainSecurityModule, }; @@ -374,11 +380,11 @@ describe('EvmERC20WarpHyperlaneModule', async () => { chain, config, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); const expectedConfig: TokenRouterConfig = { ...actualConfig, - ismFactoryAddresses, interchainSecurityModule: { type: IsmType.ROUTING, owner: randomAddress(), @@ -415,6 +421,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { interchainSecurityModule: ismAddress, }, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); const numOfRouters = Math.floor(Math.random() * 10); await sendTxs( @@ -446,6 +453,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { interchainSecurityModule: ismAddress, }, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); const remoteRouters = randomRemoteRouters(1); await sendTxs( @@ -498,6 +506,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { interchainSecurityModule: ismAddress, }, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); const currentConfig = await evmERC20WarpModule.read(); @@ -527,7 +536,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { ...baseConfig, type: TokenType.native, hook: hookAddress, - ismFactoryAddresses, }; const owner = signer.address.toLowerCase(); @@ -538,6 +546,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { interchainSecurityModule: ismAddress, }, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); const currentConfig = await evmERC20WarpModule.read(); @@ -571,7 +580,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { ...baseConfig, type: TokenType.native, hook: hookAddress, - ismFactoryAddresses, remoteRouters: { [domain]: randomAddress(), }, @@ -584,6 +592,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { ...config, }, multiProvider, + proxyFactoryFactories: ismFactoryAddresses, }); await sendTxs( await evmERC20WarpModule.update({ diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.ts b/typescript/sdk/src/token/EvmERC20WarpModule.ts index 2652c782697..729842fca47 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.ts @@ -6,7 +6,11 @@ import { TokenRouter__factory, } from '@hyperlane-xyz/core'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; -import { ContractVerifier, ExplorerLicenseType } from '@hyperlane-xyz/sdk'; +import { + ContractVerifier, + ExplorerLicenseType, + HyperlaneAddresses, +} from '@hyperlane-xyz/sdk'; import { Address, Domain, @@ -25,6 +29,7 @@ import { HyperlaneModule, HyperlaneModuleParams, } from '../core/AbstractHyperlaneModule.js'; +import { ProxyFactoryFactories } from '../deploy/contracts.js'; import { proxyAdminUpdateTxs } from '../deploy/proxy.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js'; import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; @@ -40,7 +45,7 @@ import { TokenRouterConfig, TokenRouterConfigSchema } from './schemas.js'; export class EvmERC20WarpModule extends HyperlaneModule< ProtocolType.Ethereum, TokenRouterConfig, - { + HyperlaneAddresses & { deployedTokenRoute: Address; } > { @@ -56,7 +61,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< protected readonly multiProvider: MultiProvider, args: HyperlaneModuleParams< TokenRouterConfig, - { + HyperlaneAddresses & { deployedTokenRoute: Address; } >, @@ -242,36 +247,34 @@ export class EvmERC20WarpModule extends HyperlaneModule< return []; } - if (expectedConfig.ismFactoryAddresses) { - const actualDeployedIsm = ( - actualConfig.interchainSecurityModule as DerivedIsmConfig - ).address; - - // Try to update (may also deploy) Ism with the expected config - const { - deployedIsm: expectedDeployedIsm, - updateTransactions: ismUpdateTransactions, - } = await this.deployOrUpdateIsm(actualConfig, expectedConfig); - - // If an ISM is updated in-place, push the update txs - updateTransactions.push(...ismUpdateTransactions); - - // If a new ISM is deployed, push the setInterchainSecurityModule tx - if (actualDeployedIsm !== expectedDeployedIsm) { - const contractToUpdate = MailboxClient__factory.connect( - this.args.addresses.deployedTokenRoute, - this.multiProvider.getProvider(this.domainId), - ); - updateTransactions.push({ - chainId: this.chainId, - annotation: `Setting ISM for Warp Route to ${expectedDeployedIsm}`, - to: contractToUpdate.address, - data: contractToUpdate.interface.encodeFunctionData( - 'setInterchainSecurityModule', - [expectedDeployedIsm], - ), - }); - } + const actualDeployedIsm = ( + actualConfig.interchainSecurityModule as DerivedIsmConfig + ).address; + + // Try to update (may also deploy) Ism with the expected config + const { + deployedIsm: expectedDeployedIsm, + updateTransactions: ismUpdateTransactions, + } = await this.deployOrUpdateIsm(actualConfig, expectedConfig); + + // If an ISM is updated in-place, push the update txs + updateTransactions.push(...ismUpdateTransactions); + + // If a new ISM is deployed, push the setInterchainSecurityModule tx + if (actualDeployedIsm !== expectedDeployedIsm) { + const contractToUpdate = MailboxClient__factory.connect( + this.args.addresses.deployedTokenRoute, + this.multiProvider.getProvider(this.domainId), + ); + updateTransactions.push({ + chainId: this.chainId, + annotation: `Setting ISM for Warp Route to ${expectedDeployedIsm}`, + to: contractToUpdate.address, + data: contractToUpdate.interface.encodeFunctionData( + 'setInterchainSecurityModule', + [expectedDeployedIsm], + ), + }); } return updateTransactions; @@ -313,10 +316,6 @@ export class EvmERC20WarpModule extends HyperlaneModule< expectedConfig.interchainSecurityModule, 'Ism not derived correctly', ); - assert( - expectedConfig.ismFactoryAddresses, - 'Ism Factories addresses not provided', - ); const ismModule = new EvmIsmModule( this.multiProvider, @@ -324,7 +323,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< chain: this.args.chain, config: expectedConfig.interchainSecurityModule, addresses: { - ...expectedConfig.ismFactoryAddresses, + ...this.args.addresses, mailbox: expectedConfig.mailbox, deployedIsm: ( actualConfig.interchainSecurityModule as DerivedIsmConfig @@ -357,8 +356,15 @@ export class EvmERC20WarpModule extends HyperlaneModule< config: TokenRouterConfig; multiProvider: MultiProvider; contractVerifier?: ContractVerifier; + proxyFactoryFactories: HyperlaneAddresses; }): Promise { - const { chain, config, multiProvider, contractVerifier } = params; + const { + chain, + config, + multiProvider, + contractVerifier, + proxyFactoryFactories, + } = params; const chainName = multiProvider.getChainName(chain); const deployer = new HypERC20Deployer(multiProvider); const deployedContracts = await deployer.deployContracts(chainName, config); @@ -367,6 +373,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< multiProvider, { addresses: { + ...proxyFactoryFactories, deployedTokenRoute: deployedContracts[config.type].address, }, chain, diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index df2ab1affec..d65f4fad7bf 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -124,7 +124,7 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { const warpRoute = factory.connect(warpRouteAddress, this.provider); await warpRoute[method](); return tokenType as TokenType; - } catch (e) { + } catch { continue; } finally { this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger diff --git a/typescript/sdk/src/token/Token.ts b/typescript/sdk/src/token/Token.ts index b639159692d..0217802e7cf 100644 --- a/typescript/sdk/src/token/Token.ts +++ b/typescript/sdk/src/token/Token.ts @@ -40,6 +40,7 @@ import { } from './adapters/CosmosTokenAdapter.js'; import { EvmHypCollateralAdapter, + EvmHypCollateralFiatAdapter, EvmHypNativeAdapter, EvmHypSyntheticAdapter, EvmHypXERC20Adapter, @@ -192,13 +193,16 @@ export class Token implements IToken { }); } else if ( standard === TokenStandard.EvmHypCollateral || - standard === TokenStandard.EvmHypCollateralFiat || standard === TokenStandard.EvmHypOwnerCollateral || standard === TokenStandard.EvmHypRebaseCollateral ) { return new EvmHypCollateralAdapter(chainName, multiProvider, { token: addressOrDenom, }); + } else if (standard === TokenStandard.EvmHypCollateralFiat) { + return new EvmHypCollateralFiatAdapter(chainName, multiProvider, { + token: addressOrDenom, + }); } else if ( standard === TokenStandard.EvmHypSynthetic || standard === TokenStandard.EvmHypSyntheticRebase diff --git a/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.test.ts b/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.test.ts index 1205d123bba..f699780c68d 100644 --- a/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.test.ts +++ b/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ - /* eslint-disable no-console */ import { CosmWasmClient, diff --git a/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts b/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts index 794d5cdff97..cd9910a5a23 100644 --- a/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts @@ -261,9 +261,7 @@ export class EvmHypCollateralAdapter return this.wrappedTokenAddress!; } - protected async getWrappedTokenAdapter(): Promise< - ITokenAdapter - > { + protected async getWrappedTokenAdapter(): Promise { return new EvmTokenAdapter(this.chainName, this.multiProvider, { token: await this.getWrappedTokenAddress(), }); @@ -304,6 +302,21 @@ export class EvmHypCollateralAdapter } } +export class EvmHypCollateralFiatAdapter + extends EvmHypCollateralAdapter + implements IHypTokenAdapter +{ + /** + * Note this may be inaccurate, as this returns the total supply + * of the fiat token, which may be used by other bridges. + * However this is the best we can do with a simple view call. + */ + override async getBridgedSupply(): Promise { + const wrapped = await this.getWrappedTokenAdapter(); + return wrapped.getTotalSupply(); + } +} + // Interacts with HypXERC20Lockbox contracts export class EvmHypXERC20LockboxAdapter extends EvmHypCollateralAdapter diff --git a/typescript/sdk/src/token/checker.ts b/typescript/sdk/src/token/checker.ts index 9dcc0569ba6..156658ddc11 100644 --- a/typescript/sdk/src/token/checker.ts +++ b/typescript/sdk/src/token/checker.ts @@ -73,7 +73,7 @@ export class HypERC20Checker extends ProxiedRouterChecker< from: await this.multiProvider.getSignerAddress(chain), value: BigNumber.from(1), }); - } catch (e) { + } catch { const violation: TokenMismatchViolation = { type: 'deployed token not payable', chain, diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index aa7376c8836..87f77bc8829 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { constants } from 'ethers'; import { diff --git a/typescript/sdk/src/token/schemas.ts b/typescript/sdk/src/token/schemas.ts index ae9ee15a418..7c975530f10 100644 --- a/typescript/sdk/src/token/schemas.ts +++ b/typescript/sdk/src/token/schemas.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { objMap } from '@hyperlane-xyz/utils'; -import { GasRouterConfigSchema } from '../router/schemas.js'; +import { GasRouterConfigSchema } from '../router/types.js'; import { isCompliant } from '../utils/schemas.js'; import { TokenType } from './config.js'; diff --git a/typescript/sdk/src/utils/.eslintrc b/typescript/sdk/src/utils/.eslintrc deleted file mode 100644 index ba8754a123d..00000000000 --- a/typescript/sdk/src/utils/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "@typescript-eslint/explicit-module-boundary-types": ["off"] - } -} diff --git a/typescript/sdk/src/utils/gnosisSafe.js b/typescript/sdk/src/utils/gnosisSafe.js index c5c14fca8d7..51235e21212 100644 --- a/typescript/sdk/src/utils/gnosisSafe.js +++ b/typescript/sdk/src/utils/gnosisSafe.js @@ -103,7 +103,7 @@ export async function canProposeSafeTransactions( let safeService; try { safeService = getSafeService(chain, multiProvider); - } catch (e) { + } catch { return false; } const safe = await getSafe(chain, multiProvider, safeAddress); diff --git a/typescript/sdk/src/utils/ism.ts b/typescript/sdk/src/utils/ism.ts index 92c267e662a..669cf7e9d59 100644 --- a/typescript/sdk/src/utils/ism.ts +++ b/typescript/sdk/src/utils/ism.ts @@ -25,7 +25,7 @@ function lowerCaseConfig(obj: any): any { } else if (obj !== null && typeof obj === 'object') { const newObj: any = {}; for (const key in obj) { - if (key !== 'address') { + if (key !== 'address' && key !== 'ownerOverrides') { newObj[key] = key === 'type' ? obj[key] : normalizeConfig(obj[key]); } } diff --git a/typescript/sdk/src/utils/logUtils.ts b/typescript/sdk/src/utils/logUtils.ts index 5e2eeb3665e..b35a8e51ac9 100644 --- a/typescript/sdk/src/utils/logUtils.ts +++ b/typescript/sdk/src/utils/logUtils.ts @@ -10,7 +10,7 @@ export function findMatchingLogEvents( .map((log) => { try { return iface.parseLog(log); - } catch (e) { + } catch { return undefined; } }) diff --git a/typescript/sdk/src/utils/sealevelSerialization.ts b/typescript/sdk/src/utils/sealevelSerialization.ts index 13b3dcacd39..8d0907c22d0 100644 --- a/typescript/sdk/src/utils/sealevelSerialization.ts +++ b/typescript/sdk/src/utils/sealevelSerialization.ts @@ -10,7 +10,6 @@ export class SealevelAccountDataWrapper { initialized!: boolean; discriminator?: unknown; data!: T; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types constructor(public readonly fields: any) { Object.assign(this, fields); } diff --git a/typescript/sdk/src/utils/viem.ts b/typescript/sdk/src/utils/viem.ts deleted file mode 100644 index 38b574b2ddd..00000000000 --- a/typescript/sdk/src/utils/viem.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Chain, defineChain } from 'viem'; - -import { test1 } from '../consts/testChains.js'; -import { - ChainMetadata, - getChainIdNumber, -} from '../metadata/chainMetadataTypes.js'; - -export function chainMetadataToViemChain(metadata: ChainMetadata): Chain { - return defineChain({ - id: getChainIdNumber(metadata), - name: metadata.displayName || metadata.name, - network: metadata.name, - nativeCurrency: metadata.nativeToken || test1.nativeToken!, - rpcUrls: { - public: { http: [metadata.rpcUrls[0].http] }, - default: { http: [metadata.rpcUrls[0].http] }, - }, - blockExplorers: metadata.blockExplorers?.length - ? { - default: { - name: metadata.blockExplorers[0].name, - url: metadata.blockExplorers[0].url, - }, - } - : undefined, - testnet: !!metadata.isTestnet, - }); -} diff --git a/typescript/utils/CHANGELOG.md b/typescript/utils/CHANGELOG.md index cecdf5b646f..9d852ab8669 100644 --- a/typescript/utils/CHANGELOG.md +++ b/typescript/utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @hyperlane-xyz/utils +## 7.1.0 + +### Minor Changes + +- 0e285a443: Add an isRelativeUrl function + ## 7.0.0 ### Major Changes diff --git a/typescript/utils/eslint.config.mjs b/typescript/utils/eslint.config.mjs new file mode 100644 index 00000000000..b82e3eedb1e --- /dev/null +++ b/typescript/utils/eslint.config.mjs @@ -0,0 +1,3 @@ +import MonorepoDefaults from '../../eslint.config.mjs'; + +export default [...MonorepoDefaults, { files: ['./src/**/*.ts'] }]; diff --git a/typescript/utils/package.json b/typescript/utils/package.json index 0442c8e62e6..4da150a9284 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/utils", "description": "General utilities and types for the Hyperlane network", - "version": "7.0.0", + "version": "7.1.0", "dependencies": { "@cosmjs/encoding": "^0.32.4", "@solana/web3.js": "^1.95.4", @@ -12,11 +12,18 @@ "yaml": "2.4.5" }, "devDependencies": { + "@eslint/js": "^9.15.0", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.1", "@types/sinon": "^17.0.1", "@types/sinon-chai": "^3.2.12", - "chai": "4.5.0", + "@typescript-eslint/eslint-plugin": "^8.1.6", + "@typescript-eslint/parser": "^8.1.6", + "chai": "^4.5.0", + "eslint": "^9.15.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", "mocha": "^10.2.0", "prettier": "^2.8.8", "sinon": "^13.0.2", @@ -36,6 +43,7 @@ "build": "tsc", "clean": "rm -rf ./dist", "check": "tsc --noEmit", + "lint": "eslint -c ./eslint.config.mjs", "prettier": "prettier --write ./src", "test": "mocha --config .mocharc.json './src/**/*.test.ts'", "test:ci": "yarn test" diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index b43d22d96ee..93426071efb 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -80,7 +80,7 @@ export function isValidAddressEvm(address: Address) { try { const isValid = address && ethersUtils.isAddress(address); return !!isValid; - } catch (error) { + } catch { return false; } } @@ -90,7 +90,7 @@ export function isValidAddressSealevel(address: Address) { try { const isValid = address && new PublicKey(address).toBase58(); return !!isValid; - } catch (error) { + } catch { return false; } } @@ -104,7 +104,7 @@ export function isValidAddressCosmos(address: Address) { COSMOS_FACTORY_TOKEN_REGEX.test(address) || fromBech32(address)); return !!isValid; - } catch (error) { + } catch { return false; } } @@ -126,7 +126,7 @@ export function normalizeAddressEvm(address: Address) { if (isZeroishAddress(address)) return address; try { return ethersUtils.getAddress(address); - } catch (error) { + } catch { return address; } } @@ -135,7 +135,7 @@ export function normalizeAddressSealevel(address: Address) { if (isZeroishAddress(address)) return address; try { return new PublicKey(address).toBase58(); - } catch (error) { + } catch { return address; } } @@ -144,7 +144,7 @@ export function normalizeAddressCosmos(address: Address) { if (isZeroishAddress(address)) return address; try { return normalizeBech32(address); - } catch (error) { + } catch { return address; } } diff --git a/typescript/utils/src/amount.ts b/typescript/utils/src/amount.ts index f415c268d37..ab8a3b33476 100644 --- a/typescript/utils/src/amount.ts +++ b/typescript/utils/src/amount.ts @@ -82,7 +82,7 @@ export function tryParseAmount( const parsed = BigNumber(value); if (!parsed || parsed.isNaN() || !parsed.isFinite()) return null; else return parsed; - } catch (error) { + } catch { return null; } } diff --git a/typescript/utils/src/base64.ts b/typescript/utils/src/base64.ts index 1d2ae02fab8..d09c4502e28 100644 --- a/typescript/utils/src/base64.ts +++ b/typescript/utils/src/base64.ts @@ -4,7 +4,7 @@ export function toBase64(data: any): string | undefined { try { if (!data) throw new Error('No data to encode'); return btoa(JSON.stringify(data)); - } catch (error) { + } catch { rootLogger.error('Unable to serialize + encode data to base64', data); return undefined; } @@ -15,7 +15,7 @@ export function fromBase64(data: string | string[]): T | undefined { if (!data) throw new Error('No data to decode'); const msg = Array.isArray(data) ? data[0] : data; return JSON.parse(atob(msg)); - } catch (error) { + } catch { rootLogger.error('Unable to decode + deserialize data from base64', data); return undefined; } diff --git a/typescript/utils/src/big-numbers.ts b/typescript/utils/src/big-numbers.ts index fa20e0759d1..51e7a31ba98 100644 --- a/typescript/utils/src/big-numbers.ts +++ b/typescript/utils/src/big-numbers.ts @@ -15,7 +15,7 @@ export function isBigNumberish( try { const val = BigNumber(value!); return !val.isNaN() && val.isFinite() && BigNumber.isBigNumber(val); - } catch (error) { + } catch { return false; } } @@ -28,7 +28,7 @@ export function isBigNumberish( export function isZeroish(value: BigNumber.Value): boolean { try { return BigNumber(value).isZero(); - } catch (error) { + } catch { return false; } } diff --git a/typescript/utils/src/env.ts b/typescript/utils/src/env.ts index ff24f2486ad..8841f56a805 100644 --- a/typescript/utils/src/env.ts +++ b/typescript/utils/src/env.ts @@ -3,7 +3,7 @@ export function safelyAccessEnvVar(name: string, toLowerCase = false) { try { return toLowerCase ? process.env[name]?.toLowerCase() : process.env[name]; - } catch (error) { + } catch { return undefined; } } diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index b6e68e660ca..3c4c413f7c1 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -109,6 +109,7 @@ export { arrayToObject, deepCopy, deepEquals, + deepFind, diffObjMerge, invertKeysAndValues, isObjEmpty, @@ -140,6 +141,7 @@ export { streamToString, toHexString, toTitleCase, + toUpperCamelCase, trimToLength, } from './strings.js'; export { isNullish, isNumeric } from './typeof.js'; diff --git a/typescript/utils/src/strings.ts b/typescript/utils/src/strings.ts index 26d40838aa1..bc50107bba3 100644 --- a/typescript/utils/src/strings.ts +++ b/typescript/utils/src/strings.ts @@ -6,6 +6,10 @@ export function toTitleCase(str: string) { }); } +export function toUpperCamelCase(string: string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + // Only allows letters and numbers const alphanumericRgex = /[^a-zA-Z0-9]/gi; export function sanitizeString(str: string) { diff --git a/typescript/utils/src/url.ts b/typescript/utils/src/url.ts index 98f9ee412d1..0d1c2b0abc2 100644 --- a/typescript/utils/src/url.ts +++ b/typescript/utils/src/url.ts @@ -3,7 +3,7 @@ export function isUrl(value?: string | null) { if (!value) return false; const url = new URL(value); return !!url.hostname; - } catch (error) { + } catch { return false; } } @@ -13,7 +13,7 @@ export function isHttpsUrl(value?: string | null) { if (!value) return false; const url = new URL(value); return url.protocol === 'https:'; - } catch (error) { + } catch { return false; } } diff --git a/typescript/widgets/.eslintignore b/typescript/widgets/.eslintignore deleted file mode 100644 index c047b2694b9..00000000000 --- a/typescript/widgets/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules -dist -coverage -tailwind.config.js -postcss.config.js -src/stories/**/*.stories.tsx diff --git a/typescript/widgets/.eslintrc b/typescript/widgets/.eslintrc deleted file mode 100644 index 5cf219d569f..00000000000 --- a/typescript/widgets/.eslintrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": [ - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "prettier" - ], - "plugins": ["react", "react-hooks"], - "rules": { - // TODO use utils rootLogger in widgets lib - "no-console": ["off"], - "react/react-in-jsx-scope": "off", - "react/prop-types": "off", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn" - } -} diff --git a/typescript/widgets/.storybook/main.ts b/typescript/widgets/.storybook/main.ts index d4aa7c99489..b14ff0a597d 100644 --- a/typescript/widgets/.storybook/main.ts +++ b/typescript/widgets/.storybook/main.ts @@ -10,6 +10,11 @@ const config: StorybookConfig = { '@storybook/addon-onboarding', '@storybook/addon-interactions', ], + refs: { + '@chakra-ui/react': { + disable: true, + }, + }, framework: { name: '@storybook/react-vite', options: {}, diff --git a/typescript/widgets/CHANGELOG.md b/typescript/widgets/CHANGELOG.md index d1ec7cde41d..bcfbecee33d 100644 --- a/typescript/widgets/CHANGELOG.md +++ b/typescript/widgets/CHANGELOG.md @@ -1,5 +1,30 @@ # @hyperlane-xyz/widgets +## 7.1.0 + +### Minor Changes + +- 0cd65c571: Add multi-protocol wallet integration hooks and types +- 186663505: New Icons + Updated modal with new props + Updated storybook for modal and icon list +- 0e285a443: Add various utility hooks: useIsSsr, useTimeout, useDebounce, useInterval +- 92b5fe777: Props and style update: IconButton and Tooltip + New Icons: XCircleIcon and SwapIcon + +### Patch Changes + +- 794501ba6: Prevent propagation of form submissions from ChainSearchMenu +- Updated dependencies [6f2d50fbd] +- Updated dependencies [1159e0f4b] +- Updated dependencies [0e285a443] +- Updated dependencies [ff2b4e2fb] +- Updated dependencies [0e285a443] +- Updated dependencies [5db46bd31] +- Updated dependencies [0cd65c571] + - @hyperlane-xyz/sdk@7.1.0 + - @hyperlane-xyz/utils@7.1.0 + ## 7.0.0 ### Patch Changes diff --git a/typescript/widgets/eslint.config.mjs b/typescript/widgets/eslint.config.mjs new file mode 100644 index 00000000000..e9f4b3ff393 --- /dev/null +++ b/typescript/widgets/eslint.config.mjs @@ -0,0 +1,40 @@ +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; + +import MonorepoDefaults, {compat} from '../../eslint.config.mjs'; + +export default [ + ...MonorepoDefaults, + ...compat.extends("plugin:react/recommended", "plugin:react-hooks/recommended"), + { + settings: { + react: { + version: '18', + defaultVersion: '18', + }, + }, + }, + { + files: ['./src/**/*.ts', './src/**/*.tsx'], + plugins: { + react, + 'react-hooks': reactHooks, + }, + + + rules: { + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + }, + { + ignores: [ + '**/src/stories/*', + 'tailwind.config.js', + 'postcss.config.js', + '.storybook/*', + ], + }, +]; diff --git a/typescript/widgets/mg.eslint.config.mjs b/typescript/widgets/mg.eslint.config.mjs new file mode 100644 index 00000000000..a66e159b3d2 --- /dev/null +++ b/typescript/widgets/mg.eslint.config.mjs @@ -0,0 +1,38 @@ +import { fixupConfigRules, fixupPluginRules } from "@eslint/compat"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [...fixupConfigRules( + compat.extends("plugin:react/recommended", "plugin:react-hooks/recommended", "prettier"), +), { + plugins: { + react: fixupPluginRules(react), + "react-hooks": fixupPluginRules(reactHooks), + }, + + settings: { + react: { + version: "18", + defaultVersion: "18", + }, + }, + + rules: { + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + }, +}]; \ No newline at end of file diff --git a/typescript/widgets/package.json b/typescript/widgets/package.json index 27f88002bb8..ec40cfe6d63 100644 --- a/typescript/widgets/package.json +++ b/typescript/widgets/package.json @@ -1,20 +1,33 @@ { "name": "@hyperlane-xyz/widgets", "description": "Common react components for Hyperlane projects", - "version": "7.0.0", + "version": "7.1.0", "peerDependencies": { "react": "^18", "react-dom": "^18" }, "dependencies": { + "@cosmos-kit/react": "^2.18.0", "@headlessui/react": "^2.1.8", - "@hyperlane-xyz/sdk": "7.0.0", - "@hyperlane-xyz/utils": "7.0.0", + "@hyperlane-xyz/sdk": "7.1.0", + "@hyperlane-xyz/utils": "7.1.0", + "@interchain-ui/react": "^1.23.28", + "@rainbow-me/rainbowkit": "^2.2.0", + "@solana/wallet-adapter-react": "^0.15.32", + "@solana/wallet-adapter-react-ui": "^0.9.31", + "@solana/web3.js": "^1.95.4", "clsx": "^2.1.1", - "react-tooltip": "^5.28.0" + "react-tooltip": "^5.28.0", + "viem": "^2.21.45", + "wagmi": "^2.12.26" }, "devDependencies": { - "@hyperlane-xyz/registry": "4.7.0", + "@chakra-ui/react": "^2.8.2", + "@cosmjs/cosmwasm-stargate": "^0.32.4", + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "@eslint/js": "^9.15.0", + "@hyperlane-xyz/registry": "6.1.0", "@storybook/addon-essentials": "^7.6.14", "@storybook/addon-interactions": "^7.6.14", "@storybook/addon-links": "^7.6.14", @@ -23,18 +36,22 @@ "@storybook/react": "^7.6.14", "@storybook/react-vite": "^7.6.14", "@storybook/test": "^7.6.14", - "@types/node": "^18.11.18", + "@tanstack/react-query": "^5.59.20", + "@types/node": "^18.14.5", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@types/ws": "^8.5.5", - "@typescript-eslint/eslint-plugin": "^7.4.0", - "@typescript-eslint/parser": "^7.4.0", + "@typescript-eslint/eslint-plugin": "^8.1.6", + "@typescript-eslint/parser": "^8.1.6", "babel-loader": "^8.3.0", - "eslint": "^8.57.0", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-storybook": "^0.6.15", + "eslint-plugin-storybook": "^0.11.1", + "framer-motion": "^10.16.4", "postcss": "^8.4.21", "prettier": "^2.8.8", "react": "^18.2.0", @@ -72,7 +89,7 @@ "build:ts": "tsc", "build:css": "tailwindcss -c ./tailwind.config.cjs -i ./src/styles.css -o ./dist/styles.css --minify", "clean": "rm -rf ./dist ./cache ./storybook-static", - "lint": "eslint ./src --ext .ts", + "lint": "eslint -c ./eslint.config.mjs", "prettier": "prettier --write ./src", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" diff --git a/typescript/widgets/src/chains/ChainAddMenu.tsx b/typescript/widgets/src/chains/ChainAddMenu.tsx index 102b1bf5e1a..032366f01ed 100644 --- a/typescript/widgets/src/chains/ChainAddMenu.tsx +++ b/typescript/widgets/src/chains/ChainAddMenu.tsx @@ -1,4 +1,4 @@ -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { useState } from 'react'; import { DEFAULT_GITHUB_REGISTRY } from '@hyperlane-xyz/registry'; @@ -21,6 +21,7 @@ import { CopyButton } from '../components/CopyButton.js'; import { LinkButton } from '../components/LinkButton.js'; import { ChevronIcon } from '../icons/Chevron.js'; import { PlusIcon } from '../icons/Plus.js'; +import { widgetLogger } from '../logger.js'; export interface ChainAddMenuProps { chainMetadata: ChainMap; @@ -143,7 +144,7 @@ function tryParseMetadataInput( const result = ChainMetadataSchema.safeParse(parsed.data); if (!result.success) { - console.error('Error validating chain config', result.error); + widgetLogger.error('Error validating chain config', result.error); const firstIssue = result.error.issues[0]; return failure(`${firstIssue.path} => ${firstIssue.message}`); } diff --git a/typescript/widgets/src/chains/ChainDetailsMenu.tsx b/typescript/widgets/src/chains/ChainDetailsMenu.tsx index 7c175d0d887..468b60a8e44 100644 --- a/typescript/widgets/src/chains/ChainDetailsMenu.tsx +++ b/typescript/widgets/src/chains/ChainDetailsMenu.tsx @@ -1,4 +1,4 @@ -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'; import { stringify as yamlStringify } from 'yaml'; diff --git a/typescript/widgets/src/chains/ChainLogo.tsx b/typescript/widgets/src/chains/ChainLogo.tsx index 082c8eaf3f9..0f58c66f1be 100644 --- a/typescript/widgets/src/chains/ChainLogo.tsx +++ b/typescript/widgets/src/chains/ChainLogo.tsx @@ -4,6 +4,7 @@ import type { IRegistry } from '@hyperlane-xyz/registry'; import { Circle } from '../icons/Circle.js'; import { QuestionMarkIcon } from '../icons/QuestionMark.js'; +import { widgetLogger } from '../logger.js'; type SvgIcon = (props: { width: number; @@ -41,8 +42,8 @@ export function ChainLogo({ registry .getChainLogoUri(chainName) .then((uri) => uri && setSvgLogos({ ...svgLogos, [chainName]: uri })) - .catch((err) => console.error(err)); - }, [chainName, registry, svgLogos, Icon]); + .catch((err) => widgetLogger.error('Error fetching log uri', err)); + }, [chainName, logoUri, registry, svgLogos, Icon]); if (!uri && !Icon) { return ( diff --git a/typescript/widgets/src/chains/ChainSearchMenu.tsx b/typescript/widgets/src/chains/ChainSearchMenu.tsx index 3d88152f5de..a945e4bd93b 100644 --- a/typescript/widgets/src/chains/ChainSearchMenu.tsx +++ b/typescript/widgets/src/chains/ChainSearchMenu.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { ChainMap, @@ -77,11 +77,11 @@ export function ChainSearchMenu({ showAddChainMenu, defaultSortField, }: ChainSearchMenuProps) { - const [drilldownChain, setDrilldownChain] = React.useState< - ChainName | undefined - >(showChainDetails); + const [drilldownChain, setDrilldownChain] = useState( + showChainDetails, + ); - const [addChain, setAddChain] = React.useState(showAddChainMenu || false); + const [addChain, setAddChain] = useState(showAddChainMenu || false); const { listData, mergedMetadata } = useMemo(() => { const mergedMetadata = mergeChainMetadataMap( @@ -89,7 +89,7 @@ export function ChainSearchMenu({ overrideChainMetadata, ); return { mergedMetadata, listData: Object.values(mergedMetadata) }; - }, [chainMetadata]); + }, [chainMetadata, overrideChainMetadata]); const { ListComponent, searchFn, sortOptions, defaultSortState } = useCustomizedListItems(customListItemField, defaultSortField); @@ -297,7 +297,7 @@ function useCustomizedListItems( ({ data }: { data: ChainMetadata<{ disabled?: boolean }> }) => ( ), - [ChainListItem, customListItemField], + [customListItemField], ); // Bind the custom field to the search function diff --git a/typescript/widgets/src/components/Button.tsx b/typescript/widgets/src/components/Button.tsx index 3177af05b74..e983bd56458 100644 --- a/typescript/widgets/src/components/Button.tsx +++ b/typescript/widgets/src/components/Button.tsx @@ -1,4 +1,4 @@ -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { ButtonHTMLAttributes, PropsWithChildren } from 'react'; type Props = PropsWithChildren>; diff --git a/typescript/widgets/src/components/ErrorBoundary.tsx b/typescript/widgets/src/components/ErrorBoundary.tsx index a21611ec086..79a7ce15e2a 100644 --- a/typescript/widgets/src/components/ErrorBoundary.tsx +++ b/typescript/widgets/src/components/ErrorBoundary.tsx @@ -3,6 +3,7 @@ import React, { Component, PropsWithChildren, ReactNode } from 'react'; import { errorToString } from '@hyperlane-xyz/utils'; import { ErrorIcon } from '../icons/Error.js'; +import { widgetLogger } from '../logger.js'; type Props = PropsWithChildren<{ supportLink?: ReactNode; @@ -24,7 +25,7 @@ export class ErrorBoundary extends Component { error, errorInfo, }); - console.error('Error caught by error boundary', error, errorInfo); + widgetLogger.error('Error caught by error boundary', error, errorInfo); } render() { diff --git a/typescript/widgets/src/components/IconButton.tsx b/typescript/widgets/src/components/IconButton.tsx index 314ba40fa42..eeeddc57d79 100644 --- a/typescript/widgets/src/components/IconButton.tsx +++ b/typescript/widgets/src/components/IconButton.tsx @@ -1,4 +1,4 @@ -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { ButtonHTMLAttributes, PropsWithChildren } from 'react'; type Props = PropsWithChildren> & { diff --git a/typescript/widgets/src/components/LinkButton.tsx b/typescript/widgets/src/components/LinkButton.tsx index 026bf6ba110..5fb6c53c9ca 100644 --- a/typescript/widgets/src/components/LinkButton.tsx +++ b/typescript/widgets/src/components/LinkButton.tsx @@ -1,4 +1,4 @@ -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { ButtonHTMLAttributes, PropsWithChildren } from 'react'; type Props = PropsWithChildren>; diff --git a/typescript/widgets/src/components/SearchMenu.tsx b/typescript/widgets/src/components/SearchMenu.tsx index 831b46e7484..750d2bfb3d4 100644 --- a/typescript/widgets/src/components/SearchMenu.tsx +++ b/typescript/widgets/src/components/SearchMenu.tsx @@ -1,6 +1,7 @@ -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { ComponentType, + forwardRef, useCallback, useEffect, useMemo, @@ -102,12 +103,13 @@ export function SearchMenu< const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); + e.stopPropagation(); if (results.length === 1) { const item = results[0]; isEditMode ? onClickEditItem(item) : onClickItem(item); } }, - [results, isEditMode], + [results, isEditMode, onClickEditItem, onClickItem], ); useEffect(() => { @@ -183,7 +185,7 @@ export function SearchMenu< ); } -const SearchBar = React.forwardRef(function SearchBar( +const SearchBar = forwardRef(function SearchBar( { onChange, value, ...props }: InputProps, ref: React.Ref, ) { @@ -254,6 +256,7 @@ function SortDropdown({ buttonClassname="htw-flex htw-items-stretch hover:htw-bg-gray-100 active:htw-scale-95" menuClassname="htw-py-1.5 htw-px-2 htw-flex htw-flex-col htw-gap-2 htw-text-sm htw-border htw-border-gray-100" menuItems={options.map((o) => ( + // eslint-disable-next-line react/jsx-key
onSetSortBy(o)} @@ -298,7 +301,7 @@ function FilterDropdown({ (k) => !deepEquals(value[k], defaultValue[k]), ); return modifiedKeys.map((k) => value[k]); - }, [value]); + }, [value, defaultValue]); const hasFilters = filterValues.length > 0; const onClear = () => { diff --git a/typescript/widgets/src/components/TextInput.tsx b/typescript/widgets/src/components/TextInput.tsx index e2dfa8e8e72..42f74f199d2 100644 --- a/typescript/widgets/src/components/TextInput.tsx +++ b/typescript/widgets/src/components/TextInput.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, InputHTMLAttributes } from 'react'; +import React, { ChangeEvent, InputHTMLAttributes, forwardRef } from 'react'; export type InputProps = Omit< InputHTMLAttributes, @@ -28,4 +28,4 @@ export function _TextInput( ); } -export const TextInput = React.forwardRef(_TextInput); +export const TextInput = forwardRef(_TextInput); diff --git a/typescript/widgets/src/icons/Web.tsx b/typescript/widgets/src/icons/Web.tsx index 8915ed026dd..228087f5cd7 100644 --- a/typescript/widgets/src/icons/Web.tsx +++ b/typescript/widgets/src/icons/Web.tsx @@ -57,9 +57,9 @@ function _Web({ color = ColorPalette.Black, ...rest }: DefaultIconProps) { /> ); diff --git a/typescript/widgets/src/index.ts b/typescript/widgets/src/index.ts index fcc90c30060..655e3c17313 100644 --- a/typescript/widgets/src/index.ts +++ b/typescript/widgets/src/index.ts @@ -56,12 +56,17 @@ export { WalletIcon } from './icons/Wallet.js'; export { WarningIcon } from './icons/Warning.js'; export { WebIcon } from './icons/Web.js'; export { WideChevronIcon } from './icons/WideChevron.js'; -export { XCircleIcon } from './icons/XCircle.js'; export { XIcon } from './icons/X.js'; +export { XCircleIcon } from './icons/XCircle.js'; export { DropdownMenu, type DropdownMenuProps } from './layout/DropdownMenu.js'; export { Modal, useModal, type ModalProps } from './layout/Modal.js'; export { Popover, type PopoverProps } from './layout/Popover.js'; +export { CosmosLogo } from './logos/Cosmos.js'; +export { EthereumLogo } from './logos/Ethereum.js'; export { HyperlaneLogo } from './logos/Hyperlane.js'; +export { PROTOCOL_TO_LOGO } from './logos/protocols.js'; +export { SolanaLogo } from './logos/Solana.js'; +export { WalletConnectLogo } from './logos/WalletConnect.js'; export { MessageTimeline } from './messages/MessageTimeline.js'; export { MessageStage, @@ -81,3 +86,62 @@ export { useDebounce } from './utils/debounce.js'; export { useIsSsr } from './utils/ssr.js'; export { useInterval, useTimeout } from './utils/timeout.js'; export { useConnectionHealthTest } from './utils/useChainConnectionTest.js'; +export { + AccountList, + AccountSummary, +} from './walletIntegrations/AccountList.js'; +export { ConnectWalletButton } from './walletIntegrations/ConnectWalletButton.js'; +export { + getCosmosKitChainConfigs, + useCosmosAccount, + useCosmosActiveChain, + useCosmosConnectFn, + useCosmosDisconnectFn, + useCosmosTransactionFns, + useCosmosWalletDetails, +} from './walletIntegrations/cosmos.js'; +export { + getWagmiChainConfigs, + useEthereumAccount, + useEthereumActiveChain, + useEthereumConnectFn, + useEthereumDisconnectFn, + useEthereumTransactionFns, + useEthereumWalletDetails, +} from './walletIntegrations/ethereum.js'; +export { + getAccountAddressAndPubKey, + getAccountAddressForChain, + useAccountAddressForChain, + useAccountForChain, + useAccounts, + useActiveChains, + useConnectFns, + useDisconnectFns, + useTransactionFns, + useWalletDetails, +} from './walletIntegrations/multiProtocol.js'; +export { MultiProtocolWalletModal } from './walletIntegrations/MultiProtocolWalletModal.js'; +export { + useSolanaAccount, + useSolanaActiveChain, + useSolanaConnectFn, + useSolanaDisconnectFn, + useSolanaTransactionFns, + useSolanaWalletDetails, +} from './walletIntegrations/solana.js'; +export type { + AccountInfo, + ActiveChainInfo, + ChainAddress, + ChainTransactionFns, + SendTransactionFn, + SwitchNetworkFn, + WalletDetails, +} from './walletIntegrations/types.js'; +export { + ethers5TxToWagmiTx, + findChainByRpcUrl, + getChainsForProtocol, +} from './walletIntegrations/utils.js'; +export { WalletLogo } from './walletIntegrations/WalletLogo.js'; diff --git a/typescript/widgets/src/layout/DropdownMenu.tsx b/typescript/widgets/src/layout/DropdownMenu.tsx index 837a219e761..6dbca2248ae 100644 --- a/typescript/widgets/src/layout/DropdownMenu.tsx +++ b/typescript/widgets/src/layout/DropdownMenu.tsx @@ -1,5 +1,5 @@ import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'; -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { ComponentProps, ReactNode } from 'react'; export type DropdownMenuProps = { diff --git a/typescript/widgets/src/layout/Modal.tsx b/typescript/widgets/src/layout/Modal.tsx index bc0b872f01e..12a774bbe7a 100644 --- a/typescript/widgets/src/layout/Modal.tsx +++ b/typescript/widgets/src/layout/Modal.tsx @@ -4,7 +4,7 @@ import { DialogPanel, DialogTitle, } from '@headlessui/react'; -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { ComponentProps, PropsWithChildren, useState } from 'react'; import { IconButton } from '../components/IconButton.js'; diff --git a/typescript/widgets/src/layout/Popover.tsx b/typescript/widgets/src/layout/Popover.tsx index 34e7793fb24..f0f1b822923 100644 --- a/typescript/widgets/src/layout/Popover.tsx +++ b/typescript/widgets/src/layout/Popover.tsx @@ -3,7 +3,7 @@ import { PopoverPanel, Popover as _Popover, } from '@headlessui/react'; -import clsx from 'clsx'; +import { clsx } from 'clsx'; import React, { ComponentProps, ReactNode } from 'react'; export type PopoverProps = { diff --git a/typescript/widgets/src/logger.ts b/typescript/widgets/src/logger.ts new file mode 100644 index 00000000000..b459551117b --- /dev/null +++ b/typescript/widgets/src/logger.ts @@ -0,0 +1,3 @@ +import { rootLogger } from '@hyperlane-xyz/utils'; + +export const widgetLogger = rootLogger.child({ module: 'widgets' }); diff --git a/typescript/widgets/src/logos/Cosmos.tsx b/typescript/widgets/src/logos/Cosmos.tsx new file mode 100644 index 00000000000..96b2ee81418 --- /dev/null +++ b/typescript/widgets/src/logos/Cosmos.tsx @@ -0,0 +1,40 @@ +import React, { SVGProps, memo } from 'react'; + +function _CosmosLogo(props: SVGProps) { + return ( + + + + + + + ); +} + +export const CosmosLogo = memo(_CosmosLogo); diff --git a/typescript/widgets/src/logos/Ethereum.tsx b/typescript/widgets/src/logos/Ethereum.tsx new file mode 100644 index 00000000000..6a298601c2c --- /dev/null +++ b/typescript/widgets/src/logos/Ethereum.tsx @@ -0,0 +1,27 @@ +import React, { SVGProps, memo } from 'react'; + +function _EthereumLogo(props: SVGProps) { + return ( + + + + + + + + + + + ); +} + +export const EthereumLogo = memo(_EthereumLogo); diff --git a/typescript/widgets/src/logos/Solana.tsx b/typescript/widgets/src/logos/Solana.tsx new file mode 100644 index 00000000000..07b3affa3f7 --- /dev/null +++ b/typescript/widgets/src/logos/Solana.tsx @@ -0,0 +1,63 @@ +import React, { SVGProps, memo } from 'react'; + +function _SolanaLogo(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + + + ); +} + +export const SolanaLogo = memo(_SolanaLogo); diff --git a/typescript/widgets/src/logos/WalletConnect.tsx b/typescript/widgets/src/logos/WalletConnect.tsx new file mode 100644 index 00000000000..89b474cbe2a --- /dev/null +++ b/typescript/widgets/src/logos/WalletConnect.tsx @@ -0,0 +1,33 @@ +import React, { SVGProps, memo } from 'react'; + +function _WalletConnectLogo(props: SVGProps) { + return ( + + + + + + + + + + ); +} + +export const WalletConnectLogo = memo(_WalletConnectLogo); diff --git a/typescript/widgets/src/logos/protocols.ts b/typescript/widgets/src/logos/protocols.ts new file mode 100644 index 00000000000..414e4e45070 --- /dev/null +++ b/typescript/widgets/src/logos/protocols.ts @@ -0,0 +1,16 @@ +import { FC, SVGProps } from 'react'; + +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { CosmosLogo } from './Cosmos.js'; +import { EthereumLogo } from './Ethereum.js'; +import { SolanaLogo } from './Solana.js'; + +export const PROTOCOL_TO_LOGO: Record< + ProtocolType, + FC, 'ref'>> +> = { + [ProtocolType.Ethereum]: EthereumLogo, + [ProtocolType.Sealevel]: SolanaLogo, + [ProtocolType.Cosmos]: CosmosLogo, +}; diff --git a/typescript/widgets/src/messages/useMessage.ts b/typescript/widgets/src/messages/useMessage.ts index 38471facf20..3434f621c1c 100644 --- a/typescript/widgets/src/messages/useMessage.ts +++ b/typescript/widgets/src/messages/useMessage.ts @@ -1,11 +1,14 @@ import { useCallback, useState } from 'react'; import { HYPERLANE_EXPLORER_API_URL } from '../consts.js'; +import { widgetLogger } from '../logger.js'; import { executeExplorerQuery } from '../utils/explorers.js'; import { useInterval } from '../utils/timeout.js'; import { ApiMessage, MessageStatus } from './types.js'; +const logger = widgetLogger.child({ module: 'useMessage' }); + interface Params { messageId?: string; originTxHash?: string; @@ -40,7 +43,7 @@ export function useMessage({ }) .catch((e) => setError(e.toString())) .finally(() => setIsLoading(false)); - }, [messageId, originTxHash, data]); + }, [explorerApiUrl, messageId, originTxHash, data]); useInterval(fetcher, retryInterval); @@ -66,13 +69,13 @@ async function fetchMessage( const result = await executeExplorerQuery(url, 5000); if (result.length > 1) { - console.warn('More than one message received, should not occur'); + logger.warn('More than one message received, should not occur'); return result[0]; } else if (result.length === 1) { - console.debug('Message data found, id:', result[0].id); + logger.debug('Message data found, id:', result[0].id); return result[0]; } else { - console.debug('Message data not found'); + logger.debug('Message data not found'); return null; } } diff --git a/typescript/widgets/src/messages/useMessageStage.ts b/typescript/widgets/src/messages/useMessageStage.ts index 9351e31d444..26dd1678b4e 100644 --- a/typescript/widgets/src/messages/useMessageStage.ts +++ b/typescript/widgets/src/messages/useMessageStage.ts @@ -4,6 +4,7 @@ import type { MultiProvider } from '@hyperlane-xyz/sdk'; import { fetchWithTimeout } from '@hyperlane-xyz/utils'; import { HYPERLANE_EXPLORER_API_URL } from '../consts.js'; +import { widgetLogger } from '../logger.js'; import { queryExplorerForBlock } from '../utils/explorers.js'; import { useInterval } from '../utils/timeout.js'; @@ -14,6 +15,8 @@ import { StageTimings, } from './types.js'; +const logger = widgetLogger.child({ module: 'useMessageStage' }); + const VALIDATION_TIME_EST = 5; const DEFAULT_BLOCK_TIME_EST = 3; const DEFAULT_FINALITY_BLOCKS = 3; @@ -67,7 +70,7 @@ export function useMessageStage({ }) .catch((e) => setError(e.toString())) .finally(() => setIsLoading(false)); - }, [message, data]); + }, [explorerApiUrl, multiProvider, message, data]); useInterval(fetcher, retryInterval); @@ -192,7 +195,7 @@ async function tryFetchChainLatestBlock( ) { const metadata = multiProvider.tryGetChainMetadata(domainId); if (!metadata) return null; - console.debug(`Attempting to fetch latest block for:`, metadata.name); + logger.debug(`Attempting to fetch latest block for:`, metadata.name); try { const block = await queryExplorerForBlock( metadata.name, @@ -201,7 +204,7 @@ async function tryFetchChainLatestBlock( ); return block; } catch (error) { - console.error('Error fetching latest block', error); + logger.error('Error fetching latest block', error); return null; } } @@ -213,7 +216,7 @@ async function tryFetchLatestNonce( ) { const metadata = multiProvider.tryGetChainMetadata(domainId); if (!metadata) return null; - console.debug(`Attempting to fetch nonce for:`, metadata.name); + logger.debug(`Attempting to fetch nonce for:`, metadata.name); try { const response = await fetchWithTimeout( `${explorerApiUrl}/latest-nonce`, @@ -227,10 +230,10 @@ async function tryFetchLatestNonce( 3000, ); const result = await response.json(); - console.debug(`Found nonce:`, result.nonce); + logger.debug(`Found nonce:`, result.nonce); return result.nonce; } catch (error) { - console.error('Error fetching nonce', error); + logger.error('Error fetching nonce', error); return null; } } diff --git a/typescript/widgets/src/stories/MultiProtocolWalletModal.stories.tsx b/typescript/widgets/src/stories/MultiProtocolWalletModal.stories.tsx new file mode 100644 index 00000000000..dd2aadff71f --- /dev/null +++ b/typescript/widgets/src/stories/MultiProtocolWalletModal.stories.tsx @@ -0,0 +1,124 @@ +import { ChakraProvider } from '@chakra-ui/react'; +import { ChainProvider } from '@cosmos-kit/react'; +import '@interchain-ui/react/styles'; +import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; +import '@rainbow-me/rainbowkit/styles.css'; +import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; +import { + ConnectionProvider, + WalletProvider, +} from '@solana/wallet-adapter-react'; +import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; +import '@solana/wallet-adapter-react-ui/styles.css'; +import { clusterApiUrl } from '@solana/web3.js'; +import { Meta, StoryObj } from '@storybook/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React, { PropsWithChildren, useState } from 'react'; +import { WagmiProvider, createConfig, http } from 'wagmi'; + +import { cosmoshub, ethereum, solanamainnet } from '@hyperlane-xyz/registry'; +import { MultiProtocolProvider } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { AccountList } from '../walletIntegrations/AccountList.js'; +import { ConnectWalletButton } from '../walletIntegrations/ConnectWalletButton.js'; +import { MultiProtocolWalletModal } from '../walletIntegrations/MultiProtocolWalletModal.js'; +import { getCosmosKitChainConfigs } from '../walletIntegrations/cosmos.js'; +import { getWagmiChainConfigs } from '../walletIntegrations/ethereum.js'; + +const multiProvider = new MultiProtocolProvider({ + ethereum, + cosmoshub, + solanamainnet, +}); + +function MinimalDapp({ protocols }: { protocols?: ProtocolType[] }) { + const [isOpen, setIsOpen] = useState(false); + const open = () => setIsOpen(true); + const close = () => setIsOpen(false); + + return ( + + + +
+

CONNECT BUTTON

+ +

ACCOUNT SUMMARY

+ +
+ +
+
+
+ ); +} + +const wagmiConfig = createConfig({ + chains: [getWagmiChainConfigs(multiProvider)[0]], + transports: { [ethereum.chainId]: http() }, +}); + +function EthereumWalletProvider({ children }: PropsWithChildren) { + const queryClient = new QueryClient(); + + return ( + + + {children} + + + ); +} + +const cosmosKitConfig = getCosmosKitChainConfigs(multiProvider); + +function CosmosWalletProvider({ children }: PropsWithChildren) { + return ( + + + {children} + + + ); +} + +function SolanaWalletProvider({ children }: PropsWithChildren) { + return ( + + + {children} + + + ); +} + +const meta = { + title: 'MultiProtocolWalletModal', + component: MinimalDapp, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const DefaultPicker = { + args: {}, +} satisfies Story; + +export const EvmOnlyPicker = { + args: { protocols: [ProtocolType.Ethereum] }, +} satisfies Story; diff --git a/typescript/widgets/src/utils/clipboard.ts b/typescript/widgets/src/utils/clipboard.ts index eb331af45da..454b73a766c 100644 --- a/typescript/widgets/src/utils/clipboard.ts +++ b/typescript/widgets/src/utils/clipboard.ts @@ -1,3 +1,5 @@ +import { widgetLogger } from '../logger.js'; + export function isClipboardReadSupported() { return !!navigator?.clipboard?.readText; } @@ -7,7 +9,7 @@ export async function tryClipboardSet(value: string) { await navigator.clipboard.writeText(value); return true; } catch (error) { - console.error('Failed to set clipboard', error); + widgetLogger.error('Failed to set clipboard', error); return false; } } @@ -18,7 +20,7 @@ export async function tryClipboardGet() { const value = await navigator.clipboard.readText(); return value; } catch (error) { - console.error('Failed to read from clipboard', error); + widgetLogger.error('Failed to read from clipboard', error); return null; } } diff --git a/typescript/widgets/src/utils/explorers.ts b/typescript/widgets/src/utils/explorers.ts index 38c470d31c9..b546f425dc4 100644 --- a/typescript/widgets/src/utils/explorers.ts +++ b/typescript/widgets/src/utils/explorers.ts @@ -1,6 +1,8 @@ import type { MultiProvider } from '@hyperlane-xyz/sdk'; import { fetchWithTimeout } from '@hyperlane-xyz/utils'; +import { widgetLogger } from '../logger.js'; + export interface ExplorerQueryResponse { status: string; message: string; @@ -29,7 +31,7 @@ export async function queryExplorer

( throw new Error(`No URL found for explorer for chain ${chainName}`); let url = `${baseUrl}/${path}`; - console.debug('Querying explorer url:', url); + widgetLogger.debug('Querying explorer url:', url); if (apiKey) { url += `&apikey=${apiKey}`; @@ -76,7 +78,7 @@ export async function queryExplorerForBlock( ); if (!block?.number || parseInt(block.number.toString()) < 0) { const msg = 'Invalid block result'; - console.error(msg, JSON.stringify(block), path); + widgetLogger.error(msg, JSON.stringify(block), path); throw new Error(msg); } return block; diff --git a/typescript/widgets/src/utils/useChainConnectionTest.ts b/typescript/widgets/src/utils/useChainConnectionTest.ts index 920b2e7dd0e..6892fb83bde 100644 --- a/typescript/widgets/src/utils/useChainConnectionTest.ts +++ b/typescript/widgets/src/utils/useChainConnectionTest.ts @@ -26,7 +26,7 @@ export function useConnectionHealthTest( timeout(tester(chainMetadata, index), HEALTH_TEST_TIMEOUT) .then((result) => setIsHealthy(result)) .catch(() => setIsHealthy(false)); - }, [chainMetadata, index, tester]); + }, [chainMetadata, index, type, tester]); return isHealthy; } diff --git a/typescript/widgets/src/walletIntegrations/AccountList.tsx b/typescript/widgets/src/walletIntegrations/AccountList.tsx new file mode 100644 index 00000000000..63d08baada8 --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/AccountList.tsx @@ -0,0 +1,143 @@ +import { clsx } from 'clsx'; +import React, { ButtonHTMLAttributes } from 'react'; + +import { MultiProtocolProvider } from '@hyperlane-xyz/sdk'; +import { ProtocolType, objKeys } from '@hyperlane-xyz/utils'; + +import { Button } from '../components/Button.js'; +import { IconButton } from '../components/IconButton.js'; +import { LogoutIcon } from '../icons/Logout.js'; +import { WalletIcon } from '../icons/Wallet.js'; +import { XCircleIcon } from '../icons/XCircle.js'; +import { widgetLogger } from '../logger.js'; +import { tryClipboardSet } from '../utils/clipboard.js'; +import { WalletLogo } from '../walletIntegrations/WalletLogo.js'; +import { + useAccounts, + useDisconnectFns, + useWalletDetails, +} from '../walletIntegrations/multiProtocol.js'; + +import { AccountInfo, WalletDetails } from './types.js'; + +const logger = widgetLogger.child({ module: 'walletIntegrations/AccountList' }); + +export function AccountList({ + multiProvider, + onClickConnectWallet, + onCopySuccess, + className, +}: { + multiProvider: MultiProtocolProvider; + onClickConnectWallet: () => void; + onCopySuccess?: () => void; + className?: string; +}) { + const { readyAccounts } = useAccounts(multiProvider); + const disconnectFns = useDisconnectFns(); + const walletDetails = useWalletDetails(); + + const onClickDisconnect = async (protocol: ProtocolType) => { + try { + const disconnectFn = disconnectFns[protocol]; + if (disconnectFn) await disconnectFn(); + } catch (error) { + logger.error('Error disconnecting wallet', error); + } + }; + + const onClickDisconnectAll = async () => { + for (const protocol of objKeys(disconnectFns)) { + await onClickDisconnect(protocol); + } + }; + + return ( +

+ {readyAccounts.map((acc, i) => ( + onClickDisconnect(acc.protocol)} + /> + ))} + + +
+ ); +} + +type AccountSummaryProps = { + account: AccountInfo; + walletDetails: WalletDetails; + onCopySuccess?: () => void; + onClickDisconnect: () => Promise; +} & ButtonHTMLAttributes; + +export function AccountSummary({ + account, + onCopySuccess, + walletDetails, + onClickDisconnect, + className, + ...rest +}: AccountSummaryProps) { + const numAddresses = account?.addresses?.length || 0; + const onlyAddress = + numAddresses === 1 ? account.addresses[0].address : undefined; + + const onClickCopy = async () => { + const copyValue = account.addresses.map((a) => a.address).join(', '); + await tryClipboardSet(copyValue); + onCopySuccess?.(); + }; + + return ( +
+ +
+ + + +
+
+ ); +} + +const styles = { + btn: 'htw-flex htw-w-full htw-items-center all:htw-justify-start htw-rounded-sm htw-text-sm hover:htw-bg-gray-200 all:hover:htw-opacity-100', +}; diff --git a/typescript/widgets/src/walletIntegrations/ConnectWalletButton.tsx b/typescript/widgets/src/walletIntegrations/ConnectWalletButton.tsx new file mode 100644 index 00000000000..1d64e9cf70b --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/ConnectWalletButton.tsx @@ -0,0 +1,115 @@ +import { clsx } from 'clsx'; +import React, { ButtonHTMLAttributes } from 'react'; + +import { MultiProtocolProvider } from '@hyperlane-xyz/sdk'; +import { ProtocolType, shortenAddress } from '@hyperlane-xyz/utils'; + +import { Button } from '../components/Button.js'; +import { ChevronIcon } from '../icons/Chevron.js'; +import { WalletIcon } from '../icons/Wallet.js'; +import { useIsSsr } from '../utils/ssr.js'; + +import { WalletLogo } from './WalletLogo.js'; +import { useAccounts, useWalletDetails } from './multiProtocol.js'; + +type Props = { + multiProvider: MultiProtocolProvider; + onClickWhenConnected: () => void; + onClickWhenUnconnected: () => void; + countClassName?: string; +} & ButtonHTMLAttributes; + +export function ConnectWalletButton({ + multiProvider, + onClickWhenConnected, + onClickWhenUnconnected, + className, + countClassName, + ...rest +}: Props) { + const isSsr = useIsSsr(); + + const { readyAccounts } = useAccounts(multiProvider); + const walletDetails = useWalletDetails(); + + const numReady = readyAccounts.length; + const firstAccount = readyAccounts[0]; + const firstWallet = + walletDetails[firstAccount?.protocol || ProtocolType.Ethereum]; + + if (isSsr) { + // https://github.com/wagmi-dev/wagmi/issues/542#issuecomment-1144178142 + return null; + } + + return ( +
+
+ {numReady === 0 && ( + + )} + + {numReady === 1 && ( + + )} + + {numReady > 1 && ( + + )} +
+
+ ); +} diff --git a/typescript/widgets/src/walletIntegrations/MultiProtocolWalletModal.tsx b/typescript/widgets/src/walletIntegrations/MultiProtocolWalletModal.tsx new file mode 100644 index 00000000000..2c200f1cefe --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/MultiProtocolWalletModal.tsx @@ -0,0 +1,86 @@ +import React, { PropsWithChildren } from 'react'; + +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { Modal } from '../layout/Modal.js'; +import { PROTOCOL_TO_LOGO } from '../logos/protocols.js'; + +import { useConnectFns } from './multiProtocol.js'; + +export function MultiProtocolWalletModal({ + isOpen, + close, + protocols, +}: { + isOpen: boolean; + close: () => void; + protocols?: ProtocolType[]; // defaults to all protocols if not provided +}) { + const connectFns = useConnectFns(); + + const onClickProtocol = (protocol: ProtocolType) => { + close(); + const connectFn = connectFns[protocol]; + if (connectFn) connectFn(); + }; + + const includesProtocol = (protocol: ProtocolType) => + !protocols || protocols.includes(protocol); + + return ( + +
+ {includesProtocol(ProtocolType.Ethereum) && ( + + Ethereum + + )} + {includesProtocol(ProtocolType.Sealevel) && ( + + Solana + + )} + {includesProtocol(ProtocolType.Cosmos) && ( + + Cosmos + + )} +
+
+ ); +} + +function ProtocolButton({ + onClick, + subTitle, + protocol, + children, +}: PropsWithChildren<{ + subTitle: string; + protocol: ProtocolType; + onClick: (protocol: ProtocolType) => void; +}>) { + const Logo = PROTOCOL_TO_LOGO[protocol]; + return ( + + ); +} diff --git a/typescript/widgets/src/walletIntegrations/WalletLogo.tsx b/typescript/widgets/src/walletIntegrations/WalletLogo.tsx new file mode 100644 index 00000000000..a41658295c4 --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/WalletLogo.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { WalletIcon } from '../icons/Wallet.js'; +import { WalletConnectLogo } from '../logos/WalletConnect.js'; + +import { WalletDetails } from './types.js'; + +export function WalletLogo({ + walletDetails, + size, +}: { + walletDetails: WalletDetails; + size?: number; +}) { + const src = walletDetails.logoUrl?.trim(); + + if (src) { + return ; + } else if (walletDetails.name?.toLowerCase() === 'walletconnect') { + return ; + } else { + return ; + } +} diff --git a/typescript/widgets/src/walletIntegrations/cosmos.ts b/typescript/widgets/src/walletIntegrations/cosmos.ts new file mode 100644 index 00000000000..7b54c61cf1d --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/cosmos.ts @@ -0,0 +1,208 @@ +import type { AssetList, Chain as CosmosChain } from '@chain-registry/types'; +import type { + DeliverTxResponse, + ExecuteResult, + IndexedTx, +} from '@cosmjs/cosmwasm-stargate'; +import { useChain, useChains } from '@cosmos-kit/react'; +import { useCallback, useMemo } from 'react'; + +import { cosmoshub } from '@hyperlane-xyz/registry'; +import { + ChainMetadata, + ChainName, + MultiProtocolProvider, + ProviderType, + TypedTransactionReceipt, + WarpTypedTransaction, + chainMetadataToCosmosChain, +} from '@hyperlane-xyz/sdk'; +import { HexString, ProtocolType, assert } from '@hyperlane-xyz/utils'; + +import { widgetLogger } from '../logger.js'; + +import { + AccountInfo, + ActiveChainInfo, + ChainAddress, + ChainTransactionFns, + WalletDetails, +} from './types.js'; +import { getChainsForProtocol } from './utils.js'; + +// Used because the CosmosKit hooks always require a chain name +const PLACEHOLDER_COSMOS_CHAIN = cosmoshub.name; + +const logger = widgetLogger.child({ + module: 'widgets/walletIntegrations/cosmos', +}); + +export function useCosmosAccount( + multiProvider: MultiProtocolProvider, +): AccountInfo { + const cosmosChains = getCosmosChainNames(multiProvider); + const chainToContext = useChains(cosmosChains); + return useMemo(() => { + const addresses: Array = []; + let publicKey: Promise | undefined = undefined; + let connectorName: string | undefined = undefined; + let isReady = false; + for (const [chainName, context] of Object.entries(chainToContext)) { + if (!context.address) continue; + addresses.push({ address: context.address, chainName }); + publicKey = context + .getAccount() + .then((acc) => Buffer.from(acc.pubkey).toString('hex')); + isReady = true; + connectorName ||= context.wallet?.prettyName; + } + return { + protocol: ProtocolType.Cosmos, + addresses, + publicKey, + isReady, + }; + }, [chainToContext]); +} + +export function useCosmosWalletDetails() { + const { wallet } = useChain(PLACEHOLDER_COSMOS_CHAIN); + const { logo, prettyName } = wallet || {}; + + return useMemo( + () => ({ + name: prettyName, + logoUrl: typeof logo === 'string' ? logo : undefined, + }), + [prettyName, logo], + ); +} + +export function useCosmosConnectFn(): () => void { + const { openView } = useChain(PLACEHOLDER_COSMOS_CHAIN); + return openView; +} + +export function useCosmosDisconnectFn(): () => Promise { + const { disconnect, address } = useChain(PLACEHOLDER_COSMOS_CHAIN); + const safeDisconnect = async () => { + if (address) await disconnect(); + }; + return safeDisconnect; +} + +export function useCosmosActiveChain( + _multiProvider: MultiProtocolProvider, +): ActiveChainInfo { + // Cosmoskit doesn't have the concept of an active chain + return useMemo(() => ({} as ActiveChainInfo), []); +} + +export function useCosmosTransactionFns( + multiProvider: MultiProtocolProvider, +): ChainTransactionFns { + const cosmosChains = getCosmosChainNames(multiProvider); + const chainToContext = useChains(cosmosChains); + + const onSwitchNetwork = useCallback( + async (chainName: ChainName) => { + const displayName = + multiProvider.getChainMetadata(chainName).displayName || chainName; + // CosmosKit does not have switch capability + throw new Error( + `Cosmos wallet must be connected to origin chain ${displayName}}`, + ); + }, + [multiProvider], + ); + + const onSendTx = useCallback( + async ({ + tx, + chainName, + activeChainName, + }: { + tx: WarpTypedTransaction; + chainName: ChainName; + activeChainName?: ChainName; + }) => { + const chainContext = chainToContext[chainName]; + if (!chainContext?.address) + throw new Error(`Cosmos wallet not connected for ${chainName}`); + + if (activeChainName && activeChainName !== chainName) + await onSwitchNetwork(chainName); + + logger.debug(`Sending tx on chain ${chainName}`); + const { getSigningCosmWasmClient, getSigningStargateClient } = + chainContext; + let result: ExecuteResult | DeliverTxResponse; + let txDetails: IndexedTx | null; + if (tx.type === ProviderType.CosmJsWasm) { + const client = await getSigningCosmWasmClient(); + result = await client.executeMultiple( + chainContext.address, + [tx.transaction], + 'auto', + ); + txDetails = await client.getTx(result.transactionHash); + } else if (tx.type === ProviderType.CosmJs) { + const client = await getSigningStargateClient(); + // The fee param of 'auto' here stopped working for Neutron-based IBC transfers + // It seems the signAndBroadcast method uses a default fee multiplier of 1.4 + // https://github.com/cosmos/cosmjs/blob/e819a1fc0e99a3e5320d8d6667a08d3b92e5e836/packages/stargate/src/signingstargateclient.ts#L115 + // A multiplier of 1.6 was insufficient for Celestia -> Neutron|Cosmos -> XXX transfers, but 2 worked. + result = await client.signAndBroadcast( + chainContext.address, + [tx.transaction], + 2, + ); + txDetails = await client.getTx(result.transactionHash); + } else { + throw new Error(`Invalid cosmos provider type ${tx.type}`); + } + + const confirm = async (): Promise => { + assert(txDetails, `Cosmos tx failed: ${JSON.stringify(result)}`); + return { + type: tx.type, + receipt: { ...txDetails, transactionHash: result.transactionHash }, + }; + }; + return { hash: result.transactionHash, confirm }; + }, + [onSwitchNetwork, chainToContext], + ); + + return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork }; +} + +function getCosmosChains( + multiProvider: MultiProtocolProvider, +): ChainMetadata[] { + return [ + ...getChainsForProtocol(multiProvider, ProtocolType.Cosmos), + cosmoshub, + ]; +} + +function getCosmosChainNames( + multiProvider: MultiProtocolProvider, +): ChainName[] { + return getCosmosChains(multiProvider).map((c) => c.name); +} + +// Metadata formatted for use in Wagmi config +export function getCosmosKitChainConfigs( + multiProvider: MultiProtocolProvider, +): { + chains: CosmosChain[]; + assets: AssetList[]; +} { + const chains = getCosmosChains(multiProvider); + const configList = chains.map(chainMetadataToCosmosChain); + return { + chains: configList.map((c) => c.chain), + assets: configList.map((c) => c.assets), + }; +} diff --git a/typescript/widgets/src/walletIntegrations/ethereum.ts b/typescript/widgets/src/walletIntegrations/ethereum.ts new file mode 100644 index 00000000000..0a9e8a4a132 --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/ethereum.ts @@ -0,0 +1,169 @@ +import { useConnectModal } from '@rainbow-me/rainbowkit'; +import { + getAccount, + sendTransaction, + switchChain, + waitForTransactionReceipt, +} from '@wagmi/core'; +import { useCallback, useMemo } from 'react'; +import { Chain as ViemChain } from 'viem'; +import { useAccount, useConfig, useDisconnect } from 'wagmi'; + +import { + ChainName, + MultiProtocolProvider, + ProviderType, + TypedTransactionReceipt, + WarpTypedTransaction, + chainMetadataToViemChain, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType, assert, sleep } from '@hyperlane-xyz/utils'; + +import { widgetLogger } from '../logger.js'; + +import { + AccountInfo, + ActiveChainInfo, + ChainTransactionFns, + WalletDetails, +} from './types.js'; +import { ethers5TxToWagmiTx, getChainsForProtocol } from './utils.js'; + +const logger = widgetLogger.child({ module: 'walletIntegrations/ethereum' }); + +export function useEthereumAccount( + _multiProvider: MultiProtocolProvider, +): AccountInfo { + const { address, isConnected, connector } = useAccount(); + const isReady = !!(address && isConnected && connector); + + return useMemo( + () => ({ + protocol: ProtocolType.Ethereum, + addresses: address ? [{ address: `${address}` }] : [], + isReady: isReady, + }), + [address, isReady], + ); +} + +export function useEthereumWalletDetails() { + const { connector } = useAccount(); + const name = connector?.name; + const logoUrl = connector?.icon; + + return useMemo( + () => ({ + name, + logoUrl, + }), + [name, logoUrl], + ); +} + +export function useEthereumConnectFn(): () => void { + const { openConnectModal } = useConnectModal(); + return useCallback(() => openConnectModal?.(), [openConnectModal]); +} + +export function useEthereumDisconnectFn(): () => Promise { + const { disconnectAsync } = useDisconnect(); + return disconnectAsync; +} + +export function useEthereumActiveChain( + multiProvider: MultiProtocolProvider, +): ActiveChainInfo { + const { chain } = useAccount(); + return useMemo( + () => ({ + chainDisplayName: chain?.name, + chainName: chain + ? multiProvider.tryGetChainMetadata(chain.id)?.name + : undefined, + }), + [chain, multiProvider], + ); +} + +export function useEthereumTransactionFns( + multiProvider: MultiProtocolProvider, +): ChainTransactionFns { + const config = useConfig(); + + const onSwitchNetwork = useCallback( + async (chainName: ChainName) => { + const chainId = multiProvider.getChainMetadata(chainName) + .chainId as number; + await switchChain(config, { chainId }); + // Some wallets seem to require a brief pause after switch + await sleep(2000); + }, + [config, multiProvider], + ); + // Note, this doesn't use wagmi's prepare + send pattern because we're potentially sending two transactions + // The prepare hooks are recommended to use pre-click downtime to run async calls, but since the flow + // may require two serial txs, the prepare hooks aren't useful and complicate hook architecture considerably. + // See https://github.com/hyperlane-xyz/hyperlane-warp-ui-template/issues/19 + // See https://github.com/wagmi-dev/wagmi/discussions/1564 + const onSendTx = useCallback( + async ({ + tx, + chainName, + activeChainName, + }: { + tx: WarpTypedTransaction; + chainName: ChainName; + activeChainName?: ChainName; + }) => { + if (tx.type !== ProviderType.EthersV5) + throw new Error(`Unsupported tx type: ${tx.type}`); + + // If the active chain is different from tx origin chain, try to switch network first + if (activeChainName && activeChainName !== chainName) + await onSwitchNetwork(chainName); + + // Since the network switching is not foolproof, we also force a network check here + const chainId = multiProvider.getChainMetadata(chainName) + .chainId as number; + logger.debug('Checking wallet current chain'); + const latestNetwork = await getAccount(config); + assert( + latestNetwork?.chain?.id === chainId, + `Wallet not on chain ${chainName} (ChainMismatchError)`, + ); + + logger.debug(`Sending tx on chain ${chainName}`); + const wagmiTx = ethers5TxToWagmiTx(tx.transaction); + const hash = await sendTransaction(config, { + chainId, + ...wagmiTx, + }); + const confirm = (): Promise => { + const foo = waitForTransactionReceipt(config, { + chainId, + hash, + confirmations: 1, + }); + return foo.then((r) => ({ + type: ProviderType.Viem, + receipt: { ...r, contractAddress: r.contractAddress || null }, + })); + }; + + return { hash, confirm }; + }, + [config, onSwitchNetwork, multiProvider], + ); + + return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork }; +} + +// Metadata formatted for use in Wagmi config +export function getWagmiChainConfigs( + multiProvider: MultiProtocolProvider, +): ViemChain[] { + return getChainsForProtocol(multiProvider, ProtocolType.Ethereum).map( + chainMetadataToViemChain, + ); +} diff --git a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx new file mode 100644 index 00000000000..9fb58abb173 --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx @@ -0,0 +1,256 @@ +import { useMemo } from 'react'; + +import { ChainName, MultiProtocolProvider } from '@hyperlane-xyz/sdk'; +import { Address, HexString, ProtocolType } from '@hyperlane-xyz/utils'; + +import { widgetLogger } from '../logger.js'; + +import { + useCosmosAccount, + useCosmosActiveChain, + useCosmosConnectFn, + useCosmosDisconnectFn, + useCosmosTransactionFns, + useCosmosWalletDetails, +} from './cosmos.js'; +import { + useEthereumAccount, + useEthereumActiveChain, + useEthereumConnectFn, + useEthereumDisconnectFn, + useEthereumTransactionFns, + useEthereumWalletDetails, +} from './ethereum.js'; +import { + useSolanaAccount, + useSolanaActiveChain, + useSolanaConnectFn, + useSolanaDisconnectFn, + useSolanaTransactionFns, + useSolanaWalletDetails, +} from './solana.js'; +import { + AccountInfo, + ActiveChainInfo, + ChainTransactionFns, + WalletDetails, +} from './types.js'; + +const logger = widgetLogger.child({ + module: 'walletIntegrations/multiProtocol', +}); + +export function useAccounts( + multiProvider: MultiProtocolProvider, + blacklistedAddresses: Address[] = [], +): { + accounts: Record; + readyAccounts: Array; +} { + const evmAccountInfo = useEthereumAccount(multiProvider); + const solAccountInfo = useSolanaAccount(multiProvider); + const cosmAccountInfo = useCosmosAccount(multiProvider); + + // Filtered ready accounts + const readyAccounts = useMemo( + () => + [evmAccountInfo, solAccountInfo, cosmAccountInfo].filter( + (a) => a.isReady, + ), + [evmAccountInfo, solAccountInfo, cosmAccountInfo], + ); + + // Check if any of the ready accounts are blacklisted + const readyAddresses = readyAccounts + .map((a) => a.addresses) + .flat() + .map((a) => a.address.toLowerCase()); + if (readyAddresses.some((a) => blacklistedAddresses.includes(a))) { + throw new Error('Wallet address is blacklisted'); + } + + return useMemo( + () => ({ + accounts: { + [ProtocolType.Ethereum]: evmAccountInfo, + [ProtocolType.Sealevel]: solAccountInfo, + [ProtocolType.Cosmos]: cosmAccountInfo, + }, + readyAccounts, + }), + [evmAccountInfo, solAccountInfo, cosmAccountInfo, readyAccounts], + ); +} + +export function useAccountForChain( + multiProvider: MultiProtocolProvider, + chainName?: ChainName, +): AccountInfo | undefined { + const { accounts } = useAccounts(multiProvider); + const protocol = chainName ? multiProvider.getProtocol(chainName) : undefined; + if (!chainName || !protocol) return undefined; + return accounts?.[protocol]; +} + +export function useAccountAddressForChain( + multiProvider: MultiProtocolProvider, + chainName?: ChainName, +): Address | undefined { + const { accounts } = useAccounts(multiProvider); + return getAccountAddressForChain(multiProvider, chainName, accounts); +} + +export function getAccountAddressForChain( + multiProvider: MultiProtocolProvider, + chainName?: ChainName, + accounts?: Record, +): Address | undefined { + if (!chainName || !accounts) return undefined; + const protocol = multiProvider.getProtocol(chainName); + const account = accounts[protocol]; + if (protocol === ProtocolType.Cosmos) { + return account?.addresses.find((a) => a.chainName === chainName)?.address; + } else { + // Use first because only cosmos has the notion of per-chain addresses + return account?.addresses[0]?.address; + } +} + +export function getAccountAddressAndPubKey( + multiProvider: MultiProtocolProvider, + chainName?: ChainName, + accounts?: Record, +): { address?: Address; publicKey?: Promise } { + const address = getAccountAddressForChain(multiProvider, chainName, accounts); + if (!accounts || !chainName || !address) return {}; + const protocol = multiProvider.getProtocol(chainName); + const publicKey = accounts[protocol]?.publicKey; + return { address, publicKey }; +} + +export function useWalletDetails(): Record { + const evmWallet = useEthereumWalletDetails(); + const solWallet = useSolanaWalletDetails(); + const cosmosWallet = useCosmosWalletDetails(); + + return useMemo( + () => ({ + [ProtocolType.Ethereum]: evmWallet, + [ProtocolType.Sealevel]: solWallet, + [ProtocolType.Cosmos]: cosmosWallet, + }), + [evmWallet, solWallet, cosmosWallet], + ); +} + +export function useConnectFns(): Record void> { + const onConnectEthereum = useEthereumConnectFn(); + const onConnectSolana = useSolanaConnectFn(); + const onConnectCosmos = useCosmosConnectFn(); + + return useMemo( + () => ({ + [ProtocolType.Ethereum]: onConnectEthereum, + [ProtocolType.Sealevel]: onConnectSolana, + [ProtocolType.Cosmos]: onConnectCosmos, + }), + [onConnectEthereum, onConnectSolana, onConnectCosmos], + ); +} + +export function useDisconnectFns(): Record Promise> { + const disconnectEvm = useEthereumDisconnectFn(); + const disconnectSol = useSolanaDisconnectFn(); + const disconnectCosmos = useCosmosDisconnectFn(); + + const onClickDisconnect = + (env: ProtocolType, disconnectFn?: () => Promise | void) => + async () => { + try { + if (!disconnectFn) throw new Error('Disconnect function is null'); + await disconnectFn(); + } catch (error) { + logger.error(`Error disconnecting from ${env} wallet`, error); + } + }; + + return useMemo( + () => ({ + [ProtocolType.Ethereum]: onClickDisconnect( + ProtocolType.Ethereum, + disconnectEvm, + ), + [ProtocolType.Sealevel]: onClickDisconnect( + ProtocolType.Sealevel, + disconnectSol, + ), + [ProtocolType.Cosmos]: onClickDisconnect( + ProtocolType.Cosmos, + disconnectCosmos, + ), + }), + [disconnectEvm, disconnectSol, disconnectCosmos], + ); +} + +export function useActiveChains(multiProvider: MultiProtocolProvider): { + chains: Record; + readyChains: Array; +} { + const evmChain = useEthereumActiveChain(multiProvider); + const solChain = useSolanaActiveChain(multiProvider); + const cosmChain = useCosmosActiveChain(multiProvider); + + const readyChains = useMemo( + () => [evmChain, solChain, cosmChain].filter((c) => !!c.chainDisplayName), + [evmChain, solChain, cosmChain], + ); + + return useMemo( + () => ({ + chains: { + [ProtocolType.Ethereum]: evmChain, + [ProtocolType.Sealevel]: solChain, + [ProtocolType.Cosmos]: cosmChain, + }, + readyChains, + }), + [evmChain, solChain, cosmChain, readyChains], + ); +} + +export function useTransactionFns( + multiProvider: MultiProtocolProvider, +): Record { + const { switchNetwork: onSwitchEvmNetwork, sendTransaction: onSendEvmTx } = + useEthereumTransactionFns(multiProvider); + const { switchNetwork: onSwitchSolNetwork, sendTransaction: onSendSolTx } = + useSolanaTransactionFns(multiProvider); + const { switchNetwork: onSwitchCosmNetwork, sendTransaction: onSendCosmTx } = + useCosmosTransactionFns(multiProvider); + + return useMemo( + () => ({ + [ProtocolType.Ethereum]: { + sendTransaction: onSendEvmTx, + switchNetwork: onSwitchEvmNetwork, + }, + [ProtocolType.Sealevel]: { + sendTransaction: onSendSolTx, + switchNetwork: onSwitchSolNetwork, + }, + [ProtocolType.Cosmos]: { + sendTransaction: onSendCosmTx, + switchNetwork: onSwitchCosmNetwork, + }, + }), + [ + onSendEvmTx, + onSendSolTx, + onSwitchEvmNetwork, + onSwitchSolNetwork, + onSendCosmTx, + onSwitchCosmNetwork, + ], + ); +} diff --git a/typescript/widgets/src/walletIntegrations/solana.ts b/typescript/widgets/src/walletIntegrations/solana.ts new file mode 100644 index 00000000000..94874645afd --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/solana.ts @@ -0,0 +1,138 @@ +import { useConnection, useWallet } from '@solana/wallet-adapter-react'; +import { useWalletModal } from '@solana/wallet-adapter-react-ui'; +import { Connection } from '@solana/web3.js'; +import { useCallback, useMemo } from 'react'; + +import { + ChainName, + MultiProtocolProvider, + ProviderType, + TypedTransactionReceipt, + WarpTypedTransaction, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { widgetLogger } from '../logger.js'; + +import { + AccountInfo, + ActiveChainInfo, + ChainTransactionFns, + WalletDetails, +} from './types.js'; +import { findChainByRpcUrl } from './utils.js'; + +const logger = widgetLogger.child({ module: 'walletIntegrations/solana' }); + +export function useSolanaAccount( + _multiProvider: MultiProtocolProvider, +): AccountInfo { + const { publicKey, connected, wallet } = useWallet(); + const isReady = !!(publicKey && wallet && connected); + const address = publicKey?.toBase58(); + + return useMemo( + () => ({ + protocol: ProtocolType.Sealevel, + addresses: address ? [{ address: address }] : [], + isReady: isReady, + }), + [address, isReady], + ); +} + +export function useSolanaWalletDetails() { + const { wallet } = useWallet(); + const { name, icon } = wallet?.adapter || {}; + + return useMemo( + () => ({ + name, + logoUrl: icon, + }), + [name, icon], + ); +} + +export function useSolanaConnectFn(): () => void { + const { setVisible } = useWalletModal(); + return useCallback(() => setVisible(true), [setVisible]); +} + +export function useSolanaDisconnectFn(): () => Promise { + const { disconnect } = useWallet(); + return disconnect; +} + +export function useSolanaActiveChain( + multiProvider: MultiProtocolProvider, +): ActiveChainInfo { + const { connection } = useConnection(); + const connectionEndpoint = connection?.rpcEndpoint; + return useMemo(() => { + try { + const hostname = new URL(connectionEndpoint).hostname; + const metadata = findChainByRpcUrl(multiProvider, hostname); + if (!metadata) return {}; + return { + chainDisplayName: metadata.displayName, + chainName: metadata.name, + }; + } catch (error) { + logger.warn('Error finding sol active chain', error); + return {}; + } + }, [connectionEndpoint, multiProvider]); +} + +export function useSolanaTransactionFns( + multiProvider: MultiProtocolProvider, +): ChainTransactionFns { + const { sendTransaction: sendSolTransaction } = useWallet(); + + const onSwitchNetwork = useCallback(async (chainName: ChainName) => { + logger.warn(`Solana wallet must be connected to origin chain ${chainName}`); + }, []); + + const onSendTx = useCallback( + async ({ + tx, + chainName, + activeChainName, + }: { + tx: WarpTypedTransaction; + chainName: ChainName; + activeChainName?: ChainName; + }) => { + if (tx.type !== ProviderType.SolanaWeb3) + throw new Error(`Unsupported tx type: ${tx.type}`); + if (activeChainName && activeChainName !== chainName) + await onSwitchNetwork(chainName); + const rpcUrl = multiProvider.getRpcUrl(chainName); + const connection = new Connection(rpcUrl, 'confirmed'); + const { + context: { slot: minContextSlot }, + value: { blockhash, lastValidBlockHeight }, + } = await connection.getLatestBlockhashAndContext(); + + logger.debug(`Sending tx on chain ${chainName}`); + const signature = await sendSolTransaction(tx.transaction, connection, { + minContextSlot, + }); + + const confirm = (): Promise => + connection + .confirmTransaction({ blockhash, lastValidBlockHeight, signature }) + .then(() => connection.getTransaction(signature)) + .then((r) => ({ + type: ProviderType.SolanaWeb3, + receipt: r!, + })); + + return { hash: signature, confirm }; + }, + [onSwitchNetwork, sendSolTransaction, multiProvider], + ); + + return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork }; +} diff --git a/typescript/widgets/src/walletIntegrations/types.ts b/typescript/widgets/src/walletIntegrations/types.ts new file mode 100644 index 00000000000..cab39fbdf2a --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/types.ts @@ -0,0 +1,48 @@ +import { + ChainName, + TypedTransactionReceipt, + WarpTypedTransaction, +} from '@hyperlane-xyz/sdk'; +import { HexString, ProtocolType } from '@hyperlane-xyz/utils'; + +export interface ChainAddress { + address: string; + chainName?: ChainName; +} + +export interface AccountInfo { + protocol: ProtocolType; + // This needs to be an array instead of a single address b.c. + // Cosmos wallets have different addresses per chain + addresses: Array; + // And another Cosmos exception, public keys are needed + // for tx simulation and gas estimation + publicKey?: Promise; + isReady: boolean; +} + +export interface WalletDetails { + name?: string; + logoUrl?: string; +} + +export interface ActiveChainInfo { + chainDisplayName?: string; + chainName?: ChainName; +} + +export type SendTransactionFn< + TxReq extends WarpTypedTransaction = WarpTypedTransaction, + TxResp extends TypedTransactionReceipt = TypedTransactionReceipt, +> = (params: { + tx: TxReq; + chainName: ChainName; + activeChainName?: ChainName; +}) => Promise<{ hash: string; confirm: () => Promise }>; + +export type SwitchNetworkFn = (chainName: ChainName) => Promise; + +export interface ChainTransactionFns { + sendTransaction: SendTransactionFn; + switchNetwork?: SwitchNetworkFn; +} diff --git a/typescript/widgets/src/walletIntegrations/utils.ts b/typescript/widgets/src/walletIntegrations/utils.ts new file mode 100644 index 00000000000..b0a994f1d86 --- /dev/null +++ b/typescript/widgets/src/walletIntegrations/utils.ts @@ -0,0 +1,56 @@ +import { SendTransactionParameters } from '@wagmi/core'; +import { + PopulatedTransaction as Ethers5Transaction, + BigNumber as EthersBN, +} from 'ethers'; + +import { ChainMetadata, MultiProtocolProvider } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +export function ethers5TxToWagmiTx( + tx: Ethers5Transaction, +): SendTransactionParameters { + if (!tx.to) throw new Error('No tx recipient address specified'); + if (!tx.data) throw new Error('No tx data specified'); + return { + to: tx.to as `0x${string}`, + value: ethersBnToBigInt(tx.value || EthersBN.from('0')), + data: tx.data as `0x{string}`, + nonce: tx.nonce, + chainId: tx.chainId, + gas: tx.gasLimit ? ethersBnToBigInt(tx.gasLimit) : undefined, + gasPrice: tx.gasPrice ? ethersBnToBigInt(tx.gasPrice) : undefined, + maxFeePerGas: tx.maxFeePerGas + ? ethersBnToBigInt(tx.maxFeePerGas) + : undefined, + maxPriorityFeePerGas: tx.maxPriorityFeePerGas + ? ethersBnToBigInt(tx.maxPriorityFeePerGas) + : undefined, + }; +} + +function ethersBnToBigInt(bn: EthersBN): bigint { + return BigInt(bn.toString()); +} + +export function getChainsForProtocol( + multiProvider: MultiProtocolProvider, + protocol: ProtocolType, +): ChainMetadata[] { + return Object.values(multiProvider.metadata).filter( + (c) => c.protocol === protocol, + ); +} + +export function findChainByRpcUrl( + multiProvider: MultiProtocolProvider, + url?: string, +) { + if (!url) return undefined; + const allMetadata = Object.values(multiProvider.metadata); + const searchUrl = url.toLowerCase(); + return allMetadata.find( + (m) => + !!m.rpcUrls.find((rpc) => rpc.http.toLowerCase().includes(searchUrl)), + ); +} diff --git a/typescript/widgets/tailwind.config.cjs b/typescript/widgets/tailwind.config.cjs index f410968ec40..48dedefff00 100644 --- a/typescript/widgets/tailwind.config.cjs +++ b/typescript/widgets/tailwind.config.cjs @@ -12,6 +12,7 @@ module.exports = { mono: ['Courier New', 'monospace'], }, screens: { + all: '1px', xs: '480px', ...defaultTheme.screens, }, diff --git a/yarn.lock b/yarn.lock index 9ba2eef3e18..bc7551faf59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,7 +19,7 @@ __metadata: languageName: node linkType: hard -"@adraffy/ens-normalize@npm:1.11.0": +"@adraffy/ens-normalize@npm:^1.10.1": version: 1.11.0 resolution: "@adraffy/ens-normalize@npm:1.11.0" checksum: 10/abef75f21470ea43dd6071168e092d2d13e38067e349e76186c78838ae174a46c3e18ca50921d05bea6ec3203074147c9e271f8cb6531d1c2c0e146f3199ddcb @@ -55,18 +55,6 @@ __metadata: languageName: node linkType: hard -"@arbitrum/sdk@npm:^3.0.0": - version: 3.0.0 - resolution: "@arbitrum/sdk@npm:3.0.0" - dependencies: - "@ethersproject/address": "npm:^5.0.8" - "@ethersproject/bignumber": "npm:^5.1.1" - "@ethersproject/bytes": "npm:^5.0.8" - ethers: "npm:^5.1.0" - checksum: 10/f4f7d05631d2014546cccff85926a638e3725e522e2c9c73c70caafec8f14cf7b22f58c8f942ced2f8bd44ea545b63c99cf5044c833edd0d52934afdddbf1d40 - languageName: node - linkType: hard - "@arbitrum/sdk@npm:^4.0.0": version: 4.0.1 resolution: "@arbitrum/sdk@npm:4.0.1" @@ -91,17 +79,6 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/crc32@npm:2.0.0": - version: 2.0.0 - resolution: "@aws-crypto/crc32@npm:2.0.0" - dependencies: - "@aws-crypto/util": "npm:^2.0.0" - "@aws-sdk/types": "npm:^3.1.0" - tslib: "npm:^1.11.1" - checksum: 10/da8e32353f958775b4476150c6b457ce1a04b962cdb227842f8a3fa52cb996eb979b2858d19abd6703256c0befc08ee80e3ce48e02d39900640dc6696b151701 - languageName: node - linkType: hard - "@aws-crypto/crc32@npm:3.0.0": version: 3.0.0 resolution: "@aws-crypto/crc32@npm:3.0.0" @@ -113,17 +90,6 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/crc32c@npm:2.0.0": - version: 2.0.0 - resolution: "@aws-crypto/crc32c@npm:2.0.0" - dependencies: - "@aws-crypto/util": "npm:^2.0.0" - "@aws-sdk/types": "npm:^3.1.0" - tslib: "npm:^1.11.1" - checksum: 10/04496af8f9a4822bf09793c7df20fbebbdda75484eb1c8a89e98b41ab4e0b57ce9e33b4b30d6b741d85c2f751298fc39919184b5985c66e252d180ddc6f30ab1 - languageName: node - linkType: hard - "@aws-crypto/crc32c@npm:3.0.0": version: 3.0.0 resolution: "@aws-crypto/crc32c@npm:3.0.0" @@ -153,20 +119,6 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/sha1-browser@npm:2.0.0": - version: 2.0.0 - resolution: "@aws-crypto/sha1-browser@npm:2.0.0" - dependencies: - "@aws-crypto/ie11-detection": "npm:^2.0.0" - "@aws-crypto/supports-web-crypto": "npm:^2.0.0" - "@aws-sdk/types": "npm:^3.1.0" - "@aws-sdk/util-locate-window": "npm:^3.0.0" - "@aws-sdk/util-utf8-browser": "npm:^3.0.0" - tslib: "npm:^1.11.1" - checksum: 10/7a1e828741339effdb26e89b28d30010f954192c75dc197fe9856faf46d9fd998b3a8c473c3f8b86ebc259ef1162191c6bd4c9c23803ea0b66b3abcff511917a - languageName: node - linkType: hard - "@aws-crypto/sha1-browser@npm:3.0.0": version: 3.0.0 resolution: "@aws-crypto/sha1-browser@npm:3.0.0" @@ -319,16 +271,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/abort-controller@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/abort-controller@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/97a5a4dfb433be2738a4ca11b830e434df1e7af9448dbbce3bd11d9f13f1e29eb987a859163250b8d49e3f0e8b8cbc5199131138b5e16a2f99daa9c2a1454f79 - languageName: node - linkType: hard - "@aws-sdk/abort-controller@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/abort-controller@npm:3.78.0" @@ -339,25 +281,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/chunked-blob-reader-native@npm:3.58.0": - version: 3.58.0 - resolution: "@aws-sdk/chunked-blob-reader-native@npm:3.58.0" - dependencies: - "@aws-sdk/util-base64-browser": "npm:3.58.0" - tslib: "npm:^2.3.1" - checksum: 10/7826f67d2f1f4af0939ae0d764ee617e02fd4e46e14e12714b559043c9ab77dcd7287ec94636bc2849b2b179803058313a1902944d647a4afe81989730c5de08 - languageName: node - linkType: hard - -"@aws-sdk/chunked-blob-reader@npm:3.55.0": - version: 3.55.0 - resolution: "@aws-sdk/chunked-blob-reader@npm:3.55.0" - dependencies: - tslib: "npm:^2.3.1" - checksum: 10/e19fcff0162b2b28a46a3c3c4936f4e7ae7c2588aac0948ec2796ac6b0d51ae8e3d50bd855f7b07230c80e00708283352435d024e16834b61d7d353b22120d73 - languageName: node - linkType: hard - "@aws-sdk/client-iam@npm:^3.74.0": version: 3.107.0 resolution: "@aws-sdk/client-iam@npm:3.107.0" @@ -403,47 +326,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-kms@npm:3.48.0": - version: 3.48.0 - resolution: "@aws-sdk/client-kms@npm:3.48.0" - dependencies: - "@aws-crypto/sha256-browser": "npm:2.0.0" - "@aws-crypto/sha256-js": "npm:2.0.0" - "@aws-sdk/client-sts": "npm:3.48.0" - "@aws-sdk/config-resolver": "npm:3.47.2" - "@aws-sdk/credential-provider-node": "npm:3.48.0" - "@aws-sdk/fetch-http-handler": "npm:3.47.2" - "@aws-sdk/hash-node": "npm:3.47.2" - "@aws-sdk/invalid-dependency": "npm:3.47.2" - "@aws-sdk/middleware-content-length": "npm:3.47.2" - "@aws-sdk/middleware-host-header": "npm:3.47.2" - "@aws-sdk/middleware-logger": "npm:3.47.2" - "@aws-sdk/middleware-retry": "npm:3.47.2" - "@aws-sdk/middleware-serde": "npm:3.47.2" - "@aws-sdk/middleware-signing": "npm:3.47.2" - "@aws-sdk/middleware-stack": "npm:3.47.2" - "@aws-sdk/middleware-user-agent": "npm:3.47.2" - "@aws-sdk/node-config-provider": "npm:3.47.2" - "@aws-sdk/node-http-handler": "npm:3.47.2" - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/smithy-client": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/url-parser": "npm:3.47.2" - "@aws-sdk/util-base64-browser": "npm:3.47.1" - "@aws-sdk/util-base64-node": "npm:3.47.2" - "@aws-sdk/util-body-length-browser": "npm:3.47.1" - "@aws-sdk/util-body-length-node": "npm:3.47.1" - "@aws-sdk/util-defaults-mode-browser": "npm:3.47.2" - "@aws-sdk/util-defaults-mode-node": "npm:3.47.2" - "@aws-sdk/util-user-agent-browser": "npm:3.47.2" - "@aws-sdk/util-user-agent-node": "npm:3.47.2" - "@aws-sdk/util-utf8-browser": "npm:3.47.1" - "@aws-sdk/util-utf8-node": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/eff5d95d51bddcefe533dbab1c45cc06e1cd4b6549ae9cc2e1cbfb23dec21292649e5d9335751cfb773674e73507e35fc026245878d7e61c903ed12242f76e9b - languageName: node - linkType: hard - "@aws-sdk/client-kms@npm:^3.28.0, @aws-sdk/client-kms@npm:^3.39.0": version: 3.142.0 resolution: "@aws-sdk/client-kms@npm:3.142.0" @@ -601,68 +483,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-s3@npm:^3.74.0": - version: 3.107.0 - resolution: "@aws-sdk/client-s3@npm:3.107.0" - dependencies: - "@aws-crypto/sha1-browser": "npm:2.0.0" - "@aws-crypto/sha256-browser": "npm:2.0.0" - "@aws-crypto/sha256-js": "npm:2.0.0" - "@aws-sdk/client-sts": "npm:3.105.0" - "@aws-sdk/config-resolver": "npm:3.80.0" - "@aws-sdk/credential-provider-node": "npm:3.105.0" - "@aws-sdk/eventstream-serde-browser": "npm:3.78.0" - "@aws-sdk/eventstream-serde-config-resolver": "npm:3.78.0" - "@aws-sdk/eventstream-serde-node": "npm:3.78.0" - "@aws-sdk/fetch-http-handler": "npm:3.78.0" - "@aws-sdk/hash-blob-browser": "npm:3.78.0" - "@aws-sdk/hash-node": "npm:3.78.0" - "@aws-sdk/hash-stream-node": "npm:3.78.0" - "@aws-sdk/invalid-dependency": "npm:3.78.0" - "@aws-sdk/md5-js": "npm:3.78.0" - "@aws-sdk/middleware-bucket-endpoint": "npm:3.80.0" - "@aws-sdk/middleware-content-length": "npm:3.78.0" - "@aws-sdk/middleware-expect-continue": "npm:3.78.0" - "@aws-sdk/middleware-flexible-checksums": "npm:3.78.0" - "@aws-sdk/middleware-host-header": "npm:3.78.0" - "@aws-sdk/middleware-location-constraint": "npm:3.78.0" - "@aws-sdk/middleware-logger": "npm:3.78.0" - "@aws-sdk/middleware-recursion-detection": "npm:3.105.0" - "@aws-sdk/middleware-retry": "npm:3.80.0" - "@aws-sdk/middleware-sdk-s3": "npm:3.105.0" - "@aws-sdk/middleware-serde": "npm:3.78.0" - "@aws-sdk/middleware-signing": "npm:3.78.0" - "@aws-sdk/middleware-ssec": "npm:3.78.0" - "@aws-sdk/middleware-stack": "npm:3.78.0" - "@aws-sdk/middleware-user-agent": "npm:3.78.0" - "@aws-sdk/node-config-provider": "npm:3.80.0" - "@aws-sdk/node-http-handler": "npm:3.94.0" - "@aws-sdk/protocol-http": "npm:3.78.0" - "@aws-sdk/signature-v4-multi-region": "npm:3.88.0" - "@aws-sdk/smithy-client": "npm:3.99.0" - "@aws-sdk/types": "npm:3.78.0" - "@aws-sdk/url-parser": "npm:3.78.0" - "@aws-sdk/util-base64-browser": "npm:3.58.0" - "@aws-sdk/util-base64-node": "npm:3.55.0" - "@aws-sdk/util-body-length-browser": "npm:3.55.0" - "@aws-sdk/util-body-length-node": "npm:3.55.0" - "@aws-sdk/util-defaults-mode-browser": "npm:3.99.0" - "@aws-sdk/util-defaults-mode-node": "npm:3.99.0" - "@aws-sdk/util-stream-browser": "npm:3.78.0" - "@aws-sdk/util-stream-node": "npm:3.78.0" - "@aws-sdk/util-user-agent-browser": "npm:3.78.0" - "@aws-sdk/util-user-agent-node": "npm:3.80.0" - "@aws-sdk/util-utf8-browser": "npm:3.55.0" - "@aws-sdk/util-utf8-node": "npm:3.55.0" - "@aws-sdk/util-waiter": "npm:3.78.0" - "@aws-sdk/xml-builder": "npm:3.55.0" - entities: "npm:2.2.0" - fast-xml-parser: "npm:3.19.0" - tslib: "npm:^2.3.1" - checksum: 10/827f4fae394677bba41f581c851effc438935e49480d99a26c15c3315ea7a1a696c15163b2de44ef0b40e7bb05396c372e3b200e974133e238d262c33fbe6f1d - languageName: node - linkType: hard - "@aws-sdk/client-sso-oidc@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/client-sso-oidc@npm:3.577.0" @@ -789,44 +609,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.48.0": - version: 3.48.0 - resolution: "@aws-sdk/client-sso@npm:3.48.0" - dependencies: - "@aws-crypto/sha256-browser": "npm:2.0.0" - "@aws-crypto/sha256-js": "npm:2.0.0" - "@aws-sdk/config-resolver": "npm:3.47.2" - "@aws-sdk/fetch-http-handler": "npm:3.47.2" - "@aws-sdk/hash-node": "npm:3.47.2" - "@aws-sdk/invalid-dependency": "npm:3.47.2" - "@aws-sdk/middleware-content-length": "npm:3.47.2" - "@aws-sdk/middleware-host-header": "npm:3.47.2" - "@aws-sdk/middleware-logger": "npm:3.47.2" - "@aws-sdk/middleware-retry": "npm:3.47.2" - "@aws-sdk/middleware-serde": "npm:3.47.2" - "@aws-sdk/middleware-stack": "npm:3.47.2" - "@aws-sdk/middleware-user-agent": "npm:3.47.2" - "@aws-sdk/node-config-provider": "npm:3.47.2" - "@aws-sdk/node-http-handler": "npm:3.47.2" - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/smithy-client": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/url-parser": "npm:3.47.2" - "@aws-sdk/util-base64-browser": "npm:3.47.1" - "@aws-sdk/util-base64-node": "npm:3.47.2" - "@aws-sdk/util-body-length-browser": "npm:3.47.1" - "@aws-sdk/util-body-length-node": "npm:3.47.1" - "@aws-sdk/util-defaults-mode-browser": "npm:3.47.2" - "@aws-sdk/util-defaults-mode-node": "npm:3.47.2" - "@aws-sdk/util-user-agent-browser": "npm:3.47.2" - "@aws-sdk/util-user-agent-node": "npm:3.47.2" - "@aws-sdk/util-utf8-browser": "npm:3.47.1" - "@aws-sdk/util-utf8-node": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/917bb56fdc67a324aa7c933f0dc9405036f98306ddf4a8cccade01801d91a8becde30f5b979a7f4f8084fddc3b028292a8c3fce03d9d1f60c9ed450e602aa607 - languageName: node - linkType: hard - "@aws-sdk/client-sso@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/client-sso@npm:3.577.0" @@ -961,49 +743,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.48.0": - version: 3.48.0 - resolution: "@aws-sdk/client-sts@npm:3.48.0" - dependencies: - "@aws-crypto/sha256-browser": "npm:2.0.0" - "@aws-crypto/sha256-js": "npm:2.0.0" - "@aws-sdk/config-resolver": "npm:3.47.2" - "@aws-sdk/credential-provider-node": "npm:3.48.0" - "@aws-sdk/fetch-http-handler": "npm:3.47.2" - "@aws-sdk/hash-node": "npm:3.47.2" - "@aws-sdk/invalid-dependency": "npm:3.47.2" - "@aws-sdk/middleware-content-length": "npm:3.47.2" - "@aws-sdk/middleware-host-header": "npm:3.47.2" - "@aws-sdk/middleware-logger": "npm:3.47.2" - "@aws-sdk/middleware-retry": "npm:3.47.2" - "@aws-sdk/middleware-sdk-sts": "npm:3.47.2" - "@aws-sdk/middleware-serde": "npm:3.47.2" - "@aws-sdk/middleware-signing": "npm:3.47.2" - "@aws-sdk/middleware-stack": "npm:3.47.2" - "@aws-sdk/middleware-user-agent": "npm:3.47.2" - "@aws-sdk/node-config-provider": "npm:3.47.2" - "@aws-sdk/node-http-handler": "npm:3.47.2" - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/smithy-client": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/url-parser": "npm:3.47.2" - "@aws-sdk/util-base64-browser": "npm:3.47.1" - "@aws-sdk/util-base64-node": "npm:3.47.2" - "@aws-sdk/util-body-length-browser": "npm:3.47.1" - "@aws-sdk/util-body-length-node": "npm:3.47.1" - "@aws-sdk/util-defaults-mode-browser": "npm:3.47.2" - "@aws-sdk/util-defaults-mode-node": "npm:3.47.2" - "@aws-sdk/util-user-agent-browser": "npm:3.47.2" - "@aws-sdk/util-user-agent-node": "npm:3.47.2" - "@aws-sdk/util-utf8-browser": "npm:3.47.1" - "@aws-sdk/util-utf8-node": "npm:3.47.2" - entities: "npm:2.2.0" - fast-xml-parser: "npm:3.19.0" - tslib: "npm:^2.3.0" - checksum: 10/2ee2017400b1fc8113a2b74325f3bbc5f333143df910c78ad9dd3ab109ec1e1066b713d54192791966dfe7497e69b675938ddd9a896a125eada237f0d2bd2152 - languageName: node - linkType: hard - "@aws-sdk/client-sts@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/client-sts@npm:3.577.0" @@ -1065,18 +804,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/config-resolver@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/config-resolver@npm:3.47.2" - dependencies: - "@aws-sdk/signature-v4": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-config-provider": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/7e4f342261a12bb8c041fdc8ab76f9e1b2a2370e607f50a7a099cba174a3d11a32640399d344ed312ace34d7e118b71ca7c4c83df595989718b501cecc894413 - languageName: node - linkType: hard - "@aws-sdk/config-resolver@npm:3.80.0": version: 3.80.0 resolution: "@aws-sdk/config-resolver@npm:3.80.0" @@ -1116,17 +843,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/credential-provider-env@npm:3.47.2" - dependencies: - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/11416fb43d27a60c5c3b493f4145adabb6e4277642e94e429eb770c5ee9029a49230ac63b9088a9465c8bf93acf48673c289ae1666a7e34ab6c6f9d58539e08e - languageName: node - linkType: hard - "@aws-sdk/credential-provider-env@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/credential-provider-env@npm:3.577.0" @@ -1180,19 +896,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-imds@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/credential-provider-imds@npm:3.47.2" - dependencies: - "@aws-sdk/node-config-provider": "npm:3.47.2" - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/url-parser": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/37745d9957be76f2edcb73c7668e1af35c7e532f0dec477927222a692ebf84dcaa4393d8c27b8460eb2fdf7d8a4bbf1f959ac9dd3a8f05f0884d5bb20b2f2663 - languageName: node - linkType: hard - "@aws-sdk/credential-provider-imds@npm:3.81.0": version: 3.81.0 resolution: "@aws-sdk/credential-provider-imds@npm:3.81.0" @@ -1238,23 +941,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.48.0": - version: 3.48.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.48.0" - dependencies: - "@aws-sdk/credential-provider-env": "npm:3.47.2" - "@aws-sdk/credential-provider-imds": "npm:3.47.2" - "@aws-sdk/credential-provider-sso": "npm:3.48.0" - "@aws-sdk/credential-provider-web-identity": "npm:3.47.2" - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/shared-ini-file-loader": "npm:3.47.1" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-credentials": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/9c2d58944e5d6dd9076b82d6858562e7a5b4766b32d8a4a23040b6d0933b8cb8481a97939d1c53a29a26dd6b2486601e466146b0f2e99b37afd21cc58ae19537 - languageName: node - linkType: hard - "@aws-sdk/credential-provider-ini@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/credential-provider-ini@npm:3.577.0" @@ -1311,25 +997,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.48.0": - version: 3.48.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.48.0" - dependencies: - "@aws-sdk/credential-provider-env": "npm:3.47.2" - "@aws-sdk/credential-provider-imds": "npm:3.47.2" - "@aws-sdk/credential-provider-ini": "npm:3.48.0" - "@aws-sdk/credential-provider-process": "npm:3.47.2" - "@aws-sdk/credential-provider-sso": "npm:3.48.0" - "@aws-sdk/credential-provider-web-identity": "npm:3.47.2" - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/shared-ini-file-loader": "npm:3.47.1" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-credentials": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/6b038691cffad9a8cd54a1e7cb4da927205fea7e5ba2add4788216f2af4280635d1e6b0c63927866fc0ba6f156b76485643670a462dacfb841e9abba91fb4584 - languageName: node - linkType: hard - "@aws-sdk/credential-provider-node@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/credential-provider-node@npm:3.577.0" @@ -1362,19 +1029,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/credential-provider-process@npm:3.47.2" - dependencies: - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/shared-ini-file-loader": "npm:3.47.1" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-credentials": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/d59d900f443b8fd2f8b74902bc0c0d6bdf0156a8d44981165e051e6856793d80b84ba43b87f77c24fe1569426365bfb226e3599d60e589abbe2349de00e32666 - languageName: node - linkType: hard - "@aws-sdk/credential-provider-process@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/credential-provider-process@npm:3.577.0" @@ -1426,20 +1080,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.48.0": - version: 3.48.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.48.0" - dependencies: - "@aws-sdk/client-sso": "npm:3.48.0" - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/shared-ini-file-loader": "npm:3.47.1" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-credentials": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/0c012e75023687c72fcf8945b0b83242d8d2e2f526b2c241f77ab9b1ab462292f836b5e1866966b6bf5db3ff2c1fba2a554304651e8257ccbe2e47d3bc1f8db1 - languageName: node - linkType: hard - "@aws-sdk/credential-provider-sso@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/credential-provider-sso@npm:3.577.0" @@ -1466,17 +1106,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.47.2" - dependencies: - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/eb85ea63ad1875dccb1efec76cb8c297a256fa9988a45141d242812e0ea059fa9b41ab4a912aea96c66a25321cc7c003ab834c707df96949c6380c6ca1111929 - languageName: node - linkType: hard - "@aws-sdk/credential-provider-web-identity@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/credential-provider-web-identity@npm:3.577.0" @@ -1502,63 +1131,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/eventstream-marshaller@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/eventstream-marshaller@npm:3.78.0" - dependencies: - "@aws-crypto/crc32": "npm:2.0.0" - "@aws-sdk/types": "npm:3.78.0" - "@aws-sdk/util-hex-encoding": "npm:3.58.0" - tslib: "npm:^2.3.1" - checksum: 10/e62d0792d28d2bc9b7ce04c95442d698f2d7043872bfc0c91422ed4e3a5b38fa1284148dc4be06545aaa0d52107650bb53da9a37733f23e30ff83ca83a93652d - languageName: node - linkType: hard - -"@aws-sdk/eventstream-serde-browser@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/eventstream-serde-browser@npm:3.78.0" - dependencies: - "@aws-sdk/eventstream-marshaller": "npm:3.78.0" - "@aws-sdk/eventstream-serde-universal": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/f2be29f11c2c02c41fd161495edab2b2a90a0087840626aa8d5209e005e6be510d48cf543871a77e508db472c47922823101fda35dd9f1f2e65401ce652edfb5 - languageName: node - linkType: hard - -"@aws-sdk/eventstream-serde-config-resolver@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/eventstream-serde-config-resolver@npm:3.78.0" - dependencies: - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/38bf3a5a496bedf5f5bb1a32da4934dbfe720594fb0d3028da861428f1d0fba2091d1d634eada9e132019486682a5d318648120bf5ca87ddfe38b8a3be7c0c62 - languageName: node - linkType: hard - -"@aws-sdk/eventstream-serde-node@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/eventstream-serde-node@npm:3.78.0" - dependencies: - "@aws-sdk/eventstream-marshaller": "npm:3.78.0" - "@aws-sdk/eventstream-serde-universal": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/d0eb279d27d4757c33e7ccb69ff31036ab1d5573570daebeabac773a708e89fa85c0bf8569c6d6faf976734ef4e09d5375f589723819e095d076090ff68361c7 - languageName: node - linkType: hard - -"@aws-sdk/eventstream-serde-universal@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/eventstream-serde-universal@npm:3.78.0" - dependencies: - "@aws-sdk/eventstream-marshaller": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/fe8f7f219ce43d8920ff8a1434507f1504b652e03b18a53f01f7d9d60a1f0ccf6d5d0abff8c3317be20e93c317491ae140af404f5175f6b8056cf315ceb9cb86 - languageName: node - linkType: hard - "@aws-sdk/fetch-http-handler@npm:3.131.0": version: 3.131.0 resolution: "@aws-sdk/fetch-http-handler@npm:3.131.0" @@ -1572,19 +1144,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/fetch-http-handler@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/fetch-http-handler@npm:3.47.2" - dependencies: - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/querystring-builder": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-base64-browser": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/a37cc6cce12ab8d1a280d7f9c60e97972ed4bc9d7677418af65cd0ea18e8db4d01561a6d8d4ce8da643256b5e9087369d881532001c7076d85810a60143de69b - languageName: node - linkType: hard - "@aws-sdk/fetch-http-handler@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/fetch-http-handler@npm:3.78.0" @@ -1598,18 +1157,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/hash-blob-browser@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/hash-blob-browser@npm:3.78.0" - dependencies: - "@aws-sdk/chunked-blob-reader": "npm:3.55.0" - "@aws-sdk/chunked-blob-reader-native": "npm:3.58.0" - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/5e257b36f93bfa3af233c061f8cb2f8be28d1a21a176654acffc90b0040174fff6d181012d33729a59d7e991f0e960dfbd455a34181c1298ede52418ad606a58 - languageName: node - linkType: hard - "@aws-sdk/hash-node@npm:3.127.0": version: 3.127.0 resolution: "@aws-sdk/hash-node@npm:3.127.0" @@ -1621,17 +1168,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/hash-node@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/hash-node@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-buffer-from": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/b240e43e4bd4744e32e7f578118435cc9e0d069e50ec9c9669beb8813582fb1f0ddb5d58948593d7227d7cce4d2fe416255b91c8c5c8ee7385e7f7520b0a9e10 - languageName: node - linkType: hard - "@aws-sdk/hash-node@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/hash-node@npm:3.78.0" @@ -1643,16 +1179,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/hash-stream-node@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/hash-stream-node@npm:3.78.0" - dependencies: - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/dba65ef684458e45c03b9555d6ecb16747299bc30be3341b34f542f1b511bbda685df384dc729717254e4ae1756b775e2743b3e93e1635cf6d8dd34a90f167ac - languageName: node - linkType: hard - "@aws-sdk/invalid-dependency@npm:3.127.0": version: 3.127.0 resolution: "@aws-sdk/invalid-dependency@npm:3.127.0" @@ -1663,16 +1189,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/invalid-dependency@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/invalid-dependency@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/a5808f8e4d4d696610a3be8e6dee2ad387187eef9a05edaf008b4af12838f9f5cb900729698228fdeaed5ededdae080d91d1c2607cc30d650cb92dd09a8c7b05 - languageName: node - linkType: hard - "@aws-sdk/invalid-dependency@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/invalid-dependency@npm:3.78.0" @@ -1683,15 +1199,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/is-array-buffer@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/is-array-buffer@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/5287771b7ca6e1d8b02e32c62fb7065cd6c827ea9ef030a5868cfacc7cf88aec9d9d6fc65ab216326e00184c423a9e574d93704b8cbc63338c2a378a00fdd02b - languageName: node - linkType: hard - "@aws-sdk/is-array-buffer@npm:3.55.0": version: 3.55.0 resolution: "@aws-sdk/is-array-buffer@npm:3.55.0" @@ -1701,18 +1208,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/md5-js@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/md5-js@npm:3.78.0" - dependencies: - "@aws-sdk/types": "npm:3.78.0" - "@aws-sdk/util-utf8-browser": "npm:3.55.0" - "@aws-sdk/util-utf8-node": "npm:3.55.0" - tslib: "npm:^2.3.1" - checksum: 10/03967189ad87b6e5b234f8125cee0da890e024517e7d58474c4714fcc134069233bae65e949cd285b1e963be07c06500b1a82481880836fe978788002f3360c3 - languageName: node - linkType: hard - "@aws-sdk/middleware-bucket-endpoint@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.577.0" @@ -1728,19 +1223,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.80.0": - version: 3.80.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.80.0" - dependencies: - "@aws-sdk/protocol-http": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - "@aws-sdk/util-arn-parser": "npm:3.55.0" - "@aws-sdk/util-config-provider": "npm:3.55.0" - tslib: "npm:^2.3.1" - checksum: 10/06ba62bcb728d16bc70079af23385902880580f2f69f1a7fb3dc24012157974989a846047a9f08424f3b7e662089b34b09b4d560017785d699fa71e8da3cbb05 - languageName: node - linkType: hard - "@aws-sdk/middleware-content-length@npm:3.127.0": version: 3.127.0 resolution: "@aws-sdk/middleware-content-length@npm:3.127.0" @@ -1752,17 +1234,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-content-length@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-content-length@npm:3.47.2" - dependencies: - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/0c76fd9c82e9cc568fb6431b3ef34e97bd03a0a1827800363acab75bf1a47384b05fe1d9377becd4781f075de3f95fc089b083791595a01c4deca95f3db626df - languageName: node - linkType: hard - "@aws-sdk/middleware-content-length@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/middleware-content-length@npm:3.78.0" @@ -1786,18 +1257,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.78.0" - dependencies: - "@aws-sdk/middleware-header-default": "npm:3.78.0" - "@aws-sdk/protocol-http": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/6db82f13c17a66a1e897419b707480298608caa32b5543229d29a2d1e2ef266d4dfda397164429ca1fe87e730da4d7c4f2a537a0ab0b4aa9d41b88509fd4c8f9 - languageName: node - linkType: hard - "@aws-sdk/middleware-flexible-checksums@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.577.0" @@ -1814,31 +1273,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.78.0" - dependencies: - "@aws-crypto/crc32": "npm:2.0.0" - "@aws-crypto/crc32c": "npm:2.0.0" - "@aws-sdk/is-array-buffer": "npm:3.55.0" - "@aws-sdk/protocol-http": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/5062b9f8fbd3f8700aa0ee221d6be08e10715b3b473ae1251ff33df7751b995c1efaed52a20d116fa96c517066b696fce1fb29ecf0dddc2bd5fd696c45a46f56 - languageName: node - linkType: hard - -"@aws-sdk/middleware-header-default@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/middleware-header-default@npm:3.78.0" - dependencies: - "@aws-sdk/protocol-http": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/fba1438bbc32e39120a7657d6d0204e616c4170f564638ed7b33c122ee229a9199f2d74b4efaa380e3da59aa2896b7f365c41d5fef7c7aa8ad0e0edb3a2e3e9d - languageName: node - linkType: hard - "@aws-sdk/middleware-host-header@npm:3.127.0": version: 3.127.0 resolution: "@aws-sdk/middleware-host-header@npm:3.127.0" @@ -1850,17 +1284,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-host-header@npm:3.47.2" - dependencies: - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/8f17bf82fa35a175a2443188860e21c52dbdd1cdf7be2dff685f12e32f6b7b7cbda749a45900a5b78cb7392d8b15f6893f22bbb16dfd5737a1e56a59741021fb - languageName: node - linkType: hard - "@aws-sdk/middleware-host-header@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/middleware-host-header@npm:3.577.0" @@ -1895,16 +1318,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.78.0" - dependencies: - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/88e2aa48c49af4cacbe44022b4729c52dc173ec1c7672f44326641b7169e298e6cff435d1e1502455982521b407d1df5ab7cf33e7770bcf5c947427ec5e160f5 - languageName: node - linkType: hard - "@aws-sdk/middleware-logger@npm:3.127.0": version: 3.127.0 resolution: "@aws-sdk/middleware-logger@npm:3.127.0" @@ -1915,16 +1328,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-logger@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/515f84cbd57346d9599d12e3cb9dd7b80a353ef79dba4601e3d37a32db2d2abc0066d397658dbb5c6f93c5ca49ae6e91b9e3ca76dc9ab6feb23ddbf081383612 - languageName: node - linkType: hard - "@aws-sdk/middleware-logger@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/middleware-logger@npm:3.577.0" @@ -1994,19 +1397,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-retry@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-retry@npm:3.47.2" - dependencies: - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/service-error-classification": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - uuid: "npm:^8.3.2" - checksum: 10/e9f56166045ca3446f864ee75d7226cbf665c9a21d2237daba87c7bbeea9aa1035c05fc03dc9153a92fe780b4a3469277e8ebb33fa1b57233573c1a0f066462b - languageName: node - linkType: hard - "@aws-sdk/middleware-retry@npm:3.80.0": version: 3.80.0 resolution: "@aws-sdk/middleware-retry@npm:3.80.0" @@ -2021,18 +1411,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:3.105.0": - version: 3.105.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.105.0" - dependencies: - "@aws-sdk/protocol-http": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - "@aws-sdk/util-arn-parser": "npm:3.55.0" - tslib: "npm:^2.3.1" - checksum: 10/bf7d38974d8ff75e01f6ade411e9b09e3e90ad57a78bf21c45762b4eea78fe9741b36395c418c9bccf6b17020a678d519575300bd6a0296769b576a5c8865d5c - languageName: node - linkType: hard - "@aws-sdk/middleware-sdk-s3@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/middleware-sdk-s3@npm:3.577.0" @@ -2064,20 +1442,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sts@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-sdk-sts@npm:3.47.2" - dependencies: - "@aws-sdk/middleware-signing": "npm:3.47.2" - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/signature-v4": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/135c88d46b6350a13485c046a1ca27a7d44dc9fea3ea9206785b418a59f9cbbb4d7ca79c75d1521ed8920a7d57acd588d77b8d1e14ab4e5fddaccf87c559dea2 - languageName: node - linkType: hard - "@aws-sdk/middleware-sdk-sts@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/middleware-sdk-sts@npm:3.78.0" @@ -2102,16 +1466,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-serde@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-serde@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/89f0b72750e1f4f25f94277381d8e4c7ee1694570f99830edd5ef4c21f714737b49eb206c6d78139ae0cfb861831dbfcbc29c66cf565ec59468189ca3003fedb - languageName: node - linkType: hard - "@aws-sdk/middleware-serde@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/middleware-serde@npm:3.78.0" @@ -2135,19 +1489,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-signing@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-signing@npm:3.47.2" - dependencies: - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/signature-v4": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/1127e94e56efb3caad0b1bc5ccddb80daae0ca810a6b75a5b6b12d1da9812ec0734e15f80d33e7a94a173debf0a8135a5d31c7f308cf7fe99bb2ee56df9335d7 - languageName: node - linkType: hard - "@aws-sdk/middleware-signing@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/middleware-signing@npm:3.577.0" @@ -2187,16 +1528,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.78.0" - dependencies: - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/fa9b087bd6c718f0c5f04f7f75ad8afcba743a95231e50c9b913ab1202bc23b5a96e0c81363457889c4184ed0342818a7c2f90f977379bce22e03865a6f6ce42 - languageName: node - linkType: hard - "@aws-sdk/middleware-stack@npm:3.127.0": version: 3.127.0 resolution: "@aws-sdk/middleware-stack@npm:3.127.0" @@ -2206,15 +1537,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-stack@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-stack@npm:3.47.2" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/427ed4888602ebd342c0443eb992f0daf419ea5cbe50b715b9d89dd99529ec14603d171e29d9adda11121e2bb84bafd586c2719b631aae5e396038c99a2e669a - languageName: node - linkType: hard - "@aws-sdk/middleware-stack@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/middleware-stack@npm:3.78.0" @@ -2235,17 +1557,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/middleware-user-agent@npm:3.47.2" - dependencies: - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/fb4620b773d8927236308eacb58422cac3792558a3cef307580e67412cc8f1bf9af18509f29417238078fb8b6334205c63b7cdce81921e4b678278f7b346ae44 - languageName: node - linkType: hard - "@aws-sdk/middleware-user-agent@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/middleware-user-agent@npm:3.577.0" @@ -2282,18 +1593,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/node-config-provider@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/node-config-provider@npm:3.47.2" - dependencies: - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/shared-ini-file-loader": "npm:3.47.1" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/17a83bd6b259d627ab6869a889ea7a21197d629ba320004c3d6fe771e8ff162e275445fa7eda9007a5ae761af53a3d9586d1c8fbb45e11c5a2d41601b3bbda23 - languageName: node - linkType: hard - "@aws-sdk/node-config-provider@npm:3.80.0": version: 3.80.0 resolution: "@aws-sdk/node-config-provider@npm:3.80.0" @@ -2319,19 +1618,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/node-http-handler@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/node-http-handler@npm:3.47.2" - dependencies: - "@aws-sdk/abort-controller": "npm:3.47.2" - "@aws-sdk/protocol-http": "npm:3.47.2" - "@aws-sdk/querystring-builder": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/c864b92c35a6d7955af3c4764538324e02d7aa34f54c78dd3679515d989ca151e7ba55a65de7e5686a311aa3070c12c331fd88210aadb008925134bd31afe4ef - languageName: node - linkType: hard - "@aws-sdk/node-http-handler@npm:3.94.0": version: 3.94.0 resolution: "@aws-sdk/node-http-handler@npm:3.94.0" @@ -2355,16 +1641,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/property-provider@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/property-provider@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/e731ed31300fa0352744a5209e5f476a5ec82390ad2bbe1d58ded4a6de7ce3ec299e9580d07d4bddebe626ef3069c5806a0e060f63ce419f7a5dc48b6967cc9a - languageName: node - linkType: hard - "@aws-sdk/property-provider@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/property-provider@npm:3.78.0" @@ -2385,16 +1661,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/protocol-http@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/protocol-http@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/8d6cfcbe620256480198f0782a1f7a51f954ad076593ec09d01ffb23bf7f069c7716e629221b9793d2ab2903268d21597e4e386d9aacdd3d8ea2bdaa8a657d16 - languageName: node - linkType: hard - "@aws-sdk/protocol-http@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/protocol-http@npm:3.78.0" @@ -2416,17 +1682,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/querystring-builder@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/querystring-builder@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-uri-escape": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/4408739767ae6ef17c00d8df3b3936ba8a49737af34d1bd3e67e0a855b6fece11a75b539e7955ab6404688176f1ff6d6625528eae90518bf91e78bb9ce3f7ee7 - languageName: node - linkType: hard - "@aws-sdk/querystring-builder@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/querystring-builder@npm:3.78.0" @@ -2448,16 +1703,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/querystring-parser@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/querystring-parser@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/a975024dd2df3391c19ea468fa826fdb2c1dca2947993978a1e461db60f232c689da4a413d1205cbcac53c64f8cea9fbcfe1f10459bd2cc3cc86c9c95ae75d98 - languageName: node - linkType: hard - "@aws-sdk/querystring-parser@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/querystring-parser@npm:3.78.0" @@ -2489,13 +1734,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/service-error-classification@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/service-error-classification@npm:3.47.2" - checksum: 10/f7be80e30f7fc5f980a58e73c9a54e93217de05c9146eed7649bff6f28d4568da4aeeed927801512ddcc55664756d7caab2f62c9dd814185188c0b761611c7bf - languageName: node - linkType: hard - "@aws-sdk/service-error-classification@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/service-error-classification@npm:3.78.0" @@ -2512,15 +1750,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/shared-ini-file-loader@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/shared-ini-file-loader@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/1068855025162e82bcef8eeaeea7b2f2e3ec5d488c7ea361616f8f40447c0ac34ad27ddbe89adfb9dcb20e6ef8986dc5a42632636a9e541e25247d135dc42548 - languageName: node - linkType: hard - "@aws-sdk/shared-ini-file-loader@npm:3.80.0": version: 3.80.0 resolution: "@aws-sdk/shared-ini-file-loader@npm:3.80.0" @@ -2544,24 +1773,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.88.0": - version: 3.88.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.88.0" - dependencies: - "@aws-sdk/protocol-http": "npm:3.78.0" - "@aws-sdk/signature-v4": "npm:3.78.0" - "@aws-sdk/types": "npm:3.78.0" - "@aws-sdk/util-arn-parser": "npm:3.55.0" - tslib: "npm:^2.3.1" - peerDependencies: - "@aws-sdk/signature-v4-crt": ^3.79.0 - peerDependenciesMeta: - "@aws-sdk/signature-v4-crt": - optional: true - checksum: 10/2382e05d517f18a4229ceef89e975e84eb94630afd9a11793deb70b5ea26446f50311caae586b38016ee5e1bb6d17b316774b93263be6c74994b2704ea03347b - languageName: node - linkType: hard - "@aws-sdk/signature-v4@npm:3.130.0": version: 3.130.0 resolution: "@aws-sdk/signature-v4@npm:3.130.0" @@ -2576,19 +1787,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/signature-v4@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/signature-v4@npm:3.47.2" - dependencies: - "@aws-sdk/is-array-buffer": "npm:3.47.1" - "@aws-sdk/types": "npm:3.47.1" - "@aws-sdk/util-hex-encoding": "npm:3.47.1" - "@aws-sdk/util-uri-escape": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/281e618b58c2ec5e508cabafa965d3f66ce6f310ac612f39458a4aa2622a8ab978585a24effa0c2eb70e8aaf1d5ac47658878bd6fbac363e515727c5e766909c - languageName: node - linkType: hard - "@aws-sdk/signature-v4@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/signature-v4@npm:3.78.0" @@ -2614,17 +1812,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/smithy-client@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/smithy-client@npm:3.47.2" - dependencies: - "@aws-sdk/middleware-stack": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/b3a3bf3182542d93bbd4badcce4a5656dbbfb42f675c213441bdfc8de27df13ca802aa5940fcad6158e77372cdffe205993d2aaf802d55427b71197b72330a8e - languageName: node - linkType: hard - "@aws-sdk/smithy-client@npm:3.99.0": version: 3.99.0 resolution: "@aws-sdk/smithy-client@npm:3.99.0" @@ -2658,13 +1845,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/types@npm:3.47.1" - checksum: 10/6a799ce69fe87aa52c931ba86337a1b280ddfddbd59abebf4222113e2e2757c64f18dbcbc680ec88a1a14ce7b3a84a60fda3f6a55c6d0ab0665453720e976874 - languageName: node - linkType: hard - "@aws-sdk/types@npm:3.577.0, @aws-sdk/types@npm:^3.222.0": version: 3.577.0 resolution: "@aws-sdk/types@npm:3.577.0" @@ -2693,17 +1873,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/url-parser@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/url-parser@npm:3.47.2" - dependencies: - "@aws-sdk/querystring-parser": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/c286c19a2768b529dcf8477aa4c97c64f1e756211ef402707adc393b45bfebcc44e6c660acb11e722e12975a86e6b43e6c006a65174828284fdc3f26c2c7e33c - languageName: node - linkType: hard - "@aws-sdk/url-parser@npm:3.78.0": version: 3.78.0 resolution: "@aws-sdk/url-parser@npm:3.78.0" @@ -2715,15 +1884,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-arn-parser@npm:3.55.0": - version: 3.55.0 - resolution: "@aws-sdk/util-arn-parser@npm:3.55.0" - dependencies: - tslib: "npm:^2.3.1" - checksum: 10/d8e56df636218c247ba3c6d1bc5c96ad853cdd5660f74bd0c43bcde4ddc1d6d6f2ee975f78772d99371d3d7315730cf03da73d47e90f32fdfc54359ee2e54915 - languageName: node - linkType: hard - "@aws-sdk/util-arn-parser@npm:3.568.0": version: 3.568.0 resolution: "@aws-sdk/util-arn-parser@npm:3.568.0" @@ -2742,15 +1902,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-base64-browser@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/util-base64-browser@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/f79516ee53849cd7995a3372a25c5309456c719aa2f6444fe68807ee82dc0b4aba7990f30d4a1f1a504501201f67ab5b28826115c664930f33cb6856ca423194 - languageName: node - linkType: hard - "@aws-sdk/util-base64-browser@npm:3.58.0": version: 3.58.0 resolution: "@aws-sdk/util-base64-browser@npm:3.58.0" @@ -2760,16 +1911,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-base64-node@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/util-base64-node@npm:3.47.2" - dependencies: - "@aws-sdk/util-buffer-from": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/abcea31871d4666fb59fc49432ce5c57ebe8f732b01e5fe6270df4a8926d794a7fb88282011543d96938686899bedffb9373bb2e0412460ff3dbed4b03007406 - languageName: node - linkType: hard - "@aws-sdk/util-base64-node@npm:3.55.0": version: 3.55.0 resolution: "@aws-sdk/util-base64-node@npm:3.55.0" @@ -2780,15 +1921,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-body-length-browser@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/util-body-length-browser@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/273caa8cba716ed61235695554d59c940224035a53b5188891bb09d0bdbf13bd9e40603040a682c77b47e3e30764aed8649c49929bfcbc024a2b4404e2bb5a0d - languageName: node - linkType: hard - "@aws-sdk/util-body-length-browser@npm:3.55.0": version: 3.55.0 resolution: "@aws-sdk/util-body-length-browser@npm:3.55.0" @@ -2798,15 +1930,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-body-length-node@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/util-body-length-node@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/1e226859ea45fbb2cc5f4ac694fc8b4b0f531ff7df64621f1b43c7412c44a85e9d5e8fdd0de48d73d10cbb35c8aece52e6ad5e0858c886e66c22315f075c2811 - languageName: node - linkType: hard - "@aws-sdk/util-body-length-node@npm:3.55.0": version: 3.55.0 resolution: "@aws-sdk/util-body-length-node@npm:3.55.0" @@ -2816,16 +1939,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-buffer-from@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/util-buffer-from@npm:3.47.2" - dependencies: - "@aws-sdk/is-array-buffer": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/bacddc53a02b1e601c34601685d2bc829856f257f531d3e969a2b9ca5523244c314b214ce680ac7f8d411e58ed4800dd1a08eb18ccd697561f153d766416692a - languageName: node - linkType: hard - "@aws-sdk/util-buffer-from@npm:3.55.0": version: 3.55.0 resolution: "@aws-sdk/util-buffer-from@npm:3.55.0" @@ -2845,15 +1958,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-config-provider@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/util-config-provider@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/747ab73fd87dc99dadacad255d36a1766d4eb15cf2a553050d46bff48fb66e906d099bf1594049fe79ba1353fa51d5225af2a67703ec2ec98759beae981a55b7 - languageName: node - linkType: hard - "@aws-sdk/util-config-provider@npm:3.55.0": version: 3.55.0 resolution: "@aws-sdk/util-config-provider@npm:3.55.0" @@ -2863,16 +1967,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-credentials@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/util-credentials@npm:3.47.2" - dependencies: - "@aws-sdk/shared-ini-file-loader": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/801cd30e796e8b09873beab22cc02ca941b3eb93e0f8241bf473bc26ddd3bd87cdfe7f289e9846fa68a5dad0032cf2bca011cce63eecc3a635ef0aa0c2117f73 - languageName: node - linkType: hard - "@aws-sdk/util-defaults-mode-browser@npm:3.142.0": version: 3.142.0 resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.142.0" @@ -2885,18 +1979,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-defaults-mode-browser@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.47.2" - dependencies: - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - bowser: "npm:^2.11.0" - tslib: "npm:^2.3.0" - checksum: 10/d987d132b6e0efb8d5b27f72dfa7ea5f642b2f6f9a13bec6bceb4d37c5345f04ce9e5ef296e83fc4f68d4f2d5bd08f656fe28390a57a0b86a87f7e3de909d6cd - languageName: node - linkType: hard - "@aws-sdk/util-defaults-mode-browser@npm:3.99.0": version: 3.99.0 resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.99.0" @@ -2923,20 +2005,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-defaults-mode-node@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/util-defaults-mode-node@npm:3.47.2" - dependencies: - "@aws-sdk/config-resolver": "npm:3.47.2" - "@aws-sdk/credential-provider-imds": "npm:3.47.2" - "@aws-sdk/node-config-provider": "npm:3.47.2" - "@aws-sdk/property-provider": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/dfb2892338a3b0f2b58ab7569fc6271eaae5b0bf2889ca87fc6f1deffe59813c70e8874a8a8ec45882cbf97d5e56536b157022249bcc951f7f29a098a1623412 - languageName: node - linkType: hard - "@aws-sdk/util-defaults-mode-node@npm:3.99.0": version: 3.99.0 resolution: "@aws-sdk/util-defaults-mode-node@npm:3.99.0" @@ -2972,15 +2040,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-hex-encoding@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/util-hex-encoding@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/9a76516009af3f32eecbf69062de7178ba61dac9f4a9203337712cd2a31a723f74bf452d864cb8d3b1351d548acb9ae12bfd43429df53e354d4993c393d4f0ee - languageName: node - linkType: hard - "@aws-sdk/util-hex-encoding@npm:3.58.0": version: 3.58.0 resolution: "@aws-sdk/util-hex-encoding@npm:3.58.0" @@ -3017,35 +2076,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-stream-browser@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/util-stream-browser@npm:3.78.0" - dependencies: - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/b7ffa0c49561c816625a1d147b3958c9320f90469cb2bbbc475a99c7ec6bc009e4a8a05032d55e91d4d51a54743ab6eeb96ee91b867985bc688364340bbe2550 - languageName: node - linkType: hard - -"@aws-sdk/util-stream-node@npm:3.78.0": - version: 3.78.0 - resolution: "@aws-sdk/util-stream-node@npm:3.78.0" - dependencies: - "@aws-sdk/types": "npm:3.78.0" - tslib: "npm:^2.3.1" - checksum: 10/2054d281919c54a1b85ce04de3d8e4c0a6273ee88f7d3073cfaefe0fd1d815203eebefe17f550d5a7e4884f66c03faa6c60ac94ca34d1eaa274590f562d20b28 - languageName: node - linkType: hard - -"@aws-sdk/util-uri-escape@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/util-uri-escape@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/68fc6e1909d884c2ef7d019428c003f61fc70971e020aa1656a12012a6e8e0a04df844a0b16ee20b757b3940c40ddeb27f8b35739bfef23b9629f74cf195e0e3 - languageName: node - linkType: hard - "@aws-sdk/util-uri-escape@npm:3.55.0": version: 3.55.0 resolution: "@aws-sdk/util-uri-escape@npm:3.55.0" @@ -3066,17 +2096,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.47.2" - dependencies: - "@aws-sdk/types": "npm:3.47.1" - bowser: "npm:^2.11.0" - tslib: "npm:^2.3.0" - checksum: 10/d174e3cc9f1ccc771d816ff0d509673252f2950a097ed0752a1d323834b8bcc2e4598d4bd18b1f9bc07dcc7648e9c4e023461bace2dc55469babf09700baa421 - languageName: node - linkType: hard - "@aws-sdk/util-user-agent-browser@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/util-user-agent-browser@npm:3.577.0" @@ -3116,17 +2135,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/util-user-agent-node@npm:3.47.2" - dependencies: - "@aws-sdk/node-config-provider": "npm:3.47.2" - "@aws-sdk/types": "npm:3.47.1" - tslib: "npm:^2.3.0" - checksum: 10/3fdaf3e174ead8b08029b7c30c9802a27b6a61b02581f11bb2c0fa3b66b9eafd3739c9bdeb6df554473e3a75d680553f1a98adb48d035b858c20b379341d5bfc - languageName: node - linkType: hard - "@aws-sdk/util-user-agent-node@npm:3.577.0": version: 3.577.0 resolution: "@aws-sdk/util-user-agent-node@npm:3.577.0" @@ -3164,15 +2172,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-utf8-browser@npm:3.47.1": - version: 3.47.1 - resolution: "@aws-sdk/util-utf8-browser@npm:3.47.1" - dependencies: - tslib: "npm:^2.3.0" - checksum: 10/c6a7eeef1ddf9a08621c08036b2f44fe6dd700feb4beb71e150c3060c493c907df6b745663bc18d53ce03483c54fae55532b479ba34440bbfb8dcb385013bbd1 - languageName: node - linkType: hard - "@aws-sdk/util-utf8-browser@npm:3.55.0, @aws-sdk/util-utf8-browser@npm:^3.0.0": version: 3.55.0 resolution: "@aws-sdk/util-utf8-browser@npm:3.55.0" @@ -3192,16 +2191,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-utf8-node@npm:3.47.2": - version: 3.47.2 - resolution: "@aws-sdk/util-utf8-node@npm:3.47.2" - dependencies: - "@aws-sdk/util-buffer-from": "npm:3.47.2" - tslib: "npm:^2.3.0" - checksum: 10/eaf0696d0430e7c4084a08adf305e1ff9840bb1a6c30df5687b822802d550f82d4ade355d6d62a1955f397f094ee582c3e37e0a5f0bf37a2cab95e9e02d7c970 - languageName: node - linkType: hard - "@aws-sdk/util-utf8-node@npm:3.55.0": version: 3.55.0 resolution: "@aws-sdk/util-utf8-node@npm:3.55.0" @@ -3223,15 +2212,6 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/xml-builder@npm:3.55.0": - version: 3.55.0 - resolution: "@aws-sdk/xml-builder@npm:3.55.0" - dependencies: - tslib: "npm:^2.3.1" - checksum: 10/0924d5725921c1e6fa6685a9dba135747f977457dc9d957e35e7b92f4bccfc80b9a57d0d044179dcd73be4029fb0c3826571595dcaaf7da30016641e9e81f3d4 - languageName: node - linkType: hard - "@aws-sdk/xml-builder@npm:3.575.0": version: 3.575.0 resolution: "@aws-sdk/xml-builder@npm:3.575.0" @@ -3281,6 +2261,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.25.9": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.25.9" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/db2c2122af79d31ca916755331bb4bac96feb2b334cdaca5097a6b467fdd41963b89b14b6836a14f083de7ff887fc78fa1b3c10b14e743d33e12dbfe5ee3d223 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.24.8, @babel/compat-data@npm:^7.25.0": version: 7.25.0 resolution: "@babel/compat-data@npm:7.25.0" @@ -3388,6 +2379,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.25.9": + version: 7.26.2 + resolution: "@babel/generator@npm:7.26.2" + dependencies: + "@babel/parser": "npm:^7.26.2" + "@babel/types": "npm:^7.26.0" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^3.0.2" + checksum: 10/71ace82b5b07a554846a003624bfab93275ccf73cdb9f1a37a4c1094bf9dc94bb677c67e8b8c939dbd6c5f0eda2e8f268aa2b0d9c3b9511072565660e717e045 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-annotate-as-pure@npm:7.24.7" @@ -3514,6 +2518,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.16.7": + version: 7.25.9 + resolution: "@babel/helper-module-imports@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e090be5dee94dda6cd769972231b21ddfae988acd76b703a480ac0c96f3334557d70a965bf41245d6ee43891e7571a8b400ccf2b2be5803351375d0f4e5bcf08 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-module-imports@npm:7.22.15" @@ -3670,6 +2684,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 10/c28656c52bd48e8c1d9f3e8e68ecafd09d949c57755b0d353739eb4eae7ba4f7e67e92e4036f1cd43378cc1397a2c943ed7bcaf5949b04ab48607def0258b775 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.16.7": version: 7.16.7 resolution: "@babel/helper-validator-identifier@npm:7.16.7" @@ -3698,6 +2719,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 10/3f9b649be0c2fd457fa1957b694b4e69532a668866b8a0d81eabfa34ba16dbf3107b39e0e7144c55c3c652bf773ec816af8df4a61273a2bb4eb3145ca9cf478e + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.23.5": version: 7.23.5 resolution: "@babel/helper-validator-option@npm:7.23.5" @@ -3834,6 +2862,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.2": + version: 7.26.2 + resolution: "@babel/parser@npm:7.26.2" + dependencies: + "@babel/types": "npm:^7.26.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/8baee43752a3678ad9f9e360ec845065eeee806f1fdc8e0f348a8a0e13eef0959dabed4a197c978896c493ea205c804d0a1187cc52e4a1ba017c7935bab4983d + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.0": version: 7.25.0 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.0" @@ -4961,6 +4000,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.25.0": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 + languageName: node + linkType: hard + "@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": version: 7.25.0 resolution: "@babel/runtime@npm:7.25.0" @@ -4997,15 +4045,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.25.0": - version: 7.26.0 - resolution: "@babel/runtime@npm:7.26.0" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 - languageName: node - linkType: hard - "@babel/template@npm:^7.22.15": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" @@ -5039,6 +4078,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e861180881507210150c1335ad94aff80fd9e9be6202e1efa752059c93224e2d5310186ddcdd4c0f0b0fc658ce48cb47823f15142b5c00c8456dde54f5de80b2 + languageName: node + linkType: hard + "@babel/traverse@npm:7.23.2": version: 7.23.2 resolution: "@babel/traverse@npm:7.23.2" @@ -5090,6 +4140,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/traverse@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/generator": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/template": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/7431614d76d4a053e429208db82f2846a415833f3d9eb2e11ef72eeb3c64dfd71f4a4d983de1a4a047b36165a1f5a64de8ca2a417534cc472005c740ffcb9c6a + languageName: node + linkType: hard + "@babel/types@npm:7.17.0": version: 7.17.0 resolution: "@babel/types@npm:7.17.0" @@ -5165,6 +4230,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e + languageName: node + linkType: hard + "@base2/pretty-print-object@npm:1.0.1": version: 1.0.1 resolution: "@base2/pretty-print-object@npm:1.0.1" @@ -5179,6 +4254,62 @@ __metadata: languageName: node linkType: hard +"@chain-registry/client@npm:^1.49.11": + version: 1.53.13 + resolution: "@chain-registry/client@npm:1.53.13" + dependencies: + "@chain-registry/types": "npm:^0.50.13" + "@chain-registry/utils": "npm:^1.51.13" + bfs-path: "npm:^1.0.2" + cross-fetch: "npm:^3.1.5" + checksum: 10/d80b71ad6c607a2577cc4cc96213de192f7d004c7901c52f50d6a2f79570ff5b1bd477d409f7d6ba88aca5c8f555911414805e4dc81a1a58ec3f0b41f79530ea + languageName: node + linkType: hard + +"@chain-registry/keplr@npm:^1.69.13": + version: 1.74.32 + resolution: "@chain-registry/keplr@npm:1.74.32" + dependencies: + "@chain-registry/types": "npm:^0.50.13" + "@keplr-wallet/cosmos": "npm:0.12.28" + "@keplr-wallet/crypto": "npm:0.12.28" + semver: "npm:^7.5.0" + checksum: 10/df93c4253c75ab52b682a166921e826030836bf77a434903472c60290fc7ee477841173931a2d804ec68671014d97bae8c919a32ade12990b6638f282c1644cd + languageName: node + linkType: hard + +"@chain-registry/types@npm:^0.46.11": + version: 0.46.15 + resolution: "@chain-registry/types@npm:0.46.15" + checksum: 10/de63ace15b36ec3f06a401483d8ad8822c794658df3541cff00aa54756499ea5e881f8709b0a7876ae2a0b8b05848e87e41e8baf0fdb46e12e8c68cacbccb5f3 + languageName: node + linkType: hard + +"@chain-registry/types@npm:^0.50.13": + version: 0.50.13 + resolution: "@chain-registry/types@npm:0.50.13" + checksum: 10/de71473b2de9fa2a3deff734496ef3c5839345355bd816f1d87c71a379e01b5740ba286b0203310fe803e69f25349447d11b6c088d80022973d7da7b5145784b + languageName: node + linkType: hard + +"@chain-registry/types@npm:^0.50.14": + version: 0.50.14 + resolution: "@chain-registry/types@npm:0.50.14" + checksum: 10/4a1e6978de66d50f98a804174fb8bfa712e42284473edc9e85e6c308dac3fcf381d1888248579128e7a78cec7539378bf7a149b20654741daa85b1c88b8beb8c + languageName: node + linkType: hard + +"@chain-registry/utils@npm:^1.51.13": + version: 1.51.13 + resolution: "@chain-registry/utils@npm:1.51.13" + dependencies: + "@chain-registry/types": "npm:^0.50.13" + bignumber.js: "npm:9.1.2" + sha.js: "npm:^2.4.11" + checksum: 10/d8b1bf249ae13f794a70bd12d2e916394f44d1d949dedf72c4e7532c08bc0da137dd87abd8f2d78562adf5c8c68fa37ccf0a00eed2ee35367d9975721b9b3a93 + languageName: node + linkType: hard + "@chainlink/ccip-read-server@npm:^0.2.1": version: 0.2.1 resolution: "@chainlink/ccip-read-server@npm:0.2.1" @@ -5236,6 +4367,99 @@ __metadata: languageName: node linkType: hard +"@chakra-ui/anatomy@npm:2.3.5": + version: 2.3.5 + resolution: "@chakra-ui/anatomy@npm:2.3.5" + checksum: 10/14b56dfffb76730ac94760443811f952bb2373939ad7539d56b313505733e840eea8cee45c95f96805cb0ed80bbde95d98227b155a02d86d3c1984747b4466ab + languageName: node + linkType: hard + +"@chakra-ui/hooks@npm:2.4.3": + version: 2.4.3 + resolution: "@chakra-ui/hooks@npm:2.4.3" + dependencies: + "@chakra-ui/utils": "npm:2.2.3" + "@zag-js/element-size": "npm:0.31.1" + copy-to-clipboard: "npm:3.3.3" + framesync: "npm:6.1.2" + peerDependencies: + react: ">=18" + checksum: 10/cf740474d5deba1286df8e61acadd1558e97900c6ca162bd0d0354edebfb38a311c0182c35ed95592d2b5ca7af0781dffb354d2048e5268ed3ae5efb61929e39 + languageName: node + linkType: hard + +"@chakra-ui/react@npm:^2.8.2": + version: 2.10.4 + resolution: "@chakra-ui/react@npm:2.10.4" + dependencies: + "@chakra-ui/hooks": "npm:2.4.3" + "@chakra-ui/styled-system": "npm:2.12.1" + "@chakra-ui/theme": "npm:3.4.7" + "@chakra-ui/utils": "npm:2.2.3" + "@popperjs/core": "npm:^2.11.8" + "@zag-js/focus-visible": "npm:^0.31.1" + aria-hidden: "npm:^1.2.3" + react-fast-compare: "npm:3.2.2" + react-focus-lock: "npm:^2.9.6" + react-remove-scroll: "npm:^2.5.7" + peerDependencies: + "@emotion/react": ">=11" + "@emotion/styled": ">=11" + framer-motion: ">=4.0.0" + react: ">=18" + react-dom: ">=18" + checksum: 10/d56ce89499c4c3b9563e7ce3d6fa981201b5e0bc0d7265d995b7ffccf71cbac0d308b304c9d42a220dba07ee4cac6243d276cf1258370208e71c49d2fcda9310 + languageName: node + linkType: hard + +"@chakra-ui/styled-system@npm:2.12.1": + version: 2.12.1 + resolution: "@chakra-ui/styled-system@npm:2.12.1" + dependencies: + "@chakra-ui/utils": "npm:2.2.3" + csstype: "npm:^3.1.2" + checksum: 10/2abddc5fa5cd6c2d8de555f4fb7ffeda1c3ffd65d4649286af2a58a5cc9560fe3c3f9acab8a1cdc58e16f35deaf5719fe3c9ff3f26dc45a4e5e5c9655085e549 + languageName: node + linkType: hard + +"@chakra-ui/theme-tools@npm:2.2.7": + version: 2.2.7 + resolution: "@chakra-ui/theme-tools@npm:2.2.7" + dependencies: + "@chakra-ui/anatomy": "npm:2.3.5" + "@chakra-ui/utils": "npm:2.2.3" + color2k: "npm:^2.0.2" + peerDependencies: + "@chakra-ui/styled-system": ">=2.0.0" + checksum: 10/0ab73ffe6eac1c7211f8661e323c99c844f1a0e466d39351898a3f08bb0436dd820726a989c3016098bf4c799c99b50cd82c13c6dee27cd4c59a8b78cd911a43 + languageName: node + linkType: hard + +"@chakra-ui/theme@npm:3.4.7": + version: 3.4.7 + resolution: "@chakra-ui/theme@npm:3.4.7" + dependencies: + "@chakra-ui/anatomy": "npm:2.3.5" + "@chakra-ui/theme-tools": "npm:2.2.7" + "@chakra-ui/utils": "npm:2.2.3" + peerDependencies: + "@chakra-ui/styled-system": ">=2.8.0" + checksum: 10/d15a7fe94c4fd8984a6167e67f41e72201bbc860171ddd7d8553c8db4e1327ae1c763ec505b80936b758b9ac524ee5c10905b59506f16b38f237e8966e310b70 + languageName: node + linkType: hard + +"@chakra-ui/utils@npm:2.2.3": + version: 2.2.3 + resolution: "@chakra-ui/utils@npm:2.2.3" + dependencies: + "@types/lodash.mergewith": "npm:4.6.9" + lodash.mergewith: "npm:4.6.2" + peerDependencies: + react: ">=16.8.0" + checksum: 10/0c22261c77b565e9bc44aa6891cc30324b05539de73061d2218c9e8859600eabca930217b11a1b614816d0bef04cdeab0f43d6060607670c5490fc9e8c166298 + languageName: node + linkType: hard + "@changesets/apply-release-plan@npm:^6.1.4": version: 6.1.4 resolution: "@changesets/apply-release-plan@npm:6.1.4" @@ -5550,6 +4774,18 @@ __metadata: languageName: node linkType: hard +"@coinbase/wallet-sdk@npm:4.2.3": + version: 4.2.3 + resolution: "@coinbase/wallet-sdk@npm:4.2.3" + dependencies: + "@noble/hashes": "npm:^1.4.0" + clsx: "npm:^1.2.1" + eventemitter3: "npm:^5.0.1" + preact: "npm:^10.24.2" + checksum: 10/dd16ae6d5f7f81b38dfcd95e7538adbe920380bb52206f494f1fafaa38906f2f97265f3eb3f5fe2e9bfab19618447cc19581935586c113d51945c8bab62c7f9a + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -5567,7 +4803,7 @@ __metadata: languageName: node linkType: hard -"@cosmjs/amino@npm:^0.32.4": +"@cosmjs/amino@npm:^0.32.3, @cosmjs/amino@npm:^0.32.4": version: 0.32.4 resolution: "@cosmjs/amino@npm:0.32.4" dependencies: @@ -5579,7 +4815,7 @@ __metadata: languageName: node linkType: hard -"@cosmjs/cosmwasm-stargate@npm:^0.32.4": +"@cosmjs/cosmwasm-stargate@npm:^0.32.3, @cosmjs/cosmwasm-stargate@npm:^0.32.4": version: 0.32.4 resolution: "@cosmjs/cosmwasm-stargate@npm:0.32.4" dependencies: @@ -5642,7 +4878,7 @@ __metadata: languageName: node linkType: hard -"@cosmjs/proto-signing@npm:^0.32.4": +"@cosmjs/proto-signing@npm:^0.32.3, @cosmjs/proto-signing@npm:^0.32.4": version: 0.32.4 resolution: "@cosmjs/proto-signing@npm:0.32.4" dependencies: @@ -5668,7 +4904,7 @@ __metadata: languageName: node linkType: hard -"@cosmjs/stargate@npm:^0.32.4": +"@cosmjs/stargate@npm:^0.32.3, @cosmjs/stargate@npm:^0.32.4": version: 0.32.4 resolution: "@cosmjs/stargate@npm:0.32.4" dependencies: @@ -5720,6 +4956,62 @@ __metadata: languageName: node linkType: hard +"@cosmos-kit/core@npm:^2.15.0": + version: 2.15.0 + resolution: "@cosmos-kit/core@npm:2.15.0" + dependencies: + "@chain-registry/client": "npm:^1.49.11" + "@chain-registry/keplr": "npm:^1.69.13" + "@chain-registry/types": "npm:^0.46.11" + "@cosmjs/amino": "npm:^0.32.3" + "@cosmjs/cosmwasm-stargate": "npm:^0.32.3" + "@cosmjs/proto-signing": "npm:^0.32.3" + "@cosmjs/stargate": "npm:^0.32.3" + "@dao-dao/cosmiframe": "npm:^0.1.0" + "@walletconnect/types": "npm:2.11.0" + bowser: "npm:2.11.0" + cosmjs-types: "npm:^0.9.0" + events: "npm:3.3.0" + nock: "npm:13.5.4" + uuid: "npm:^9.0.1" + checksum: 10/fe13203a71390cbbcb014454ab28c9440281fdeedb23f8bca0984c0b756c4049d0716e104be7eeae809726ebf55bb1a67fc92cd3159e19b9d864177e0bf83215 + languageName: node + linkType: hard + +"@cosmos-kit/react-lite@npm:^2.15.1": + version: 2.15.1 + resolution: "@cosmos-kit/react-lite@npm:2.15.1" + dependencies: + "@chain-registry/types": "npm:^0.46.11" + "@cosmos-kit/core": "npm:^2.15.0" + "@dao-dao/cosmiframe": "npm:^0.1.0" + peerDependencies: + "@types/react": ">= 17" + "@types/react-dom": ">= 17" + react: ^18 + react-dom: ^18 + checksum: 10/3b3c3b7f9fea92c65d1fbb5585d397b09a06fb0a91089a6e4960259fa43d96fdf85ad4d285512df33373ee35fb85e487dace701700afb846960e505e795d80cb + languageName: node + linkType: hard + +"@cosmos-kit/react@npm:^2.18.0": + version: 2.20.1 + resolution: "@cosmos-kit/react@npm:2.20.1" + dependencies: + "@chain-registry/types": "npm:^0.46.11" + "@cosmos-kit/core": "npm:^2.15.0" + "@cosmos-kit/react-lite": "npm:^2.15.1" + "@react-icons/all-files": "npm:^4.1.0" + peerDependencies: + "@interchain-ui/react": ^1.23.9 + "@types/react": ">= 17" + "@types/react-dom": ">= 17" + react: ^18 + react-dom: ^18 + checksum: 10/dc66aedf2cf485262cac805b128f49cf8466d9e16916c242684e47468b5b9f7dec94d396926ae4be808532446864de48987311578d4fd1343d4abdf4876205d2 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:0.8.1, @cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -5729,6 +5021,18 @@ __metadata: languageName: node linkType: hard +"@dao-dao/cosmiframe@npm:^0.1.0": + version: 0.1.0 + resolution: "@dao-dao/cosmiframe@npm:0.1.0" + dependencies: + uuid: "npm:^9.0.1" + peerDependencies: + "@cosmjs/amino": "*" + "@cosmjs/proto-signing": "*" + checksum: 10/7a53a9047b3deecf1ad9b9aa80467d7d9e91f7187bfdd97b8fde77cdfaf4e828f9c9a85cacfb8f13ebe9ad30f578d1e652c7afc4dcd13b30f04440ff50c3a1d0 + languageName: node + linkType: hard + "@discoveryjs/json-ext@npm:^0.5.3": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" @@ -5736,7 +5040,166 @@ __metadata: languageName: node linkType: hard -"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.0": +"@ecies/ciphers@npm:^0.2.1": + version: 0.2.1 + resolution: "@ecies/ciphers@npm:0.2.1" + peerDependencies: + "@noble/ciphers": ^1.0.0 + checksum: 10/4a2012358f79ef842c6a9fdcf3d4e1f7d3d59ad3d025cca52b3e7135f62d5c35d394882cbfe8ad5aa17f707663921bf466707d20712b5027a0af5813a6ad7b08 + languageName: node + linkType: hard + +"@effect/schema@npm:0.71.1": + version: 0.71.1 + resolution: "@effect/schema@npm:0.71.1" + dependencies: + fast-check: "npm:^3.21.0" + peerDependencies: + effect: ^3.6.5 + checksum: 10/d8ef78980409ce6fbe3de5ccba976cdcce86014916613a12dc9826ee27ad64e8f94579718a3099c6a9fc460aaafd471f573e75f74dfeaafb89b98275b4d2ed70 + languageName: node + linkType: hard + +"@emotion/babel-plugin@npm:^11.12.0": + version: 11.12.0 + resolution: "@emotion/babel-plugin@npm:11.12.0" + dependencies: + "@babel/helper-module-imports": "npm:^7.16.7" + "@babel/runtime": "npm:^7.18.3" + "@emotion/hash": "npm:^0.9.2" + "@emotion/memoize": "npm:^0.9.0" + "@emotion/serialize": "npm:^1.2.0" + babel-plugin-macros: "npm:^3.1.0" + convert-source-map: "npm:^1.5.0" + escape-string-regexp: "npm:^4.0.0" + find-root: "npm:^1.1.0" + source-map: "npm:^0.5.7" + stylis: "npm:4.2.0" + checksum: 10/fe6f4522ea2b61ef4214dd0b0f3778aad9c18434b47e50ae5091af226526bf305455c313065826a090682520c9462c151d4df62ec128f14671d3125afc05b148 + languageName: node + linkType: hard + +"@emotion/cache@npm:^11.13.0": + version: 11.13.1 + resolution: "@emotion/cache@npm:11.13.1" + dependencies: + "@emotion/memoize": "npm:^0.9.0" + "@emotion/sheet": "npm:^1.4.0" + "@emotion/utils": "npm:^1.4.0" + "@emotion/weak-memoize": "npm:^0.4.0" + stylis: "npm:4.2.0" + checksum: 10/090c8ad2e5b23f1b3a95e94f1f0554a40ed1dcd844c9d31629a68ff824eff40f32d1362f67aefa440ee0aabd5a8cabcc76870fd6d77144d3ff251bdcdf1420b9 + languageName: node + linkType: hard + +"@emotion/hash@npm:^0.9.0, @emotion/hash@npm:^0.9.2": + version: 0.9.2 + resolution: "@emotion/hash@npm:0.9.2" + checksum: 10/379bde2830ccb0328c2617ec009642321c0e009a46aa383dfbe75b679c6aea977ca698c832d225a893901f29d7b3eef0e38cf341f560f6b2b56f1ff23c172387 + languageName: node + linkType: hard + +"@emotion/is-prop-valid@npm:^0.8.2": + version: 0.8.8 + resolution: "@emotion/is-prop-valid@npm:0.8.8" + dependencies: + "@emotion/memoize": "npm:0.7.4" + checksum: 10/e85bdeb9d9d23de422f271e0f5311a0142b15055bb7e610440dbf250f0cdfd049df88af72a49e2c6081954481f1cbeca9172e2116ff536b38229397dfbed8082 + languageName: node + linkType: hard + +"@emotion/is-prop-valid@npm:^1.3.0": + version: 1.3.1 + resolution: "@emotion/is-prop-valid@npm:1.3.1" + dependencies: + "@emotion/memoize": "npm:^0.9.0" + checksum: 10/abbc5c7bf4017415da5b06067fc0b4771d1f22cf94ec37fd54c07b3bd1bcffbda2405ca686e7ee64a9cfc51461262b712f724850e838775347a949f72949ad03 + languageName: node + linkType: hard + +"@emotion/memoize@npm:0.7.4": + version: 0.7.4 + resolution: "@emotion/memoize@npm:0.7.4" + checksum: 10/4e3920d4ec95995657a37beb43d3f4b7d89fed6caa2b173a4c04d10482d089d5c3ea50bbc96618d918b020f26ed6e9c4026bbd45433566576c1f7b056c3271dc + languageName: node + linkType: hard + +"@emotion/memoize@npm:^0.9.0": + version: 0.9.0 + resolution: "@emotion/memoize@npm:0.9.0" + checksum: 10/038132359397348e378c593a773b1148cd0cf0a2285ffd067a0f63447b945f5278860d9de718f906a74c7c940ba1783ac2ca18f1c06a307b01cc0e3944e783b1 + languageName: node + linkType: hard + +"@emotion/react@npm:^11.13.3": + version: 11.13.3 + resolution: "@emotion/react@npm:11.13.3" + dependencies: + "@babel/runtime": "npm:^7.18.3" + "@emotion/babel-plugin": "npm:^11.12.0" + "@emotion/cache": "npm:^11.13.0" + "@emotion/serialize": "npm:^1.3.1" + "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.1.0" + "@emotion/utils": "npm:^1.4.0" + "@emotion/weak-memoize": "npm:^0.4.0" + hoist-non-react-statics: "npm:^3.3.1" + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/ee70d3afc2e8dd771e6fe176d27dd87a5e21a54e54d871438fd1caa5aa2312d848c6866292fdc65a6ea1c945147c8422bda2d22ed739178af9902dc86d6b298a + languageName: node + linkType: hard + +"@emotion/serialize@npm:^1.2.0, @emotion/serialize@npm:^1.3.0, @emotion/serialize@npm:^1.3.1": + version: 1.3.2 + resolution: "@emotion/serialize@npm:1.3.2" + dependencies: + "@emotion/hash": "npm:^0.9.2" + "@emotion/memoize": "npm:^0.9.0" + "@emotion/unitless": "npm:^0.10.0" + "@emotion/utils": "npm:^1.4.1" + csstype: "npm:^3.0.2" + checksum: 10/ead557c1ff19d917ef8169c02738ef36f0851fbfdf0bf69a543045bddea3b7281dc8252ee466cc5fb44ed27d1e61280ff943bb60a2c04158751fb07b3457cc93 + languageName: node + linkType: hard + +"@emotion/sheet@npm:^1.4.0": + version: 1.4.0 + resolution: "@emotion/sheet@npm:1.4.0" + checksum: 10/8ac6e9bf6b373a648f26ae7f1c24041038524f4c72f436f4f8c4761c665e58880c3229d8d89b1f7a4815dd8e5b49634d03e60187cb6f93097d7f7c1859e869d5 + languageName: node + linkType: hard + +"@emotion/styled@npm:^11.13.0": + version: 11.13.0 + resolution: "@emotion/styled@npm:11.13.0" + dependencies: + "@babel/runtime": "npm:^7.18.3" + "@emotion/babel-plugin": "npm:^11.12.0" + "@emotion/is-prop-valid": "npm:^1.3.0" + "@emotion/serialize": "npm:^1.3.0" + "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.1.0" + "@emotion/utils": "npm:^1.4.0" + peerDependencies: + "@emotion/react": ^11.0.0-rc.0 + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/5463a0f15fc12a9e20340f52df49461e948c3ae7e2dd763db0ff937b0b96dd4e82eed85cd15e24621efb3b097a095b88b01d60f50cf6f38fe3ab7db6e77f9615 + languageName: node + linkType: hard + +"@emotion/unitless@npm:^0.10.0": + version: 0.10.0 + resolution: "@emotion/unitless@npm:0.10.0" + checksum: 10/6851c16edce01c494305f43b2cad7a26b939a821131b7c354e49b8e3b012c8810024755b0f4a03ef51117750309e55339825a97bd10411fb3687e68904769106 + languageName: node + linkType: hard + +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.0, @emotion/use-insertion-effect-with-fallbacks@npm:^1.1.0": version: 1.1.0 resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.1.0" peerDependencies: @@ -5745,6 +5208,20 @@ __metadata: languageName: node linkType: hard +"@emotion/utils@npm:^1.4.0, @emotion/utils@npm:^1.4.1": + version: 1.4.1 + resolution: "@emotion/utils@npm:1.4.1" + checksum: 10/95e56fc0c9e05cf01a96268f0486ce813f1109a8653d2f575c67df9e8765d9c1b2daf09ad1ada67d933efbb08ca7990228e14b210c713daf90156b4869abe6a7 + languageName: node + linkType: hard + +"@emotion/weak-memoize@npm:^0.4.0": + version: 0.4.0 + resolution: "@emotion/weak-memoize@npm:0.4.0" + checksum: 10/db5da0e89bd752c78b6bd65a1e56231f0abebe2f71c0bd8fc47dff96408f7065b02e214080f99924f6a3bfe7ee15afc48dad999d76df86b39b16e513f7a94f52 + languageName: node + linkType: hard + "@esbuild-plugins/node-globals-polyfill@npm:^0.2.3": version: 0.2.3 resolution: "@esbuild-plugins/node-globals-polyfill@npm:0.2.3" @@ -5766,13 +5243,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/aix-ppc64@npm:0.19.12" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/aix-ppc64@npm:0.21.5" @@ -5801,13 +5271,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/android-arm64@npm:0.19.12" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm64@npm:0.21.5" @@ -5836,13 +5299,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/android-arm@npm:0.19.12" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm@npm:0.21.5" @@ -5871,13 +5327,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/android-x64@npm:0.19.12" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-x64@npm:0.21.5" @@ -5906,13 +5355,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/darwin-arm64@npm:0.19.12" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-arm64@npm:0.21.5" @@ -5941,13 +5383,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/darwin-x64@npm:0.19.12" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-x64@npm:0.21.5" @@ -5976,13 +5411,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/freebsd-arm64@npm:0.19.12" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-arm64@npm:0.21.5" @@ -6011,13 +5439,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/freebsd-x64@npm:0.19.12" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-x64@npm:0.21.5" @@ -6046,13 +5467,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-arm64@npm:0.19.12" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm64@npm:0.21.5" @@ -6081,13 +5495,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-arm@npm:0.19.12" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm@npm:0.21.5" @@ -6116,13 +5523,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-ia32@npm:0.19.12" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ia32@npm:0.21.5" @@ -6151,13 +5551,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-loong64@npm:0.19.12" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-loong64@npm:0.21.5" @@ -6186,13 +5579,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-mips64el@npm:0.19.12" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-mips64el@npm:0.21.5" @@ -6221,13 +5607,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-ppc64@npm:0.19.12" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ppc64@npm:0.21.5" @@ -6256,13 +5635,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-riscv64@npm:0.19.12" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-riscv64@npm:0.21.5" @@ -6291,13 +5663,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-s390x@npm:0.19.12" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-s390x@npm:0.21.5" @@ -6326,13 +5691,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-x64@npm:0.19.12" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-x64@npm:0.21.5" @@ -6361,13 +5719,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/netbsd-x64@npm:0.19.12" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/netbsd-x64@npm:0.21.5" @@ -6403,13 +5754,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/openbsd-x64@npm:0.19.12" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/openbsd-x64@npm:0.21.5" @@ -6438,13 +5782,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/sunos-x64@npm:0.19.12" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/sunos-x64@npm:0.21.5" @@ -6473,13 +5810,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/win32-arm64@npm:0.19.12" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-arm64@npm:0.21.5" @@ -6508,13 +5838,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/win32-ia32@npm:0.19.12" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-ia32@npm:0.21.5" @@ -6543,13 +5866,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/win32-x64@npm:0.19.12" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-x64@npm:0.21.5" @@ -6575,34 +5891,68 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": - version: 4.10.0 - resolution: "@eslint-community/regexpp@npm:4.10.0" - checksum: 10/8c36169c815fc5d726078e8c71a5b592957ee60d08c6470f9ce0187c8046af1a00afbda0a065cc40ff18d5d83f82aed9793c6818f7304a74a7488dc9f3ecbd42 +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1": + version: 4.12.1 + resolution: "@eslint-community/regexpp@npm:4.12.1" + checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.4": - version: 2.1.4 - resolution: "@eslint/eslintrc@npm:2.1.4" +"@eslint/config-array@npm:^0.19.0": + version: 0.19.0 + resolution: "@eslint/config-array@npm:0.19.0" + dependencies: + "@eslint/object-schema": "npm:^2.1.4" + debug: "npm:^4.3.1" + minimatch: "npm:^3.1.2" + checksum: 10/16e4ec468ebcb10255ab8c61234c1b3e7ac5506016e432fb489a1c5528cace7a60ddb07515516e7fc166b1dbe6c407d8a6bfbaa2e7531d445d8feb845c989913 + languageName: node + linkType: hard + +"@eslint/core@npm:^0.9.0": + version: 0.9.0 + resolution: "@eslint/core@npm:0.9.0" + checksum: 10/2d11e9c6fac14cfa817c7a9939fd6b79f2120928e4933952d061651db93797e0fcd67c858a14980ac26e90f6e0e49051436aefa4a4b06a26f24e3028366f73d9 + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^3.2.0": + version: 3.2.0 + resolution: "@eslint/eslintrc@npm:3.2.0" dependencies: ajv: "npm:^6.12.4" debug: "npm:^4.3.2" - espree: "npm:^9.6.0" - globals: "npm:^13.19.0" + espree: "npm:^10.0.1" + globals: "npm:^14.0.0" ignore: "npm:^5.2.0" import-fresh: "npm:^3.2.1" js-yaml: "npm:^4.1.0" minimatch: "npm:^3.1.2" strip-json-comments: "npm:^3.1.1" - checksum: 10/7a3b14f4b40fc1a22624c3f84d9f467a3d9ea1ca6e9a372116cb92507e485260359465b58e25bcb6c9981b155416b98c9973ad9b796053fd7b3f776a6946bce8 + checksum: 10/b32dd90ce7da68e89b88cd729db46b27aac79a2e6cb1fa75d25a6b766d586b443bfbf59622489efbd3c6f696f147b51111e81ec7cd23d70f215c5d474cad0261 + languageName: node + linkType: hard + +"@eslint/js@npm:9.15.0, @eslint/js@npm:^9.15.0": + version: 9.15.0 + resolution: "@eslint/js@npm:9.15.0" + checksum: 10/cdea71574a8be164147f426ffa5eca05a9c7fbfbae98387ed0cf772292fc9fb5ded69ce96eac110aaa633f6b7504ec551e1d33f2d6690ae95b11ec395553bae1 + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/object-schema@npm:2.1.4" + checksum: 10/221e8d9f281c605948cd6e030874aacce83fe097f8f9c1964787037bccf08e82b7aa9eff1850a30fffac43f1d76555727ec22a2af479d91e268e89d1e035131e languageName: node linkType: hard -"@eslint/js@npm:8.57.0": - version: 8.57.0 - resolution: "@eslint/js@npm:8.57.0" - checksum: 10/3c501ce8a997cf6cbbaf4ed358af5492875e3550c19b9621413b82caa9ae5382c584b0efa79835639e6e0ddaa568caf3499318e5bdab68643ef4199dce5eb0a0 +"@eslint/plugin-kit@npm:^0.2.3": + version: 0.2.3 + resolution: "@eslint/plugin-kit@npm:0.2.3" + dependencies: + levn: "npm:^0.4.1" + checksum: 10/0d0653ef840823fd5c0354ef8f1937e7763dbe830173eb6d2d55a19374bf04a06dff0e5214330c10a9425cf38655f632bb0d7d0666249b366e506ae291d82f7e languageName: node linkType: hard @@ -6812,6 +6162,16 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/common@npm:^3.2.0": + version: 3.2.0 + resolution: "@ethereumjs/common@npm:3.2.0" + dependencies: + "@ethereumjs/util": "npm:^8.1.0" + crc-32: "npm:^1.2.0" + checksum: 10/b3f612406b6bcefaf9117ceb42eff58d311e2b50205e3d55b4c793d803de517efbc84075e058dc0e2ec27a2bff11dfc279dda1fa2b249ed6ab3973be045898f4 + languageName: node + linkType: hard + "@ethereumjs/ethash@npm:^1.1.0": version: 1.1.0 resolution: "@ethereumjs/ethash@npm:1.1.0" @@ -6825,6 +6185,15 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/rlp@npm:^4.0.1": + version: 4.0.1 + resolution: "@ethereumjs/rlp@npm:4.0.1" + bin: + rlp: bin/rlp + checksum: 10/bfdffd634ce72f3b17e3d085d071f2fe7ce9680aebdf10713d74b30afd80ef882d17f19ff7175fcb049431a56e800bd3558d3b028bd0d82341927edb303ab450 + languageName: node + linkType: hard + "@ethereumjs/tx@npm:3.3.2": version: 3.3.2 resolution: "@ethereumjs/tx@npm:3.3.2" @@ -6855,6 +6224,29 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/tx@npm:^4.1.2, @ethereumjs/tx@npm:^4.2.0": + version: 4.2.0 + resolution: "@ethereumjs/tx@npm:4.2.0" + dependencies: + "@ethereumjs/common": "npm:^3.2.0" + "@ethereumjs/rlp": "npm:^4.0.1" + "@ethereumjs/util": "npm:^8.1.0" + ethereum-cryptography: "npm:^2.0.0" + checksum: 10/cbd2ffc3ef76ca5416d58f2f694858d9fcac946e6a107fef44cf3f308a7c9fcc996a6847868609354d72d5b356faee68408e9d5601c4c4f7dad8e18cb2c24a95 + languageName: node + linkType: hard + +"@ethereumjs/util@npm:^8.1.0": + version: 8.1.0 + resolution: "@ethereumjs/util@npm:8.1.0" + dependencies: + "@ethereumjs/rlp": "npm:^4.0.1" + ethereum-cryptography: "npm:^2.0.0" + micro-ftch: "npm:^0.3.1" + checksum: 10/cc35338932e49b15e54ca6e548b32a1f48eed7d7e1d34ee743e4d3600dd616668bd50f70139e86c5c35f55aac35fba3b6cc4e6f679cf650aeba66bf93016200c + languageName: node + linkType: hard + "@ethereumjs/vm@npm:5.6.0": version: 5.6.0 resolution: "@ethereumjs/vm@npm:5.6.0" @@ -6965,7 +6357,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/address@npm:5.7.0, @ethersproject/address@npm:^5.0.8, @ethersproject/address@npm:^5.4.0, @ethersproject/address@npm:^5.7.0": +"@ethersproject/address@npm:5.7.0, @ethersproject/address@npm:^5.0.8, @ethersproject/address@npm:^5.4.0, @ethersproject/address@npm:^5.6.0, @ethersproject/address@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/address@npm:5.7.0" dependencies: @@ -7561,6 +6953,15 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.6.7": + version: 1.6.8 + resolution: "@floating-ui/core@npm:1.6.8" + dependencies: + "@floating-ui/utils": "npm:^0.2.8" + checksum: 10/87d52989c3d2cc80373bc153b7a40814db3206ce7d0b2a2bdfb63e2ff39ffb8b999b1b0ccf28e548000ebf863bf16e2bed45eab4c4d287a5dbe974ef22368d82 + languageName: node + linkType: hard + "@floating-ui/dom@npm:^1.0.0": version: 1.6.8 resolution: "@floating-ui/dom@npm:1.6.8" @@ -7581,6 +6982,16 @@ __metadata: languageName: node linkType: hard +"@floating-ui/dom@npm:^1.6.10": + version: 1.6.12 + resolution: "@floating-ui/dom@npm:1.6.12" + dependencies: + "@floating-ui/core": "npm:^1.6.0" + "@floating-ui/utils": "npm:^0.2.8" + checksum: 10/5c8e5fdcd3843140a606ab6dc6c12ad740f44e66b898966ef877393faaede0bbe14586e1049e2c2f08856437da8847e884a2762e78275fefa65a5a9cd71e580d + languageName: node + linkType: hard + "@floating-ui/react-dom@npm:^2.0.0": version: 2.1.1 resolution: "@floating-ui/react-dom@npm:2.1.1" @@ -7593,7 +7004,7 @@ __metadata: languageName: node linkType: hard -"@floating-ui/react-dom@npm:^2.1.2": +"@floating-ui/react-dom@npm:^2.1.1, @floating-ui/react-dom@npm:^2.1.2": version: 2.1.2 resolution: "@floating-ui/react-dom@npm:2.1.2" dependencies: @@ -7619,6 +7030,20 @@ __metadata: languageName: node linkType: hard +"@floating-ui/react@npm:^0.26.23": + version: 0.26.27 + resolution: "@floating-ui/react@npm:0.26.27" + dependencies: + "@floating-ui/react-dom": "npm:^2.1.2" + "@floating-ui/utils": "npm:^0.2.8" + tabbable: "npm:^6.0.0" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10/ab6d05e5cc2c29272a6830c06dff07e8330f2b90234674f2d74ed8084659ebe3ac9472770a5f0e76007dd2ae89f9da75189d188437aea4b8ac2789afc21bb6b2 + languageName: node + linkType: hard + "@floating-ui/utils@npm:^0.2.5": version: 0.2.5 resolution: "@floating-ui/utils@npm:0.2.5" @@ -7640,6 +7065,63 @@ __metadata: languageName: node linkType: hard +"@formatjs/ecma402-abstract@npm:2.2.3": + version: 2.2.3 + resolution: "@formatjs/ecma402-abstract@npm:2.2.3" + dependencies: + "@formatjs/fast-memoize": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" + tslib: "npm:2" + checksum: 10/d39e9f0d36c296a635f52aa35e07a67b6aa90383a30a046a0508e5d730676399fd0e67188eff463fe2a4d5febc9f567af45788fdf881e070910be7eb9294dd8c + languageName: node + linkType: hard + +"@formatjs/fast-memoize@npm:2.2.3": + version: 2.2.3 + resolution: "@formatjs/fast-memoize@npm:2.2.3" + dependencies: + tslib: "npm:2" + checksum: 10/a9634acb5e03d051e09881eea5484ab02271f7d6b5f96ae9485674ab3c359aa881bc45fc07a1181ae4b2d6e288dadc169f578d142d698913ebbefa373014cac2 + languageName: node + linkType: hard + +"@formatjs/icu-messageformat-parser@npm:2.9.3": + version: 2.9.3 + resolution: "@formatjs/icu-messageformat-parser@npm:2.9.3" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/icu-skeleton-parser": "npm:1.8.7" + tslib: "npm:2" + checksum: 10/b24a3db43e4bf612107e981d5b40c077543d2266a08aac5cf01d5f65bf60527d5d16795e2e30063cb180b1d36d401944cd2ffb3a19d79b0cd28fa59751d19b7c + languageName: node + linkType: hard + +"@formatjs/icu-skeleton-parser@npm:1.8.7": + version: 1.8.7 + resolution: "@formatjs/icu-skeleton-parser@npm:1.8.7" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.2.3" + tslib: "npm:2" + checksum: 10/1a39815e5048f3c12a8d6a5b553271437b62e302724fc15c3b6967dc3e24823fcd9b8d3231a064991e163c147e54e588c571a092d557e93e78e738d218c6ef43 + languageName: node + linkType: hard + +"@formatjs/intl-localematcher@npm:0.5.7": + version: 0.5.7 + resolution: "@formatjs/intl-localematcher@npm:0.5.7" + dependencies: + tslib: "npm:2" + checksum: 10/52201f12212e7e9cba1a4f99020da587b13e44e06e03c4ccd4e5ac0829b411e73dfe0904a9039ef81eeabeea04ed8cfae9e727e6791acd0230745b7bd3ad059e + languageName: node + linkType: hard + +"@formkit/auto-animate@npm:^0.8.2": + version: 0.8.2 + resolution: "@formkit/auto-animate@npm:0.8.2" + checksum: 10/4414fbc9d13ddf8f9e39adfa1a96a9d3affca8d5fb3b148bd4bb9149066910a179b8477a9ce0af51d2abb2ee8d126d25d290460845ed8018858c569eed15655f + languageName: node + linkType: hard + "@ganache/ethereum-address@npm:0.1.4": version: 0.1.4 resolution: "@ganache/ethereum-address@npm:0.1.4" @@ -7771,14 +7253,20 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.14": - version: 0.11.14 - resolution: "@humanwhocodes/config-array@npm:0.11.14" +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 10/270d936be483ab5921702623bc74ce394bf12abbf57d9145a69e8a0d1c87eb1c768bd2d93af16c5705041e257e6d9cc7529311f63a1349f3678abc776fc28523 + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.6": + version: 0.16.6 + resolution: "@humanfs/node@npm:0.16.6" dependencies: - "@humanwhocodes/object-schema": "npm:^2.0.2" - debug: "npm:^4.3.1" - minimatch: "npm:^3.0.5" - checksum: 10/3ffb24ecdfab64014a230e127118d50a1a04d11080cbb748bc21629393d100850496456bbcb4e8c438957fe0934430d731042f1264d6a167b62d32fc2863580a + "@humanfs/core": "npm:^0.19.1" + "@humanwhocodes/retry": "npm:^0.3.0" + checksum: 10/6d43c6727463772d05610aa05c83dab2bfbe78291022ee7a92cb50999910b8c720c76cc312822e2dea2b497aa1b3fef5fe9f68803fc45c9d4ed105874a65e339 languageName: node linkType: hard @@ -7789,10 +7277,17 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^2.0.2": - version: 2.0.2 - resolution: "@humanwhocodes/object-schema@npm:2.0.2" - checksum: 10/ef915e3e2f34652f3d383b28a9a99cfea476fa991482370889ab14aac8ecd2b38d47cc21932526c6d949da0daf4a4a6bf629d30f41b0caca25e146819cbfa70e +"@humanwhocodes/retry@npm:^0.3.0": + version: 0.3.1 + resolution: "@humanwhocodes/retry@npm:0.3.1" + checksum: 10/eb457f699529de7f07649679ec9e0353055eebe443c2efe71c6dd950258892475a038e13c6a8c5e13ed1fb538cdd0a8794faa96b24b6ffc4c87fb1fc9f70ad7f + languageName: node + linkType: hard + +"@humanwhocodes/retry@npm:^0.4.1": + version: 0.4.1 + resolution: "@humanwhocodes/retry@npm:0.4.1" + checksum: 10/39fafc7319e88f61befebd5e1b4f0136534ea6a9bd10d74366698187bd63544210ec5d79a87ed4d91297f1cc64c4c53d45fb0077a2abfdce212cf0d3862d5f04 languageName: node linkType: hard @@ -7802,15 +7297,16 @@ __metadata: dependencies: "@chainlink/ccip-read-server": "npm:^0.2.1" "@jest/globals": "npm:^29.7.0" - "@types/node": "npm:^16.9.1" + "@types/node": "npm:^18.14.5" dotenv-flow: "npm:^4.1.0" - ethers: "npm:5.7.2" + eslint: "npm:^9.15.0" + ethers: "npm:^5.7.2" jest: "npm:^29.7.0" nodemon: "npm:^3.0.3" prettier: "npm:^2.8.8" ts-jest: "npm:^29.1.2" ts-node: "npm:^10.8.0" - tsx: "npm:^4.7.1" + tsx: "npm:^4.19.1" typescript: "npm:5.3.3" languageName: unknown linkType: soft @@ -7821,34 +7317,37 @@ __metadata: dependencies: "@aws-sdk/client-kms": "npm:^3.577.0" "@aws-sdk/client-s3": "npm:^3.577.0" + "@eslint/js": "npm:^9.15.0" "@ethersproject/abi": "npm:*" "@ethersproject/providers": "npm:*" - "@hyperlane-xyz/registry": "npm:4.7.0" - "@hyperlane-xyz/sdk": "npm:7.0.0" - "@hyperlane-xyz/utils": "npm:7.0.0" + "@hyperlane-xyz/registry": "npm:6.1.0" + "@hyperlane-xyz/sdk": "npm:7.1.0" + "@hyperlane-xyz/utils": "npm:7.1.0" "@inquirer/core": "npm:9.0.10" "@inquirer/figures": "npm:1.0.5" - "@inquirer/prompts": "npm:^3.0.0" + "@inquirer/prompts": "npm:3.3.2" "@types/chai-as-promised": "npm:^8" "@types/mocha": "npm:^10.0.1" "@types/node": "npm:^18.14.5" "@types/yargs": "npm:^17.0.24" - "@typescript-eslint/eslint-plugin": "npm:^7.4.0" - "@typescript-eslint/parser": "npm:^7.4.0" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" ansi-escapes: "npm:^7.0.0" asn1.js: "npm:^5.4.1" bignumber.js: "npm:^9.1.1" chai: "npm:^4.5.0" chai-as-promised: "npm:^8.0.0" chalk: "npm:^5.3.0" - eslint: "npm:^8.57.0" + eslint: "npm:^9.15.0" eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" ethers: "npm:^5.7.2" latest-version: "npm:^8.0.0" mocha: "npm:^10.2.0" prettier: "npm:^2.8.8" terminal-link: "npm:^3.0.0" - tsx: "npm:^4.7.1" + tsx: "npm:^4.19.1" typescript: "npm:5.3.3" yaml: "npm:2.4.5" yargs: "npm:^17.7.2" @@ -7860,24 +7359,24 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/core@npm:5.8.0, @hyperlane-xyz/core@workspace:solidity": +"@hyperlane-xyz/core@npm:5.8.1, @hyperlane-xyz/core@workspace:solidity": version: 0.0.0-use.local resolution: "@hyperlane-xyz/core@workspace:solidity" dependencies: "@arbitrum/nitro-contracts": "npm:^1.2.1" "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:7.0.0" + "@hyperlane-xyz/utils": "npm:7.1.0" "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" "@layerzerolabs/solidity-examples": "npm:^1.1.0" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts": "npm:^4.9.3" - "@openzeppelin/contracts-upgradeable": "npm:^v4.9.3" + "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" "@typechain/ethers-v5": "npm:^11.1.2" "@typechain/ethers-v6": "npm:^0.5.1" "@typechain/hardhat": "npm:^9.1.0" "@types/node": "npm:^18.14.5" - chai: "npm:4.5.0" + chai: "npm:^4.5.0" ethereum-waffle: "npm:^4.0.10" ethers: "npm:^5.7.2" fx-portal: "npm:^1.0.3" @@ -7909,7 +7408,7 @@ __metadata: "@cloudflare/vitest-pool-workers": "npm:^0.4.5" "@cloudflare/workers-types": "npm:^4.20240821.1" "@faker-js/faker": "npm:^8.4.1" - chai: "npm:4.5.0" + chai: "npm:^4.5.0" prettier: "npm:^2.8.8" typescript: "npm:5.3.3" vitest: "npm:1.4.0" @@ -7917,13 +7416,14 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/helloworld@npm:7.0.0, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": +"@hyperlane-xyz/helloworld@npm:7.1.0, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": version: 0.0.0-use.local resolution: "@hyperlane-xyz/helloworld@workspace:typescript/helloworld" dependencies: - "@hyperlane-xyz/core": "npm:5.8.0" - "@hyperlane-xyz/registry": "npm:4.7.0" - "@hyperlane-xyz/sdk": "npm:7.0.0" + "@eslint/js": "npm:^9.15.0" + "@hyperlane-xyz/core": "npm:5.8.1" + "@hyperlane-xyz/registry": "npm:6.1.0" + "@hyperlane-xyz/sdk": "npm:7.1.0" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" @@ -7931,11 +7431,13 @@ __metadata: "@typechain/ethers-v5": "npm:^11.1.2" "@typechain/ethers-v6": "npm:^0.5.1" "@typechain/hardhat": "npm:^9.1.0" - "@typescript-eslint/eslint-plugin": "npm:^7.4.0" - "@typescript-eslint/parser": "npm:^7.4.0" - chai: "npm:4.5.0" - eslint: "npm:^8.57.0" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" + chai: "npm:^4.5.0" + eslint: "npm:^9.15.0" eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" ethereum-waffle: "npm:^4.0.10" ethers: "npm:^5.7.2" hardhat: "npm:^2.22.2" @@ -7960,21 +7462,21 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperlane-xyz/infra@workspace:typescript/infra" dependencies: - "@arbitrum/sdk": "npm:^3.0.0" + "@arbitrum/sdk": "npm:^4.0.0" "@aws-sdk/client-iam": "npm:^3.74.0" - "@aws-sdk/client-kms": "npm:3.48.0" - "@aws-sdk/client-s3": "npm:^3.74.0" + "@aws-sdk/client-kms": "npm:^3.577.0" + "@aws-sdk/client-s3": "npm:^3.577.0" "@cosmjs/amino": "npm:^0.32.4" "@eth-optimism/sdk": "npm:^3.1.6" "@ethersproject/experimental": "npm:^5.7.0" "@ethersproject/hardware-wallets": "npm:^5.7.0" - "@ethersproject/providers": "npm:^5.7.2" + "@ethersproject/providers": "npm:*" "@google-cloud/secret-manager": "npm:^5.5.0" - "@hyperlane-xyz/helloworld": "npm:7.0.0" - "@hyperlane-xyz/registry": "npm:4.10.0" - "@hyperlane-xyz/sdk": "npm:7.0.0" - "@hyperlane-xyz/utils": "npm:7.0.0" - "@inquirer/prompts": "npm:^5.3.8" + "@hyperlane-xyz/helloworld": "npm:7.1.0" + "@hyperlane-xyz/registry": "npm:6.1.0" + "@hyperlane-xyz/sdk": "npm:7.1.0" + "@hyperlane-xyz/utils": "npm:7.1.0" + "@inquirer/prompts": "npm:3.3.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-etherscan": "npm:^3.0.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" @@ -7985,13 +7487,13 @@ __metadata: "@types/chai": "npm:^4.2.21" "@types/json-stable-stringify": "npm:^1.0.36" "@types/mocha": "npm:^10.0.1" - "@types/node": "npm:^16.9.1" + "@types/node": "npm:^18.14.5" "@types/prompts": "npm:^2.0.14" "@types/sinon-chai": "npm:^3.2.12" "@types/yargs": "npm:^17.0.24" - asn1.js: "npm:5.4.1" + asn1.js: "npm:^5.4.1" aws-kms-ethers-signer: "npm:^0.1.3" - chai: "npm:4.5.0" + chai: "npm:^4.5.0" deep-object-diff: "npm:^1.1.9" dotenv: "npm:^10.0.0" ethereum-waffle: "npm:^4.0.10" @@ -8002,7 +7504,7 @@ __metadata: prettier: "npm:^2.8.8" prom-client: "npm:^14.0.1" prompts: "npm:^2.4.2" - tsx: "npm:^4.7.1" + tsx: "npm:^4.19.1" typescript: "npm:5.3.3" yaml: "npm:2.4.5" yargs: "npm:^17.7.2" @@ -8016,49 +7518,45 @@ __metadata: resolution: "@hyperlane-xyz/monorepo@workspace:." dependencies: "@changesets/cli": "npm:^2.26.2" + "@eslint/js": "npm:^9.15.0" "@trivago/prettier-plugin-sort-imports": "npm:^4.2.1" - "@typescript-eslint/eslint-plugin": "npm:^7.4.0" - "@typescript-eslint/parser": "npm:^7.4.0" - eslint: "npm:^8.57.0" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" + eslint: "npm:^9.15.0" eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:^28.2.0" husky: "npm:^8.0.0" lint-staged: "npm:^12.4.3" prettier: "npm:^2.8.8" - tsx: "npm:^4.7.1" + syncpack: "npm:^13.0.0" + tsx: "npm:^4.19.1" languageName: unknown linkType: soft -"@hyperlane-xyz/registry@npm:4.10.0": - version: 4.10.0 - resolution: "@hyperlane-xyz/registry@npm:4.10.0" - dependencies: - yaml: "npm:2.4.5" - zod: "npm:^3.21.2" - checksum: 10/22bb18f426cbada8b97db0894fe5d0980dfc08ecbd5174c978b7aeb6d8df9706f93d7e9cf0630644d2455ad05feee714dc2a38ec515a717b0b257184637902fb - languageName: node - linkType: hard - -"@hyperlane-xyz/registry@npm:4.7.0": - version: 4.7.0 - resolution: "@hyperlane-xyz/registry@npm:4.7.0" +"@hyperlane-xyz/registry@npm:6.1.0": + version: 6.1.0 + resolution: "@hyperlane-xyz/registry@npm:6.1.0" dependencies: yaml: "npm:2.4.5" zod: "npm:^3.21.2" - checksum: 10/d5b0090869417c3fc263c379791f439070113aee239990ffc20d9d90d74102b77008f3c630ce955a9b3f1f92f79b1df67d83a097b327cd5db2b01b382bf40f18 + checksum: 10/a0e1ecc02d83604793ddda0a3e00a9ffcaa38b1cddf9883b47cf8f1919b4474abd6cc2ee84846e6a35e1bc7539299b9bec92bfdf06be72beecff6aa44b73d382 languageName: node linkType: hard -"@hyperlane-xyz/sdk@npm:7.0.0, @hyperlane-xyz/sdk@workspace:typescript/sdk": +"@hyperlane-xyz/sdk@npm:7.1.0, @hyperlane-xyz/sdk@workspace:typescript/sdk": version: 0.0.0-use.local resolution: "@hyperlane-xyz/sdk@workspace:typescript/sdk" dependencies: "@arbitrum/sdk": "npm:^4.0.0" - "@aws-sdk/client-s3": "npm:^3.74.0" + "@aws-sdk/client-s3": "npm:^3.577.0" + "@chain-registry/types": "npm:^0.50.14" "@cosmjs/cosmwasm-stargate": "npm:^0.32.4" "@cosmjs/stargate": "npm:^0.32.4" - "@hyperlane-xyz/core": "npm:5.8.0" - "@hyperlane-xyz/utils": "npm:7.0.0" + "@eslint/js": "npm:^9.15.0" + "@hyperlane-xyz/core": "npm:5.8.1" + "@hyperlane-xyz/utils": "npm:7.1.0" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@safe-global/api-kit": "npm:1.3.0" @@ -8067,16 +7565,21 @@ __metadata: "@solana/spl-token": "npm:^0.4.9" "@solana/web3.js": "npm:^1.95.4" "@types/mocha": "npm:^10.0.1" - "@types/node": "npm:^16.9.1" + "@types/node": "npm:^18.14.5" "@types/sinon": "npm:^17.0.1" "@types/sinon-chai": "npm:^3.2.12" "@types/ws": "npm:^8.5.5" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" bignumber.js: "npm:^9.1.1" - chai: "npm:4.5.0" + chai: "npm:^4.5.0" cosmjs-types: "npm:^0.9.0" cross-fetch: "npm:^3.1.5" dotenv: "npm:^10.0.0" - eslint: "npm:^8.57.0" + eslint: "npm:^9.15.0" + eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" ethereum-waffle: "npm:^4.0.10" ethers: "npm:^5.7.2" hardhat: "npm:^2.22.2" @@ -8085,9 +7588,9 @@ __metadata: prettier: "npm:^2.8.8" sinon: "npm:^13.0.2" ts-node: "npm:^10.8.0" - tsx: "npm:^4.7.1" + tsx: "npm:^4.19.1" typescript: "npm:5.3.3" - viem: "npm:^2.21.40" + viem: "npm:^2.21.45" yaml: "npm:2.4.5" zod: "npm:^3.21.2" peerDependencies: @@ -8096,18 +7599,25 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/utils@npm:7.0.0, @hyperlane-xyz/utils@workspace:typescript/utils": +"@hyperlane-xyz/utils@npm:7.1.0, @hyperlane-xyz/utils@workspace:typescript/utils": version: 0.0.0-use.local resolution: "@hyperlane-xyz/utils@workspace:typescript/utils" dependencies: "@cosmjs/encoding": "npm:^0.32.4" + "@eslint/js": "npm:^9.15.0" "@solana/web3.js": "npm:^1.95.4" "@types/lodash-es": "npm:^4.17.12" "@types/mocha": "npm:^10.0.1" "@types/sinon": "npm:^17.0.1" "@types/sinon-chai": "npm:^3.2.12" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" bignumber.js: "npm:^9.1.1" - chai: "npm:4.5.0" + chai: "npm:^4.5.0" + eslint: "npm:^9.15.0" + eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" ethers: "npm:^5.7.2" lodash-es: "npm:^4.17.21" mocha: "npm:^10.2.0" @@ -8123,10 +7633,21 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperlane-xyz/widgets@workspace:typescript/widgets" dependencies: + "@chakra-ui/react": "npm:^2.8.2" + "@cosmjs/cosmwasm-stargate": "npm:^0.32.4" + "@cosmos-kit/react": "npm:^2.18.0" + "@emotion/react": "npm:^11.13.3" + "@emotion/styled": "npm:^11.13.0" + "@eslint/js": "npm:^9.15.0" "@headlessui/react": "npm:^2.1.8" - "@hyperlane-xyz/registry": "npm:4.7.0" - "@hyperlane-xyz/sdk": "npm:7.0.0" - "@hyperlane-xyz/utils": "npm:7.0.0" + "@hyperlane-xyz/registry": "npm:6.1.0" + "@hyperlane-xyz/sdk": "npm:7.1.0" + "@hyperlane-xyz/utils": "npm:7.1.0" + "@interchain-ui/react": "npm:^1.23.28" + "@rainbow-me/rainbowkit": "npm:^2.2.0" + "@solana/wallet-adapter-react": "npm:^0.15.32" + "@solana/wallet-adapter-react-ui": "npm:^0.9.31" + "@solana/web3.js": "npm:^1.95.4" "@storybook/addon-essentials": "npm:^7.6.14" "@storybook/addon-interactions": "npm:^7.6.14" "@storybook/addon-links": "npm:^7.6.14" @@ -8135,19 +7656,23 @@ __metadata: "@storybook/react": "npm:^7.6.14" "@storybook/react-vite": "npm:^7.6.14" "@storybook/test": "npm:^7.6.14" - "@types/node": "npm:^18.11.18" + "@tanstack/react-query": "npm:^5.59.20" + "@types/node": "npm:^18.14.5" "@types/react": "npm:^18.0.27" "@types/react-dom": "npm:^18.0.10" "@types/ws": "npm:^8.5.5" - "@typescript-eslint/eslint-plugin": "npm:^7.4.0" - "@typescript-eslint/parser": "npm:^7.4.0" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" babel-loader: "npm:^8.3.0" clsx: "npm:^2.1.1" - eslint: "npm:^8.57.0" + eslint: "npm:^9.15.0" eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" eslint-plugin-react: "npm:^7.37.2" eslint-plugin-react-hooks: "npm:^5.0.0" - eslint-plugin-storybook: "npm:^0.6.15" + eslint-plugin-storybook: "npm:^0.11.1" + framer-motion: "npm:^10.16.4" postcss: "npm:^8.4.21" prettier: "npm:^2.8.8" react: "npm:^18.2.0" @@ -8157,61 +7682,40 @@ __metadata: tailwindcss: "npm:^3.4.13" ts-node: "npm:^10.8.0" typescript: "npm:5.3.3" + viem: "npm:^2.21.45" vite: "npm:^5.1.1" + wagmi: "npm:^2.12.26" peerDependencies: react: ^18 react-dom: ^18 languageName: unknown linkType: soft -"@inquirer/checkbox@npm:^1.3.5": - version: 1.3.5 - resolution: "@inquirer/checkbox@npm:1.3.5" +"@inquirer/checkbox@npm:^1.5.2": + version: 1.5.2 + resolution: "@inquirer/checkbox@npm:1.5.2" dependencies: - "@inquirer/core": "npm:^3.0.0" - "@inquirer/type": "npm:^1.1.1" + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" ansi-escapes: "npm:^4.3.2" chalk: "npm:^4.1.2" figures: "npm:^3.2.0" - checksum: 10/e7e984ef44afe2dcdcf2bb56f24065ca15954ee0a16edbc0d614df8de383123a0e956c7c22e1786b60bc0dfb41f98fbcfb89fd33adabff26e223c5acd918aac1 - languageName: node - linkType: hard - -"@inquirer/checkbox@npm:^2.4.7": - version: 2.4.7 - resolution: "@inquirer/checkbox@npm:2.4.7" - dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/figures": "npm:^1.0.5" - "@inquirer/type": "npm:^1.5.2" - ansi-escapes: "npm:^4.3.2" - yoctocolors-cjs: "npm:^2.1.2" - checksum: 10/9bc0d6e9d6db90bcda3771d6b96e885e8c4e1f03d96a4fcd04b4eab2fafbecfafbced7a5cc24eca73f677452f9e354505f9cfb79a884dcf06772550845014d6f + checksum: 10/00e4dd403c739ce91368915d08ad98000a8dc7a83fe6fca12a4445b47768beb1c86dd99c675d79df6658a93cebca54286e34415c51f8926e6ffb338a2feb4db5 languageName: node linkType: hard -"@inquirer/confirm@npm:^2.0.6": - version: 2.0.6 - resolution: "@inquirer/confirm@npm:2.0.6" +"@inquirer/confirm@npm:^2.0.17": + version: 2.0.17 + resolution: "@inquirer/confirm@npm:2.0.17" dependencies: - "@inquirer/core": "npm:^3.0.0" - "@inquirer/type": "npm:^1.1.1" + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" chalk: "npm:^4.1.2" - checksum: 10/efbfeca4c2750ec65fd603d041039356d1f3f5b321305e11fefe40ebc7aa69e8e82fde42f216967462541a9a742768b8f816e4b5c86b3e82c0c886f7227e65ac - languageName: node - linkType: hard - -"@inquirer/confirm@npm:^3.1.22": - version: 3.1.22 - resolution: "@inquirer/confirm@npm:3.1.22" - dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/type": "npm:^1.5.2" - checksum: 10/14e547ae3194c6447d41bb87135c03aa5598fd340fced19e4e8bae1be4ae54a9ad3cf335a9c3c6dc54e2ffb7928319e0f4b428531b8ce720cd23d2444292ca36 + checksum: 10/76cdf50881c21bcab4813600502fb3975cbed56a85ad6deaaf06832b92b78b9c932842ffda0e911f29d7dee79dd9dc4724735bd1d60562a509c8ef6317c28c69 languageName: node linkType: hard -"@inquirer/core@npm:9.0.10, @inquirer/core@npm:^9.0.10": +"@inquirer/core@npm:9.0.10": version: 9.0.10 resolution: "@inquirer/core@npm:9.0.10" dependencies: @@ -8232,71 +7736,49 @@ __metadata: languageName: node linkType: hard -"@inquirer/core@npm:^3.0.0": - version: 3.0.0 - resolution: "@inquirer/core@npm:3.0.0" +"@inquirer/core@npm:^6.0.0": + version: 6.0.0 + resolution: "@inquirer/core@npm:6.0.0" dependencies: - "@inquirer/type": "npm:^1.1.1" - "@types/mute-stream": "npm:^0.0.1" - "@types/node": "npm:^20.4.2" + "@inquirer/type": "npm:^1.1.6" + "@types/mute-stream": "npm:^0.0.4" + "@types/node": "npm:^20.10.7" "@types/wrap-ansi": "npm:^3.0.0" ansi-escapes: "npm:^4.3.2" chalk: "npm:^4.1.2" - cli-spinners: "npm:^2.8.0" - cli-width: "npm:^4.0.0" + cli-spinners: "npm:^2.9.2" + cli-width: "npm:^4.1.0" figures: "npm:^3.2.0" mute-stream: "npm:^1.0.0" run-async: "npm:^3.0.0" - string-width: "npm:^4.2.3" + signal-exit: "npm:^4.1.0" strip-ansi: "npm:^6.0.1" - wrap-ansi: "npm:^6.0.1" - checksum: 10/7524c15d004e1686c5b66086fe70b50ab50983dd6f887a90fa765cc4f9ae2ba7e063dbf0cb740233d55ee1c8b60f10a7b957245bee95223e755e9632c77a9ca4 + wrap-ansi: "npm:^6.2.0" + checksum: 10/a9f79fe538deab439afc845e16fa8832872b4562be6e39a5de8b50eca3e2b27be0e470fc4ee014f202a750213adc8a06068402d51d6d7b34b118b12b08200f85 languageName: node linkType: hard -"@inquirer/editor@npm:^1.2.4": - version: 1.2.4 - resolution: "@inquirer/editor@npm:1.2.4" +"@inquirer/editor@npm:^1.2.15": + version: 1.2.15 + resolution: "@inquirer/editor@npm:1.2.15" dependencies: - "@inquirer/core": "npm:^3.0.0" - "@inquirer/type": "npm:^1.1.1" + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" chalk: "npm:^4.1.2" - external-editor: "npm:^3.0.3" - checksum: 10/a3dac45256d334f79061b0e79b8e9427ee5d5b244367ac582811148f0fe2c7fc3bfaec3aab5fda0ea0293ff23c3c2ef2e66fdf6f6f9b56cf628b265d9cab5afd - languageName: node - linkType: hard - -"@inquirer/editor@npm:^2.1.22": - version: 2.1.22 - resolution: "@inquirer/editor@npm:2.1.22" - dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/type": "npm:^1.5.2" external-editor: "npm:^3.1.0" - checksum: 10/d36255567c88ea48bf1071b00c502d6a32bc1402966db4f9ae1be59d41d64d11e02111317d880d0bdc42fbfb1b819240fb229c89b07dfb804a6d5fb176ab8bb0 + checksum: 10/fbb79f9972aae4cbf7a2e4c36995cf7c6e77b235c47cdd1a05e69f71626aa1ac48ddb3adc3a118568eea2bf5322bfafea71bd62a7969c883c15dbf71e9630e39 languageName: node linkType: hard -"@inquirer/expand@npm:^1.1.5": - version: 1.1.5 - resolution: "@inquirer/expand@npm:1.1.5" +"@inquirer/expand@npm:^1.1.16": + version: 1.1.16 + resolution: "@inquirer/expand@npm:1.1.16" dependencies: - "@inquirer/core": "npm:^3.0.0" - "@inquirer/type": "npm:^1.1.1" + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" chalk: "npm:^4.1.2" figures: "npm:^3.2.0" - checksum: 10/04df9f1713864a965bfaff12358cbd4de97f58363dfb0b625ba4c4b4736bdb1cef38b5a726024ea41b0a55cdbdd1d271fbb6659a8e7fce8927383a8d1bf77562 - languageName: node - linkType: hard - -"@inquirer/expand@npm:^2.1.22": - version: 2.1.22 - resolution: "@inquirer/expand@npm:2.1.22" - dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/type": "npm:^1.5.2" - yoctocolors-cjs: "npm:^2.1.2" - checksum: 10/f997ba916d3ddcc6e2563158805e2ae7a7a6f98e24cf0a08e23d4101b7d78f78e7dce28e648b85ca7f41759eeefdf1c6f6abf2bce0f041fbda54aacf68522454 + checksum: 10/5158f8eb807bd1d55e7e7ca9288051f5947970b32b1a33cd9c94ea228c03cdb18508be5ecd0237061276721f5c9ca96741b3e2c288123c526cdd5049cdf566db languageName: node linkType: hard @@ -8307,167 +7789,159 @@ __metadata: languageName: node linkType: hard -"@inquirer/input@npm:^1.2.5": - version: 1.2.5 - resolution: "@inquirer/input@npm:1.2.5" +"@inquirer/input@npm:^1.2.16": + version: 1.2.16 + resolution: "@inquirer/input@npm:1.2.16" dependencies: - "@inquirer/core": "npm:^3.0.0" - "@inquirer/type": "npm:^1.1.1" + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" chalk: "npm:^4.1.2" - checksum: 10/6a524e231e19f9d2dc80b475722c25b94cfdbc3f4e70491f70f9bdd92409eb1a4f6c856f21d76953c4b6880ce5b657a8acfd12a5cfa7fe2b38f460df8729ce56 + checksum: 10/b4b189832ee900b9e088f43a1ce3a959c493ffb06bb112ff166603962e79981086cdf809b63ad908e1f531b52cd467c0681a8dae98005fc0eebe7cee43e41286 languageName: node linkType: hard -"@inquirer/input@npm:^2.2.9": - version: 2.2.9 - resolution: "@inquirer/input@npm:2.2.9" +"@inquirer/password@npm:^1.1.16": + version: 1.1.16 + resolution: "@inquirer/password@npm:1.1.16" dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/type": "npm:^1.5.2" - checksum: 10/9d0c97da9cc6972d4fb5bcb077e00e581aae90f6891d33c1c5e2f0324023c1772c6d5b03cd30ec7d4f71d22791d38bf45c47bafbe7dd9f74446693e7b120a2b0 + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" + ansi-escapes: "npm:^4.3.2" + chalk: "npm:^4.1.2" + checksum: 10/f40309775c690a1d4d39fba1c1bb2a2b4b1beb8b20b1956a4541d4191d7b5eab8ac663c0d0c01caff413cecdf3b2e36e527c391f3aa9a8fc1931f329056e0a81 languageName: node linkType: hard -"@inquirer/number@npm:^1.0.10": - version: 1.0.10 - resolution: "@inquirer/number@npm:1.0.10" +"@inquirer/prompts@npm:3.3.2": + version: 3.3.2 + resolution: "@inquirer/prompts@npm:3.3.2" dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/type": "npm:^1.5.2" - checksum: 10/0f9323b581e1c35ee8fbf2acde301c3e354896aeac83f6854e9672575090e0d092d19aadadb3477659079c403e63a3206bf668dd4c87e86834f775744f57c955 + "@inquirer/checkbox": "npm:^1.5.2" + "@inquirer/confirm": "npm:^2.0.17" + "@inquirer/core": "npm:^6.0.0" + "@inquirer/editor": "npm:^1.2.15" + "@inquirer/expand": "npm:^1.1.16" + "@inquirer/input": "npm:^1.2.16" + "@inquirer/password": "npm:^1.1.16" + "@inquirer/rawlist": "npm:^1.2.16" + "@inquirer/select": "npm:^1.3.3" + checksum: 10/d8b18c1fc87fd6774d6934600d2d7eda493bb3cceeb7f9d079acfac507d851b66cf9e9cd2dee3146649bf72db5d1788dacd7e22708bbcebc4c5a00fd94f08b67 languageName: node linkType: hard -"@inquirer/password@npm:^1.1.5": - version: 1.1.5 - resolution: "@inquirer/password@npm:1.1.5" +"@inquirer/rawlist@npm:^1.2.16": + version: 1.2.16 + resolution: "@inquirer/rawlist@npm:1.2.16" dependencies: - "@inquirer/input": "npm:^1.2.5" - "@inquirer/type": "npm:^1.1.1" + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" chalk: "npm:^4.1.2" - checksum: 10/81aa1a101cfffdcaab6e12487ffc672e5a5cad0c4e2c504f0c56b7bf3119077826d6df5b0a35a2a938c1ff888618d68e656f7acc5103abd5d7b2b56e97aa5ff3 + checksum: 10/a4acefb0f54e4d4c3f7c44d35971cb0b8cbf2acd6dbe490576cd24369f3304ff4a36255cd4cc851c2de7c037cf70f71c129bc6c8c5c80dce495998e6168904fd languageName: node linkType: hard -"@inquirer/password@npm:^2.1.22": - version: 2.1.22 - resolution: "@inquirer/password@npm:2.1.22" +"@inquirer/select@npm:^1.3.3": + version: 1.3.3 + resolution: "@inquirer/select@npm:1.3.3" dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/type": "npm:^1.5.2" + "@inquirer/core": "npm:^6.0.0" + "@inquirer/type": "npm:^1.1.6" ansi-escapes: "npm:^4.3.2" - checksum: 10/ce4e7c268f267c7436cf3a1b2890a9c92fddc2928bbe141d48f2f5a5daedbb3a2c601e44b0fe4e255792676ed241118ba69756b0d0b7d4492e0b7ee8fc548b90 - languageName: node - linkType: hard - -"@inquirer/prompts@npm:^3.0.0": - version: 3.0.0 - resolution: "@inquirer/prompts@npm:3.0.0" - dependencies: - "@inquirer/checkbox": "npm:^1.3.5" - "@inquirer/confirm": "npm:^2.0.6" - "@inquirer/core": "npm:^3.0.0" - "@inquirer/editor": "npm:^1.2.4" - "@inquirer/expand": "npm:^1.1.5" - "@inquirer/input": "npm:^1.2.5" - "@inquirer/password": "npm:^1.1.5" - "@inquirer/rawlist": "npm:^1.2.5" - "@inquirer/select": "npm:^1.2.5" - checksum: 10/aa35f8543f78f52b8cb939ac8fefd994fa3b118414525209c0993c40ecff856797c3050b8974bfec49eecadfdd25a6ecb50bb122ffc253ae2b0e4e9b5af1043b + chalk: "npm:^4.1.2" + figures: "npm:^3.2.0" + checksum: 10/0f33c51ab69c156b96092bfb107d08dd0f4227274917b9406aa23877e3ba94fd6738800fb973c44c051aaebdba72d07dc328df4b76e6e1285f68aa01a7ed0ed8 languageName: node linkType: hard -"@inquirer/prompts@npm:^5.3.8": - version: 5.3.8 - resolution: "@inquirer/prompts@npm:5.3.8" +"@inquirer/type@npm:^1.1.6": + version: 1.5.5 + resolution: "@inquirer/type@npm:1.5.5" dependencies: - "@inquirer/checkbox": "npm:^2.4.7" - "@inquirer/confirm": "npm:^3.1.22" - "@inquirer/editor": "npm:^2.1.22" - "@inquirer/expand": "npm:^2.1.22" - "@inquirer/input": "npm:^2.2.9" - "@inquirer/number": "npm:^1.0.10" - "@inquirer/password": "npm:^2.1.22" - "@inquirer/rawlist": "npm:^2.2.4" - "@inquirer/search": "npm:^1.0.7" - "@inquirer/select": "npm:^2.4.7" - checksum: 10/e60eba0d64590c96fed722107962f433fbd8ff13f5d8a3ad6ba56964db69c8bc6b91a439b4e90209184090aacf73d84b0504e8c5a6a0f778ced70deb580ac1cd + mute-stream: "npm:^1.0.0" + checksum: 10/bd3f3d7510785af4ad599e042e99e4be6380f52f79f3db140fe6fed0a605acf27b1a0a20fb5cc688eaf7b8aa0c36dacb1d89c7bba4586f38cbf58ba9f159e7b5 languageName: node linkType: hard -"@inquirer/rawlist@npm:^1.2.5": - version: 1.2.5 - resolution: "@inquirer/rawlist@npm:1.2.5" +"@inquirer/type@npm:^1.5.2": + version: 1.5.2 + resolution: "@inquirer/type@npm:1.5.2" dependencies: - "@inquirer/core": "npm:^3.0.0" - "@inquirer/type": "npm:^1.1.1" - chalk: "npm:^4.1.2" - checksum: 10/e94388228305b0d1b49b4f4c6b744c89614c7f413cae2098ff5f7d685ae485b9ad3af9f93796061fad2c8bfa816e7daa9e349ee2499398011744d7c03f6718c3 + mute-stream: "npm:^1.0.0" + checksum: 10/90d9203b5d7da8530e210c5421630b577f24554c8b683a4b45ea0f5c6a89c451771170aa34f2b62ca57e4be4de41d6761c941475e25c54c82b527c05644f181f languageName: node linkType: hard -"@inquirer/rawlist@npm:^2.2.4": - version: 2.2.4 - resolution: "@inquirer/rawlist@npm:2.2.4" +"@interchain-ui/react@npm:^1.23.28": + version: 1.26.1 + resolution: "@interchain-ui/react@npm:1.26.1" dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/type": "npm:^1.5.2" - yoctocolors-cjs: "npm:^2.1.2" - checksum: 10/dd9d34a5cca081d53a9798cdfed2fdb61455dcfa856f54bc036dc5f57aceb95a7484487632c157bdba75e50de24990ebb3bb178ee765b8c0a735ff61b29cebf4 + "@floating-ui/core": "npm:^1.6.7" + "@floating-ui/dom": "npm:^1.6.10" + "@floating-ui/react": "npm:^0.26.23" + "@floating-ui/react-dom": "npm:^2.1.1" + "@floating-ui/utils": "npm:^0.2.7" + "@formkit/auto-animate": "npm:^0.8.2" + "@react-aria/listbox": "npm:^3.13.3" + "@react-aria/overlays": "npm:^3.23.2" + "@react-aria/utils": "npm:^3.25.2" + "@tanstack/react-virtual": "npm:^3.10.5" + "@vanilla-extract/css": "npm:^1.15.5" + "@vanilla-extract/css-utils": "npm:^0.1.4" + "@vanilla-extract/dynamic": "npm:^2.1.2" + "@vanilla-extract/private": "npm:^1.0.6" + "@vanilla-extract/recipes": "npm:^0.5.5" + animejs: "npm:^3.2.2" + bignumber.js: "npm:^9.1.2" + client-only: "npm:^0.0.1" + clsx: "npm:^2.1.1" + copy-to-clipboard: "npm:^3.3.3" + immer: "npm:^10.1.1" + lodash: "npm:^4.17.21" + rainbow-sprinkles: "npm:^0.17.3" + react-aria: "npm:^3.34.3" + react-stately: "npm:^3.32.2" + zustand: "npm:^4.5.5" + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 + checksum: 10/30bc0bc47799586f9eb58ed955aa2dc640dce1674653c87603a838462f3bacaaf64ae3202731f86b60aaff7393bf066b472657f51a3e2272e95e3df23b0a878a languageName: node linkType: hard -"@inquirer/search@npm:^1.0.7": - version: 1.0.7 - resolution: "@inquirer/search@npm:1.0.7" +"@internationalized/date@npm:^3.5.6": + version: 3.5.6 + resolution: "@internationalized/date@npm:3.5.6" dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/figures": "npm:^1.0.5" - "@inquirer/type": "npm:^1.5.2" - yoctocolors-cjs: "npm:^2.1.2" - checksum: 10/3cd401cc1a7b01772e0e50ee27a0560cc647900f475d28a4f9b07843d4a85e1555c6adc1d7bc38ad2ef3546c524ca82c60272490d0bb159632c03cbe01f52bb1 + "@swc/helpers": "npm:^0.5.0" + checksum: 10/54734b53ca74a32aae368a8f963324352b1fd5b13029b6e82555307b8f2ff355658c90e82a4f38f154a3edf874387d1efd26fc80f2edd068ce04f48f6467f26c languageName: node linkType: hard -"@inquirer/select@npm:^1.2.5": - version: 1.2.5 - resolution: "@inquirer/select@npm:1.2.5" +"@internationalized/message@npm:^3.1.5": + version: 3.1.5 + resolution: "@internationalized/message@npm:3.1.5" dependencies: - "@inquirer/core": "npm:^3.0.0" - "@inquirer/type": "npm:^1.1.1" - ansi-escapes: "npm:^4.3.2" - chalk: "npm:^4.1.2" - figures: "npm:^3.2.0" - checksum: 10/42dec9663740db98d043d39aa799f1292d08ed12725520b316bbe3f7cb48d55e3587725072bdf5720220a2d29befb6f80544e914aa5e54bdfb4d4e00c9299f7d + "@swc/helpers": "npm:^0.5.0" + intl-messageformat: "npm:^10.1.0" + checksum: 10/210951fd8055af4db70d465e49bcbbdf2545ed223b936af9c1f18b745a51689ecb0ca49cbd5ee2dbfeccce2447808b7fe309bd12ee81f7e09283f20bf04200e9 languageName: node linkType: hard -"@inquirer/select@npm:^2.4.7": - version: 2.4.7 - resolution: "@inquirer/select@npm:2.4.7" +"@internationalized/number@npm:^3.5.4": + version: 3.5.4 + resolution: "@internationalized/number@npm:3.5.4" dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/figures": "npm:^1.0.5" - "@inquirer/type": "npm:^1.5.2" - ansi-escapes: "npm:^4.3.2" - yoctocolors-cjs: "npm:^2.1.2" - checksum: 10/854a3d0393073913f9bd3bf2e4ec7b8d114dfb48308a0a6698cf5c2c627da2700db5bdb69d054eaec89bd4e52a1274e493fa78d4fa26a5893972d91825456047 - languageName: node - linkType: hard - -"@inquirer/type@npm:^1.1.1": - version: 1.1.1 - resolution: "@inquirer/type@npm:1.1.1" - checksum: 10/1fcce0bd6c92611ed67ee9252deebba0fa9a54aeb4e37fc349ec736c8e1ffaa58f3a02dbe84489ff9a90bebc6b1080a19169ef30c16a18128cf8f42d06a49f51 + "@swc/helpers": "npm:^0.5.0" + checksum: 10/16641aecb58c075a6322dc6b36a2c6e521845296f81b86a128d015f072d1af998289b71b4d8b9521e7576bdeabfaf8067a3e741b0116c8595d82a4461c1ae03b languageName: node linkType: hard -"@inquirer/type@npm:^1.5.2": - version: 1.5.2 - resolution: "@inquirer/type@npm:1.5.2" +"@internationalized/string@npm:^3.2.4": + version: 3.2.4 + resolution: "@internationalized/string@npm:3.2.4" dependencies: - mute-stream: "npm:^1.0.0" - checksum: 10/90d9203b5d7da8530e210c5421630b577f24554c8b683a4b45ea0f5c6a89c451771170aa34f2b62ca57e4be4de41d6761c941475e25c54c82b527c05644f181f + "@swc/helpers": "npm:^0.5.0" + checksum: 10/5fdb7f0bf7fa7055cdf62ded4efd6849d3db9cf0e6d53f349889e2ec9517b9135ad38a6bb8dcf25142c69c381618c0dd1a6a072117dd7cf2867ce17374f0f835 languageName: node linkType: hard @@ -8915,6 +8389,91 @@ __metadata: languageName: node linkType: hard +"@keplr-wallet/common@npm:0.12.28": + version: 0.12.28 + resolution: "@keplr-wallet/common@npm:0.12.28" + dependencies: + "@keplr-wallet/crypto": "npm:0.12.28" + "@keplr-wallet/types": "npm:0.12.28" + buffer: "npm:^6.0.3" + delay: "npm:^4.4.0" + mobx: "npm:^6.1.7" + checksum: 10/32efe4a89290c89aa5a593a0fa4bf635c0c5b8814d61b80ecc3baea837d2733a355db4cb057184227cf1712658795978cd66e7797574d639e6ba9dbcb7fde3d2 + languageName: node + linkType: hard + +"@keplr-wallet/cosmos@npm:0.12.28": + version: 0.12.28 + resolution: "@keplr-wallet/cosmos@npm:0.12.28" + dependencies: + "@ethersproject/address": "npm:^5.6.0" + "@keplr-wallet/common": "npm:0.12.28" + "@keplr-wallet/crypto": "npm:0.12.28" + "@keplr-wallet/proto-types": "npm:0.12.28" + "@keplr-wallet/simple-fetch": "npm:0.12.28" + "@keplr-wallet/types": "npm:0.12.28" + "@keplr-wallet/unit": "npm:0.12.28" + bech32: "npm:^1.1.4" + buffer: "npm:^6.0.3" + long: "npm:^4.0.0" + protobufjs: "npm:^6.11.2" + checksum: 10/685e84369f9fa0fd0e292e8c5031b8aa7ee2998285727eb31436b1f7a862e97c6731de6f84fd66ef4a20f0731c013f2f46296f57b7bf52ace6a874c8540004b8 + languageName: node + linkType: hard + +"@keplr-wallet/crypto@npm:0.12.28": + version: 0.12.28 + resolution: "@keplr-wallet/crypto@npm:0.12.28" + dependencies: + "@ethersproject/keccak256": "npm:^5.5.0" + bip32: "npm:^2.0.6" + bip39: "npm:^3.0.3" + bs58check: "npm:^2.1.2" + buffer: "npm:^6.0.3" + crypto-js: "npm:^4.0.0" + elliptic: "npm:^6.5.3" + sha.js: "npm:^2.4.11" + checksum: 10/76e5cf1c60b5108b9fb5bb5b8a841e6e51cbca76e0707198bef16bf6073cf574baee020216ead8acea8e5e60ee4f7386ee2c290bacfbe333ec1dfc849d942347 + languageName: node + linkType: hard + +"@keplr-wallet/proto-types@npm:0.12.28": + version: 0.12.28 + resolution: "@keplr-wallet/proto-types@npm:0.12.28" + dependencies: + long: "npm:^4.0.0" + protobufjs: "npm:^6.11.2" + checksum: 10/0537db803ce9a295443c206180220e932798f2769aaa8b552a3d99cbfa3f052ddace0dd50d09eb28bdd32c7d1ca3e10dcd2012c1611575da42650f478b162d5d + languageName: node + linkType: hard + +"@keplr-wallet/simple-fetch@npm:0.12.28": + version: 0.12.28 + resolution: "@keplr-wallet/simple-fetch@npm:0.12.28" + checksum: 10/36f8d769098b309a5069b992ffc3ed562e97d4e2553d04a97c0f55c423b80435d3b8c38f4ed57b5255c2342bf1675e2eb5cb64f158a6499e753b7e96ca90ef18 + languageName: node + linkType: hard + +"@keplr-wallet/types@npm:0.12.28": + version: 0.12.28 + resolution: "@keplr-wallet/types@npm:0.12.28" + dependencies: + long: "npm:^4.0.0" + checksum: 10/17ee47aac5690b89feb2109fb901a9af81ad7f94b743ba5de0203b051f7977941a78d3da70ef4ae5c2d82af0afd2de8de156f027c95c05d4f1a1fed39ea2df7d + languageName: node + linkType: hard + +"@keplr-wallet/unit@npm:0.12.28": + version: 0.12.28 + resolution: "@keplr-wallet/unit@npm:0.12.28" + dependencies: + "@keplr-wallet/types": "npm:0.12.28" + big-integer: "npm:^1.6.48" + utility-types: "npm:^3.10.0" + checksum: 10/f18361c2bdc2b59c0b7c3340466cab50d12634056ed89aee8363414e65eb314b570e43d019107c8ae6ee67e533b1b5e5bfa3beaec313763e68297bb9a3d6a3e8 + languageName: node + linkType: hard + "@layerzerolabs/lz-evm-messagelib-v2@npm:^2.0.2": version: 2.0.6 resolution: "@layerzerolabs/lz-evm-messagelib-v2@npm:2.0.6" @@ -9092,6 +8651,22 @@ __metadata: languageName: node linkType: hard +"@lit-labs/ssr-dom-shim@npm:^1.0.0, @lit-labs/ssr-dom-shim@npm:^1.1.0": + version: 1.2.1 + resolution: "@lit-labs/ssr-dom-shim@npm:1.2.1" + checksum: 10/48e28c1f132eb1d5b385454dd23db2837bf913d108a0908e73816ceb594b1b09db34e05ccb86a18fb9c02fc100d62bbab350b6ec88e2c175f2c21c5f0220bfdd + languageName: node + linkType: hard + +"@lit/reactive-element@npm:^1.3.0, @lit/reactive-element@npm:^1.6.0": + version: 1.6.3 + resolution: "@lit/reactive-element@npm:1.6.3" + dependencies: + "@lit-labs/ssr-dom-shim": "npm:^1.0.0" + checksum: 10/664c899bb0b144590dc4faf83b358b1504810eac107778c3aeb384affc65a7ef4eda754944bcc34a57237db03dff145332406345ac24da19ca37cf4b3cb343d3 + languageName: node + linkType: hard + "@manypkg/find-root@npm:^1.1.0": version: 1.1.0 resolution: "@manypkg/find-root@npm:1.1.0" @@ -9130,6 +8705,17 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-json-rpc-provider@npm:^1.0.0": + version: 1.0.1 + resolution: "@metamask/eth-json-rpc-provider@npm:1.0.1" + dependencies: + "@metamask/json-rpc-engine": "npm:^7.0.0" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^5.0.1" + checksum: 10/4ed1a96afc32eb46f585ff54e16cb2aee2e7027dcf6a142d875b9c6248f15c9a00dd1df43035f2e64efbf01a96954040699d9d97e3b483c958f5b1d6c0fa6f50 + languageName: node + linkType: hard + "@metamask/eth-sig-util@npm:^4.0.0": version: 4.0.1 resolution: "@metamask/eth-sig-util@npm:4.0.1" @@ -9143,6 +8729,318 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-engine@npm:^7.0.0": + version: 7.3.3 + resolution: "@metamask/json-rpc-engine@npm:7.3.3" + dependencies: + "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^8.3.0" + checksum: 10/116664c974c522d280335d9a02cba731e4f08562c2980415f7535513cd308c7e612e52618086996e5ac2b67db7f1e6ac1bd8201aba7825163db17a25f2874cc9 + languageName: node + linkType: hard + +"@metamask/json-rpc-engine@npm:^8.0.1, @metamask/json-rpc-engine@npm:^8.0.2": + version: 8.0.2 + resolution: "@metamask/json-rpc-engine@npm:8.0.2" + dependencies: + "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^8.3.0" + checksum: 10/f088f4b648b9b55875b56e8237853e7282f13302a9db6a1f9bba06314dfd6cd0a23b3d27f8fde05a157b97ebb03b67bc2699ba455c99553dfb2ecccd73ab3474 + languageName: node + linkType: hard + +"@metamask/json-rpc-middleware-stream@npm:^7.0.1": + version: 7.0.2 + resolution: "@metamask/json-rpc-middleware-stream@npm:7.0.2" + dependencies: + "@metamask/json-rpc-engine": "npm:^8.0.2" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^8.3.0" + readable-stream: "npm:^3.6.2" + checksum: 10/850a857418fc6b8c73fb4f978b76d2cdc0372ccb2f0f7e6f0229117882a4687d716fc37638483c9ac1338f7957b3f8207bc6be8a3d4c0708339fe9dfc3510fe0 + languageName: node + linkType: hard + +"@metamask/object-multiplex@npm:^2.0.0": + version: 2.1.0 + resolution: "@metamask/object-multiplex@npm:2.1.0" + dependencies: + once: "npm:^1.4.0" + readable-stream: "npm:^3.6.2" + checksum: 10/e119f695e89eb20c3174f8ac6d74587498d85cff92c37e83e167cb758b3d3147d5b5e1a997d6198d430ebcf2cede6265bf5d4513fe96dbb2d82bbc6167752caa + languageName: node + linkType: hard + +"@metamask/onboarding@npm:^1.0.1": + version: 1.0.1 + resolution: "@metamask/onboarding@npm:1.0.1" + dependencies: + bowser: "npm:^2.9.0" + checksum: 10/2aa288e58fc34cb4708e311fc08abd33a0d9bc67671610955a2bd8d43a16330261f1159174c365611e249751ec984da9a9cb963bb0a87b3a6945d7caa6cc8799 + languageName: node + linkType: hard + +"@metamask/providers@npm:16.1.0": + version: 16.1.0 + resolution: "@metamask/providers@npm:16.1.0" + dependencies: + "@metamask/json-rpc-engine": "npm:^8.0.1" + "@metamask/json-rpc-middleware-stream": "npm:^7.0.1" + "@metamask/object-multiplex": "npm:^2.0.0" + "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/safe-event-emitter": "npm:^3.1.1" + "@metamask/utils": "npm:^8.3.0" + detect-browser: "npm:^5.2.0" + extension-port-stream: "npm:^3.0.0" + fast-deep-equal: "npm:^3.1.3" + is-stream: "npm:^2.0.0" + readable-stream: "npm:^3.6.2" + webextension-polyfill: "npm:^0.10.0" + checksum: 10/596bcc0206355e5698cc41458b07caa748f589790e1a3210f1a32d21103a3318902d953a641d4583b8179d653659ba29c42e65fba019a98533bdcf68316bf915 + languageName: node + linkType: hard + +"@metamask/rpc-errors@npm:^6.2.1": + version: 6.4.0 + resolution: "@metamask/rpc-errors@npm:6.4.0" + dependencies: + "@metamask/utils": "npm:^9.0.0" + fast-safe-stringify: "npm:^2.0.6" + checksum: 10/9a17525aa8ce9ac142a94c04000dba7f0635e8e155c6c045f57eca36cc78c255318cca2fad4571719a427dfd2df64b70bc6442989523a8de555480668d666ad5 + languageName: node + linkType: hard + +"@metamask/safe-event-emitter@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/safe-event-emitter@npm:2.0.0" + checksum: 10/3e4f00c64aa1ddf9b9ae5c2337fb8cee359b6c481ded0ec21ef70610960c51cdcc4a9b569de334dcd7cb1fe445cafd298360907c1e211e244c5990b55246f350 + languageName: node + linkType: hard + +"@metamask/safe-event-emitter@npm:^3.0.0, @metamask/safe-event-emitter@npm:^3.1.1": + version: 3.1.2 + resolution: "@metamask/safe-event-emitter@npm:3.1.2" + checksum: 10/8ef7579f9317eb5c94ecf3e6abb8d13b119af274b678805eac76abe4c0667bfdf539f385e552bb973e96333b71b77aa7c787cb3fce9cd5fb4b00f1dbbabf880d + languageName: node + linkType: hard + +"@metamask/sdk-communication-layer@npm:0.30.0": + version: 0.30.0 + resolution: "@metamask/sdk-communication-layer@npm:0.30.0" + dependencies: + bufferutil: "npm:^4.0.8" + date-fns: "npm:^2.29.3" + debug: "npm:^4.3.4" + utf-8-validate: "npm:^5.0.2" + uuid: "npm:^8.3.2" + peerDependencies: + cross-fetch: ^4.0.0 + eciesjs: ^0.3.16 + eventemitter2: ^6.4.7 + readable-stream: ^3.6.2 + socket.io-client: ^4.5.1 + checksum: 10/a68f67abbff258f89d3179869f85f7353e36ea26d2ba1e226a43959701207dff1015c5c2536a2a7afd72c8414131e451c84df9b926079f8b930c299328342b92 + languageName: node + linkType: hard + +"@metamask/sdk-install-modal-web@npm:0.30.0": + version: 0.30.0 + resolution: "@metamask/sdk-install-modal-web@npm:0.30.0" + dependencies: + qr-code-styling: "npm:^1.6.0-rc.1" + peerDependencies: + i18next: 23.11.5 + react: ^18.2.0 + react-dom: ^18.2.0 + react-native: "*" + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + react-native: + optional: true + checksum: 10/b1ea701706fcbb734c6e780bb3a28e4fe2cea99b8e03faf4330b0fe2682b0ec31d35c79fab4bd007584937d32602f4eb0f09ae1c1dd0fdec927de229014e1c6d + languageName: node + linkType: hard + +"@metamask/sdk@npm:0.30.1": + version: 0.30.1 + resolution: "@metamask/sdk@npm:0.30.1" + dependencies: + "@metamask/onboarding": "npm:^1.0.1" + "@metamask/providers": "npm:16.1.0" + "@metamask/sdk-communication-layer": "npm:0.30.0" + "@metamask/sdk-install-modal-web": "npm:0.30.0" + bowser: "npm:^2.9.0" + cross-fetch: "npm:^4.0.0" + debug: "npm:^4.3.4" + eciesjs: "npm:^0.4.8" + eth-rpc-errors: "npm:^4.0.3" + eventemitter2: "npm:^6.4.7" + i18next: "npm:23.11.5" + i18next-browser-languagedetector: "npm:7.1.0" + obj-multiplex: "npm:^1.0.0" + pump: "npm:^3.0.0" + qrcode-terminal-nooctal: "npm:^0.12.1" + react-native-webview: "npm:^11.26.0" + readable-stream: "npm:^3.6.2" + socket.io-client: "npm:^4.5.1" + util: "npm:^0.12.4" + uuid: "npm:^8.3.2" + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 10/a30e975de75493daefcd34eebf37ebbe13a9aa811cf5acb82f727742f86fdef3a051f9abd209478e0f9c65efa0d4ea5010d4efcdb0d5701c05e317172ac30dfc + languageName: node + linkType: hard + +"@metamask/superstruct@npm:^3.0.0, @metamask/superstruct@npm:^3.1.0": + version: 3.1.0 + resolution: "@metamask/superstruct@npm:3.1.0" + checksum: 10/5066fe228d5f11da387606d7f9545de2b473ab5a9e0f1bb8aea2f52d3e2c9d25e427151acde61f4a2de80a07a9871fe9505ad06abca6a61b7c3b54ed5c403b01 + languageName: node + linkType: hard + +"@metamask/utils@npm:^5.0.1": + version: 5.0.2 + resolution: "@metamask/utils@npm:5.0.2" + dependencies: + "@ethereumjs/tx": "npm:^4.1.2" + "@types/debug": "npm:^4.1.7" + debug: "npm:^4.3.4" + semver: "npm:^7.3.8" + superstruct: "npm:^1.0.3" + checksum: 10/c0d3ee4c3144b557936ab01c1a64950c0f99782bd0cf5596c0fabe8fd224dba48ed3483c0ea954791fe2ee81064a445adb489df50c776bbbeb67b5b96e930115 + languageName: node + linkType: hard + +"@metamask/utils@npm:^8.3.0": + version: 8.5.0 + resolution: "@metamask/utils@npm:8.5.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.0.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + debug: "npm:^4.3.4" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/68a42a55f7dc750b75467fb7c05a496c20dac073a2753e0f4d9642c4d8dcb3f9ddf51a09d30337e11637f1777f3dfe22e15b5159dbafb0fdb7bd8c9236056153 + languageName: node + linkType: hard + +"@metamask/utils@npm:^9.0.0": + version: 9.3.0 + resolution: "@metamask/utils@npm:9.3.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + debug: "npm:^4.3.4" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/ed6648cd973bbf3b4eb0e862903b795a99d27784c820e19f62f0bc0ddf353e98c2858d7e9aaebc0249a586391b344e35b9249d13c08e3ea0c74b23dc1c6b1558 + languageName: node + linkType: hard + +"@motionone/animation@npm:^10.15.1, @motionone/animation@npm:^10.18.0": + version: 10.18.0 + resolution: "@motionone/animation@npm:10.18.0" + dependencies: + "@motionone/easing": "npm:^10.18.0" + "@motionone/types": "npm:^10.17.1" + "@motionone/utils": "npm:^10.18.0" + tslib: "npm:^2.3.1" + checksum: 10/c7fc04dd10d6cade3d3b63d26f2532a2b2731233afc0454722e55ad8061fb3923d926db9cc09f1bcedb39f504fcee1e80adaab270523846998aad3017364a583 + languageName: node + linkType: hard + +"@motionone/dom@npm:^10.16.2, @motionone/dom@npm:^10.16.4": + version: 10.18.0 + resolution: "@motionone/dom@npm:10.18.0" + dependencies: + "@motionone/animation": "npm:^10.18.0" + "@motionone/generators": "npm:^10.18.0" + "@motionone/types": "npm:^10.17.1" + "@motionone/utils": "npm:^10.18.0" + hey-listen: "npm:^1.0.8" + tslib: "npm:^2.3.1" + checksum: 10/18abb5c174a84c90b2e59459fa3a9f8b655d063c259f2f3be5b6740e660285d2f66a8b25437dd963c3b9cdeae9fa5984ee8d217881088ea4d392cf39f8493a84 + languageName: node + linkType: hard + +"@motionone/easing@npm:^10.18.0": + version: 10.18.0 + resolution: "@motionone/easing@npm:10.18.0" + dependencies: + "@motionone/utils": "npm:^10.18.0" + tslib: "npm:^2.3.1" + checksum: 10/a455a06ccee907ce9da7b1dfe392060a473132733e3f92bbee3a99c36af7baa333cf3c6e38c6d44ad0f9878fdafca3c3f4bcfe55aaeb2a633e45d8e0429f8fa5 + languageName: node + linkType: hard + +"@motionone/generators@npm:^10.18.0": + version: 10.18.0 + resolution: "@motionone/generators@npm:10.18.0" + dependencies: + "@motionone/types": "npm:^10.17.1" + "@motionone/utils": "npm:^10.18.0" + tslib: "npm:^2.3.1" + checksum: 10/149720881e8db6a1ff38cea98349c3a00f72e5318b645459b68a2aeddb1f2be63ad2ae8978f6c4a63e2414f39e65f06de13a43fd35cf24dc3fb3e3c7f87526bc + languageName: node + linkType: hard + +"@motionone/svelte@npm:^10.16.2": + version: 10.16.4 + resolution: "@motionone/svelte@npm:10.16.4" + dependencies: + "@motionone/dom": "npm:^10.16.4" + tslib: "npm:^2.3.1" + checksum: 10/5ad532d4d9bb16a9f311487e6409fa7e1a66ec12f82e3c36434ab6dfe3cedc61b35dae6314cee4fba8dca463b8a259cafb83801a932b7ad5f4a6e45baaa581f4 + languageName: node + linkType: hard + +"@motionone/types@npm:^10.15.1, @motionone/types@npm:^10.17.1": + version: 10.17.1 + resolution: "@motionone/types@npm:10.17.1" + checksum: 10/21d92d733ba30f810b72609fe04f2ef86125ba0160b826974605cc4cc5fbb6ab7bbf1640cbc64fd6298eb8d36fb920ad3ca646c76adf0e2c47a4920200616952 + languageName: node + linkType: hard + +"@motionone/utils@npm:^10.15.1, @motionone/utils@npm:^10.18.0": + version: 10.18.0 + resolution: "@motionone/utils@npm:10.18.0" + dependencies: + "@motionone/types": "npm:^10.17.1" + hey-listen: "npm:^1.0.8" + tslib: "npm:^2.3.1" + checksum: 10/0fa9232d132383880d6004522ded763d60f490946584e02bca7f64df98fae07421071f3a85de06aa6ecb52632a47a7586b4143e824e459a87cc852fab657e549 + languageName: node + linkType: hard + +"@motionone/vue@npm:^10.16.2": + version: 10.16.4 + resolution: "@motionone/vue@npm:10.16.4" + dependencies: + "@motionone/dom": "npm:^10.16.4" + tslib: "npm:^2.3.1" + checksum: 10/2400d31bbf5c3e02bc68f4b88d96d9c0672ba646bca0b6566e555cd7e8f14849a645f558f574e658fd90574a0b548b61712ae5edcee055c60288fd9382d711ea + languageName: node + linkType: hard + "@ndelangen/get-tarball@npm:^3.0.7": version: 3.0.9 resolution: "@ndelangen/get-tarball@npm:3.0.9" @@ -9154,7 +9052,23 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.6.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:~1.6.0": +"@noble/ciphers@npm:^1.0.0": + version: 1.0.0 + resolution: "@noble/ciphers@npm:1.0.0" + checksum: 10/0a03d2bfac316f6f235ae4cdbeeba372f8d32997239c27cb56d55cbd3d42e0f867e8d7c8d76716f5f645bb7d5d73f05ba1f2d2e7d8391e86936e3b97021bfcf6 + languageName: node + linkType: hard + +"@noble/curves@npm:1.4.2, @noble/curves@npm:~1.4.0": + version: 1.4.2 + resolution: "@noble/curves@npm:1.4.2" + dependencies: + "@noble/hashes": "npm:1.4.0" + checksum: 10/f433a2e8811ae345109388eadfa18ef2b0004c1f79417553241db4f0ad0d59550be6298a4f43d989c627e9f7551ffae6e402a4edf0173981e6da95fc7cab5123 + languageName: node + linkType: hard + +"@noble/curves@npm:1.6.0, @noble/curves@npm:^1.1.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:^1.6.0, @noble/curves@npm:~1.6.0": version: 1.6.0 resolution: "@noble/curves@npm:1.6.0" dependencies: @@ -9186,7 +9100,14 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:~1.5.0": +"@noble/hashes@npm:1.4.0, @noble/hashes@npm:~1.4.0": + version: 1.4.0 + resolution: "@noble/hashes@npm:1.4.0" + checksum: 10/e156e65794c473794c52fa9d06baf1eb20903d0d96719530f523cc4450f6c721a957c544796e6efd0197b2296e7cd70efeb312f861465e17940a3e3c7e0febc6 + languageName: node + linkType: hard + +"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.5.0": version: 1.5.0 resolution: "@noble/hashes@npm:1.5.0" checksum: 10/da7fc7af52af7afcf59810a7eea6155075464ff462ffda2572dc6d57d53e2669b1ea2ec774e814f6273f1697e567f28d36823776c9bf7068cba2a2855140f26e @@ -9224,7 +9145,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": +"@nodelib/fs.walk@npm:^1.2.3": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -9234,6 +9155,13 @@ __metadata: languageName: node linkType: hard +"@nolyfill/is-core-module@npm:1.0.39": + version: 1.0.39 + resolution: "@nolyfill/is-core-module@npm:1.0.39" + checksum: 10/0d6e098b871eca71d875651288e1f0fa770a63478b0b50479c99dc760c64175a56b5b04f58d5581bbcc6b552b8191ab415eada093d8df9597ab3423c8cac1815 + languageName: node + linkType: hard + "@nomicfoundation/edr-darwin-arm64@npm:0.3.3": version: 0.3.3 resolution: "@nomicfoundation/edr-darwin-arm64@npm:0.3.3" @@ -9758,7 +9686,7 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts-upgradeable@npm:^4.9.3, @openzeppelin/contracts-upgradeable@npm:^v4.9.3": +"@openzeppelin/contracts-upgradeable@npm:^4.9.3": version: 4.9.3 resolution: "@openzeppelin/contracts-upgradeable@npm:4.9.3" checksum: 10/d8fd6fd9d2271fbdd3958c20769b72a241687883ecd3bea05a3969568cdcabdee9d53c21ac776a651c397507d9c22d8db0a4fb970d27bdab918979fae7285a2f @@ -9875,6 +9803,161 @@ __metadata: languageName: node linkType: hard +"@parcel/watcher-android-arm64@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-android-arm64@npm:2.5.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-arm64@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-darwin-arm64@npm:2.5.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-x64@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-darwin-x64@npm:2.5.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-freebsd-x64@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-freebsd-x64@npm:2.5.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-glibc@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-linux-arm-glibc@npm:2.5.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-musl@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-linux-arm-musl@npm:2.5.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-glibc@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.5.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-musl@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-linux-arm64-musl@npm:2.5.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-glibc@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-linux-x64-glibc@npm:2.5.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-musl@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-linux-x64-musl@npm:2.5.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-wasm@npm:^2.4.1": + version: 2.5.0 + resolution: "@parcel/watcher-wasm@npm:2.5.0" + dependencies: + is-glob: "npm:^4.0.3" + micromatch: "npm:^4.0.5" + napi-wasm: "npm:^1.1.0" + checksum: 10/2e17915320267b6d6305406a4b59cb0b0e88eb93ba6acc61c5382c517421a9132992fb8d1468a0030ee9945a1d6216ee6112452e78b30089590cd206c49d98a0 + languageName: node + linkType: hard + +"@parcel/watcher-win32-arm64@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-win32-arm64@npm:2.5.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-win32-ia32@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-win32-ia32@npm:2.5.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@parcel/watcher-win32-x64@npm:2.5.0": + version: 2.5.0 + resolution: "@parcel/watcher-win32-x64@npm:2.5.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher@npm:^2.4.1": + version: 2.5.0 + resolution: "@parcel/watcher@npm:2.5.0" + dependencies: + "@parcel/watcher-android-arm64": "npm:2.5.0" + "@parcel/watcher-darwin-arm64": "npm:2.5.0" + "@parcel/watcher-darwin-x64": "npm:2.5.0" + "@parcel/watcher-freebsd-x64": "npm:2.5.0" + "@parcel/watcher-linux-arm-glibc": "npm:2.5.0" + "@parcel/watcher-linux-arm-musl": "npm:2.5.0" + "@parcel/watcher-linux-arm64-glibc": "npm:2.5.0" + "@parcel/watcher-linux-arm64-musl": "npm:2.5.0" + "@parcel/watcher-linux-x64-glibc": "npm:2.5.0" + "@parcel/watcher-linux-x64-musl": "npm:2.5.0" + "@parcel/watcher-win32-arm64": "npm:2.5.0" + "@parcel/watcher-win32-ia32": "npm:2.5.0" + "@parcel/watcher-win32-x64": "npm:2.5.0" + detect-libc: "npm:^1.0.3" + is-glob: "npm:^4.0.3" + micromatch: "npm:^4.0.5" + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:latest" + dependenciesMeta: + "@parcel/watcher-android-arm64": + optional: true + "@parcel/watcher-darwin-arm64": + optional: true + "@parcel/watcher-darwin-x64": + optional: true + "@parcel/watcher-freebsd-x64": + optional: true + "@parcel/watcher-linux-arm-glibc": + optional: true + "@parcel/watcher-linux-arm-musl": + optional: true + "@parcel/watcher-linux-arm64-glibc": + optional: true + "@parcel/watcher-linux-arm64-musl": + optional: true + "@parcel/watcher-linux-x64-glibc": + optional: true + "@parcel/watcher-linux-x64-musl": + optional: true + "@parcel/watcher-win32-arm64": + optional: true + "@parcel/watcher-win32-ia32": + optional: true + "@parcel/watcher-win32-x64": + optional: true + checksum: 10/1e28b1aa9a63456ebfa7af3e41297d088bd31d9e32548604f4f26ed96c5808f4330cd515062e879c24a9eaab7894066c8a3951ee30b59e7cbe6786ab2c790dae + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -9909,6 +9992,13 @@ __metadata: languageName: node linkType: hard +"@popperjs/core@npm:^2.11.8": + version: 2.11.8 + resolution: "@popperjs/core@npm:2.11.8" + checksum: 10/ddd16090cde777aaf102940f05d0274602079a95ad9805bd20bc55dcc7c3a2ba1b99dd5c73e5cc2753c3d31250ca52a67d58059459d7d27debb983a9f552936c + languageName: node + linkType: hard + "@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": version: 1.1.2 resolution: "@protobufjs/aspromise@npm:1.1.2" @@ -10703,6 +10793,219 @@ __metadata: languageName: node linkType: hard +"@rainbow-me/rainbowkit@npm:^2.2.0": + version: 2.2.0 + resolution: "@rainbow-me/rainbowkit@npm:2.2.0" + dependencies: + "@vanilla-extract/css": "npm:1.15.5" + "@vanilla-extract/dynamic": "npm:2.1.2" + "@vanilla-extract/sprinkles": "npm:1.6.3" + clsx: "npm:2.1.1" + qrcode: "npm:1.5.4" + react-remove-scroll: "npm:2.6.0" + ua-parser-js: "npm:^1.0.37" + peerDependencies: + "@tanstack/react-query": ">=5.0.0" + react: ">=18" + react-dom: ">=18" + viem: 2.x + wagmi: ^2.9.0 + checksum: 10/5848d39c2b34b533481ed636e44de2cde9d25cbe3fc5bebd77ce1e60ebddcb5cbfc3bfc3ecbc93a74c0fc8ddff4a4b098e2174b6a35a45231a0351e54ccd3ed4 + languageName: node + linkType: hard + +"@react-aria/breadcrumbs@npm:^3.5.18": + version: 3.5.18 + resolution: "@react-aria/breadcrumbs@npm:3.5.18" + dependencies: + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/link": "npm:^3.7.6" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/breadcrumbs": "npm:^3.7.8" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/4f4eaa50606a725bc00e95cc2123bed3db32c03c4a93067aeac12ea1d577cde34f2b4021f0eaace3a4e35dd77a0d36ad057449e7f50dbff1964d2de82f3aa9a0 + languageName: node + linkType: hard + +"@react-aria/button@npm:^3.10.1": + version: 3.10.1 + resolution: "@react-aria/button@npm:3.10.1" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/toggle": "npm:^3.7.8" + "@react-types/button": "npm:^3.10.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/da59869337fb810adf4fef3f10a8b687604193533deec347e1f67ce48b04a2a07b4b83ef463f5b03fa6134e254fad0afc3a94e63cbda59022fbb09e3b99f7f5b + languageName: node + linkType: hard + +"@react-aria/calendar@npm:^3.5.13": + version: 3.5.13 + resolution: "@react-aria/calendar@npm:3.5.13" + dependencies: + "@internationalized/date": "npm:^3.5.6" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/live-announcer": "npm:^3.4.0" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/calendar": "npm:^3.5.5" + "@react-types/button": "npm:^3.10.0" + "@react-types/calendar": "npm:^3.4.10" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/8e5d5a556d9b94e0f98452944f409d189cab20111ea87ef1151c27763236cd778dc836d63a49832d792279ab2a6dc5cffb01fa099073986b1dac58f0d41af7ee + languageName: node + linkType: hard + +"@react-aria/checkbox@npm:^3.14.8": + version: 3.14.8 + resolution: "@react-aria/checkbox@npm:3.14.8" + dependencies: + "@react-aria/form": "npm:^3.0.10" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/toggle": "npm:^3.10.9" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/checkbox": "npm:^3.6.9" + "@react-stately/form": "npm:^3.0.6" + "@react-stately/toggle": "npm:^3.7.8" + "@react-types/checkbox": "npm:^3.8.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/4389f9650da1a58c027ac63ce7973c6886ba0d9b3c19d2267e2e0c08f64aa0f97070c2a67d74b2d1758f9b50ca009ed2f2a5999dca5a6f392c163b7e0c4a9b23 + languageName: node + linkType: hard + +"@react-aria/color@npm:^3.0.1": + version: 3.0.1 + resolution: "@react-aria/color@npm:3.0.1" + dependencies: + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/numberfield": "npm:^3.11.8" + "@react-aria/slider": "npm:^3.7.13" + "@react-aria/spinbutton": "npm:^3.6.9" + "@react-aria/textfield": "npm:^3.14.10" + "@react-aria/utils": "npm:^3.25.3" + "@react-aria/visually-hidden": "npm:^3.8.17" + "@react-stately/color": "npm:^3.8.0" + "@react-stately/form": "npm:^3.0.6" + "@react-types/color": "npm:^3.0.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/5a805e173fe48db2ef5e550fee96829891d527be190d6af2c7d6ce66db6a04c6a3524148576f6e86d99dff43b8475493a428f81a07676a0d954611f45fe304ef + languageName: node + linkType: hard + +"@react-aria/combobox@npm:^3.10.5": + version: 3.10.5 + resolution: "@react-aria/combobox@npm:3.10.5" + dependencies: + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/listbox": "npm:^3.13.5" + "@react-aria/live-announcer": "npm:^3.4.0" + "@react-aria/menu": "npm:^3.15.5" + "@react-aria/overlays": "npm:^3.23.4" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/textfield": "npm:^3.14.10" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/combobox": "npm:^3.10.0" + "@react-stately/form": "npm:^3.0.6" + "@react-types/button": "npm:^3.10.0" + "@react-types/combobox": "npm:^3.13.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/8267d6dc11fdaef68cbabce8d17f4d9f4943e3eb7de8cad93e316a4f3fd0d68334b68e01a9e9bc3ed6f4f09b2e5c6f6c497d49b2544a15242dec64ee8a7b84e5 + languageName: node + linkType: hard + +"@react-aria/datepicker@npm:^3.11.4": + version: 3.11.4 + resolution: "@react-aria/datepicker@npm:3.11.4" + dependencies: + "@internationalized/date": "npm:^3.5.6" + "@internationalized/number": "npm:^3.5.4" + "@internationalized/string": "npm:^3.2.4" + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/form": "npm:^3.0.10" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/spinbutton": "npm:^3.6.9" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/datepicker": "npm:^3.10.3" + "@react-stately/form": "npm:^3.0.6" + "@react-types/button": "npm:^3.10.0" + "@react-types/calendar": "npm:^3.4.10" + "@react-types/datepicker": "npm:^3.8.3" + "@react-types/dialog": "npm:^3.5.13" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/11a6f8249139ca902bad9ec5b52a252ed08635afbcebd4a5516359e6b79487b70a42d000b0cf11c40cf4b4c3cb2e5a34aae9b702c67f0b20420086ac6b6b4233 + languageName: node + linkType: hard + +"@react-aria/dialog@npm:^3.5.19": + version: 3.5.19 + resolution: "@react-aria/dialog@npm:3.5.19" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/overlays": "npm:^3.23.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/dialog": "npm:^3.5.13" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/44eba70f927cbdb003da37021ee6dde363abfa3c179437d83e19c72931c0fab12db20dbd603578d372df534ddce119e22b6908935544ce7a696523aa71b72e2e + languageName: node + linkType: hard + +"@react-aria/dnd@npm:^3.7.4": + version: 3.7.4 + resolution: "@react-aria/dnd@npm:3.7.4" + dependencies: + "@internationalized/string": "npm:^3.2.4" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/live-announcer": "npm:^3.4.0" + "@react-aria/overlays": "npm:^3.23.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/dnd": "npm:^3.4.3" + "@react-types/button": "npm:^3.10.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/02ea56a553a79bc9669e1ed1fbfec5d073e0627262112a215f9c8789c069cb8393f3ee761860e369601939a2f7f15a1462ca1e5ada1a8373bd29a21517e2dfae + languageName: node + linkType: hard + "@react-aria/focus@npm:^3.17.1": version: 3.18.2 resolution: "@react-aria/focus@npm:3.18.2" @@ -10718,6 +11021,100 @@ __metadata: languageName: node linkType: hard +"@react-aria/focus@npm:^3.18.4": + version: 3.18.4 + resolution: "@react-aria/focus@npm:3.18.4" + dependencies: + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + clsx: "npm:^2.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/912cd8a98cbe978240991bec8077c7956ca03ee78cb10152c7a1131a53fb622a5c9b87a4047909f032a7550c37ed9ec50488437a17c761c5c852b721cbaa0bd2 + languageName: node + linkType: hard + +"@react-aria/form@npm:^3.0.10": + version: 3.0.10 + resolution: "@react-aria/form@npm:3.0.10" + dependencies: + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/form": "npm:^3.0.6" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/339b75055b9332e6f082dccf3b3d8c5fb2b5f72ab65a2b1f2e17ac2d2140dde543f18b36501378e7ee4703e4dee49a1dc324ddea2f42614f6c11a827c72856c8 + languageName: node + linkType: hard + +"@react-aria/grid@npm:^3.10.5": + version: 3.10.5 + resolution: "@react-aria/grid@npm:3.10.5" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/live-announcer": "npm:^3.4.0" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/grid": "npm:^3.9.3" + "@react-stately/selection": "npm:^3.17.0" + "@react-types/checkbox": "npm:^3.8.4" + "@react-types/grid": "npm:^3.2.9" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/bbfccebfdc0aafb53d21f5bdc0b3e96ecd2e7d197c277938e6d373fdc560496cd633358aeb03ec3c2b020d2f7a2ddddb2bc3ab2c7fbc4d95a4403fee0dca1a2b + languageName: node + linkType: hard + +"@react-aria/gridlist@npm:^3.9.5": + version: 3.9.5 + resolution: "@react-aria/gridlist@npm:3.9.5" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/grid": "npm:^3.10.5" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/list": "npm:^3.11.0" + "@react-stately/tree": "npm:^3.8.5" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/a36d1028bf1affa6d628326ce1af303dbc2691d14e02f154563fe4177bfae3116fbef69282898a1bfeaf8d1c6c0f04e28ab2335e0cd09506a64a9b1a084987e7 + languageName: node + linkType: hard + +"@react-aria/i18n@npm:^3.12.3": + version: 3.12.3 + resolution: "@react-aria/i18n@npm:3.12.3" + dependencies: + "@internationalized/date": "npm:^3.5.6" + "@internationalized/message": "npm:^3.1.5" + "@internationalized/number": "npm:^3.5.4" + "@internationalized/string": "npm:^3.2.4" + "@react-aria/ssr": "npm:^3.9.6" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/54f111d9a9da68edcb8b821f7c8ead92f0c4d85307dbabee78bc5c89f5a19cdfa406b1e40b7c6f9dc26f7cedce4c9c5a10f8dcdae289e5a404c07b6fdda98aba + languageName: node + linkType: hard + "@react-aria/interactions@npm:^3.21.3, @react-aria/interactions@npm:^3.22.2": version: 3.22.2 resolution: "@react-aria/interactions@npm:3.22.2" @@ -10732,6 +11129,306 @@ __metadata: languageName: node linkType: hard +"@react-aria/interactions@npm:^3.22.4": + version: 3.22.4 + resolution: "@react-aria/interactions@npm:3.22.4" + dependencies: + "@react-aria/ssr": "npm:^3.9.6" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/095d084bd642b47a5cc2a846fa50e0953682ddcad694cc78df344b1f235e292945746692f84d27f465f7ff0117b485c3f5b69f050be196df0c3e7343d3239551 + languageName: node + linkType: hard + +"@react-aria/label@npm:^3.7.12": + version: 3.7.12 + resolution: "@react-aria/label@npm:3.7.12" + dependencies: + "@react-aria/utils": "npm:^3.25.3" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/f93475462e0b962e9447f4c13c6d6ed0862e56c02747f87a81c986f6abaa1226fd215d5a32844e94b0a032cba36f80b3b6e05eed2926553cd30ab631a791bb64 + languageName: node + linkType: hard + +"@react-aria/link@npm:^3.7.6": + version: 3.7.6 + resolution: "@react-aria/link@npm:3.7.6" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/link": "npm:^3.5.8" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/c14fb2abbd5c6b3f539fce11c244aa36f698ba6cb8dd591a0a79b2aedd499d81b1c2c6af03fa29a1531509bb9194483805c30773f17be015fbc9ccbac47b5023 + languageName: node + linkType: hard + +"@react-aria/listbox@npm:^3.13.3, @react-aria/listbox@npm:^3.13.5": + version: 3.13.5 + resolution: "@react-aria/listbox@npm:3.13.5" + dependencies: + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/list": "npm:^3.11.0" + "@react-types/listbox": "npm:^3.5.2" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/631ec625e0c3e9db0fa2701bd288f093128e4b49558129467d34dee0748eb6ac371bf63f716ef62fbdff9e4f5d4a33e0ca1b45aae1213c7f624f799b2f0ec4b6 + languageName: node + linkType: hard + +"@react-aria/live-announcer@npm:^3.4.0": + version: 3.4.0 + resolution: "@react-aria/live-announcer@npm:3.4.0" + dependencies: + "@swc/helpers": "npm:^0.5.0" + checksum: 10/d5fdd048eaaf2d048881d5b8fec369e558c039a4461908b10a28f2d7dc39b94b3b7b1eb385f9e577f7cf6e5f8e0e233facfbed0da98df3abffb1d74b3c8beb89 + languageName: node + linkType: hard + +"@react-aria/menu@npm:^3.15.5": + version: 3.15.5 + resolution: "@react-aria/menu@npm:3.15.5" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/overlays": "npm:^3.23.4" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/menu": "npm:^3.8.3" + "@react-stately/tree": "npm:^3.8.5" + "@react-types/button": "npm:^3.10.0" + "@react-types/menu": "npm:^3.9.12" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/6714b959f34497e6377732103b83dabb6011cb95df61fa6a415a9737d8fad70a8e419b60ab4f4f553b5f034fba9f07104b0e6f7756c5cec64048e7d9ef743ac0 + languageName: node + linkType: hard + +"@react-aria/meter@npm:^3.4.17": + version: 3.4.17 + resolution: "@react-aria/meter@npm:3.4.17" + dependencies: + "@react-aria/progress": "npm:^3.4.17" + "@react-types/meter": "npm:^3.4.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/4d485940733dfb2f2a3fc77ef3c98ec78a37fcbd258cc343444616591998c72ef473dfbc3f3111b845de41f9cfc211a2636c579b122844226f39dca0072c5f73 + languageName: node + linkType: hard + +"@react-aria/numberfield@npm:^3.11.8": + version: 3.11.8 + resolution: "@react-aria/numberfield@npm:3.11.8" + dependencies: + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/spinbutton": "npm:^3.6.9" + "@react-aria/textfield": "npm:^3.14.10" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/form": "npm:^3.0.6" + "@react-stately/numberfield": "npm:^3.9.7" + "@react-types/button": "npm:^3.10.0" + "@react-types/numberfield": "npm:^3.8.6" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/2709e946298ee4a3be1802fddc87921e96be23c058c1fd50564a7c13e96779507de0ad005a3b51aefe41bcf0abf115ebb15f3be69887db4151bf2bcf7efec0eb + languageName: node + linkType: hard + +"@react-aria/overlays@npm:^3.23.2, @react-aria/overlays@npm:^3.23.4": + version: 3.23.4 + resolution: "@react-aria/overlays@npm:3.23.4" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/ssr": "npm:^3.9.6" + "@react-aria/utils": "npm:^3.25.3" + "@react-aria/visually-hidden": "npm:^3.8.17" + "@react-stately/overlays": "npm:^3.6.11" + "@react-types/button": "npm:^3.10.0" + "@react-types/overlays": "npm:^3.8.10" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/6f641a1f7c1976758dd062c4345d2e8882575e2b645a4e4693f051699bae48c48979db3539e77c387aee64600d7a555b94687cd01449094cf875ce80ec6aa2ed + languageName: node + linkType: hard + +"@react-aria/progress@npm:^3.4.17": + version: 3.4.17 + resolution: "@react-aria/progress@npm:3.4.17" + dependencies: + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/progress": "npm:^3.5.7" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/72c97dcd5b0fbbf5bde0d4282887d6621f7ad31f1895c93f77528ad54c673122a93aeea555ef517eb58dd3ae5e57310d15d5cd722870b4797a97673ebd449c95 + languageName: node + linkType: hard + +"@react-aria/radio@npm:^3.10.9": + version: 3.10.9 + resolution: "@react-aria/radio@npm:3.10.9" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/form": "npm:^3.0.10" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/radio": "npm:^3.10.8" + "@react-types/radio": "npm:^3.8.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/5254109a582d97eb9505c98b256de9b45ef019fb8a0e397def279be05ed539b69ea3c0e7842896df1574d576600a5168ae6e5a1178d8ee3ea0ba600be33ec143 + languageName: node + linkType: hard + +"@react-aria/searchfield@npm:^3.7.10": + version: 3.7.10 + resolution: "@react-aria/searchfield@npm:3.7.10" + dependencies: + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/textfield": "npm:^3.14.10" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/searchfield": "npm:^3.5.7" + "@react-types/button": "npm:^3.10.0" + "@react-types/searchfield": "npm:^3.5.9" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/1ec6fc47c85b7ca8677683bcc54a88c471314931a876040425ec16e1f92293e635f55ee638f885592927b4c2d7f7fb797c6272511cca526d3fcfbb0c64660ba0 + languageName: node + linkType: hard + +"@react-aria/select@npm:^3.14.11": + version: 3.14.11 + resolution: "@react-aria/select@npm:3.14.11" + dependencies: + "@react-aria/form": "npm:^3.0.10" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/listbox": "npm:^3.13.5" + "@react-aria/menu": "npm:^3.15.5" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/utils": "npm:^3.25.3" + "@react-aria/visually-hidden": "npm:^3.8.17" + "@react-stately/select": "npm:^3.6.8" + "@react-types/button": "npm:^3.10.0" + "@react-types/select": "npm:^3.9.7" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/580a72f0d5738d30fbf31c6f4e56b69f5a0f1c5ef56d5da069c128540740ecc88b2ef285829ad3bedf7a2fed76255250fd54c94d46e705667ccc8f6d045e0553 + languageName: node + linkType: hard + +"@react-aria/selection@npm:^3.20.1": + version: 3.20.1 + resolution: "@react-aria/selection@npm:3.20.1" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/selection": "npm:^3.17.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/38693581ab743fb4e23a7d16a370330b5462c42119ae38862623a7ace2466f74f080dee50df9f3d1ab4d83c58a3ac17a5197061e5a31b70d6f14993109c468e8 + languageName: node + linkType: hard + +"@react-aria/separator@npm:^3.4.3": + version: 3.4.3 + resolution: "@react-aria/separator@npm:3.4.3" + dependencies: + "@react-aria/utils": "npm:^3.25.3" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/c30b4386283dd7658377952a708b27c103c8c1f9077bdca3f67e4458be6dd327d8884f2036442ef8cebc698a4444be248bcc0ace202db9a2c30d44ce1b3ee5db + languageName: node + linkType: hard + +"@react-aria/slider@npm:^3.7.13": + version: 3.7.13 + resolution: "@react-aria/slider@npm:3.7.13" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/slider": "npm:^3.5.8" + "@react-types/shared": "npm:^3.25.0" + "@react-types/slider": "npm:^3.7.6" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/48f666cd6171daa5c6913409a0ef9ee94fdb5471af5b03f9960fd659308e586dafc821251c7b861595e44d5b9222796ddb7783e239ddc039f9d7f944309f454f + languageName: node + linkType: hard + +"@react-aria/spinbutton@npm:^3.6.9": + version: 3.6.9 + resolution: "@react-aria/spinbutton@npm:3.6.9" + dependencies: + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/live-announcer": "npm:^3.4.0" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/button": "npm:^3.10.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/98ed79bfd2b502a91833f5546e665c514aec3367d9cc902d32c7c6a18be4b332e905322f9916e39e64aea2cbf08da368faee6916991a861c6719ad4b7b58951f + languageName: node + linkType: hard + "@react-aria/ssr@npm:^3.9.5": version: 3.9.5 resolution: "@react-aria/ssr@npm:3.9.5" @@ -10743,6 +11440,151 @@ __metadata: languageName: node linkType: hard +"@react-aria/ssr@npm:^3.9.6": + version: 3.9.6 + resolution: "@react-aria/ssr@npm:3.9.6" + dependencies: + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/ea6b290346ce1e119ed9233fc0e34693d52ab9dc2509f07ab10710409b89484a544b7f26c1438802e97f3fb634844ae54638850cdd95caca0d1f5571781bf982 + languageName: node + linkType: hard + +"@react-aria/switch@npm:^3.6.9": + version: 3.6.9 + resolution: "@react-aria/switch@npm:3.6.9" + dependencies: + "@react-aria/toggle": "npm:^3.10.9" + "@react-stately/toggle": "npm:^3.7.8" + "@react-types/shared": "npm:^3.25.0" + "@react-types/switch": "npm:^3.5.6" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/ce627d19e48db97fbf7f5201e0d8c8108aa5f59714ca44e852902a1e6483e5f0b929e6523534ca9d5fdefbbf3c36414cb9c52a914ce13e8bd44d2f818bd4c853 + languageName: node + linkType: hard + +"@react-aria/table@npm:^3.15.5": + version: 3.15.5 + resolution: "@react-aria/table@npm:3.15.5" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/grid": "npm:^3.10.5" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/live-announcer": "npm:^3.4.0" + "@react-aria/utils": "npm:^3.25.3" + "@react-aria/visually-hidden": "npm:^3.8.17" + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/flags": "npm:^3.0.4" + "@react-stately/table": "npm:^3.12.3" + "@react-types/checkbox": "npm:^3.8.4" + "@react-types/grid": "npm:^3.2.9" + "@react-types/shared": "npm:^3.25.0" + "@react-types/table": "npm:^3.10.2" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/ef229d6d6319c8cfa135c56a7eeb6915e72a3f9d05dc49f51222cf79990976fdb18529b3bf3038aca82ac71d75058b0b09507d70dbfa0fd4166afa3671e5f19a + languageName: node + linkType: hard + +"@react-aria/tabs@npm:^3.9.7": + version: 3.9.7 + resolution: "@react-aria/tabs@npm:3.9.7" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/tabs": "npm:^3.6.10" + "@react-types/shared": "npm:^3.25.0" + "@react-types/tabs": "npm:^3.3.10" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/17837148337d68cb1b2468b76967b809a3c085c19da1dab3e7d701e585499e66d926f9ebec410de2c6a925b37d232871412b36c657fa54c4e48048b63ef4ad74 + languageName: node + linkType: hard + +"@react-aria/tag@npm:^3.4.7": + version: 3.4.7 + resolution: "@react-aria/tag@npm:3.4.7" + dependencies: + "@react-aria/gridlist": "npm:^3.9.5" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/list": "npm:^3.11.0" + "@react-types/button": "npm:^3.10.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/1624d1b6f44bf350dc46e0ee8a1550123ce73c4a2508e325e2eee4603bc5cb13d8f8d8a997be12c34756b47154d4a1e60281d572615a97507198bcd65d80b870 + languageName: node + linkType: hard + +"@react-aria/textfield@npm:^3.14.10": + version: 3.14.10 + resolution: "@react-aria/textfield@npm:3.14.10" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/form": "npm:^3.0.10" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/form": "npm:^3.0.6" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/shared": "npm:^3.25.0" + "@react-types/textfield": "npm:^3.9.7" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/bac8bb98030ff889bee1d57abbfd073b31a23b275e6aea0a0f27b829e37eb041c836873daa507d4def8abc233a24bf56bf9aad6bdfb9a21412e4a3e7b47e1a34 + languageName: node + linkType: hard + +"@react-aria/toggle@npm:^3.10.9": + version: 3.10.9 + resolution: "@react-aria/toggle@npm:3.10.9" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/toggle": "npm:^3.7.8" + "@react-types/checkbox": "npm:^3.8.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/51adc665376a36cf0ccf2250d7ef1c5c4c97073c5e9dc987b7e0710a1c3c7916e668d2fca6e84fff60a16b586206260b6fc27e3267e65ae4fd670b4ca70643d1 + languageName: node + linkType: hard + +"@react-aria/tooltip@npm:^3.7.9": + version: 3.7.9 + resolution: "@react-aria/tooltip@npm:3.7.9" + dependencies: + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-stately/tooltip": "npm:^3.4.13" + "@react-types/shared": "npm:^3.25.0" + "@react-types/tooltip": "npm:^3.4.12" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/2e043bc87a8aa4e95e16c8d0ec98812cedd6b6e32099700b1fd2acdea74cf357c1476e1f5bf17c3b1bd45642089b2a09a0f1fb245544023146226df665fae594 + languageName: node + linkType: hard + "@react-aria/utils@npm:^3.25.2": version: 3.25.2 resolution: "@react-aria/utils@npm:3.25.2" @@ -10758,6 +11600,418 @@ __metadata: languageName: node linkType: hard +"@react-aria/utils@npm:^3.25.3": + version: 3.25.3 + resolution: "@react-aria/utils@npm:3.25.3" + dependencies: + "@react-aria/ssr": "npm:^3.9.6" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + clsx: "npm:^2.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/86aed35da5cb0d48d949e40bf8226d5a6d6c92a8cdc60e3e12d524d1f3cc91ab6b54c5e1642823773cbb889fb61af7da22e89488b704b56fc5f4d8d59da7519b + languageName: node + linkType: hard + +"@react-aria/visually-hidden@npm:^3.8.17": + version: 3.8.17 + resolution: "@react-aria/visually-hidden@npm:3.8.17" + dependencies: + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/utils": "npm:^3.25.3" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/d9cdc97d80e750f4582cc3f2bcc0bde12b6c70fbd408ca9ebba2ef654ab27b0181cdd072a8b30ae36f14d9ec16a79c2d38d7cbe834c96fad693daebc7be41616 + languageName: node + linkType: hard + +"@react-icons/all-files@npm:^4.1.0": + version: 4.1.0 + resolution: "@react-icons/all-files@npm:4.1.0" + peerDependencies: + react: "*" + checksum: 10/56252b205bd5960e605e9de9a9eacd4b2a5cf9f2e6156f4a1803eee502a7d1749e479a56d68dfc5514decc06cf4ffdf11e36c71359a984892c54cfc4a4604bc8 + languageName: node + linkType: hard + +"@react-native-async-storage/async-storage@npm:^1.17.7": + version: 1.24.0 + resolution: "@react-native-async-storage/async-storage@npm:1.24.0" + dependencies: + merge-options: "npm:^3.0.4" + peerDependencies: + react-native: ^0.0.0-0 || >=0.60 <1.0 + checksum: 10/5a6b7ac8bd7a9e537a53a3f2301530c284fd885a45ce4a4e0014859bc0f7c89bee5c4b5a6b3740b8d83751561159b237474d18f32fad75ea7d56d4ddb2180d91 + languageName: node + linkType: hard + +"@react-stately/calendar@npm:^3.5.5": + version: 3.5.5 + resolution: "@react-stately/calendar@npm:3.5.5" + dependencies: + "@internationalized/date": "npm:^3.5.6" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/calendar": "npm:^3.4.10" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/97d436bcedfff4975c74da301ad71fd63b329da5cf2ffe4ed80c71539decaa47000e24f11de2e6bf78378cfa68501aa4320448f686cdde6f7b1d7d6ecddc7b97 + languageName: node + linkType: hard + +"@react-stately/checkbox@npm:^3.6.9": + version: 3.6.9 + resolution: "@react-stately/checkbox@npm:3.6.9" + dependencies: + "@react-stately/form": "npm:^3.0.6" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/checkbox": "npm:^3.8.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/45fe2277fc40ab457d374de150376f3093868153aeb144c3d7810ca2cfab15f5edb2adba7ed213eb3ee4e038cacb0607ce1f67e5066eaf9cd69b8b59577c8339 + languageName: node + linkType: hard + +"@react-stately/collections@npm:^3.11.0": + version: 3.11.0 + resolution: "@react-stately/collections@npm:3.11.0" + dependencies: + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/241bedc27fb6f156d21642ab6a5ecc67b000569c6da58b93e693c1d2c57b27eaf58e71aabab8b5041e023d67ea8cbdf771e8a5bea43b86f1904ff1f4682a49da + languageName: node + linkType: hard + +"@react-stately/color@npm:^3.8.0": + version: 3.8.0 + resolution: "@react-stately/color@npm:3.8.0" + dependencies: + "@internationalized/number": "npm:^3.5.4" + "@internationalized/string": "npm:^3.2.4" + "@react-aria/i18n": "npm:^3.12.3" + "@react-stately/form": "npm:^3.0.6" + "@react-stately/numberfield": "npm:^3.9.7" + "@react-stately/slider": "npm:^3.5.8" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/color": "npm:^3.0.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/9049c3222707e3699fb2a22ee6d6be89e5b9c4b784a46fb9d4b9d22c9e09974905902d37a228bc344efed8147d40dd5bbc59875cc54ccb92d6d2d4827e89281e + languageName: node + linkType: hard + +"@react-stately/combobox@npm:^3.10.0": + version: 3.10.0 + resolution: "@react-stately/combobox@npm:3.10.0" + dependencies: + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/form": "npm:^3.0.6" + "@react-stately/list": "npm:^3.11.0" + "@react-stately/overlays": "npm:^3.6.11" + "@react-stately/select": "npm:^3.6.8" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/combobox": "npm:^3.13.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/8897052576e311c522db246f11f950e62f19d41f4739c4992c9485f1c4ee0e1d7675421c6f0d982f3a5ae5c7de874d2050749ed44e08a316405a5fa960e993ac + languageName: node + linkType: hard + +"@react-stately/data@npm:^3.11.7": + version: 3.11.7 + resolution: "@react-stately/data@npm:3.11.7" + dependencies: + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/31ba0cb0d1363370c7b22c3bb152e4556ff93e72dca2a4cb042e4750c32a835a15c42d262bde4e0ab4e9ddee6c0c5e501a0ebcb032d476db16fd63407f9504b8 + languageName: node + linkType: hard + +"@react-stately/datepicker@npm:^3.10.3": + version: 3.10.3 + resolution: "@react-stately/datepicker@npm:3.10.3" + dependencies: + "@internationalized/date": "npm:^3.5.6" + "@internationalized/string": "npm:^3.2.4" + "@react-stately/form": "npm:^3.0.6" + "@react-stately/overlays": "npm:^3.6.11" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/datepicker": "npm:^3.8.3" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/f3435997c14dc8466ecac63171412296021323c2b935795b067bddb165afcccb06ef85f425315aa3b0d2073b9b26549d33cc0e97d7416368c45b955ecc4228ef + languageName: node + linkType: hard + +"@react-stately/dnd@npm:^3.4.3": + version: 3.4.3 + resolution: "@react-stately/dnd@npm:3.4.3" + dependencies: + "@react-stately/selection": "npm:^3.17.0" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/ecfadefa4be8e914af040a376c64e1a2945f3a53f9374d4446615c5a33e81e0f1a2732c177a4fac37fec44ee6acb66338757f7ebbb2d6c20d643fdcc92e6366a + languageName: node + linkType: hard + +"@react-stately/flags@npm:^3.0.4": + version: 3.0.4 + resolution: "@react-stately/flags@npm:3.0.4" + dependencies: + "@swc/helpers": "npm:^0.5.0" + checksum: 10/de1004c2e670a4876d4cb7beae9058e1e1ded410c4b51b11d076ad66feb83b0461321617fce488143330d17c0ab468641ddbd3e9883ed4371ed4578d61834bdf + languageName: node + linkType: hard + +"@react-stately/form@npm:^3.0.6": + version: 3.0.6 + resolution: "@react-stately/form@npm:3.0.6" + dependencies: + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/9bd911e7d23ae334a025a0000f9b4faa4e86384c26f07534acfc86e467d2d9507b89f6bf54f54f3aad661cf97d86398ef2bc5b0bd69f2a8602b42b7f5bded78c + languageName: node + linkType: hard + +"@react-stately/grid@npm:^3.9.3": + version: 3.9.3 + resolution: "@react-stately/grid@npm:3.9.3" + dependencies: + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/selection": "npm:^3.17.0" + "@react-types/grid": "npm:^3.2.9" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/c2dcd9cda30c4bc6ae7da0f612c9d288734e5ab58be674f1e4983d6dd5e0def0cac4be3a759b4974a49b97476d9119c92b79fba8b40576696f370fc30b7b684f + languageName: node + linkType: hard + +"@react-stately/list@npm:^3.11.0": + version: 3.11.0 + resolution: "@react-stately/list@npm:3.11.0" + dependencies: + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/selection": "npm:^3.17.0" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/1ba76430c5c112311fc07a6861fd7046562a60d25676d2a14dfb76cb9a00d0673b241ef11093551f819276b3dc7261d2fd7ffaf2b61c8fd634b03672c4443861 + languageName: node + linkType: hard + +"@react-stately/menu@npm:^3.8.3": + version: 3.8.3 + resolution: "@react-stately/menu@npm:3.8.3" + dependencies: + "@react-stately/overlays": "npm:^3.6.11" + "@react-types/menu": "npm:^3.9.12" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/67c73ca6adfaa24a836a148a7b57cbe348aac5b865cd58525d1fc41f2abd17da0540565ffba509c216de1008d8b24ae87a198aa8057b34825a0668090e2d0d27 + languageName: node + linkType: hard + +"@react-stately/numberfield@npm:^3.9.7": + version: 3.9.7 + resolution: "@react-stately/numberfield@npm:3.9.7" + dependencies: + "@internationalized/number": "npm:^3.5.4" + "@react-stately/form": "npm:^3.0.6" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/numberfield": "npm:^3.8.6" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/da4d0493d5a8e76af9a4d66ec477999fd9e26acc07dfc025494425ccdf3c5d72b104cfa9933b549e5c47b389dc7dfdc00ba34703d4d88da3e2171ddc44508385 + languageName: node + linkType: hard + +"@react-stately/overlays@npm:^3.6.11": + version: 3.6.11 + resolution: "@react-stately/overlays@npm:3.6.11" + dependencies: + "@react-stately/utils": "npm:^3.10.4" + "@react-types/overlays": "npm:^3.8.10" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/98190b4b0ced5c94d924cf97b5d43a6e28f68aa44de7bb789c20354f30f00309c86089fb6948b5ec9d09f01605b5a412fb246545b7ee9bc34e3183e7261a2805 + languageName: node + linkType: hard + +"@react-stately/radio@npm:^3.10.8": + version: 3.10.8 + resolution: "@react-stately/radio@npm:3.10.8" + dependencies: + "@react-stately/form": "npm:^3.0.6" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/radio": "npm:^3.8.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/a750896af85433719281814d394378b612465d3078488b8b541812260a795933699b1d0ca3e71fbb68003aff203916670370ef0de6895cc0cce0b9d8c6e85b84 + languageName: node + linkType: hard + +"@react-stately/searchfield@npm:^3.5.7": + version: 3.5.7 + resolution: "@react-stately/searchfield@npm:3.5.7" + dependencies: + "@react-stately/utils": "npm:^3.10.4" + "@react-types/searchfield": "npm:^3.5.9" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/bcd6f7fcb644bf0202c365f6017faf0e0cea1320a81b7e44d1c4faa49e541a8911d027da695bb6870718cbfb70a6028bd870d69eb6cc91178bccb8232d25741e + languageName: node + linkType: hard + +"@react-stately/select@npm:^3.6.8": + version: 3.6.8 + resolution: "@react-stately/select@npm:3.6.8" + dependencies: + "@react-stately/form": "npm:^3.0.6" + "@react-stately/list": "npm:^3.11.0" + "@react-stately/overlays": "npm:^3.6.11" + "@react-types/select": "npm:^3.9.7" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/fe79ed549db5ef5fbccf4312a90cd7a43e62e2a6ba7c183cb2b3c11dd6f17ba81c7c3f44eac42be67e7ebdf4eecf7a33513d5ef2badb9c13b22552da55b43df7 + languageName: node + linkType: hard + +"@react-stately/selection@npm:^3.17.0": + version: 3.17.0 + resolution: "@react-stately/selection@npm:3.17.0" + dependencies: + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/5ecb13294b85c0b6fa1fda1665fd81105601ca3976672a5faae95a67ab484a2538b913be68cb924ca0be0b010c9eb7bba139c34da7c61e3f581d2054029b9978 + languageName: node + linkType: hard + +"@react-stately/slider@npm:^3.5.8": + version: 3.5.8 + resolution: "@react-stately/slider@npm:3.5.8" + dependencies: + "@react-stately/utils": "npm:^3.10.4" + "@react-types/shared": "npm:^3.25.0" + "@react-types/slider": "npm:^3.7.6" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/c7c51f7d153b70d785b5ced5e918fd0f72278a4f288bb78c62da51a3ef0277db50f365803a1c7d9b57071cb99315a17fc9796b8aca608682711adb288c888dc6 + languageName: node + linkType: hard + +"@react-stately/table@npm:^3.12.3": + version: 3.12.3 + resolution: "@react-stately/table@npm:3.12.3" + dependencies: + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/flags": "npm:^3.0.4" + "@react-stately/grid": "npm:^3.9.3" + "@react-stately/selection": "npm:^3.17.0" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/grid": "npm:^3.2.9" + "@react-types/shared": "npm:^3.25.0" + "@react-types/table": "npm:^3.10.2" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/c82b4153c9205101b63e7974c72238997d05345b6c8cd15c4bb5484174df56f85128c3b4fe731734e0720e4ad98a3e8f1c933831c1cf7b5e24ed20c97229cd1b + languageName: node + linkType: hard + +"@react-stately/tabs@npm:^3.6.10": + version: 3.6.10 + resolution: "@react-stately/tabs@npm:3.6.10" + dependencies: + "@react-stately/list": "npm:^3.11.0" + "@react-types/shared": "npm:^3.25.0" + "@react-types/tabs": "npm:^3.3.10" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/b5b2c78300fdaa6bd5a94964bd9e44e2aad39070170ac7202f4c78d327e174b8c1c4e051c7edc794c729e0430ab57e76a1e510942efe1adcf7477f2ecd40004c + languageName: node + linkType: hard + +"@react-stately/toggle@npm:^3.7.8": + version: 3.7.8 + resolution: "@react-stately/toggle@npm:3.7.8" + dependencies: + "@react-stately/utils": "npm:^3.10.4" + "@react-types/checkbox": "npm:^3.8.4" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/65c662eab8a57e09228864399e46cbca3fdab1e7fea8c994431441bfc87a6d40c6897473ec48145cb9220067a7bf058d22d0f5257f2d49bfc6bc981b59a3b3ef + languageName: node + linkType: hard + +"@react-stately/tooltip@npm:^3.4.13": + version: 3.4.13 + resolution: "@react-stately/tooltip@npm:3.4.13" + dependencies: + "@react-stately/overlays": "npm:^3.6.11" + "@react-types/tooltip": "npm:^3.4.12" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/8c2a44b20dad82bb10e7b8eb26c5844dd842468ac8e39e17c511e4122e1ee9a8ba63b66adc14a643330a0f08f6407b0e4a3896a05d4c71b170624006d67b55ac + languageName: node + linkType: hard + +"@react-stately/tree@npm:^3.8.5": + version: 3.8.5 + resolution: "@react-stately/tree@npm:3.8.5" + dependencies: + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/selection": "npm:^3.17.0" + "@react-stately/utils": "npm:^3.10.4" + "@react-types/shared": "npm:^3.25.0" + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/a853a272b53b6159c98118171cff2088ab5b262860106d4f0867d38c13c1ca7ab3f4b2c6c335b79cb07e045ffa9ac81c3061e46914150a460839e2b1dbd695ea + languageName: node + linkType: hard + "@react-stately/utils@npm:^3.10.3": version: 3.10.3 resolution: "@react-stately/utils@npm:3.10.3" @@ -10769,6 +12023,235 @@ __metadata: languageName: node linkType: hard +"@react-stately/utils@npm:^3.10.4": + version: 3.10.4 + resolution: "@react-stately/utils@npm:3.10.4" + dependencies: + "@swc/helpers": "npm:^0.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/8a56b4d0cf8d5a7a692d6f94ffff63feac2d7078fbc5642b94b0afcaaf7c8f7f4682cfe546f98265034c52576c198be5502cff3f9b145137884e50eb9ffb96d5 + languageName: node + linkType: hard + +"@react-types/breadcrumbs@npm:^3.7.8": + version: 3.7.8 + resolution: "@react-types/breadcrumbs@npm:3.7.8" + dependencies: + "@react-types/link": "npm:^3.5.8" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/5a085a3e6fa8486ce39451c9698c1fc9d1d33526db78e06cdf8e38a02e6c9765c232ea8b7bdb2a5462d2917ccef62b570dc04cb825204747c29d43af30af776c + languageName: node + linkType: hard + +"@react-types/button@npm:^3.10.0": + version: 3.10.0 + resolution: "@react-types/button@npm:3.10.0" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/13973108d935e81a9e852bdc3a530a26a4cacc4a7ec37f1dde48202be0545066a71f4d7c476806d7911e91b2b9193c79f4e89dc616280b74db37cec3dd749fea + languageName: node + linkType: hard + +"@react-types/calendar@npm:^3.4.10": + version: 3.4.10 + resolution: "@react-types/calendar@npm:3.4.10" + dependencies: + "@internationalized/date": "npm:^3.5.6" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/0f90aa3394d160b09e93ce0acb66a226c1d4211e818f69b5ac0d9faf01a6729085d1fcad83566ddf425d18308c05f70e56007429ef2060f0bf4f595d3c28f066 + languageName: node + linkType: hard + +"@react-types/checkbox@npm:^3.8.4": + version: 3.8.4 + resolution: "@react-types/checkbox@npm:3.8.4" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/15eb9951dc7ab24cb9f95b500df09d17e1c675f33121b589f778508f8c803f6f29bf6744441c48c1dc5e6d7755feda83e9464832b6771b1662757b564bfb80dd + languageName: node + linkType: hard + +"@react-types/color@npm:^3.0.0": + version: 3.0.0 + resolution: "@react-types/color@npm:3.0.0" + dependencies: + "@react-types/shared": "npm:^3.25.0" + "@react-types/slider": "npm:^3.7.6" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/ca56d14fbb70e43b67aa696abeeb1aba4df44b09081227b49bc476342119b5b374be4bf25f753799f28cb994f0dffc2abecea16f9c940fa091007416e485c81b + languageName: node + linkType: hard + +"@react-types/combobox@npm:^3.13.0": + version: 3.13.0 + resolution: "@react-types/combobox@npm:3.13.0" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/e69ae7389ff7b38aca0e6cee6c854b2f343c9a18cbec03a3773d59f2e148a97b3948abd49ed1f79c8d62e36f9f067dbd08c42c1e871090cab4dc2c421c3b6fc9 + languageName: node + linkType: hard + +"@react-types/datepicker@npm:^3.8.3": + version: 3.8.3 + resolution: "@react-types/datepicker@npm:3.8.3" + dependencies: + "@internationalized/date": "npm:^3.5.6" + "@react-types/calendar": "npm:^3.4.10" + "@react-types/overlays": "npm:^3.8.10" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/15e4c21569f69be8de94ea3831c4fc7e1c62d7b704cfa4970da32677a152e20c13dd47dd4bd969e5598a9e100c572031159445100c4d08b7dcf92dcb932228db + languageName: node + linkType: hard + +"@react-types/dialog@npm:^3.5.13": + version: 3.5.13 + resolution: "@react-types/dialog@npm:3.5.13" + dependencies: + "@react-types/overlays": "npm:^3.8.10" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/c7ee923c326b7377660400ec794ef98330388250aa32df23f5c22a63e483bcad221283de26181bf2a5cab2c20d517f528bf351c9786ac9fece3e3726e4ae07c3 + languageName: node + linkType: hard + +"@react-types/grid@npm:^3.2.9": + version: 3.2.9 + resolution: "@react-types/grid@npm:3.2.9" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/db85cb813a662de2673d14119036aa437c60358436bb9099c46706485fad47d1f7aa9df69131ec7ce122f78a684f9082a2aa03ac37d0370069ffd1bec1c11c3e + languageName: node + linkType: hard + +"@react-types/link@npm:^3.5.8": + version: 3.5.8 + resolution: "@react-types/link@npm:3.5.8" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/3e20323be7cebfcd10cde779a884e1bea3cd5068cbd6eff60f62cb03718fb88cb024c14d0f79c86fe41fd790e10849d405b0115a68a08ffcc9715f303cc4563d + languageName: node + linkType: hard + +"@react-types/listbox@npm:^3.5.2": + version: 3.5.2 + resolution: "@react-types/listbox@npm:3.5.2" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/5aadf4665538613c8fc60dab1fe58ff02eff9f9b653fc6df772c94df17170cc7b31dd869e9293bdd437fccf168124d165162080084d32c2c01cd51f64ec2e3fd + languageName: node + linkType: hard + +"@react-types/menu@npm:^3.9.12": + version: 3.9.12 + resolution: "@react-types/menu@npm:3.9.12" + dependencies: + "@react-types/overlays": "npm:^3.8.10" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/a6276115c5da08c63b9641b985c366435e69847af680ace5fef32ce2ca6eab9669db5cc8f5a20e01377a54f17392a95864ee40831b13b2258ed6d43bcc1398f5 + languageName: node + linkType: hard + +"@react-types/meter@npm:^3.4.4": + version: 3.4.4 + resolution: "@react-types/meter@npm:3.4.4" + dependencies: + "@react-types/progress": "npm:^3.5.7" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/cd05949d2ff361dedc576369d7a38688ac8be62b237881c8cf0314825bae5d923814c81dc83922b76d54c46936571ad7da186d6dcfeb37fb5425c119f77d8f21 + languageName: node + linkType: hard + +"@react-types/numberfield@npm:^3.8.6": + version: 3.8.6 + resolution: "@react-types/numberfield@npm:3.8.6" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/47ca14f31d970bad272ff9a2d06ee722d5ca7d5d5d60e47277040f9bbf0cb6b9835c234387e29446148c28df0a0d747c4ddb56d8a53c510b63a91547f808cf51 + languageName: node + linkType: hard + +"@react-types/overlays@npm:^3.8.10": + version: 3.8.10 + resolution: "@react-types/overlays@npm:3.8.10" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/2e8edf37f75df2884a280cbd25a3798d24b93669b8b2b606cadacaf40f605f63e437749cea28861fabecd78293302ac39108f4e65cedd412c474e92be9895561 + languageName: node + linkType: hard + +"@react-types/progress@npm:^3.5.7": + version: 3.5.7 + resolution: "@react-types/progress@npm:3.5.7" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/610cfd88946e566dd3cfe745a4b4f90b6216b6df6c55d19aab9e1c0c099e062baa7cf221c79e7fd1f46a7d4604df71bcd8914d8ec5e79b2694e9e605a0115dcc + languageName: node + linkType: hard + +"@react-types/radio@npm:^3.8.4": + version: 3.8.4 + resolution: "@react-types/radio@npm:3.8.4" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/b00f2a97ef56b4860b2bb196433d78a068031427b4f99d2350f45ee444f49de7cad646220dae53ed1126555fe715050deefc67f9728e264a27b910b274f4a6b3 + languageName: node + linkType: hard + +"@react-types/searchfield@npm:^3.5.9": + version: 3.5.9 + resolution: "@react-types/searchfield@npm:3.5.9" + dependencies: + "@react-types/shared": "npm:^3.25.0" + "@react-types/textfield": "npm:^3.9.7" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/b5d53113f7cd6d55c69350345e042412fcd31fbd69ea9eff3a0b74e9575b9a50b6e3b9f6443e760bfce73c6950c513a6b0c43a6b5cbffa9b0d5f8a29236ee56d + languageName: node + linkType: hard + +"@react-types/select@npm:^3.9.7": + version: 3.9.7 + resolution: "@react-types/select@npm:3.9.7" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/c53a9408e22cc7c622955a11a80339c412564720d08d621bbd5a28953731eb8afda8d0bb56b5242f8f94e99c3264b913f1228ec9454c3b637f4e8d2374e00f99 + languageName: node + linkType: hard + "@react-types/shared@npm:^3.24.1": version: 3.24.1 resolution: "@react-types/shared@npm:3.24.1" @@ -10778,6 +12261,83 @@ __metadata: languageName: node linkType: hard +"@react-types/shared@npm:^3.25.0": + version: 3.25.0 + resolution: "@react-types/shared@npm:3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/fa31eb6153c223210c2eee46934a63b922917bcde0ee583f2cfe59675db122c10e1cbae6549b1fea4284391fdbeca6888b36e9dc797231ad4a76def01490aea5 + languageName: node + linkType: hard + +"@react-types/slider@npm:^3.7.6": + version: 3.7.6 + resolution: "@react-types/slider@npm:3.7.6" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/01d15d8f946188cc45ddf63f3300c21aebaea015c88bd8883d64e740ead54873407fdbfb3dcc8b3c5198a40355c5c0027c006d41123220189783d1ac2193eee6 + languageName: node + linkType: hard + +"@react-types/switch@npm:^3.5.6": + version: 3.5.6 + resolution: "@react-types/switch@npm:3.5.6" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/1741c5c98686847a3a9c2f07bb47f409c0f35d32c3b8759c418ddee24a2e02e8f9a7f49690f8dfe304e811af191ea3d97bb68a0dd9355bfb85e95140dd8433c8 + languageName: node + linkType: hard + +"@react-types/table@npm:^3.10.2": + version: 3.10.2 + resolution: "@react-types/table@npm:3.10.2" + dependencies: + "@react-types/grid": "npm:^3.2.9" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/0fb7b38c56809500e0cc7a958965b54fb2d5172da3df28b994f24630c3aa931fef0993f15d3ec1c2716b910435aeb90c210abd27badec2de1f77ecdf6d03ab16 + languageName: node + linkType: hard + +"@react-types/tabs@npm:^3.3.10": + version: 3.3.10 + resolution: "@react-types/tabs@npm:3.3.10" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/4b2fd7d6f978067a7a603c83ed7292ff12c47b89a9c556da2cdc10c8a185868311318d4f6f7703e4cf4234a5cd2cb882ec48e4771fb61b2dfa00f47c0474949f + languageName: node + linkType: hard + +"@react-types/textfield@npm:^3.9.7": + version: 3.9.7 + resolution: "@react-types/textfield@npm:3.9.7" + dependencies: + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/73fc23fd7a83f0a44add2bdb1c10e1361a3aa4ee1d5e1dfb6b63f661140b58052d8dce735e79c0a2c9c66db596dfe9c312ee70ebcd100d2c8b102fbe6b8199ad + languageName: node + linkType: hard + +"@react-types/tooltip@npm:^3.4.12": + version: 3.4.12 + resolution: "@react-types/tooltip@npm:3.4.12" + dependencies: + "@react-types/overlays": "npm:^3.8.10" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/3321d0c938a46343f2961b53d9533ab62ea5fc1c4b199e7e1740dab2ba59765bc87ed04903819596f61bdaa07e2a1c8c29d6f6b34e3d495c0f43d656a7ac8d18 + languageName: node + linkType: hard + "@resolver-engine/core@npm:^0.3.3": version: 0.3.3 resolution: "@resolver-engine/core@npm:0.3.3" @@ -11063,6 +12623,13 @@ __metadata: languageName: node linkType: hard +"@rtsao/scc@npm:^1.1.0": + version: 1.1.0 + resolution: "@rtsao/scc@npm:1.1.0" + checksum: 10/17d04adf404e04c1e61391ed97bca5117d4c2767a76ae3e879390d6dec7b317fcae68afbf9e98badee075d0b64fa60f287729c4942021b4d19cd01db77385c01 + languageName: node + linkType: hard + "@safe-global/api-kit@npm:1.3.0": version: 1.3.0 resolution: "@safe-global/api-kit@npm:1.3.0" @@ -11092,6 +12659,26 @@ __metadata: languageName: node linkType: hard +"@safe-global/safe-apps-provider@npm:0.18.4": + version: 0.18.4 + resolution: "@safe-global/safe-apps-provider@npm:0.18.4" + dependencies: + "@safe-global/safe-apps-sdk": "npm:^9.1.0" + events: "npm:^3.3.0" + checksum: 10/252ccad3416f73e9fa5e7bdd074955ca6b81c55be89c3cd3e25a7cab9b01922cb9f9a02d2766dff15003908c7cccc47bc22f58dd2d4a65b504fd7870eabda41b + languageName: node + linkType: hard + +"@safe-global/safe-apps-sdk@npm:9.1.0, @safe-global/safe-apps-sdk@npm:^9.1.0": + version: 9.1.0 + resolution: "@safe-global/safe-apps-sdk@npm:9.1.0" + dependencies: + "@safe-global/safe-gateway-typescript-sdk": "npm:^3.5.3" + viem: "npm:^2.1.1" + checksum: 10/b81e1a554509fc41f5b8ec3bcccaf477fd55824010774699dd2c00dee8431cfd351bf13893ff6acb1450028ce4de31a1316548a0e77a66d801ff9e0b4e08b9ff + languageName: node + linkType: hard + "@safe-global/safe-core-sdk-types@npm:2.3.0": version: 2.3.0 resolution: "@safe-global/safe-core-sdk-types@npm:2.3.0" @@ -11136,20 +12723,27 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:~1.0.0": - version: 1.0.0 - resolution: "@scure/base@npm:1.0.0" - checksum: 10/cbf631e1f13536287e1f19137039d29757a008fad2d9b0c8113c140e1900519a95f4884cd61633fbb2adbe426c3c9bbfde2b77519145b3d81bac9853a52c599e +"@safe-global/safe-gateway-typescript-sdk@npm:^3.5.3": + version: 3.22.2 + resolution: "@safe-global/safe-gateway-typescript-sdk@npm:3.22.2" + checksum: 10/7f2b3cab4a1673647c8f7fd927be280f891dc74dba733f302862dee135fedd9d8e1875b1790c75b84c54164b517727bfe08a6dcaf7411659db13eeaefd1407fd languageName: node linkType: hard -"@scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": +"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.6, @scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" checksum: 10/f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb languageName: node linkType: hard +"@scure/base@npm:~1.0.0": + version: 1.0.0 + resolution: "@scure/base@npm:1.0.0" + checksum: 10/cbf631e1f13536287e1f19137039d29757a008fad2d9b0c8113c140e1900519a95f4884cd61633fbb2adbe426c3c9bbfde2b77519145b3d81bac9853a52c599e + languageName: node + linkType: hard + "@scure/bip32@npm:1.0.1": version: 1.0.1 resolution: "@scure/bip32@npm:1.0.1" @@ -11161,7 +12755,18 @@ __metadata: languageName: node linkType: hard -"@scure/bip32@npm:1.5.0": +"@scure/bip32@npm:1.4.0": + version: 1.4.0 + resolution: "@scure/bip32@npm:1.4.0" + dependencies: + "@noble/curves": "npm:~1.4.0" + "@noble/hashes": "npm:~1.4.0" + "@scure/base": "npm:~1.1.6" + checksum: 10/6cd5062d902564d9e970597ec8b1adacb415b2eadfbb95aee1a1a0480a52eb0de4d294d3753aa8b48548064c9795ed108d348a31a8ce3fc88785377bb12c63b9 + languageName: node + linkType: hard + +"@scure/bip32@npm:1.5.0, @scure/bip32@npm:^1.5.0": version: 1.5.0 resolution: "@scure/bip32@npm:1.5.0" dependencies: @@ -11182,7 +12787,17 @@ __metadata: languageName: node linkType: hard -"@scure/bip39@npm:1.4.0": +"@scure/bip39@npm:1.3.0": + version: 1.3.0 + resolution: "@scure/bip39@npm:1.3.0" + dependencies: + "@noble/hashes": "npm:~1.4.0" + "@scure/base": "npm:~1.1.6" + checksum: 10/7d71fd58153de22fe8cd65b525f6958a80487bc9d0fbc32c71c328aeafe41fa259f989d2f1e0fa4fdfeaf83b8fcf9310d52ed9862987e46c2f2bfb9dd8cf9fc1 + languageName: node + linkType: hard + +"@scure/bip39@npm:1.4.0, @scure/bip39@npm:^1.4.0": version: 1.4.0 resolution: "@scure/bip39@npm:1.4.0" dependencies: @@ -11295,6 +12910,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/merge-streams@npm:^2.1.0": + version: 2.3.0 + resolution: "@sindresorhus/merge-streams@npm:2.3.0" + checksum: 10/798bcb53cd1ace9df84fcdd1ba86afdc9e0cd84f5758d26ae9b1eefd8e8887e5fc30051132b9e74daf01bb41fa5a2faf1369361f83d76a3b3d7ee938058fd71c + languageName: node + linkType: hard + "@sinonjs/commons@npm:^1.6.0, @sinonjs/commons@npm:^1.7.0, @sinonjs/commons@npm:^1.8.3": version: 1.8.3 resolution: "@sinonjs/commons@npm:1.8.3" @@ -11913,6 +13535,60 @@ __metadata: languageName: node linkType: hard +"@socket.io/component-emitter@npm:~3.1.0": + version: 3.1.2 + resolution: "@socket.io/component-emitter@npm:3.1.2" + checksum: 10/89888f00699eb34e3070624eb7b8161fa29f064aeb1389a48f02195d55dd7c52a504e52160016859f6d6dffddd54324623cdd47fd34b3d46f9ed96c18c456edc + languageName: node + linkType: hard + +"@solana-mobile/mobile-wallet-adapter-protocol-web3js@npm:^2.1.4": + version: 2.1.4 + resolution: "@solana-mobile/mobile-wallet-adapter-protocol-web3js@npm:2.1.4" + dependencies: + "@solana-mobile/mobile-wallet-adapter-protocol": "npm:^2.1.4" + bs58: "npm:^5.0.0" + js-base64: "npm:^3.7.5" + peerDependencies: + "@solana/web3.js": ^1.58.0 + checksum: 10/477ccde5bfa9bb9f108dac4a3ae515b4c37489d9314076951cc467f9c0212780a03f10356c1d57d34197a384cb1aeaba90a1f743ee3cb7733373e6b1ff7d4f19 + languageName: node + linkType: hard + +"@solana-mobile/mobile-wallet-adapter-protocol@npm:^2.1.4": + version: 2.1.4 + resolution: "@solana-mobile/mobile-wallet-adapter-protocol@npm:2.1.4" + dependencies: + "@solana/wallet-standard": "npm:^1.1.2" + "@solana/wallet-standard-util": "npm:^1.1.1" + "@wallet-standard/core": "npm:^1.0.3" + js-base64: "npm:^3.7.5" + peerDependencies: + "@solana/web3.js": ^1.58.0 + react-native: ">0.69" + checksum: 10/48270b536251f601b635a0c6ecb46da332bf3f1f419b83affbb609a15242f199fda896babbea4a647a747af57e62f0aa8abf0e5d7b2047d260576c17e883528d + languageName: node + linkType: hard + +"@solana-mobile/wallet-adapter-mobile@npm:^2.0.0": + version: 2.1.4 + resolution: "@solana-mobile/wallet-adapter-mobile@npm:2.1.4" + dependencies: + "@react-native-async-storage/async-storage": "npm:^1.17.7" + "@solana-mobile/mobile-wallet-adapter-protocol-web3js": "npm:^2.1.4" + "@solana/wallet-adapter-base": "npm:^0.9.23" + "@solana/wallet-standard-features": "npm:^1.2.0" + js-base64: "npm:^3.7.5" + qrcode: "npm:^1.5.4" + peerDependencies: + "@solana/web3.js": ^1.58.0 + dependenciesMeta: + "@react-native-async-storage/async-storage": + optional: true + checksum: 10/9dfc46a6b030f2b9dd8ae80c3bb6173a6cd9b0024b89894f14618fb7eb7b90c6041966a2265b71dcde5445ccf91eb2b8d2b1e2f3e73c7e13ea496717fcbbe632 + languageName: node + linkType: hard + "@solana/buffer-layout-utils@npm:^0.2.0": version: 0.2.0 resolution: "@solana/buffer-layout-utils@npm:0.2.0" @@ -12065,6 +13741,155 @@ __metadata: languageName: node linkType: hard +"@solana/wallet-adapter-base-ui@npm:^0.1.2": + version: 0.1.2 + resolution: "@solana/wallet-adapter-base-ui@npm:0.1.2" + dependencies: + "@solana/wallet-adapter-react": "npm:^0.15.35" + peerDependencies: + "@solana/web3.js": ^1.77.3 + react: "*" + checksum: 10/5f4045defe8caedfbd777c7e46590b320a5e8526291aba83c2b1c2666b9bdfa97f46ee1820d2beb1d2cfe62a09c134018a40679023e54e9d583826d2333981d0 + languageName: node + linkType: hard + +"@solana/wallet-adapter-base@npm:^0.9.23": + version: 0.9.23 + resolution: "@solana/wallet-adapter-base@npm:0.9.23" + dependencies: + "@solana/wallet-standard-features": "npm:^1.1.0" + "@wallet-standard/base": "npm:^1.0.1" + "@wallet-standard/features": "npm:^1.0.3" + eventemitter3: "npm:^4.0.7" + peerDependencies: + "@solana/web3.js": ^1.77.3 + checksum: 10/7b0ab2a3b33bf4796c9e544d13b3ac2b6628cdbff9e839772eb2b2ab34355708fe662cc8971b68748febffdcc2ced79725f6c1ff7832d0c1660558ad0052b372 + languageName: node + linkType: hard + +"@solana/wallet-adapter-react-ui@npm:^0.9.31": + version: 0.9.35 + resolution: "@solana/wallet-adapter-react-ui@npm:0.9.35" + dependencies: + "@solana/wallet-adapter-base": "npm:^0.9.23" + "@solana/wallet-adapter-base-ui": "npm:^0.1.2" + "@solana/wallet-adapter-react": "npm:^0.15.35" + peerDependencies: + "@solana/web3.js": ^1.77.3 + react: "*" + react-dom: "*" + checksum: 10/4c1dccec09bf2197bb757952d4229cdf272ea0b1c1b06e0f09aea55482c0754b96495ffdcf67391ff6a5df6a3f5bd144b501bb90946bd0038dec302ec6fbfb20 + languageName: node + linkType: hard + +"@solana/wallet-adapter-react@npm:^0.15.32, @solana/wallet-adapter-react@npm:^0.15.35": + version: 0.15.35 + resolution: "@solana/wallet-adapter-react@npm:0.15.35" + dependencies: + "@solana-mobile/wallet-adapter-mobile": "npm:^2.0.0" + "@solana/wallet-adapter-base": "npm:^0.9.23" + "@solana/wallet-standard-wallet-adapter-react": "npm:^1.1.0" + peerDependencies: + "@solana/web3.js": ^1.77.3 + react: "*" + checksum: 10/52e009d8a49fb92e2fac90e9427fa790644e3f4b387ef39a1bb30047f8ae394a6367d3e9a9a97785d8e3fd90141800e7268f492aee0b9de0b88e113f565dd071 + languageName: node + linkType: hard + +"@solana/wallet-standard-chains@npm:^1.1.0": + version: 1.1.0 + resolution: "@solana/wallet-standard-chains@npm:1.1.0" + dependencies: + "@wallet-standard/base": "npm:^1.0.1" + checksum: 10/c87141660a01b1e4cb394c12bfa1b779e2c231dfe098518273b90c80afa0a4185bc4aeadca801452af7f8396eecec81c7e9f820d478cd4495d5918924a8bfaf3 + languageName: node + linkType: hard + +"@solana/wallet-standard-core@npm:^1.1.1": + version: 1.1.1 + resolution: "@solana/wallet-standard-core@npm:1.1.1" + dependencies: + "@solana/wallet-standard-chains": "npm:^1.1.0" + "@solana/wallet-standard-features": "npm:^1.2.0" + "@solana/wallet-standard-util": "npm:^1.1.1" + checksum: 10/3c4f8aca67856cea12928a5cf99f2cb022a787263765f879024c69ef3d89a090777301b4ec68e534de2d1df33feee34ec530bf755488dd49415c131521d06b6c + languageName: node + linkType: hard + +"@solana/wallet-standard-features@npm:^1.1.0, @solana/wallet-standard-features@npm:^1.2.0": + version: 1.2.0 + resolution: "@solana/wallet-standard-features@npm:1.2.0" + dependencies: + "@wallet-standard/base": "npm:^1.0.1" + "@wallet-standard/features": "npm:^1.0.3" + checksum: 10/6a638783b282078f7c38ba0d2a69f302293d0c3226ea257d1cafd16d7b7332631d284e738d53d443dac984900a3b6d5fa34a1c92a51200901a43966048d4475c + languageName: node + linkType: hard + +"@solana/wallet-standard-util@npm:^1.1.1": + version: 1.1.1 + resolution: "@solana/wallet-standard-util@npm:1.1.1" + dependencies: + "@noble/curves": "npm:^1.1.0" + "@solana/wallet-standard-chains": "npm:^1.1.0" + "@solana/wallet-standard-features": "npm:^1.2.0" + checksum: 10/be04a8905d1cb2bfe7b10119be374a3e51787375c3435563a935133ff3e6a4bcb482a65ba18024400fe2a2781e7900bce867fd4789de86731a7f15dd0d7978b2 + languageName: node + linkType: hard + +"@solana/wallet-standard-wallet-adapter-base@npm:^1.1.2": + version: 1.1.2 + resolution: "@solana/wallet-standard-wallet-adapter-base@npm:1.1.2" + dependencies: + "@solana/wallet-adapter-base": "npm:^0.9.23" + "@solana/wallet-standard-chains": "npm:^1.1.0" + "@solana/wallet-standard-features": "npm:^1.2.0" + "@solana/wallet-standard-util": "npm:^1.1.1" + "@wallet-standard/app": "npm:^1.0.1" + "@wallet-standard/base": "npm:^1.0.1" + "@wallet-standard/features": "npm:^1.0.3" + "@wallet-standard/wallet": "npm:^1.0.1" + peerDependencies: + "@solana/web3.js": ^1.58.0 + bs58: ^4.0.1 + checksum: 10/458269e1ca5205be4b3fe758a5bda528a03a6f65199a87968d008498901b221428690c0b4e75fd63344470a0a84206a28ab27868489b445428001cee7f770f68 + languageName: node + linkType: hard + +"@solana/wallet-standard-wallet-adapter-react@npm:^1.1.0, @solana/wallet-standard-wallet-adapter-react@npm:^1.1.2": + version: 1.1.2 + resolution: "@solana/wallet-standard-wallet-adapter-react@npm:1.1.2" + dependencies: + "@solana/wallet-standard-wallet-adapter-base": "npm:^1.1.2" + "@wallet-standard/app": "npm:^1.0.1" + "@wallet-standard/base": "npm:^1.0.1" + peerDependencies: + "@solana/wallet-adapter-base": "*" + react: "*" + checksum: 10/1e64f9ee7be8371b16ba51dfb01a6cea499bf0157c333bbc759f6e8c113a7930d6e47cf44ab25a497296be9a83a36caabc53f8ce0837091f38391f08b4a45539 + languageName: node + linkType: hard + +"@solana/wallet-standard-wallet-adapter@npm:^1.1.2": + version: 1.1.2 + resolution: "@solana/wallet-standard-wallet-adapter@npm:1.1.2" + dependencies: + "@solana/wallet-standard-wallet-adapter-base": "npm:^1.1.2" + "@solana/wallet-standard-wallet-adapter-react": "npm:^1.1.2" + checksum: 10/01571705e747129099006c45d2272117248d7f1be55190e519b78cfcfee9ce5f3307a65c32f50d6a4d388e56cb5a0be4dd028419c842d70680c665180f50dbf6 + languageName: node + linkType: hard + +"@solana/wallet-standard@npm:^1.1.2": + version: 1.1.2 + resolution: "@solana/wallet-standard@npm:1.1.2" + dependencies: + "@solana/wallet-standard-core": "npm:^1.1.1" + "@solana/wallet-standard-wallet-adapter": "npm:^1.1.2" + checksum: 10/3c17b9cafde0d796c50242916ac3fe07edbd1655dfe8a34d4e44dfbe8a82e5cbc389e867c48b802914785354c2310e237edb4ce7b9d22977ab2430f3cf52557f + languageName: node + linkType: hard + "@solana/web3.js@npm:^1.32.0": version: 1.78.5 resolution: "@solana/web3.js@npm:1.78.5" @@ -12136,6 +13961,176 @@ __metadata: languageName: node linkType: hard +"@stablelib/aead@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/aead@npm:1.0.1" + checksum: 10/1a6f68d138f105d17dd65349751515bd252ab0498c77255b8555478d28415600dde493f909eb718245047a993f838dfae546071e1687566ffb7b8c3e10c918d9 + languageName: node + linkType: hard + +"@stablelib/binary@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/binary@npm:1.0.1" + dependencies: + "@stablelib/int": "npm:^1.0.1" + checksum: 10/c5ed769e2b5d607a5cdb72d325fcf98db437627862fade839daad934bd9ccf02a6f6e34f9de8cb3b18d72fce2ba6cc019a5d22398187d7d69d2607165f27f8bf + languageName: node + linkType: hard + +"@stablelib/bytes@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/bytes@npm:1.0.1" + checksum: 10/23d4d632a8a15ca91be1dc56da92eefed695d9b66068d1ab27a5655d0233dc2ac0b8668f875af542ca4ed526893c65dd53e777c72c8056f3648115aac98823ee + languageName: node + linkType: hard + +"@stablelib/chacha20poly1305@npm:1.0.1": + version: 1.0.1 + resolution: "@stablelib/chacha20poly1305@npm:1.0.1" + dependencies: + "@stablelib/aead": "npm:^1.0.1" + "@stablelib/binary": "npm:^1.0.1" + "@stablelib/chacha": "npm:^1.0.1" + "@stablelib/constant-time": "npm:^1.0.1" + "@stablelib/poly1305": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/2a4df136b078b7c09acb3c6fe029613d4c9f70a0ce8bec65551a4a5016930a4f9091d3b83ed1cfc9c2e7bd6ec7f5ee93a7dc729b784b3900dcb97f3c7f5da84a + languageName: node + linkType: hard + +"@stablelib/chacha@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/chacha@npm:1.0.1" + dependencies: + "@stablelib/binary": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/38cd8095d94eda29a9bb8a742b1c945dba7f9ec91fc07ab351c826680d03976641ac6366c3d004a00a72d746fcd838215fe1263ef4b0660c453c5de18a0a4295 + languageName: node + linkType: hard + +"@stablelib/constant-time@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/constant-time@npm:1.0.1" + checksum: 10/dba4f4bf508de2ff15f7f0cbd875e70391aa3ba3698290fe1ed2feb151c243ba08a90fc6fb390ec2230e30fcc622318c591a7c0e35dcb8150afb50c797eac3d7 + languageName: node + linkType: hard + +"@stablelib/ed25519@npm:^1.0.2": + version: 1.0.3 + resolution: "@stablelib/ed25519@npm:1.0.3" + dependencies: + "@stablelib/random": "npm:^1.0.2" + "@stablelib/sha512": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/52e861e4fbd9d3d0a1a370d9ad96de8e2e15f133249bbbc32da66b8993e843db598054a3af17a746beb3fd5043b7529613a5dda7f2e79de6613eb3ebe5ffe3dd + languageName: node + linkType: hard + +"@stablelib/hash@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/hash@npm:1.0.1" + checksum: 10/3ff1f12d1a4082aaf4b6cdf40c2010aabe5c4209d3b40b97b5bbb0d9abc0ee94abdc545e57de0614afaea807ca0212ac870e247ec8f66cdce91ec39ce82948cf + languageName: node + linkType: hard + +"@stablelib/hkdf@npm:1.0.1": + version: 1.0.1 + resolution: "@stablelib/hkdf@npm:1.0.1" + dependencies: + "@stablelib/hash": "npm:^1.0.1" + "@stablelib/hmac": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/9d45e303715a1835c8612b78e6c1b9d2b7463699b484241d8681fb5c17e0f2bbde5ce211c882134b64616a402e09177baeba80426995ff227b3654a155ab225d + languageName: node + linkType: hard + +"@stablelib/hmac@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/hmac@npm:1.0.1" + dependencies: + "@stablelib/constant-time": "npm:^1.0.1" + "@stablelib/hash": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/d3ac9e2fea2b4972a5d874ee9d96c94f8c8207452e2d243a2668b1325a7b20bd9a1541df32387789a0e9bfef82c3fe021a785f46eb3442c782443863faf75205 + languageName: node + linkType: hard + +"@stablelib/int@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/int@npm:1.0.1" + checksum: 10/65bfbf50a382eea70c68e05366bf379cfceff8fbc076f1c267ef2f2411d7aed64fd140c415cb6c29f19a3910d3b8b7805d4b32ad5721a5007a8e744a808c7ae3 + languageName: node + linkType: hard + +"@stablelib/keyagreement@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/keyagreement@npm:1.0.1" + dependencies: + "@stablelib/bytes": "npm:^1.0.1" + checksum: 10/3c8ec904dd50f72f3162f5447a0fa8f1d9ca6e24cd272d3dbe84971267f3b47f9bd5dc4e4eeedf3fbac2fe01f2d9277053e57c8e60db8c5544bfb35c62d290dd + languageName: node + linkType: hard + +"@stablelib/poly1305@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/poly1305@npm:1.0.1" + dependencies: + "@stablelib/constant-time": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/b01d4b532a42e5260f7f263e3a670924849c7ba51569abd8ece8279a448e625cbe4049bff1d50ad0d3a9d5f268c1b52fc611808640a6e684550edd7589a0a581 + languageName: node + linkType: hard + +"@stablelib/random@npm:1.0.2, @stablelib/random@npm:^1.0.1, @stablelib/random@npm:^1.0.2": + version: 1.0.2 + resolution: "@stablelib/random@npm:1.0.2" + dependencies: + "@stablelib/binary": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/f5ace0a588dc4c21f01cb85837892d4c872e994ae77a58a8eb7dd61aa0b26fb1e9b46b0445e71af57d963ef7d9f5965c64258fc0d04df7b2947bc48f2d3560c5 + languageName: node + linkType: hard + +"@stablelib/sha256@npm:1.0.1": + version: 1.0.1 + resolution: "@stablelib/sha256@npm:1.0.1" + dependencies: + "@stablelib/binary": "npm:^1.0.1" + "@stablelib/hash": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/4d55f6c676e2cc0dd2a32be0cfa96837f3e15ae48dc50a340e56db2b201f1341a9ecabb429a3a44a5bf31adee0a8151467a8e7cc15346c561c914faad415d4d4 + languageName: node + linkType: hard + +"@stablelib/sha512@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/sha512@npm:1.0.1" + dependencies: + "@stablelib/binary": "npm:^1.0.1" + "@stablelib/hash": "npm:^1.0.1" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/35d188cd62f20d27e1d61ea07984022e9a78815a023c8f7c747d92456a60823f0683138591e87158a47cd72e73cf24ecf97f8936aa6fba8b3bef6fcb138e723d + languageName: node + linkType: hard + +"@stablelib/wipe@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/wipe@npm:1.0.1" + checksum: 10/287802eb146810a46ba72af70b82022caf83a8aeebde23605f5ee0decf64fe2b97a60c856e43b6617b5801287c30cfa863cfb0469e7fcde6f02d143cf0c6cbf4 + languageName: node + linkType: hard + +"@stablelib/x25519@npm:1.0.3": + version: 1.0.3 + resolution: "@stablelib/x25519@npm:1.0.3" + dependencies: + "@stablelib/keyagreement": "npm:^1.0.1" + "@stablelib/random": "npm:^1.0.2" + "@stablelib/wipe": "npm:^1.0.1" + checksum: 10/fb5469e390ee2515d926633e3e179038894ac4f5e8c8cd2c2fc912022e34a051112eab0fe80c4dbc6e59129679844182562a036abff89444e5c4a05dd42ed329 + languageName: node + linkType: hard + "@storybook/addon-actions@npm:7.6.20": version: 7.6.20 resolution: "@storybook/addon-actions@npm:7.6.20" @@ -12650,16 +14645,7 @@ __metadata: languageName: node linkType: hard -"@storybook/csf@npm:^0.0.1": - version: 0.0.1 - resolution: "@storybook/csf@npm:0.0.1" - dependencies: - lodash: "npm:^4.17.15" - checksum: 10/f6bb019bccd8abc14e45a85258158b7bd8cc525887ac8dc9151ed8c4908be3b5f5523da8a7a9b96ff11b13b6c1744e1a0e070560d63d836b950f595f9a5719d4 - languageName: node - linkType: hard - -"@storybook/csf@npm:^0.1.2": +"@storybook/csf@npm:^0.1.11, @storybook/csf@npm:^0.1.2": version: 0.1.11 resolution: "@storybook/csf@npm:0.1.11" dependencies: @@ -12948,6 +14934,36 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:5.60.5": + version: 5.60.5 + resolution: "@tanstack/query-core@npm:5.60.5" + checksum: 10/2c85ed3a26db31ea9fb2e44f94afee4564c8a5e47fc2c9f4e7a3b477e90642d1330ecdbc48f3b503205401772cedd94545eff23e628badd2bff5509b1da62e94 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^5.59.20": + version: 5.60.5 + resolution: "@tanstack/react-query@npm:5.60.5" + dependencies: + "@tanstack/query-core": "npm:5.60.5" + peerDependencies: + react: ^18 || ^19 + checksum: 10/00a5ef74ceb50f6bbacec2545392de65d3d9e6dc6de2a7107943460a83d1988eb66740f40b8682c8de0e9a1faff76256ab1584a816c70dd4ff9205dedc209f0a + languageName: node + linkType: hard + +"@tanstack/react-virtual@npm:^3.10.5": + version: 3.10.9 + resolution: "@tanstack/react-virtual@npm:3.10.9" + dependencies: + "@tanstack/virtual-core": "npm:3.10.9" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/a6c90118d0b084aedf0a2b02bc718df5cc1e594fb60d1dededf8a393d3e519e574e2ba67bb7adcaf8b4d6b206f6a10b37166f006bc7e50ad566475323d545b8c + languageName: node + linkType: hard + "@tanstack/react-virtual@npm:^3.8.1": version: 3.10.8 resolution: "@tanstack/react-virtual@npm:3.10.8" @@ -12967,6 +14983,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/virtual-core@npm:3.10.9": + version: 3.10.9 + resolution: "@tanstack/virtual-core@npm:3.10.9" + checksum: 10/15140fc41c728ed08486eba4c9caadbdb3c594f02d3b55fddca63813bc32e8cde64faf6ca6385f9815aeeedbc441dd8c9590aca4319c16a91f39b1937ef4eac7 + languageName: node + linkType: hard + "@testing-library/dom@npm:^9.3.1": version: 9.3.4 resolution: "@testing-library/dom@npm:9.3.4" @@ -13306,6 +15329,15 @@ __metadata: languageName: node linkType: hard +"@types/debug@npm:^4.1.7": + version: 4.1.12 + resolution: "@types/debug@npm:4.1.12" + dependencies: + "@types/ms": "npm:*" + checksum: 10/47876a852de8240bfdaf7481357af2b88cb660d30c72e73789abf00c499d6bc7cd5e52f41c915d1b9cd8ec9fef5b05688d7b7aef17f7f272c2d04679508d1053 + languageName: node + linkType: hard + "@types/detect-port@npm:^1.3.0": version: 1.3.5 resolution: "@types/detect-port@npm:1.3.5" @@ -13362,6 +15394,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.6": + version: 1.0.6 + resolution: "@types/estree@npm:1.0.6" + checksum: 10/9d35d475095199c23e05b431bcdd1f6fec7380612aed068b14b2a08aa70494de8a9026765a5a91b1073f636fb0368f6d8973f518a31391d519e20c59388ed88d + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:^4.17.33": version: 4.19.5 resolution: "@types/express-serve-static-core@npm:4.19.5" @@ -13486,7 +15525,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.5": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -13500,6 +15539,13 @@ __metadata: languageName: node linkType: hard +"@types/json5@npm:^0.0.29": + version: 0.0.29 + resolution: "@types/json5@npm:0.0.29" + checksum: 10/4e5aed58cabb2bbf6f725da13421aa50a49abb6bc17bfab6c31b8774b073fa7b50d557c61f961a09a85f6056151190f8ac95f13f5b48136ba5841f7d4484ec56 + languageName: node + linkType: hard + "@types/jsonfile@npm:*": version: 6.1.4 resolution: "@types/jsonfile@npm:6.1.4" @@ -13545,6 +15591,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash.mergewith@npm:4.6.9": + version: 4.6.9 + resolution: "@types/lodash.mergewith@npm:4.6.9" + dependencies: + "@types/lodash": "npm:*" + checksum: 10/c5a67e83040103decfd37090127118f5758773d0ce2a1756d442b371721737c7752f48f62544cc970f44abec8471f260cc4c844e1a4fdef8b76cb96bdec8a595 + languageName: node + linkType: hard + "@types/lodash@npm:*, @types/lodash@npm:^4.14.167": version: 4.17.7 resolution: "@types/lodash@npm:4.17.7" @@ -13617,12 +15672,10 @@ __metadata: languageName: node linkType: hard -"@types/mute-stream@npm:^0.0.1": - version: 0.0.1 - resolution: "@types/mute-stream@npm:0.0.1" - dependencies: - "@types/node": "npm:*" - checksum: 10/01bb9f45a77b691538cba7f0c89166a5bfb112993056ae06a8218cd47d417a5f6a5cfc31f0d31293790c647d27564fe6aa39aa9cb2ef08792d42ed40f18de8d5 +"@types/ms@npm:*": + version: 0.7.34 + resolution: "@types/ms@npm:0.7.34" + checksum: 10/f38d36e7b6edecd9badc9cf50474159e9da5fa6965a75186cceaf883278611b9df6669dc3a3cc122b7938d317b68a9e3d573d316fcb35d1be47ec9e468c6bd8a languageName: node linkType: hard @@ -13671,6 +15724,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:10.12.18": + version: 10.12.18 + resolution: "@types/node@npm:10.12.18" + checksum: 10/cfa39e797eed0f9eb2070315c66a5be3839ba91c57ace4eff63203f2e8c871127dd45fa5a3bd2ee6d7760608ea13f30b4fbd9a7b0b6a71b478162b2eb23d07b6 + languageName: node + linkType: hard + "@types/node@npm:11.11.6": version: 11.11.6 resolution: "@types/node@npm:11.11.6" @@ -13710,14 +15770,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^16.9.1": - version: 16.18.38 - resolution: "@types/node@npm:16.18.38" - checksum: 10/233cc3c4ebbfb011ecd68a552080d4879ced7f66558ecab07d5adc504b3d52181ef31ce7be03c1a616afbc187530aac38f0016ce274d6b8fb89f79365b7c721a - languageName: node - linkType: hard - -"@types/node@npm:^18.0.0, @types/node@npm:^18.11.18": +"@types/node@npm:^18.0.0": version: 18.19.42 resolution: "@types/node@npm:18.19.42" dependencies: @@ -13733,10 +15786,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.4.2": - version: 20.4.5 - resolution: "@types/node@npm:20.4.5" - checksum: 10/aa31081f82a2d377f00cfd7ced73925753db1f542fca19e6b0442585a7322b8eacd957fdccaaff65d9976454d213af0980c12390c59a975cf8a368e3f872717a +"@types/node@npm:^20.10.7": + version: 20.17.8 + resolution: "@types/node@npm:20.17.8" + dependencies: + undici-types: "npm:~6.19.2" + checksum: 10/e3e968b327abc70fd437a223f8950dd4436047e954aa7db09abde5df1f58a5c49f33d6f14524e256d09719e1960d22bf072d62e4bda7375f7895a092c7eb2f9d languageName: node linkType: hard @@ -13763,6 +15818,13 @@ __metadata: languageName: node linkType: hard +"@types/parse-json@npm:^4.0.0": + version: 4.0.2 + resolution: "@types/parse-json@npm:4.0.2" + checksum: 10/5bf62eec37c332ad10059252fc0dab7e7da730764869c980b0714777ad3d065e490627be9f40fc52f238ffa3ac4199b19de4127196910576c2fe34dd47c7a470 + languageName: node + linkType: hard + "@types/pbkdf2@npm:^3.0.0": version: 3.1.0 resolution: "@types/pbkdf2@npm:3.1.0" @@ -13910,7 +15972,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4": +"@types/semver@npm:^7.3.4": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 @@ -13994,6 +16056,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.2": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10/8e4202766a65877efcf5d5a41b7dd458480b36195e580a3b1085ad21e948bc417d55d6f8af1fd2a7ad008015d4117d5fdfe432731157da3c68678487174e4ba3 + languageName: node + linkType: hard + "@types/unist@npm:^2.0.0": version: 2.0.10 resolution: "@types/unist@npm:2.0.10" @@ -14083,56 +16152,44 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/eslint-plugin@npm:7.4.0" +"@typescript-eslint/eslint-plugin@npm:^8.1.6": + version: 8.16.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.16.0" dependencies: - "@eslint-community/regexpp": "npm:^4.5.1" - "@typescript-eslint/scope-manager": "npm:7.4.0" - "@typescript-eslint/type-utils": "npm:7.4.0" - "@typescript-eslint/utils": "npm:7.4.0" - "@typescript-eslint/visitor-keys": "npm:7.4.0" - debug: "npm:^4.3.4" + "@eslint-community/regexpp": "npm:^4.10.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/type-utils": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" graphemer: "npm:^1.4.0" - ignore: "npm:^5.2.4" + ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" + ts-api-utils: "npm:^1.3.0" peerDependencies: - "@typescript-eslint/parser": ^7.0.0 - eslint: ^8.56.0 + "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/9bd8852c7e4e9608c3fded94f7c60506cc7d2b6d8a8c1cad6d48969a7363751b20282874e55ccdf180635cf204cb10b3e1e5c3d1cff34d4fcd07762be3fc138e + checksum: 10/aa3d551d4f09940eee0c08328cb0db3a2391a8bba6d044f6bb38c51ac864896519c647d4b8fd99f7c094cc677bcf22454b27322014a08b2f2fb25695a43820db languageName: node linkType: hard -"@typescript-eslint/parser@npm:^7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/parser@npm:7.4.0" +"@typescript-eslint/parser@npm:^8.1.6": + version: 8.16.0 + resolution: "@typescript-eslint/parser@npm:8.16.0" dependencies: - "@typescript-eslint/scope-manager": "npm:7.4.0" - "@typescript-eslint/types": "npm:7.4.0" - "@typescript-eslint/typescript-estree": "npm:7.4.0" - "@typescript-eslint/visitor-keys": "npm:7.4.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" debug: "npm:^4.3.4" peerDependencies: - eslint: ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/142a9e1187d305ed43b4fef659c36fa4e28359467198c986f0955c70b4067c9799f4c85d9881fbf099c55dfb265e30666e28b3ef290520e242b45ca7cb8e4ca9 - languageName: node - linkType: hard - -"@typescript-eslint/scope-manager@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/scope-manager@npm:5.62.0" - dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - checksum: 10/e827770baa202223bc0387e2fd24f630690809e460435b7dc9af336c77322290a770d62bd5284260fa881c86074d6a9fd6c97b07382520b115f6786b8ed499da + checksum: 10/ac1e2bfdbfe212da470bb17915b5228f7a6b027332b05eb8bcbbad440a81b2476c649e54e232084838e1edc005e6d7dc7a44899587d73672dd3d5484d9dbf9f8 languageName: node linkType: hard @@ -14146,37 +16203,30 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/scope-manager@npm:7.4.0" +"@typescript-eslint/scope-manager@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/scope-manager@npm:8.16.0" dependencies: - "@typescript-eslint/types": "npm:7.4.0" - "@typescript-eslint/visitor-keys": "npm:7.4.0" - checksum: 10/8cf9292444f9731017a707cac34bef5ae0eb33b5cd42ed07fcd046e981d97889d9201d48e02f470f2315123f53771435e10b1dc81642af28a11df5352a8e8be2 + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" + checksum: 10/e0aea61f248b39049d4ce21c19f9c8af1a8024f4f92abc8c1d5b79ea65b013c6c4ff41efb92995050036aa95b6a705601917b56809d9ec1fbbab387054aeb269 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/type-utils@npm:7.4.0" +"@typescript-eslint/type-utils@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/type-utils@npm:8.16.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.4.0" - "@typescript-eslint/utils": "npm:7.4.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" debug: "npm:^4.3.4" - ts-api-utils: "npm:^1.0.1" + ts-api-utils: "npm:^1.3.0" peerDependencies: - eslint: ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/a8bd0929d8237679b2b8a7817f070a4b9658ee976882fba8ff37e4a70dd33f87793e1b157771104111fe8054eaa8ad437a010b6aa465072fbdb932647125db2d - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/types@npm:5.62.0" - checksum: 10/24e8443177be84823242d6729d56af2c4b47bfc664dd411a1d730506abf2150d6c31bdefbbc6d97c8f91043e3a50e0c698239dcb145b79bb6b0c34469aaf6c45 + checksum: 10/b91f6cef6af7e4f82a1dba9622d5ec9f46d1983eecfb88a1adbd310c7f980fedf5c8a198bfe968aae59fc386e4c437f55a7533988252eb9cbb0bdac8321e3dba languageName: node linkType: hard @@ -14187,28 +16237,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/types@npm:7.4.0" - checksum: 10/2782c5bf65cd3dfa9cd32bc3023676bbca22144987c3f6c6b67fd96c73d4a60b85a57458c49fd11b9971ac6531824bb3ae0664491e7a6de25d80c523c9be92b7 - languageName: node - linkType: hard - -"@typescript-eslint/typescript-estree@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" - dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - semver: "npm:^7.3.7" - tsutils: "npm:^3.21.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/06c975eb5f44b43bd19fadc2e1023c50cf87038fe4c0dd989d4331c67b3ff509b17fa60a3251896668ab4d7322bdc56162a9926971218d2e1a1874d2bef9a52e +"@typescript-eslint/types@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/types@npm:8.16.0" + checksum: 10/b37b26cd0e45b0cd6f7d492a07af583e4877d798495ab5fc1cfacb3c561b6d7981e3166f0475bb997e6c6d56ef903e160895174c7e63c08322dbb42d026cf7dc languageName: node linkType: hard @@ -14231,57 +16263,39 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/typescript-estree@npm:7.4.0" +"@typescript-eslint/typescript-estree@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.16.0" dependencies: - "@typescript-eslint/types": "npm:7.4.0" - "@typescript-eslint/visitor-keys": "npm:7.4.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" debug: "npm:^4.3.4" - globby: "npm:^11.1.0" + fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" - minimatch: "npm:9.0.3" - semver: "npm:^7.5.4" - ts-api-utils: "npm:^1.0.1" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" peerDependenciesMeta: typescript: optional: true - checksum: 10/162ec9d7582f45588342e1be36fdb60e41f50bbdfbc3035c91b517ff5d45244f776921c88d88e543e1c7d0f1e6ada5474a8316b78f1b0e6d2233b101bc45b166 + checksum: 10/823cf55d331cf7283547a2860a5d7bfd7dbd497be6e87b226dd7456b36db214de1504855afbbaef8d89932c11a1e589d4cb2a4093b6f1c542a4ce8319d988006 languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/utils@npm:7.4.0" +"@typescript-eslint/utils@npm:8.16.0, @typescript-eslint/utils@npm:^8.8.1": + version: 8.16.0 + resolution: "@typescript-eslint/utils@npm:8.16.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@types/json-schema": "npm:^7.0.12" - "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:7.4.0" - "@typescript-eslint/types": "npm:7.4.0" - "@typescript-eslint/typescript-estree": "npm:7.4.0" - semver: "npm:^7.5.4" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" peerDependencies: - eslint: ^8.56.0 - checksum: 10/ffed27e770c486cd000ff892d9049b0afe8b9d6318452a5355b78a37436cbb414bceacae413a2ac813f3e584684825d5e0baa2e6376b7ad6013a108ac91bc19d - languageName: node - linkType: hard - -"@typescript-eslint/utils@npm:^5.45.0": - version: 5.62.0 - resolution: "@typescript-eslint/utils@npm:5.62.0" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@types/json-schema": "npm:^7.0.9" - "@types/semver": "npm:^7.3.12" - "@typescript-eslint/scope-manager": "npm:5.62.0" - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/typescript-estree": "npm:5.62.0" - eslint-scope: "npm:^5.1.1" - semver: "npm:^7.3.7" - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10/15ef13e43998a082b15f85db979f8d3ceb1f9ce4467b8016c267b1738d5e7cdb12aa90faf4b4e6dd6486c236cf9d33c463200465cf25ff997dbc0f12358550a1 + eslint: ^8.57.0 || ^9.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/80ba35b97a8e80ac2b54a56ac041b4f4583328d764e1693e7d3750de383cbcefcb7e838b75e550e8aa4df446f4b41460da6dc83543517280a4e3a61546c1a8dc languageName: node linkType: hard @@ -14302,16 +16316,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" - dependencies: - "@typescript-eslint/types": "npm:5.62.0" - eslint-visitor-keys: "npm:^3.3.0" - checksum: 10/dc613ab7569df9bbe0b2ca677635eb91839dfb2ca2c6fa47870a5da4f160db0b436f7ec0764362e756d4164e9445d49d5eb1ff0b87f4c058946ae9d8c92eb388 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:6.21.0": version: 6.21.0 resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" @@ -14322,20 +16326,94 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/visitor-keys@npm:7.4.0" +"@typescript-eslint/visitor-keys@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.16.0" dependencies: - "@typescript-eslint/types": "npm:7.4.0" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/70dc99f2ad116c6e2d9e55af249e4453e06bba2ceea515adef2d2e86e97e557865bb1b1d467667462443eb0d624baba36f7442fd1082f3874339bbc381c26e93 + "@typescript-eslint/types": "npm:8.16.0" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10/e3f231a3e8ca2f7a3dc0e9ebdc3ea1f51a377b1285727413b4c89c44dbfaf342f2574b1b4e7f478f295963045a6058e27b4827816fe2a5a2d09f565eb68522c7 languageName: node linkType: hard -"@ungap/structured-clone@npm:^1.2.0": - version: 1.2.0 - resolution: "@ungap/structured-clone@npm:1.2.0" - checksum: 10/c6fe89a505e513a7592e1438280db1c075764793a2397877ff1351721fe8792a966a5359769e30242b3cd023f2efb9e63ca2ca88019d73b564488cc20e3eab12 +"@vanilla-extract/css-utils@npm:^0.1.4": + version: 0.1.4 + resolution: "@vanilla-extract/css-utils@npm:0.1.4" + checksum: 10/37edf1f8a6dae124bc84bc0ed1260b866f563a9e4487cca6888b4eaf8d74c2f07c92566f7290e435deb38c6cba6e4c6b8fb7c22c62917afb22a9491e5d7e4c2c + languageName: node + linkType: hard + +"@vanilla-extract/css@npm:1.15.5": + version: 1.15.5 + resolution: "@vanilla-extract/css@npm:1.15.5" + dependencies: + "@emotion/hash": "npm:^0.9.0" + "@vanilla-extract/private": "npm:^1.0.6" + css-what: "npm:^6.1.0" + cssesc: "npm:^3.0.0" + csstype: "npm:^3.0.7" + dedent: "npm:^1.5.3" + deep-object-diff: "npm:^1.1.9" + deepmerge: "npm:^4.2.2" + lru-cache: "npm:^10.4.3" + media-query-parser: "npm:^2.0.2" + modern-ahocorasick: "npm:^1.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/4820caea8f7d63d5e691c72d3d324a09707040afa6b0abaaf0fea7d9ee1c133a19e5f3a383fd903453680cd0d698de0428ad2a7316e0c5e9771ffd79d813ddf6 + languageName: node + linkType: hard + +"@vanilla-extract/css@npm:^1.15.5": + version: 1.16.0 + resolution: "@vanilla-extract/css@npm:1.16.0" + dependencies: + "@emotion/hash": "npm:^0.9.0" + "@vanilla-extract/private": "npm:^1.0.6" + css-what: "npm:^6.1.0" + cssesc: "npm:^3.0.0" + csstype: "npm:^3.0.7" + dedent: "npm:^1.5.3" + deep-object-diff: "npm:^1.1.9" + deepmerge: "npm:^4.2.2" + lru-cache: "npm:^10.4.3" + media-query-parser: "npm:^2.0.2" + modern-ahocorasick: "npm:^1.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/101471e44239ca38ce2d2261a35ecf5457fb53ae346fc73fb4f1dbe545b7bc5e82fea3e0ea9c07409d53c80507410fb41260c32fcf6a5afa3be5f7cab9fab93d + languageName: node + linkType: hard + +"@vanilla-extract/dynamic@npm:2.1.2, @vanilla-extract/dynamic@npm:^2.1.2": + version: 2.1.2 + resolution: "@vanilla-extract/dynamic@npm:2.1.2" + dependencies: + "@vanilla-extract/private": "npm:^1.0.6" + checksum: 10/576b22e3f1a61abad2bc758d95f6f9eae9418f1bb6c8366211e82da7eed97ac8cf9b69fea3239832ccba280dab93d5b1def4290f64943b295f146fca78049d2d + languageName: node + linkType: hard + +"@vanilla-extract/private@npm:^1.0.6": + version: 1.0.6 + resolution: "@vanilla-extract/private@npm:1.0.6" + checksum: 10/50463610da0fc9069b3e2b33b6222ea2f005487432db9110ea430e474e29b3b756bcd1fffd47b87536358829d47bce6510398f050b5f6de07ee1e4e92eeade5a + languageName: node + linkType: hard + +"@vanilla-extract/recipes@npm:^0.5.5": + version: 0.5.5 + resolution: "@vanilla-extract/recipes@npm:0.5.5" + peerDependencies: + "@vanilla-extract/css": ^1.0.0 + checksum: 10/8d2b4f8163369424226ec9a47e754002b8a095bcf86c1a60a91b2183f59508519bd31ed41baefc950ad7ca225d75b3184c3b84d3c741c5c60d91618dd70452aa + languageName: node + linkType: hard + +"@vanilla-extract/sprinkles@npm:1.6.3": + version: 1.6.3 + resolution: "@vanilla-extract/sprinkles@npm:1.6.3" + peerDependencies: + "@vanilla-extract/css": ^1.0.0 + checksum: 10/74f8e2b189c0d48e279f1b85b5fedebf1f615ab31839964cf3861f2c5cf574567c0caeddf9c8b11327d81213f81d195efc79f136b725e6013b6d645d238d5c2b languageName: node linkType: hard @@ -14439,6 +16517,458 @@ __metadata: languageName: node linkType: hard +"@wagmi/connectors@npm:5.3.10": + version: 5.3.10 + resolution: "@wagmi/connectors@npm:5.3.10" + dependencies: + "@coinbase/wallet-sdk": "npm:4.2.3" + "@metamask/sdk": "npm:0.30.1" + "@safe-global/safe-apps-provider": "npm:0.18.4" + "@safe-global/safe-apps-sdk": "npm:9.1.0" + "@walletconnect/ethereum-provider": "npm:2.17.0" + cbw-sdk: "npm:@coinbase/wallet-sdk@3.9.3" + peerDependencies: + "@wagmi/core": 2.14.6 + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/2fa9852552d555d4dfbf45ede11ec22afd614f2b244a0bba5cd3e59196b13510d87d85b4f6677ed1949b746ab701ea47345bea3b49aa2a23441c974f5a9fe0fd + languageName: node + linkType: hard + +"@wagmi/core@npm:2.14.6": + version: 2.14.6 + resolution: "@wagmi/core@npm:2.14.6" + dependencies: + eventemitter3: "npm:5.0.1" + mipd: "npm:0.0.7" + zustand: "npm:5.0.0" + peerDependencies: + "@tanstack/query-core": ">=5.0.0" + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + "@tanstack/query-core": + optional: true + typescript: + optional: true + checksum: 10/3862660a83179e612bd357967b29b85ecd16f29030cb192eb5380f8fc7fd19ed49852b3315a012e9c2506b746dd1af870d07e9de322a3774dad2189775d3970e + languageName: node + linkType: hard + +"@wallet-standard/app@npm:^1.0.1, @wallet-standard/app@npm:^1.1.0": + version: 1.1.0 + resolution: "@wallet-standard/app@npm:1.1.0" + dependencies: + "@wallet-standard/base": "npm:^1.1.0" + checksum: 10/d233cc79fbd857689c8c14a60875df9d8ad120fa1c9d59aeeef7303489cdecd60a12bbc2f3794720aadf6ef369cf09d1409c26c0801273561bcdb12a07b08e19 + languageName: node + linkType: hard + +"@wallet-standard/base@npm:^1.0.1, @wallet-standard/base@npm:^1.1.0": + version: 1.1.0 + resolution: "@wallet-standard/base@npm:1.1.0" + checksum: 10/11dbb8ed80566265916ab193ad5eab1585d55996781a88039d2bc4480428b1e778901b2dcff3e688dcac7de45e8a9272026f37f07f1e75168caff581906c5079 + languageName: node + linkType: hard + +"@wallet-standard/core@npm:^1.0.3": + version: 1.1.0 + resolution: "@wallet-standard/core@npm:1.1.0" + dependencies: + "@wallet-standard/app": "npm:^1.1.0" + "@wallet-standard/base": "npm:^1.1.0" + "@wallet-standard/errors": "npm:^0.1.0" + "@wallet-standard/features": "npm:^1.1.0" + "@wallet-standard/wallet": "npm:^1.1.0" + checksum: 10/0f6a0045c3faa826dd64bea4245c287acb87f111396c261a199d3fa48344bf0b89f3e2bec874be8e32c215da728e7a19a9fe7365d008e8a969d980010a417c18 + languageName: node + linkType: hard + +"@wallet-standard/errors@npm:^0.1.0": + version: 0.1.0 + resolution: "@wallet-standard/errors@npm:0.1.0" + dependencies: + chalk: "npm:^5.3.0" + commander: "npm:^12.1.0" + bin: + errors: bin/cli.mjs + checksum: 10/5d00d0ddf5cb77cc1c9804ef7ef11fd8c7a5281c9642e9bc040a3bb01b809ef4caee7c784b695fbe61ed93bcf2c8c4d624c8b2c91050c94b0f356b124942951d + languageName: node + linkType: hard + +"@wallet-standard/features@npm:^1.0.3, @wallet-standard/features@npm:^1.1.0": + version: 1.1.0 + resolution: "@wallet-standard/features@npm:1.1.0" + dependencies: + "@wallet-standard/base": "npm:^1.1.0" + checksum: 10/e046f813ec4bfea172aeb6c11358a962afe8f9a6961453e621d624f89d8b5fc8a44404dacfe18d33be815df6e9117bbf914009f5a9f9ea91ff90a136043fcac8 + languageName: node + linkType: hard + +"@wallet-standard/wallet@npm:^1.0.1, @wallet-standard/wallet@npm:^1.1.0": + version: 1.1.0 + resolution: "@wallet-standard/wallet@npm:1.1.0" + dependencies: + "@wallet-standard/base": "npm:^1.1.0" + checksum: 10/b56846709c43b1dee6b44f7a9e15d89a00e4408d3d967eb438f415b42c5c52c4cf33a7b3126d0cf0dc0d78f244755e3d084a05824c1397ce58be169426c5337b + languageName: node + linkType: hard + +"@walletconnect/core@npm:2.17.0": + version: 2.17.0 + resolution: "@walletconnect/core@npm:2.17.0" + dependencies: + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/jsonrpc-ws-connection": "npm:1.0.14" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.0.4" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.17.0" + "@walletconnect/utils": "npm:2.17.0" + events: "npm:3.3.0" + lodash.isequal: "npm:4.5.0" + uint8arrays: "npm:3.1.0" + checksum: 10/a37eff1a9b479fe1d51b4173128adecc0b9afd4897d912b396d19e5c2df6a928caa0fdb487f47ca26fae7f3ca59f263754f21b1861a178cfc11b4b2a783e50c4 + languageName: node + linkType: hard + +"@walletconnect/environment@npm:^1.0.1": + version: 1.0.1 + resolution: "@walletconnect/environment@npm:1.0.1" + dependencies: + tslib: "npm:1.14.1" + checksum: 10/f6a1e3456e50cc7cfa58d99fd513ecac75573d0b8bcbbedcb1d7ec04ca9108df16b471afd40761b2a5cb4f66d8e33b7ba25f02c62c8365d68b1bd1ef52c1813e + languageName: node + linkType: hard + +"@walletconnect/ethereum-provider@npm:2.17.0": + version: 2.17.0 + resolution: "@walletconnect/ethereum-provider@npm:2.17.0" + dependencies: + "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/modal": "npm:2.7.0" + "@walletconnect/sign-client": "npm:2.17.0" + "@walletconnect/types": "npm:2.17.0" + "@walletconnect/universal-provider": "npm:2.17.0" + "@walletconnect/utils": "npm:2.17.0" + events: "npm:3.3.0" + checksum: 10/7f86efca38e6a1a59623de090296f5beff3886af50757ea024c6c0d3237e7dd7e3719be979770d4257dfae3708b1c33a242fb061b9f981fe298d666522a2610f + languageName: node + linkType: hard + +"@walletconnect/events@npm:1.0.1, @walletconnect/events@npm:^1.0.1": + version: 1.0.1 + resolution: "@walletconnect/events@npm:1.0.1" + dependencies: + keyvaluestorage-interface: "npm:^1.0.0" + tslib: "npm:1.14.1" + checksum: 10/b5a105e9ac4d7d0a500085afd77b71e71a8ab78fd38b033e4ce91f8626fd8c254b1ba49a59c8c0ed8a00a7e8b93995163f414eda73c58694f8f830e453a902b6 + languageName: node + linkType: hard + +"@walletconnect/heartbeat@npm:1.2.1": + version: 1.2.1 + resolution: "@walletconnect/heartbeat@npm:1.2.1" + dependencies: + "@walletconnect/events": "npm:^1.0.1" + "@walletconnect/time": "npm:^1.0.2" + tslib: "npm:1.14.1" + checksum: 10/a68d7efe4e69c9749dd7c3a9e351dd22adccbb925447dd7f2b2978a4cd730695cc0b4e717a08bad0d0c60e0177b77618a53f3bfb4347659f3ccfe72d412c27fb + languageName: node + linkType: hard + +"@walletconnect/heartbeat@npm:1.2.2": + version: 1.2.2 + resolution: "@walletconnect/heartbeat@npm:1.2.2" + dependencies: + "@walletconnect/events": "npm:^1.0.1" + "@walletconnect/time": "npm:^1.0.2" + events: "npm:^3.3.0" + checksum: 10/f3a1c3c255ac9bd374b25e1ef65a61b1f623b9118d48471acaac1f9ee4ee1438d8d8cbc77733cdd980809b468443c046328fe5ac4084e01e0892f8c699cf44e7 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-http-connection@npm:1.0.8": + version: 1.0.8 + resolution: "@walletconnect/jsonrpc-http-connection@npm:1.0.8" + dependencies: + "@walletconnect/jsonrpc-utils": "npm:^1.0.6" + "@walletconnect/safe-json": "npm:^1.0.1" + cross-fetch: "npm:^3.1.4" + events: "npm:^3.3.0" + checksum: 10/c545906243df27fdbde3c8e9005217069dd22ce0f496c59f55843ca8fcb0c1a90d2c0ac6ecb16fa110ed85c36e5486f5a74621a5ca6230667d77ee3b0ae36cc6 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-provider@npm:1.0.14": + version: 1.0.14 + resolution: "@walletconnect/jsonrpc-provider@npm:1.0.14" + dependencies: + "@walletconnect/jsonrpc-utils": "npm:^1.0.8" + "@walletconnect/safe-json": "npm:^1.0.2" + events: "npm:^3.3.0" + checksum: 10/c3c78f00148043b70213f5174d537b210f1fb231d96103cbf7d0101626578d3c13fe99ac080df7a0056c7128ce488b0523eda0e3d1deed75754672848b4909a5 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-types@npm:1.0.3": + version: 1.0.3 + resolution: "@walletconnect/jsonrpc-types@npm:1.0.3" + dependencies: + keyvaluestorage-interface: "npm:^1.0.0" + tslib: "npm:1.14.1" + checksum: 10/7b1209c2e6ff476e45b0d828bd4d7773873c4cff41e5ed235ff8014b4e8ff09ec704817347702fe3b8ca1c1b7920abfd0af94e0cdf582a92d8a0192d8c42dce8 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-types@npm:1.0.4, @walletconnect/jsonrpc-types@npm:^1.0.2, @walletconnect/jsonrpc-types@npm:^1.0.3": + version: 1.0.4 + resolution: "@walletconnect/jsonrpc-types@npm:1.0.4" + dependencies: + events: "npm:^3.3.0" + keyvaluestorage-interface: "npm:^1.0.0" + checksum: 10/8cdc9f7b5e3ae0d702a44a6fc4c388a2b627188df758ffd103ba9aac6596a787d2f319aa8f6928a03d990c71c17d9b876028f36b8e0c37bd5c9026231ed9ba45 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-utils@npm:1.0.8, @walletconnect/jsonrpc-utils@npm:^1.0.6, @walletconnect/jsonrpc-utils@npm:^1.0.8": + version: 1.0.8 + resolution: "@walletconnect/jsonrpc-utils@npm:1.0.8" + dependencies: + "@walletconnect/environment": "npm:^1.0.1" + "@walletconnect/jsonrpc-types": "npm:^1.0.3" + tslib: "npm:1.14.1" + checksum: 10/4687b4582a5c33883d94e87ca8bb22d129a2a47b6e1d9e2c3210b74f02d9677723b3bf2283d2f0fa69866b0a66a80cdfada9a2f1c204d485fbd10d2baed1f0a6 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-ws-connection@npm:1.0.14": + version: 1.0.14 + resolution: "@walletconnect/jsonrpc-ws-connection@npm:1.0.14" + dependencies: + "@walletconnect/jsonrpc-utils": "npm:^1.0.6" + "@walletconnect/safe-json": "npm:^1.0.2" + events: "npm:^3.3.0" + ws: "npm:^7.5.1" + checksum: 10/2ad66217b62fb57a43c8edd33c27da0c9ba09cfec79f4d43e5d30bcb8224a48c1d1f0d6273be0371f2c7e33d8138a6fe03afa499b429ab7829d719677cd48f4d + languageName: node + linkType: hard + +"@walletconnect/keyvaluestorage@npm:1.1.1, @walletconnect/keyvaluestorage@npm:^1.1.1": + version: 1.1.1 + resolution: "@walletconnect/keyvaluestorage@npm:1.1.1" + dependencies: + "@walletconnect/safe-json": "npm:^1.0.1" + idb-keyval: "npm:^6.2.1" + unstorage: "npm:^1.9.0" + peerDependencies: + "@react-native-async-storage/async-storage": 1.x + peerDependenciesMeta: + "@react-native-async-storage/async-storage": + optional: true + checksum: 10/fd9c275b3249d8e9f722866703b5c040eb35d0670c92a297428ffb700ac36c6b9978242beac5d2cfe97eb522ae01307cacd9c79ecf95640878804fce0f13c5e7 + languageName: node + linkType: hard + +"@walletconnect/logger@npm:2.1.2, @walletconnect/logger@npm:^2.0.1": + version: 2.1.2 + resolution: "@walletconnect/logger@npm:2.1.2" + dependencies: + "@walletconnect/safe-json": "npm:^1.0.2" + pino: "npm:7.11.0" + checksum: 10/2e6d438bd352595fff6691712c83953e3ad6b2b9ab298c5a8b670a024f53a3f744b165e5aa081a79261ee4801b93b6c60698a39947d613d49a8f6e6215ecd4c2 + languageName: node + linkType: hard + +"@walletconnect/modal-core@npm:2.7.0": + version: 2.7.0 + resolution: "@walletconnect/modal-core@npm:2.7.0" + dependencies: + valtio: "npm:1.11.2" + checksum: 10/1549f9ba5c98dfed2f97fbfccfcd2e342550c7ba7a85970bff224258dd397bad0a29721b90fef408dcc6cdfa65c52253476a04c16fece9b4d48792f03c3a4b4f + languageName: node + linkType: hard + +"@walletconnect/modal-ui@npm:2.7.0": + version: 2.7.0 + resolution: "@walletconnect/modal-ui@npm:2.7.0" + dependencies: + "@walletconnect/modal-core": "npm:2.7.0" + lit: "npm:2.8.0" + motion: "npm:10.16.2" + qrcode: "npm:1.5.3" + checksum: 10/00d17001bde7646def34eaffef81c4a580f09fdf10902a7a938cd2a3738f8f1cbb10520c229989b64e147df9f4df8ca31bd1d904f9019acc63327b495fb5b3ed + languageName: node + linkType: hard + +"@walletconnect/modal@npm:2.7.0": + version: 2.7.0 + resolution: "@walletconnect/modal@npm:2.7.0" + dependencies: + "@walletconnect/modal-core": "npm:2.7.0" + "@walletconnect/modal-ui": "npm:2.7.0" + checksum: 10/a6b78cc06479e0aa98516784ff1f81b24839777f0ec38d2f9cc85b4dc932ad6e823187bbb699f80f898e7d4b09d1232134f348eb9d12697e74e742eeaec189f2 + languageName: node + linkType: hard + +"@walletconnect/relay-api@npm:1.0.11": + version: 1.0.11 + resolution: "@walletconnect/relay-api@npm:1.0.11" + dependencies: + "@walletconnect/jsonrpc-types": "npm:^1.0.2" + checksum: 10/d85f88b9744917ee5b36d2df23bf4012819b14b73229f9bdca942bee11dd3b3428808c7528c2b1f6b3d91fa1d34a22b1e20b46533e402301318cbd4ab59b9c17 + languageName: node + linkType: hard + +"@walletconnect/relay-auth@npm:1.0.4": + version: 1.0.4 + resolution: "@walletconnect/relay-auth@npm:1.0.4" + dependencies: + "@stablelib/ed25519": "npm:^1.0.2" + "@stablelib/random": "npm:^1.0.1" + "@walletconnect/safe-json": "npm:^1.0.1" + "@walletconnect/time": "npm:^1.0.2" + tslib: "npm:1.14.1" + uint8arrays: "npm:^3.0.0" + checksum: 10/d9128b2a25f38ebf2f49f8c184dad5c997ad6343513bddd7941459c2f2757e6acfbcdd36dc9c12d0491f55723d5e2c5c0ee2e9cf381b3247274b920e95d4db0e + languageName: node + linkType: hard + +"@walletconnect/safe-json@npm:1.0.2, @walletconnect/safe-json@npm:^1.0.1, @walletconnect/safe-json@npm:^1.0.2": + version: 1.0.2 + resolution: "@walletconnect/safe-json@npm:1.0.2" + dependencies: + tslib: "npm:1.14.1" + checksum: 10/b9d031dab3916d20fa5241d7ad2be425368ae489995ba3ba18d6ad88e81ad3ed093b8e867b8a4fc44759099896aeb5afee5635858cb80c4819ebc7ebb71ed5a6 + languageName: node + linkType: hard + +"@walletconnect/sign-client@npm:2.17.0": + version: 2.17.0 + resolution: "@walletconnect/sign-client@npm:2.17.0" + dependencies: + "@walletconnect/core": "npm:2.17.0" + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.17.0" + "@walletconnect/utils": "npm:2.17.0" + events: "npm:3.3.0" + checksum: 10/e3eb391b4f01ae353e7c5f3580971ac7e5b9bd5a6bdb77783d8954e9c0243bb32945de230cfd09fddb2a589f28a9359de8ca313e83eae2b2e396753957d87b4c + languageName: node + linkType: hard + +"@walletconnect/time@npm:1.0.2, @walletconnect/time@npm:^1.0.2": + version: 1.0.2 + resolution: "@walletconnect/time@npm:1.0.2" + dependencies: + tslib: "npm:1.14.1" + checksum: 10/ea84d0850e63306837f98a228e08a59f6945da38ba5553b1f158abeaa8ec4dc8a0025a0f0cfc843ddf05ce2947da95c02ac1e8cedce7092bbe1c2d46ca816dd9 + languageName: node + linkType: hard + +"@walletconnect/types@npm:2.11.0": + version: 2.11.0 + resolution: "@walletconnect/types@npm:2.11.0" + dependencies: + "@walletconnect/events": "npm:^1.0.1" + "@walletconnect/heartbeat": "npm:1.2.1" + "@walletconnect/jsonrpc-types": "npm:1.0.3" + "@walletconnect/keyvaluestorage": "npm:^1.1.1" + "@walletconnect/logger": "npm:^2.0.1" + events: "npm:^3.3.0" + checksum: 10/72602e3a38dbc8db789467cbf40fad5500c2fdf736277dde11990b8d11d989c3c075cb3ab8a6e88dc7f1a6b559cd791a2aa5da843239c34a0ee726d1f73fd723 + languageName: node + linkType: hard + +"@walletconnect/types@npm:2.17.0": + version: 2.17.0 + resolution: "@walletconnect/types@npm:2.17.0" + dependencies: + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:2.1.2" + events: "npm:3.3.0" + checksum: 10/a0ac222a0ef92c88c159178e643752345978c69109522adea6bebb217c29a182337b2698cb16864151a0c79a457ea1b8659602af1f00dd45e1bd5308c89585cf + languageName: node + linkType: hard + +"@walletconnect/universal-provider@npm:2.17.0": + version: 2.17.0 + resolution: "@walletconnect/universal-provider@npm:2.17.0" + dependencies: + "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/sign-client": "npm:2.17.0" + "@walletconnect/types": "npm:2.17.0" + "@walletconnect/utils": "npm:2.17.0" + events: "npm:3.3.0" + checksum: 10/d03d5178677864c996460eb48072e7f9ca290fe2a1f660f4b9ec8c52e3d574af483fdbca8a95206cbe41cbc89a21b75b2ad13c55ababd3cad2e9a6e3567d2a0a + languageName: node + linkType: hard + +"@walletconnect/utils@npm:2.17.0": + version: 2.17.0 + resolution: "@walletconnect/utils@npm:2.17.0" + dependencies: + "@stablelib/chacha20poly1305": "npm:1.0.1" + "@stablelib/hkdf": "npm:1.0.1" + "@stablelib/random": "npm:1.0.2" + "@stablelib/sha256": "npm:1.0.1" + "@stablelib/x25519": "npm:1.0.3" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.0.4" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.17.0" + "@walletconnect/window-getters": "npm:1.0.1" + "@walletconnect/window-metadata": "npm:1.0.1" + detect-browser: "npm:5.3.0" + elliptic: "npm:^6.5.7" + query-string: "npm:7.1.3" + uint8arrays: "npm:3.1.0" + checksum: 10/b460aeb0eb0c8e9d50677596f5fd04f940a922027b4d348e53b026e4290cb67a08941bfc6dc0fad8ae55d7434554fbe07907741658845f710a1befa39e47698c + languageName: node + linkType: hard + +"@walletconnect/window-getters@npm:1.0.1, @walletconnect/window-getters@npm:^1.0.1": + version: 1.0.1 + resolution: "@walletconnect/window-getters@npm:1.0.1" + dependencies: + tslib: "npm:1.14.1" + checksum: 10/8d3fcb134fbbe903ba4a63f1fa5a7849fd443874bf45488260afc2fe3b1cbe211f86da1d76ee844be7c0e8618ae67402f94c213432fd80b04715eaf72e2e00e3 + languageName: node + linkType: hard + +"@walletconnect/window-metadata@npm:1.0.1": + version: 1.0.1 + resolution: "@walletconnect/window-metadata@npm:1.0.1" + dependencies: + "@walletconnect/window-getters": "npm:^1.0.1" + tslib: "npm:1.14.1" + checksum: 10/cf322e0860c4448cefcd81f34bc6d49d1a235a81e74a6146baefb74e47cf6c3c8050b65e534a3dc13f8d2aed3fc59732ccf48d5a01b5b23e08e1847fcffa950c + languageName: node + linkType: hard + "@yarnpkg/esbuild-plugin-pnp@npm:^3.0.0-rc.10": version: 3.0.0-rc.15 resolution: "@yarnpkg/esbuild-plugin-pnp@npm:3.0.0-rc.15" @@ -14477,6 +17007,29 @@ __metadata: languageName: node linkType: hard +"@zag-js/dom-query@npm:0.31.1": + version: 0.31.1 + resolution: "@zag-js/dom-query@npm:0.31.1" + checksum: 10/61f7c28e7bccf1568eb4bbf7b74a9ba41f991be792098f9c512359fae70b460895c3e48f46f86738174b079dbeab58d6771fd2565927cfe2f6c05cb50bfa812f + languageName: node + linkType: hard + +"@zag-js/element-size@npm:0.31.1": + version: 0.31.1 + resolution: "@zag-js/element-size@npm:0.31.1" + checksum: 10/a0bd5937cacce2da7705912dd79d49b162a28f9bb3f14171bc1d11aca07be9754a555d5af661c45d102833dc4558fb07ccb3aa89b9922339871bf03c7b8fa9d1 + languageName: node + linkType: hard + +"@zag-js/focus-visible@npm:^0.31.1": + version: 0.31.1 + resolution: "@zag-js/focus-visible@npm:0.31.1" + dependencies: + "@zag-js/dom-query": "npm:0.31.1" + checksum: 10/da879cf88d28a4c2da4d3f7e17797ef43dbdd9e00d6eafc72d6728be32785015bafcbc60736146fa819b33ae48bde5d8f1c9f4afff646b0dc7a7f4701b2dcd6d + languageName: node + linkType: hard + "JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -14503,7 +17056,7 @@ __metadata: languageName: node linkType: hard -"abitype@npm:1.0.6": +"abitype@npm:1.0.6, abitype@npm:^1.0.6": version: 1.0.6 resolution: "abitype@npm:1.0.6" peerDependencies: @@ -14649,7 +17202,16 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.4.1, acorn@npm:^8.9.0": +"acorn@npm:^8.14.0": + version: 8.14.0 + resolution: "acorn@npm:8.14.0" + bin: + acorn: bin/acorn + checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 + languageName: node + linkType: hard + +"acorn@npm:^8.4.1": version: 8.11.3 resolution: "acorn@npm:8.11.3" bin: @@ -14787,6 +17349,13 @@ __metadata: languageName: node linkType: hard +"animejs@npm:^3.2.2": + version: 3.2.2 + resolution: "animejs@npm:3.2.2" + checksum: 10/7abdb56f415c666ba02f4e64fdbb10d457fed7e3711b0f006f97e48e5650097013397d890e8ceb31e9e06b73bf6dfd9202309d0dae0fc0b5190aa7c4e0ab7054 + languageName: node + linkType: hard + "ansi-align@npm:^3.0.0": version: 3.0.1 resolution: "ansi-align@npm:3.0.1" @@ -14941,7 +17510,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3": +"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -15032,7 +17601,7 @@ __metadata: languageName: node linkType: hard -"aria-hidden@npm:^1.1.1": +"aria-hidden@npm:^1.1.1, aria-hidden@npm:^1.2.3": version: 1.2.4 resolution: "aria-hidden@npm:1.2.4" dependencies: @@ -15155,7 +17724,21 @@ __metadata: languageName: node linkType: hard -"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.3.1": +"array.prototype.findlastindex@npm:^1.2.5": + version: 1.2.5 + resolution: "array.prototype.findlastindex@npm:1.2.5" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10/7c5c821f357cd53ab6cc305de8086430dd8d7a2485db87b13f843e868055e9582b1fd338f02338f67fc3a1603ceaf9610dd2a470b0b506f9d18934780f95b246 + languageName: node + linkType: hard + +"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": version: 1.3.2 resolution: "array.prototype.flat@npm:1.3.2" dependencies: @@ -15259,7 +17842,7 @@ __metadata: languageName: node linkType: hard -"asn1.js@npm:5.4.1, asn1.js@npm:^5.4.1": +"asn1.js@npm:^5.4.1": version: 5.4.1 resolution: "asn1.js@npm:5.4.1" dependencies: @@ -15357,6 +17940,15 @@ __metadata: languageName: node linkType: hard +"async-mutex@npm:^0.2.6": + version: 0.2.6 + resolution: "async-mutex@npm:0.2.6" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10/3cf676fc48b4686abf534cc02d4784bab3f35d7836a0a7476c96e57c3f6607dd3d94cc0989b29d33ce5ae5cde8be8e1a96f3e769ba3b0e1ba4a244f873aa5623 + languageName: node + linkType: hard + "async-mutex@npm:^0.4.0": version: 0.4.1 resolution: "async-mutex@npm:0.4.1" @@ -15565,6 +18157,17 @@ __metadata: languageName: node linkType: hard +"babel-plugin-macros@npm:^3.1.0": + version: 3.1.0 + resolution: "babel-plugin-macros@npm:3.1.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + cosmiconfig: "npm:^7.0.0" + resolve: "npm:^1.19.0" + checksum: 10/30be6ca45e9a124c58ca00af9a0753e5410ec0b79a737714fc4722bbbeb693e55d9258f05c437145ef4a867c2d1603e06a1c292d66c243ce1227458c8ea2ca8c + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs2@npm:^0.4.10": version: 0.4.11 resolution: "babel-plugin-polyfill-corejs2@npm:0.4.11" @@ -15651,6 +18254,13 @@ __metadata: languageName: node linkType: hard +"base-x@npm:^4.0.0": + version: 4.0.0 + resolution: "base-x@npm:4.0.0" + checksum: 10/b25db9e07eb1998472a20557c7f00c797dc0595f79df95155ab74274e7fa98b9f2659b3ee547ac8773666b7f69540656793aeb97ad2b1ceccdb6fa5faaf69ac0 + languageName: node + linkType: hard + "base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -15692,7 +18302,14 @@ __metadata: languageName: node linkType: hard -"big-integer@npm:^1.6.44": +"bfs-path@npm:^1.0.2": + version: 1.0.2 + resolution: "bfs-path@npm:1.0.2" + checksum: 10/0c629521b925864315ed8c73b05aff561bac9819abc872180aace924b35b337a98ffd430a1b1f580896003c7e991527f761d2a4d4161edcad844a2f122cb9489 + languageName: node + linkType: hard + +"big-integer@npm:^1.6.44, big-integer@npm:^1.6.48": version: 1.6.52 resolution: "big-integer@npm:1.6.52" checksum: 10/4bc6ae152a96edc9f95020f5fc66b13d26a9ad9a021225a9f0213f7e3dc44269f423aa8c42e19d6ac4a63bb2b22140b95d10be8f9ca7a6d9aa1b22b330d1f514 @@ -15732,6 +18349,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:9.1.2, bignumber.js@npm:^9.1.2": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 10/d89b8800a987225d2c00dcbf8a69dc08e92aa0880157c851c287b307d31ceb2fc2acb0c62c3e3a3d42b6c5fcae9b004035f13eb4386e56d529d7edac18d5c9d8 + languageName: node + linkType: hard + "bignumber.js@npm:^9.0.0, bignumber.js@npm:^9.0.1": version: 9.0.2 resolution: "bignumber.js@npm:9.0.2" @@ -15769,6 +18393,21 @@ __metadata: languageName: node linkType: hard +"bip32@npm:^2.0.6": + version: 2.0.6 + resolution: "bip32@npm:2.0.6" + dependencies: + "@types/node": "npm:10.12.18" + bs58check: "npm:^2.1.1" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + tiny-secp256k1: "npm:^1.1.3" + typeforce: "npm:^1.11.5" + wif: "npm:^2.0.6" + checksum: 10/9d71a946722c302b080771ab22dc83a0d7337a29845f80715c9d258cddaf42bb42b35ab49c3498e5cc60b27c1864397d6bdac713c9726ecf72c04f07b4cb5d40 + languageName: node + linkType: hard + "bip39@npm:3.0.4": version: 3.0.4 resolution: "bip39@npm:3.0.4" @@ -15781,6 +18420,15 @@ __metadata: languageName: node linkType: hard +"bip39@npm:^3.0.3": + version: 3.1.0 + resolution: "bip39@npm:3.1.0" + dependencies: + "@noble/hashes": "npm:^1.2.0" + checksum: 10/406c0b5bdab0d43df2ff8916c5b57bb7f65cd36a115cbd7620a75b972638f8511840bfc27bfc68946027086fd33ed3050d8cd304e85fd527503b23d857a8486e + languageName: node + linkType: hard + "birpc@npm:0.2.14": version: 0.2.14 resolution: "birpc@npm:0.2.14" @@ -15912,7 +18560,7 @@ __metadata: languageName: node linkType: hard -"bowser@npm:^2.11.0": +"bowser@npm:2.11.0, bowser@npm:^2.11.0, bowser@npm:^2.9.0": version: 2.11.0 resolution: "bowser@npm:2.11.0" checksum: 10/ef46500eafe35072455e7c3ae771244e97827e0626686a9a3601c436d16eb272dad7ccbd49e2130b599b617ca9daa67027de827ffc4c220e02f63c84b69a8751 @@ -16083,7 +18731,16 @@ __metadata: languageName: node linkType: hard -"bs58check@npm:^2.1.2": +"bs58@npm:^5.0.0": + version: 5.0.0 + resolution: "bs58@npm:5.0.0" + dependencies: + base-x: "npm:^4.0.0" + checksum: 10/2475cb0684e07077521aac718e604a13e0f891d58cff923d437a2f7e9e28703ab39fce9f84c7c703ab369815a675f11e3bd394d38643bfe8969fbe42e6833d45 + languageName: node + linkType: hard + +"bs58check@npm:<3.0.0, bs58check@npm:^2.1.1, bs58check@npm:^2.1.2": version: 2.1.2 resolution: "bs58check@npm:2.1.2" dependencies: @@ -16205,6 +18862,16 @@ __metadata: languageName: node linkType: hard +"bufferutil@npm:^4.0.8": + version: 4.0.8 + resolution: "bufferutil@npm:4.0.8" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/d9337badc960a19d5a031db5de47159d7d8a11b6bab399bdfbf464ffa9ecd2972fef19bb61a7d2827e0c55f912c20713e12343386b86cb013f2b99c2324ab6a3 + languageName: node + linkType: hard + "bufio@npm:^1.0.7": version: 1.2.0 resolution: "bufio@npm:1.2.0" @@ -16449,6 +19116,23 @@ __metadata: languageName: node linkType: hard +"cbw-sdk@npm:@coinbase/wallet-sdk@3.9.3": + version: 3.9.3 + resolution: "@coinbase/wallet-sdk@npm:3.9.3" + dependencies: + bn.js: "npm:^5.2.1" + buffer: "npm:^6.0.3" + clsx: "npm:^1.2.1" + eth-block-tracker: "npm:^7.1.0" + eth-json-rpc-filters: "npm:^6.0.0" + eventemitter3: "npm:^5.0.1" + keccak: "npm:^3.0.3" + preact: "npm:^10.16.0" + sha.js: "npm:^2.4.11" + checksum: 10/3bc3f0edad8ea46cb7a127993373093d95b6fef03d2a6a40bae7983a1d9a20a114faa8e7bf1230efd380ffb67b42dae405c6617cd6fad6d278bf9b9e021a0280 + languageName: node + linkType: hard + "chai-as-promised@npm:^8.0.0": version: 8.0.0 resolution: "chai-as-promised@npm:8.0.0" @@ -16460,7 +19144,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:4.5.0, chai@npm:^4.3.10, chai@npm:^4.3.7, chai@npm:^4.5.0": +"chai@npm:^4.3.10, chai@npm:^4.3.7, chai@npm:^4.5.0": version: 4.5.0 resolution: "chai@npm:4.5.0" dependencies: @@ -16505,6 +19189,22 @@ __metadata: languageName: node linkType: hard +"chalk-template@npm:1.1.0": + version: 1.1.0 + resolution: "chalk-template@npm:1.1.0" + dependencies: + chalk: "npm:^5.2.0" + checksum: 10/868aae8d4e7556ad2f35de4e04fe65dbe1ea6c5c80ad783f1c156d0a5c33f444c6814f49cbb68fe348c78e99daf2bcf566b47ad7e13603e4691ca78b2f422824 + languageName: node + linkType: hard + +"chalk@npm:5.3.0, chalk@npm:^5.2.0, chalk@npm:^5.3.0": + version: 5.3.0 + resolution: "chalk@npm:5.3.0" + checksum: 10/6373caaab21bd64c405bfc4bd9672b145647fc9482657b5ea1d549b3b2765054e9d3d928870cdf764fb4aad67555f5061538ff247b8310f110c5c888d92397ea + languageName: node + linkType: hard + "chalk@npm:^2.0.0, chalk@npm:^2.1.0, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -16536,13 +19236,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.3.0": - version: 5.3.0 - resolution: "chalk@npm:5.3.0" - checksum: 10/6373caaab21bd64c405bfc4bd9672b145647fc9482657b5ea1d549b3b2765054e9d3d928870cdf764fb4aad67555f5061538ff247b8310f110c5c888d92397ea - languageName: node - linkType: hard - "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -16695,7 +19388,7 @@ __metadata: languageName: node linkType: hard -"citty@npm:^0.1.6": +"citty@npm:^0.1.5, citty@npm:^0.1.6": version: 0.1.6 resolution: "citty@npm:0.1.6" dependencies: @@ -16769,6 +19462,15 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^4.0.0": + version: 4.0.0 + resolution: "cli-cursor@npm:4.0.0" + dependencies: + restore-cursor: "npm:^4.0.0" + checksum: 10/ab3f3ea2076e2176a1da29f9d64f72ec3efad51c0960898b56c8a17671365c26e67b735920530eaf7328d61f8bd41c27f46b9cf6e4e10fe2fa44b5e8c0e392cc + languageName: node + linkType: hard + "cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.2": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" @@ -16776,13 +19478,6 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:^2.8.0": - version: 2.9.0 - resolution: "cli-spinners@npm:2.9.0" - checksum: 10/457497ccef70eec3f1d0825e4a3396ba43f6833a4900c2047c0efe2beecb1c0df476949ea378bcb6595754f7508e28ae943eeb30bbda807f59f547b270ec334c - languageName: node - linkType: hard - "cli-table3@npm:^0.5.0": version: 0.5.1 resolution: "cli-table3@npm:0.5.1" @@ -16843,13 +19538,6 @@ __metadata: languageName: node linkType: hard -"cli-width@npm:^4.0.0": - version: 4.0.0 - resolution: "cli-width@npm:4.0.0" - checksum: 10/6de44fee34dadfc95a68ba012ea4d06d776289c251a283473e5ee240f26bbade4816766eb699c78b91804943c405097155bddf8c3e492daf1da7d9ab38a89878 - languageName: node - linkType: hard - "cli-width@npm:^4.1.0": version: 4.1.0 resolution: "cli-width@npm:4.1.0" @@ -16857,6 +19545,24 @@ __metadata: languageName: node linkType: hard +"client-only@npm:^0.0.1": + version: 0.0.1 + resolution: "client-only@npm:0.0.1" + checksum: 10/0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8 + languageName: node + linkType: hard + +"clipboardy@npm:^4.0.0": + version: 4.0.0 + resolution: "clipboardy@npm:4.0.0" + dependencies: + execa: "npm:^8.0.1" + is-wsl: "npm:^3.1.0" + is64bit: "npm:^2.0.0" + checksum: 10/ec4ebe7e5c81d9c9cb994637e7b0e068c1c8fc272167ecd5519f967427271ec66e0e64da7268a2630b860eff42933aeabe25ba5e42bb80dbf1fae6362df059ed + languageName: node + linkType: hard + "cliui@npm:^5.0.0": version: 5.0.0 resolution: "cliui@npm:5.0.0" @@ -16928,13 +19634,20 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^2.0.0, clsx@npm:^2.1.1": +"clsx@npm:2.1.1, clsx@npm:^2.0.0, clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" checksum: 10/cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919 languageName: node linkType: hard +"clsx@npm:^1.2.1": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 10/5ded6f61f15f1fa0350e691ccec43a28b12fb8e64c8e94715f2a937bc3722d4c3ed41d6e945c971fc4dcc2a7213a43323beaf2e1c28654af63ba70c9968a8643 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -16997,6 +19710,13 @@ __metadata: languageName: node linkType: hard +"color2k@npm:^2.0.2": + version: 2.0.3 + resolution: "color2k@npm:2.0.3" + checksum: 10/63385b3c43749a96a4edfd5f4d30103f850e5a4ab01ad39ec70bebd940a237ab79cbd2d7b2bf4eede6ef6122a1b904877f628500fdc5521310e39d3572370d6c + languageName: node + linkType: hard + "colorette@npm:^2.0.16": version: 2.0.17 resolution: "colorette@npm:2.0.17" @@ -17051,6 +19771,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:12.1.0, commander@npm:^12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93 + languageName: node + linkType: hard + "commander@npm:3.0.2": version: 3.0.2 resolution: "commander@npm:3.0.2" @@ -17065,13 +19792,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^12.1.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93 - languageName: node - linkType: hard - "commander@npm:^2.20.3": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -17229,6 +19949,13 @@ __metadata: languageName: node linkType: hard +"convert-source-map@npm:^1.5.0": + version: 1.9.0 + resolution: "convert-source-map@npm:1.9.0" + checksum: 10/dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 + languageName: node + linkType: hard + "convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" @@ -17236,6 +19963,13 @@ __metadata: languageName: node linkType: hard +"cookie-es@npm:^1.2.2": + version: 1.2.2 + resolution: "cookie-es@npm:1.2.2" + checksum: 10/0fd742c11caa185928e450543f84df62d4b2c1fc7b5041196b57b7db04e1c6ac6585fb40e4f579a2819efefd2d6a9cbb4d17f71240d05f4dcd8f74ae81341a20 + languageName: node + linkType: hard + "cookie-signature@npm:1.0.6": version: 1.0.6 resolution: "cookie-signature@npm:1.0.6" @@ -17264,6 +19998,15 @@ __metadata: languageName: node linkType: hard +"copy-to-clipboard@npm:3.3.3, copy-to-clipboard@npm:^3.3.3": + version: 3.3.3 + resolution: "copy-to-clipboard@npm:3.3.3" + dependencies: + toggle-selection: "npm:^1.0.6" + checksum: 10/e0a325e39b7615108e6c1c8ac110ae7b829cdc4ee3278b1df6a0e4228c490442cc86444cd643e2da344fbc424b3aab8909e2fec82f8bc75e7e5b190b7c24eecf + languageName: node + linkType: hard + "core-js-compat@npm:^3.36.1, core-js-compat@npm:^3.37.1": version: 3.37.1 resolution: "core-js-compat@npm:3.37.1" @@ -17304,6 +20047,36 @@ __metadata: languageName: node linkType: hard +"cosmiconfig@npm:9.0.0": + version: 9.0.0 + resolution: "cosmiconfig@npm:9.0.0" + dependencies: + env-paths: "npm:^2.2.1" + import-fresh: "npm:^3.3.0" + js-yaml: "npm:^4.1.0" + parse-json: "npm:^5.2.0" + peerDependencies: + typescript: ">=4.9.5" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/8bdf1dfbb6fdb3755195b6886dc0649a3c742ec75afa4cb8da7b070936aed22a4f4e5b7359faafe03180358f311dbc300d248fd6586c458203d376a40cc77826 + languageName: node + linkType: hard + +"cosmiconfig@npm:^7.0.0": + version: 7.1.0 + resolution: "cosmiconfig@npm:7.1.0" + dependencies: + "@types/parse-json": "npm:^4.0.0" + import-fresh: "npm:^3.2.1" + parse-json: "npm:^5.0.0" + path-type: "npm:^4.0.0" + yaml: "npm:^1.10.0" + checksum: 10/03600bb3870c80ed151b7b706b99a1f6d78df8f4bdad9c95485072ea13358ef294b13dd99f9e7bf4cc0b43bcd3599d40df7e648750d21c2f6817ca2cd687e071 + languageName: node + linkType: hard + "cosmiconfig@npm:^8.0.0": version: 8.3.6 resolution: "cosmiconfig@npm:8.3.6" @@ -17397,6 +20170,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^4.0.0": + version: 4.0.0 + resolution: "cross-fetch@npm:4.0.0" + dependencies: + node-fetch: "npm:^2.6.12" + checksum: 10/e231a71926644ef122d334a3a4e73d9ba3ba4b480a8a277fb9badc434c1ba905b3d60c8034e18b348361a09afbec40ba9371036801ba2b675a7b84588f9f55d8 + languageName: node + linkType: hard + "cross-spawn@npm:^5.1.0": version: 5.1.0 resolution: "cross-spawn@npm:5.1.0" @@ -17421,7 +20203,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -17432,6 +20214,26 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^7.0.5": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10/0d52657d7ae36eb130999dffff1168ec348687b48dd38e2ff59992ed916c88d328cf1d07ff4a4a10bc78de5e1c23f04b306d569e42f7a2293915c081e4dfee86 + languageName: node + linkType: hard + +"crossws@npm:>=0.2.0 <0.4.0": + version: 0.3.1 + resolution: "crossws@npm:0.3.1" + dependencies: + uncrypto: "npm:^0.1.3" + checksum: 10/d358a58b364b3314a0e42ee66b1432c01d416128e53eda983eb121abdad5ff39831a1f1ea3e90e80157ceaa0fc925f5193c151b156aa62af9e0c9bcb2fb2a15a + languageName: node + linkType: hard + "crypt@npm:>= 0.0.1": version: 0.0.2 resolution: "crypt@npm:0.0.2" @@ -17439,7 +20241,7 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.2.0": +"crypto-js@npm:^4.0.0, crypto-js@npm:^4.2.0": version: 4.2.0 resolution: "crypto-js@npm:4.2.0" checksum: 10/c7bcc56a6e01c3c397e95aa4a74e4241321f04677f9a618a8f48a63b5781617248afb9adb0629824792e7ec20ca0d4241a49b6b2938ae6f973ec4efc5c53c924 @@ -17453,6 +20255,13 @@ __metadata: languageName: node linkType: hard +"css-what@npm:^6.1.0": + version: 6.1.0 + resolution: "css-what@npm:6.1.0" + checksum: 10/c67a3a2d0d81843af87f8bf0a4d0845b0f952377714abbb2884e48942409d57a2110eabee003609d02ee487b054614bdfcfc59ee265728ff105bd5aa221c1d0e + languageName: node + linkType: hard + "css.escape@npm:^1.5.1": version: 1.5.1 resolution: "css.escape@npm:1.5.1" @@ -17469,7 +20278,7 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.2": +"csstype@npm:^3.0.2, csstype@npm:^3.0.7, csstype@npm:^3.1.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" checksum: 10/f593cce41ff5ade23f44e77521e3a1bcc2c64107041e1bf6c3c32adc5187d0d60983292fda326154d20b01079e24931aa5b08e4467cc488b60bb1e7f6d478ade @@ -17568,6 +20377,15 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^2.29.3": + version: 2.30.0 + resolution: "date-fns@npm:2.30.0" + dependencies: + "@babel/runtime": "npm:^7.21.0" + checksum: 10/70b3e8ea7aaaaeaa2cd80bd889622a4bcb5d8028b4de9162cbcda359db06e16ff6e9309e54eead5341e71031818497f19aaf9839c87d1aba1e27bb4796e758a9 + languageName: node + linkType: hard + "date-fns@npm:^3.6.0": version: 3.6.0 resolution: "date-fns@npm:3.6.0" @@ -17612,7 +20430,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^3.1.0": +"debug@npm:^3.1.0, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" dependencies: @@ -17621,6 +20439,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.5, debug@npm:~4.3.1, debug@npm:~4.3.2": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a + languageName: node + linkType: hard + "decamelize-keys@npm:^1.1.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -17652,6 +20482,13 @@ __metadata: languageName: node linkType: hard +"decode-uri-component@npm:^0.2.2": + version: 0.2.2 + resolution: "decode-uri-component@npm:0.2.2" + checksum: 10/17a0e5fa400bf9ea84432226e252aa7b5e72793e16bf80b907c99b46a799aeacc139ec20ea57121e50c7bd875a1a4365928f884e92abf02e21a5a13790a0f33e + languageName: node + linkType: hard + "decompress-response@npm:^3.2.0, decompress-response@npm:^3.3.0": version: 3.3.0 resolution: "decompress-response@npm:3.3.0" @@ -17691,6 +20528,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.5.3": + version: 1.5.3 + resolution: "dedent@npm:1.5.3" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 10/e5277f6268f288649503125b781a7b7a2c9b22d011139688c0b3619fe40121e600eb1f077c891938d4b2428bdb6326cc3c77a763e4b1cc681bd9666ab1bad2a1 + languageName: node + linkType: hard + "deep-eql@npm:^3.0.1": version: 3.0.1 resolution: "deep-eql@npm:3.0.1" @@ -17872,6 +20721,13 @@ __metadata: languageName: node linkType: hard +"delay@npm:^4.4.0": + version: 4.4.1 + resolution: "delay@npm:4.4.1" + checksum: 10/97b001126a3979a398b6c5f33e437d78acda3b19731d9e6f991a05e2e09e7a410d655b8fdcaedc05743928bb533c0ac9401826cccb2af71c81d2cab50e199351 + languageName: node + linkType: hard + "delay@npm:^5.0.0": version: 5.0.0 resolution: "delay@npm:5.0.0" @@ -17914,6 +20770,13 @@ __metadata: languageName: node linkType: hard +"destr@npm:^2.0.3": + version: 2.0.3 + resolution: "destr@npm:2.0.3" + checksum: 10/dbb756baa876810ec0ca4bcb702d86cc3b480ed14f36bf5747718ed211f96bca5520b63a4109eb181ad940ee2a645677d9a63d4a0ed11a7510619dae97317201 + languageName: node + linkType: hard + "destroy@npm:1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -17921,6 +20784,13 @@ __metadata: languageName: node linkType: hard +"detect-browser@npm:5.3.0, detect-browser@npm:^5.2.0": + version: 5.3.0 + resolution: "detect-browser@npm:5.3.0" + checksum: 10/4a8551e1f5170633c9aa976f16c57f81f1044d071b2eb853c572bd817bf9cd0cc90c9c520d950edb5accd31b1b0c8ddb7a96e82040b0b5579f9f09c77446a117 + languageName: node + linkType: hard + "detect-indent@npm:^6.0.0, detect-indent@npm:^6.1.0": version: 6.1.0 resolution: "detect-indent@npm:6.1.0" @@ -18031,6 +20901,13 @@ __metadata: languageName: node linkType: hard +"dijkstrajs@npm:^1.0.1": + version: 1.0.3 + resolution: "dijkstrajs@npm:1.0.3" + checksum: 10/0d8429699a6d5897ed371de494ef3c7072e8052b42abbd978e686a9b8689e70af005fa3e93e93263ee3653673ff5f89c36db830a57ae7c2e088cb9c496307507 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -18142,7 +21019,7 @@ __metadata: languageName: node linkType: hard -"duplexify@npm:^4.0.0": +"duplexify@npm:^4.0.0, duplexify@npm:^4.1.2": version: 4.1.3 resolution: "duplexify@npm:4.1.3" dependencies: @@ -18180,6 +21057,18 @@ __metadata: languageName: node linkType: hard +"eciesjs@npm:^0.4.8": + version: 0.4.11 + resolution: "eciesjs@npm:0.4.11" + dependencies: + "@ecies/ciphers": "npm:^0.2.1" + "@noble/ciphers": "npm:^1.0.0" + "@noble/curves": "npm:^1.6.0" + "@noble/hashes": "npm:^1.5.0" + checksum: 10/3906d6286c4cde8dd93f5b8e1ad085aa0fdfd9a272c77a382062a782693247d19b6a99d749aff77d037777cfc49c02a8869a3aad47f192ac4f473b87cdbff4af + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -18187,6 +21076,13 @@ __metadata: languageName: node linkType: hard +"effect@npm:3.6.5": + version: 3.6.5 + resolution: "effect@npm:3.6.5" + checksum: 10/e722cc1d262dfcff85b3e43d11edafb03d68e0acf670ed0d709d32218e6bf2ae7084ac627430094b1be6aee6ffdeec061b1d097d2216fce18ebc7264087ab2f0 + languageName: node + linkType: hard + "ejs@npm:^3.1.8": version: 3.1.10 resolution: "ejs@npm:3.1.10" @@ -18227,6 +21123,21 @@ __metadata: languageName: node linkType: hard +"elliptic@npm:^6.5.3, elliptic@npm:^6.5.7": + version: 6.6.1 + resolution: "elliptic@npm:6.6.1" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10/dc678c9febd89a219c4008ba3a9abb82237be853d9fd171cd602c8fb5ec39927e65c6b5e7a1b2a4ea82ee8e0ded72275e7932bb2da04a5790c2638b818e4e1c5 + languageName: node + linkType: hard + "emittery@npm:0.10.0": version: 0.10.0 resolution: "emittery@npm:0.10.0" @@ -18241,6 +21152,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.3.0": + version: 10.4.0 + resolution: "emoji-regex@npm:10.4.0" + checksum: 10/76bb92c5bcf0b6980d37e535156231e4a9d0aa6ab3b9f5eabf7690231d5aa5d5b8e516f36e6804cbdd0f1c23dfef2a60c40ab7bb8aedd890584281a565b97c50 + languageName: node + linkType: hard + "emoji-regex@npm:^7.0.1": version: 7.0.3 resolution: "emoji-regex@npm:7.0.3" @@ -18269,7 +21187,7 @@ __metadata: languageName: node linkType: hard -"encode-utf8@npm:^1.0.2": +"encode-utf8@npm:^1.0.2, encode-utf8@npm:^1.0.3": version: 1.0.3 resolution: "encode-utf8@npm:1.0.3" checksum: 10/0204c37cda21bf19bb8f87f7ec6c89a23d43488c2ef1e5cfa40b64ee9568e63e15dc323fa7f50a491e2c6d33843a6b409f6de09afbf6cf371cb8da596cc64b44 @@ -18304,7 +21222,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.0, end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -18313,16 +21231,37 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.3.0": - version: 2.3.6 - resolution: "enquirer@npm:2.3.6" +"engine.io-client@npm:~6.6.1": + version: 6.6.2 + resolution: "engine.io-client@npm:6.6.2" dependencies: - ansi-colors: "npm:^4.1.1" - checksum: 10/751d14f037eb7683997e696fb8d5fe2675e0b0cde91182c128cf598acf3f5bd9005f35f7c2a9109e291140af496ebec237b6dac86067d59a9b44f3688107f426 + "@socket.io/component-emitter": "npm:~3.1.0" + debug: "npm:~4.3.1" + engine.io-parser: "npm:~5.2.1" + ws: "npm:~8.17.1" + xmlhttprequest-ssl: "npm:~2.1.1" + checksum: 10/c006b3389bb8bd0381926b9633e9f547dec187ea28d2dd99cb42d516b0720bc4373f3f937c199ca616c95b2832e0f547f73326f614caedfe39c02fa93b7ac733 languageName: node linkType: hard -"enquirer@npm:^2.3.6": +"engine.io-parser@npm:~5.2.1": + version: 5.2.3 + resolution: "engine.io-parser@npm:5.2.3" + checksum: 10/eb0023fff5766e7ae9d59e52d92df53fea06d472cfd7b52e5d2c36b4c1dbf78cab5fde1052bcb3d4bb85bdb5aee10ae85d8a1c6c04676dac0c6cdf16bcba6380 + languageName: node + linkType: hard + +"enhanced-resolve@npm:^5.15.0": + version: 5.17.1 + resolution: "enhanced-resolve@npm:5.17.1" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.2.0" + checksum: 10/e8e03cb7a4bf3c0250a89afbd29e5ec20e90ba5fcd026066232a0754864d7d0a393fa6fc0e5379314a6529165a1834b36731147080714459d98924520410d8f5 + languageName: node + linkType: hard + +"enquirer@npm:2.4.1, enquirer@npm:^2.3.6": version: 2.4.1 resolution: "enquirer@npm:2.4.1" dependencies: @@ -18332,6 +21271,15 @@ __metadata: languageName: node linkType: hard +"enquirer@npm:^2.3.0": + version: 2.3.6 + resolution: "enquirer@npm:2.3.6" + dependencies: + ansi-colors: "npm:^4.1.1" + checksum: 10/751d14f037eb7683997e696fb8d5fe2675e0b0cde91182c128cf598acf3f5bd9005f35f7c2a9109e291140af496ebec237b6dac86067d59a9b44f3688107f426 + languageName: node + linkType: hard + "entities@npm:2.2.0": version: 2.2.0 resolution: "entities@npm:2.2.0" @@ -18339,7 +21287,7 @@ __metadata: languageName: node linkType: hard -"env-paths@npm:^2.2.0": +"env-paths@npm:^2.2.0, env-paths@npm:^2.2.1": version: 2.2.1 resolution: "env-paths@npm:2.2.1" checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e @@ -18865,117 +21813,37 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/1f723ec71c3aa196473bf3298316eedc3f62d523924652dfeb60701b609792f918fc60db84b420d1d8ba9bfa7d69de2fc1d3157ba47c028bdae5d507a26a3c64 - languageName: node - linkType: hard - -"esbuild@npm:^0.21.3": - version: 0.21.5 - resolution: "esbuild@npm:0.21.5" - dependencies: - "@esbuild/aix-ppc64": "npm:0.21.5" - "@esbuild/android-arm": "npm:0.21.5" - "@esbuild/android-arm64": "npm:0.21.5" - "@esbuild/android-x64": "npm:0.21.5" - "@esbuild/darwin-arm64": "npm:0.21.5" - "@esbuild/darwin-x64": "npm:0.21.5" - "@esbuild/freebsd-arm64": "npm:0.21.5" - "@esbuild/freebsd-x64": "npm:0.21.5" - "@esbuild/linux-arm": "npm:0.21.5" - "@esbuild/linux-arm64": "npm:0.21.5" - "@esbuild/linux-ia32": "npm:0.21.5" - "@esbuild/linux-loong64": "npm:0.21.5" - "@esbuild/linux-mips64el": "npm:0.21.5" - "@esbuild/linux-ppc64": "npm:0.21.5" - "@esbuild/linux-riscv64": "npm:0.21.5" - "@esbuild/linux-s390x": "npm:0.21.5" - "@esbuild/linux-x64": "npm:0.21.5" - "@esbuild/netbsd-x64": "npm:0.21.5" - "@esbuild/openbsd-x64": "npm:0.21.5" - "@esbuild/sunos-x64": "npm:0.21.5" - "@esbuild/win32-arm64": "npm:0.21.5" - "@esbuild/win32-ia32": "npm:0.21.5" - "@esbuild/win32-x64": "npm:0.21.5" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10/d2ff2ca84d30cce8e871517374d6c2290835380dc7cd413b2d49189ed170d45e407be14de2cb4794cf76f75cf89955c4714726ebd3de7444b3046f5cab23ab6b + checksum: 10/1f723ec71c3aa196473bf3298316eedc3f62d523924652dfeb60701b609792f918fc60db84b420d1d8ba9bfa7d69de2fc1d3157ba47c028bdae5d507a26a3c64 languageName: node linkType: hard -"esbuild@npm:~0.19.10": - version: 0.19.12 - resolution: "esbuild@npm:0.19.12" - dependencies: - "@esbuild/aix-ppc64": "npm:0.19.12" - "@esbuild/android-arm": "npm:0.19.12" - "@esbuild/android-arm64": "npm:0.19.12" - "@esbuild/android-x64": "npm:0.19.12" - "@esbuild/darwin-arm64": "npm:0.19.12" - "@esbuild/darwin-x64": "npm:0.19.12" - "@esbuild/freebsd-arm64": "npm:0.19.12" - "@esbuild/freebsd-x64": "npm:0.19.12" - "@esbuild/linux-arm": "npm:0.19.12" - "@esbuild/linux-arm64": "npm:0.19.12" - "@esbuild/linux-ia32": "npm:0.19.12" - "@esbuild/linux-loong64": "npm:0.19.12" - "@esbuild/linux-mips64el": "npm:0.19.12" - "@esbuild/linux-ppc64": "npm:0.19.12" - "@esbuild/linux-riscv64": "npm:0.19.12" - "@esbuild/linux-s390x": "npm:0.19.12" - "@esbuild/linux-x64": "npm:0.19.12" - "@esbuild/netbsd-x64": "npm:0.19.12" - "@esbuild/openbsd-x64": "npm:0.19.12" - "@esbuild/sunos-x64": "npm:0.19.12" - "@esbuild/win32-arm64": "npm:0.19.12" - "@esbuild/win32-ia32": "npm:0.19.12" - "@esbuild/win32-x64": "npm:0.19.12" +"esbuild@npm:^0.21.3": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.21.5" + "@esbuild/android-arm": "npm:0.21.5" + "@esbuild/android-arm64": "npm:0.21.5" + "@esbuild/android-x64": "npm:0.21.5" + "@esbuild/darwin-arm64": "npm:0.21.5" + "@esbuild/darwin-x64": "npm:0.21.5" + "@esbuild/freebsd-arm64": "npm:0.21.5" + "@esbuild/freebsd-x64": "npm:0.21.5" + "@esbuild/linux-arm": "npm:0.21.5" + "@esbuild/linux-arm64": "npm:0.21.5" + "@esbuild/linux-ia32": "npm:0.21.5" + "@esbuild/linux-loong64": "npm:0.21.5" + "@esbuild/linux-mips64el": "npm:0.21.5" + "@esbuild/linux-ppc64": "npm:0.21.5" + "@esbuild/linux-riscv64": "npm:0.21.5" + "@esbuild/linux-s390x": "npm:0.21.5" + "@esbuild/linux-x64": "npm:0.21.5" + "@esbuild/netbsd-x64": "npm:0.21.5" + "@esbuild/openbsd-x64": "npm:0.21.5" + "@esbuild/sunos-x64": "npm:0.21.5" + "@esbuild/win32-arm64": "npm:0.21.5" + "@esbuild/win32-ia32": "npm:0.21.5" + "@esbuild/win32-x64": "npm:0.21.5" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -19025,7 +21893,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/861fa8eb2428e8d6521a4b7c7930139e3f45e8d51a86985cc29408172a41f6b18df7b3401e7e5e2d528cdf83742da601ddfdc77043ddc4f1c715a8ddb2d8a255 + checksum: 10/d2ff2ca84d30cce8e871517374d6c2290835380dc7cd413b2d49189ed170d45e407be14de2cb4794cf76f75cf89955c4714726ebd3de7444b3046f5cab23ab6b languageName: node linkType: hard @@ -19140,6 +22008,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:2.0.0, escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 10/9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395 + languageName: node + linkType: hard + "escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -19147,13 +22022,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^2.0.0": - version: 2.0.0 - resolution: "escape-string-regexp@npm:2.0.0" - checksum: 10/9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395 - languageName: node - linkType: hard - "escodegen@npm:1.8.x": version: 1.8.1 resolution: "escodegen@npm:1.8.1" @@ -19202,6 +22070,83 @@ __metadata: languageName: node linkType: hard +"eslint-import-resolver-node@npm:^0.3.9": + version: 0.3.9 + resolution: "eslint-import-resolver-node@npm:0.3.9" + dependencies: + debug: "npm:^3.2.7" + is-core-module: "npm:^2.13.0" + resolve: "npm:^1.22.4" + checksum: 10/d52e08e1d96cf630957272e4f2644dcfb531e49dcfd1edd2e07e43369eb2ec7a7d4423d417beee613201206ff2efa4eb9a582b5825ee28802fc7c71fcd53ca83 + languageName: node + linkType: hard + +"eslint-import-resolver-typescript@npm:^3.6.3": + version: 3.6.3 + resolution: "eslint-import-resolver-typescript@npm:3.6.3" + dependencies: + "@nolyfill/is-core-module": "npm:1.0.39" + debug: "npm:^4.3.5" + enhanced-resolve: "npm:^5.15.0" + eslint-module-utils: "npm:^2.8.1" + fast-glob: "npm:^3.3.2" + get-tsconfig: "npm:^4.7.5" + is-bun-module: "npm:^1.0.2" + is-glob: "npm:^4.0.3" + peerDependencies: + eslint: "*" + eslint-plugin-import: "*" + eslint-plugin-import-x: "*" + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + checksum: 10/5f9956dbbd0becc3d6c6cb945dad0e5e6f529cfd0f488d5688f3c59840cd7f4a44ab6aee0f54b5c4188134dab9a01cb63c1201767bde7fc330b7c1a14747f8ac + languageName: node + linkType: hard + +"eslint-module-utils@npm:^2.12.0, eslint-module-utils@npm:^2.8.1": + version: 2.12.0 + resolution: "eslint-module-utils@npm:2.12.0" + dependencies: + debug: "npm:^3.2.7" + peerDependenciesMeta: + eslint: + optional: true + checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee + languageName: node + linkType: hard + +"eslint-plugin-import@npm:^2.31.0": + version: 2.31.0 + resolution: "eslint-plugin-import@npm:2.31.0" + dependencies: + "@rtsao/scc": "npm:^1.1.0" + array-includes: "npm:^3.1.8" + array.prototype.findlastindex: "npm:^1.2.5" + array.prototype.flat: "npm:^1.3.2" + array.prototype.flatmap: "npm:^1.3.2" + debug: "npm:^3.2.7" + doctrine: "npm:^2.1.0" + eslint-import-resolver-node: "npm:^0.3.9" + eslint-module-utils: "npm:^2.12.0" + hasown: "npm:^2.0.2" + is-core-module: "npm:^2.15.1" + is-glob: "npm:^4.0.3" + minimatch: "npm:^3.1.2" + object.fromentries: "npm:^2.0.8" + object.groupby: "npm:^1.0.3" + object.values: "npm:^1.2.0" + semver: "npm:^6.3.1" + string.prototype.trimend: "npm:^1.0.8" + tsconfig-paths: "npm:^3.15.0" + peerDependencies: + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + checksum: 10/6b76bd009ac2db0615d9019699d18e2a51a86cb8c1d0855a35fb1b418be23b40239e6debdc6e8c92c59f1468ed0ea8d7b85c817117a113d5cc225be8a02ad31c + languageName: node + linkType: hard + "eslint-plugin-jest@npm:^28.2.0": version: 28.2.0 resolution: "eslint-plugin-jest@npm:28.2.0" @@ -19257,37 +22202,26 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-storybook@npm:^0.6.15": - version: 0.6.15 - resolution: "eslint-plugin-storybook@npm:0.6.15" +"eslint-plugin-storybook@npm:^0.11.1": + version: 0.11.1 + resolution: "eslint-plugin-storybook@npm:0.11.1" dependencies: - "@storybook/csf": "npm:^0.0.1" - "@typescript-eslint/utils": "npm:^5.45.0" - requireindex: "npm:^1.1.0" + "@storybook/csf": "npm:^0.1.11" + "@typescript-eslint/utils": "npm:^8.8.1" ts-dedent: "npm:^2.2.0" peerDependencies: eslint: ">=6" - checksum: 10/0c278594c8474ce2f176ffc6610240ae9d6c8f9dafbff02be61e6ae05f15081ce858c5b16e64d8995a3a3777c9d1725953fcde4312efab9118aa544a75b27c46 + checksum: 10/3a8757e403227665566a9ee35a735bf72529a8eb2d6ba270c99e6df140601984b43e7fcf274ebee601fe3d946c76edfeefcce4200077da53edc26212ba5bd03c languageName: node linkType: hard -"eslint-scope@npm:^5.1.1": - version: 5.1.1 - resolution: "eslint-scope@npm:5.1.1" - dependencies: - esrecurse: "npm:^4.3.0" - estraverse: "npm:^4.1.1" - checksum: 10/c541ef384c92eb5c999b7d3443d80195fcafb3da335500946f6db76539b87d5826c8f2e1d23bf6afc3154ba8cd7c8e566f8dc00f1eea25fdf3afc8fb9c87b238 - languageName: node - linkType: hard - -"eslint-scope@npm:^7.2.2": - version: 7.2.2 - resolution: "eslint-scope@npm:7.2.2" +"eslint-scope@npm:^8.2.0": + version: 8.2.0 + resolution: "eslint-scope@npm:8.2.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10/5c660fb905d5883ad018a6fea2b49f3cb5b1cbf2cd4bd08e98646e9864f9bc2c74c0839bed2d292e90a4a328833accc197c8f0baed89cbe8d605d6f918465491 + checksum: 10/cd9ab60d5a68f3a0fcac04d1cff5a7383d0f331964d5f1c446259123caec5b3ccc542284d07846e4f4d1389da77750821cc9a6e1ce18558c674977351666f9a6 languageName: node linkType: hard @@ -19305,69 +22239,70 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.4.3": - version: 3.4.3 - resolution: "eslint-visitor-keys@npm:3.4.3" - checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b +"eslint-visitor-keys@npm:^4.2.0": + version: 4.2.0 + resolution: "eslint-visitor-keys@npm:4.2.0" + checksum: 10/9651b3356b01760e586b4c631c5268c0e1a85236e3292bf754f0472f465bf9a856c0ddc261fceace155334118c0151778effafbab981413dbf9288349343fa25 languageName: node linkType: hard -"eslint@npm:^8.57.0": - version: 8.57.0 - resolution: "eslint@npm:8.57.0" +"eslint@npm:^9.15.0": + version: 9.15.0 + resolution: "eslint@npm:9.15.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.6.1" - "@eslint/eslintrc": "npm:^2.1.4" - "@eslint/js": "npm:8.57.0" - "@humanwhocodes/config-array": "npm:^0.11.14" + "@eslint-community/regexpp": "npm:^4.12.1" + "@eslint/config-array": "npm:^0.19.0" + "@eslint/core": "npm:^0.9.0" + "@eslint/eslintrc": "npm:^3.2.0" + "@eslint/js": "npm:9.15.0" + "@eslint/plugin-kit": "npm:^0.2.3" + "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" - "@nodelib/fs.walk": "npm:^1.2.8" - "@ungap/structured-clone": "npm:^1.2.0" + "@humanwhocodes/retry": "npm:^0.4.1" + "@types/estree": "npm:^1.0.6" + "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" + cross-spawn: "npm:^7.0.5" debug: "npm:^4.3.2" - doctrine: "npm:^3.0.0" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^7.2.2" - eslint-visitor-keys: "npm:^3.4.3" - espree: "npm:^9.6.1" - esquery: "npm:^1.4.2" + eslint-scope: "npm:^8.2.0" + eslint-visitor-keys: "npm:^4.2.0" + espree: "npm:^10.3.0" + esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" - file-entry-cache: "npm:^6.0.1" + file-entry-cache: "npm:^8.0.0" find-up: "npm:^5.0.0" glob-parent: "npm:^6.0.2" - globals: "npm:^13.19.0" - graphemer: "npm:^1.4.0" ignore: "npm:^5.2.0" imurmurhash: "npm:^0.1.4" is-glob: "npm:^4.0.0" - is-path-inside: "npm:^3.0.3" - js-yaml: "npm:^4.1.0" json-stable-stringify-without-jsonify: "npm:^1.0.1" - levn: "npm:^0.4.1" lodash.merge: "npm:^4.6.2" minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" - strip-ansi: "npm:^6.0.1" - text-table: "npm:^0.2.0" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true bin: eslint: bin/eslint.js - checksum: 10/00496e218b23747a7a9817bf58b522276d0dc1f2e546dceb4eea49f9871574088f72f1f069a6b560ef537efa3a75261b8ef70e51ef19033da1cc4c86a755ef15 + checksum: 10/7ac1a2e6070bae64b2b0588fabad528cd3e478a6ba5e9f8185d8d9f2dce17a36630bd019b5d32d1052ea177444ab9c83f3c08baa76121c13e1ed0584ef158956 languageName: node linkType: hard -"espree@npm:^9.6.0, espree@npm:^9.6.1": - version: 9.6.1 - resolution: "espree@npm:9.6.1" +"espree@npm:^10.0.1, espree@npm:^10.3.0": + version: 10.3.0 + resolution: "espree@npm:10.3.0" dependencies: - acorn: "npm:^8.9.0" + acorn: "npm:^8.14.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/255ab260f0d711a54096bdeda93adff0eadf02a6f9b92f02b323e83a2b7fc258797919437ad331efec3930475feb0142c5ecaaf3cdab4befebd336d47d3f3134 + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10/3412d44d4204c9e29d6b5dd0277400cfa0cd68495dc09eae1b9ce79d0c8985c1c5cc09cb9ba32a1cd963f48a49b0c46bdb7736afe395a300aa6bb1c0d86837e8 languageName: node linkType: hard @@ -19391,12 +22326,12 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2": - version: 1.5.0 - resolution: "esquery@npm:1.5.0" +"esquery@npm:^1.5.0": + version: 1.6.0 + resolution: "esquery@npm:1.6.0" dependencies: estraverse: "npm:^5.1.0" - checksum: 10/e65fcdfc1e0ff5effbf50fb4f31ea20143ae5df92bb2e4953653d8d40aa4bc148e0d06117a592ce4ea53eeab1dafdfded7ea7e22a5be87e82d73757329a1b01d + checksum: 10/c587fb8ec9ed83f2b1bc97cf2f6854cc30bf784a79d62ba08c6e358bf22280d69aee12827521cf38e69ae9761d23fb7fde593ce315610f85655c139d99b05e5a languageName: node linkType: hard @@ -19416,13 +22351,6 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^4.1.1": - version: 4.3.0 - resolution: "estraverse@npm:4.3.0" - checksum: 10/3f67ad02b6dbfaddd9ea459cf2b6ef4ecff9a6082a7af9d22e445b9abc082ad9ca47e1825557b293fcdae477f4714e561123e30bb6a5b2f184fb2bad4a9497eb - languageName: node - linkType: hard - "estraverse@npm:^5.1.0, estraverse@npm:^5.2.0, estraverse@npm:^5.3.0": version: 5.3.0 resolution: "estraverse@npm:5.3.0" @@ -19467,6 +22395,19 @@ __metadata: languageName: node linkType: hard +"eth-block-tracker@npm:^7.1.0": + version: 7.1.0 + resolution: "eth-block-tracker@npm:7.1.0" + dependencies: + "@metamask/eth-json-rpc-provider": "npm:^1.0.0" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^5.0.1" + json-rpc-random-id: "npm:^1.0.1" + pify: "npm:^3.0.0" + checksum: 10/b001ecb126e949a9ff19950596d5180b2f1bc5504e3dec0c01b3417e8ad190f4a53dfc61be901b72ab6dd558d1d711b73eca560bc8a605d0348eef9f501defab + languageName: node + linkType: hard + "eth-ens-namehash@npm:2.0.8": version: 2.0.8 resolution: "eth-ens-namehash@npm:2.0.8" @@ -19505,6 +22446,19 @@ __metadata: languageName: node linkType: hard +"eth-json-rpc-filters@npm:^6.0.0": + version: 6.0.1 + resolution: "eth-json-rpc-filters@npm:6.0.1" + dependencies: + "@metamask/safe-event-emitter": "npm:^3.0.0" + async-mutex: "npm:^0.2.6" + eth-query: "npm:^2.1.2" + json-rpc-engine: "npm:^6.1.0" + pify: "npm:^5.0.0" + checksum: 10/d1fa8bb21da07c2f5d37c1e6053d499b272b4f49542077efc6b05eebe49affa9df7221c8c2439c4e33caa3f4ccb35240a6105abc83b83375dae03c0de53113a7 + languageName: node + linkType: hard + "eth-lib@npm:0.2.8": version: 0.2.8 resolution: "eth-lib@npm:0.2.8" @@ -19530,6 +22484,25 @@ __metadata: languageName: node linkType: hard +"eth-query@npm:^2.1.2": + version: 2.1.2 + resolution: "eth-query@npm:2.1.2" + dependencies: + json-rpc-random-id: "npm:^1.0.0" + xtend: "npm:^4.0.1" + checksum: 10/af4f3575b8315f8156a83a24e850881053748aca97e4aee12dd6645ab56f0985c7000a5c45ccf315702f3e532f0c6464e03f4aba294c658dee89f5e5d1b86702 + languageName: node + linkType: hard + +"eth-rpc-errors@npm:^4.0.2, eth-rpc-errors@npm:^4.0.3": + version: 4.0.3 + resolution: "eth-rpc-errors@npm:4.0.3" + dependencies: + fast-safe-stringify: "npm:^2.0.6" + checksum: 10/47ce14170eabaee51ab1cc7e643bb3ef96ee6b15c6404806aedcd51750e00ae0b1a12c37785b180679b8d452b6dd44a0240bb018d01fa73efc85fcfa808b35a7 + languageName: node + linkType: hard + "ethereum-bloom-filters@npm:^1.0.6": version: 1.0.10 resolution: "ethereum-bloom-filters@npm:1.0.10" @@ -19574,6 +22547,18 @@ __metadata: languageName: node linkType: hard +"ethereum-cryptography@npm:^2.0.0": + version: 2.2.1 + resolution: "ethereum-cryptography@npm:2.2.1" + dependencies: + "@noble/curves": "npm:1.4.2" + "@noble/hashes": "npm:1.4.0" + "@scure/bip32": "npm:1.4.0" + "@scure/bip39": "npm:1.3.0" + checksum: 10/ab123bbfe843500ac2d645ce9edc4bc814962ffb598db6bf8bf01fbecac656e6c81ff4cf2472f1734844bbcbad2bf658d8b699cb7248d768e0f06ae13ecf43b8 + languageName: node + linkType: hard + "ethereum-waffle@npm:^4.0.10": version: 4.0.10 resolution: "ethereum-waffle@npm:4.0.10" @@ -19643,7 +22628,24 @@ __metadata: languageName: node linkType: hard -"ethers@npm:5.7.2, ethers@npm:^5.1.0, ethers@npm:^5.3.1, ethers@npm:^5.7.0, ethers@npm:^5.7.1, ethers@npm:^5.7.2": +"ethers@npm:^4.0.40": + version: 4.0.49 + resolution: "ethers@npm:4.0.49" + dependencies: + aes-js: "npm:3.0.0" + bn.js: "npm:^4.11.9" + elliptic: "npm:6.5.4" + hash.js: "npm:1.1.3" + js-sha3: "npm:0.5.7" + scrypt-js: "npm:2.0.4" + setimmediate: "npm:1.0.4" + uuid: "npm:2.0.1" + xmlhttprequest: "npm:1.8.0" + checksum: 10/a4cec0254f940a0fb118317d23676faa46eb5540fc0a3b9177b8aef71318f509ed19b8264f102b1a2a32d0256274ecc526fd926bd22a4a4ac25cd8e0e6560f12 + languageName: node + linkType: hard + +"ethers@npm:^5.1.0, ethers@npm:^5.3.1, ethers@npm:^5.7.0, ethers@npm:^5.7.1, ethers@npm:^5.7.2": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -19681,23 +22683,6 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^4.0.40": - version: 4.0.49 - resolution: "ethers@npm:4.0.49" - dependencies: - aes-js: "npm:3.0.0" - bn.js: "npm:^4.11.9" - elliptic: "npm:6.5.4" - hash.js: "npm:1.1.3" - js-sha3: "npm:0.5.7" - scrypt-js: "npm:2.0.4" - setimmediate: "npm:1.0.4" - uuid: "npm:2.0.1" - xmlhttprequest: "npm:1.8.0" - checksum: 10/a4cec0254f940a0fb118317d23676faa46eb5540fc0a3b9177b8aef71318f509ed19b8264f102b1a2a32d0256274ecc526fd926bd22a4a4ac25cd8e0e6560f12 - languageName: node - linkType: hard - "ethjs-unit@npm:0.1.6": version: 0.1.6 resolution: "ethjs-unit@npm:0.1.6" @@ -19725,6 +22710,13 @@ __metadata: languageName: node linkType: hard +"eventemitter2@npm:^6.4.7": + version: 6.4.9 + resolution: "eventemitter2@npm:6.4.9" + checksum: 10/b829b1c6b11e15926b635092b5ad62b4463d1c928859831dcae606e988cf41893059e3541f5a8209d21d2f15314422ddd4d84d20830b4bf44978608d15b06b08 + languageName: node + linkType: hard + "eventemitter3@npm:4.0.4": version: 4.0.4 resolution: "eventemitter3@npm:4.0.4" @@ -19732,6 +22724,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:5.0.1, eventemitter3@npm:^5.0.1": + version: 5.0.1 + resolution: "eventemitter3@npm:5.0.1" + checksum: 10/ac6423ec31124629c84c7077eed1e6987f6d66c31cf43c6fcbf6c87791d56317ce808d9ead483652436df171b526fc7220eccdc9f3225df334e81582c3cf7dd5 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.7": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -19739,14 +22738,7 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^5.0.1": - version: 5.0.1 - resolution: "eventemitter3@npm:5.0.1" - checksum: 10/ac6423ec31124629c84c7077eed1e6987f6d66c31cf43c6fcbf6c87791d56317ce808d9ead483652436df171b526fc7220eccdc9f3225df334e81582c3cf7dd5 - languageName: node - linkType: hard - -"events@npm:^3.2.0, events@npm:^3.3.0": +"events@npm:3.3.0, events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be @@ -19972,7 +22964,17 @@ __metadata: languageName: node linkType: hard -"external-editor@npm:^3.0.3, external-editor@npm:^3.1.0": +"extension-port-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "extension-port-stream@npm:3.0.0" + dependencies: + readable-stream: "npm:^3.6.2 || ^4.4.2" + webextension-polyfill: "npm:>=0.10.0 <1.0" + checksum: 10/4f51d2258a96154c2d916a8a5425636a2b0817763e9277f7dc378d08b6f050c90d185dbde4313d27cf66ad99d4b3116479f9f699c40358c64cccfa524d2b55bf + languageName: node + linkType: hard + +"external-editor@npm:^3.1.0": version: 3.1.0 resolution: "external-editor@npm:3.1.0" dependencies: @@ -20025,6 +23027,24 @@ __metadata: languageName: node linkType: hard +"fast-check@npm:3.21.0": + version: 3.21.0 + resolution: "fast-check@npm:3.21.0" + dependencies: + pure-rand: "npm:^6.1.0" + checksum: 10/64e221858d5d98c6ea10c81e6a1a66760565bca41883466891974197a5439c7f0fe1dc293b8d887eaf959d0ff85197cc9d76813ae6215b018cde313254db7d53 + languageName: node + linkType: hard + +"fast-check@npm:^3.21.0": + version: 3.23.1 + resolution: "fast-check@npm:3.23.1" + dependencies: + pure-rand: "npm:^6.1.0" + checksum: 10/03720c2d4adf02701a2e974b83d6439477851a6524c5980df0870dc0032f0200cc5e157f47641afa79dc42733b05058f2333df54291d5ac39d108d317a62e6c0 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -20059,7 +23079,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.3.0": +"fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -20086,13 +23106,20 @@ __metadata: languageName: node linkType: hard -"fast-redact@npm:^3.1.1": +"fast-redact@npm:^3.0.0, fast-redact@npm:^3.1.1": version: 3.5.0 resolution: "fast-redact@npm:3.5.0" checksum: 10/24b27e2023bd5a62f908d97a753b1adb8d89206b260f97727728e00b693197dea2fc2aa3711147a385d0ec6e713569fd533df37a4ef947e08cb65af3019c7ad5 languageName: node linkType: hard +"fast-safe-stringify@npm:^2.0.6": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 + languageName: node + linkType: hard + "fast-stable-stringify@npm:^1.0.0": version: 1.0.0 resolution: "fast-stable-stringify@npm:1.0.0" @@ -20163,12 +23190,12 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^6.0.1": - version: 6.0.1 - resolution: "file-entry-cache@npm:6.0.1" +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" dependencies: - flat-cache: "npm:^3.0.4" - checksum: 10/099bb9d4ab332cb93c48b14807a6918a1da87c45dce91d4b61fd40e6505d56d0697da060cb901c729c90487067d93c9243f5da3dc9c41f0358483bfdebca736b + flat-cache: "npm:^4.0.0" + checksum: 10/afe55c4de4e0d226a23c1eae62a7219aafb390859122608a89fa4df6addf55c7fd3f1a2da6f5b41e7cdff496e4cf28bbd215d53eab5c817afa96d2b40c81bfb0 languageName: node linkType: hard @@ -20207,6 +23234,13 @@ __metadata: languageName: node linkType: hard +"filter-obj@npm:^1.1.0": + version: 1.1.0 + resolution: "filter-obj@npm:1.1.0" + checksum: 10/9d681939eec2b4b129cb4f307b7e93d954a0657421d4e5357d86093b26d3f4f570909ed43717dcfd62428b3cf8cddd9841b35f9d40d12ac62cfabaa677942593 + languageName: node + linkType: hard + "finalhandler@npm:1.2.0": version: 1.2.0 resolution: "finalhandler@npm:1.2.0" @@ -20253,6 +23287,13 @@ __metadata: languageName: node linkType: hard +"find-root@npm:^1.1.0": + version: 1.1.0 + resolution: "find-root@npm:1.1.0" + checksum: 10/caa799c976a14925ba7f31ca1a226fe73d3aa270f4f1b623fcfeb1c6e263111db4beb807d8acd31bd4d48d44c343b93688a9288dfbccca27463c36a0301b0bb9 + languageName: node + linkType: hard + "find-up@npm:3.0.0, find-up@npm:^3.0.0": version: 3.0.0 resolution: "find-up@npm:3.0.0" @@ -20310,13 +23351,13 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^3.0.4": - version: 3.0.4 - resolution: "flat-cache@npm:3.0.4" +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" dependencies: - flatted: "npm:^3.1.0" - rimraf: "npm:^3.0.2" - checksum: 10/9fe5d0cb97c988e3b25242e71346965fae22757674db3fca14206850af2efa3ca3b04a3ba0eba8d5e20fd8a3be80a2e14b1c2917e70ffe1acb98a8c3327e4c9f + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.4" + checksum: 10/58ce851d9045fffc7871ce2bd718bc485ad7e777bf748c054904b87c351ff1080c2c11da00788d78738bfb51b71e4d5ea12d13b98eb36e3358851ffe495b62dc languageName: node linkType: hard @@ -20329,10 +23370,10 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.1.0": - version: 3.2.5 - resolution: "flatted@npm:3.2.5" - checksum: 10/eed01f72ad0317561e4d6187f7408dc391f7849d9cd6700520ce06155d1859539b6899afdfefc815ce51ec48f97d1015350287c541b5302a49581cf25cec1cd2 +"flatted@npm:^3.2.9": + version: 3.3.2 + resolution: "flatted@npm:3.3.2" + checksum: 10/ac3c159742e01d0e860a861164bcfd35bb567ccbebb8a0dd041e61cf3c64a435b917dd1e7ed1c380c2ebca85735fb16644485ec33665bc6aafc3b316aa1eed44 languageName: node linkType: hard @@ -20352,6 +23393,15 @@ __metadata: languageName: node linkType: hard +"focus-lock@npm:^1.3.5": + version: 1.3.5 + resolution: "focus-lock@npm:1.3.5" + dependencies: + tslib: "npm:^2.0.3" + checksum: 10/1078c9d1f5515c47961a27d22b3b09c2a4eaf42a405f9be621d5ec5f426086a65f5937bff8a104e93d4bd84a7d8364e2f3ec07be876dae259df207384bbfb5de + languageName: node + linkType: hard + "follow-redirects@npm:^1.12.1": version: 1.15.1 resolution: "follow-redirects@npm:1.15.1" @@ -20483,6 +23533,36 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^10.16.4": + version: 10.18.0 + resolution: "framer-motion@npm:10.18.0" + dependencies: + "@emotion/is-prop-valid": "npm:^0.8.2" + tslib: "npm:^2.4.0" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependenciesMeta: + "@emotion/is-prop-valid": + optional: true + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 10/8dc61e16af34ea7c7e830e03f588c54b7a186a767787eb6373ac692bbffa219959d50b7cf983f54305951e121ea7d50ebd4819920290fc6ad58b03fc5eb2bdde + languageName: node + linkType: hard + +"framesync@npm:6.1.2": + version: 6.1.2 + resolution: "framesync@npm:6.1.2" + dependencies: + tslib: "npm:2.4.0" + checksum: 10/741161b8978173acaf515ab45ff127496476e6262e624de3ccb995a67a8b32c65a4242e178646bb5554f128dd4a350ecff675c7fbd4e2aa4568dffab932ebe29 + languageName: node + linkType: hard + "fresh@npm:0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -20833,6 +23913,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.3.0 + resolution: "get-east-asian-width@npm:1.3.0" + checksum: 10/8e8e779eb28701db7fdb1c8cab879e39e6ae23f52dadd89c8aed05869671cee611a65d4f8557b83e981428623247d8bc5d0c7a4ef3ea7a41d826e73600112ad8 + languageName: node + linkType: hard + "get-func-name@npm:^2.0.0": version: 2.0.0 resolution: "get-func-name@npm:2.0.0" @@ -20904,6 +23991,13 @@ __metadata: languageName: node linkType: hard +"get-port-please@npm:^3.1.2": + version: 3.1.2 + resolution: "get-port-please@npm:3.1.2" + checksum: 10/ec8b8da9f816edde114b76742ec29695730094904bb0e94309081e4adf3f797b483b9d648abcf5e0511c4e21a7bf68334672b9575f8b23bccf93bf97eb517f0e + languageName: node + linkType: hard + "get-port@npm:^3.1.0": version: 3.2.0 resolution: "get-port@npm:3.2.0" @@ -20979,15 +24073,6 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.7.2": - version: 4.7.3 - resolution: "get-tsconfig@npm:4.7.3" - dependencies: - resolve-pkg-maps: "npm:^1.0.0" - checksum: 10/7397bb4f8aef936df4d9016555b662dcf5279f3c46428b7c7c1ff5e94ab2b87d018b3dda0f4bc1a28b154d5affd0eac5d014511172c085fd8a9cdff9ea7fe043 - languageName: node - linkType: hard - "get-tsconfig@npm:^4.7.5": version: 4.8.1 resolution: "get-tsconfig@npm:4.8.1" @@ -21227,19 +24312,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:^11.1.0": - version: 11.12.0 - resolution: "globals@npm:11.12.0" - checksum: 10/9f054fa38ff8de8fa356502eb9d2dae0c928217b8b5c8de1f09f5c9b6c8a96d8b9bd3afc49acbcd384a98a81fea713c859e1b09e214c60509517bb8fc2bc13c2 - languageName: node - linkType: hard - -"globals@npm:^13.19.0": - version: 13.20.0 - resolution: "globals@npm:13.20.0" - dependencies: - type-fest: "npm:^0.20.2" - checksum: 10/9df85cde2f0dce6ac9b3a5e08bec109d2f3b38ddd055a83867e0672c55704866d53ce6a4265859fa630624baadd46f50ca38602a13607ad86be853a8c179d3e7 +"globals@npm:^14.0.0": + version: 14.0.0 + resolution: "globals@npm:14.0.0" + checksum: 10/03939c8af95c6df5014b137cac83aa909090c3a3985caef06ee9a5a669790877af8698ab38007e4c0186873adc14c0b13764acc754b16a754c216cc56aa5f021 languageName: node linkType: hard @@ -21262,6 +24338,20 @@ __metadata: languageName: node linkType: hard +"globby@npm:14.0.2": + version: 14.0.2 + resolution: "globby@npm:14.0.2" + dependencies: + "@sindresorhus/merge-streams": "npm:^2.1.0" + fast-glob: "npm:^3.3.2" + ignore: "npm:^5.2.4" + path-type: "npm:^5.0.0" + slash: "npm:^5.1.0" + unicorn-magic: "npm:^0.1.0" + checksum: 10/67660da70fc1223f7170c1a62ba6c373385e9e39765d952b6518606dec15ed8c7958e9dae6ba5752a31dbc1e9126f146938b830ad680fe794141734ffc3fbb75 + languageName: node + linkType: hard + "globby@npm:^10.0.1": version: 10.0.2 resolution: "globby@npm:10.0.2" @@ -21477,6 +24567,24 @@ __metadata: languageName: node linkType: hard +"h3@npm:^1.12.0, h3@npm:^1.13.0": + version: 1.13.0 + resolution: "h3@npm:1.13.0" + dependencies: + cookie-es: "npm:^1.2.2" + crossws: "npm:>=0.2.0 <0.4.0" + defu: "npm:^6.1.4" + destr: "npm:^2.0.3" + iron-webcrypto: "npm:^1.2.1" + ohash: "npm:^1.1.4" + radix3: "npm:^1.1.2" + ufo: "npm:^1.5.4" + uncrypto: "npm:^0.1.3" + unenv: "npm:^1.10.0" + checksum: 10/ecdbe3cdddc767ea6f9be9939b14192dd296eb434641bbecc5b665f7210de8c03910ae40931668788395b5de6cd517afaa628d7b5ce0fb60786fce1ad6e81bcb + languageName: node + linkType: hard + "handlebars@npm:^4.0.1": version: 4.7.7 resolution: "handlebars@npm:4.7.7" @@ -21926,6 +25034,13 @@ __metadata: languageName: node linkType: hard +"hey-listen@npm:^1.0.8": + version: 1.0.8 + resolution: "hey-listen@npm:1.0.8" + checksum: 10/744b5f4c18c7cfb82b22bd22e1d300a9ac4eafe05a22e58fb87e48addfca8be00604d9aa006434ea02f9530990eb4b393ddb28659e2ab7f833ce873e32eb809c + languageName: node + linkType: hard + "hmac-drbg@npm:^1.0.1": version: 1.0.1 resolution: "hmac-drbg@npm:1.0.1" @@ -21937,6 +25052,15 @@ __metadata: languageName: node linkType: hard +"hoist-non-react-statics@npm:^3.3.1": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: "npm:^16.7.0" + checksum: 10/1acbe85f33e5a39f90c822ad4d28b24daeb60f71c545279431dc98c312cd28a54f8d64788e477fe21dc502b0e3cf58589ebe5c1ad22af27245370391c2d24ea6 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4, hosted-git-info@npm:^2.6.0": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -21944,6 +25068,15 @@ __metadata: languageName: node linkType: hard +"hosted-git-info@npm:^7.0.0": + version: 7.0.2 + resolution: "hosted-git-info@npm:7.0.2" + dependencies: + lru-cache: "npm:^10.0.1" + checksum: 10/8f085df8a4a637d995f357f48b1e3f6fc1f9f92e82b33fb406415b5741834ed431a510a09141071001e8deea2eee43ce72786463e2aa5e5a70db8648c0eedeab + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -22024,6 +25157,13 @@ __metadata: languageName: node linkType: hard +"http-shutdown@npm:^1.2.2": + version: 1.2.2 + resolution: "http-shutdown@npm:1.2.2" + checksum: 10/1c99b575b1a7ebd749950e7f59410348723638808336063321d89588b7f7b548d61c8e3566af0f1f4f961d941c758677d062d2289bc63356ead143da4d8f3daf + languageName: node + linkType: hard + "http-signature@npm:~1.2.0": version: 1.2.0 resolution: "http-signature@npm:1.2.0" @@ -22114,6 +25254,24 @@ __metadata: languageName: node linkType: hard +"i18next-browser-languagedetector@npm:7.1.0": + version: 7.1.0 + resolution: "i18next-browser-languagedetector@npm:7.1.0" + dependencies: + "@babel/runtime": "npm:^7.19.4" + checksum: 10/3b06c8a5df09092cffc0b6637b542bb572e8a25dcba97d0d8a5e5dd7539b90bf00000f3a279654693f4b5908c5fc4d1d4f3766dfb461dacab46be3d071266384 + languageName: node + linkType: hard + +"i18next@npm:23.11.5": + version: 23.11.5 + resolution: "i18next@npm:23.11.5" + dependencies: + "@babel/runtime": "npm:^7.23.2" + checksum: 10/3a8e0d5d2b9ac6c6fa8c2180452aaf816d60e1cc790da69d6be515feec85553f8af9fcc19414ade1a621f08236e84f38df4415a8234919fa97fa2e35624e86b6 + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -22132,6 +25290,13 @@ __metadata: languageName: node linkType: hard +"idb-keyval@npm:^6.2.1": + version: 6.2.1 + resolution: "idb-keyval@npm:6.2.1" + checksum: 10/9a1416ff5e2ceff3832f5645518f438833a5ff6ee316fe3ec111d580db120425991d64d8098a847be7541bbbb7cc941984b4d0d62d541c39f7a0f415594837c2 + languageName: node + linkType: hard + "idna-uts46-hx@npm:^2.3.1": version: 2.3.1 resolution: "idna-uts46-hx@npm:2.3.1" @@ -22169,6 +25334,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^5.3.1": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 + languageName: node + linkType: hard + "immediate@npm:^3.2.3": version: 3.3.0 resolution: "immediate@npm:3.3.0" @@ -22183,6 +25355,13 @@ __metadata: languageName: node linkType: hard +"immer@npm:^10.1.1": + version: 10.1.1 + resolution: "immer@npm:10.1.1" + checksum: 10/9dacf1e8c201d69191ccd88dc5d733bafe166cd45a5a360c5d7c88f1de0dff974a94114d72b35f3106adfe587fcfb131c545856184a2247d89d735ad25589863 + languageName: node + linkType: hard + "immutable@npm:^4.0.0-rc.12": version: 4.1.0 resolution: "immutable@npm:4.1.0" @@ -22304,7 +25483,19 @@ __metadata: languageName: node linkType: hard -"invariant@npm:2, invariant@npm:^2.2.4": +"intl-messageformat@npm:^10.1.0": + version: 10.7.6 + resolution: "intl-messageformat@npm:10.7.6" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/fast-memoize": "npm:2.2.3" + "@formatjs/icu-messageformat-parser": "npm:2.9.3" + tslib: "npm:2" + checksum: 10/53f40e386fcc2eaf1ec7d974b18c91e436bc2dc8188587aa652b307160220847b06275d28ca9757ffd9e8471bb6993bf503a71363ce5f9c155d8dc33b43ab97a + languageName: node + linkType: hard + +"invariant@npm:2, invariant@npm:2.2.4, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" dependencies: @@ -22336,6 +25527,13 @@ __metadata: languageName: node linkType: hard +"iron-webcrypto@npm:^1.2.1": + version: 1.2.1 + resolution: "iron-webcrypto@npm:1.2.1" + checksum: 10/c1f52ccfe2780efa5438c134538ee4b26c96a87d22f351d896781219efbce25b4fe716d1cb7f248e02da96881760541135acbcc7c0622ffedf71cb0e227bebf9 + languageName: node + linkType: hard + "is-absolute-url@npm:^3.0.0": version: 3.0.3 resolution: "is-absolute-url@npm:3.0.3" @@ -22425,6 +25623,15 @@ __metadata: languageName: node linkType: hard +"is-bun-module@npm:^1.0.2": + version: 1.2.1 + resolution: "is-bun-module@npm:1.2.1" + dependencies: + semver: "npm:^7.6.3" + checksum: 10/1c2cbcf1a76991add1b640d2d7fe09848e8697a76f96e1289dff44133a48c97f5dc601d4a66d3f3a86217a77178d72d33d10d0c9e14194e58e70ec8df3eae41a + languageName: node + linkType: hard + "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": version: 1.2.4 resolution: "is-callable@npm:1.2.4" @@ -22470,6 +25677,15 @@ __metadata: languageName: node linkType: hard +"is-core-module@npm:^2.15.1": + version: 2.15.1 + resolution: "is-core-module@npm:2.15.1" + dependencies: + hasown: "npm:^2.0.2" + checksum: 10/77316d5891d5743854bcef2cd2f24c5458fb69fbc9705c12ca17d54a2017a67d0693bbf1ba8c77af376c0eef6bf6d1b27a4ab08e4db4e69914c3789bdf2ceec5 + languageName: node + linkType: hard + "is-core-module@npm:^2.8.1": version: 2.9.0 resolution: "is-core-module@npm:2.9.0" @@ -22513,6 +25729,15 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^3.0.0": + version: 3.0.0 + resolution: "is-docker@npm:3.0.0" + bin: + is-docker: cli.js + checksum: 10/b698118f04feb7eaf3338922bd79cba064ea54a1c3db6ec8c0c8d8ee7613e7e5854d802d3ef646812a8a3ace81182a085dfa0a71cc68b06f3fa794b9783b3c90 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -22605,6 +25830,17 @@ __metadata: languageName: node linkType: hard +"is-inside-container@npm:^1.0.0": + version: 1.0.0 + resolution: "is-inside-container@npm:1.0.0" + dependencies: + is-docker: "npm:^3.0.0" + bin: + is-inside-container: cli.js + checksum: 10/c50b75a2ab66ab3e8b92b3bc534e1ea72ca25766832c0623ac22d134116a98bcf012197d1caabe1d1c4bd5f84363d4aa5c36bb4b585fbcaf57be172cd10a1a03 + languageName: node + linkType: hard + "is-interactive@npm:^1.0.0": version: 1.0.0 resolution: "is-interactive@npm:1.0.0" @@ -22612,6 +25848,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^2.0.0": + version: 2.0.0 + resolution: "is-interactive@npm:2.0.0" + checksum: 10/e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -22680,7 +25923,7 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.2, is-path-inside@npm:^3.0.3": +"is-path-inside@npm:^3.0.2": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" checksum: 10/abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 @@ -22852,6 +26095,20 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^1.3.0": + version: 1.3.0 + resolution: "is-unicode-supported@npm:1.3.0" + checksum: 10/20a1fc161afafaf49243551a5ac33b6c4cf0bbcce369fcd8f2951fbdd000c30698ce320de3ee6830497310a8f41880f8066d440aa3eb0a853e2aa4836dd89abc + languageName: node + linkType: hard + +"is-unicode-supported@npm:^2.0.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10/f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 + languageName: node + linkType: hard + "is-url@npm:^1.2.4": version: 1.2.4 resolution: "is-url@npm:1.2.4" @@ -22901,6 +26158,24 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^3.1.0": + version: 3.1.0 + resolution: "is-wsl@npm:3.1.0" + dependencies: + is-inside-container: "npm:^1.0.0" + checksum: 10/f9734c81f2f9cf9877c5db8356bfe1ff61680f1f4c1011e91278a9c0564b395ae796addb4bf33956871041476ec82c3e5260ed57b22ac91794d4ae70a1d2f0a9 + languageName: node + linkType: hard + +"is64bit@npm:^2.0.0": + version: 2.0.0 + resolution: "is64bit@npm:2.0.0" + dependencies: + system-architecture: "npm:^0.1.0" + checksum: 10/94dafd5f29bfb96c542e89ef8c33e811159ca7d07a2890ab83026fa87706612af4101308d9392e9ee68e046e8604a6b59a8f41091f8556f6235efbcfd9c5574c + languageName: node + linkType: hard + "isarray@npm:0.0.1": version: 0.0.1 resolution: "isarray@npm:0.0.1" @@ -23595,6 +26870,22 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^2.1.2": + version: 2.4.0 + resolution: "jiti@npm:2.4.0" + bin: + jiti: lib/jiti-cli.mjs + checksum: 10/10aa999a4f9bccc82b1dab9ebaf4484a8770450883c1bf7fafc07f8fca1e417fd8e7731e651337d1060c9e2ff3f97362dcdfd27e86d1f385db97f4adf7b5a21d + languageName: node + linkType: hard + +"js-base64@npm:^3.7.5": + version: 3.7.7 + resolution: "js-base64@npm:3.7.7" + checksum: 10/185e34c536a6b1c4e1ad8bd96d25b49a9ea4e6803e259eaaaca95f1b392a0d590b2933c5ca8580c776f7279507944b81ff1faf889d84baa5e31f026e96d676a5 + languageName: node + linkType: hard + "js-cookie@npm:^2.2.1": version: 2.2.1 resolution: "js-cookie@npm:2.2.1" @@ -23661,7 +26952,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0": +"js-yaml@npm:4.1.0, js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" dependencies: @@ -23723,6 +27014,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:^3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" + bin: + jsesc: bin/jsesc + checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 + languageName: node + linkType: hard + "jsesc@npm:~0.5.0": version: 0.5.0 resolution: "jsesc@npm:0.5.0" @@ -23755,6 +27055,23 @@ __metadata: languageName: node linkType: hard +"json-rpc-engine@npm:^6.1.0": + version: 6.1.0 + resolution: "json-rpc-engine@npm:6.1.0" + dependencies: + "@metamask/safe-event-emitter": "npm:^2.0.0" + eth-rpc-errors: "npm:^4.0.2" + checksum: 10/00d5b5228e90f126dd52176598db6e5611d295d3a3f7be21254c30c1b6555811260ef2ec2df035cd8e583e4b12096259da721e29f4ea2affb615f7dfc960a6a6 + languageName: node + linkType: hard + +"json-rpc-random-id@npm:^1.0.0, json-rpc-random-id@npm:^1.0.1": + version: 1.0.1 + resolution: "json-rpc-random-id@npm:1.0.1" + checksum: 10/fcd2e884193a129ace4002bd65a86e9cdb206733b4693baea77bd8b372cf8de3043fbea27716a2c9a716581a908ca8d978d9dfec4847eb2cf77edb4cf4b2252c + languageName: node + linkType: hard + "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -23802,6 +27119,17 @@ __metadata: languageName: node linkType: hard +"json5@npm:^1.0.2": + version: 1.0.2 + resolution: "json5@npm:1.0.2" + dependencies: + minimist: "npm:^1.2.0" + bin: + json5: lib/cli.js + checksum: 10/a78d812dbbd5642c4f637dd130954acfd231b074965871c3e28a5bbd571f099d623ecf9161f1960c4ddf68e0cc98dee8bebfdb94a71ad4551f85a1afc94b63f6 + languageName: node + linkType: hard + "json5@npm:^2.1.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -23811,6 +27139,13 @@ __metadata: languageName: node linkType: hard +"jsonc-parser@npm:3.3.1": + version: 3.3.1 + resolution: "jsonc-parser@npm:3.3.1" + checksum: 10/9b0dc391f20b47378f843ef1e877e73ec652a5bdc3c5fa1f36af0f119a55091d147a86c1ee86a232296f55c929bba174538c2bf0312610e0817a22de131cc3f4 + languageName: node + linkType: hard + "jsonfile@npm:^2.1.0": version: 2.4.0 resolution: "jsonfile@npm:2.4.0" @@ -23944,6 +27279,18 @@ __metadata: languageName: node linkType: hard +"keccak@npm:^3.0.3": + version: 3.0.4 + resolution: "keccak@npm:3.0.4" + dependencies: + node-addon-api: "npm:^2.0.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.2.0" + readable-stream: "npm:^3.6.0" + checksum: 10/45478bb0a57e44d0108646499b8360914b0fbc8b0e088f1076659cb34faaa9eb829c40f6dd9dadb3460bb86cc33153c41fed37fe5ce09465a60e71e78c23fa55 + languageName: node + linkType: hard + "keyv@npm:^4.0.0": version: 4.5.2 resolution: "keyv@npm:4.5.2" @@ -23953,7 +27300,7 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": +"keyv@npm:^4.5.3, keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" dependencies: @@ -23962,6 +27309,13 @@ __metadata: languageName: node linkType: hard +"keyvaluestorage-interface@npm:^1.0.0": + version: 1.0.0 + resolution: "keyvaluestorage-interface@npm:1.0.0" + checksum: 10/e652448bc915f9c21b9916678ed58f5314c831f0a284d190a340c0370296c71918e0cdc1156a17b12d1993941b302f0881e23fb9c395079e2065a7d2f33d0199 + languageName: node + linkType: hard + "kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -24272,6 +27626,35 @@ __metadata: languageName: node linkType: hard +"listhen@npm:^1.9.0": + version: 1.9.0 + resolution: "listhen@npm:1.9.0" + dependencies: + "@parcel/watcher": "npm:^2.4.1" + "@parcel/watcher-wasm": "npm:^2.4.1" + citty: "npm:^0.1.6" + clipboardy: "npm:^4.0.0" + consola: "npm:^3.2.3" + crossws: "npm:>=0.2.0 <0.4.0" + defu: "npm:^6.1.4" + get-port-please: "npm:^3.1.2" + h3: "npm:^1.12.0" + http-shutdown: "npm:^1.2.2" + jiti: "npm:^2.1.2" + mlly: "npm:^1.7.1" + node-forge: "npm:^1.3.1" + pathe: "npm:^1.1.2" + std-env: "npm:^3.7.0" + ufo: "npm:^1.5.4" + untun: "npm:^0.1.3" + uqr: "npm:^0.1.2" + bin: + listen: bin/listhen.mjs + listhen: bin/listhen.mjs + checksum: 10/72b869c8604301352c5d5825a7737705f0df2ce1795af8e779b6f956ba71302e13b12b2d35142687fb4e1e8ccc2747e2be3c9cbf20f7f96b73f897881aa3c384 + languageName: node + linkType: hard + "listr2@npm:^4.0.5": version: 4.0.5 resolution: "listr2@npm:4.0.5" @@ -24293,6 +27676,37 @@ __metadata: languageName: node linkType: hard +"lit-element@npm:^3.3.0": + version: 3.3.3 + resolution: "lit-element@npm:3.3.3" + dependencies: + "@lit-labs/ssr-dom-shim": "npm:^1.1.0" + "@lit/reactive-element": "npm:^1.3.0" + lit-html: "npm:^2.8.0" + checksum: 10/7968e7f3ce3994911f27c4c54acc956488c91d8af81677cce3d6f0c2eaea45cceb79b064077159392238d6e43d46015a950269db9914fea8913566aacb17eaa1 + languageName: node + linkType: hard + +"lit-html@npm:^2.8.0": + version: 2.8.0 + resolution: "lit-html@npm:2.8.0" + dependencies: + "@types/trusted-types": "npm:^2.0.2" + checksum: 10/3503e55e2927c2ff94773cf041fc4128f92291869c9192f36eacb7f95132d11f6b329e5b910ab60a4456349cd2e6d23b33d83291b24d557bcd6b904d6314ac1a + languageName: node + linkType: hard + +"lit@npm:2.8.0": + version: 2.8.0 + resolution: "lit@npm:2.8.0" + dependencies: + "@lit/reactive-element": "npm:^1.6.0" + lit-element: "npm:^3.3.0" + lit-html: "npm:^2.8.0" + checksum: 10/aa64c1136b855ba328d41157dba67657d480345aeec3c1dd829abeb67719d759c9ff2ade9903f9cfb4f9d012b16087034aaa5b33f1182e70c615765562e3251b + languageName: node + linkType: hard + "load-yaml-file@npm:^0.2.0": version: 0.2.0 resolution: "load-yaml-file@npm:0.2.0" @@ -24392,6 +27806,13 @@ __metadata: languageName: node linkType: hard +"lodash.isequal@npm:4.5.0": + version: 4.5.0 + resolution: "lodash.isequal@npm:4.5.0" + checksum: 10/82fc58a83a1555f8df34ca9a2cd300995ff94018ac12cc47c349655f0ae1d4d92ba346db4c19bbfc90510764e0c00ddcc985a358bdcd4b3b965abf8f2a48a214 + languageName: node + linkType: hard + "lodash.memoize@npm:4.x": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" @@ -24406,6 +27827,13 @@ __metadata: languageName: node linkType: hard +"lodash.mergewith@npm:4.6.2": + version: 4.6.2 + resolution: "lodash.mergewith@npm:4.6.2" + checksum: 10/aea75a4492541a4902ac7e551dc6c54b722da0c187f84385d02e8fc33a7ae3454b837822446e5f63fcd5ad1671534ea408740b776670ea4d9c7890b10105fce0 + languageName: node + linkType: hard + "lodash.startcase@npm:^4.4.0": version: 4.4.0 resolution: "lodash.startcase@npm:4.4.0" @@ -24446,6 +27874,16 @@ __metadata: languageName: node linkType: hard +"log-symbols@npm:^6.0.0": + version: 6.0.0 + resolution: "log-symbols@npm:6.0.0" + dependencies: + chalk: "npm:^5.3.0" + is-unicode-supported: "npm:^1.3.0" + checksum: 10/510cdda36700cbcd87a2a691ea08d310a6c6b449084018f7f2ec4f732ca5e51b301ff1327aadd96f53c08318e616276c65f7fe22f2a16704fb0715d788bc3c33 + languageName: node + linkType: hard + "log-update@npm:^4.0.0": version: 4.0.0 resolution: "log-update@npm:4.0.0" @@ -24522,7 +27960,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.2.0": +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a @@ -24760,6 +28198,15 @@ __metadata: languageName: node linkType: hard +"media-query-parser@npm:^2.0.2": + version: 2.0.2 + resolution: "media-query-parser@npm:2.0.2" + dependencies: + "@babel/runtime": "npm:^7.12.5" + checksum: 10/9dff3ed135149944717a8687567f4fda1d39d28637f265c6ce7efe5ed55cd88ed49136c912ee0c7f3a6e5debc50b1ff969db609d862318f1af97f48752b08b0b + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -24834,6 +28281,15 @@ __metadata: languageName: node linkType: hard +"merge-options@npm:^3.0.4": + version: 3.0.4 + resolution: "merge-options@npm:3.0.4" + dependencies: + is-plain-obj: "npm:^2.1.0" + checksum: 10/d86ddb3dd6e85d558dbf25dc944f3527b6bacb944db3fdda6e84a3f59c4e4b85231095f58b835758b9a57708342dee0f8de0dffa352974a48221487fe9f4584f + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -24882,6 +28338,13 @@ __metadata: languageName: node linkType: hard +"micro-ftch@npm:^0.3.1": + version: 0.3.1 + resolution: "micro-ftch@npm:0.3.1" + checksum: 10/a7ab07d25e28ec4ae492ce4542ea9b06eee85538742b3b1263b247366ee8872f2c5ce9c8651138b2f1d22c8212f691a7b8b5384fe86ead5aff1852e211f1c035 + languageName: node + linkType: hard + "micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": version: 4.0.5 resolution: "micromatch@npm:4.0.5" @@ -25084,6 +28547,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.5, minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348 + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.1.0 resolution: "minimatch@npm:5.1.0" @@ -25102,15 +28574,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.4": - version: 9.0.5 - resolution: "minimatch@npm:9.0.5" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348 - languageName: node - linkType: hard - "minimist-options@npm:^4.0.2": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -25239,6 +28702,18 @@ __metadata: languageName: node linkType: hard +"mipd@npm:0.0.7": + version: 0.0.7 + resolution: "mipd@npm:0.0.7" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/c14dffef0ef7a3e71469aee553f5735f4a6a9f9a2b47ca02798040f2e006261c2e7e8b26ee0dc56a815c04d5612eb4be1eed474e7bb4e496eb0f5ada2fe1d2e7 + languageName: node + linkType: hard + "mixme@npm:^0.5.1": version: 0.5.10 resolution: "mixme@npm:0.5.10" @@ -25314,6 +28789,13 @@ __metadata: languageName: node linkType: hard +"mobx@npm:^6.1.7": + version: 6.13.5 + resolution: "mobx@npm:6.13.5" + checksum: 10/1b0842ae4f3d7319a532ee5fcb29d4ccde714248af9111e7c375bed4adbe49c4535c6383fd14933c4e7ec022c0b730deb55e32344dcfad025c711435b3e21c42 + languageName: node + linkType: hard + "mocha@npm:7.1.2": version: 7.1.2 resolution: "mocha@npm:7.1.2" @@ -25423,6 +28905,13 @@ __metadata: languageName: node linkType: hard +"modern-ahocorasick@npm:^1.0.0": + version: 1.0.1 + resolution: "modern-ahocorasick@npm:1.0.1" + checksum: 10/ec83479f406511f37a966d66ce1c2b1701bb4a2cc2aabbbc257001178c9fbc48ce748c88eb10dfe72ba8b7f991a0bc7f1fa14683f444685edd1a9eeb32ecbc1e + languageName: node + linkType: hard + "module-error@npm:^1.0.1, module-error@npm:^1.0.2": version: 1.0.2 resolution: "module-error@npm:1.0.2" @@ -25430,6 +28919,20 @@ __metadata: languageName: node linkType: hard +"motion@npm:10.16.2": + version: 10.16.2 + resolution: "motion@npm:10.16.2" + dependencies: + "@motionone/animation": "npm:^10.15.1" + "@motionone/dom": "npm:^10.16.2" + "@motionone/svelte": "npm:^10.16.2" + "@motionone/types": "npm:^10.15.1" + "@motionone/utils": "npm:^10.15.1" + "@motionone/vue": "npm:^10.16.2" + checksum: 10/2470f12b97371eb876337b355ad158c545622b2cc7c83b0ba540d2c02afedb49990e78898e520b8f74cccc9ecf11d366ae005a35c60e92178fadd7434860a966 + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -25451,7 +28954,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -25497,6 +29000,13 @@ __metadata: languageName: node linkType: hard +"multiformats@npm:^9.4.2": + version: 9.9.0 + resolution: "multiformats@npm:9.9.0" + checksum: 10/ad55c7d480d22f4258a68fd88aa2aab744fe0cb1e68d732fc886f67d858b37e3aa6c2cec12b2960ead7730d43be690931485238569952d8a3d7f90fdc726c652 + languageName: node + linkType: hard + "multihashes@npm:^0.4.15, multihashes@npm:~0.4.15": version: 0.4.21 resolution: "multihashes@npm:0.4.21" @@ -25546,6 +29056,15 @@ __metadata: languageName: node linkType: hard +"nan@npm:^2.13.2": + version: 2.22.0 + resolution: "nan@npm:2.22.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10/ab165ba910e549fcc21fd561a33f534d86e81ae36c97b1019dcfe506b09692ff867c97794a54b49c9a83b8b485f529f0f58d24966c3a11863c97dc70814f4d50 + languageName: node + linkType: hard + "nan@npm:^2.14.0": version: 2.16.0 resolution: "nan@npm:2.16.0" @@ -25594,6 +29113,13 @@ __metadata: languageName: node linkType: hard +"napi-wasm@npm:^1.1.0": + version: 1.1.3 + resolution: "napi-wasm@npm:1.1.3" + checksum: 10/5cad19c3ba4c8b176453149542ea72f156be5db6d249611a76537833381f5cec802ed4d7ae5c3f7c0ef69d439c037f7247bbae7db711ed84f915be2a9fc43bb4 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -25642,6 +29168,17 @@ __metadata: languageName: node linkType: hard +"nock@npm:13.5.4": + version: 13.5.4 + resolution: "nock@npm:13.5.4" + dependencies: + debug: "npm:^4.1.0" + json-stringify-safe: "npm:^5.0.1" + propagate: "npm:^2.0.0" + checksum: 10/75bad391bae4efb81b742734af5f2d87309cd93d3ca6b78372fd37946d78ccb254d79104676619866915e6734abfc1b00fee2aa42073a4843ca3c746aad35a4d + languageName: node + linkType: hard + "node-abi@npm:^2.18.0, node-abi@npm:^2.21.0, node-abi@npm:^2.7.0": version: 2.30.1 resolution: "node-abi@npm:2.30.1" @@ -25678,6 +29215,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^7.0.0": + version: 7.1.1 + resolution: "node-addon-api@npm:7.1.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10/ee1e1ed6284a2f8cd1d59ac6175ecbabf8978dcf570345e9a8095a9d0a2b9ced591074ae77f9009287b00c402352b38aa9322a34f2199cdc9f567b842a636b94 + languageName: node + linkType: hard + "node-dir@npm:^0.1.17": version: 0.1.17 resolution: "node-dir@npm:0.1.17" @@ -25706,7 +29252,7 @@ __metadata: languageName: node linkType: hard -"node-fetch-native@npm:^1.6.3": +"node-fetch-native@npm:^1.6.3, node-fetch-native@npm:^1.6.4": version: 1.6.4 resolution: "node-fetch-native@npm:1.6.4" checksum: 10/39c4c6d0c2a4bed1444943e1647ad0d79eb6638cf159bc37dffeafd22cffcf6a998e006aa1f3dd1d9d2258db7d78dee96b44bee4ba0bbaf0440ed348794f2543 @@ -25741,7 +29287,7 @@ __metadata: languageName: node linkType: hard -"node-forge@npm:^1": +"node-forge@npm:^1, node-forge@npm:^1.3.1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" checksum: 10/05bab6868633bf9ad4c3b1dd50ec501c22ffd69f556cdf169a00998ca1d03e8107a6032ba013852f202035372021b845603aeccd7dfcb58cdb7430013b3daa8d @@ -25949,6 +29495,18 @@ __metadata: languageName: node linkType: hard +"npm-package-arg@npm:11.0.3": + version: 11.0.3 + resolution: "npm-package-arg@npm:11.0.3" + dependencies: + hosted-git-info: "npm:^7.0.0" + proc-log: "npm:^4.0.0" + semver: "npm:^7.3.5" + validate-npm-package-name: "npm:^5.0.0" + checksum: 10/bacc863907edf98940286edc2fd80327901c1e8b34426d538cdc708ed66bc6567f06d742d838eaf35db6804347bb4ba56ca9cef032c4b52743b33e7a22a2678e + languageName: node + linkType: hard + "npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" @@ -26031,6 +29589,17 @@ __metadata: languageName: node linkType: hard +"obj-multiplex@npm:^1.0.0": + version: 1.0.0 + resolution: "obj-multiplex@npm:1.0.0" + dependencies: + end-of-stream: "npm:^1.4.0" + once: "npm:^1.4.0" + readable-stream: "npm:^2.3.3" + checksum: 10/6bdcb7d48a1cd4458a7ff0be0b3c1dc58e8e9e6504f937c10b1eac096a3d459b85d7ba32bdd9a45382bb238e245eb42ebcd91430c72f04b0a57c97f846f2d06f + languageName: node + linkType: hard + "object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -26159,6 +29728,17 @@ __metadata: languageName: node linkType: hard +"object.groupby@npm:^1.0.3": + version: 1.0.3 + resolution: "object.groupby@npm:1.0.3" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + checksum: 10/44cb86dd2c660434be65f7585c54b62f0425b0c96b5c948d2756be253ef06737da7e68d7106e35506ce4a44d16aa85a413d11c5034eb7ce5579ec28752eb42d0 + languageName: node + linkType: hard + "object.values@npm:^1.1.6, object.values@npm:^1.2.0": version: 1.2.0 resolution: "object.values@npm:1.2.0" @@ -26186,6 +29766,17 @@ __metadata: languageName: node linkType: hard +"ofetch@npm:^1.4.1": + version: 1.4.1 + resolution: "ofetch@npm:1.4.1" + dependencies: + destr: "npm:^2.0.3" + node-fetch-native: "npm:^1.6.4" + ufo: "npm:^1.5.4" + checksum: 10/329ecd5595eff6da090c728e66f4223ad7ba5c2c309446f3707245c1b213da47dfd1eb1740f26b3da9e31ed7b7a903733bdaae85187b714514da865a0c5a4a9c + languageName: node + linkType: hard + "ohash@npm:^1.1.3": version: 1.1.3 resolution: "ohash@npm:1.1.3" @@ -26193,6 +29784,20 @@ __metadata: languageName: node linkType: hard +"ohash@npm:^1.1.4": + version: 1.1.4 + resolution: "ohash@npm:1.1.4" + checksum: 10/b11445234e59c9c2b00f357f8f00b6ba00e14c84fc0a232cdc14eb1d80066479b09d27af0201631e84b7a15ba7c4a1939f4cc47f2030e9bf83c9e8afc3ff7dfd + languageName: node + linkType: hard + +"on-exit-leak-free@npm:^0.2.0": + version: 0.2.0 + resolution: "on-exit-leak-free@npm:0.2.0" + checksum: 10/36a3a1baea964dc01088884e9d87824cc1a3304ae702e7c688bdb5deec61fbb79325977dd6cba5988f60ad40fedc6ef31ec705adf65b4b042bc0d2686186c0dd + languageName: node + linkType: hard + "on-exit-leak-free@npm:^2.1.0": version: 2.1.2 resolution: "on-exit-leak-free@npm:2.1.2" @@ -26292,6 +29897,23 @@ __metadata: languageName: node linkType: hard +"ora@npm:8.0.1": + version: 8.0.1 + resolution: "ora@npm:8.0.1" + dependencies: + chalk: "npm:^5.3.0" + cli-cursor: "npm:^4.0.0" + cli-spinners: "npm:^2.9.2" + is-interactive: "npm:^2.0.0" + is-unicode-supported: "npm:^2.0.0" + log-symbols: "npm:^6.0.0" + stdin-discarder: "npm:^0.2.1" + string-width: "npm:^7.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/3d37bb3f53e965e5176004af319f82feef7323ee0b2428db5ee6f689b9b9ba939d7b1e81691d4614333c4fb9e294790eb049db9c1e990b14b9bbe150c6f09993 + languageName: node + linkType: hard + "ora@npm:^5.4.1": version: 5.4.1 resolution: "ora@npm:5.4.1" @@ -26323,6 +29945,26 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.1.2": + version: 0.1.2 + resolution: "ox@npm:0.1.2" + dependencies: + "@adraffy/ens-normalize": "npm:^1.10.1" + "@noble/curves": "npm:^1.6.0" + "@noble/hashes": "npm:^1.5.0" + "@scure/bip32": "npm:^1.5.0" + "@scure/bip39": "npm:^1.4.0" + abitype: "npm:^1.0.6" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/cba00f13289599ff03cee3dbc19167c1d0f01829379d119f962b4e951ee2bf0d14491c7a45974e6a2a745117b13b22e9e4131d285e1f5247ea4e1cbc43c5c3d8 + languageName: node + linkType: hard + "p-cancelable@npm:^0.3.0": version: 0.3.0 resolution: "p-cancelable@npm:0.3.0" @@ -26671,6 +30313,13 @@ __metadata: languageName: node linkType: hard +"path-type@npm:^5.0.0": + version: 5.0.0 + resolution: "path-type@npm:5.0.0" + checksum: 10/15ec24050e8932c2c98d085b72cfa0d6b4eeb4cbde151a0a05726d8afae85784fc5544f733d8dfc68536587d5143d29c0bd793623fad03d7e61cc00067291cd5 + languageName: node + linkType: hard + "pathe@npm:^1.1.1, pathe@npm:^1.1.2": version: 1.1.2 resolution: "pathe@npm:1.1.2" @@ -26760,6 +30409,13 @@ __metadata: languageName: node linkType: hard +"pify@npm:^3.0.0": + version: 3.0.0 + resolution: "pify@npm:3.0.0" + checksum: 10/668c1dc8d9fc1b34b9ce3b16ba59deb39d4dc743527bf2ed908d2b914cb8ba40aa5ba6960b27c417c241531c5aafd0598feeac2d50cb15278cf9863fa6b02a77 + languageName: node + linkType: hard + "pify@npm:^4.0.1": version: 4.0.1 resolution: "pify@npm:4.0.1" @@ -26767,6 +30423,23 @@ __metadata: languageName: node linkType: hard +"pify@npm:^5.0.0": + version: 5.0.0 + resolution: "pify@npm:5.0.0" + checksum: 10/443e3e198ad6bfa8c0c533764cf75c9d5bc976387a163792fb553ffe6ce923887cf14eebf5aea9b7caa8eab930da8c33612990ae85bd8c2bc18bedb9eae94ecb + languageName: node + linkType: hard + +"pino-abstract-transport@npm:v0.5.0": + version: 0.5.0 + resolution: "pino-abstract-transport@npm:0.5.0" + dependencies: + duplexify: "npm:^4.1.2" + split2: "npm:^4.0.0" + checksum: 10/d304a104e5cb0c3fef62ea544a4a39bf2472a602cdd7ddb136b0671b9c324ad93fa7888825c4cf33e624802436e897081ba92440f40518b9f2dbdbc0c889e409 + languageName: node + linkType: hard + "pino-abstract-transport@npm:v1.1.0": version: 1.1.0 resolution: "pino-abstract-transport@npm:1.1.0" @@ -26777,6 +30450,13 @@ __metadata: languageName: node linkType: hard +"pino-std-serializers@npm:^4.0.0": + version: 4.0.0 + resolution: "pino-std-serializers@npm:4.0.0" + checksum: 10/cec586f9634ef0e6582f62bc8fc5ca5b6e5e11ab88fe3950c66fb0fd5d6690f66bc39cd3f27216b925d2963ad5c3bba415718819ac20ebe0390c7d056cbfea1b + languageName: node + linkType: hard + "pino-std-serializers@npm:^6.0.0": version: 6.2.2 resolution: "pino-std-serializers@npm:6.2.2" @@ -26784,6 +30464,27 @@ __metadata: languageName: node linkType: hard +"pino@npm:7.11.0": + version: 7.11.0 + resolution: "pino@npm:7.11.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + fast-redact: "npm:^3.0.0" + on-exit-leak-free: "npm:^0.2.0" + pino-abstract-transport: "npm:v0.5.0" + pino-std-serializers: "npm:^4.0.0" + process-warning: "npm:^1.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.1.0" + safe-stable-stringify: "npm:^2.1.0" + sonic-boom: "npm:^2.2.1" + thread-stream: "npm:^0.15.1" + bin: + pino: bin.js + checksum: 10/1c7b4b52fea76e0bc5d8b1190a0fee24279cb16d76fdb5833b32b64256fd8a94d641574b850faba5be72514f04045206b6d902a9a3f5ceae2a4296687088e073 + languageName: node + linkType: hard + "pino@npm:^8.19.0": version: 8.19.0 resolution: "pino@npm:8.19.0" @@ -26868,6 +30569,13 @@ __metadata: languageName: node linkType: hard +"pngjs@npm:^5.0.0": + version: 5.0.0 + resolution: "pngjs@npm:5.0.0" + checksum: 10/345781644740779752505af2fea3e9043f6c7cc349b18e1fb8842796360d1624791f0c24d33c0f27b05658373f90ffaa177a849e932e5fea1f540cef3975f3c9 + languageName: node + linkType: hard + "polished@npm:^4.2.2": version: 4.3.1 resolution: "polished@npm:4.3.1" @@ -26877,6 +30585,13 @@ __metadata: languageName: node linkType: hard +"pony-cause@npm:^2.1.10": + version: 2.1.11 + resolution: "pony-cause@npm:2.1.11" + checksum: 10/ed7d0bb6e3e69f753080bf736b71f40e6ae4c13ec0c8c473ff73345345c088819966fdd68a62ad7482d464bf41176cf9421f5f63715d1a4532005eedc099db55 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.0.0 resolution: "possible-typed-array-names@npm:1.0.0" @@ -26976,6 +30691,13 @@ __metadata: languageName: node linkType: hard +"preact@npm:^10.16.0, preact@npm:^10.24.2": + version: 10.24.3 + resolution: "preact@npm:10.24.3" + checksum: 10/e9c4c901a4ddd475a1072355b5c6c944b05797445e0d68f317ad0dbc976b831523573693ea75d2e12e7902042e3729af435377816d25558bf693ecf6b516c707 + languageName: node + linkType: hard + "prebuild-install@npm:^5.3.4": version: 5.3.6 resolution: "prebuild-install@npm:5.3.6" @@ -27133,6 +30855,13 @@ __metadata: languageName: node linkType: hard +"proc-log@npm:^4.0.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10/4e1394491b717f6c1ade15c570ecd4c2b681698474d3ae2d303c1e4b6ab9455bd5a81566211e82890d5a5ae9859718cc6954d5150bb18b09b72ecb297beae90a + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -27140,6 +30869,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^1.0.0": + version: 1.0.0 + resolution: "process-warning@npm:1.0.0" + checksum: 10/8736d11d8d71c349d176e210305e84d74b13af06efb3c779377b056bfd608257d1e4e32b8fbbf90637c900f0313e40f7c9f583140884f667a21fc10a869b840c + languageName: node + linkType: hard + "process-warning@npm:^3.0.0": version: 3.0.0 resolution: "process-warning@npm:3.0.0" @@ -27196,7 +30932,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.0.1, prompts@npm:^2.4.0, prompts@npm:^2.4.2": +"prompts@npm:2.4.2, prompts@npm:^2.0.1, prompts@npm:^2.4.0, prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -27206,7 +30942,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -27217,6 +30953,13 @@ __metadata: languageName: node linkType: hard +"propagate@npm:^2.0.0": + version: 2.0.1 + resolution: "propagate@npm:2.0.1" + checksum: 10/8c761c16e8232f82f6d015d3e01e8bd4109f47ad804f904d950f6fe319813b448ca112246b6bfdc182b400424b155b0b7c4525a9bb009e6fa950200157569c14 + languageName: node + linkType: hard + "proper-lockfile@npm:^4.1.1": version: 4.1.2 resolution: "proper-lockfile@npm:4.1.2" @@ -27264,7 +31007,7 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^6.8.8": +"protobufjs@npm:^6.11.2, protobufjs@npm:^6.8.8": version: 6.11.4 resolution: "protobufjs@npm:6.11.4" dependencies: @@ -27318,6 +31061,13 @@ __metadata: languageName: node linkType: hard +"proxy-compare@npm:2.5.1": + version: 2.5.1 + resolution: "proxy-compare@npm:2.5.1" + checksum: 10/64b6277d08d89f0b2c468a84decf43f82a4e88da7075651e6adebc69d1b87fadc17cfeb43c024c00b65faa3f0908f7ac1e61f5f6849a404a547a742e6aa527a6 + languageName: node + linkType: hard + "proxy-from-env@npm:^1.0.0, proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -27430,6 +31180,13 @@ __metadata: languageName: node linkType: hard +"pure-rand@npm:^6.1.0": + version: 6.1.0 + resolution: "pure-rand@npm:6.1.0" + checksum: 10/256aa4bcaf9297256f552914e03cbdb0039c8fe1db11fa1e6d3f80790e16e563eb0a859a1e61082a95e224fc0c608661839439f8ecc6a3db4e48d46d99216ee4 + languageName: node + linkType: hard + "pvtsutils@npm:^1.3.2": version: 1.3.2 resolution: "pvtsutils@npm:1.3.2" @@ -27446,6 +31203,58 @@ __metadata: languageName: node linkType: hard +"qr-code-styling@npm:^1.6.0-rc.1": + version: 1.8.4 + resolution: "qr-code-styling@npm:1.8.4" + dependencies: + qrcode-generator: "npm:^1.4.4" + checksum: 10/ddc193ac15c16bed24a7a27eb15e14ba59efa48bad390a116802f57cb537905238a23fe4a06488951c4f107f0c7271950ebd58ce7f30cfd9ee07c4fea32bebff + languageName: node + linkType: hard + +"qrcode-generator@npm:^1.4.4": + version: 1.4.4 + resolution: "qrcode-generator@npm:1.4.4" + checksum: 10/65b2bba237d1f230eba0d08ae4267d04f326859c2265775ade99191be1b522158b623fcc0b613bbfc9d4edbbafb928fc41c66d61053b333f2eb0bcedb2ebadca + languageName: node + linkType: hard + +"qrcode-terminal-nooctal@npm:^0.12.1": + version: 0.12.1 + resolution: "qrcode-terminal-nooctal@npm:0.12.1" + bin: + qrcode-terminal: bin/qrcode-terminal.js + checksum: 10/8f437f9e95d8211c3b4eb3de572abd8e9695efa51b327e68e843fcbc2f017e32d6407caf4d8a8dca64d2d1270cf1cc1b16ebb6f2a69a1f891df430e8efdef66a + languageName: node + linkType: hard + +"qrcode@npm:1.5.3": + version: 1.5.3 + resolution: "qrcode@npm:1.5.3" + dependencies: + dijkstrajs: "npm:^1.0.1" + encode-utf8: "npm:^1.0.3" + pngjs: "npm:^5.0.0" + yargs: "npm:^15.3.1" + bin: + qrcode: bin/qrcode + checksum: 10/823642d59a81ba5f406a1e78415fee37fd53856038f49a85c4ca7aa32ba6b8505ab059a832718ac16612bed75aa2a18584faae38cf3c25e2c90fb19b8c55fe46 + languageName: node + linkType: hard + +"qrcode@npm:1.5.4, qrcode@npm:^1.5.4": + version: 1.5.4 + resolution: "qrcode@npm:1.5.4" + dependencies: + dijkstrajs: "npm:^1.0.1" + pngjs: "npm:^5.0.0" + yargs: "npm:^15.3.1" + bin: + qrcode: bin/qrcode + checksum: 10/9a1b61760e4ea334545a0f54bbc11c537aba0a17cf52cab9fa1b07f8a1337eed0bc6f7fde41b197f2c82c249bc48728983bfaf861bb7ecb29dc597b2ae33c424 + languageName: node + linkType: hard + "qs@npm:6.10.3": version: 6.10.3 resolution: "qs@npm:6.10.3" @@ -27498,6 +31307,18 @@ __metadata: languageName: node linkType: hard +"query-string@npm:7.1.3": + version: 7.1.3 + resolution: "query-string@npm:7.1.3" + dependencies: + decode-uri-component: "npm:^0.2.2" + filter-obj: "npm:^1.1.0" + split-on-first: "npm:^1.0.0" + strict-uri-encode: "npm:^2.0.0" + checksum: 10/3b6f2c167e76ca4094c5f1a9eb276efcbb9ebfd8b1a28c413f3c4e4e7d6428c8187bf46c8cbc9f92a229369dd0015de10a7fd712c8cee98d5d84c2ac6140357e + languageName: node + linkType: hard + "query-string@npm:^5.0.1": version: 5.1.1 resolution: "query-string@npm:5.1.1" @@ -27544,6 +31365,23 @@ __metadata: languageName: node linkType: hard +"radix3@npm:^1.1.2": + version: 1.1.2 + resolution: "radix3@npm:1.1.2" + checksum: 10/5ed01a8e4b753e325c6ecb01d993de77f690e548ef9e149e7dc403ee7b109c2cb41e3d09bc3ce004d872c67c8dca1d556dbf7808b1ac7df9f86994e57d757557 + languageName: node + linkType: hard + +"rainbow-sprinkles@npm:^0.17.3": + version: 0.17.3 + resolution: "rainbow-sprinkles@npm:0.17.3" + peerDependencies: + "@vanilla-extract/css": ^1 + "@vanilla-extract/dynamic": ^2 + checksum: 10/63c668a58032a6914974e4e13c587338f70c1cd76d388ba133f920112c7f6e002b3ed6d98f6dc2c045f5522eafd78dc58aa6103b520150dcb8ca23b3b8744ce0 + languageName: node + linkType: hard + "ramda@npm:0.29.0": version: 0.29.0 resolution: "ramda@npm:0.29.0" @@ -27605,6 +31443,66 @@ __metadata: languageName: node linkType: hard +"react-aria@npm:^3.34.3": + version: 3.35.1 + resolution: "react-aria@npm:3.35.1" + dependencies: + "@internationalized/string": "npm:^3.2.4" + "@react-aria/breadcrumbs": "npm:^3.5.18" + "@react-aria/button": "npm:^3.10.1" + "@react-aria/calendar": "npm:^3.5.13" + "@react-aria/checkbox": "npm:^3.14.8" + "@react-aria/color": "npm:^3.0.1" + "@react-aria/combobox": "npm:^3.10.5" + "@react-aria/datepicker": "npm:^3.11.4" + "@react-aria/dialog": "npm:^3.5.19" + "@react-aria/dnd": "npm:^3.7.4" + "@react-aria/focus": "npm:^3.18.4" + "@react-aria/gridlist": "npm:^3.9.5" + "@react-aria/i18n": "npm:^3.12.3" + "@react-aria/interactions": "npm:^3.22.4" + "@react-aria/label": "npm:^3.7.12" + "@react-aria/link": "npm:^3.7.6" + "@react-aria/listbox": "npm:^3.13.5" + "@react-aria/menu": "npm:^3.15.5" + "@react-aria/meter": "npm:^3.4.17" + "@react-aria/numberfield": "npm:^3.11.8" + "@react-aria/overlays": "npm:^3.23.4" + "@react-aria/progress": "npm:^3.4.17" + "@react-aria/radio": "npm:^3.10.9" + "@react-aria/searchfield": "npm:^3.7.10" + "@react-aria/select": "npm:^3.14.11" + "@react-aria/selection": "npm:^3.20.1" + "@react-aria/separator": "npm:^3.4.3" + "@react-aria/slider": "npm:^3.7.13" + "@react-aria/ssr": "npm:^3.9.6" + "@react-aria/switch": "npm:^3.6.9" + "@react-aria/table": "npm:^3.15.5" + "@react-aria/tabs": "npm:^3.9.7" + "@react-aria/tag": "npm:^3.4.7" + "@react-aria/textfield": "npm:^3.14.10" + "@react-aria/tooltip": "npm:^3.7.9" + "@react-aria/utils": "npm:^3.25.3" + "@react-aria/visually-hidden": "npm:^3.8.17" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/eb63ad498582374f1708c6691faf08ecaa887da02f95277e858b384bed7d4a2fbca0c24adf1d16c0929ca965163bbd5c73cea5251d4b834b221a65313760f89c + languageName: node + linkType: hard + +"react-clientside-effect@npm:^1.2.6": + version: 1.2.6 + resolution: "react-clientside-effect@npm:1.2.6" + dependencies: + "@babel/runtime": "npm:^7.12.13" + peerDependencies: + react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: 10/45411b2e1d5e77ce8586ef0fa6cef2d394da4660af90a2c0f044a2170a0b601ac023ac2bc62d6109201969329a8dbd13bd1a4bd4027be3980e4fde7c6a48bee3 + languageName: node + linkType: hard + "react-colorful@npm:^5.1.2": version: 5.6.1 resolution: "react-colorful@npm:5.6.1" @@ -27679,6 +31577,33 @@ __metadata: languageName: node linkType: hard +"react-fast-compare@npm:3.2.2": + version: 3.2.2 + resolution: "react-fast-compare@npm:3.2.2" + checksum: 10/a6826180ba75cefba1c8d3ac539735f9b627ca05d3d307fe155487f5d0228d376dac6c9708d04a283a7b9f9aee599b637446635b79c8c8753d0b4eece56c125c + languageName: node + linkType: hard + +"react-focus-lock@npm:^2.9.6": + version: 2.13.2 + resolution: "react-focus-lock@npm:2.13.2" + dependencies: + "@babel/runtime": "npm:^7.0.0" + focus-lock: "npm:^1.3.5" + prop-types: "npm:^15.6.2" + react-clientside-effect: "npm:^1.2.6" + use-callback-ref: "npm:^1.3.2" + use-sidecar: "npm:^1.1.2" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/a169e060e2a2457062489fb5e48811f334823b3878a4599162878c93c683f47807407044009c3886da07f2adaa9d7325cbb4fafc104a7f8cd4d1dad7325304f8 + languageName: node + linkType: hard + "react-is@npm:18.1.0": version: 18.1.0 resolution: "react-is@npm:18.1.0" @@ -27686,7 +31611,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10/5aa564a1cde7d391ac980bedee21202fc90bdea3b399952117f54fb71a932af1e5902020144fb354b4690b2414a0c7aafe798eb617b76a3d441d956db7726fdf @@ -27707,6 +31632,19 @@ __metadata: languageName: node linkType: hard +"react-native-webview@npm:^11.26.0": + version: 11.26.1 + resolution: "react-native-webview@npm:11.26.1" + dependencies: + escape-string-regexp: "npm:2.0.0" + invariant: "npm:2.2.4" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/d64123c73e7795096434135a1bec2aef5caf71a4c1c95b1416cc528bc55f5c4a89df2d311ad3637594f120e864b5798e2c4ea4eb7153bf938ad167c54e7a7e61 + languageName: node + linkType: hard + "react-refresh@npm:^0.14.0": version: 0.14.2 resolution: "react-refresh@npm:0.14.2" @@ -27714,7 +31652,7 @@ __metadata: languageName: node linkType: hard -"react-remove-scroll-bar@npm:^2.3.3": +"react-remove-scroll-bar@npm:^2.3.3, react-remove-scroll-bar@npm:^2.3.6": version: 2.3.6 resolution: "react-remove-scroll-bar@npm:2.3.6" dependencies: @@ -27749,6 +31687,59 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll@npm:2.6.0, react-remove-scroll@npm:^2.5.7": + version: 2.6.0 + resolution: "react-remove-scroll@npm:2.6.0" + dependencies: + react-remove-scroll-bar: "npm:^2.3.6" + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.0" + use-sidecar: "npm:^1.1.2" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/9fac79e1c2ed2c85729bfe82f61ef4ae5ce51f478736a13892a9a11e05cbd4e9599f9f0e012cb5fc0719e18dc1dd687ab61f516193228615df636db8b851245e + languageName: node + linkType: hard + +"react-stately@npm:^3.32.2": + version: 3.33.0 + resolution: "react-stately@npm:3.33.0" + dependencies: + "@react-stately/calendar": "npm:^3.5.5" + "@react-stately/checkbox": "npm:^3.6.9" + "@react-stately/collections": "npm:^3.11.0" + "@react-stately/color": "npm:^3.8.0" + "@react-stately/combobox": "npm:^3.10.0" + "@react-stately/data": "npm:^3.11.7" + "@react-stately/datepicker": "npm:^3.10.3" + "@react-stately/dnd": "npm:^3.4.3" + "@react-stately/form": "npm:^3.0.6" + "@react-stately/list": "npm:^3.11.0" + "@react-stately/menu": "npm:^3.8.3" + "@react-stately/numberfield": "npm:^3.9.7" + "@react-stately/overlays": "npm:^3.6.11" + "@react-stately/radio": "npm:^3.10.8" + "@react-stately/searchfield": "npm:^3.5.7" + "@react-stately/select": "npm:^3.6.8" + "@react-stately/selection": "npm:^3.17.0" + "@react-stately/slider": "npm:^3.5.8" + "@react-stately/table": "npm:^3.12.3" + "@react-stately/tabs": "npm:^3.6.10" + "@react-stately/toggle": "npm:^3.7.8" + "@react-stately/tooltip": "npm:^3.4.13" + "@react-stately/tree": "npm:^3.8.5" + "@react-types/shared": "npm:^3.25.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + checksum: 10/69da664db3427d19aa8802fa0b7ca5fd49dad2f63e670ae00cefc2398b773568e5a72a0b909e51b64ea2a67dfd4e376ced324b096f159496bc571c5e6a2c0ec9 + languageName: node + linkType: hard + "react-style-singleton@npm:^2.2.1": version: 2.2.1 resolution: "react-style-singleton@npm:2.2.1" @@ -27820,6 +31811,16 @@ __metadata: languageName: node linkType: hard +"read-yaml-file@npm:2.1.0": + version: 2.1.0 + resolution: "read-yaml-file@npm:2.1.0" + dependencies: + js-yaml: "npm:^4.0.0" + strip-bom: "npm:^4.0.0" + checksum: 10/52765eb183e79466f51eebeb19b933cc0f0e907052d062d67300b97e79910064a24b370cdb0b5dd8b05afff3d0ec57282670fd9070dc608e13b11820ac79183d + languageName: node + linkType: hard + "read-yaml-file@npm:^1.1.0": version: 1.1.0 resolution: "read-yaml-file@npm:1.1.0" @@ -27832,7 +31833,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.0, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.3.3, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -27862,7 +31863,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.0": +"readable-stream@npm:^3.1.0, readable-stream@npm:^3.6.2": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -27884,7 +31885,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^4.0.0": +"readable-stream@npm:^3.6.2 || ^4.4.2, readable-stream@npm:^4.0.0": version: 4.5.2 resolution: "readable-stream@npm:4.5.2" dependencies: @@ -27922,6 +31923,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.1.0": + version: 0.1.0 + resolution: "real-require@npm:0.1.0" + checksum: 10/0ba1c440dc9b7777d35a97f755312bf236be0847249f76cc9789c5c08d141f5d80b8564888e6a94ed0253fabf597b6892f8502c4e5658fb98f88642633a39723 + languageName: node + linkType: hard + "real-require@npm:^0.2.0": version: 0.2.0 resolution: "real-require@npm:0.2.0" @@ -28223,13 +32231,6 @@ __metadata: languageName: node linkType: hard -"requireindex@npm:^1.1.0": - version: 1.2.0 - resolution: "requireindex@npm:1.2.0" - checksum: 10/266d1cb31f6cbc4b6cf2e898f5bbc45581f7919bcf61bba5c45d0adb69b722b9ff5a13727be3350cde4520d7cd37f39df45d58a29854baaa4552cd6b05ae4a1a - languageName: node - linkType: hard - "resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" @@ -28310,7 +32311,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.2, resolve@npm:^1.22.8": +"resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.2, resolve@npm:^1.22.4, resolve@npm:^1.22.8": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -28365,7 +32366,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": +"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -28419,6 +32420,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^4.0.0": + version: 4.0.0 + resolution: "restore-cursor@npm:4.0.0" + dependencies: + onetime: "npm:^5.1.0" + signal-exit: "npm:^3.0.2" + checksum: 10/5b675c5a59763bf26e604289eab35711525f11388d77f409453904e1e69c0d37ae5889295706b2c81d23bd780165084d040f9b68fffc32cc921519031c4fa4af + languageName: node + linkType: hard + "retry-request@npm:^7.0.0": version: 7.0.2 resolution: "retry-request@npm:7.0.2" @@ -28843,6 +32854,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.1.0": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c + languageName: node + linkType: hard + "safe-stable-stringify@npm:^2.3.1": version: 2.4.3 resolution: "safe-stable-stringify@npm:2.4.3" @@ -28960,6 +32978,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.6.3, semver@npm:^7.3.8, semver@npm:^7.5.0, semver@npm:^7.5.1, semver@npm:^7.6.0, semver@npm:^7.6.3": + version: 7.6.3 + resolution: "semver@npm:7.6.3" + bin: + semver: bin/semver.js + checksum: 10/36b1fbe1a2b6f873559cd57b238f1094a053dbfd997ceeb8757d79d1d2089c56d1321b9f1069ce263dc64cfa922fa1d2ad566b39426fe1ac6c723c1487589e10 + languageName: node + linkType: hard + "semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.7.0": version: 5.7.1 resolution: "semver@npm:5.7.1" @@ -28998,15 +33025,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.5.1": - version: 7.6.3 - resolution: "semver@npm:7.6.3" - bin: - semver: bin/semver.js - checksum: 10/36b1fbe1a2b6f873559cd57b238f1094a053dbfd997ceeb8757d79d1d2089c56d1321b9f1069ce263dc64cfa922fa1d2ad566b39426fe1ac6c723c1487589e10 - languageName: node - linkType: hard - "semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" @@ -29159,7 +33177,7 @@ __metadata: languageName: node linkType: hard -"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8": version: 2.4.11 resolution: "sha.js@npm:2.4.11" dependencies: @@ -29359,6 +33377,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^5.1.0": + version: 5.1.0 + resolution: "slash@npm:5.1.0" + checksum: 10/2c41ec6fb1414cd9bba0fa6b1dd00e8be739e3fe85d079c69d4b09ca5f2f86eafd18d9ce611c0c0f686428638a36c272a6ac14799146a8295f259c10cc45cde4 + languageName: node + linkType: hard + "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -29414,6 +33439,28 @@ __metadata: languageName: node linkType: hard +"socket.io-client@npm:^4.5.1": + version: 4.8.1 + resolution: "socket.io-client@npm:4.8.1" + dependencies: + "@socket.io/component-emitter": "npm:~3.1.0" + debug: "npm:~4.3.2" + engine.io-client: "npm:~6.6.1" + socket.io-parser: "npm:~4.2.4" + checksum: 10/7480cf1ab30eba371a96dd1ce2ce9018dcbeaf81035a066fb89d99df0d0a6388b05840c92d970317c739956b68b28b0f4833f3b18e460a24eef557b9bca127c1 + languageName: node + linkType: hard + +"socket.io-parser@npm:~4.2.4": + version: 4.2.4 + resolution: "socket.io-parser@npm:4.2.4" + dependencies: + "@socket.io/component-emitter": "npm:~3.1.0" + debug: "npm:~4.3.1" + checksum: 10/4be500a9ff7e79c50ec25af11048a3ed34b4c003a9500d656786a1e5bceae68421a8394cf3eb0aa9041f85f36c1a9a737617f4aee91a42ab4ce16ffb2aa0c89c + languageName: node + linkType: hard + "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -29682,6 +33729,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^2.2.1": + version: 2.8.0 + resolution: "sonic-boom@npm:2.8.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10/05351d9f44bac59b2a4ab42ee22bf81b8c3bbd22db20183d78d5f2067557eb623e0eaf93b2bc0f8417bee92ca372bc26e0d83e3bdb0ffebcc33738ac1c191876 + languageName: node + linkType: hard + "sonic-boom@npm:^3.7.0": version: 3.8.0 resolution: "sonic-boom@npm:3.8.0" @@ -29718,7 +33774,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.5.0": +"source-map@npm:^0.5.0, source-map@npm:^0.5.7": version: 0.5.7 resolution: "source-map@npm:0.5.7" checksum: 10/9b4ac749ec5b5831cad1f8cc4c19c4298ebc7474b24a0acf293e2f040f03f8eeccb3d01f12aa0f90cf46d555c887e03912b83a042c627f419bda5152d89c5269 @@ -29799,6 +33855,13 @@ __metadata: languageName: node linkType: hard +"split-on-first@npm:^1.0.0": + version: 1.1.0 + resolution: "split-on-first@npm:1.1.0" + checksum: 10/16ff85b54ddcf17f9147210a4022529b343edbcbea4ce977c8f30e38408b8d6e0f25f92cd35b86a524d4797f455e29ab89eb8db787f3c10708e0b47ebf528d30 + languageName: node + linkType: hard + "split2@npm:^4.0.0": version: 4.2.0 resolution: "split2@npm:4.2.0" @@ -29892,6 +33955,20 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.7.0": + version: 3.8.0 + resolution: "std-env@npm:3.8.0" + checksum: 10/034176196cfcaaab16dbdd96fc9e925a9544799fb6dc5a3e36fe43270f3a287c7f779d785b89edaf22cef2b5f1dcada2aae67430b8602e785ee74bdb3f671768 + languageName: node + linkType: hard + +"stdin-discarder@npm:^0.2.1": + version: 0.2.2 + resolution: "stdin-discarder@npm:0.2.2" + checksum: 10/642ffd05bd5b100819d6b24a613d83c6e3857c6de74eb02fc51506fa61dc1b0034665163831873868157c4538d71e31762bcf319be86cea04c3aba5336470478 + languageName: node + linkType: hard + "stealthy-require@npm:^1.1.1": version: 1.1.1 resolution: "stealthy-require@npm:1.1.1" @@ -29973,6 +34050,13 @@ __metadata: languageName: node linkType: hard +"strict-uri-encode@npm:^2.0.0": + version: 2.0.0 + resolution: "strict-uri-encode@npm:2.0.0" + checksum: 10/eaac4cf978b6fbd480f1092cab8b233c9b949bcabfc9b598dd79a758f7243c28765ef7639c876fa72940dac687181b35486ea01ff7df3e65ce3848c64822c581 + languageName: node + linkType: hard + "string-argv@npm:^0.3.1": version: 0.3.1 resolution: "string-argv@npm:0.3.1" @@ -30051,6 +34135,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.0.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" + dependencies: + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/42f9e82f61314904a81393f6ef75b832c39f39761797250de68c041d8ba4df2ef80db49ab6cd3a292923a6f0f409b8c9980d120f7d32c820b4a8a84a2598a295 + languageName: node + linkType: hard + "string.prototype.matchall@npm:^4.0.11": version: 4.0.11 resolution: "string.prototype.matchall@npm:4.0.11" @@ -30233,6 +34328,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^7.1.0": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10/475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 + languageName: node + linkType: hard + "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -30325,6 +34429,13 @@ __metadata: languageName: node linkType: hard +"stylis@npm:4.2.0": + version: 4.2.0 + resolution: "stylis@npm:4.2.0" + checksum: 10/58359185275ef1f39c339ae94e598168aa6bb789f6cf0d52e726c1e7087a94e9c17f0385a28d34483dec1ffc2c75670ec714dc5603d99c3124ec83bc2b0a0f42 + languageName: node + linkType: hard + "sucrase@npm:^3.32.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" @@ -30350,6 +34461,13 @@ __metadata: languageName: node linkType: hard +"superstruct@npm:^1.0.3": + version: 1.0.4 + resolution: "superstruct@npm:1.0.4" + checksum: 10/9b3fd70a08c5ad3ea78b5c6b7ab90d31dde71af10448208d296c3d29ba2e55dfd817dfef75957163ee032163d04c4b2e0cb2fddff30313516aa60f748c1a48da + languageName: node + linkType: hard + "superstruct@npm:^2.0.2": version: 2.0.2 resolution: "superstruct@npm:2.0.2" @@ -30479,6 +34597,50 @@ __metadata: languageName: node linkType: hard +"syncpack@npm:^13.0.0": + version: 13.0.0 + resolution: "syncpack@npm:13.0.0" + dependencies: + "@effect/schema": "npm:0.71.1" + chalk: "npm:5.3.0" + chalk-template: "npm:1.1.0" + commander: "npm:12.1.0" + cosmiconfig: "npm:9.0.0" + effect: "npm:3.6.5" + enquirer: "npm:2.4.1" + fast-check: "npm:3.21.0" + globby: "npm:14.0.2" + jsonc-parser: "npm:3.3.1" + minimatch: "npm:9.0.5" + npm-package-arg: "npm:11.0.3" + ora: "npm:8.0.1" + prompts: "npm:2.4.2" + read-yaml-file: "npm:2.1.0" + semver: "npm:7.6.3" + tightrope: "npm:0.2.0" + ts-toolbelt: "npm:9.6.0" + bin: + syncpack: dist/bin.js + syncpack-fix-mismatches: dist/bin-fix-mismatches/index.js + syncpack-format: dist/bin-format/index.js + syncpack-lint: dist/bin-lint/index.js + syncpack-lint-semver-ranges: dist/bin-lint-semver-ranges/index.js + syncpack-list: dist/bin-list/index.js + syncpack-list-mismatches: dist/bin-list-mismatches/index.js + syncpack-prompt: dist/bin-prompt/index.js + syncpack-set-semver-ranges: dist/bin-set-semver-ranges/index.js + syncpack-update: dist/bin-update/index.js + checksum: 10/c80f60faaad640f38de4e1fb5e7daf5dac62963def211b7b3989186c503dec8800c197d3051798553998331c0b0e05ab4b96dd765cd51c7c6cd3651f87c559ed + languageName: node + linkType: hard + +"system-architecture@npm:^0.1.0": + version: 0.1.0 + resolution: "system-architecture@npm:0.1.0" + checksum: 10/ca0dd793c45c354ab57dd7fc8ce7dc9923a6e07382bd3b22eb5b08f55ddb0217c390d00767549c5155fd4ce7ef23ffdd8cfb33dd4344cbbd37837d085a50f6f0 + languageName: node + linkType: hard + "tabbable@npm:^6.0.0": version: 6.2.0 resolution: "tabbable@npm:6.2.0" @@ -30557,6 +34719,13 @@ __metadata: languageName: node linkType: hard +"tapable@npm:^2.2.0": + version: 2.2.1 + resolution: "tapable@npm:2.2.1" + checksum: 10/1769336dd21481ae6347611ca5fca47add0962fd8e80466515032125eca0084a4f0ede11e65341b9c0018ef4e1cf1ad820adbb0fba7cc99865c6005734000b0a + languageName: node + linkType: hard + "tar-fs@npm:^2.0.0, tar-fs@npm:^2.1.1": version: 2.1.1 resolution: "tar-fs@npm:2.1.1" @@ -30764,6 +34933,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^0.15.1": + version: 0.15.2 + resolution: "thread-stream@npm:0.15.2" + dependencies: + real-require: "npm:^0.1.0" + checksum: 10/ca0a4f5bf45db88b48b41af0299455eaa8f01dd3ef8279e7ba6909c295b3ab79ddf576b595cbbceb4dbdf4012b17c6449805092926163fcbf30ac1604cb595b1 + languageName: node + linkType: hard + "thread-stream@npm:^2.0.0": version: 2.4.1 resolution: "thread-stream@npm:2.4.1" @@ -30790,6 +34968,13 @@ __metadata: languageName: node linkType: hard +"tightrope@npm:0.2.0": + version: 0.2.0 + resolution: "tightrope@npm:0.2.0" + checksum: 10/b57a6dec1a83d1d9b9395bca21f0b2dc4ff84d97a2302f43af240d312573bc04327e8e40394b4c2ac7172993b76ba31b34d0295b79f4f8abe9195a051782bff6 + languageName: node + linkType: hard + "timed-out@npm:^4.0.0, timed-out@npm:^4.0.1": version: 4.0.1 resolution: "timed-out@npm:4.0.1" @@ -30804,6 +34989,20 @@ __metadata: languageName: node linkType: hard +"tiny-secp256k1@npm:^1.1.3": + version: 1.1.7 + resolution: "tiny-secp256k1@npm:1.1.7" + dependencies: + bindings: "npm:^1.3.0" + bn.js: "npm:^4.11.8" + create-hmac: "npm:^1.1.7" + elliptic: "npm:^6.4.0" + nan: "npm:^2.13.2" + node-gyp: "npm:latest" + checksum: 10/588393c36df18b2819787fc91616e1fcb930509307eecc460ad45c7011f99f900eea28493a36eb203c3933ae76f737e6a1b978fa0b8a391fcc5dd7b64d57c572 + languageName: node + linkType: hard + "tinybench@npm:^2.5.1": version: 2.9.0 resolution: "tinybench@npm:2.9.0" @@ -30864,6 +35063,13 @@ __metadata: languageName: node linkType: hard +"toggle-selection@npm:^1.0.6": + version: 1.0.6 + resolution: "toggle-selection@npm:1.0.6" + checksum: 10/9a0ed0ecbaac72b4944888dacd79fe0a55eeea76120a4c7e46b3bb3d85b24f086e90560bb22f5a965654a25ab43d79ec47dfdb3f1850ba740b14c5a50abc7040 + languageName: node + linkType: hard + "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -30922,6 +35128,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^1.3.0": + version: 1.4.1 + resolution: "ts-api-utils@npm:1.4.1" + peerDependencies: + typescript: ">=4.2.0" + checksum: 10/2f32698ed1c06e57d934704ff2579a905895441ef0a29f732242d3d3f651abd5d09610f702c656e85b73457582a1ded43adeef82e9f6d665ae0fb66497cf39f6 + languageName: node + linkType: hard + "ts-command-line-args@npm:^2.2.0": version: 2.3.1 resolution: "ts-command-line-args@npm:2.3.1" @@ -31056,27 +35271,53 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.11.1, tslib@npm:^1.13.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": +"ts-toolbelt@npm:9.6.0": + version: 9.6.0 + resolution: "ts-toolbelt@npm:9.6.0" + checksum: 10/2c2dea2631dbd7372a79cccc6d09a377a6ca2f319f767fd239d2e312cd1d9165a90f8c1777a047227bfdcda6aeba3addbadce88fdfc7f43caf4534d385a43c82 + languageName: node + linkType: hard + +"tsconfig-paths@npm:^3.15.0": + version: 3.15.0 + resolution: "tsconfig-paths@npm:3.15.0" + dependencies: + "@types/json5": "npm:^0.0.29" + json5: "npm:^1.0.2" + minimist: "npm:^1.2.6" + strip-bom: "npm:^3.0.0" + checksum: 10/2041beaedc6c271fc3bedd12e0da0cc553e65d030d4ff26044b771fac5752d0460944c0b5e680f670c2868c95c664a256cec960ae528888db6ded83524e33a14 + languageName: node + linkType: hard + +"tslib@npm:1.14.1, tslib@npm:^1.11.1, tslib@npm:^1.13.0, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1": - version: 2.6.3 - resolution: "tslib@npm:2.6.3" - checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c +"tslib@npm:2, tslib@npm:^2.0.3": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 languageName: node linkType: hard -"tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0": +"tslib@npm:2.4.0, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" checksum: 10/d8379e68b36caf082c1905ec25d17df8261e1d68ddc1abfd6c91158a064f6e4402039ae7c02cf4c81d12e3a2a2c7cd8ea2f57b233eb80136a2e3e7279daf2911 languageName: node linkType: hard +"tslib@npm:^2.0.0, tslib@npm:^2.0.1": + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c + languageName: node + linkType: hard + "tslib@npm:^2.2.0": version: 2.7.0 resolution: "tslib@npm:2.7.0" @@ -31098,17 +35339,6 @@ __metadata: languageName: node linkType: hard -"tsutils@npm:^3.21.0": - version: 3.21.0 - resolution: "tsutils@npm:3.21.0" - dependencies: - tslib: "npm:^1.8.1" - peerDependencies: - typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - checksum: 10/ea036bec1dd024e309939ffd49fda7a351c0e87a1b8eb049570dd119d447250e2c56e0e6c00554e8205760e7417793fdebff752a46e573fbe07d4f375502a5b2 - languageName: node - linkType: hard - "tsx@npm:^4.19.1": version: 4.19.1 resolution: "tsx@npm:4.19.1" @@ -31125,22 +35355,6 @@ __metadata: languageName: node linkType: hard -"tsx@npm:^4.7.1": - version: 4.7.1 - resolution: "tsx@npm:4.7.1" - dependencies: - esbuild: "npm:~0.19.10" - fsevents: "npm:~2.3.3" - get-tsconfig: "npm:^4.7.2" - dependenciesMeta: - fsevents: - optional: true - bin: - tsx: dist/cli.mjs - checksum: 10/3a462b595f31ae58b31f9c6e8c450577dc87660b1225012bd972b6b58d7d2f6c4034728763ebc53bb731acff68de8b0fa50586e4c1ec4c086226f1788ccf9b7d - languageName: node - linkType: hard - "tty-table@npm:^4.1.5": version: 4.2.3 resolution: "tty-table@npm:4.2.3" @@ -31473,6 +35687,13 @@ __metadata: languageName: node linkType: hard +"typeforce@npm:^1.11.5": + version: 1.18.0 + resolution: "typeforce@npm:1.18.0" + checksum: 10/dbf98c75b1d57e56e33c1e1271d5505e67981f4e6a2e2e6e8e31160b58777fea1726160810b6c606517db16476805b7dce315926ba2d4859b9a56cab05b7a41f + languageName: node + linkType: hard + "typescript@npm:5.3.3": version: 5.3.3 resolution: "typescript@npm:5.3.3" @@ -31514,6 +35735,15 @@ __metadata: languageName: node linkType: hard +"ua-parser-js@npm:^1.0.37": + version: 1.0.39 + resolution: "ua-parser-js@npm:1.0.39" + bin: + ua-parser-js: script/cli.js + checksum: 10/dd4026b6ece8a34a0d39b6de5542154c4506077d8def8647a300a29e1b3ffa0e23f5c8eeeb8101df6162b7b3eb3597d0b4adb031ae6104cbdb730d6ebc07f3c0 + languageName: node + linkType: hard + "ufo@npm:^1.5.3, ufo@npm:^1.5.4": version: 1.5.4 resolution: "ufo@npm:1.5.4" @@ -31530,6 +35760,24 @@ __metadata: languageName: node linkType: hard +"uint8arrays@npm:3.1.0": + version: 3.1.0 + resolution: "uint8arrays@npm:3.1.0" + dependencies: + multiformats: "npm:^9.4.2" + checksum: 10/caf1cd6a1cdbd7c59d6c8698c06a6d603380942b5745b3fddcd1b16f7a84a4f351fb8c6ac41f4cb2c59c226bb6d954733a6e20a42dec6f3fd266a02270a5088d + languageName: node + linkType: hard + +"uint8arrays@npm:^3.0.0": + version: 3.1.1 + resolution: "uint8arrays@npm:3.1.1" + dependencies: + multiformats: "npm:^9.4.2" + checksum: 10/536e70273c040484aa7d522031a9dbca1fe8c06eb58a3ace1064ba68825b4e2764d4a0b604a1c451e7b8be0986dc94f23a419cfe9334bd116716074a2d29b33d + languageName: node + linkType: hard + "ultron@npm:~1.1.0": version: 1.1.1 resolution: "ultron@npm:1.1.1" @@ -31549,6 +35797,13 @@ __metadata: languageName: node linkType: hard +"uncrypto@npm:^0.1.3": + version: 0.1.3 + resolution: "uncrypto@npm:0.1.3" + checksum: 10/0020f74b0ce34723196d8982a73bb7f40cff455a41b8f88ae146b86885f4e66e41a1241fe80a887505c3bd2c7f07ed362b6ed041968370073c40a98496e6a737 + languageName: node + linkType: hard + "undefsafe@npm:^2.0.5": version: 2.0.5 resolution: "undefsafe@npm:2.0.5" @@ -31579,6 +35834,19 @@ __metadata: languageName: node linkType: hard +"unenv@npm:^1.10.0": + version: 1.10.0 + resolution: "unenv@npm:1.10.0" + dependencies: + consola: "npm:^3.2.3" + defu: "npm:^6.1.4" + mime: "npm:^3.0.0" + node-fetch-native: "npm:^1.6.4" + pathe: "npm:^1.1.2" + checksum: 10/23198e150fd3b4db4d7abe444b75ee05a0d36768bd6d94a6aaf5dca830db82e707ccc0f6cca22671327b62c5cd85ada08d4665bf7652afec9de0bdc7a4546249 + languageName: node + linkType: hard + "unenv@npm:unenv-nightly@2.0.0-1724863496.70db6f1": version: 2.0.0-1724863496.70db6f1 resolution: "unenv-nightly@npm:2.0.0-1724863496.70db6f1" @@ -31629,6 +35897,13 @@ __metadata: languageName: node linkType: hard +"unicorn-magic@npm:^0.1.0": + version: 0.1.0 + resolution: "unicorn-magic@npm:0.1.0" + checksum: 10/9b4d0e9809807823dc91d0920a4a4c0cff2de3ebc54ee87ac1ee9bc75eafd609b09d1f14495e0173aef26e01118706196b6ab06a75fe0841028b3983a8af313f + languageName: node + linkType: hard + "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" @@ -31717,6 +35992,65 @@ __metadata: languageName: node linkType: hard +"unstorage@npm:^1.9.0": + version: 1.13.1 + resolution: "unstorage@npm:1.13.1" + dependencies: + anymatch: "npm:^3.1.3" + chokidar: "npm:^3.6.0" + citty: "npm:^0.1.6" + destr: "npm:^2.0.3" + h3: "npm:^1.13.0" + listhen: "npm:^1.9.0" + lru-cache: "npm:^10.4.3" + node-fetch-native: "npm:^1.6.4" + ofetch: "npm:^1.4.1" + ufo: "npm:^1.5.4" + peerDependencies: + "@azure/app-configuration": ^1.7.0 + "@azure/cosmos": ^4.1.1 + "@azure/data-tables": ^13.2.2 + "@azure/identity": ^4.5.0 + "@azure/keyvault-secrets": ^4.9.0 + "@azure/storage-blob": ^12.25.0 + "@capacitor/preferences": ^6.0.2 + "@netlify/blobs": ^6.5.0 || ^7.0.0 || ^8.1.0 + "@planetscale/database": ^1.19.0 + "@upstash/redis": ^1.34.3 + "@vercel/kv": ^1.0.1 + idb-keyval: ^6.2.1 + ioredis: ^5.4.1 + peerDependenciesMeta: + "@azure/app-configuration": + optional: true + "@azure/cosmos": + optional: true + "@azure/data-tables": + optional: true + "@azure/identity": + optional: true + "@azure/keyvault-secrets": + optional: true + "@azure/storage-blob": + optional: true + "@capacitor/preferences": + optional: true + "@netlify/blobs": + optional: true + "@planetscale/database": + optional: true + "@upstash/redis": + optional: true + "@vercel/kv": + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + checksum: 10/a5fea4f83189d222dcb95ce36d0de29b1ab2fa64122f4c4435b63e6dab8bacd630f6234b0599a505de620f719965ea3a3ce068ece759a96e19fd187e1fb9561c + languageName: node + linkType: hard + "untildify@npm:^4.0.0": version: 4.0.0 resolution: "untildify@npm:4.0.0" @@ -31724,6 +36058,19 @@ __metadata: languageName: node linkType: hard +"untun@npm:^0.1.3": + version: 0.1.3 + resolution: "untun@npm:0.1.3" + dependencies: + citty: "npm:^0.1.5" + consola: "npm:^3.2.3" + pathe: "npm:^1.1.1" + bin: + untun: bin/untun.mjs + checksum: 10/6a096002ca13b8442ad1d40840088888cfaa28626eefdd132cd0fd3d3b956af121a9733b7bda32647608e278fb13332d2b72e2c319a27dc55dbc8e709a2f61d4 + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.0.13": version: 1.0.13 resolution: "update-browserslist-db@npm:1.0.13" @@ -31752,6 +36099,13 @@ __metadata: languageName: node linkType: hard +"uqr@npm:^0.1.2": + version: 0.1.2 + resolution: "uqr@npm:0.1.2" + checksum: 10/31f1fe7d7a8121a2670712234524763160985b053e7eb8af7925a131bcde0df11641e15129d988358032da603185456d08dd72b26b507897272eb9640273bfa6 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -31805,7 +36159,7 @@ __metadata: languageName: node linkType: hard -"use-callback-ref@npm:^1.3.0": +"use-callback-ref@npm:^1.3.0, use-callback-ref@npm:^1.3.2": version: 1.3.2 resolution: "use-callback-ref@npm:1.3.2" dependencies: @@ -31848,6 +36202,24 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:1.2.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/a676216affc203876bd47981103f201f28c2731361bb186367e12d287a7566763213a8816910c6eb88265eccd4c230426eb783d64c373c4a180905be8820ed8e + languageName: node + linkType: hard + +"use-sync-external-store@npm:1.2.2": + version: 1.2.2 + resolution: "use-sync-external-store@npm:1.2.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/671e9c190aab9a8374a5d468c6ba17f52c38b6fae970110bc196fc1e2b57204149aea9619be49a1bb5207fb6e51d8afd19c3bcb94afe61813fed039821461dc0 + languageName: node + linkType: hard + "utf-8-validate@npm:5.0.7": version: 5.0.7 resolution: "utf-8-validate@npm:5.0.7" @@ -31909,6 +36281,13 @@ __metadata: languageName: node linkType: hard +"utility-types@npm:^3.10.0": + version: 3.11.0 + resolution: "utility-types@npm:3.11.0" + checksum: 10/a3c51463fc807ed04ccc8b5d0fa6e31f3dcd7a4cbd30ab4bc6d760ce5319dd493d95bf04244693daf316f97e9ab2a37741edfed8748ad38572a595398ad0fdaf + languageName: node + linkType: hard + "utils-merge@npm:1.0.1": version: 1.0.1 resolution: "utils-merge@npm:1.0.1" @@ -31987,6 +36366,31 @@ __metadata: languageName: node linkType: hard +"validate-npm-package-name@npm:^5.0.0": + version: 5.0.1 + resolution: "validate-npm-package-name@npm:5.0.1" + checksum: 10/0d583a1af23aeffea7748742cf22b6802458736fb8b60323ba5949763824d46f796474b0e1b9206beb716f9d75269e19dbd7795d6b038b29d561be95dd827381 + languageName: node + linkType: hard + +"valtio@npm:1.11.2": + version: 1.11.2 + resolution: "valtio@npm:1.11.2" + dependencies: + proxy-compare: "npm:2.5.1" + use-sync-external-store: "npm:1.2.0" + peerDependencies: + "@types/react": ">=16.8" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + checksum: 10/a259f5af204b801668e019855813a8f702c9558961395bb5847f583119428b997efb9b0e6feb5d6e48a76a9b541173a10fdfdb1527a7bd14477a0e0c5beba914 + languageName: node + linkType: hard + "varint@npm:^5.0.0": version: 5.0.2 resolution: "varint@npm:5.0.2" @@ -32012,17 +36416,17 @@ __metadata: languageName: node linkType: hard -"viem@npm:^2.21.40": - version: 2.21.41 - resolution: "viem@npm:2.21.41" +"viem@npm:^2.1.1, viem@npm:^2.21.45": + version: 2.21.45 + resolution: "viem@npm:2.21.45" dependencies: - "@adraffy/ens-normalize": "npm:1.11.0" "@noble/curves": "npm:1.6.0" "@noble/hashes": "npm:1.5.0" "@scure/bip32": "npm:1.5.0" "@scure/bip39": "npm:1.4.0" abitype: "npm:1.0.6" isows: "npm:1.0.6" + ox: "npm:0.1.2" webauthn-p256: "npm:0.0.10" ws: "npm:8.18.0" peerDependencies: @@ -32030,7 +36434,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/bd3d1426584eb319c6ab69949c188d7142f6fa14b38df5ed54c967c5d5246e4eb98a9412ab7d053ff3d649df3d0174fc57f8a1e6f2803ce3aa97be2e010500b9 + checksum: 10/93428588882620de5ed442a5a568367762d39660bdb4ff9c430d5409da1dca085a3648bf0afedb6631bb219b410e58ccabf600ed26228ab9ae73a77a1031c576 languageName: node linkType: hard @@ -32182,6 +36586,25 @@ __metadata: languageName: node linkType: hard +"wagmi@npm:^2.12.26": + version: 2.12.32 + resolution: "wagmi@npm:2.12.32" + dependencies: + "@wagmi/connectors": "npm:5.3.10" + "@wagmi/core": "npm:2.14.6" + use-sync-external-store: "npm:1.2.0" + peerDependencies: + "@tanstack/react-query": ">=5.0.0" + react: ">=18" + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/6456d32df5c7a6ed1bdde1c4512fccf6a4bc45f1cf78e44cc44f952e3a8016f7b413197e431bd4b3bfb5faab78ae79f7bab402f323e12bcaddd67cb3f5cd5c6a + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -32619,6 +37042,20 @@ __metadata: languageName: node linkType: hard +"webextension-polyfill@npm:>=0.10.0 <1.0": + version: 0.12.0 + resolution: "webextension-polyfill@npm:0.12.0" + checksum: 10/77e648b958b573ef075e75a0c180e2bbd74dee17b3145e86d21fcbb168c4999e4a311654fe634b8178997bee9b35ea5808d8d3d3e5ff2ad138f197f4f0ea75d9 + languageName: node + linkType: hard + +"webextension-polyfill@npm:^0.10.0": + version: 0.10.0 + resolution: "webextension-polyfill@npm:0.10.0" + checksum: 10/51ff30ebed4b1aa802b7f0347f05021b2fe492078bb1a597223d43995fcee96e2da8f914a2f6e36f988c1877ed5ab36ca7077f2f3ab828955151a59e4c01bf7e + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -32834,6 +37271,15 @@ __metadata: languageName: node linkType: hard +"wif@npm:^2.0.6": + version: 2.0.6 + resolution: "wif@npm:2.0.6" + dependencies: + bs58check: "npm:<3.0.0" + checksum: 10/c8d7581664532d9ab6d163ee5194a9bec71b089a6e50d54d6ec57a9bd714fcf84bc8d9f22f4cfc7c297fc6ad10b973b8e83eca5c41240163fc61f44b5154b7da + languageName: node + linkType: hard + "word-wrap@npm:~1.2.3": version: 1.2.3 resolution: "word-wrap@npm:1.2.3" @@ -32951,7 +37397,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0": +"wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" dependencies: @@ -33081,7 +37527,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.5.10": +"ws@npm:^7.5.1, ws@npm:^7.5.10": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: @@ -33111,6 +37557,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:~8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d + languageName: node + linkType: hard + "xhr-request-promise@npm:^0.1.2": version: 0.1.3 resolution: "xhr-request-promise@npm:0.1.3" @@ -33147,6 +37608,13 @@ __metadata: languageName: node linkType: hard +"xmlhttprequest-ssl@npm:~2.1.1": + version: 2.1.2 + resolution: "xmlhttprequest-ssl@npm:2.1.2" + checksum: 10/708a177fe41c6c8cd4ec7c04d965b4c01801d87f44383ec639be58bdc14418142969841659e0850db44feee8bec0a3d3e7d33fed22519415f3d0daab04d3f160 + languageName: node + linkType: hard + "xmlhttprequest@npm:1.8.0": version: 1.8.0 resolution: "xmlhttprequest@npm:1.8.0" @@ -33229,7 +37697,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^1.10.2": +"yaml@npm:^1.10.0, yaml@npm:^1.10.2": version: 1.10.2 resolution: "yaml@npm:1.10.2" checksum: 10/e088b37b4d4885b70b50c9fa1b7e54bd2e27f5c87205f9deaffd1fb293ab263d9c964feadb9817a7b129a5bf30a06582cb08750f810568ecc14f3cdbabb79cb3 @@ -33342,7 +37810,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^15.1.0": +"yargs@npm:^15.1.0, yargs@npm:^15.3.1": version: 15.4.1 resolution: "yargs@npm:15.4.1" dependencies: @@ -33457,6 +37925,47 @@ __metadata: languageName: node linkType: hard +"zustand@npm:5.0.0": + version: 5.0.0 + resolution: "zustand@npm:5.0.0" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 10/be75ef4d1b218b143314467bb9e23641231043cad2d5c3a4b2219c46d1609ee799cd8dc9acec9b23d55ec3a2a619a06616e593aea4049f3b7323938af9a33bfe + languageName: node + linkType: hard + +"zustand@npm:^4.5.5": + version: 4.5.5 + resolution: "zustand@npm:4.5.5" + dependencies: + use-sync-external-store: "npm:1.2.2" + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0.6" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + checksum: 10/481b8210187b69678074a1ca51107654c2379688e90407bfcb7961e0803a259742bfd0d77171c3f07e290896ad55fe9659b3863f30d34cb2572650ead1249f25 + languageName: node + linkType: hard + "zx@npm:^8.1.4": version: 8.1.4 resolution: "zx@npm:8.1.4" From 92115c029a9917f0f41d8ad1b1576f04d60b9651 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:33:53 +0330 Subject: [PATCH 068/132] fix: update getWarpCoreConfigOrExit import on MultiChainResolver --- .../cli/src/context/strategies/chain/MultiChainResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts index ec0d6e3e477..c6d96b5dc52 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -14,7 +14,7 @@ import { readYamlOrJson, runFileSelectionStep, } from '../../../utils/files.js'; -import { getWarpCoreConfigOrExit } from '../../../utils/input.js'; +import { getWarpCoreConfigOrExit } from '../../../utils/warp.js'; import { ChainResolver } from './types.js'; From ccc7df69438b031632d1dae96bd3bac5e19ca6d7 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:51:02 +0330 Subject: [PATCH 069/132] fix: no-unused-vars linting issues --- typescript/cli/src/config/strategy.ts | 4 ++-- typescript/utils/src/addresses.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index d9cee7101d1..6900df27389 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -78,7 +78,7 @@ export async function createStrategyConfig({ try { const strategyObj = await readYamlOrJson(outPath); strategy = ChainSubmissionStrategySchema.parse(strategyObj); - } catch (e) { + } catch (_) { strategy = writeYamlOrJson(outPath, {}, 'yaml'); } @@ -177,7 +177,7 @@ export async function createStrategyConfig({ writeYamlOrJson(outPath, strategyConfig); logGreen('✅ Successfully created a new strategy configuration.'); - } catch (e) { + } catch (_) { // don't log error since it may contain sensitive data errorRed( `The strategy configuration is invalid. Please review the submitter settings.`, diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index 93426071efb..e3a91230ad8 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -384,7 +384,7 @@ export function strip0x(hexstr: string) { export function isPrivateKeyEvm(privateKey: string): boolean { try { return new Wallet(privateKey).privateKey === privateKey; - } catch (e) { + } catch (_) { throw new Error('Provided Private Key is not EVM compatible!'); } } From bdf01071854bf35278dfed1082e36e1f6ed7de5d Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:00:45 +0330 Subject: [PATCH 070/132] minor: remove catch unused param --- typescript/cli/src/config/strategy.ts | 4 ++-- typescript/utils/src/addresses.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts index 6900df27389..f57c7d33783 100644 --- a/typescript/cli/src/config/strategy.ts +++ b/typescript/cli/src/config/strategy.ts @@ -78,7 +78,7 @@ export async function createStrategyConfig({ try { const strategyObj = await readYamlOrJson(outPath); strategy = ChainSubmissionStrategySchema.parse(strategyObj); - } catch (_) { + } catch { strategy = writeYamlOrJson(outPath, {}, 'yaml'); } @@ -177,7 +177,7 @@ export async function createStrategyConfig({ writeYamlOrJson(outPath, strategyConfig); logGreen('✅ Successfully created a new strategy configuration.'); - } catch (_) { + } catch { // don't log error since it may contain sensitive data errorRed( `The strategy configuration is invalid. Please review the submitter settings.`, diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index e3a91230ad8..a244c810ba6 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -384,7 +384,7 @@ export function strip0x(hexstr: string) { export function isPrivateKeyEvm(privateKey: string): boolean { try { return new Wallet(privateKey).privateKey === privateKey; - } catch (_) { + } catch { throw new Error('Provided Private Key is not EVM compatible!'); } } From dd870348fe48a32997fb4be04c3e629a88b21353 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:01:05 +0330 Subject: [PATCH 071/132] docs(changeset): Added ZKSync signer support using zksync-ethers package --- .changeset/ten-spiders-trade.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-spiders-trade.md diff --git a/.changeset/ten-spiders-trade.md b/.changeset/ten-spiders-trade.md new file mode 100644 index 00000000000..91eebf52f77 --- /dev/null +++ b/.changeset/ten-spiders-trade.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': patch +--- + +Added ZKSync signer support using zksync-ethers package From 3f58da30a0077eece4f5c20a35a13f325b64cdbc Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:30:47 +0330 Subject: [PATCH 072/132] fix: exclude starknet on widget protocol type --- typescript/widgets/src/logos/protocols.ts | 2 +- .../src/walletIntegrations/multiProtocol.tsx | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/typescript/widgets/src/logos/protocols.ts b/typescript/widgets/src/logos/protocols.ts index 414e4e45070..6cc5d89a2c2 100644 --- a/typescript/widgets/src/logos/protocols.ts +++ b/typescript/widgets/src/logos/protocols.ts @@ -7,7 +7,7 @@ import { EthereumLogo } from './Ethereum.js'; import { SolanaLogo } from './Solana.js'; export const PROTOCOL_TO_LOGO: Record< - ProtocolType, + Exclude, FC, 'ref'>> > = { [ProtocolType.Ethereum]: EthereumLogo, diff --git a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx index 9fb58abb173..ea4de77c84f 100644 --- a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx +++ b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx @@ -44,7 +44,7 @@ export function useAccounts( multiProvider: MultiProtocolProvider, blacklistedAddresses: Address[] = [], ): { - accounts: Record; + accounts: Record, AccountInfo>; readyAccounts: Array; } { const evmAccountInfo = useEthereumAccount(multiProvider); @@ -103,7 +103,7 @@ export function useAccountAddressForChain( export function getAccountAddressForChain( multiProvider: MultiProtocolProvider, chainName?: ChainName, - accounts?: Record, + accounts?: Record, AccountInfo>, ): Address | undefined { if (!chainName || !accounts) return undefined; const protocol = multiProvider.getProtocol(chainName); @@ -119,7 +119,7 @@ export function getAccountAddressForChain( export function getAccountAddressAndPubKey( multiProvider: MultiProtocolProvider, chainName?: ChainName, - accounts?: Record, + accounts?: Record, AccountInfo>, ): { address?: Address; publicKey?: Promise } { const address = getAccountAddressForChain(multiProvider, chainName, accounts); if (!accounts || !chainName || !address) return {}; @@ -128,7 +128,10 @@ export function getAccountAddressAndPubKey( return { address, publicKey }; } -export function useWalletDetails(): Record { +export function useWalletDetails(): Record< + Exclude, + WalletDetails +> { const evmWallet = useEthereumWalletDetails(); const solWallet = useSolanaWalletDetails(); const cosmosWallet = useCosmosWalletDetails(); @@ -143,7 +146,10 @@ export function useWalletDetails(): Record { ); } -export function useConnectFns(): Record void> { +export function useConnectFns(): Record< + Exclude, + () => void +> { const onConnectEthereum = useEthereumConnectFn(); const onConnectSolana = useSolanaConnectFn(); const onConnectCosmos = useCosmosConnectFn(); @@ -158,7 +164,10 @@ export function useConnectFns(): Record void> { ); } -export function useDisconnectFns(): Record Promise> { +export function useDisconnectFns(): Record< + Exclude, + () => Promise +> { const disconnectEvm = useEthereumDisconnectFn(); const disconnectSol = useSolanaDisconnectFn(); const disconnectCosmos = useCosmosDisconnectFn(); @@ -194,7 +203,7 @@ export function useDisconnectFns(): Record Promise> { } export function useActiveChains(multiProvider: MultiProtocolProvider): { - chains: Record; + chains: Record, ActiveChainInfo>; readyChains: Array; } { const evmChain = useEthereumActiveChain(multiProvider); @@ -221,7 +230,7 @@ export function useActiveChains(multiProvider: MultiProtocolProvider): { export function useTransactionFns( multiProvider: MultiProtocolProvider, -): Record { +): Record, ChainTransactionFns> { const { switchNetwork: onSwitchEvmNetwork, sendTransaction: onSendEvmTx } = useEthereumTransactionFns(multiProvider); const { switchNetwork: onSwitchSolNetwork, sendTransaction: onSendSolTx } = From 22a00ffaf96002751a0301c1b778c66d07544be5 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:37:42 +0330 Subject: [PATCH 073/132] fix: modify getWarpCoreConfigOrExit function import path --- .../cli/src/context/strategies/chain/MultiChainResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts index ec0d6e3e477..c6d96b5dc52 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -14,7 +14,7 @@ import { readYamlOrJson, runFileSelectionStep, } from '../../../utils/files.js'; -import { getWarpCoreConfigOrExit } from '../../../utils/input.js'; +import { getWarpCoreConfigOrExit } from '../../../utils/warp.js'; import { ChainResolver } from './types.js'; From 159e8c607db452cb394ab17c7341e574949f8488 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:21:49 +0330 Subject: [PATCH 074/132] fix: read user address from strategy submitter --- .../context/strategies/signer/MultiProtocolSignerFactory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index f10b82b663c..8642fa9d291 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -85,7 +85,7 @@ class StarknetSignerStrategy extends BaseMultiProtocolSigner { async getSignerConfig(chain: ChainName): Promise { const submitter = this.config[chain]?.submitter as { privateKey?: string; - address?: string; + userAddress?: string; }; const privateKey = @@ -95,7 +95,7 @@ class StarknetSignerStrategy extends BaseMultiProtocolSigner { })); const address = - submitter?.address ?? + submitter?.userAddress ?? (await password({ message: `Please enter the signer address for chain ${chain}`, })); From 5c9d890133c69ac5902795a30e2610d88f7f3a90 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:40:11 +0330 Subject: [PATCH 075/132] fix: use starknet strategy for core deployment --- typescript/cli/src/commands/core.ts | 9 +++++++- typescript/cli/src/context/types.ts | 7 +++++- typescript/cli/src/deploy/core.ts | 33 +++++++++++++---------------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/typescript/cli/src/commands/core.ts b/typescript/cli/src/commands/core.ts index 60d84f8776f..5c9e09f9ba3 100644 --- a/typescript/cli/src/commands/core.ts +++ b/typescript/cli/src/commands/core.ts @@ -119,7 +119,13 @@ export const deploy: CommandModuleWithWriteContext<{ 'from-address': fromAddressCommandOption, 'skip-confirmation': skipConfirmationOption, }, - handler: async ({ context, chain, config: configFilePath, dryRun }) => { + handler: async ({ + context, + chain, + config: configFilePath, + dryRun, + multiProtocolSigner, + }) => { logCommandHeader(`Hyperlane Core deployment${dryRun ? ' dry-run' : ''}`); try { @@ -127,6 +133,7 @@ export const deploy: CommandModuleWithWriteContext<{ context, chain, config: readYamlOrJson(configFilePath), + multiProtocolSigner, }); } catch (error: any) { evaluateIfDryRunFailure(error, dryRun); diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 7837972893d..6285cbfd097 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -9,6 +9,8 @@ import type { WarpCoreConfig, } from '@hyperlane-xyz/sdk'; +import { MultiProtocolSignerManager } from './strategies/signer/MultiProtocolSignerManager.js'; + export interface ContextSettings { registryUri: string; registryOverrideUri: string; @@ -36,6 +38,7 @@ export interface WriteCommandContext extends CommandContext { signer: ethers.Signer; isDryRun?: boolean; dryRunChain?: string; + multiProtocolSigner?: MultiProtocolSignerManager; } export type CommandModuleWithContext = CommandModule< @@ -45,5 +48,7 @@ export type CommandModuleWithContext = CommandModule< export type CommandModuleWithWriteContext = CommandModule< {}, - Args & { context: WriteCommandContext } + Args & { context: WriteCommandContext } & { + multiProtocolSigner?: MultiProtocolSignerManager; + } >; diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 7d20df3b763..2dc4dfa2acb 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -1,4 +1,4 @@ -import { Account, RpcProvider } from 'starknet'; +import { Account as StarknetAccount } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; @@ -13,10 +13,11 @@ import { ExplorerLicenseType, StarknetCoreModule, } from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert } from '@hyperlane-xyz/utils'; import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js'; import { requestAndSaveApiKeys } from '../context/context.js'; +import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { WriteCommandContext } from '../context/types.js'; import { log, logBlue, logGray, logGreen } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; @@ -33,6 +34,7 @@ interface DeployParams { context: WriteCommandContext; chain: ChainName; config: CoreConfig; + multiProtocolSigner?: MultiProtocolSignerManager; } interface ApplyParams extends DeployParams { @@ -43,9 +45,8 @@ interface ApplyParams extends DeployParams { * Executes the core deploy command. */ export async function runCoreDeploy(params: DeployParams) { - const { context, config } = params; + const { context, config, multiProtocolSigner } = params; let chain = params.chain; - const { isDryRun, chainMetadata, @@ -69,20 +70,19 @@ export async function runCoreDeploy(params: DeployParams) { if (!skipConfirmation) apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry); - const signer = multiProvider.getSigner(chain); - const deploymentParams: DeployParams = { - context: { ...context, signer }, + context: { ...context }, chain, config, }; - await runDeployPlanStep(deploymentParams); - let deployedAddresses: ChainAddresses; - switch (multiProvider.tryGetProtocol(chain)) { + switch (multiProvider.getProtocol(chain)) { case ProtocolType.Ethereum: { + const signer = multiProvider.getSigner(chain); + await runDeployPlanStep(deploymentParams); + await runPreflightChecksForChains({ ...deploymentParams, chains: [chain], @@ -120,15 +120,12 @@ export async function runCoreDeploy(params: DeployParams) { case ProtocolType.Starknet: { const domainId = multiProvider.getDomainId(chain); - const provider = new RpcProvider({ - nodeUrl: 'http://127.0.0.1:5050', - }); - const account = new Account( - provider, - '0x4acc9b79dae485fb71f309f5b62501a1329789f4418bb4c25353ad5617be4d4', - '0x000000000000000000000000000000002f663fafebbee32e0698f7e13f886c73', + const account = await multiProtocolSigner?.initSigner(chain); + assert(account, 'Starknet account failed!'); + const starknetCoreModule = new StarknetCoreModule( + account as StarknetAccount, + domainId, ); - const starknetCoreModule = new StarknetCoreModule(account, domainId); deployedAddresses = await starknetCoreModule.deploy({ chain, config, From 21758afb91326cb383837283e15eaf7e05f605e6 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:55:27 +0330 Subject: [PATCH 076/132] fix: read multiProtocolSigner from sginer middleware on warp init --- typescript/cli/src/commands/warp.ts | 5 ++++- typescript/cli/src/config/warp.ts | 20 +++----------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index 3cd99fe6a0d..f7a90e685a1 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -8,6 +8,7 @@ import { createWarpRouteDeployConfig, readWarpRouteDeployConfig, } from '../config/warp.js'; +import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { CommandModuleWithContext, CommandModuleWithWriteContext, @@ -154,6 +155,7 @@ export const deploy: CommandModuleWithWriteContext<{ export const init: CommandModuleWithContext<{ advanced: boolean; out: string; + multiProtocolSigner?: MultiProtocolSignerManager; }> = { command: 'init', describe: 'Create a warp route configuration.', @@ -165,13 +167,14 @@ export const init: CommandModuleWithContext<{ }, out: outputFileCommandOption(DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH), }, - handler: async ({ context, advanced, out }) => { + handler: async ({ context, advanced, out, multiProtocolSigner }) => { logCommandHeader('Hyperlane Warp Configure'); await createWarpRouteDeployConfig({ context, outPath: out, advanced, + multiProtocolSigner, }); process.exit(0); }, diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index a2cd19d57ca..1c315ee6255 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -21,7 +21,6 @@ import { promiseObjAll, } from '@hyperlane-xyz/utils'; -import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; @@ -37,7 +36,6 @@ import { } from '../utils/input.js'; import { createAdvancedIsmConfig } from './ism.js'; -import { readChainSubmissionStrategyConfig } from './strategy.js'; const TYPE_DESCRIPTIONS: Record = { [TokenType.synthetic]: 'A new ERC20 with remote transfer functionality', @@ -118,10 +116,12 @@ export async function createWarpRouteDeployConfig({ context, outPath, advanced = false, + multiProtocolSigner, }: { context: CommandContext; outPath: string; advanced: boolean; + multiProtocolSigner?: MultiProtocolSignerManager; }) { logBlue('Creating a new warp route deployment config...'); @@ -134,26 +134,12 @@ export async function createWarpRouteDeployConfig({ requiresConfirmation: !context.skipConfirmation, }); - const strategyConfig = await readChainSubmissionStrategyConfig( - context.strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, - ); - - const multiProtocolSigner = new MultiProtocolSignerManager( - strategyConfig, - warpChains, - context.multiProvider, - { key: context.key }, - ); - - const multiProviderWithSigners = await multiProtocolSigner.getMultiProvider(); - const result: WarpRouteDeployConfig = {}; let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); - const owner = await detectAndConfirmOrPrompt( - async () => multiProviderWithSigners.getSigner(chain).getAddress(), + async () => (await multiProtocolSigner?.initSigner(chain))?.getAddress(), 'Enter the desired', 'owner address', 'signer', From 0acfcbfe7fb4a7399ff70fe0e80b7fe597ca4cb6 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:18:09 +0330 Subject: [PATCH 077/132] refactor: implement MultiProtocolSigner for Starknet warp deployments --- typescript/cli/src/commands/warp.ts | 3 +- typescript/cli/src/config/warp.ts | 5 +- typescript/cli/src/deploy/warp.ts | 47 ++++++++++--------- .../sdk/src/token/StarknetERC20WarpModule.ts | 24 ++++++---- 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index f7a90e685a1..73910656c44 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -134,7 +134,7 @@ export const deploy: CommandModuleWithWriteContext<{ 'dry-run': dryRunCommandOption, 'from-address': fromAddressCommandOption, }, - handler: async ({ context, config, dryRun }) => { + handler: async ({ context, config, dryRun, multiProtocolSigner }) => { logCommandHeader( `Hyperlane Warp Route Deployment${dryRun ? ' Dry-Run' : ''}`, ); @@ -143,6 +143,7 @@ export const deploy: CommandModuleWithWriteContext<{ await runWarpRouteDeploy({ context, warpRouteDeploymentConfigPath: config, + multiProtocolSigner, }); } catch (error: any) { evaluateIfDryRunFailure(error, dryRun); diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index f5f69aad76a..4731b58f51e 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -1,6 +1,6 @@ import { confirm, input, select } from '@inquirer/prompts'; import { Signer } from 'ethers'; -import { Account } from 'starknet'; +import { Account as StarknetAccount } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { @@ -145,7 +145,8 @@ export async function createWarpRouteDeployConfig({ ( (await multiProtocolSigner?.initSigner(chain)) as Signer )?.getAddress() || - ((await multiProtocolSigner?.initSigner(chain)) as Account)?.address, + ((await multiProtocolSigner?.initSigner(chain)) as StarknetAccount) + ?.address, 'Enter the desired', 'owner address', 'signer', diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index c19badb5980..3e77f530edd 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -1,6 +1,5 @@ import { confirm } from '@inquirer/prompts'; import { groupBy } from 'lodash-es'; -import { Account, RpcProvider } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; @@ -67,6 +66,7 @@ import { import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { requestAndSaveApiKeys } from '../context/context.js'; +import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { WriteCommandContext } from '../context/types.js'; import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; import { getSubmitterBuilder } from '../submit/submit.js'; @@ -94,12 +94,15 @@ interface WarpApplyParams extends DeployParams { export async function runWarpRouteDeploy({ context, warpRouteDeploymentConfigPath, + multiProtocolSigner, }: { context: WriteCommandContext; warpRouteDeploymentConfigPath?: string; + multiProtocolSigner?: MultiProtocolSignerManager; }) { - const { signer, skipConfirmation, chainMetadata, registry, multiProvider } = - context; + const { skipConfirmation, chainMetadata, registry } = context; + const multiProvider = await multiProtocolSigner?.getMultiProvider(); + assert(multiProvider, 'Multiprovider failed!'); if ( !warpRouteDeploymentConfigPath || @@ -142,13 +145,12 @@ export async function runWarpRouteDeploy({ switch (protocol) { case ProtocolType.Ethereum: { - const userAddress = await signer.getAddress(); await runPreflightChecksForChains({ context, chains: protocolChains, minGas: MINIMUM_WARP_DEPLOY_GAS, }); - await prepareDeploy(context, userAddress, protocolChains); + await prepareDeploy(context, null, protocolChains); const deployedContracts = await executeDeploy( { context, warpDeployConfig: warpRouteConfig }, apiKeys, @@ -170,12 +172,18 @@ export async function runWarpRouteDeploy({ break; case ProtocolType.Starknet: + assert( + multiProtocolSigner, + 'multi protocol signer is required for starknet chain deployment', + ); const addresses = await executeStarknetDeployments({ warpRouteConfig, - context, + multiProvider, + multiProtocolSigner, }); const warpCoreConfig = await getWarpCoreConfigForStarknet( - { context, warpDeployConfig: warpRouteConfig }, + warpRouteConfig, + multiProvider, addresses, ); deployments.tokens = [...deployments.tokens, ...warpCoreConfig.tokens]; @@ -1010,41 +1018,34 @@ function groupChainsByProtocol( async function executeStarknetDeployments({ warpRouteConfig, - context, + multiProvider, + multiProtocolSigner, }: { warpRouteConfig: WarpRouteDeployConfig; - context: WriteCommandContext; + multiProvider: MultiProvider; + multiProtocolSigner: MultiProtocolSignerManager; }): Promise> { - const provider = new RpcProvider({ - nodeUrl: 'http://127.0.0.1:5050', - }); - const account = new Account( - provider, - '0x4acc9b79dae485fb71f309f5b62501a1329789f4418bb4c25353ad5617be4d4', - '0x000000000000000000000000000000002f663fafebbee32e0698f7e13f886c73', - ); - logBlue('🚀 Beginning Starknet warp deployments...'); - assert(!warpRouteConfig.isNft, 'NFT routes not supported yet!'); const starknetDeployer = new StarknetERC20WarpModule( - account, + async (chain: string) => await multiProtocolSigner.initSigner(chain), warpRouteConfig, - context.multiProvider, + multiProvider, ); return await starknetDeployer.deployToken(); } async function getWarpCoreConfigForStarknet( - { warpDeployConfig, context }: DeployParams, + warpDeployConfig: WarpRouteDeployConfig, + multiProvider: MultiProvider, contracts: ChainMap, ): Promise { const warpCoreConfig: WarpCoreConfig = { tokens: [] }; // TODO: replace with warp read const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( - context.multiProvider, + multiProvider, warpDeployConfig, ); assert( diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index 9470c8cec82..ce962cda8e0 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -1,3 +1,4 @@ +import { Signer } from 'ethers'; import { Account, byteArray, getChecksumAddress } from 'starknet'; import { TokenType } from '@hyperlane-xyz/sdk'; @@ -13,15 +14,12 @@ import { WarpRouteDeployConfig } from './types.js'; export class StarknetERC20WarpModule { protected logger = rootLogger.child({ module: 'StarknetERC20WarpModule' }); - protected deployer: StarknetDeployer; constructor( - protected readonly signer: Account, + protected readonly getAccount: (chain: string) => Promise, protected readonly config: WarpRouteDeployConfig, protected readonly multiProvider: MultiProvider, - ) { - this.deployer = new StarknetDeployer(signer); - } + ) {} public async deployToken(): Promise> { // TODO: manage this in a multi-protocol way, for now works as we just support native-synthetic pair @@ -45,14 +43,18 @@ export class StarknetERC20WarpModule { ) continue; + const account = (await this.getAccount(chain)) as Account; + const deployer = new StarknetDeployer(account); + let ismAddress = await this.getStarknetDeploymentISMAddress({ ismConfig: interchainSecurityModule, mailbox: mailbox, chain, + deployer, }); switch (type) { case TokenType.synthetic: { - const tokenAddress = await this.deployer.deployContract('HypErc20', { + const tokenAddress = await deployer.deployContract('HypErc20', { decimals: tokenMetadata.decimals, mailbox: mailbox, total_supply: tokenMetadata.totalSupply, @@ -60,17 +62,17 @@ export class StarknetERC20WarpModule { symbol: [byteArray.byteArrayFromString(tokenMetadata.symbol)], hook: getChecksumAddress(0), interchain_security_module: ismAddress, - owner: this.signer.address, //TODO: use config.owner, and in warp init ask for starknet owner + owner: account.address, //TODO: use config.owner, and in warp init ask for starknet owner }); addresses[chain] = tokenAddress; break; } case TokenType.native: { - const tokenAddress = await this.deployer.deployContract('HypNative', { + const tokenAddress = await deployer.deployContract('HypNative', { mailbox: mailbox, hook: getChecksumAddress(0), interchain_security_module: ismAddress, - owner: this.signer.address, //TODO: use config.owner, and in warp init ask for starknet owner + owner: account.address, //TODO: use config.owner, and in warp init ask for starknet owner }); addresses[chain] = tokenAddress; break; @@ -86,14 +88,16 @@ export class StarknetERC20WarpModule { ismConfig, chain, mailbox, + deployer, }: { ismConfig?: IsmConfig; chain: string; mailbox: string; + deployer: StarknetDeployer; }): Promise { if (!ismConfig) return getChecksumAddress(0); if (typeof ismConfig === 'string') return ismConfig; - return await this.deployer.deployIsm({ + return await deployer.deployIsm({ chain, ismConfig, mailbox, From 9b5fc4f74764544c15dbcb1ae8c2b34031a40664 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:48:51 +0330 Subject: [PATCH 078/132] temp: make starknet send message work --- typescript/cli/src/deploy/utils.ts | 14 +-- typescript/cli/src/send/message.ts | 37 +++++++- typescript/sdk/src/core/StarknetCore.ts | 116 ++++++++++++++++++++++++ typescript/sdk/src/index.ts | 1 + 4 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 typescript/sdk/src/core/StarknetCore.ts diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index f5ac01a1752..ab579b6b48b 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -9,7 +9,9 @@ import { MultisigConfig, getLocalProvider, } from '@hyperlane-xyz/sdk'; -import { Address, ProtocolType } from '@hyperlane-xyz/utils'; +import { + Address, // ProtocolType +} from '@hyperlane-xyz/utils'; import { parseIsmConfig } from '../config/ism.js'; import { CommandContext, WriteCommandContext } from '../context/types.js'; @@ -23,8 +25,8 @@ import { } from '../logger.js'; import { nativeBalancesAreSufficient } from '../utils/balances.js'; import { ENV } from '../utils/env.js'; -import { assertSigner } from '../utils/keys.js'; +// import { assertSigner } from '../utils/keys.js'; import { completeDryRun } from './dry-run.js'; export async function runPreflightChecksForChains({ @@ -47,10 +49,10 @@ export async function runPreflightChecksForChains({ for (const chain of chains) { const metadata = multiProvider.tryGetChainMetadata(chain); if (!metadata) throw new Error(`No chain config found for ${chain}`); - if (metadata.protocol !== ProtocolType.Ethereum) - throw new Error('Only Ethereum chains are supported for now'); - const signer = multiProvider.getSigner(chain); - assertSigner(signer); + // if (metadata.protocol !== ProtocolType.Ethereum) + // throw new Error('Only Ethereum chains are supported for now'); + // const signer = multiProvider.getSigner(chain); + // assertSigner(signer); logGreen(`✅ ${chain} signer is valid`); } logGreen('✅ Chains are valid'); diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index 430d3b7bcfc..16306304fc4 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -1,6 +1,12 @@ +import { Account, Provider } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; -import { ChainName, HyperlaneCore, HyperlaneRelayer } from '@hyperlane-xyz/sdk'; +import { + ChainName, + HyperlaneCore, + HyperlaneRelayer, + StarknetCore, +} from '@hyperlane-xyz/sdk'; import { addressToBytes32, timeout } from '@hyperlane-xyz/utils'; import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; @@ -82,16 +88,36 @@ async function executeDelivery({ }) { const { registry, multiProvider } = context; const chainAddresses = await registry.getAddresses(); - const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); - + const provider = new Provider({ + nodeUrl: 'http://127.0.0.1:5050', + }); + const account = new Account( + provider, + '0x6acf82752859a6bced2eb2e9e4346062763088e72422d6f7c2ee8a7526e07d7', + '0x000000000000000000000000000000004e4993ca00259617c8075a3f76d43abc', + ); try { - const recipient = chainAddresses[destination].testRecipient; + const recipient = + chainAddresses[destination].testRecipient || + '0x00581bb8ad9e4ecd0ba06793e2ffb26f4b12ea18ec69dfb216738efe569e2e59'; if (!recipient) { throw new Error(`Unable to find TestRecipient for ${destination}`); } const formattedRecipient = addressToBytes32(recipient); - log('Dispatching message'); + // log('Dispatching message'); + const destinationDomain = multiProvider.getDomainId(destination); + + const starknet = new StarknetCore(account); + const tx = await starknet.sendMessage({ + destinationDomain, + messageBody, + recipientAddress: chainAddresses[destination].testRecipient, + }); + console.log({ tx }); + return; + const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); + const { dispatchTx, message } = await core.sendMessage( origin, destination, @@ -121,6 +147,7 @@ async function executeDelivery({ log('Waiting for message delivery on destination chain...'); // Max wait 10 minutes + return; await core.waitForMessageProcessed(dispatchTx, 10000, 60); logGreen('Message was delivered!'); } diff --git a/typescript/sdk/src/core/StarknetCore.ts b/typescript/sdk/src/core/StarknetCore.ts new file mode 100644 index 00000000000..59ad0400f7f --- /dev/null +++ b/typescript/sdk/src/core/StarknetCore.ts @@ -0,0 +1,116 @@ +import { Account, CairoOption, CairoOptionVariant, Contract } from 'starknet'; + +import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; +import { rootLogger } from '@hyperlane-xyz/utils'; + +export class StarknetCore { + protected logger = rootLogger.child({ module: 'StarknetCore' }); + protected signer: Account; + + constructor(signer: Account) { + this.signer = signer; + } + + /** + * Convert a byte array to a starknet message + * Pads the bytes to 16 bytes chunks + * @param bytes Input byte array + * @returns Object containing size and padded data array + */ + static toStarknetMessageBytes(bytes: Uint8Array): { + size: number; + data: bigint[]; + } { + // Calculate the required padding + const padding = (16 - (bytes.length % 16)) % 16; + const totalLen = bytes.length + padding; + + // Create a new byte array with the necessary padding + const paddedBytes = new Uint8Array(totalLen); + paddedBytes.set(bytes); + // Padding remains as zeros by default in Uint8Array + + // Convert to chunks of 16 bytes + const result: bigint[] = []; + for (let i = 0; i < totalLen; i += 16) { + const chunk = paddedBytes.slice(i, i + 16); + // Convert chunk to bigint (equivalent to u128 in Rust) + const value = BigInt('0x' + Buffer.from(chunk).toString('hex')); + result.push(value); + } + + return { + size: bytes.length, + data: result, + }; + } + + async sendMessage(params: { + destinationDomain: number; + recipientAddress: string; + messageBody: string; + }): Promise<{ txHash: string }> { + const { abi } = getCompiledContract('mailbox'); + const mailboxContract = new Contract( + abi, + '0x00581bb8ad9e4ecd0ba06793e2ffb26f4b12ea18ec69dfb216738efe569e2e59', + this.signer, + ); + // Convert messageBody to Bytes struct format + const messageBodyBytes = StarknetCore.toStarknetMessageBytes( + new TextEncoder().encode(params.messageBody), + ); + console.log({ + messageBodyBytes, + encoded: new TextEncoder().encode(params.messageBody), + }); + + const nonOption = new CairoOption(CairoOptionVariant.None); + + // Quote the dispatch first to ensure enough fees are provided + const quote = await mailboxContract.call('quote_dispatch', [ + params.destinationDomain, + params.recipientAddress, + messageBodyBytes, + nonOption, + nonOption, + ]); + + // Dispatch the message + const { transaction_hash } = await mailboxContract.invoke('dispatch', [ + params.destinationDomain, + params.recipientAddress, + messageBodyBytes, + BigInt(quote.toString()), //fee amount + nonOption, + nonOption, + ]); + + this.logger.info(`Message sent with transaction hash: ${transaction_hash}`); + + return { + txHash: transaction_hash, + }; + } + + async quoteDispatch(params: { + destinationDomain: number; + recipientAddress: string; + messageBody: string; + customHookMetadata?: string; + customHook?: string; + }): Promise { + const { abi } = getCompiledContract('mailbox'); + const mailboxContract = new Contract(abi, 'mailbox_address', this.signer); + + const quote = await mailboxContract.call('quote_dispatch', [ + params.destinationDomain, + params.recipientAddress, + params.messageBody, + params.customHookMetadata || '', + params.customHook || '', + ]); + + return quote.toString(); + } +} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 617717b39d3..84c67d1a23e 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -63,6 +63,7 @@ export { export { HyperlaneLifecyleEvent } from './core/events.js'; export { EvmCoreReader } from './core/EvmCoreReader.js'; export { HyperlaneCore } from './core/HyperlaneCore.js'; +export { StarknetCore } from './core/StarknetCore.js'; export { HyperlaneCoreChecker } from './core/HyperlaneCoreChecker.js'; export { HyperlaneCoreDeployer } from './core/HyperlaneCoreDeployer.js'; export { From 38310f82917abbeaca1d6db7f6a5f1b75cc29d5f Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:47:08 +0330 Subject: [PATCH 079/132] refactor: standardize signer address handling on context --- typescript/cli/src/config/core.ts | 4 ++-- typescript/cli/src/config/hooks.ts | 6 +++--- typescript/cli/src/config/ism.ts | 6 +++--- typescript/cli/src/config/warp.ts | 4 ++-- typescript/cli/src/context/context.ts | 13 +++++++++++-- .../context/strategies/chain/MultiChainResolver.ts | 4 ++-- typescript/cli/src/context/types.ts | 3 ++- 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/typescript/cli/src/config/core.ts b/typescript/cli/src/config/core.ts index 92e6fc68827..7088ca22462 100644 --- a/typescript/cli/src/config/core.ts +++ b/typescript/cli/src/config/core.ts @@ -38,7 +38,7 @@ export async function createCoreDeployConfig({ logBlue('Creating a new core deployment config...'); const owner = await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, ENTER_DESIRED_VALUE_MSG, 'owner address', SIGNER_PROMPT_LABEL, @@ -63,7 +63,7 @@ export async function createCoreDeployConfig({ }); proxyAdmin = { owner: await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, ENTER_DESIRED_VALUE_MSG, 'ProxyAdmin owner address', SIGNER_PROMPT_LABEL, diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index e8df64dc00f..5ad005dc241 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -243,10 +243,10 @@ async function getOwnerAndBeneficiary( advanced: boolean, ) { const unnormalizedOwner = - !advanced && context.signer - ? await context.signer.getAddress() + !advanced && context.signerAddress + ? context.signerAddress : await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, `For ${module}, enter`, 'owner address', 'signer', diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts index f7f6bab9ade..81440e96eb0 100644 --- a/typescript/cli/src/config/ism.ts +++ b/typescript/cli/src/config/ism.ts @@ -163,10 +163,10 @@ export const createTrustedRelayerConfig = callWithConfigCreationLogs( advanced: boolean = false, ): Promise => { const relayer = - !advanced && context.signer - ? await context.signer.getAddress() + !advanced && context.signerAddress + ? context.signerAddress : await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, 'For trusted relayer ISM, enter', 'relayer address', 'signer', diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 1c315ee6255..cce88ae9894 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -83,7 +83,7 @@ async function fillDefaults( let owner = config.owner; if (!owner) { owner = - (await context.signer?.getAddress()) ?? + context.signerAddress ?? (await context.multiProvider.getSignerAddress(chain)); } return { @@ -139,7 +139,7 @@ export async function createWarpRouteDeployConfig({ for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); const owner = await detectAndConfirmOrPrompt( - async () => (await multiProtocolSigner?.initSigner(chain))?.getAddress(), + async () => context.signerAddress, 'Enter the desired', 'owner address', 'signer', diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 0d1084fcc6d..0e0f3b2034c 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -24,7 +24,7 @@ import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; -import { getImpersonatedSigner } from '../utils/keys.js'; +import { getImpersonatedSigner, getSigner } from '../utils/keys.js'; import { ChainResolverFactory } from './strategies/chain/ChainResolverFactory.js'; import { MultiProtocolSignerManager } from './strategies/signer/MultiProtocolSignerManager.js'; @@ -60,7 +60,7 @@ export async function contextMiddleware(argv: Record) { export async function signerMiddleware(argv: Record) { const { key, requiresKey, multiProvider, strategyPath } = argv.context; - if (!requiresKey && !key) return argv; + if (!requiresKey) return argv; const strategyConfig = await safeReadChainSubmissionStrategyConfig( strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, @@ -109,6 +109,14 @@ export async function getContext({ }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy); + //Just for backward compability + let signerAddress: string | undefined = undefined; + if (key) { + let signer; + ({ key, signer } = await getSigner({ key, skipConfirmation })); + signerAddress = await signer.getAddress(); + } + const multiProvider = await getMultiProvider(registry); return { @@ -118,6 +126,7 @@ export async function getContext({ multiProvider, key, skipConfirmation: !!skipConfirmation, + signerAddress, } as CommandContext; } diff --git a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts index c6d96b5dc52..b61b27a5d89 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -3,7 +3,7 @@ import { assert } from '@hyperlane-xyz/utils'; import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; import { readChainSubmissionStrategyConfig } from '../../../config/strategy.js'; -import { logRed } from '../../../logger.js'; +import { log } from '../../../logger.js'; import { extractChainsFromObj, runMultiChainSelectionStep, @@ -156,7 +156,7 @@ export class MultiChainResolver implements ChainResolver { 'warp', ); } else { - logRed(`Using warp route deployment config at ${configPath}`); + log(`Using warp route deployment config at ${configPath}`); } // Alternative to readWarpRouteDeployConfig that doesn't use context for signer and zod validation diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 7837972893d..557fe750cea 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -26,7 +26,8 @@ export interface CommandContext { multiProvider: MultiProvider; skipConfirmation: boolean; key?: string; - signer?: ethers.Signer; + // just for evm chains backward compability + signerAddress?: string; warpCoreConfig?: WarpCoreConfig; strategyPath?: string; } From 6e437c7fb9344dfd431b8a58ff0eee9961926158 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:20:56 +0330 Subject: [PATCH 080/132] feat(cli): Add 'check' command to SIGN_COMMANDS and use signer for context multiprovider --- typescript/cli/src/commands/signCommands.ts | 1 + typescript/cli/src/context/context.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 6bd617302b7..235f9c00b0f 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -8,6 +8,7 @@ export const SIGN_COMMANDS = [ 'status', 'submit', 'relayer', + 'check', ]; export function isSignCommand(argv: any): boolean { diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 0e0f3b2034c..77e20b1afbe 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -1,5 +1,5 @@ import { confirm } from '@inquirer/prompts'; -import { ethers } from 'ethers'; +import { Signer, ethers } from 'ethers'; import { DEFAULT_GITHUB_REGISTRY, @@ -111,13 +111,13 @@ export async function getContext({ //Just for backward compability let signerAddress: string | undefined = undefined; + let signer: Signer | undefined; if (key) { - let signer; ({ key, signer } = await getSigner({ key, skipConfirmation })); signerAddress = await signer.getAddress(); } - const multiProvider = await getMultiProvider(registry); + const multiProvider = await getMultiProvider(registry, signer); return { registry, From 4820623f230b509d06dadadf063ca367d4b36b1e Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:25:48 +0330 Subject: [PATCH 081/132] fix: handle warp check as a sign command on signer strategies --- typescript/cli/src/commands/signCommands.ts | 1 + typescript/cli/src/context/context.ts | 4 ++-- .../cli/src/context/strategies/chain/ChainResolverFactory.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 235f9c00b0f..0316211842d 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -23,6 +23,7 @@ export enum CommandType { WARP_SEND = 'warp:send', WARP_APPLY = 'warp:apply', WARP_READ = 'warp:read', + WARP_CHECK = 'warp:check', SEND_MESSAGE = 'send:message', AGENT_KURTOSIS = 'deploy:kurtosis-agents', STATUS = 'status:', diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 77e20b1afbe..a9f3e47bf50 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -111,13 +111,13 @@ export async function getContext({ //Just for backward compability let signerAddress: string | undefined = undefined; - let signer: Signer | undefined; if (key) { + let signer: Signer | undefined; ({ key, signer } = await getSigner({ key, skipConfirmation })); signerAddress = await signer.getAddress(); } - const multiProvider = await getMultiProvider(registry, signer); + const multiProvider = await getMultiProvider(registry); return { registry, diff --git a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts index eb9fa135aa2..fa62038a613 100644 --- a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts +++ b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts @@ -14,6 +14,7 @@ export class ChainResolverFactory { [CommandType.WARP_SEND, () => MultiChainResolver.forOriginDestination()], [CommandType.WARP_APPLY, () => MultiChainResolver.forWarpRouteConfig()], [CommandType.WARP_READ, () => MultiChainResolver.forWarpCoreConfig()], + [CommandType.WARP_CHECK, () => MultiChainResolver.forWarpCoreConfig()], [CommandType.SEND_MESSAGE, () => MultiChainResolver.forOriginDestination()], [CommandType.AGENT_KURTOSIS, () => MultiChainResolver.forAgentKurtosis()], [CommandType.STATUS, () => MultiChainResolver.forOriginDestination()], From 3146e9314b9c41d35b6c77cbcb993c0e3da94d29 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:02:07 +0330 Subject: [PATCH 082/132] fix: handle warp read and warp check as sign command temporary --- typescript/cli/src/commands/signCommands.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 0316211842d..d9fa5391d11 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -8,13 +8,16 @@ export const SIGN_COMMANDS = [ 'status', 'submit', 'relayer', - 'check', ]; export function isSignCommand(argv: any): boolean { + //TODO: fix reading and checking warp without signer, and remove this + const temporarySignCommandsCheck = + argv._[0] === 'warp' && (argv._[1] === 'read' || argv._[1] === 'check'); return ( SIGN_COMMANDS.includes(argv._[0]) || - (argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1])) + (argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1])) || + temporarySignCommandsCheck ); } From fa2b0e8517ad2ff586e0cf989c512fcf0dba9a79 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:45:26 +0330 Subject: [PATCH 083/132] chore: fix typo --- typescript/cli/src/context/context.ts | 2 +- typescript/cli/src/context/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index a9f3e47bf50..7c1fcb747d1 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -109,7 +109,7 @@ export async function getContext({ }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy); - //Just for backward compability + //Just for backward compatibility let signerAddress: string | undefined = undefined; if (key) { let signer: Signer | undefined; diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 557fe750cea..c320ff3cac2 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -26,7 +26,7 @@ export interface CommandContext { multiProvider: MultiProvider; skipConfirmation: boolean; key?: string; - // just for evm chains backward compability + // just for evm chains backward compatibility signerAddress?: string; warpCoreConfig?: WarpCoreConfig; strategyPath?: string; From 4e3b65cb36f6c141d06d5ebc10d65aaad241dd73 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:36:48 +0330 Subject: [PATCH 084/132] chore: modify type of signer on context --- typescript/cli/src/context/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 7c1fcb747d1..570b233cde2 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -112,7 +112,7 @@ export async function getContext({ //Just for backward compatibility let signerAddress: string | undefined = undefined; if (key) { - let signer: Signer | undefined; + let signer: Signer; ({ key, signer } = await getSigner({ key, skipConfirmation })); signerAddress = await signer.getAddress(); } From 154c993c2bba55c9df79dd390993414425e3ccf3 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:01:15 +0330 Subject: [PATCH 085/132] fix: starknet contract files build --- starknet/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/starknet/package.json b/starknet/package.json index 5bf833fdb2e..4f8e7484f6b 100644 --- a/starknet/package.json +++ b/starknet/package.json @@ -7,8 +7,8 @@ "author": "", "license": "Apache-2.0", "scripts": { - "compile": "cd cairo && scarb build && cp -R ./target ../src/target && cd ..", - "build": "yarn compile && tsc && cp -R ./src/target ./dist/target" + "compile": "cd cairo && scarb build && cp -R ./target ../src && cd ..", + "build": "yarn compile && tsc && cp -R ./src/target ./dist" }, "devDependencies": { "@types/node": "^22.7.9", From c7718e7dcacee783195e2a631e55f5a78b50f49d Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:51:34 +0330 Subject: [PATCH 086/132] feat: add starknet contracts build files as a new workspace --- package.json | 3 +- starknet/.gitignore | 2 + starknet/fetch-contracts-release.sh | 149 ++++++++++++++++++++++++++++ starknet/package.json | 31 ++++++ starknet/src/config.ts | 32 ++++++ starknet/src/errors.ts | 18 ++++ starknet/src/index.ts | 106 ++++++++++++++++++++ starknet/src/types.ts | 76 ++++++++++++++ starknet/src/utils.ts | 99 ++++++++++++++++++ starknet/tsconfig.json | 15 +++ yarn.lock | 55 +++++++++- 11 files changed, 584 insertions(+), 2 deletions(-) create mode 100644 starknet/.gitignore create mode 100755 starknet/fetch-contracts-release.sh create mode 100644 starknet/package.json create mode 100644 starknet/src/config.ts create mode 100644 starknet/src/errors.ts create mode 100644 starknet/src/index.ts create mode 100644 starknet/src/types.ts create mode 100644 starknet/src/utils.ts create mode 100644 starknet/tsconfig.json diff --git a/package.json b/package.json index 2a2400e9654..0aab8255923 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ }, "workspaces": [ "solidity", - "typescript/*" + "typescript/*", + "starknet" ], "resolutions": { "async": "^2.6.4", diff --git a/starknet/.gitignore b/starknet/.gitignore new file mode 100644 index 00000000000..8c5bcf6780f --- /dev/null +++ b/starknet/.gitignore @@ -0,0 +1,2 @@ +.env +dist \ No newline at end of file diff --git a/starknet/fetch-contracts-release.sh b/starknet/fetch-contracts-release.sh new file mode 100755 index 00000000000..392b77293c9 --- /dev/null +++ b/starknet/fetch-contracts-release.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash + +# Strict mode configuration +set -euo pipefail +IFS=$'\n\t' + +# Constants +readonly REPO="astraly-labs/hyperlane_starknet" +readonly GITHUB_RELEASES_API="https://api.github.com/repos/${REPO}/releases" +readonly TARGET_DIR="./dist/target" + +# Color definitions +declare -r COLOR_GREEN='\033[0;32m' +declare -r COLOR_RED='\033[0;31m' +declare -r COLOR_RESET='\033[0m' + +log_error() { + echo -e "${COLOR_RED}Error: $1${COLOR_RESET}" >&2 +} + +log_success() { + echo -e "${COLOR_GREEN}$1${COLOR_RESET}" +} + +check_dependencies() { + local -r required_tools=("curl" "jq" "unzip") + + for tool in "${required_tools[@]}"; do + if ! command -v "$tool" &> /dev/null; then + log_error "$tool is not installed" + exit 1 + fi + done +} + +get_package_version() { + local package_version + if ! package_version=$(jq -r '.version' package.json 2>/dev/null); then + log_error "Failed to read version from package.json" + exit 1 + fi + echo "v${package_version}" +} + +verify_version_exists() { + local version=$1 + if ! curl --output /dev/null --silent --head --fail "${GITHUB_RELEASES_API}/tags/${version}"; then + log_error "Version ${version} does not exist" + exit 1 + fi +} + +get_release_info() { + local version=$1 + local release_info + + release_info=$(curl -sf "${GITHUB_RELEASES_API}/tags/${version}") || { + log_error "Failed to fetch release information for version ${version}" + exit 1 + } + echo "$release_info" +} + +download_and_extract() { + local version=$1 + local download_url=$2 + local base_url="${download_url%/*}" + local filename="${download_url##*/}" + + if ! mkdir -p "$TARGET_DIR"; then + log_error "Failed to create target directory" + exit 1 + fi + + log_success "Downloading version ${version}..." + + if ! curl -L "$download_url" -o "${TARGET_DIR}/release.zip"; then + log_error "Download failed" + exit 1 + fi + + if ! verify_checksum "${TARGET_DIR}/release.zip" "$base_url" "$filename"; then + rm -f "${TARGET_DIR}/release.zip" + exit 1 + fi + + if ! unzip -o "${TARGET_DIR}/release.zip" -d "${TARGET_DIR}"; then + log_error "Extraction failed" + exit 1 + fi +} + +verify_checksum() { + local file_path="$1" + local base_url="$2" + local filename="$3" + local checksum_filename + checksum_filename="${filename%.zip}.CHECKSUM" + + local downloaded_checksum + downloaded_checksum="$(sha256sum "$file_path" | cut -d' ' -f1)" + log_success "File checksum: ${downloaded_checksum}" + + local expected_checksum + if ! expected_checksum="$(curl -sL "${base_url}/${checksum_filename}")"; then + log_error "Failed to fetch checksum file" + return 1 + fi + + if [[ "${downloaded_checksum}" != "$(echo "${expected_checksum}" | awk '{print $1}')" ]]; then + log_error "Checksum verification failed" + return 1 + fi + + return 0 +} + +cleanup() { + rm -f "$TARGET_DIR/release.zip" +} + +main() { + trap cleanup EXIT + + check_dependencies + + local version + version=$(get_package_version) + log_success "Using version ${version} from package.json" + verify_version_exists "$version" + + local release_info + release_info=$(get_release_info "$version") + + local download_url + download_url=$(echo "$release_info" | jq -r '.assets[] | select(.name | startswith("hyperlane-starknet") and endswith(".zip")) | .browser_download_url') + + if [[ -z "$download_url" ]]; then + log_error "Could not find ZIP download URL for release" + exit 1 + fi + + # Process download and file checksum verification and extraction + download_and_extract "$version" "$download_url" + + log_success "Successfully downloaded and extracted version ${version}" +} + +main \ No newline at end of file diff --git a/starknet/package.json b/starknet/package.json new file mode 100644 index 00000000000..eeaabca12db --- /dev/null +++ b/starknet/package.json @@ -0,0 +1,31 @@ +{ + "name": "@hyperlane-xyz/starknet-core", + "description": "Core cairo contracts for Hyperlane", + "version": "0.2.1", + "type": "module", + "homepage": "https://www.hyperlane.xyz", + "author": "", + "license": "Apache-2.0", + "scripts": { + "build": "tsc && ./fetch-contracts-release.sh", + "clean": "rm -rf ./dist" + }, + "exports": { + ".": "./dist/index.js" + }, + "types": "./dist/index.d.ts", + "keywords": [ + "Hyperlane", + "Cairo", + "Starknet" + ], + "engines": { + "node": ">=16" + }, + "dependencies": { + "starknet": "6.11.0" + }, + "devDependencies": { + "typescript": "5.3.3" + } +} diff --git a/starknet/src/config.ts b/starknet/src/config.ts new file mode 100644 index 00000000000..1051a10379c --- /dev/null +++ b/starknet/src/config.ts @@ -0,0 +1,32 @@ +export const CONFIG = { + PATHS: { + MAIN: 'target/dev', + }, + SUFFIXES: { + STANDARD: '.contract_class.json', + COMPILED: '.compiled_contract_class.json', + }, + ERROR_CODES: { + FILE_NOT_FOUND: 'FILE_NOT_FOUND', + PARSE_ERROR: 'PARSE_ERROR', + INVALID_INPUT: 'INVALID_INPUT', + }, + CONTRACT_NAME_VALIDATION: { + MAX_LENGTH: 128, + MIN_LENGTH: 1, + FORBIDDEN_CHARS: [ + '..', + '/', + '\\', + ' ', + '*', + '?', + '<', + '>', + '|', + '"', + ':', + ] as const, + PATTERN: /^[a-zA-Z0-9_-]+$/, + }, +} as const; diff --git a/starknet/src/errors.ts b/starknet/src/errors.ts new file mode 100644 index 00000000000..2d8c5fb1c99 --- /dev/null +++ b/starknet/src/errors.ts @@ -0,0 +1,18 @@ +import { CONFIG } from './config.js'; + +export class ContractError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly details?: unknown, + ) { + super(`[${code}] ${message}`); + this.name = 'ContractError'; + } +} + +export const ErrorMessages = { + [CONFIG.ERROR_CODES.FILE_NOT_FOUND]: 'Contract file not found', + [CONFIG.ERROR_CODES.PARSE_ERROR]: 'Failed to parse contract', + [CONFIG.ERROR_CODES.INVALID_INPUT]: 'Invalid input parameters', +} as const; diff --git a/starknet/src/index.ts b/starknet/src/index.ts new file mode 100644 index 00000000000..e29dbe78136 --- /dev/null +++ b/starknet/src/index.ts @@ -0,0 +1,106 @@ +import { existsSync, readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { CairoAssembly, CompiledContract } from 'starknet'; +import { fileURLToPath } from 'url'; + +import { CONFIG } from './config.js'; +import { ContractError, ErrorMessages } from './errors.js'; +import { assertValidContractName } from './utils.js'; + +const currentDirectory = dirname(fileURLToPath(import.meta.url)); +const TARGET_DEV_PATH = join(currentDirectory, CONFIG.PATHS.MAIN); + +/** + * @notice Retrieves and parses the standard compiled contract data + * @dev Reads the contract file with STANDARD suffix and parses it as JSON + * @param name The name of the contract to retrieve + * @returns {CompiledContract} The parsed contract data + * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid + */ +export const getCompiledContract = (name: string): CompiledContract => { + try { + return JSON.parse( + readFileSync(findContractFile(name, 'STANDARD'), 'utf-8'), + ); + } catch (error: unknown) { + if (error instanceof ContractError) throw error; + throw new ContractError( + ErrorMessages[CONFIG.ERROR_CODES.PARSE_ERROR], + CONFIG.ERROR_CODES.PARSE_ERROR, + { + name, + error: (error as Error).message, + }, + ); + } +}; + +/** + * @notice Retrieves and parses the CASM compiled contract data + * @dev Reads the contract file with COMPILED suffix and parses it as JSON + * @param name The name of the contract to retrieve + * @returns {CairoAssembly} The parsed CASM contract data + * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid + */ +export const getCompiledContractCasm = (name: string): CairoAssembly => { + try { + return JSON.parse( + readFileSync(findContractFile(name, 'COMPILED'), 'utf-8'), + ); + } catch (error: unknown) { + if (error instanceof ContractError) throw error; + throw new ContractError( + ErrorMessages[CONFIG.ERROR_CODES.PARSE_ERROR], + CONFIG.ERROR_CODES.PARSE_ERROR, + { + name, + error: (error as Error).message, + }, + ); + } +}; + +/** + * @notice Finds the path to a contract file based on predefined patterns + * @dev Searches for contract files in multiple predefined locations: + * - contracts_{name}{suffix} + * - token_{name}{suffix} + * @param name The base name of the contract to find + * @param suffix The type of contract file to look for (from CONFIG.SUFFIXES) + * @returns {string} The full path to the first matching contract file + * @throws {ContractError} If no matching file is found or the contract name is invalid + */ +function findContractFile( + name: string, + suffix: keyof typeof CONFIG.SUFFIXES, +): string { + assertValidContractName(name); + + const suffixPath = CONFIG.SUFFIXES[suffix]; + const possiblePaths = [ + { + type: 'contracts', + path: `${TARGET_DEV_PATH}/contracts_${name}${suffixPath}`, + }, + { + type: 'token', + path: `${TARGET_DEV_PATH}/token_${name}${suffixPath}`, + }, + ]; + + const existingPath = possiblePaths.find(({ path }) => existsSync(path)); + + if (!existingPath) { + throw new ContractError( + ErrorMessages[CONFIG.ERROR_CODES.FILE_NOT_FOUND], + CONFIG.ERROR_CODES.FILE_NOT_FOUND, + { + name, + suffix, + path: possiblePaths[0].path, + }, + ); + } + + return existingPath.path; +} diff --git a/starknet/src/types.ts b/starknet/src/types.ts new file mode 100644 index 00000000000..f3f23316b84 --- /dev/null +++ b/starknet/src/types.ts @@ -0,0 +1,76 @@ +export interface SierraProgram { + sierra_program: string[]; + sierra_program_debug_info?: { + type_information?: unknown; + libfunc_declarations?: unknown; + user_func_declarations?: unknown; + }; + contract_class_version: string; + entry_points_by_type: { + CONSTRUCTOR: EntryPoint[]; + EXTERNAL: EntryPoint[]; + L1_HANDLER: EntryPoint[]; + }; + abi: ContractAbi; +} + +export interface EntryPoint { + selector: string; + function_idx: number; +} + +export interface ContractAbi { + type: string; + name?: string; + inputs?: AbiInput[]; + outputs?: AbiOutput[]; + state_mutability?: string; + functions: AbiFunction[]; + events: AbiEvent[]; + structs: AbiStruct[]; + l1_handler?: boolean; +} + +export interface AbiFunction { + name: string; + inputs: AbiInput[]; + outputs: AbiOutput[]; + state_mutability: string; +} + +export interface AbiEvent { + name: string; + inputs: AbiInput[]; +} + +export interface AbiInput { + name: string; + type: string; +} + +export interface AbiOutput { + type: string; +} + +export interface AbiStruct { + name: string; + size: number; + members: AbiStructMember[]; +} + +export interface AbiStructMember { + name: string; + type: string; + offset: number; +} + +// Update the main ContractData interface +export interface ContractData extends SierraProgram { + [key: string]: unknown; // Keep this for backward compatibility +} + +export interface CompiledContractCasm { + prime: string; // e.g. "0x800000000000011000000000000000000000000000000000000000000000001" + compiler_version: string; // e.g. "2.6.4" + bytecode: string[]; // Array of hex strings representing bytecode instructions +} diff --git a/starknet/src/utils.ts b/starknet/src/utils.ts new file mode 100644 index 00000000000..290c5b57dfe --- /dev/null +++ b/starknet/src/utils.ts @@ -0,0 +1,99 @@ +import { CONFIG } from './config.js'; +import { ContractError, ErrorMessages } from './errors.js'; + +/** + * @notice Represents different types of contract name validation errors + */ +export type ValidationError = + | { type: 'empty' } + | { type: 'tooLong'; maxLength: number } + | { type: 'invalidChars'; chars: string[] } + | { type: 'invalidPattern' }; + +/** + * @notice Validates a contract name and returns any validation errors + * @dev Checks for empty strings, length limits, forbidden characters, and pattern matching + * @param name The contract name to validate + * @returns An array of validation errors, empty if valid + */ +export function validateContractName(name: string): ValidationError[] { + const errors: ValidationError[] = []; + + // Check for empty or whitespace-only names + if (!name.trim()) { + errors.push({ type: 'empty' }); + return errors; // Return early as other checks don't matter + } + + // Check length + if (name.length > CONFIG.CONTRACT_NAME_VALIDATION.MAX_LENGTH) { + errors.push({ + type: 'tooLong', + maxLength: CONFIG.CONTRACT_NAME_VALIDATION.MAX_LENGTH, + }); + } + + // Check for forbidden characters + const foundForbiddenChars = + CONFIG.CONTRACT_NAME_VALIDATION.FORBIDDEN_CHARS.filter((char) => + name.includes(char), + ); + + if (foundForbiddenChars.length > 0) { + errors.push({ type: 'invalidChars', chars: foundForbiddenChars }); + } + + // Check pattern match + if (!CONFIG.CONTRACT_NAME_VALIDATION.PATTERN.test(name)) { + errors.push({ type: 'invalidPattern' }); + } + + return errors; +} + +/** + * @notice Gets a human-readable error message for validation errors + * @param errors Array of validation errors + * @returns A formatted error message + */ +export function getValidationErrorMessage(errors: ValidationError[]): string { + if (errors.length === 0) return ''; + + const messages = errors.map((error) => { + switch (error.type) { + case 'empty': + return 'Contract name cannot be empty or only whitespace'; + case 'tooLong': + return `Contract name cannot exceed ${error.maxLength} characters`; + case 'invalidChars': + return `Contract name contains invalid characters: ${error.chars.join( + ' ', + )}`; + case 'invalidPattern': + return 'Contract name can only contain letters, numbers, underscores, and hyphens'; + } + }); + + return messages.join('. '); +} + +/** + * @notice Validates a contract name and throws if invalid + * @dev Combines validation and error throwing into a single function + * @param name The contract name to validate + * @throws {ContractError} If the name is invalid + */ +export function assertValidContractName(name: string): void { + const errors = validateContractName(name); + + if (errors.length > 0) { + throw new ContractError( + ErrorMessages[CONFIG.ERROR_CODES.INVALID_INPUT], + CONFIG.ERROR_CODES.INVALID_INPUT, + { + name, + reason: getValidationErrorMessage(errors), + }, + ); + } +} diff --git a/starknet/tsconfig.json b/starknet/tsconfig.json new file mode 100644 index 00000000000..277f04966b2 --- /dev/null +++ b/starknet/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "importHelpers": true, + "noEmitHelpers": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "ts-node": { + "experimentalSpecifierResolution": "node", + "experimentalResolver": true, + "files": true + } +} diff --git a/yarn.lock b/yarn.lock index 1a5d31057e2..4c4da7f8bd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8706,6 +8706,15 @@ __metadata: languageName: unknown linkType: soft +"@hyperlane-xyz/starknet-core@workspace:starknet": + version: 0.0.0-use.local + resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" + dependencies: + starknet: "npm:6.11.0" + typescript: "npm:5.3.3" + languageName: unknown + linkType: soft + "@hyperlane-xyz/utils@npm:7.1.0, @hyperlane-xyz/utils@workspace:typescript/utils": version: 0.0.0-use.local resolution: "@hyperlane-xyz/utils@workspace:typescript/utils" @@ -15360,6 +15369,13 @@ __metadata: languageName: node linkType: hard +"@starknet-io/types-js@npm:^0.7.7": + version: 0.7.10 + resolution: "@starknet-io/types-js@npm:0.7.10" + checksum: 10/e7e10878d6d576dcd30c6910a819e8e9cbd72102c71a93be7e0282f229d75c5765d2b5d152f7ae0693987cdb00e3d04f02bad371d91f420ddbbed435aadfbe3e + languageName: node + linkType: hard + "@storybook/addon-actions@npm:7.6.20": version: 7.6.20 resolution: "@storybook/addon-actions@npm:7.6.20" @@ -18366,7 +18382,7 @@ __metadata: languageName: node linkType: hard -"abi-wan-kanabi@npm:^2.2.3": +"abi-wan-kanabi@npm:^2.2.2, abi-wan-kanabi@npm:^2.2.3": version: 2.2.3 resolution: "abi-wan-kanabi@npm:2.2.3" dependencies: @@ -25291,6 +25307,15 @@ __metadata: languageName: node linkType: hard +"get-starknet-core@npm:^4.0.0-next.3": + version: 4.0.0 + resolution: "get-starknet-core@npm:4.0.0" + dependencies: + "@starknet-io/types-js": "npm:^0.7.7" + checksum: 10/e1ae3a3b866dc86d0e8d8b59517ffcdc9b7b47105e5738068d36c957c7cf03e4d6a7c1d36eff355fb65114430b10e9defb909aba94a0df654a115961f90bbed9 + languageName: node + linkType: hard + "get-stream@npm:^3.0.0": version: 3.0.0 resolution: "get-stream@npm:3.0.0" @@ -35120,6 +35145,27 @@ __metadata: languageName: node linkType: hard +"starknet@npm:6.11.0": + version: 6.11.0 + resolution: "starknet@npm:6.11.0" + dependencies: + "@noble/curves": "npm:~1.4.0" + "@noble/hashes": "npm:^1.4.0" + "@scure/base": "npm:~1.1.3" + "@scure/starknet": "npm:~1.0.0" + abi-wan-kanabi: "npm:^2.2.2" + fetch-cookie: "npm:^3.0.0" + get-starknet-core: "npm:^4.0.0-next.3" + isomorphic-fetch: "npm:^3.0.0" + lossless-json: "npm:^4.0.1" + pako: "npm:^2.0.4" + starknet-types-07: "npm:@starknet-io/types-js@^0.7.7" + ts-mixer: "npm:^6.0.3" + url-join: "npm:^4.0.1" + checksum: 10/f2acd29277a908dbbb54dc6ebe533443d2449fc67d280ac0cec6d98ec06bbd83af497d438bb468eb3c6e7aec182c06ba134574183398361c63872ea67dbb2025 + languageName: node + linkType: hard + "starknet@npm:6.17.0": version: 6.17.0 resolution: "starknet@npm:6.17.0" @@ -37253,6 +37299,13 @@ __metadata: languageName: node linkType: hard +"url-join@npm:^4.0.1": + version: 4.0.1 + resolution: "url-join@npm:4.0.1" + checksum: 10/b53b256a9a36ed6b0f6768101e78ca97f32d7b935283fd29ce19d0bbfb6f88aa80aa6c03fd87f2f8978ab463a6539f597a63051e7086f3379685319a7495f709 + languageName: node + linkType: hard + "url-parse-lax@npm:^1.0.0": version: 1.0.0 resolution: "url-parse-lax@npm:1.0.0" From fe63c808d9361fe11b370de8f7808401ccfc5785 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:56:04 +0330 Subject: [PATCH 087/132] improvement: remove unused .md5 and .sha256 files on starknet releases --- starknet/fetch-contracts-release.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/starknet/fetch-contracts-release.sh b/starknet/fetch-contracts-release.sh index 392b77293c9..d70feb96fd0 100755 --- a/starknet/fetch-contracts-release.sh +++ b/starknet/fetch-contracts-release.sh @@ -117,6 +117,8 @@ verify_checksum() { cleanup() { rm -f "$TARGET_DIR/release.zip" + rm -f "$TARGET_DIR"/*.md5 + rm -f "$TARGET_DIR"/*.sha256 } main() { From 5d0dd89529d3c47a0ef4819913c89c5c86f42896 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:11:42 +0330 Subject: [PATCH 088/132] feat: support different contract types import for starknet (contract, token, mock) --- starknet/src/index.ts | 55 +++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/starknet/src/index.ts b/starknet/src/index.ts index e29dbe78136..f855bb59f4a 100644 --- a/starknet/src/index.ts +++ b/starknet/src/index.ts @@ -17,10 +17,13 @@ const TARGET_DEV_PATH = join(currentDirectory, CONFIG.PATHS.MAIN); * @returns {CompiledContract} The parsed contract data * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid */ -export const getCompiledContract = (name: string): CompiledContract => { +export const getCompiledContract = ( + name: string, + contractType?: ContractType, +): CompiledContract => { try { return JSON.parse( - readFileSync(findContractFile(name, 'STANDARD'), 'utf-8'), + readFileSync(findContractFile(name, 'STANDARD', contractType), 'utf-8'), ); } catch (error: unknown) { if (error instanceof ContractError) throw error; @@ -42,10 +45,13 @@ export const getCompiledContract = (name: string): CompiledContract => { * @returns {CairoAssembly} The parsed CASM contract data * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid */ -export const getCompiledContractCasm = (name: string): CairoAssembly => { +export const getCompiledContractCasm = ( + name: string, + contractType?: ContractType, +): CairoAssembly => { try { return JSON.parse( - readFileSync(findContractFile(name, 'COMPILED'), 'utf-8'), + readFileSync(findContractFile(name, 'COMPILED', contractType), 'utf-8'), ); } catch (error: unknown) { if (error instanceof ContractError) throw error; @@ -60,47 +66,40 @@ export const getCompiledContractCasm = (name: string): CairoAssembly => { } }; +/** + * @notice Contract file type enum + */ +export enum ContractType { + CONTRACT = 'contracts_', + TOKEN = 'token_', + MOCK = 'mock_', +} + /** * @notice Finds the path to a contract file based on predefined patterns - * @dev Searches for contract files in multiple predefined locations: - * - contracts_{name}{suffix} - * - token_{name}{suffix} * @param name The base name of the contract to find * @param suffix The type of contract file to look for (from CONFIG.SUFFIXES) - * @returns {string} The full path to the first matching contract file - * @throws {ContractError} If no matching file is found or the contract name is invalid + * @param type Optional contract type prefix (defaults to CONTRACT) + * @returns {string} The full path to the contract file + * @throws {ContractError} If file is not found or the contract name is invalid */ function findContractFile( name: string, suffix: keyof typeof CONFIG.SUFFIXES, + type: ContractType = ContractType.CONTRACT, ): string { assertValidContractName(name); const suffixPath = CONFIG.SUFFIXES[suffix]; - const possiblePaths = [ - { - type: 'contracts', - path: `${TARGET_DEV_PATH}/contracts_${name}${suffixPath}`, - }, - { - type: 'token', - path: `${TARGET_DEV_PATH}/token_${name}${suffixPath}`, - }, - ]; - - const existingPath = possiblePaths.find(({ path }) => existsSync(path)); + const path = `${TARGET_DEV_PATH}/${type}${name}${suffixPath}`; - if (!existingPath) { + if (!existsSync(path)) { throw new ContractError( ErrorMessages[CONFIG.ERROR_CODES.FILE_NOT_FOUND], CONFIG.ERROR_CODES.FILE_NOT_FOUND, - { - name, - suffix, - path: possiblePaths[0].path, - }, + { name, suffix, path }, ); } - return existingPath.path; + return path; } From 394055fa38c9ac42fe3e37e0d83cbc54577323ab Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Wed, 11 Dec 2024 11:46:59 +0100 Subject: [PATCH 089/132] feat: replace Ethers Wallet with ZKSync-specific wallet implementation for ZKSync signer strategy --- .../context/strategies/signer/MultiProtocolSignerFactory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index 030f11b5f4e..d6f83572fb6 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -1,5 +1,6 @@ import { password } from '@inquirer/prompts'; import { Signer, Wallet } from 'ethers'; +import { Wallet as ZKSyncWallet } from 'zksync-ethers'; import { ChainName, @@ -57,7 +58,6 @@ class EthereumSignerStrategy extends BaseMultiProtocolSigner { } // 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean -// TODO: import ZKSync signer class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { async getSignerConfig(chain: ChainName): Promise { const submitter = this.config[chain]?.submitter as { @@ -74,6 +74,6 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { } getSigner(config: SignerConfig): Signer { - return new Wallet(config.privateKey); + return new ZKSyncWallet(config.privateKey); } } From b58434ec2925831b4df5740714c419d04ab34ccc Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Wed, 11 Dec 2024 12:51:42 +0100 Subject: [PATCH 090/132] refactor(cli): remove unused multiProtocolSigner from warp init command --- typescript/cli/src/commands/warp.ts | 5 +---- typescript/cli/src/config/warp.ts | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index f7a90e685a1..3cd99fe6a0d 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -8,7 +8,6 @@ import { createWarpRouteDeployConfig, readWarpRouteDeployConfig, } from '../config/warp.js'; -import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { CommandModuleWithContext, CommandModuleWithWriteContext, @@ -155,7 +154,6 @@ export const deploy: CommandModuleWithWriteContext<{ export const init: CommandModuleWithContext<{ advanced: boolean; out: string; - multiProtocolSigner?: MultiProtocolSignerManager; }> = { command: 'init', describe: 'Create a warp route configuration.', @@ -167,14 +165,13 @@ export const init: CommandModuleWithContext<{ }, out: outputFileCommandOption(DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH), }, - handler: async ({ context, advanced, out, multiProtocolSigner }) => { + handler: async ({ context, advanced, out }) => { logCommandHeader('Hyperlane Warp Configure'); await createWarpRouteDeployConfig({ context, outPath: out, advanced, - multiProtocolSigner, }); process.exit(0); }, diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index cce88ae9894..f31069e0910 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -21,7 +21,6 @@ import { promiseObjAll, } from '@hyperlane-xyz/utils'; -import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; import { runMultiChainSelectionStep } from '../utils/chains.js'; @@ -116,12 +115,10 @@ export async function createWarpRouteDeployConfig({ context, outPath, advanced = false, - multiProtocolSigner, }: { context: CommandContext; outPath: string; advanced: boolean; - multiProtocolSigner?: MultiProtocolSignerManager; }) { logBlue('Creating a new warp route deployment config...'); From 9139d9b4075ec9af9171797e1303720ca5dd72f6 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:49:55 +0330 Subject: [PATCH 091/132] refactor: cleanup, simplify and remove redundant parts of starknet-core --- starknet/src/config.ts | 31 +++---------- starknet/src/errors.ts | 16 +------ starknet/src/index.ts | 63 ++++++++++++--------------- starknet/src/types.ts | 76 -------------------------------- starknet/src/utils.ts | 99 ------------------------------------------ 5 files changed, 35 insertions(+), 250 deletions(-) delete mode 100644 starknet/src/types.ts delete mode 100644 starknet/src/utils.ts diff --git a/starknet/src/config.ts b/starknet/src/config.ts index 1051a10379c..b50b5e8ab26 100644 --- a/starknet/src/config.ts +++ b/starknet/src/config.ts @@ -1,32 +1,11 @@ export const CONFIG = { - PATHS: { - MAIN: 'target/dev', + COMPILED_CONTRACTS_DIR: 'target', + CONTRACT_FILE_SUFFIXES: { + SIERRA_JSON: '.contract_class.json', // Sierra is the high-level representation + ASSEMBLY_JSON: '.compiled_contract_class.json', // Cairo assembly (CASM) is the low-level bytecode }, - SUFFIXES: { - STANDARD: '.contract_class.json', - COMPILED: '.compiled_contract_class.json', - }, - ERROR_CODES: { + CONTRACT_ERROR_CODES: { FILE_NOT_FOUND: 'FILE_NOT_FOUND', PARSE_ERROR: 'PARSE_ERROR', - INVALID_INPUT: 'INVALID_INPUT', - }, - CONTRACT_NAME_VALIDATION: { - MAX_LENGTH: 128, - MIN_LENGTH: 1, - FORBIDDEN_CHARS: [ - '..', - '/', - '\\', - ' ', - '*', - '?', - '<', - '>', - '|', - '"', - ':', - ] as const, - PATTERN: /^[a-zA-Z0-9_-]+$/, }, } as const; diff --git a/starknet/src/errors.ts b/starknet/src/errors.ts index 2d8c5fb1c99..b31f79a2d5c 100644 --- a/starknet/src/errors.ts +++ b/starknet/src/errors.ts @@ -1,18 +1,6 @@ -import { CONFIG } from './config.js'; - export class ContractError extends Error { - constructor( - message: string, - public readonly code: string, - public readonly details?: unknown, - ) { - super(`[${code}] ${message}`); + constructor(public readonly code: string, public readonly details?: unknown) { + super(`[${code}] ${details ? JSON.stringify(details) : ''}`); this.name = 'ContractError'; } } - -export const ErrorMessages = { - [CONFIG.ERROR_CODES.FILE_NOT_FOUND]: 'Contract file not found', - [CONFIG.ERROR_CODES.PARSE_ERROR]: 'Failed to parse contract', - [CONFIG.ERROR_CODES.INVALID_INPUT]: 'Invalid input parameters', -} as const; diff --git a/starknet/src/index.ts b/starknet/src/index.ts index f855bb59f4a..bd32f273e30 100644 --- a/starknet/src/index.ts +++ b/starknet/src/index.ts @@ -4,18 +4,16 @@ import { CairoAssembly, CompiledContract } from 'starknet'; import { fileURLToPath } from 'url'; import { CONFIG } from './config.js'; -import { ContractError, ErrorMessages } from './errors.js'; -import { assertValidContractName } from './utils.js'; +import { ContractError } from './errors.js'; const currentDirectory = dirname(fileURLToPath(import.meta.url)); -const TARGET_DEV_PATH = join(currentDirectory, CONFIG.PATHS.MAIN); +const TARGET_DEV_PATH = join(currentDirectory, CONFIG.COMPILED_CONTRACTS_DIR); /** * @notice Retrieves and parses the standard compiled contract data - * @dev Reads the contract file with STANDARD suffix and parses it as JSON * @param name The name of the contract to retrieve * @returns {CompiledContract} The parsed contract data - * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid + * @throws {ContractError} If the file is not found, cannot be parsed */ export const getCompiledContract = ( name: string, @@ -23,27 +21,25 @@ export const getCompiledContract = ( ): CompiledContract => { try { return JSON.parse( - readFileSync(findContractFile(name, 'STANDARD', contractType), 'utf-8'), + readFileSync( + findContractFile(name, 'SIERRA_JSON', contractType), + 'utf-8', + ), ); } catch (error: unknown) { if (error instanceof ContractError) throw error; - throw new ContractError( - ErrorMessages[CONFIG.ERROR_CODES.PARSE_ERROR], - CONFIG.ERROR_CODES.PARSE_ERROR, - { - name, - error: (error as Error).message, - }, - ); + throw new ContractError(CONFIG.CONTRACT_ERROR_CODES.PARSE_ERROR, { + name, + error: (error as Error).message, + }); } }; /** * @notice Retrieves and parses the CASM compiled contract data - * @dev Reads the contract file with COMPILED suffix and parses it as JSON * @param name The name of the contract to retrieve * @returns {CairoAssembly} The parsed CASM contract data - * @throws {ContractError} If the file is not found, cannot be parsed, or name is invalid + * @throws {ContractError} If the file is not found, cannot be parsed */ export const getCompiledContractCasm = ( name: string, @@ -51,18 +47,17 @@ export const getCompiledContractCasm = ( ): CairoAssembly => { try { return JSON.parse( - readFileSync(findContractFile(name, 'COMPILED', contractType), 'utf-8'), + readFileSync( + findContractFile(name, 'ASSEMBLY_JSON', contractType), + 'utf-8', + ), ); } catch (error: unknown) { if (error instanceof ContractError) throw error; - throw new ContractError( - ErrorMessages[CONFIG.ERROR_CODES.PARSE_ERROR], - CONFIG.ERROR_CODES.PARSE_ERROR, - { - name, - error: (error as Error).message, - }, - ); + throw new ContractError(CONFIG.CONTRACT_ERROR_CODES.PARSE_ERROR, { + name, + error: (error as Error).message, + }); } }; @@ -78,27 +73,25 @@ export enum ContractType { /** * @notice Finds the path to a contract file based on predefined patterns * @param name The base name of the contract to find - * @param suffix The type of contract file to look for (from CONFIG.SUFFIXES) + * @param suffix The type of contract file to look for (from CONFIG.CONTRACT_FILE_SUFFIXES) * @param type Optional contract type prefix (defaults to CONTRACT) * @returns {string} The full path to the contract file * @throws {ContractError} If file is not found or the contract name is invalid */ function findContractFile( name: string, - suffix: keyof typeof CONFIG.SUFFIXES, + suffix: keyof typeof CONFIG.CONTRACT_FILE_SUFFIXES, type: ContractType = ContractType.CONTRACT, ): string { - assertValidContractName(name); - - const suffixPath = CONFIG.SUFFIXES[suffix]; + const suffixPath = CONFIG.CONTRACT_FILE_SUFFIXES[suffix]; const path = `${TARGET_DEV_PATH}/${type}${name}${suffixPath}`; if (!existsSync(path)) { - throw new ContractError( - ErrorMessages[CONFIG.ERROR_CODES.FILE_NOT_FOUND], - CONFIG.ERROR_CODES.FILE_NOT_FOUND, - { name, suffix, path }, - ); + throw new ContractError(CONFIG.CONTRACT_ERROR_CODES.FILE_NOT_FOUND, { + name, + suffix, + path, + }); } return path; diff --git a/starknet/src/types.ts b/starknet/src/types.ts deleted file mode 100644 index f3f23316b84..00000000000 --- a/starknet/src/types.ts +++ /dev/null @@ -1,76 +0,0 @@ -export interface SierraProgram { - sierra_program: string[]; - sierra_program_debug_info?: { - type_information?: unknown; - libfunc_declarations?: unknown; - user_func_declarations?: unknown; - }; - contract_class_version: string; - entry_points_by_type: { - CONSTRUCTOR: EntryPoint[]; - EXTERNAL: EntryPoint[]; - L1_HANDLER: EntryPoint[]; - }; - abi: ContractAbi; -} - -export interface EntryPoint { - selector: string; - function_idx: number; -} - -export interface ContractAbi { - type: string; - name?: string; - inputs?: AbiInput[]; - outputs?: AbiOutput[]; - state_mutability?: string; - functions: AbiFunction[]; - events: AbiEvent[]; - structs: AbiStruct[]; - l1_handler?: boolean; -} - -export interface AbiFunction { - name: string; - inputs: AbiInput[]; - outputs: AbiOutput[]; - state_mutability: string; -} - -export interface AbiEvent { - name: string; - inputs: AbiInput[]; -} - -export interface AbiInput { - name: string; - type: string; -} - -export interface AbiOutput { - type: string; -} - -export interface AbiStruct { - name: string; - size: number; - members: AbiStructMember[]; -} - -export interface AbiStructMember { - name: string; - type: string; - offset: number; -} - -// Update the main ContractData interface -export interface ContractData extends SierraProgram { - [key: string]: unknown; // Keep this for backward compatibility -} - -export interface CompiledContractCasm { - prime: string; // e.g. "0x800000000000011000000000000000000000000000000000000000000000001" - compiler_version: string; // e.g. "2.6.4" - bytecode: string[]; // Array of hex strings representing bytecode instructions -} diff --git a/starknet/src/utils.ts b/starknet/src/utils.ts deleted file mode 100644 index 290c5b57dfe..00000000000 --- a/starknet/src/utils.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { CONFIG } from './config.js'; -import { ContractError, ErrorMessages } from './errors.js'; - -/** - * @notice Represents different types of contract name validation errors - */ -export type ValidationError = - | { type: 'empty' } - | { type: 'tooLong'; maxLength: number } - | { type: 'invalidChars'; chars: string[] } - | { type: 'invalidPattern' }; - -/** - * @notice Validates a contract name and returns any validation errors - * @dev Checks for empty strings, length limits, forbidden characters, and pattern matching - * @param name The contract name to validate - * @returns An array of validation errors, empty if valid - */ -export function validateContractName(name: string): ValidationError[] { - const errors: ValidationError[] = []; - - // Check for empty or whitespace-only names - if (!name.trim()) { - errors.push({ type: 'empty' }); - return errors; // Return early as other checks don't matter - } - - // Check length - if (name.length > CONFIG.CONTRACT_NAME_VALIDATION.MAX_LENGTH) { - errors.push({ - type: 'tooLong', - maxLength: CONFIG.CONTRACT_NAME_VALIDATION.MAX_LENGTH, - }); - } - - // Check for forbidden characters - const foundForbiddenChars = - CONFIG.CONTRACT_NAME_VALIDATION.FORBIDDEN_CHARS.filter((char) => - name.includes(char), - ); - - if (foundForbiddenChars.length > 0) { - errors.push({ type: 'invalidChars', chars: foundForbiddenChars }); - } - - // Check pattern match - if (!CONFIG.CONTRACT_NAME_VALIDATION.PATTERN.test(name)) { - errors.push({ type: 'invalidPattern' }); - } - - return errors; -} - -/** - * @notice Gets a human-readable error message for validation errors - * @param errors Array of validation errors - * @returns A formatted error message - */ -export function getValidationErrorMessage(errors: ValidationError[]): string { - if (errors.length === 0) return ''; - - const messages = errors.map((error) => { - switch (error.type) { - case 'empty': - return 'Contract name cannot be empty or only whitespace'; - case 'tooLong': - return `Contract name cannot exceed ${error.maxLength} characters`; - case 'invalidChars': - return `Contract name contains invalid characters: ${error.chars.join( - ' ', - )}`; - case 'invalidPattern': - return 'Contract name can only contain letters, numbers, underscores, and hyphens'; - } - }); - - return messages.join('. '); -} - -/** - * @notice Validates a contract name and throws if invalid - * @dev Combines validation and error throwing into a single function - * @param name The contract name to validate - * @throws {ContractError} If the name is invalid - */ -export function assertValidContractName(name: string): void { - const errors = validateContractName(name); - - if (errors.length > 0) { - throw new ContractError( - ErrorMessages[CONFIG.ERROR_CODES.INVALID_INPUT], - CONFIG.ERROR_CODES.INVALID_INPUT, - { - name, - reason: getValidationErrorMessage(errors), - }, - ); - } -} From 9ed558ce28230d0e8908a91180e06cbdf78f7f39 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:42:36 +0330 Subject: [PATCH 092/132] refactor: streamline package structure and build process --- starknet/.gitignore | 3 ++- starknet/package.json | 16 +++++++++---- .../scripts/fetch-contracts-if-missing.ts | 23 +++++++++++++++++++ .../{ => scripts}/fetch-contracts-release.sh | 4 ++-- yarn.lock | 1 + 5 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 starknet/scripts/fetch-contracts-if-missing.ts rename starknet/{ => scripts}/fetch-contracts-release.sh (97%) diff --git a/starknet/.gitignore b/starknet/.gitignore index 8c5bcf6780f..7f27dec905b 100644 --- a/starknet/.gitignore +++ b/starknet/.gitignore @@ -1,2 +1,3 @@ .env -dist \ No newline at end of file +dist +release \ No newline at end of file diff --git a/starknet/package.json b/starknet/package.json index eeaabca12db..c715a974962 100644 --- a/starknet/package.json +++ b/starknet/package.json @@ -4,16 +4,23 @@ "version": "0.2.1", "type": "module", "homepage": "https://www.hyperlane.xyz", - "author": "", "license": "Apache-2.0", "scripts": { - "build": "tsc && ./fetch-contracts-release.sh", - "clean": "rm -rf ./dist" + "prepare-contracts": "tsx ./scripts/fetch-contracts-if-missing.ts", + "build-contracts": "mkdir -p dist/target && cp release/* dist/target/", + "build": "tsc && yarn prepare-contracts && yarn build-contracts", + "clean": "rm -rf ./dist ./release" }, "exports": { - ".": "./dist/index.js" + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } }, "types": "./dist/index.d.ts", + "files": [ + "/dist" + ], "keywords": [ "Hyperlane", "Cairo", @@ -26,6 +33,7 @@ "starknet": "6.11.0" }, "devDependencies": { + "tsx": "^4.7.1", "typescript": "5.3.3" } } diff --git a/starknet/scripts/fetch-contracts-if-missing.ts b/starknet/scripts/fetch-contracts-if-missing.ts new file mode 100644 index 00000000000..5842c721ebb --- /dev/null +++ b/starknet/scripts/fetch-contracts-if-missing.ts @@ -0,0 +1,23 @@ +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { join } from 'path'; + +const RELEASE_DIR = join(process.cwd(), 'release'); + +try { + if (existsSync(RELEASE_DIR)) { + console.log( + '[INFO] Contracts already present in src/release, skipping fetch', + ); + process.exit(0); + } + + console.log('[INFO] Fetching contracts...'); + execSync('./scripts/fetch-contracts-release.sh', { + stdio: 'inherit', + cwd: join(process.cwd()), + }); +} catch (error) { + console.error('[ERROR]', (error as Error).message); + process.exit(1); +} diff --git a/starknet/fetch-contracts-release.sh b/starknet/scripts/fetch-contracts-release.sh similarity index 97% rename from starknet/fetch-contracts-release.sh rename to starknet/scripts/fetch-contracts-release.sh index d70feb96fd0..6fd49a6f274 100755 --- a/starknet/fetch-contracts-release.sh +++ b/starknet/scripts/fetch-contracts-release.sh @@ -7,7 +7,7 @@ IFS=$'\n\t' # Constants readonly REPO="astraly-labs/hyperlane_starknet" readonly GITHUB_RELEASES_API="https://api.github.com/repos/${REPO}/releases" -readonly TARGET_DIR="./dist/target" +readonly TARGET_DIR="./release" # Color definitions declare -r COLOR_GREEN='\033[0;32m' @@ -35,7 +35,7 @@ check_dependencies() { get_package_version() { local package_version - if ! package_version=$(jq -r '.version' package.json 2>/dev/null); then + if ! package_version=$(jq -r '.version' ./package.json 2>/dev/null); then log_error "Failed to read version from package.json" exit 1 fi diff --git a/yarn.lock b/yarn.lock index 4c4da7f8bd7..c279d824449 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8711,6 +8711,7 @@ __metadata: resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" dependencies: starknet: "npm:6.11.0" + tsx: "npm:^4.7.1" typescript: "npm:5.3.3" languageName: unknown linkType: soft From f8a08cb15f814f7d65eed9ef0886156c0e7db44c Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:40:52 +0330 Subject: [PATCH 093/132] improvement: setup linter and preittier for starknet sdk --- starknet/.eslintrc | 6 ++++++ starknet/package.json | 7 ++++++- yarn.lock | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 starknet/.eslintrc diff --git a/starknet/.eslintrc b/starknet/.eslintrc new file mode 100644 index 00000000000..4d2a6fe74fc --- /dev/null +++ b/starknet/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-console": ["off"], + "no-restricted-imports": ["off"] + } +} diff --git a/starknet/package.json b/starknet/package.json index c715a974962..efba77662ca 100644 --- a/starknet/package.json +++ b/starknet/package.json @@ -9,7 +9,9 @@ "prepare-contracts": "tsx ./scripts/fetch-contracts-if-missing.ts", "build-contracts": "mkdir -p dist/target && cp release/* dist/target/", "build": "tsc && yarn prepare-contracts && yarn build-contracts", - "clean": "rm -rf ./dist ./release" + "clean": "rm -rf ./dist ./release", + "lint": "eslint src --ext .ts", + "prettier": "prettier --write ./src ./scripts ./package.json" }, "exports": { ".": { @@ -33,6 +35,9 @@ "starknet": "6.11.0" }, "devDependencies": { + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^2.8.8", "tsx": "^4.7.1", "typescript": "5.3.3" } diff --git a/yarn.lock b/yarn.lock index c279d824449..9498d192da4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8710,6 +8710,9 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" dependencies: + eslint: "npm:^8.57.0" + eslint-config-prettier: "npm:^9.1.0" + prettier: "npm:^2.8.8" starknet: "npm:6.11.0" tsx: "npm:^4.7.1" typescript: "npm:5.3.3" From 6d0219e33837337acc9be93776d8539f7ccc2a54 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:35:47 +0330 Subject: [PATCH 094/132] fix: use token contract type for starknet token deployment for warp routes --- typescript/sdk/src/deploy/StarknetDeployer.ts | 6 +++-- .../sdk/src/token/StarknetERC20WarpModule.ts | 25 +++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index 1f22a777e89..a321a65ffa5 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -8,6 +8,7 @@ import { } from 'starknet'; import { + ContractType, getCompiledContract, getCompiledContractCasm, } from '@hyperlane-xyz/starknet-core'; @@ -35,11 +36,12 @@ export class StarknetDeployer { async deployContract( contractName: string, constructorArgs: RawArgs, + contractType?: ContractType, ): Promise { this.logger.info(`Deploying contract ${contractName}...`); - const compiledContract = getCompiledContract(contractName); - const casm = getCompiledContractCasm(contractName); + const compiledContract = getCompiledContract(contractName, contractType); + const casm = getCompiledContractCasm(contractName, contractType); const constructorCalldata = CallData.compile(constructorArgs); const params: ContractFactoryParams = { diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index ce962cda8e0..9d56e7b6abd 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -2,6 +2,7 @@ import { Signer } from 'ethers'; import { Account, byteArray, getChecksumAddress } from 'starknet'; import { TokenType } from '@hyperlane-xyz/sdk'; +import { ContractType } from '@hyperlane-xyz/starknet-core'; import { ProtocolType, assert, rootLogger } from '@hyperlane-xyz/utils'; import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; @@ -54,16 +55,20 @@ export class StarknetERC20WarpModule { }); switch (type) { case TokenType.synthetic: { - const tokenAddress = await deployer.deployContract('HypErc20', { - decimals: tokenMetadata.decimals, - mailbox: mailbox, - total_supply: tokenMetadata.totalSupply, - name: [byteArray.byteArrayFromString(tokenMetadata.name)], - symbol: [byteArray.byteArrayFromString(tokenMetadata.symbol)], - hook: getChecksumAddress(0), - interchain_security_module: ismAddress, - owner: account.address, //TODO: use config.owner, and in warp init ask for starknet owner - }); + const tokenAddress = await deployer.deployContract( + 'HypErc20', + { + decimals: tokenMetadata.decimals, + mailbox: mailbox, + total_supply: tokenMetadata.totalSupply, + name: [byteArray.byteArrayFromString(tokenMetadata.name)], + symbol: [byteArray.byteArrayFromString(tokenMetadata.symbol)], + hook: getChecksumAddress(0), + interchain_security_module: ismAddress, + owner: account.address, //TODO: use config.owner, and in warp init ask for starknet owner + }, + ContractType.TOKEN, + ); addresses[chain] = tokenAddress; break; } From 16501ca0a75d277e62f053ca057332bcaf272fde Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Fri, 13 Dec 2024 13:21:11 +0100 Subject: [PATCH 095/132] refactor: simplify MultiProtocolSignerManager and remove unused strategy config in warp deploy --- typescript/cli/src/config/warp.ts | 18 ------------------ .../signer/MultiProtocolSignerManager.ts | 18 +++++++++--------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 15e24680caa..f31069e0910 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -7,7 +7,6 @@ import { IsmConfig, IsmType, MailboxClientConfig, - MultiProtocolProvider, TokenType, WarpCoreConfig, WarpCoreConfigSchema, @@ -22,8 +21,6 @@ import { promiseObjAll, } from '@hyperlane-xyz/utils'; -import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; -import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; import { runMultiChainSelectionStep } from '../utils/chains.js'; @@ -38,7 +35,6 @@ import { } from '../utils/input.js'; import { createAdvancedIsmConfig } from './ism.js'; -import { readChainSubmissionStrategyConfig } from './strategy.js'; const TYPE_DESCRIPTIONS: Record = { [TokenType.synthetic]: 'A new ERC20 with remote transfer functionality', @@ -135,20 +131,6 @@ export async function createWarpRouteDeployConfig({ requiresConfirmation: !context.skipConfirmation, }); - const strategyConfig = await readChainSubmissionStrategyConfig( - context.strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, - ); - - const multiProtocolSigner = new MultiProtocolSignerManager( - strategyConfig, - warpChains, - context.multiProvider, - new MultiProtocolProvider(context.chainMetadata), - { key: context.key }, - ); - - const multiProviderWithSigners = await multiProtocolSigner.getMultiProvider(); - const result: WarpRouteDeployConfig = {}; let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts index 1d4af9ed57e..a1c0030786f 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -67,16 +67,15 @@ export class MultiProtocolSignerManager { * @dev Configures signers for EVM chains in MultiProvider */ async getMultiProvider(): Promise { - for (const chain of this.chains) { - // multiProvider is only compatible with evm chains - if ( + const ethereumChains = this.chains.filter( + (chain) => this.multiProvider.getChainMetadata(chain).protocol === - ProtocolType.Ethereum - ) { - const signer = await this.initSigner(chain); - if (signer instanceof Signer) - this.multiProvider.setSigner(chain, signer); - } + ProtocolType.Ethereum, + ); + + for (const chain of ethereumChains) { + const signer = await this.initSigner(chain); + this.multiProvider.setSigner(chain, signer as Signer); } return this.multiProvider; @@ -113,6 +112,7 @@ export class MultiProtocolSignerManager { }), ); } else { + // evm chains this.signers.set(chain, signerStrategy.getSigner({ privateKey })); } } From 6be70938064aca0afe0535544e6ef62cdcedd009 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:13:00 +0330 Subject: [PATCH 096/132] temp: make collateral <=> syntetic for starknet --- .../sdk/src/token/StarknetERC20WarpModule.ts | 5 ++ typescript/sdk/src/token/deploy.ts | 63 +++++++++++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index 9d56e7b6abd..c1ae1a868a6 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -82,6 +82,11 @@ export class StarknetERC20WarpModule { addresses[chain] = tokenAddress; break; } + + case TokenType.collateral: { + addresses[chain] = ''; + break; + } default: throw Error('Token type is not supported on starknet'); } diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index 87f77bc8829..51cab2cfb28 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -1,4 +1,5 @@ import { constants } from 'ethers'; +import { Contract, RpcProvider, shortString } from 'starknet'; import { ERC20__factory, @@ -66,9 +67,7 @@ abstract class TokenDeployer< assert(config.decimals, 'decimals is undefined for config'); // decimals must be defined by this point return [config.decimals, config.mailbox]; } else if (isSyntheticRebaseConfig(config)) { - const collateralDomain = this.multiProvider.getDomainId( - config.collateralChainName, - ); + const collateralDomain = 5854809; return [config.decimals, config.mailbox, collateralDomain]; } else { throw new Error('Unknown token type when constructing arguments'); @@ -120,7 +119,19 @@ abstract class TokenDeployer< } if (isCollateralConfig(config)) { - const provider = multiProvider.getProvider(chain); + // Add chain type checking + const chainMetadata = multiProvider.getChainMetadata(chain); + const isStarknet = chainMetadata.protocol === 'starknet'; + let provider; + + // Handle different chain types + if (isStarknet) { + provider = new RpcProvider({ + nodeUrl: chainMetadata.rpcUrls[0].http as any, // Use the actual RPC URL from chain metadata + }); + } else { + provider = multiProvider.getProvider(chain) as any; + } if (config.isNft) { const erc721 = ERC721Enumerable__factory.connect( @@ -156,6 +167,50 @@ abstract class TokenDeployer< token = config.token; break; } + if (isStarknet) { + const erc20Abi = [ + { + name: 'name', + type: 'function', + inputs: [], + outputs: [{ name: '', type: 'felt' }], + stateMutability: 'view', + }, + { + name: 'symbol', + type: 'function', + inputs: [], + outputs: [{ name: '', type: 'felt' }], + stateMutability: 'view', + }, + { + name: 'decimals', + type: 'function', + inputs: [], + outputs: [{ name: '', type: 'felt' }], + stateMutability: 'view', + }, + ]; + // Create contract instance + const erc20 = new Contract(erc20Abi, token, provider); + const [nameResult, symbolResult, decimalsResult] = await Promise.all([ + erc20.name(), + erc20.symbol(), + erc20.decimals(), + ]); + + // Parse the results - extract the values and convert them properly + const name = shortString.decodeShortString(nameResult['']); + const symbol = shortString.decodeShortString(symbolResult['']); + const decimals = Number(decimalsResult['']); // Convert BigInt to number + console.log(name, symbol, decimals); + return TokenMetadataSchema.parse({ + name, + symbol, + decimals, + totalSupply: DERIVED_TOKEN_SUPPLY, + }); + } const erc20 = ERC20__factory.connect(token, provider); const [name, symbol, decimals] = await Promise.all([ From e4b4254aa9e4843c1de8be8a2087377269ec37a1 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Mon, 16 Dec 2024 11:13:54 +0100 Subject: [PATCH 097/132] refactor: rename address to userAddress for consistency in signer configs --- .../signer/BaseMultiProtocolSigner.ts | 2 +- .../signer/MultiProtocolSignerFactory.ts | 8 ++-- .../signer/MultiProtocolSignerManager.ts | 10 ++--- typescript/cli/src/send/message.ts | 37 +++---------------- typescript/sdk/src/index.ts | 1 - 5 files changed, 15 insertions(+), 43 deletions(-) diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts index ad122fb67b4..791bbc200f8 100644 --- a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts +++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts @@ -8,7 +8,7 @@ export type TypedSigner = Signer | StarknetAccount; export interface SignerConfig { privateKey: string; - address?: Address; // For chains like StarkNet that require address + userAddress?: Address; // For chains like StarkNet that require address extraParams?: Record; // For any additional chain-specific params } diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index 67a0d6ce10e..a38ad422a05 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -100,18 +100,18 @@ class StarknetSignerStrategy extends BaseMultiProtocolSigner { message: `Please enter the signer address for chain ${chain}`, })); - return { privateKey, address }; + return { privateKey, userAddress: address }; } getSigner({ privateKey, - address, + userAddress, extraParams, }: SignerConfig): StarknetAccount { assert( - address && extraParams?.provider, + userAddress && extraParams?.provider, 'Missing StarknetAccount arguments', ); - return new StarknetAccount(extraParams.provider, address, privateKey); + return new StarknetAccount(extraParams.provider, userAddress, privateKey); } } diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts index d75df31aca2..9be5b8a15de 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -106,7 +106,7 @@ export class MultiProtocolSignerManager { throw new Error(`No private key found for chain ${chain}`); } config.privateKey = strategyConfig.privateKey; - config.userAddress = strategyConfig.address; + config.userAddress = strategyConfig.userAddress; } const { protocol } = this.multiProvider.getChainMetadata(chain); @@ -158,7 +158,7 @@ export class MultiProtocolSignerManager { async initAllSigners(): Promise { const signerConfigs = await this.resolveAllConfigs(); - for (const { chain, privateKey, address } of signerConfigs) { + for (const { chain, privateKey, userAddress } of signerConfigs) { const signerStrategy = this.signerStrategies.get(chain); if (signerStrategy) { const { protocol } = this.multiProvider.getChainMetadata(chain); @@ -169,7 +169,7 @@ export class MultiProtocolSignerManager { chain, signerStrategy.getSigner({ privateKey, - address, + userAddress, extraParams: { provider }, }), ); @@ -250,7 +250,7 @@ export class MultiProtocolSignerManager { strategyConfig.privateKey, `No private key found for chain ${chain}`, ); - assert(strategyConfig.address, 'No Starknet Address found'); + assert(strategyConfig.userAddress, 'No Starknet Address found'); assert(provider, 'No Starknet Provider found'); this.logger.info(`Using strategy config for Starknet chain ${chain}`); @@ -258,7 +258,7 @@ export class MultiProtocolSignerManager { return { chain, privateKey: strategyConfig.privateKey, - address: strategyConfig.address, + userAddress: strategyConfig.userAddress, extraParams: { provider }, }; } diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index 101620543aa..f7d5eb4e151 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -1,12 +1,6 @@ -import { Account, Provider } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; -import { - ChainName, - HyperlaneCore, - HyperlaneRelayer, - StarknetCore, -} from '@hyperlane-xyz/sdk'; +import { ChainName, HyperlaneCore, HyperlaneRelayer } from '@hyperlane-xyz/sdk'; import { addressToBytes32, timeout } from '@hyperlane-xyz/utils'; import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; @@ -88,36 +82,16 @@ async function executeDelivery({ }) { const { registry, multiProvider } = context; const chainAddresses = await registry.getAddresses(); - const provider = new Provider({ - nodeUrl: 'http://127.0.0.1:5050', - }); - const account = new Account( - provider, - '0x6acf82752859a6bced2eb2e9e4346062763088e72422d6f7c2ee8a7526e07d7', - '0x000000000000000000000000000000004e4993ca00259617c8075a3f76d43abc', - ); + const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); + try { - const recipient = - chainAddresses[destination].testRecipient || - '0x00581bb8ad9e4ecd0ba06793e2ffb26f4b12ea18ec69dfb216738efe569e2e59'; + const recipient = chainAddresses[destination].testRecipient; if (!recipient) { throw new Error(`Unable to find TestRecipient for ${destination}`); } const formattedRecipient = addressToBytes32(recipient); - // log('Dispatching message'); - const destinationDomain = multiProvider.getDomainId(destination); - - const starknet = new StarknetCore(account); - const tx = await starknet.sendMessage({ - destinationDomain, - messageBody, - recipientAddress: chainAddresses[destination].testRecipient, - }); - console.log({ tx }); - return; - const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); - + log('Dispatching message'); const { dispatchTx, message } = await core.sendMessage( origin, destination, @@ -147,7 +121,6 @@ async function executeDelivery({ log('Waiting for message delivery on destination chain...'); // Max wait 10 minutes - return; await core.waitForMessageProcessed(dispatchTx, 10000, 60); logGreen('Message was delivered!'); } diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 39a53a17b7d..4704a089519 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -63,7 +63,6 @@ export { export { HyperlaneLifecyleEvent } from './core/events.js'; export { EvmCoreReader } from './core/EvmCoreReader.js'; export { HyperlaneCore } from './core/HyperlaneCore.js'; -export { StarknetCore } from './core/StarknetCore.js'; export { HyperlaneCoreChecker } from './core/HyperlaneCoreChecker.js'; export { HyperlaneCoreDeployer } from './core/HyperlaneCoreDeployer.js'; export { From b2829fd600f01e136392f776fe44eb3d6e055833 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:01:26 +0330 Subject: [PATCH 098/132] feat: deploy HypErc20Collateral contract during warp deploy --- .../sdk/src/token/StarknetERC20WarpModule.ts | 16 ++++++++++++++-- typescript/sdk/src/token/deploy.ts | 1 - 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index c1ae1a868a6..1ff5a474e78 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -35,7 +35,7 @@ export class StarknetERC20WarpModule { const addresses: ChainMap = {}; for (const [ chain, - { mailbox, interchainSecurityModule, type }, + { mailbox, interchainSecurityModule, type, ...rest }, ] of Object.entries(this.config)) { //Ignore non-starknet chains if ( @@ -84,7 +84,19 @@ export class StarknetERC20WarpModule { } case TokenType.collateral: { - addresses[chain] = ''; + const tokenAddress = await deployer.deployContract( + 'HypErc20Collateral', + { + mailbox: mailbox, + // @ts-ignore + erc20: rest.token, + owner: account.address, //TODO: use config.owner, and in warp init ask for starknet owner + hook: getChecksumAddress(0), + interchain_security_module: ismAddress, + }, + ContractType.TOKEN, + ); + addresses[chain] = tokenAddress; break; } default: diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index 51cab2cfb28..cd2b7191a09 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -203,7 +203,6 @@ abstract class TokenDeployer< const name = shortString.decodeShortString(nameResult['']); const symbol = shortString.decodeShortString(symbolResult['']); const decimals = Number(decimalsResult['']); // Convert BigInt to number - console.log(name, symbol, decimals); return TokenMetadataSchema.parse({ name, symbol, From 27ff277b5605bd4b2f015ece65864088ce9f2f32 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:50:51 +0330 Subject: [PATCH 099/132] feat: use signer per chain for starkner warp deployments --- typescript/cli/src/deploy/warp.ts | 22 +++++++++++-------- .../sdk/src/token/StarknetERC20WarpModule.ts | 20 ++++++++++++----- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 470aa660680..d0395669cd4 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -1,6 +1,6 @@ import { confirm } from '@inquirer/prompts'; import { groupBy } from 'lodash-es'; -import { Account } from 'starknet'; +import { Account as StarknetAccount } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; @@ -179,13 +179,17 @@ export async function runWarpRouteDeploy({ multiProtocolSigner, 'multi protocol signer is required for starknet chain deployment', ); - // TODO: support multiple starknet chains - const starknetChain = chainsByProtocol[protocol][0]; // Only one Starknet chain supported at a time - const starknetSigner = await multiProtocolSigner.getStarknetSigner( - starknetChain, + const starknetSigners = await promiseObjAll( + protocolChains.reduce>>( + (acc, chain) => ({ + ...acc, + [chain]: multiProtocolSigner.getStarknetSigner(chain), + }), + {}, + ), ); const addresses = await executeStarknetDeployments({ - starknetSigner, + starknetSigners, warpRouteConfig, multiProvider, }); @@ -1143,18 +1147,18 @@ function groupChainsByProtocol( } async function executeStarknetDeployments({ - starknetSigner, + starknetSigners, warpRouteConfig, multiProvider, }: { - starknetSigner: Account; + starknetSigners: ChainMap; warpRouteConfig: WarpRouteDeployConfig; multiProvider: MultiProvider; }): Promise> { assert(!warpRouteConfig.isNft, 'NFT routes not supported yet!'); const starknetDeployer = new StarknetERC20WarpModule( - starknetSigner, + starknetSigners, warpRouteConfig, multiProvider, ); diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index d50c90d2261..9553bc28385 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -16,7 +16,7 @@ export class StarknetERC20WarpModule { protected logger = rootLogger.child({ module: 'StarknetERC20WarpModule' }); constructor( - protected readonly account: Account, + protected readonly account: ChainMap, protected readonly config: WarpRouteDeployConfig, protected readonly multiProvider: MultiProvider, ) {} @@ -43,8 +43,8 @@ export class StarknetERC20WarpModule { ) continue; - const deployer = new StarknetDeployer(this.account); - + const deployer = new StarknetDeployer(this.account[chain]); + const deployerAccountAddress = this.account[chain].address; const ismAddress = await this.getStarknetDeploymentISMAddress({ ismConfig: interchainSecurityModule, mailbox: mailbox, @@ -63,7 +63,7 @@ export class StarknetERC20WarpModule { symbol: [byteArray.byteArrayFromString(tokenMetadata.symbol)], hook: getChecksumAddress(0), interchain_security_module: ismAddress, - owner: this.account.address, //TODO: use config.owner, and in warp init ask for starknet owner + owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner }, ContractType.TOKEN, ); @@ -75,20 +75,28 @@ export class StarknetERC20WarpModule { mailbox: mailbox, hook: getChecksumAddress(0), interchain_security_module: ismAddress, - owner: this.account.address, //TODO: use config.owner, and in warp init ask for starknet owner + owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner }); addresses[chain] = tokenAddress; break; } case TokenType.collateral: { + console.log({ + mailbox: mailbox, + // @ts-ignore + erc20: rest.token, + owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner + hook: getChecksumAddress(0), + interchain_security_module: ismAddress, + }); const tokenAddress = await deployer.deployContract( 'HypErc20Collateral', { mailbox: mailbox, // @ts-ignore erc20: rest.token, - owner: this.account.address, //TODO: use config.owner, and in warp init ask for starknet owner + owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner hook: getChecksumAddress(0), interchain_security_module: ismAddress, }, From 74695f6d9a77c57abb42fed1f5f6e16261b9b75d Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:50:52 +0330 Subject: [PATCH 100/132] feat: handle warp deploy submission for starknet chains --- typescript/cli/src/deploy/warp.ts | 39 +++++++++++++++++++---- typescript/sdk/src/index.ts | 2 ++ typescript/sdk/src/token/Token.test.ts | 6 ++++ typescript/sdk/src/token/TokenStandard.ts | 31 +++++++++++++++++- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index d0395669cd4..f7e428128e4 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -37,10 +37,13 @@ import { PausableIsmConfig, RemoteRouters, RoutingIsmConfig, + STARKNET_SUPPORTED_TOKEN_TYPES, + STARKNET_TOKEN_TYPE_TO_STANDARD, StarknetERC20WarpModule, SubmissionStrategy, TOKEN_TYPE_TO_STANDARD, TokenFactories, + TokenType, TrustedRelayerIsmConfig, TxSubmitterBuilder, TxSubmitterType, @@ -504,7 +507,7 @@ async function getWarpCoreConfig( decimals, ); - fullyConnectTokens(warpCoreConfig); + fullyConnectTokens(warpCoreConfig, context.multiProvider); return warpCoreConfig; } @@ -546,7 +549,10 @@ function generateTokenConfigs( * Assumes full interconnectivity between all tokens for now b.c. that's * what the deployers do by default. */ -function fullyConnectTokens(warpCoreConfig: WarpCoreConfig): void { +function fullyConnectTokens( + warpCoreConfig: WarpCoreConfig, + multiProvider: MultiProvider, +): void { for (const token1 of warpCoreConfig.tokens) { for (const token2 of warpCoreConfig.tokens) { if ( @@ -557,7 +563,7 @@ function fullyConnectTokens(warpCoreConfig: WarpCoreConfig): void { token1.connections ||= []; token1.connections.push({ token: getTokenConnectionId( - ProtocolType.Ethereum, + multiProvider.getChainMetadata(token2.chainName).protocol, token2.chainName, token2.addressOrDenom!, ), @@ -1155,7 +1161,7 @@ async function executeStarknetDeployments({ warpRouteConfig: WarpRouteDeployConfig; multiProvider: MultiProvider; }): Promise> { - assert(!warpRouteConfig.isNft, 'NFT routes not supported yet!'); + validateStarknetWarpConfig(warpRouteConfig); const starknetDeployer = new StarknetERC20WarpModule( starknetSigners, @@ -1194,7 +1200,7 @@ async function getWarpCoreConfigForStarknet( decimals, ); - fullyConnectTokens(warpCoreConfig); + fullyConnectTokens(warpCoreConfig, multiProvider); return warpCoreConfig; } @@ -1214,7 +1220,10 @@ function generateTokenConfigsForStarknet( : undefined; warpCoreConfig.tokens.push({ chainName, - standard: TOKEN_TYPE_TO_STANDARD[config.type], + standard: + STARKNET_TOKEN_TYPE_TO_STANDARD[ + config.type as keyof typeof STARKNET_TOKEN_TYPE_TO_STANDARD + ], decimals, symbol, name, @@ -1223,3 +1232,21 @@ function generateTokenConfigsForStarknet( }); } } + +function validateStarknetWarpConfig(warpRouteConfig: WarpRouteDeployConfig) { + assert(!warpRouteConfig.isNft, 'NFT routes not supported yet!'); + + // token type validation for Starknet chains + for (const [chain, config] of Object.entries(warpRouteConfig)) { + assert( + (STARKNET_SUPPORTED_TOKEN_TYPES as readonly TokenType[]).includes( + config.type, + ), + `Token type "${ + config.type + }" is not supported on Starknet chains (${chain}}). Supported types: ${STARKNET_SUPPORTED_TOKEN_TYPES.join( + ', ', + )}`, + ); + } +} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 4704a089519..4d85ee2c2bd 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -547,6 +547,8 @@ export { TOKEN_NFT_STANDARDS, TOKEN_STANDARD_TO_PROTOCOL, TOKEN_TYPE_TO_STANDARD, + STARKNET_TOKEN_TYPE_TO_STANDARD, + STARKNET_SUPPORTED_TOKEN_TYPES, TokenStandard, } from './token/TokenStandard.js'; export { diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index 1a6ac978ad5..4b682bb879d 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -199,7 +199,13 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'TIA.n', name: 'TIA.n', }, + [TokenStandard.CwHypSynthetic]: null, + + //TODO: check this and manage it. + [TokenStandard.StarknetHypCollateral]: null, + [TokenStandard.StarknetHypNative]: null, + [TokenStandard.StarknetHypSynthetic]: null, }; const PROTOCOL_TO_ADDRESS_FOR_BALANCE_CHECK: Partial< diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 2a8ec8c5aac..d984ae6a048 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -43,6 +43,11 @@ export enum TokenStandard { CwHypNative = 'CwHypNative', CwHypCollateral = 'CwHypCollateral', CwHypSynthetic = 'CwHypSynthetic', + + //Starknet + StarknetHypNative = 'StarknetHypNative', + StarknetHypCollateral = 'StarknetHypCollateral', + StarknetHypSynthetic = 'StarknetHypSynthetic', } // Allows for omission of protocol field in token args @@ -82,6 +87,11 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record = { CwHypNative: ProtocolType.Cosmos, CwHypCollateral: ProtocolType.Cosmos, CwHypSynthetic: ProtocolType.Cosmos, + + // Starknet + StarknetHypCollateral: ProtocolType.Starknet, + StarknetHypNative: ProtocolType.Starknet, + StarknetHypSynthetic: ProtocolType.Starknet, }; export const TOKEN_STANDARD_TO_PROVIDER_TYPE: Record< @@ -166,10 +176,29 @@ export const TOKEN_TYPE_TO_STANDARD: Record = { [TokenType.nativeScaled]: TokenStandard.EvmHypNative, }; +// Starknet supported token types +export const STARKNET_SUPPORTED_TOKEN_TYPES = [ + TokenType.collateral, + TokenType.native, + TokenType.synthetic, +] as const; + +type StarknetSupportedTokenTypes = + (typeof STARKNET_SUPPORTED_TOKEN_TYPES)[number]; + +export const STARKNET_TOKEN_TYPE_TO_STANDARD: Record< + StarknetSupportedTokenTypes, + TokenStandard +> = { + [TokenType.collateral]: TokenStandard.StarknetHypCollateral, + [TokenType.native]: TokenStandard.StarknetHypNative, + [TokenType.synthetic]: TokenStandard.StarknetHypSynthetic, +}; + export const PROTOCOL_TO_NATIVE_STANDARD: Record = { [ProtocolType.Ethereum]: TokenStandard.EvmNative, [ProtocolType.Cosmos]: TokenStandard.CosmosNative, [ProtocolType.Sealevel]: TokenStandard.SealevelNative, - [ProtocolType.Starknet]: TokenStandard.EvmNative, // TODO: define starknet token types based on cairo contracts + [ProtocolType.Starknet]: TokenStandard.StarknetHypNative, }; From 72807763f22fecf73b04764e4d20d16283a3aaf5 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 17 Dec 2024 18:12:49 +0330 Subject: [PATCH 101/132] feat: fullyConnectTokens after all protocol tokens deployed --- typescript/cli/src/deploy/warp.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index f7e428128e4..f8ad34a2069 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -208,6 +208,7 @@ export async function runWarpRouteDeploy({ throw new Error(`Unsupported protocol type: ${protocol}`); } } + fullyConnectTokens(deployments, context.multiProvider); await writeDeploymentArtifacts(deployments, context); } From 2af6c5154b0743581af43f5dd3c2b353dfee3736 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:09:00 +0330 Subject: [PATCH 102/132] feat: starknet core, with send functionalities --- typescript/sdk/src/core/StarknetCore.ts | 116 ++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 typescript/sdk/src/core/StarknetCore.ts diff --git a/typescript/sdk/src/core/StarknetCore.ts b/typescript/sdk/src/core/StarknetCore.ts new file mode 100644 index 00000000000..59ad0400f7f --- /dev/null +++ b/typescript/sdk/src/core/StarknetCore.ts @@ -0,0 +1,116 @@ +import { Account, CairoOption, CairoOptionVariant, Contract } from 'starknet'; + +import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; +import { rootLogger } from '@hyperlane-xyz/utils'; + +export class StarknetCore { + protected logger = rootLogger.child({ module: 'StarknetCore' }); + protected signer: Account; + + constructor(signer: Account) { + this.signer = signer; + } + + /** + * Convert a byte array to a starknet message + * Pads the bytes to 16 bytes chunks + * @param bytes Input byte array + * @returns Object containing size and padded data array + */ + static toStarknetMessageBytes(bytes: Uint8Array): { + size: number; + data: bigint[]; + } { + // Calculate the required padding + const padding = (16 - (bytes.length % 16)) % 16; + const totalLen = bytes.length + padding; + + // Create a new byte array with the necessary padding + const paddedBytes = new Uint8Array(totalLen); + paddedBytes.set(bytes); + // Padding remains as zeros by default in Uint8Array + + // Convert to chunks of 16 bytes + const result: bigint[] = []; + for (let i = 0; i < totalLen; i += 16) { + const chunk = paddedBytes.slice(i, i + 16); + // Convert chunk to bigint (equivalent to u128 in Rust) + const value = BigInt('0x' + Buffer.from(chunk).toString('hex')); + result.push(value); + } + + return { + size: bytes.length, + data: result, + }; + } + + async sendMessage(params: { + destinationDomain: number; + recipientAddress: string; + messageBody: string; + }): Promise<{ txHash: string }> { + const { abi } = getCompiledContract('mailbox'); + const mailboxContract = new Contract( + abi, + '0x00581bb8ad9e4ecd0ba06793e2ffb26f4b12ea18ec69dfb216738efe569e2e59', + this.signer, + ); + // Convert messageBody to Bytes struct format + const messageBodyBytes = StarknetCore.toStarknetMessageBytes( + new TextEncoder().encode(params.messageBody), + ); + console.log({ + messageBodyBytes, + encoded: new TextEncoder().encode(params.messageBody), + }); + + const nonOption = new CairoOption(CairoOptionVariant.None); + + // Quote the dispatch first to ensure enough fees are provided + const quote = await mailboxContract.call('quote_dispatch', [ + params.destinationDomain, + params.recipientAddress, + messageBodyBytes, + nonOption, + nonOption, + ]); + + // Dispatch the message + const { transaction_hash } = await mailboxContract.invoke('dispatch', [ + params.destinationDomain, + params.recipientAddress, + messageBodyBytes, + BigInt(quote.toString()), //fee amount + nonOption, + nonOption, + ]); + + this.logger.info(`Message sent with transaction hash: ${transaction_hash}`); + + return { + txHash: transaction_hash, + }; + } + + async quoteDispatch(params: { + destinationDomain: number; + recipientAddress: string; + messageBody: string; + customHookMetadata?: string; + customHook?: string; + }): Promise { + const { abi } = getCompiledContract('mailbox'); + const mailboxContract = new Contract(abi, 'mailbox_address', this.signer); + + const quote = await mailboxContract.call('quote_dispatch', [ + params.destinationDomain, + params.recipientAddress, + params.messageBody, + params.customHookMetadata || '', + params.customHook || '', + ]); + + return quote.toString(); + } +} From 64681bf6d1f7140bea480942fda3633171c13f49 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:35:15 +0330 Subject: [PATCH 103/132] fix: update starknet core version, use release that include compile files --- starknet/package.json | 2 +- typescript/sdk/package.json | 2 +- yarn.lock | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/starknet/package.json b/starknet/package.json index efba77662ca..fcf70ae4264 100644 --- a/starknet/package.json +++ b/starknet/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/starknet-core", "description": "Core cairo contracts for Hyperlane", - "version": "0.2.1", + "version": "0.2.2", "type": "module", "homepage": "https://www.hyperlane.xyz", "license": "Apache-2.0", diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index adc6b8a577c..b7b5ae5d85b 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -10,7 +10,7 @@ "@cosmjs/stargate": "^0.32.4", "@google-cloud/storage": "7.14.0", "@hyperlane-xyz/core": "5.8.3", - "@hyperlane-xyz/starknet-core": "0.2.1", + "@hyperlane-xyz/starknet-core": "0.2.2", "@hyperlane-xyz/utils": "7.3.0", "@safe-global/api-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0", diff --git a/yarn.lock b/yarn.lock index e9247ff162a..e1bec9df137 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7648,7 +7648,7 @@ __metadata: "@eslint/js": "npm:^9.15.0" "@google-cloud/storage": "npm:7.14.0" "@hyperlane-xyz/core": "npm:5.8.3" - "@hyperlane-xyz/starknet-core": "npm:0.2.1" + "@hyperlane-xyz/starknet-core": "npm:0.2.2" "@hyperlane-xyz/utils": "npm:7.3.0" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" @@ -7693,7 +7693,7 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/starknet-core@npm:0.2.1, @hyperlane-xyz/starknet-core@workspace:starknet": +"@hyperlane-xyz/starknet-core@npm:0.2.2, @hyperlane-xyz/starknet-core@workspace:starknet": version: 0.0.0-use.local resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" dependencies: From 638369674f6b5b76f5a5dcad4e1784fd196381b8 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Wed, 18 Dec 2024 16:43:04 +0100 Subject: [PATCH 104/132] feat: sending test message on starknet --- typescript/cli/src/commands/send.ts | 2 + typescript/cli/src/context/context.ts | 3 +- typescript/cli/src/context/types.ts | 5 +- typescript/cli/src/deploy/utils.ts | 17 +++-- typescript/cli/src/send/message.ts | 60 +++++++++++++++-- typescript/sdk/src/core/StarknetCore.ts | 64 +++++++++++++++++-- typescript/sdk/src/core/StarknetCoreModule.ts | 6 ++ typescript/sdk/src/index.ts | 1 + 8 files changed, 140 insertions(+), 18 deletions(-) diff --git a/typescript/cli/src/commands/send.ts b/typescript/cli/src/commands/send.ts index 1167b3b5599..6b6ee03bd8c 100644 --- a/typescript/cli/src/commands/send.ts +++ b/typescript/cli/src/commands/send.ts @@ -74,6 +74,7 @@ const messageCommand: CommandModuleWithWriteContext< quick, relay, body, + multiProtocolSigner, }) => { await sendTestMessage({ context, @@ -83,6 +84,7 @@ const messageCommand: CommandModuleWithWriteContext< timeoutSec: timeout, skipWaitForDelivery: quick, selfRelay: relay, + multiProtocolSigner, }); process.exit(0); }, diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 9dfb8e94ff9..a32ea8d87d5 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -63,7 +63,7 @@ export async function signerMiddleware(argv: Record) { argv.context; const multiProtocolProvider = new MultiProtocolProvider(chainMetadata); - argv.multiProtocolProvider = multiProtocolProvider; + argv.context.multiProtocolProvider = multiProtocolProvider; if (!requiresKey) return argv; const strategyConfig = await safeReadChainSubmissionStrategyConfig( @@ -96,6 +96,7 @@ export async function signerMiddleware(argv: Record) { */ argv.multiProvider = await multiProtocolSigner.getMultiProvider(); argv.multiProtocolSigner = multiProtocolSigner; + argv.context.multiProtocolSigner = multiProtocolSigner; return argv; } diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 2059371ab65..3ea9c747276 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -5,6 +5,7 @@ import type { IRegistry } from '@hyperlane-xyz/registry'; import type { ChainMap, ChainMetadata, + MultiProtocolProvider, MultiProvider, WarpCoreConfig, } from '@hyperlane-xyz/sdk'; @@ -25,8 +26,10 @@ export interface ContextSettings { export interface CommandContext { registry: IRegistry; chainMetadata: ChainMap; - multiProvider: MultiProvider; skipConfirmation: boolean; + multiProvider: MultiProvider; + multiProtocolProvider?: MultiProtocolProvider; + multiProtocolSigner?: MultiProtocolSignerManager; key?: string; // just for evm chains backward compatibility signerAddress?: string; diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index f5ac01a1752..e7b89dd8163 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -47,11 +47,18 @@ export async function runPreflightChecksForChains({ for (const chain of chains) { const metadata = multiProvider.tryGetChainMetadata(chain); if (!metadata) throw new Error(`No chain config found for ${chain}`); - if (metadata.protocol !== ProtocolType.Ethereum) - throw new Error('Only Ethereum chains are supported for now'); - const signer = multiProvider.getSigner(chain); - assertSigner(signer); - logGreen(`✅ ${chain} signer is valid`); + if ( + metadata.protocol !== ProtocolType.Ethereum && + metadata.protocol !== ProtocolType.Starknet + ) + throw new Error( + 'Only Ethereum and Starknet chains are supported for now', + ); + if (metadata.protocol === ProtocolType.Ethereum) { + const signer = multiProvider.getSigner(chain); + assertSigner(signer); + logGreen(`✅ ${chain} signer is valid`); + } } logGreen('✅ Chains are valid'); diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index f7d5eb4e151..4cb71faaac2 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -1,9 +1,20 @@ import { stringify as yamlStringify } from 'yaml'; -import { ChainName, HyperlaneCore, HyperlaneRelayer } from '@hyperlane-xyz/sdk'; -import { addressToBytes32, timeout } from '@hyperlane-xyz/utils'; +import { + ChainName, + HyperlaneCore, + HyperlaneRelayer, + StarknetCore, +} from '@hyperlane-xyz/sdk'; +import { + ProtocolType, + addressToBytes32, + assert, + timeout, +} from '@hyperlane-xyz/utils'; import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; +import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext, WriteCommandContext } from '../context/types.js'; import { runPreflightChecksForChains } from '../deploy/utils.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; @@ -19,6 +30,7 @@ export async function sendTestMessage({ timeoutSec, skipWaitForDelivery, selfRelay, + multiProtocolSigner, }: { context: WriteCommandContext; origin?: ChainName; @@ -27,6 +39,7 @@ export async function sendTestMessage({ timeoutSec: number; skipWaitForDelivery: boolean; selfRelay?: boolean; + multiProtocolSigner?: MultiProtocolSignerManager; }) { const { chainMetadata } = context; @@ -59,6 +72,7 @@ export async function sendTestMessage({ messageBody, skipWaitForDelivery, selfRelay, + multiProtocolSigner, }), timeoutSec * 1000, 'Timed out waiting for messages to be delivered', @@ -72,6 +86,7 @@ async function executeDelivery({ messageBody, skipWaitForDelivery, selfRelay, + multiProtocolSigner, }: { context: CommandContext; origin: ChainName; @@ -79,19 +94,55 @@ async function executeDelivery({ messageBody: string; skipWaitForDelivery: boolean; selfRelay?: boolean; + multiProtocolSigner?: MultiProtocolSignerManager; }) { const { registry, multiProvider } = context; const chainAddresses = await registry.getAddresses(); - const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); + const destinationDomain = multiProvider.getDomainId(destination); + const originProtocol = multiProvider.getProtocol(origin); + + if (originProtocol === ProtocolType.Starknet) { + assert(multiProtocolSigner, 'MultiProtocolSignerManager is required'); + + const starknetSigner = await multiProtocolSigner.getStarknetSigner(origin); + assert(starknetSigner, `Signer for ${origin} chain is required`); + + const starknet = new StarknetCore( + starknetSigner, + chainAddresses, + multiProvider, + ); + const { messages } = await starknet.sendMessage({ + origin, + destinationDomain, + messageBody, + recipientAddress: chainAddresses[destination].testRecipient, + }); + + console.log({ messages }); + if (messages.length > 0) { + const message = messages[0]; + logBlue( + `Sent message from ${origin} to ${chainAddresses[destination].testRecipient} on ${destination}.`, + ); + logBlue(`Message ID: ${message.id}`); + log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`); + } + + return; + } try { + const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); + const recipient = chainAddresses[destination].testRecipient; + if (!recipient) { throw new Error(`Unable to find TestRecipient for ${destination}`); } + const formattedRecipient = addressToBytes32(recipient); - log('Dispatching message'); const { dispatchTx, message } = await core.sendMessage( origin, destination, @@ -121,6 +172,7 @@ async function executeDelivery({ log('Waiting for message delivery on destination chain...'); // Max wait 10 minutes + return; await core.waitForMessageProcessed(dispatchTx, 10000, 60); logGreen('Message was delivered!'); } diff --git a/typescript/sdk/src/core/StarknetCore.ts b/typescript/sdk/src/core/StarknetCore.ts index 59ad0400f7f..532c16eb50c 100644 --- a/typescript/sdk/src/core/StarknetCore.ts +++ b/typescript/sdk/src/core/StarknetCore.ts @@ -1,14 +1,27 @@ import { Account, CairoOption, CairoOptionVariant, Contract } from 'starknet'; +import { + ChainName, + HyperlaneAddressesMap, + MultiProvider, +} from '@hyperlane-xyz/sdk'; import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; import { rootLogger } from '@hyperlane-xyz/utils'; export class StarknetCore { protected logger = rootLogger.child({ module: 'StarknetCore' }); protected signer: Account; - - constructor(signer: Account) { + protected addressesMap: HyperlaneAddressesMap; + protected multiProvider: MultiProvider; + + constructor( + signer: Account, + addressesMap: HyperlaneAddressesMap, + multiProvider: MultiProvider, + ) { this.signer = signer; + this.addressesMap = addressesMap; + this.multiProvider = multiProvider; } /** @@ -45,17 +58,23 @@ export class StarknetCore { }; } + static getMailboxContract(address: string, signer: Account): Contract { + const { abi } = getCompiledContract('mailbox'); + return new Contract(abi, address, signer); + } + async sendMessage(params: { + origin: ChainName; destinationDomain: number; recipientAddress: string; messageBody: string; - }): Promise<{ txHash: string }> { - const { abi } = getCompiledContract('mailbox'); - const mailboxContract = new Contract( - abi, - '0x00581bb8ad9e4ecd0ba06793e2ffb26f4b12ea18ec69dfb216738efe569e2e59', + }): Promise<{ txHash: string; messages: any[] }> { + const mailboxAddress = this.addressesMap[params.origin].mailbox; + const mailboxContract = StarknetCore.getMailboxContract( + mailboxAddress, this.signer, ); + // Convert messageBody to Bytes struct format const messageBodyBytes = StarknetCore.toStarknetMessageBytes( new TextEncoder().encode(params.messageBody), @@ -88,8 +107,12 @@ export class StarknetCore { this.logger.info(`Message sent with transaction hash: ${transaction_hash}`); + const receipt = await this.signer.waitForTransaction(transaction_hash); + const parsedEvents = mailboxContract.parseEvents(receipt); + return { txHash: transaction_hash, + messages: this.getDispatchedMessages(parsedEvents), }; } @@ -113,4 +136,31 @@ export class StarknetCore { return quote.toString(); } + + getDispatchedMessages(parsedEvents: any): any { + return parsedEvents + .filter((event: any) => 'contracts::mailbox::mailbox::Dispatch' in event) + .map((event: any) => { + const dispatchEvent = event['contracts::mailbox::mailbox::Dispatch']; + const message = dispatchEvent.message; + + const originChain = + this.multiProvider.tryGetChainName(message.origin) ?? undefined; + const destinationChain = + this.multiProvider.tryGetChainName(message.destination) ?? undefined; + + // Convert the message to the expected format + const messageString = { + version: Number(message.version), + nonce: Number(message.nonce), + origin: originChain, + sender: message.sender.toString(), + destination: destinationChain, + recipient: message.recipient.toString(), + body: Array.from(message.body.data).map((n: any) => n.toString()), + }; + + return messageString; + }); + } } diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 913dd5d0e9b..2c7eca2e5b9 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -76,6 +76,11 @@ export class StarknetCoreModule { [mailboxContract.address, config.owner], ); + const testRecipient = await this.deployer.deployContract( + 'message_recipient', + [defaultIsm || noopIsm], + ); + return { noopIsm, defaultHook, @@ -84,6 +89,7 @@ export class StarknetCoreModule { mailbox: mailboxContract.address, merkleTreeHook: requiredHook || '', validatorAnnounce, + testRecipient, }; } diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 4d85ee2c2bd..0760ba96527 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -635,3 +635,4 @@ export { MailboxClientConfigSchema as mailboxClientConfigSchema } from './router export { StarknetCoreModule } from './core/StarknetCoreModule.js'; export { StarknetERC20WarpModule } from './token/StarknetERC20WarpModule.js'; +export { StarknetCore } from './core/StarknetCore.js'; From 7ddef193a6648e50c663d92642fa6e1ecb3489ac Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 19 Dec 2024 10:47:11 +0100 Subject: [PATCH 105/132] fix: remove premature return to allow message delivery tracking --- typescript/cli/src/send/message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index 4cb71faaac2..2639466a74a 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -143,6 +143,7 @@ async function executeDelivery({ const formattedRecipient = addressToBytes32(recipient); + log('Dispatching message'); const { dispatchTx, message } = await core.sendMessage( origin, destination, @@ -172,7 +173,6 @@ async function executeDelivery({ log('Waiting for message delivery on destination chain...'); // Max wait 10 minutes - return; await core.waitForMessageProcessed(dispatchTx, 10000, 60); logGreen('Message was delivered!'); } From 78dfba2885dd578dd39786ce8602b952cc7f9f3b Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 19 Dec 2024 10:56:56 +0100 Subject: [PATCH 106/132] refactor: move multiProtocolSigner to context and cleanup message sending flow --- typescript/cli/src/commands/send.ts | 2 -- typescript/cli/src/context/context.ts | 2 +- typescript/cli/src/context/types.ts | 6 +++--- typescript/cli/src/send/message.ts | 13 ++----------- typescript/sdk/src/core/StarknetCore.ts | 5 +++-- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/typescript/cli/src/commands/send.ts b/typescript/cli/src/commands/send.ts index 6b6ee03bd8c..1167b3b5599 100644 --- a/typescript/cli/src/commands/send.ts +++ b/typescript/cli/src/commands/send.ts @@ -74,7 +74,6 @@ const messageCommand: CommandModuleWithWriteContext< quick, relay, body, - multiProtocolSigner, }) => { await sendTestMessage({ context, @@ -84,7 +83,6 @@ const messageCommand: CommandModuleWithWriteContext< timeoutSec: timeout, skipWaitForDelivery: quick, selfRelay: relay, - multiProtocolSigner, }); process.exit(0); }, diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index a32ea8d87d5..15fb3719eb4 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -95,7 +95,7 @@ export async function signerMiddleware(argv: Record) { * @notice Attaches signers to MultiProvider and assigns it to argv.multiProvider */ argv.multiProvider = await multiProtocolSigner.getMultiProvider(); - argv.multiProtocolSigner = multiProtocolSigner; + argv.multiProtocolSigner = multiProtocolSigner; // TODO: remove this line after making sure `argv.context.multiProtocolSigner` is working properly argv.context.multiProtocolSigner = multiProtocolSigner; return argv; diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 3ea9c747276..0a69ff8b4c1 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -26,15 +26,15 @@ export interface ContextSettings { export interface CommandContext { registry: IRegistry; chainMetadata: ChainMap; - skipConfirmation: boolean; multiProvider: MultiProvider; - multiProtocolProvider?: MultiProtocolProvider; - multiProtocolSigner?: MultiProtocolSignerManager; + skipConfirmation: boolean; key?: string; // just for evm chains backward compatibility signerAddress?: string; warpCoreConfig?: WarpCoreConfig; strategyPath?: string; + multiProtocolProvider?: MultiProtocolProvider; + multiProtocolSigner?: MultiProtocolSignerManager; } export interface WriteCommandContext extends CommandContext { diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index 2639466a74a..ca0d71abc65 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -14,7 +14,6 @@ import { } from '@hyperlane-xyz/utils'; import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; -import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext, WriteCommandContext } from '../context/types.js'; import { runPreflightChecksForChains } from '../deploy/utils.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; @@ -30,7 +29,6 @@ export async function sendTestMessage({ timeoutSec, skipWaitForDelivery, selfRelay, - multiProtocolSigner, }: { context: WriteCommandContext; origin?: ChainName; @@ -39,7 +37,6 @@ export async function sendTestMessage({ timeoutSec: number; skipWaitForDelivery: boolean; selfRelay?: boolean; - multiProtocolSigner?: MultiProtocolSignerManager; }) { const { chainMetadata } = context; @@ -72,7 +69,6 @@ export async function sendTestMessage({ messageBody, skipWaitForDelivery, selfRelay, - multiProtocolSigner, }), timeoutSec * 1000, 'Timed out waiting for messages to be delivered', @@ -86,7 +82,6 @@ async function executeDelivery({ messageBody, skipWaitForDelivery, selfRelay, - multiProtocolSigner, }: { context: CommandContext; origin: ChainName; @@ -94,10 +89,10 @@ async function executeDelivery({ messageBody: string; skipWaitForDelivery: boolean; selfRelay?: boolean; - multiProtocolSigner?: MultiProtocolSignerManager; }) { - const { registry, multiProvider } = context; + const { registry, multiProvider, multiProtocolSigner } = context; const chainAddresses = await registry.getAddresses(); + const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); const destinationDomain = multiProvider.getDomainId(destination); const originProtocol = multiProvider.getProtocol(origin); @@ -133,14 +128,10 @@ async function executeDelivery({ } try { - const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); - const recipient = chainAddresses[destination].testRecipient; - if (!recipient) { throw new Error(`Unable to find TestRecipient for ${destination}`); } - const formattedRecipient = addressToBytes32(recipient); log('Dispatching message'); diff --git a/typescript/sdk/src/core/StarknetCore.ts b/typescript/sdk/src/core/StarknetCore.ts index 532c16eb50c..6e0bd7b3597 100644 --- a/typescript/sdk/src/core/StarknetCore.ts +++ b/typescript/sdk/src/core/StarknetCore.ts @@ -15,7 +15,7 @@ export class StarknetCore { protected multiProvider: MultiProvider; constructor( - signer: Account, + signer: Account, // Use MultiProtocolSignerManager instead addressesMap: HyperlaneAddressesMap, multiProvider: MultiProvider, ) { @@ -150,6 +150,7 @@ export class StarknetCore { this.multiProvider.tryGetChainName(message.destination) ?? undefined; // Convert the message to the expected format + // TODO: stringify the message body const messageString = { version: Number(message.version), nonce: Number(message.nonce), @@ -157,7 +158,7 @@ export class StarknetCore { sender: message.sender.toString(), destination: destinationChain, recipient: message.recipient.toString(), - body: Array.from(message.body.data).map((n: any) => n.toString()), + body: Array.from(message.body.data).map((n: any) => n.toString()), // TODO: causes stringify error }; return messageString; From 0d4d3e21b9be7b96e9dba3abdcf7e08480d58397 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:07:56 +0330 Subject: [PATCH 107/132] fix: update starknet native token warp deployment with latest cairo contracts change --- .../sdk/src/token/StarknetERC20WarpModule.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index 9553bc28385..71c2148bfea 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -71,12 +71,18 @@ export class StarknetERC20WarpModule { break; } case TokenType.native: { - const tokenAddress = await deployer.deployContract('HypNative', { - mailbox: mailbox, - hook: getChecksumAddress(0), - interchain_security_module: ismAddress, - owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner - }); + const tokenAddress = await deployer.deployContract( + 'HypNative', + { + mailbox: mailbox, + native_token: + '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', // ETH address on Starknet chains + hook: getChecksumAddress(0), + interchain_security_module: ismAddress, + owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner + }, + ContractType.TOKEN, + ); addresses[chain] = tokenAddress; break; } From 58999271c4dcc7d33f94c4d187f228ba571e7ee3 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:28:27 +0330 Subject: [PATCH 108/132] feat(agent-config): add Starknet chain support for start block fetching --- typescript/cli/src/config/agent.ts | 94 ++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/typescript/cli/src/config/agent.ts b/typescript/cli/src/config/agent.ts index a176a6f040e..b5536844dd5 100644 --- a/typescript/cli/src/config/agent.ts +++ b/typescript/cli/src/config/agent.ts @@ -6,11 +6,12 @@ import { AgentConfig, AgentConfigSchema, ChainMap, + ChainMetadata, HyperlaneCore, HyperlaneDeploymentArtifacts, buildAgentConfig, } from '@hyperlane-xyz/sdk'; -import { objMap, pick, promiseObjAll } from '@hyperlane-xyz/utils'; +import { assert, objMap, pick, promiseObjAll } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, logBlue, logGreen, warnYellow } from '../logger.js'; @@ -36,8 +37,46 @@ export async function createAgentConfig({ const chainAddresses = filterChainAddresses(addresses, chains); - const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); - const startBlocks = await getStartBlocks(chainAddresses, core, chainMetadata); + // Categorize chains by protocol + const chainsByProtocol: Record = { + ethereum: [], + starknet: [], + }; + + Object.keys(chainMetadata).forEach((chain) => { + const protocol = chainMetadata[chain].protocol; + if (protocol === 'starknet') { + chainsByProtocol.starknet.push(chain); + } else { + chainsByProtocol.ethereum.push(chain); + } + }); + + // Initialize core for Ethereum chains + const ethereumChainAddresses = pick( + chainAddresses, + chainsByProtocol.ethereum, + ); + const core = HyperlaneCore.fromAddressesMap( + ethereumChainAddresses, + multiProvider, + ); + const ethereumStartBlocks = await getStartBlocks( + ethereumChainAddresses, + core, + chainMetadata, + ); + + const starknetStartBlocks = await getStartBlocksForStarknetChains( + chainsByProtocol.starknet, + pick(chainAddresses, chainsByProtocol.starknet), + chainMetadata, + ); + + let startBlocks: ChainMap = { + ...ethereumStartBlocks, + ...starknetStartBlocks, + }; await handleMissingInterchainGasPaymaster(chainAddresses, skipConfirmation); @@ -83,10 +122,11 @@ function filterChainAddresses( return pick(addresses, chains); } +// handle starknet async function getStartBlocks( chainAddresses: ChainMap, core: HyperlaneCore, - chainMetadata: any, + chainMetadata: ChainMap, ): Promise> { return promiseObjAll( objMap(chainAddresses, async (chain, _) => { @@ -155,3 +195,49 @@ async function validateAgentConfig( logGreen('✅ Agent config successfully created'); } } + +async function getStartBlocksForStarknetChains( + starknetChains: string[], + starknetChainAddresses: ChainMap, + chainMetadata: ChainMap, +): Promise> { + const startBlocks: ChainMap = {}; + + for (const chain of starknetChains) { + try { + // Assert chain data and explorer existence + const chainData = chainMetadata[chain]; + assert(chainData, `No chain metadata found for ${chain}`); + assert(chainData.blockExplorers?.[0], `No explorer found for ${chain}`); + + // Assert mailbox address existence + const mailboxAddress = starknetChainAddresses[chain]?.mailbox; + assert(mailboxAddress, `No mailbox address found for ${chain}`); + + const explorer = chainData.blockExplorers[0]; + const response = await fetch( + `${explorer.apiUrl}/contract/${mailboxAddress}`, + ); + + // Assert response status + assert( + response.ok, + `API request failed for ${chain}: ${response.statusText}`, + ); + + const data = await response.json(); + // Assert block number existence and type + assert( + typeof data.blockNumber === 'number', + `Invalid block number format for ${chain}`, + ); + + startBlocks[chain] = data.blockNumber; + } catch (error) { + console.error(`Failed to fetch start block for ${chain}:`, error); + startBlocks[chain] = undefined; + } + } + + return startBlocks; +} From 7c59375983d09b050c340226384c60afab19b5fe Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:23:14 +0330 Subject: [PATCH 109/132] fix: replace hardcoded starknet protocol fee and protocol fee max amd read from config --- typescript/sdk/src/core/StarknetCoreModule.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 2c7eca2e5b9..242e43a68ec 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -1,3 +1,4 @@ +import { BigNumber } from 'ethers'; import { Account, Contract } from 'starknet'; import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; @@ -44,20 +45,18 @@ export class StarknetCoreModule { config.requiredHook.type === HookType.PROTOCOL_FEE, 'only protocolFee hook is accepted for required hook', ); + const noopIsm = await this.deployer.deployContract('noop_ism', []); const defaultHook = await this.deployer.deployContract('hook', []); const protocolFee = await this.deployer.deployContract('protocol_fee', [ - '1000000000000000000', - '0', - '10000000000000000', - '0', + BigNumber.from(config.requiredHook.maxProtocolFee), + BigNumber.from(config.requiredHook.protocolFee), config.requiredHook.beneficiary, config.owner, '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', // ETH address on Starknet chains ]); - const mailboxContract = await this.deployMailbox( config.owner, noopIsm, From f17180f021b1e2c87806989954fc139b9204aa94 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 26 Dec 2024 12:26:24 +0330 Subject: [PATCH 110/132] feat: cross chain enrollment, enroll for evm chains --- typescript/cli/src/deploy/warp.ts | 161 +++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 2 deletions(-) diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index f8ad34a2069..957accff97c 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -145,9 +145,18 @@ export async function runWarpRouteDeploy({ const chainsByProtocol = groupChainsByProtocol(chains, multiProvider); const deployments: WarpCoreConfig = { tokens: [] }; + const routerAddresses: { + evm: ChainMap
; + starknet: ChainMap
; + } = { + evm: {}, + starknet: {}, + }; + // Execute deployments for each protocol for (const protocol of Object.keys(chainsByProtocol) as ProtocolType[]) { const protocolChains = chainsByProtocol[protocol]; + switch (protocol) { case ProtocolType.Ethereum: { @@ -162,6 +171,12 @@ export async function runWarpRouteDeploy({ apiKeys, ); + // Store EVM router addresses + routerAddresses.evm = objMap( + deployedContracts as HyperlaneContractsMap, + (_, contracts) => getRouter(contracts).address, + ); + const warpCoreConfig = await getWarpCoreConfig( { context, warpDeployConfig: warpRouteConfig }, deployedContracts, @@ -191,7 +206,7 @@ export async function runWarpRouteDeploy({ {}, ), ); - const addresses = await executeStarknetDeployments({ + routerAddresses.starknet = await executeStarknetDeployments({ starknetSigners, warpRouteConfig, multiProvider, @@ -199,7 +214,7 @@ export async function runWarpRouteDeploy({ const warpCoreConfig = await getWarpCoreConfigForStarknet( warpRouteConfig, multiProvider, - addresses, + routerAddresses.starknet, ); deployments.tokens = [...deployments.tokens, ...warpCoreConfig.tokens]; break; @@ -208,6 +223,16 @@ export async function runWarpRouteDeploy({ throw new Error(`Unsupported protocol type: ${protocol}`); } } + + await enrollCrossChainRouters({ + evmAddresses: routerAddresses.evm, + starknetAddresses: routerAddresses.starknet, + context, + warpRouteConfig, + deployments, + multiProvider, + }); + fullyConnectTokens(deployments, context.multiProvider); await writeDeploymentArtifacts(deployments, context); } @@ -1251,3 +1276,135 @@ function validateStarknetWarpConfig(warpRouteConfig: WarpRouteDeployConfig) { ); } } + +async function enrollRemoteRoutersOfStarknetOnEvm( + multiProvider: MultiProvider, + evmChains: ChainName[], + evmRouterAddresses: ChainMap
, + starknetDeployedAddresses: ChainMap
, + registryAddresses: ChainMap, +): Promise { + const transactions: AnnotatedEV5Transaction[] = []; + + await promiseObjAll( + objMap(evmRouterAddresses, async (evmChain, evmRouterAddress) => { + if (!evmChains.includes(evmChain)) return; + + await retryAsync(async () => { + // Create warp route reader for the EVM chain + const warpRouteReader = new EvmERC20WarpRouteReader( + multiProvider, + evmChain, + ); + + // Get current config from the deployed router + const mutatedWarpRouteConfig = + await warpRouteReader.deriveWarpRouteConfig(evmRouterAddress); + + // Filter for only Starknet chains + const starknetChains = Object.keys(starknetDeployedAddresses).filter( + (chain) => + multiProvider.getChainMetadata(chain).protocol === + ProtocolType.Starknet, + ); + + // Add Starknet routers to the config + mutatedWarpRouteConfig.remoteRouters = + starknetChains.reduce( + (remoteRouters, starknetChain) => { + remoteRouters[multiProvider.getDomainId(starknetChain)] = { + address: starknetDeployedAddresses[starknetChain], + }; + return remoteRouters; + }, + {}, + ); + + const { + domainRoutingIsmFactory, + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + } = registryAddresses[evmChain]; + + // Create warp module to update the router + const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { + config: mutatedWarpRouteConfig, + chain: evmChain, + addresses: { + deployedTokenRoute: evmRouterAddress, + domainRoutingIsmFactory, + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + }, + }); + + // Generate transactions to update the router + const chainTxs = await evmERC20WarpModule.update( + mutatedWarpRouteConfig, + ); + if (chainTxs.length > 0) { + transactions.push(...chainTxs); + } + }); + }), + ); + + return transactions; +} + +interface EnrollRoutersParams { + evmAddresses: ChainMap
; + starknetAddresses: ChainMap
; + context: WriteCommandContext; + warpRouteConfig: WarpRouteDeployConfig; + deployments: WarpCoreConfig; + multiProvider: MultiProvider; +} + +async function enrollCrossChainRouters({ + evmAddresses, + starknetAddresses, + context, + warpRouteConfig, + deployments, + multiProvider, +}: EnrollRoutersParams): Promise { + const hasEvmChains = Object.keys(evmAddresses).length > 0; + const hasStarknetChains = Object.keys(starknetAddresses).length > 0; + + if (!hasEvmChains || !hasStarknetChains) return; + + logBlue('Enrolling Starknet routers with EVM chains...'); + + const registryAddresses = await context.registry.getAddresses(); + const evmChains = Object.keys(evmAddresses); + + const enrollmentTxs = await enrollRemoteRoutersOfStarknetOnEvm( + multiProvider, + evmChains, + evmAddresses, + starknetAddresses, + registryAddresses, + ); + + if (enrollmentTxs.length === 0) return; + + const chainTransactions = groupBy(enrollmentTxs, 'chainId'); + await submitWarpApplyTransactions( + { + context, + warpDeployConfig: warpRouteConfig, + warpCoreConfig: deployments, + receiptsDir: './generated/transactions', + }, + chainTransactions, + ); +} From 739912d154e93dc8c97bb9cb8c2e1bcfc8cbaefe Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 26 Dec 2024 14:24:20 +0330 Subject: [PATCH 111/132] feat: enroll routers on starknet chains --- typescript/cli/src/deploy/warp.ts | 24 ++++- .../sdk/src/token/StarknetERC20WarpModule.ts | 89 ++++++++++++++++++- 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 957accff97c..0b651ae2b1f 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -153,6 +153,8 @@ export async function runWarpRouteDeploy({ starknet: {}, }; + let starknetSigners: ChainMap = {}; + // Execute deployments for each protocol for (const protocol of Object.keys(chainsByProtocol) as ProtocolType[]) { const protocolChains = chainsByProtocol[protocol]; @@ -197,7 +199,7 @@ export async function runWarpRouteDeploy({ multiProtocolSigner, 'multi protocol signer is required for starknet chain deployment', ); - const starknetSigners = await promiseObjAll( + starknetSigners = await promiseObjAll( protocolChains.reduce>>( (acc, chain) => ({ ...acc, @@ -231,6 +233,7 @@ export async function runWarpRouteDeploy({ warpRouteConfig, deployments, multiProvider, + starknetSigners, }); fullyConnectTokens(deployments, context.multiProvider); @@ -1367,6 +1370,7 @@ interface EnrollRoutersParams { warpRouteConfig: WarpRouteDeployConfig; deployments: WarpCoreConfig; multiProvider: MultiProvider; + starknetSigners: ChainMap; } async function enrollCrossChainRouters({ @@ -1376,6 +1380,7 @@ async function enrollCrossChainRouters({ warpRouteConfig, deployments, multiProvider, + starknetSigners, }: EnrollRoutersParams): Promise { const hasEvmChains = Object.keys(evmAddresses).length > 0; const hasStarknetChains = Object.keys(starknetAddresses).length > 0; @@ -1387,7 +1392,18 @@ async function enrollCrossChainRouters({ const registryAddresses = await context.registry.getAddresses(); const evmChains = Object.keys(evmAddresses); - const enrollmentTxs = await enrollRemoteRoutersOfStarknetOnEvm( + const starknetWarpModule = new StarknetERC20WarpModule( + starknetSigners, + warpRouteConfig, + multiProvider, + ); + + await starknetWarpModule.enrollRemoteRouters({ + ...evmAddresses, + ...starknetAddresses, + }); + + const evmEnrollmentTxs = await enrollRemoteRoutersOfStarknetOnEvm( multiProvider, evmChains, evmAddresses, @@ -1395,9 +1411,9 @@ async function enrollCrossChainRouters({ registryAddresses, ); - if (enrollmentTxs.length === 0) return; + if (evmEnrollmentTxs.length === 0) return; - const chainTransactions = groupBy(enrollmentTxs, 'chainId'); + const chainTransactions = groupBy(evmEnrollmentTxs, 'chainId'); await submitWarpApplyTransactions( { context, diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index 71c2148bfea..46f3b2f6c24 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -1,4 +1,11 @@ -import { Account, byteArray, getChecksumAddress } from 'starknet'; +import { + Account, + Contract, + byteArray, + eth, + getChecksumAddress, + uint256, +} from 'starknet'; import { TokenType } from '@hyperlane-xyz/sdk'; import { ContractType } from '@hyperlane-xyz/starknet-core'; @@ -115,6 +122,9 @@ export class StarknetERC20WarpModule { throw Error('Token type is not supported on starknet'); } } + + // After all deployments are done, enroll the routers + await this.enrollRemoteRouters(addresses); return addresses; } @@ -137,4 +147,81 @@ export class StarknetERC20WarpModule { mailbox, }); } + + /** + * Enrolls remote routers for all Starknet chains using the deployed token addresses + * @param routerAddresses Map of chain name to token/router address + */ + public async enrollRemoteRouters( + routerAddresses: ChainMap, + ): Promise { + // Process only Starknet chains + for (const [chain, tokenAddress] of Object.entries(routerAddresses)) { + const isStarknetChain = + this.multiProvider.getChainMetadata(chain).protocol !== + ProtocolType.Starknet; + if (isStarknetChain) { + continue; + } + + const account = this.account[chain]; + + // Router ABI for enrollment + const ROUTER_ABI = [ + { + type: 'function', + name: 'enroll_remote_router', + inputs: [ + { + name: 'domain', + type: 'core::integer::u32', + }, + { + name: 'router', + type: 'core::integer::u256', + }, + ], + outputs: [], + state_mutability: 'external', + }, + ]; + + // Initialize contract with the deployed token address + const contract = new Contract(ROUTER_ABI, tokenAddress, account); + + // For each non-Starknet chain, enroll its router + for (const [remoteChain, remoteAddress] of Object.entries( + routerAddresses, + )) { + if (remoteChain === chain) continue; // Skip self-enrollment + + try { + const remoteDomain = this.multiProvider.getDomainId(remoteChain); + const remoteRouter = uint256.bnToUint256( + eth.validateAndParseEthAddress(remoteAddress), + ); + + this.logger.info( + `Enrolling remote router on ${chain} for domain ${remoteDomain} with address ${remoteAddress}`, + ); + + const tx = await contract.invoke('enroll_remote_router', [ + remoteDomain, + remoteRouter, + ]); + + await account.waitForTransaction(tx.transaction_hash); + + this.logger.info( + `Successfully enrolled remote router on ${chain}. Transaction: ${tx.transaction_hash}`, + ); + } catch (error) { + this.logger.error( + `Failed to enroll remote router on ${chain} for chain ${remoteChain}: ${error}`, + ); + throw error; + } + } + } + } } From ba8e1d359fa2f0fd21f20e97bf8c9a399e9d8a20 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 26 Dec 2024 18:35:31 +0330 Subject: [PATCH 112/132] improvement: use batch enrollment on starknet routers --- .../sdk/src/token/StarknetERC20WarpModule.ts | 86 ++++++++++--------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index 46f3b2f6c24..88ce5787a84 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -1,6 +1,7 @@ import { Account, Contract, + Uint256, byteArray, eth, getChecksumAddress, @@ -155,7 +156,6 @@ export class StarknetERC20WarpModule { public async enrollRemoteRouters( routerAddresses: ChainMap, ): Promise { - // Process only Starknet chains for (const [chain, tokenAddress] of Object.entries(routerAddresses)) { const isStarknetChain = this.multiProvider.getChainMetadata(chain).protocol !== @@ -166,19 +166,19 @@ export class StarknetERC20WarpModule { const account = this.account[chain]; - // Router ABI for enrollment + // Updated Router ABI to include batch enrollment const ROUTER_ABI = [ { type: 'function', - name: 'enroll_remote_router', + name: 'enroll_remote_routers', inputs: [ { - name: 'domain', - type: 'core::integer::u32', + name: 'domains', + type: 'core::array::Array::', }, { - name: 'router', - type: 'core::integer::u256', + name: 'routers', + type: 'core::array::Array::', }, ], outputs: [], @@ -186,41 +186,47 @@ export class StarknetERC20WarpModule { }, ]; - // Initialize contract with the deployed token address const contract = new Contract(ROUTER_ABI, tokenAddress, account); - // For each non-Starknet chain, enroll its router - for (const [remoteChain, remoteAddress] of Object.entries( - routerAddresses, - )) { - if (remoteChain === chain) continue; // Skip self-enrollment - - try { - const remoteDomain = this.multiProvider.getDomainId(remoteChain); - const remoteRouter = uint256.bnToUint256( - eth.validateAndParseEthAddress(remoteAddress), - ); - - this.logger.info( - `Enrolling remote router on ${chain} for domain ${remoteDomain} with address ${remoteAddress}`, - ); - - const tx = await contract.invoke('enroll_remote_router', [ - remoteDomain, - remoteRouter, - ]); - - await account.waitForTransaction(tx.transaction_hash); - - this.logger.info( - `Successfully enrolled remote router on ${chain}. Transaction: ${tx.transaction_hash}`, - ); - } catch (error) { - this.logger.error( - `Failed to enroll remote router on ${chain} for chain ${remoteChain}: ${error}`, - ); - throw error; - } + try { + // Prepare arrays for batch enrollment + const domains: number[] = []; + const routers: Uint256[] = []; + + // Collect all remote chains' data + Object.entries(routerAddresses).forEach( + ([remoteChain, remoteAddress]) => { + if (remoteChain === chain) return; // Skip self-enrollment + + const remoteDomain = this.multiProvider.getDomainId(remoteChain); + const remoteRouter = uint256.bnToUint256( + eth.validateAndParseEthAddress(remoteAddress), + ); + + domains.push(remoteDomain); + routers.push(remoteRouter); + }, + ); + + this.logger.info( + `Batch enrolling ${domains.length} remote routers on ${chain}`, + ); + + const tx = await contract.invoke('enroll_remote_routers', [ + domains, + routers, + ]); + + await account.waitForTransaction(tx.transaction_hash); + + this.logger.info( + `Successfully enrolled all remote routers on ${chain}. Transaction: ${tx.transaction_hash}`, + ); + } catch (error) { + this.logger.error( + `Failed to enroll remote routers on ${chain}: ${error}`, + ); + throw error; } } } From 645149c295acc1ebd6a033860d86080e2a1d934e Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 26 Dec 2024 19:48:56 +0330 Subject: [PATCH 113/132] fix(sdk): handle non-Ethereum addresses in StarknetERC20WarpModule --- typescript/sdk/src/token/StarknetERC20WarpModule.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index 88ce5787a84..482d400a734 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -199,8 +199,14 @@ export class StarknetERC20WarpModule { if (remoteChain === chain) return; // Skip self-enrollment const remoteDomain = this.multiProvider.getDomainId(remoteChain); + const remoteProtocol = + this.multiProvider.getChainMetadata(remoteChain).protocol; + + // Only validate and parse ETH address for Ethereum chains const remoteRouter = uint256.bnToUint256( - eth.validateAndParseEthAddress(remoteAddress), + remoteProtocol === ProtocolType.Ethereum + ? eth.validateAndParseEthAddress(remoteAddress) + : remoteAddress, ); domains.push(remoteDomain); From 9470d82379c95134799b2c5e627cda3775e227f7 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:12:44 +0330 Subject: [PATCH 114/132] docs: add explanatory comments to StarknetCoreModule deployment flow --- typescript/sdk/src/core/StarknetCoreModule.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 242e43a68ec..18f1dfa4f39 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -46,10 +46,14 @@ export class StarknetCoreModule { 'only protocolFee hook is accepted for required hook', ); + // Deploy core components in sequence: + // 1. NoopISM - A basic interchain security module that performs no validation const noopIsm = await this.deployer.deployContract('noop_ism', []); + // 2. Default Hook - A basic hook implementation for message processing const defaultHook = await this.deployer.deployContract('hook', []); + // 3. Protocol Fee Hook - Handles fee collection for cross-chain messages const protocolFee = await this.deployer.deployContract('protocol_fee', [ BigNumber.from(config.requiredHook.maxProtocolFee), BigNumber.from(config.requiredHook.protocolFee), @@ -57,6 +61,8 @@ export class StarknetCoreModule { config.owner, '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', // ETH address on Starknet chains ]); + + // 4. Deploy Mailbox with initial configuration const mailboxContract = await this.deployMailbox( config.owner, noopIsm, @@ -64,6 +70,7 @@ export class StarknetCoreModule { protocolFee, ); + // 5. Update the configuration with custom ISM and hooks if specified const { defaultIsm, requiredHook } = await this.update(config, { chain, mailboxContract, @@ -122,6 +129,7 @@ export class StarknetCoreModule { const actualConfig = await this.read(args.mailboxContract); + // Update ISM if specified in config if (expectedConfig.defaultIsm) { const defaultIsm = await this.deployer.deployIsm({ chain: args.chain.toString(), @@ -140,6 +148,7 @@ export class StarknetCoreModule { result.defaultIsm = defaultIsm; } + // Update required hook to MerkleTreeHook if specified if (expectedConfig.requiredHook) { const merkleTreeHook = await this.deployer.deployContract( 'merkle_tree_hook', @@ -160,6 +169,7 @@ export class StarknetCoreModule { result.requiredHook = merkleTreeHook; } + // Update owner if different from current if (expectedConfig.owner && actualConfig.owner !== expectedConfig.owner) { this.logger.trace(`Updating mailbox owner ${expectedConfig.owner}..`); const { transaction_hash: transferOwnershipTxHash } = From aa6a41c9a5e1b1a010dd53ffab38ebc68bae0bff Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:20:46 +0330 Subject: [PATCH 115/132] feat(starknet): add ownership clarification logs for MerkleTreeHook --- typescript/sdk/src/core/StarknetCoreModule.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 18f1dfa4f39..666a3b5805e 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -150,6 +150,11 @@ export class StarknetCoreModule { // Update required hook to MerkleTreeHook if specified if (expectedConfig.requiredHook) { + this.logger.info( + `Deploying MerkleTreeHook with explicit owner (${args.owner}). Note: Unlike EVM where deployer becomes owner, ` + + `in Starknet the owner is specified during construction.`, + ); + const merkleTreeHook = await this.deployer.deployContract( 'merkle_tree_hook', [args.mailboxContract.address, args.owner], From b7f8f9fa016ea22df13b011aa8c02ca1398df596 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:24:16 +0330 Subject: [PATCH 116/132] feat(starknet): add ownership clarification for ownable ISMs --- typescript/sdk/src/deploy/StarknetDeployer.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index a321a65ffa5..e4f5abd09a2 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -90,6 +90,21 @@ export class StarknetDeployer { const contractName = StarknetIsmContractName[ismType as SupportedIsmTypesOnStarknetType]; let constructorArgs: RawArgs | undefined; + + // Log ownership model difference for ownable ISMs + if ( + [ + IsmType.MERKLE_ROOT_MULTISIG, + IsmType.MESSAGE_ID_MULTISIG, + IsmType.AGGREGATION, + ].includes(ismType) + ) { + this.logger.info( + `Deploying ${ismType} with deployer (${this.account.address}) as initial owner. ` + + 'Note: Unlike EVM, this ISM type is ownable on Starknet and ownership can be transferred later.', + ); + } + switch (ismType) { case IsmType.MERKLE_ROOT_MULTISIG: case IsmType.MESSAGE_ID_MULTISIG: From 6f843cc35809a2bb380384043fb84ab98cb25468 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 6 Mar 2025 12:45:21 +0100 Subject: [PATCH 117/132] feat: Enhance warp deployment utilities --- typescript/cli/src/config/chain.ts | 12 +- typescript/cli/src/deploy/utils.ts | 1534 +++------------------------- typescript/cli/src/deploy/warp.ts | 91 +- 3 files changed, 208 insertions(+), 1429 deletions(-) diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index 2cf3f2d65be..0ded1db60f9 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -1,7 +1,10 @@ import { confirm, input, select } from '@inquirer/prompts'; import { ethers } from 'ethers'; import { keccak256 } from 'ethers/lib/utils.js'; -import { provider as starknetProvider } from 'starknet'; +import { + Provider as StarknetProvider, + provider as starknetProvider, +} from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { @@ -104,6 +107,7 @@ export async function createChainConfig({ const arbitrumNitroMetadata: Pick = {}; if (technicalStack === ChainTechnicalStack.ArbitrumNitro) { + const provider = new ethers.providers.JsonRpcProvider(rpcUrl); const indexFrom = await detectAndConfirmOrPrompt( async () => { return (await provider.getBlockNumber()).toString(); @@ -280,7 +284,7 @@ async function addNativeTokenConfig(metadata: ChainMetadata): Promise { message: 'Do you want to set native token properties for this chain config (defaults to ETH)', }); - let symbol, name, decimals; + let symbol, name, decimals, denom; if (wantNativeConfig) { symbol = await input({ message: "Enter the native token's symbol:", @@ -291,12 +295,16 @@ async function addNativeTokenConfig(metadata: ChainMetadata): Promise { decimals = await input({ message: "Enter the native token's decimals:", }); + denom = await input({ + message: "Enter the native token's address:", + }); } metadata.nativeToken = { symbol: symbol ?? 'ETH', name: name ?? 'Ether', decimals: decimals ? parseInt(decimals, 10) : 18, + denom: denom ?? undefined, }; } diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index 0bdcfec2c4f..55acb393594 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -1,1442 +1,200 @@ import { confirm } from '@inquirer/prompts'; -import { groupBy } from 'lodash-es'; -import { Account as StarknetAccount } from 'starknet'; -import { stringify as yamlStringify } from 'yaml'; +import { BigNumber, ethers } from 'ethers'; -import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; -import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; -import { AddWarpRouteOptions, ChainAddresses } from '@hyperlane-xyz/registry'; import { - AggregationIsmConfig, - AnnotatedEV5Transaction, ChainMap, + ChainMetadata, ChainName, - ChainSubmissionStrategy, - ChainSubmissionStrategySchema, - ContractVerifier, - DestinationGas, - EvmERC20WarpModule, - EvmERC20WarpRouteReader, - EvmHookModule, - EvmIsmModule, - ExplorerLicenseType, - HookConfig, - HypERC20Deployer, - HypERC20Factories, - HypERC721Deployer, - HypERC721Factories, - HypTokenRouterConfig, - HyperlaneContracts, - HyperlaneContractsMap, - HyperlaneProxyFactoryDeployer, IsmConfig, - IsmType, - MultiProvider, - MultisigIsmConfig, - OpStackIsmConfig, - PausableIsmConfig, - RemoteRouters, - RoutingIsmConfig, - STARKNET_SUPPORTED_TOKEN_TYPES, - STARKNET_TOKEN_TYPE_TO_STANDARD, - StarknetERC20WarpModule, - SubmissionStrategy, - TOKEN_TYPE_TO_STANDARD, - TokenFactories, - TokenType, - TrustedRelayerIsmConfig, - TxSubmitterBuilder, - TxSubmitterType, - WarpCoreConfig, - WarpCoreConfigSchema, - WarpRouteDeployConfig, - WarpRouteDeployConfigSchema, - attachContractsMap, - connectContractsMap, - getTokenConnectionId, - hypERC20factories, - isCollateralTokenConfig, - isTokenMetadata, + MultisigConfig, + getLocalProvider, } from '@hyperlane-xyz/sdk'; -import { - Address, - ProtocolType, - assert, - objFilter, - objKeys, - objMap, - promiseObjAll, - retryAsync, -} from '@hyperlane-xyz/utils'; +import { Address, ProtocolType } from '@hyperlane-xyz/utils'; -import { readWarpRouteDeployConfig } from '../config/warp.js'; -import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; -import { requestAndSaveApiKeys } from '../context/context.js'; -import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; -import { WriteCommandContext } from '../context/types.js'; -import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; -import { getSubmitterBuilder } from '../submit/submit.js'; +import { parseIsmConfig } from '../config/ism.js'; +import { CommandContext, WriteCommandContext } from '../context/types.js'; import { - indentYamlOrJson, - isFile, - readYamlOrJson, - runFileSelectionStep, - writeYamlOrJson, -} from '../utils/files.js'; - -import { prepareDeploy, runPreflightChecksForChains } from './utils.js'; - -interface DeployParams { - context: WriteCommandContext; - warpDeployConfig: WarpRouteDeployConfig; -} - -interface WarpApplyParams extends DeployParams { - warpCoreConfig: WarpCoreConfig; - strategyUrl?: string; - receiptsDir: string; -} - -export async function runWarpRouteDeploy({ + log, + logBlue, + logGray, + logGreen, + logPink, + logTable, +} from '../logger.js'; +import { nativeBalancesAreSufficient } from '../utils/balances.js'; +import { ENV } from '../utils/env.js'; +import { assertSigner } from '../utils/keys.js'; + +import { completeDryRun } from './dry-run.js'; + +export async function runPreflightChecksForChains({ context, - warpRouteDeploymentConfigPath, - multiProtocolSigner, + chains, + minGas, + chainsToGasCheck, }: { context: WriteCommandContext; - warpRouteDeploymentConfigPath?: string; - multiProtocolSigner?: MultiProtocolSignerManager; + chains: ChainName[]; + minGas: string; + // Chains for which to assert a native balance + // Defaults to all chains if not specified + chainsToGasCheck?: ChainName[]; }) { - const { skipConfirmation, chainMetadata, registry } = context; - const multiProvider = await multiProtocolSigner?.getMultiProvider(); - assert(multiProvider, 'Multiprovider failed!'); - - if ( - !warpRouteDeploymentConfigPath || - !isFile(warpRouteDeploymentConfigPath) - ) { - if (skipConfirmation) - throw new Error('Warp route deployment config required'); - warpRouteDeploymentConfigPath = await runFileSelectionStep( - './configs', - 'Warp route deployment config', - 'warp', - ); - } else { - log( - `Using warp route deployment config at ${warpRouteDeploymentConfigPath}`, - ); - } - const warpRouteConfig = await readWarpRouteDeployConfig( - warpRouteDeploymentConfigPath, - context, - ); - - const chains = Object.keys(warpRouteConfig); - - let apiKeys: ChainMap = {}; - if (!skipConfirmation) - apiKeys = await requestAndSaveApiKeys(chains, chainMetadata, registry); - - await runDeployPlanStep({ - context, - warpDeployConfig: warpRouteConfig, - }); - - const chainsByProtocol = groupChainsByProtocol(chains, multiProvider); - const deployments: WarpCoreConfig = { tokens: [] }; - - const routerAddresses: { - evm: ChainMap
; - starknet: ChainMap
; - } = { - evm: {}, - starknet: {}, - }; - - let starknetSigners: ChainMap = {}; - - // Execute deployments for each protocol - for (const protocol of Object.keys(chainsByProtocol) as ProtocolType[]) { - const protocolChains = chainsByProtocol[protocol]; - - switch (protocol) { - case ProtocolType.Ethereum: - { - await runPreflightChecksForChains({ - context, - chains: protocolChains, - minGas: MINIMUM_WARP_DEPLOY_GAS, - }); - await prepareDeploy(context, null, protocolChains); - const deployedContracts = await executeDeploy( - { context, warpDeployConfig: warpRouteConfig }, - apiKeys, - ); - - // Store EVM router addresses - routerAddresses.evm = objMap( - deployedContracts as HyperlaneContractsMap, - (_, contracts) => getRouter(contracts).address, - ); - - const { warpCoreConfig } = await getWarpCoreConfig( - { context, warpDeployConfig: warpRouteConfig }, - deployedContracts, - ); - deployments.tokens = [ - ...deployments.tokens, - ...warpCoreConfig.tokens, - ]; - deployments.options = { - ...deployments.options, - ...warpCoreConfig.options, - }; - } - break; - - case ProtocolType.Starknet: - assert( - multiProtocolSigner, - 'multi protocol signer is required for starknet chain deployment', - ); - starknetSigners = protocolChains.reduce>( - (acc, chain) => ({ - ...acc, - [chain]: multiProtocolSigner.getStarknetSigner(chain), - }), - {}, - ); - routerAddresses.starknet = await executeStarknetDeployments({ - starknetSigners, - warpRouteConfig, - multiProvider, - }); - const { warpCoreConfig } = await getWarpCoreConfigForStarknet( - warpRouteConfig, - multiProvider, - routerAddresses.starknet, - ); - deployments.tokens = [...deployments.tokens, ...warpCoreConfig.tokens]; - break; - - default: - throw new Error(`Unsupported protocol type: ${protocol}`); + log('Running pre-flight checks for chains...'); + const { multiProvider, skipConfirmation } = context; + + if (!chains?.length) throw new Error('Empty chain selection'); + for (const chain of chains) { + const metadata = multiProvider.tryGetChainMetadata(chain); + if (!metadata) throw new Error(`No chain config found for ${chain}`); + if ( + metadata.protocol !== ProtocolType.Ethereum && + metadata.protocol !== ProtocolType.Starknet + ) + throw new Error( + 'Only Ethereum and Starknet chains are supported for now', + ); + if (metadata.protocol === ProtocolType.Ethereum) { + const signer = multiProvider.getSigner(chain); + assertSigner(signer); + logGreen(`✅ ${chain} signer is valid`); } } + logGreen('✅ Chains are valid'); - await enrollCrossChainRouters({ - evmAddresses: routerAddresses.evm, - starknetAddresses: routerAddresses.starknet, - context, - warpRouteConfig, - deployments, + await nativeBalancesAreSufficient( multiProvider, - starknetSigners, - }); - - fullyConnectTokens(deployments, context.multiProvider); - await writeDeploymentArtifacts(deployments, context); -} - -async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { - const { skipConfirmation } = context; - - displayWarpDeployPlan(warpDeployConfig); - - if (skipConfirmation || context.isDryRun) return; - - const isConfirmed = await confirm({ - message: 'Is this deployment plan correct?', - }); - if (!isConfirmed) throw new Error('Deployment cancelled'); -} - -async function executeDeploy( - params: DeployParams, - apiKeys: ChainMap, -): Promise> { - logBlue('🚀 All systems ready, captain! Beginning deployment...'); - - const { - warpDeployConfig, - context: { multiProvider, isDryRun, dryRunChain }, - } = params; - - const deployer = warpDeployConfig.isNft - ? new HypERC721Deployer(multiProvider) - : new HypERC20Deployer(multiProvider); // TODO: replace with EvmERC20WarpModule - - const config: WarpRouteDeployConfig = - isDryRun && dryRunChain - ? { [dryRunChain]: warpDeployConfig[dryRunChain] } - : warpDeployConfig; - - const contractVerifier = new ContractVerifier( - multiProvider, - apiKeys, - coreBuildArtifact, - ExplorerLicenseType.MIT, - ); - - const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer( - multiProvider, - contractVerifier, - ); - - // For each chain in WarpRouteConfig, deploy each Ism Factory, if it's not in the registry - // Then return a modified config with the ism and/or hook address as a string - const modifiedConfig = await resolveWarpIsmAndHook( - config, - params.context, - ismFactoryDeployer, - contractVerifier, + chainsToGasCheck ?? chains, + minGas, + skipConfirmation, ); - - const deployedContracts = await deployer.deploy(modifiedConfig); - - logGreen('✅ Warp contract deployments complete'); - return deployedContracts; } -async function writeDeploymentArtifacts( - warpCoreConfig: WarpCoreConfig, - context: WriteCommandContext, - addWarpRouteOptions?: AddWarpRouteOptions, -) { - if (!context.isDryRun) { - log('Writing deployment artifacts...'); - await context.registry.addWarpRoute(warpCoreConfig, addWarpRouteOptions); - } - log(indentYamlOrJson(yamlStringify(warpCoreConfig, null, 2), 4)); -} - -async function resolveWarpIsmAndHook( - warpConfig: WarpRouteDeployConfig, - context: WriteCommandContext, - ismFactoryDeployer: HyperlaneProxyFactoryDeployer, - contractVerifier?: ContractVerifier, -): Promise { - return promiseObjAll( - objMap(warpConfig, async (chain, config) => { - if ( - // for non-evm chains just return config as it is - context.multiProvider.getChainMetadata(chain).protocol !== - ProtocolType.Ethereum || - !config.interchainSecurityModule || - typeof config.interchainSecurityModule === 'string' - ) { - logGray( - `Config Ism is ${ - !config.interchainSecurityModule - ? 'empty' - : config.interchainSecurityModule - }, skipping deployment.`, - ); - return config; - } - - logBlue(`Loading registry factory addresses for ${chain}...`); - const chainAddresses = await context.registry.getChainAddresses(chain); - - if (!chainAddresses) { - throw `Registry factory addresses not found for ${chain}.`; - } - - config.interchainSecurityModule = await createWarpIsm({ - chain, - chainAddresses, - context, - contractVerifier, - ismFactoryDeployer, - warpConfig: config, - }); // TODO write test - - config.hook = await createWarpHook({ - chain, - chainAddresses, - context, - contractVerifier, - ismFactoryDeployer, - warpConfig: config, - }); - return config; - }), - ); -} - -/** - * Deploys the Warp ISM for a given config - * - * @returns The deployed ism address - */ -async function createWarpIsm({ - chain, - chainAddresses, +export async function runDeployPlanStep({ context, - contractVerifier, - warpConfig, -}: { - chain: string; - chainAddresses: Record; - context: WriteCommandContext; - contractVerifier?: ContractVerifier; - warpConfig: HypTokenRouterConfig; - ismFactoryDeployer: HyperlaneProxyFactoryDeployer; -}): Promise { - const { interchainSecurityModule } = warpConfig; - if ( - !interchainSecurityModule || - typeof interchainSecurityModule === 'string' - ) { - logGray( - `Config Ism is ${ - !interchainSecurityModule ? 'empty' : interchainSecurityModule - }, skipping deployment.`, - ); - return interchainSecurityModule; - } - - logBlue(`Loading registry factory addresses for ${chain}...`); - - logGray( - `Creating ${interchainSecurityModule.type} ISM for token on ${chain} chain...`, - ); - - logGreen( - `Finished creating ${interchainSecurityModule.type} ISM for token on ${chain} chain.`, - ); - - const { - mailbox, - domainRoutingIsmFactory, - staticAggregationHookFactory, - staticAggregationIsmFactory, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - } = chainAddresses; - const evmIsmModule = await EvmIsmModule.create({ - chain, - mailbox, - multiProvider: context.multiProvider, - proxyFactoryFactories: { - domainRoutingIsmFactory, - staticAggregationHookFactory, - staticAggregationIsmFactory, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - }, - config: interchainSecurityModule, - contractVerifier, - }); - const { deployedIsm } = evmIsmModule.serialize(); - return deployedIsm; -} - -async function createWarpHook({ chain, - chainAddresses, - context, - contractVerifier, - warpConfig, }: { - chain: string; - chainAddresses: Record; context: WriteCommandContext; - contractVerifier?: ContractVerifier; - warpConfig: HypTokenRouterConfig; - ismFactoryDeployer: HyperlaneProxyFactoryDeployer; -}): Promise { - const { hook } = warpConfig; - - if (!hook || typeof hook === 'string') { - logGray(`Config Hook is ${!hook ? 'empty' : hook}, skipping deployment.`); - return hook; - } - - logBlue(`Loading registry factory addresses for ${chain}...`); - - logGray(`Creating ${hook.type} Hook for token on ${chain} chain...`); - + chain: ChainName; +}) { const { - mailbox, - domainRoutingIsmFactory, - staticAggregationHookFactory, - staticAggregationIsmFactory, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - } = chainAddresses; - const proxyFactoryFactories = { - domainRoutingIsmFactory, - staticAggregationHookFactory, - staticAggregationIsmFactory, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - }; - - // If config.proxyadmin.address exists, then use that. otherwise deploy a new proxyAdmin - const proxyAdminAddress: Address = - warpConfig.proxyAdmin?.address ?? - ( - await context.multiProvider.handleDeploy( - chain, - new ProxyAdmin__factory(), - [], - ) - ).address; - - const evmHookModule = await EvmHookModule.create({ - chain, - multiProvider: context.multiProvider, - coreAddresses: { - mailbox, - proxyAdmin: proxyAdminAddress, - }, - config: hook, - contractVerifier, - proxyFactoryFactories, - }); - logGreen(`Finished creating ${hook.type} Hook for token on ${chain} chain.`); - const { deployedHook } = evmHookModule.serialize(); - return deployedHook; -} - -async function getWarpCoreConfig( - { warpDeployConfig, context }: DeployParams, - contracts: HyperlaneContractsMap, -): Promise<{ - warpCoreConfig: WarpCoreConfig; - addWarpRouteOptions?: AddWarpRouteOptions; -}> { - const warpCoreConfig: WarpCoreConfig = { tokens: [] }; - - // TODO: replace with warp read - const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( - context.multiProvider, - warpDeployConfig, - ); - assert( - tokenMetadata && isTokenMetadata(tokenMetadata), - 'Missing required token metadata', - ); - const { decimals, symbol, name } = tokenMetadata; - assert(decimals, 'Missing decimals on token metadata'); - - generateTokenConfigs( - warpCoreConfig, - warpDeployConfig, - contracts, - symbol, - name, - decimals, - ); - - fullyConnectTokens(warpCoreConfig, context.multiProvider); - - return { warpCoreConfig, addWarpRouteOptions: { symbol } }; -} - -/** - * Creates token configs. - */ -function generateTokenConfigs( - warpCoreConfig: WarpCoreConfig, - warpDeployConfig: WarpRouteDeployConfig, - contracts: HyperlaneContractsMap, - symbol: string, - name: string, - decimals: number, -): void { - for (const [chainName, contract] of Object.entries(contracts)) { - const config = warpDeployConfig[chainName]; - const collateralAddressOrDenom = isCollateralTokenConfig(config) - ? config.token // gets set in the above deriveTokenMetadata() - : undefined; - - warpCoreConfig.tokens.push({ - chainName, - standard: TOKEN_TYPE_TO_STANDARD[config.type], - decimals, - symbol: config.symbol || symbol, - name, - addressOrDenom: - contract[warpDeployConfig[chainName].type as keyof TokenFactories] - .address, - collateralAddressOrDenom, - }); - } -} - -/** - * Adds connections between tokens. - * - * Assumes full interconnectivity between all tokens for now b.c. that's - * what the deployers do by default. - */ -function fullyConnectTokens( - warpCoreConfig: WarpCoreConfig, - multiProvider: MultiProvider, -): void { - for (const token1 of warpCoreConfig.tokens) { - for (const token2 of warpCoreConfig.tokens) { - if ( - token1.chainName === token2.chainName && - token1.addressOrDenom === token2.addressOrDenom - ) - continue; - token1.connections ||= []; - token1.connections.push({ - token: getTokenConnectionId( - multiProvider.getChainMetadata(token2.chainName).protocol, - token2.chainName, - token2.addressOrDenom!, - ), - }); - } - } -} - -export async function runWarpRouteApply( - params: WarpApplyParams, -): Promise { - const { warpDeployConfig, warpCoreConfig, context } = params; - const { chainMetadata, skipConfirmation } = context; - - WarpRouteDeployConfigSchema.parse(warpDeployConfig); - WarpCoreConfigSchema.parse(warpCoreConfig); - - const warpCoreConfigByChain = Object.fromEntries( - warpCoreConfig.tokens.map((token) => [token.chainName, token]), - ); - - const chains = Object.keys(warpDeployConfig); - - let apiKeys: ChainMap = {}; - if (!skipConfirmation) - apiKeys = await requestAndSaveApiKeys( - chains, - chainMetadata, - context.registry, - ); - - const transactions: AnnotatedEV5Transaction[] = [ - ...(await extendWarpRoute( - params, - apiKeys, - warpDeployConfig, - warpCoreConfigByChain, - )), - ...(await updateExistingWarpRoute( - params, - apiKeys, - warpDeployConfig, - warpCoreConfigByChain, - )), - ]; - if (transactions.length == 0) - return logGreen(`Warp config is the same as target. No updates needed.`); - - await submitWarpApplyTransactions(params, groupBy(transactions, 'chainId')); -} - -async function extendWarpRoute( - params: WarpApplyParams, - apiKeys: ChainMap, - warpDeployConfig: WarpRouteDeployConfig, - warpCoreConfigByChain: ChainMap, -) { - const { multiProvider } = params.context; - const warpCoreChains = Object.keys(warpCoreConfigByChain); - - // Split between the existing and additional config - const existingConfigs: WarpRouteDeployConfig = objFilter( - warpDeployConfig, - (chain, _config): _config is any => warpCoreChains.includes(chain), - ); - - let extendedConfigs: WarpRouteDeployConfig = objFilter( - warpDeployConfig, - (chain, _config): _config is any => !warpCoreChains.includes(chain), - ); - - const extendedChains = Object.keys(extendedConfigs); - if (extendedChains.length === 0) return []; - - logBlue(`Extending Warp Route to ${extendedChains.join(', ')}`); - - extendedConfigs = await deriveMetadataFromExisting( + chainMetadata: chainMetadataMap, multiProvider, - existingConfigs, - extendedConfigs, - ); + skipConfirmation, + } = context; - const newDeployedContracts = await executeDeploy( - { - context: params.context, - warpDeployConfig: extendedConfigs, - }, - apiKeys, - ); + const address = await multiProvider.getSigner(chain).getAddress(); - const mergedRouters = mergeAllRouters( - multiProvider, - existingConfigs, - newDeployedContracts, - warpCoreConfigByChain, + logBlue('\nDeployment plan'); + logGray('==============='); + log(`Transaction signer and owner of new contracts: ${address}`); + log(`Deploying core contracts to network: ${chain}`); + const transformedChainMetadata = transformChainMetadataForDisplay( + chainMetadataMap[chain], ); - - const { warpCoreConfig: updatedWarpCoreConfig, addWarpRouteOptions } = - await getWarpCoreConfig(params, mergedRouters); - WarpCoreConfigSchema.parse(updatedWarpCoreConfig); - - await writeDeploymentArtifacts( - updatedWarpCoreConfig, - params.context, - addWarpRouteOptions, + logTable(transformedChainMetadata); + log( + `Note: There are several contracts required for each chain, but contracts in your provided registries will be skipped.`, ); - return enrollRemoteRouters(params, mergedRouters); -} - -async function updateExistingWarpRoute( - params: WarpApplyParams, - apiKeys: ChainMap, - warpDeployConfig: WarpRouteDeployConfig, - warpCoreConfigByChain: ChainMap, -) { - logBlue('Updating deployed Warp Routes'); - const { multiProvider, registry } = params.context; - const registryAddresses = - (await registry.getAddresses()) as ChainMap; - const contractVerifier = new ContractVerifier( - multiProvider, - apiKeys, - coreBuildArtifact, - ExplorerLicenseType.MIT, - ); - const transactions: AnnotatedEV5Transaction[] = []; - - await promiseObjAll( - objMap(warpDeployConfig, async (chain, config) => { - await retryAsync(async () => { - logGray(`Update existing warp route for chain ${chain}`); - const deployedConfig = warpCoreConfigByChain[chain]; - if (!deployedConfig) - return logGray( - `Missing artifacts for ${chain}. Probably new deployment. Skipping update...`, - ); - - const deployedTokenRoute = deployedConfig.addressOrDenom!; - const { - domainRoutingIsmFactory, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticAggregationIsmFactory, - staticAggregationHookFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - } = registryAddresses[chain]; - - const evmERC20WarpModule = new EvmERC20WarpModule( - multiProvider, - { - config, - chain, - addresses: { - deployedTokenRoute, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticAggregationIsmFactory, - staticAggregationHookFactory, - domainRoutingIsmFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - }, - }, - contractVerifier, - ); - transactions.push(...(await evmERC20WarpModule.update(config))); - }); - }), - ); - return transactions; -} - -/** - * Retrieves a chain submission strategy from the provided filepath. - * @param submissionStrategyFilepath a filepath to the submission strategy file - * @returns a formatted submission strategy - */ -export function readChainSubmissionStrategy( - submissionStrategyFilepath: string, -): ChainSubmissionStrategy { - const submissionStrategyFileContent = readYamlOrJson( - submissionStrategyFilepath.trim(), - ); - return ChainSubmissionStrategySchema.parse(submissionStrategyFileContent); -} - -/** - * Derives token metadata from existing config and merges it with extended config. - * @returns The merged Warp route deployment config with token metadata. - */ -async function deriveMetadataFromExisting( - multiProvider: MultiProvider, - existingConfigs: WarpRouteDeployConfig, - extendedConfigs: WarpRouteDeployConfig, -): Promise { - const existingTokenMetadata = await HypERC20Deployer.deriveTokenMetadata( - multiProvider, - existingConfigs, - ); - return objMap(extendedConfigs, (_chain, extendedConfig) => { - return { - ...existingTokenMetadata, - ...extendedConfig, - }; + if (skipConfirmation) return; + await confirmExistingMailbox(context, chain); + const isConfirmed = await confirm({ + message: 'Is this deployment plan correct?', }); + if (!isConfirmed) throw new Error('Deployment cancelled'); } -/** - * Merges existing router configs with newly deployed router contracts. - */ -function mergeAllRouters( - multiProvider: MultiProvider, - existingConfigs: WarpRouteDeployConfig, - deployedContractsMap: HyperlaneContractsMap< - HypERC20Factories | HypERC721Factories - >, - warpCoreConfigByChain: ChainMap, +async function confirmExistingMailbox( + context: CommandContext, + chain: ChainName, ) { - const existingContractAddresses = objMap( - existingConfigs, - (chain, config) => ({ - [config.type]: warpCoreConfigByChain[chain].addressOrDenom!, - }), - ); - return { - ...connectContractsMap( - attachContractsMap(existingContractAddresses, hypERC20factories), - multiProvider, - ), - ...deployedContractsMap, - } as HyperlaneContractsMap; -} - -/** - * Enroll all deployed routers with each other. - * - * @param deployedContractsMap - A map of deployed Hyperlane contracts by chain. - * @param multiProvider - A MultiProvider instance to interact with multiple chains. - */ -async function enrollRemoteRouters( - params: WarpApplyParams, - deployedContractsMap: HyperlaneContractsMap, -): Promise { - logBlue(`Enrolling deployed routers with each other...`); - const { multiProvider, registry } = params.context; - const registryAddresses = await registry.getAddresses(); - const deployedRoutersAddresses: ChainMap
= objMap( - deployedContractsMap, - (_, contracts) => getRouter(contracts).address, - ); - const deployedDestinationGas: DestinationGas = await populateDestinationGas( - multiProvider, - params.warpDeployConfig, - deployedContractsMap, - ); - - const deployedChains = Object.keys(deployedRoutersAddresses); - const transactions: AnnotatedEV5Transaction[] = []; - await promiseObjAll( - objMap(deployedContractsMap, async (chain, contracts) => { - await retryAsync(async () => { - const router = getRouter(contracts); // Assume deployedContract always has 1 value - const deployedTokenRoute = router.address; - // Mutate the config.remoteRouters by setting it to all other routers to update - const warpRouteReader = new EvmERC20WarpRouteReader( - multiProvider, - chain, - ); - const mutatedWarpRouteConfig = - await warpRouteReader.deriveWarpRouteConfig(deployedTokenRoute); - const { - domainRoutingIsmFactory, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticAggregationIsmFactory, - staticAggregationHookFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - } = registryAddresses[chain]; - - const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { - config: mutatedWarpRouteConfig, - chain, - addresses: { - deployedTokenRoute, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticAggregationIsmFactory, - staticAggregationHookFactory, - domainRoutingIsmFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - }, - }); - - const otherChains = multiProvider - .getRemoteChains(chain) - .filter((c) => deployedChains.includes(c)); - - mutatedWarpRouteConfig.remoteRouters = - otherChains.reduce((remoteRouters, otherChain) => { - remoteRouters[multiProvider.getDomainId(otherChain)] = { - address: deployedRoutersAddresses[otherChain], - }; - return remoteRouters; - }, {}); - - mutatedWarpRouteConfig.destinationGas = - otherChains.reduce((destinationGas, otherChain) => { - const otherChainDomain = multiProvider.getDomainId(otherChain); - destinationGas[otherChainDomain] = - deployedDestinationGas[otherChainDomain]; - return destinationGas; - }, {}); - - const mutatedConfigTxs: AnnotatedEV5Transaction[] = - await evmERC20WarpModule.update(mutatedWarpRouteConfig); - - if (mutatedConfigTxs.length == 0) - return logGreen( - `Warp config on ${chain} is the same as target. No updates needed.`, - ); - transactions.push(...mutatedConfigTxs); - }); - }), - ); - - return transactions; -} - -/** - * Populates the destination gas amounts for each chain using warpConfig.gas OR querying other router's destinationGas - */ -async function populateDestinationGas( - multiProvider: MultiProvider, - warpDeployConfig: WarpRouteDeployConfig, - deployedContractsMap: HyperlaneContractsMap, -): Promise { - const destinationGas: DestinationGas = {}; - const deployedChains = Object.keys(deployedContractsMap); - await promiseObjAll( - objMap(deployedContractsMap, async (chain, contracts) => { - await retryAsync(async () => { - const router = getRouter(contracts); - - const otherChains = multiProvider - .getRemoteChains(chain) - .filter((c) => deployedChains.includes(c)); - - for (const otherChain of otherChains) { - const otherDomain = multiProvider.getDomainId(otherChain); - if (!destinationGas[otherDomain]) - destinationGas[otherDomain] = - warpDeployConfig[otherChain].gas?.toString() || - (await router.destinationGas(otherDomain)).toString(); - } - }); - }), - ); - return destinationGas; -} - -function getRouter(contracts: HyperlaneContracts) { - for (const key of objKeys(hypERC20factories)) { - if (contracts[key]) return contracts[key]; - } - throw new Error('No matching contract found.'); -} - -function displayWarpDeployPlan(deployConfig: WarpRouteDeployConfig) { - logBlue('\nWarp Route Deployment Plan'); - logGray('=========================='); - log(`📋 Token Standard: ${deployConfig.isNft ? 'ERC721' : 'ERC20'}`); - - const { transformedDeployConfig, transformedIsmConfigs } = - transformDeployConfigForDisplay(deployConfig); - - log('📋 Warp Route Config:'); - logTable(transformedDeployConfig); - objMap(transformedIsmConfigs, (chain, ismConfigs) => { - log(`📋 ${chain} ISM Config(s):`); - ismConfigs.forEach((ismConfig) => { - logTable(ismConfig); + const addresses = await context.registry.getChainAddresses(chain); + if (addresses?.mailbox) { + const isConfirmed = await confirm({ + message: `Mailbox already exists at ${addresses.mailbox}. Are you sure you want to deploy a new mailbox and overwrite existing registry artifacts?`, + default: false, }); - }); -} - -/* only used for transformIsmForDisplay type-sense */ -type IsmDisplayConfig = - | RoutingIsmConfig // type, owner, ownerOverrides, domain - | AggregationIsmConfig // type, modules, threshold - | MultisigIsmConfig // type, validators, threshold - | OpStackIsmConfig // type, origin, nativeBridge - | PausableIsmConfig // type, owner, paused, ownerOverrides - | TrustedRelayerIsmConfig; // type, relayer -function transformDeployConfigForDisplay(deployConfig: WarpRouteDeployConfig) { - const transformedIsmConfigs: Record = {}; - const transformedDeployConfig = objMap(deployConfig, (chain, config) => { - if (config.interchainSecurityModule) - transformedIsmConfigs[chain] = transformIsmConfigForDisplay( - config.interchainSecurityModule as IsmDisplayConfig, - ); - - return { - 'NFT?': config.isNft ?? false, - Type: config.type, - Owner: config.owner, - Mailbox: config.mailbox, - 'ISM Config(s)': config.interchainSecurityModule - ? 'See table(s) below.' - : 'No ISM config(s) specified.', - }; - }); - - return { - transformedDeployConfig, - transformedIsmConfigs, - }; -} - -function transformIsmConfigForDisplay(ismConfig: IsmDisplayConfig): any[] { - const ismConfigs: any[] = []; - switch (ismConfig.type) { - case IsmType.AGGREGATION: - ismConfigs.push({ - Type: ismConfig.type, - Threshold: ismConfig.threshold, - Modules: 'See table(s) below.', - }); - ismConfig.modules.forEach((module) => { - ismConfigs.push( - ...transformIsmConfigForDisplay(module as IsmDisplayConfig), - ); - }); - return ismConfigs; - case IsmType.ROUTING: - return [ - { - Type: ismConfig.type, - Owner: ismConfig.owner, - 'Owner Overrides': ismConfig.ownerOverrides ?? 'Undefined', - Domains: 'See warp config for domain specification.', - }, - ]; - case IsmType.FALLBACK_ROUTING: - return [ - { - Type: ismConfig.type, - Owner: ismConfig.owner, - 'Owner Overrides': ismConfig.ownerOverrides ?? 'Undefined', - Domains: 'See warp config for domain specification.', - }, - ]; - case IsmType.MERKLE_ROOT_MULTISIG: - return [ - { - Type: ismConfig.type, - Validators: ismConfig.validators, - Threshold: ismConfig.threshold, - }, - ]; - case IsmType.MESSAGE_ID_MULTISIG: - return [ - { - Type: ismConfig.type, - Validators: ismConfig.validators, - Threshold: ismConfig.threshold, - }, - ]; - case IsmType.OP_STACK: - return [ - { - Type: ismConfig.type, - Origin: ismConfig.origin, - 'Native Bridge': ismConfig.nativeBridge, - }, - ]; - case IsmType.PAUSABLE: - return [ - { - Type: ismConfig.type, - Owner: ismConfig.owner, - 'Paused ?': ismConfig.paused, - 'Owner Overrides': ismConfig.ownerOverrides ?? 'Undefined', - }, - ]; - case IsmType.TRUSTED_RELAYER: - return [ - { - Type: ismConfig.type, - Relayer: ismConfig.relayer, - }, - ]; - default: - return [ismConfig]; + if (!isConfirmed) { + throw Error('Deployment cancelled'); + } } } -/** - * Submits a set of transactions to the specified chain and outputs transaction receipts - */ -async function submitWarpApplyTransactions( - params: WarpApplyParams, - chainTransactions: Record, -): Promise { - // Create mapping of chain ID to chain name for all chains in warpDeployConfig - const chains = Object.keys(params.warpDeployConfig); - const chainIdToName = Object.fromEntries( - chains.map((chain) => [ - params.context.multiProvider.getChainId(chain), - chain, - ]), - ); - - await promiseObjAll( - objMap(chainTransactions, async (chainId, transactions) => { - try { - await retryAsync( - async () => { - const chain = chainIdToName[chainId]; - const submitter: TxSubmitterBuilder = - await getWarpApplySubmitter({ - chain, - context: params.context, - strategyUrl: params.strategyUrl, - }); - const transactionReceipts = await submitter.submit(...transactions); - if (transactionReceipts) { - const receiptPath = `${params.receiptsDir}/${chain}-${ - submitter.txSubmitterType - }-${Date.now()}-receipts.json`; - writeYamlOrJson(receiptPath, transactionReceipts); - logGreen( - `Transactions receipts successfully written to ${receiptPath}`, - ); - } - }, - 5, // attempts - 100, // baseRetryMs - ); - } catch (e) { - logBlue(`Error in submitWarpApplyTransactions`, e); - console.dir(transactions); - } - }), - ); +// from parsed types +export function isISMConfig( + config: ChainMap | ChainMap, +): boolean { + return Object.values(config).some((c) => 'type' in c); } -/** - * Helper function to get warp apply specific submitter. - * - * @returns the warp apply submitter - */ -async function getWarpApplySubmitter({ - chain, - context, - strategyUrl, -}: { - chain: ChainName; - context: WriteCommandContext; - strategyUrl?: string; -}): Promise> { - const { multiProvider } = context; - - const submissionStrategy: SubmissionStrategy = strategyUrl - ? readChainSubmissionStrategy(strategyUrl)[chain] - : { - submitter: { - chain, - type: TxSubmitterType.JSON_RPC, - }, - }; - - return getSubmitterBuilder({ - submissionStrategy, - multiProvider, - }); +// directly from filepath +export function isZODISMConfig(filepath: string): boolean { + return parseIsmConfig(filepath).success; } -function groupChainsByProtocol( +export async function prepareDeploy( + context: WriteCommandContext, + userAddress: Address | null, chains: ChainName[], - multiProvider: MultiProvider, -): Record { - return chains.reduce((protocolMap, chainName) => { - const protocolType = multiProvider.tryGetProtocol(chainName); - assert(protocolType, `Protocol not found for chain: ${chainName}`); - - if (!protocolMap[protocolType]) { - protocolMap[protocolType] = []; - } - - protocolMap[protocolType].push(chainName); - return protocolMap; - }, {} as Record); -} - -async function executeStarknetDeployments({ - starknetSigners, - warpRouteConfig, - multiProvider, -}: { - starknetSigners: ChainMap; - warpRouteConfig: WarpRouteDeployConfig; - multiProvider: MultiProvider; -}): Promise> { - validateStarknetWarpConfig(warpRouteConfig); - - const starknetDeployer = new StarknetERC20WarpModule( - starknetSigners, - warpRouteConfig, - multiProvider, - ); - - return starknetDeployer.deployToken(); -} - -async function getWarpCoreConfigForStarknet( - warpDeployConfig: WarpRouteDeployConfig, - multiProvider: MultiProvider, - contracts: ChainMap, -): Promise<{ - warpCoreConfig: WarpCoreConfig; - addWarpRouteOptions?: AddWarpRouteOptions; -}> { - const warpCoreConfig: WarpCoreConfig = { tokens: [] }; - - // TODO: replace with warp read - const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( - multiProvider, - warpDeployConfig, - ); - assert( - tokenMetadata && isTokenMetadata(tokenMetadata), - 'Missing required token metadata', - ); - const { decimals, symbol, name } = tokenMetadata; - assert(decimals, 'Missing decimals on token metadata'); - - generateTokenConfigsForStarknet( - warpCoreConfig, - warpDeployConfig, - contracts, - symbol, - name, - decimals, +): Promise> { + const { multiProvider, isDryRun } = context; + const initialBalances: Record = {}; + await Promise.all( + chains.map(async (chain: ChainName) => { + const provider = isDryRun + ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) + : multiProvider.getProvider(chain); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); + initialBalances[chain] = currentBalance; + }), ); - - fullyConnectTokens(warpCoreConfig, multiProvider); - - return { warpCoreConfig, addWarpRouteOptions: { symbol } }; -} - -function generateTokenConfigsForStarknet( - warpCoreConfig: WarpCoreConfig, - warpDeployConfig: WarpRouteDeployConfig, - contracts: ChainMap, - symbol: string, - name: string, - decimals: number, -): void { - for (const [chainName, contract] of Object.entries(contracts)) { - const config = warpDeployConfig[chainName]; - const collateralAddressOrDenom = isCollateralTokenConfig(config) - ? config.token // gets set in the above deriveTokenMetadata() - : undefined; - warpCoreConfig.tokens.push({ - chainName, - standard: - STARKNET_TOKEN_TYPE_TO_STANDARD[ - config.type as keyof typeof STARKNET_TOKEN_TYPE_TO_STANDARD - ], - decimals, - symbol, - name, - addressOrDenom: contract, - collateralAddressOrDenom, - }); - } + return initialBalances; } -function validateStarknetWarpConfig(warpRouteConfig: WarpRouteDeployConfig) { - assert(!warpRouteConfig.isNft, 'NFT routes not supported yet!'); - - // token type validation for Starknet chains - for (const [chain, config] of Object.entries(warpRouteConfig)) { - assert( - (STARKNET_SUPPORTED_TOKEN_TYPES as readonly TokenType[]).includes( - config.type, - ), - `Token type "${ - config.type - }" is not supported on Starknet chains (${chain}}). Supported types: ${STARKNET_SUPPORTED_TOKEN_TYPES.join( - ', ', - )}`, +export async function completeDeploy( + context: WriteCommandContext, + command: string, + initialBalances: Record, + userAddress: Address | null, + chains: ChainName[], +) { + const { multiProvider, isDryRun } = context; + if (chains.length > 0) logPink(`⛽️ Gas Usage Statistics`); + for (const chain of chains) { + const provider = isDryRun + ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) + : multiProvider.getProvider(chain); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); + const balanceDelta = initialBalances[chain].sub(currentBalance); + if (isDryRun && balanceDelta.lt(0)) break; + logPink( + `\t- Gas required for ${command} ${ + isDryRun ? 'dry-run' : 'deploy' + } on ${chain}: ${ethers.utils.formatEther(balanceDelta)} ${ + multiProvider.getChainMetadata(chain).nativeToken?.symbol ?? 'ETH' + }`, ); } -} - -async function enrollRemoteRoutersOfStarknetOnEvm( - multiProvider: MultiProvider, - evmChains: ChainName[], - evmRouterAddresses: ChainMap
, - starknetDeployedAddresses: ChainMap
, - registryAddresses: ChainMap, -): Promise { - const transactions: AnnotatedEV5Transaction[] = []; - - await promiseObjAll( - objMap(evmRouterAddresses, async (evmChain, evmRouterAddress) => { - if (!evmChains.includes(evmChain)) return; - - await retryAsync(async () => { - // Create warp route reader for the EVM chain - const warpRouteReader = new EvmERC20WarpRouteReader( - multiProvider, - evmChain, - ); - - // Get current config from the deployed router - const mutatedWarpRouteConfig = - await warpRouteReader.deriveWarpRouteConfig(evmRouterAddress); - - // Filter for only Starknet chains - const starknetChains = Object.keys(starknetDeployedAddresses).filter( - (chain) => - multiProvider.getChainMetadata(chain).protocol === - ProtocolType.Starknet, - ); - - // Add Starknet routers to the config - mutatedWarpRouteConfig.remoteRouters = - starknetChains.reduce( - (remoteRouters, starknetChain) => { - remoteRouters[multiProvider.getDomainId(starknetChain)] = { - address: starknetDeployedAddresses[starknetChain], - }; - return remoteRouters; - }, - {}, - ); - const { - domainRoutingIsmFactory, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticAggregationIsmFactory, - staticAggregationHookFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - } = registryAddresses[evmChain]; - - // Create warp module to update the router - const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { - config: mutatedWarpRouteConfig, - chain: evmChain, - addresses: { - deployedTokenRoute: evmRouterAddress, - domainRoutingIsmFactory, - staticMerkleRootMultisigIsmFactory, - staticMessageIdMultisigIsmFactory, - staticAggregationIsmFactory, - staticAggregationHookFactory, - staticMerkleRootWeightedMultisigIsmFactory, - staticMessageIdWeightedMultisigIsmFactory, - }, - }); - - // Generate transactions to update the router - const chainTxs = await evmERC20WarpModule.update( - mutatedWarpRouteConfig, - ); - if (chainTxs.length > 0) { - transactions.push(...chainTxs); - } - }); - }), - ); - - return transactions; -} - -interface EnrollRoutersParams { - evmAddresses: ChainMap
; - starknetAddresses: ChainMap
; - context: WriteCommandContext; - warpRouteConfig: WarpRouteDeployConfig; - deployments: WarpCoreConfig; - multiProvider: MultiProvider; - starknetSigners: ChainMap; + if (isDryRun) await completeDryRun(command); } -async function enrollCrossChainRouters({ - evmAddresses, - starknetAddresses, - context, - warpRouteConfig, - deployments, - multiProvider, - starknetSigners, -}: EnrollRoutersParams): Promise { - const hasEvmChains = Object.keys(evmAddresses).length > 0; - const hasStarknetChains = Object.keys(starknetAddresses).length > 0; - - if (!hasEvmChains || !hasStarknetChains) return; - - logBlue('Enrolling Starknet routers with EVM chains...'); - - const registryAddresses = await context.registry.getAddresses(); - const evmChains = Object.keys(evmAddresses); - - const starknetWarpModule = new StarknetERC20WarpModule( - starknetSigners, - warpRouteConfig, - multiProvider, - ); - - await starknetWarpModule.enrollRemoteRouters({ - ...evmAddresses, - ...starknetAddresses, - }); - - const evmEnrollmentTxs = await enrollRemoteRoutersOfStarknetOnEvm( - multiProvider, - evmChains, - evmAddresses, - starknetAddresses, - registryAddresses, - ); - - if (evmEnrollmentTxs.length === 0) return; - - const chainTransactions = groupBy(evmEnrollmentTxs, 'chainId'); - await submitWarpApplyTransactions( - { - context, - warpDeployConfig: warpRouteConfig, - warpCoreConfig: deployments, - receiptsDir: './generated/transactions', - }, - chainTransactions, - ); +function transformChainMetadataForDisplay(chainMetadata: ChainMetadata) { + return { + Name: chainMetadata.name, + 'Display Name': chainMetadata.displayName, + 'Chain ID': chainMetadata.chainId, + 'Domain ID': chainMetadata.domainId, + Protocol: chainMetadata.protocol, + 'JSON RPC URL': chainMetadata.rpcUrls[0].http, + 'Native Token: Symbol': chainMetadata.nativeToken?.symbol, + 'Native Token: Name': chainMetadata.nativeToken?.name, + 'Native Token: Decimals': chainMetadata.nativeToken?.decimals, + }; } diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 58f0af75ab3..0c887bf475e 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -144,6 +144,7 @@ export async function runWarpRouteDeploy({ const chainsByProtocol = groupChainsByProtocol(chains, multiProvider); const deployments: WarpCoreConfig = { tokens: [] }; + let deploymentAddWarpRouteOptions: AddWarpRouteOptions | undefined; const routerAddresses: { evm: ChainMap
; @@ -179,10 +180,12 @@ export async function runWarpRouteDeploy({ (_, contracts) => getRouter(contracts).address, ); - const { warpCoreConfig } = await getWarpCoreConfig( - { context, warpDeployConfig: warpRouteConfig }, - deployedContracts, - ); + const { warpCoreConfig, addWarpRouteOptions } = + await getWarpCoreConfig( + { context, warpDeployConfig: warpRouteConfig }, + deployedContracts, + ); + deploymentAddWarpRouteOptions = addWarpRouteOptions; deployments.tokens = [ ...deployments.tokens, ...warpCoreConfig.tokens, @@ -211,11 +214,13 @@ export async function runWarpRouteDeploy({ warpRouteConfig, multiProvider, }); - const { warpCoreConfig } = await getWarpCoreConfigForStarknet( - warpRouteConfig, - multiProvider, - routerAddresses.starknet, - ); + const { warpCoreConfig, addWarpRouteOptions } = + await getWarpCoreConfigForStarknet( + warpRouteConfig, + multiProvider, + routerAddresses.starknet, + ); + deploymentAddWarpRouteOptions = addWarpRouteOptions; deployments.tokens = [...deployments.tokens, ...warpCoreConfig.tokens]; break; @@ -235,7 +240,11 @@ export async function runWarpRouteDeploy({ }); fullyConnectTokens(deployments, context.multiProvider); - await writeDeploymentArtifacts(deployments, context, addWarpRouteOptions); + await writeDeploymentArtifacts( + deployments, + context, + deploymentAddWarpRouteOptions, + ); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { @@ -508,9 +517,18 @@ async function createWarpHook({ return deployedHook; } -async function getWarpCoreConfig( - { warpDeployConfig, context }: DeployParams, - contracts: HyperlaneContractsMap, +async function getWarpCoreConfigCore( + warpDeployConfig: WarpRouteDeployConfig, + multiProvider: MultiProvider, + contracts: T, + generateConfigsFn: ( + warpCoreConfig: WarpCoreConfig, + warpDeployConfig: WarpRouteDeployConfig, + contracts: T, + symbol: string, + name: string, + decimals: number, + ) => void, ): Promise<{ warpCoreConfig: WarpCoreConfig; addWarpRouteOptions?: AddWarpRouteOptions; @@ -519,7 +537,7 @@ async function getWarpCoreConfig( // TODO: replace with warp read const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( - context.multiProvider, + multiProvider, warpDeployConfig, ); assert( @@ -529,7 +547,7 @@ async function getWarpCoreConfig( const { decimals, symbol, name } = tokenMetadata; assert(decimals, 'Missing decimals on token metadata'); - generateTokenConfigs( + generateConfigsFn( warpCoreConfig, warpDeployConfig, contracts, @@ -538,11 +556,26 @@ async function getWarpCoreConfig( decimals, ); - fullyConnectTokens(warpCoreConfig, context.multiProvider); + fullyConnectTokens(warpCoreConfig, multiProvider); return { warpCoreConfig, addWarpRouteOptions: { symbol } }; } +async function getWarpCoreConfig( + { warpDeployConfig, context }: DeployParams, + contracts: HyperlaneContractsMap, +): Promise<{ + warpCoreConfig: WarpCoreConfig; + addWarpRouteOptions?: AddWarpRouteOptions; +}> { + return getWarpCoreConfigCore( + warpDeployConfig, + context.multiProvider, + contracts, + generateTokenConfigs, + ); +} + /** * Creates token configs. */ @@ -1222,32 +1255,12 @@ async function getWarpCoreConfigForStarknet( warpCoreConfig: WarpCoreConfig; addWarpRouteOptions?: AddWarpRouteOptions; }> { - const warpCoreConfig: WarpCoreConfig = { tokens: [] }; - - // TODO: replace with warp read - const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( - multiProvider, - warpDeployConfig, - ); - assert( - tokenMetadata && isTokenMetadata(tokenMetadata), - 'Missing required token metadata', - ); - const { decimals, symbol, name } = tokenMetadata; - assert(decimals, 'Missing decimals on token metadata'); - - generateTokenConfigsForStarknet( - warpCoreConfig, + return getWarpCoreConfigCore( warpDeployConfig, + multiProvider, contracts, - symbol, - name, - decimals, + generateTokenConfigsForStarknet, ); - - fullyConnectTokens(warpCoreConfig, multiProvider); - - return { warpCoreConfig, addWarpRouteOptions: { symbol } }; } function generateTokenConfigsForStarknet( From 9e508105a5ed8e9a5410a1cd9272643b892618c9 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 13 Mar 2025 14:58:38 +0100 Subject: [PATCH 118/132] checkout: feat/starknet-core-package -- starknet --- starknet/eslint.config.mjs | 11 + starknet/package.json | 7 +- starknet/scripts/generate-artifacts.ts | 299 ++++++++++++++++ starknet/src/artifacts/index.ts | 7 + starknet/src/index.ts | 130 ++++--- starknet/tsconfig.json | 9 +- typescript/sdk/package.json | 2 +- .../sdk/src/token/StarknetERC20WarpModule.ts | 8 +- yarn.lock | 319 +++++++----------- 9 files changed, 508 insertions(+), 284 deletions(-) create mode 100644 starknet/eslint.config.mjs create mode 100644 starknet/scripts/generate-artifacts.ts create mode 100644 starknet/src/artifacts/index.ts diff --git a/starknet/eslint.config.mjs b/starknet/eslint.config.mjs new file mode 100644 index 00000000000..cd3febcd1cd --- /dev/null +++ b/starknet/eslint.config.mjs @@ -0,0 +1,11 @@ +import MonorepoDefaults from '../eslint.config.mjs'; + +export default [ + ...MonorepoDefaults, + { + files: ['**/*.ts'], + }, + { + ignores: ['scripts/**/*'], + }, +]; diff --git a/starknet/package.json b/starknet/package.json index 1ce93590c5b..f4ad2d7b997 100644 --- a/starknet/package.json +++ b/starknet/package.json @@ -32,8 +32,13 @@ "starknet": "6.23.1" }, "devDependencies": { - "eslint": "^8.57.0", + "@eslint/js": "^9.15.0", + "@typescript-eslint/eslint-plugin": "^8.1.6", + "@typescript-eslint/parser": "^8.1.6", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", "globby": "^14.1.0", "prettier": "^2.8.8", "tsx": "^4.7.1", diff --git a/starknet/scripts/generate-artifacts.ts b/starknet/scripts/generate-artifacts.ts new file mode 100644 index 00000000000..3e8eca6f49f --- /dev/null +++ b/starknet/scripts/generate-artifacts.ts @@ -0,0 +1,299 @@ +import { promises as fs } from 'fs'; +import { globby } from 'globby'; +import { basename, dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +import { CONFIG } from '../src/config.js'; + +const cwd = process.cwd(); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const ROOT_OUTPUT_DIR = join(__dirname, '../dist/artifacts/'); +const RELEASE_DIR = join(cwd, 'release'); + +/** + * @notice Contract categories based on prefix + */ +enum ContractType { + CONTRACT = 'contracts_', + TOKEN = 'token_', + MOCK = 'mock_', +} + +/** + * @notice Contract class types + */ +enum ContractClass { + SIERRA = 'contract_class', + CASM = 'compiled_contract_class', +} + +/** + * @notice Templates for TypeScript artifact generation + */ +const TEMPLATES = { + JS_ARTIFACT: `\ +export const {name} = {artifact}; +`, + + DTS_ARTIFACT: `\ +import type { CompiledContract, CairoAssembly } from 'starknet'; + +export declare const {name}: {type}; +`, + + JS_INDEX: `\ +{imports} + +export const starknetContracts = { + contracts: { +{contractExports} + }, + tokens: { +{tokenExports} + }, + mocks: { +{mockExports} + } +}; +`, + + DTS_INDEX: `\ +import type { CompiledContract, CairoAssembly } from 'starknet'; + +export interface StarknetContractGroup { + [name: string]: { + contract_class?: CompiledContract; + compiled_contract_class?: CairoAssembly; + }; +} + +export interface StarknetContracts { + contracts: StarknetContractGroup; + tokens: StarknetContractGroup; + mocks: StarknetContractGroup; +} + +export declare const starknetContracts: StarknetContracts; +`, +}; + +class StarknetArtifactGenerator { + private processedFiles: Map< + string, + { type: ContractType; sierra: boolean; casm: boolean } + >; + private contractExports: string[] = []; + private tokenExports: string[] = []; + private mockExports: string[] = []; + + constructor() { + this.processedFiles = new Map(); + } + + getContractTypeFromPath(filePath: string): ContractType { + const fileName = basename(filePath); + // Check for exact prefix matches to avoid double categorization + if (fileName.startsWith('token_')) return ContractType.TOKEN; + if (fileName.startsWith('mock_') || fileName.startsWith('mocks_')) + return ContractType.MOCK; + if (fileName.startsWith('contracts_')) return ContractType.CONTRACT; + return ContractType.CONTRACT; // default case + } + + getContractClassFromPath(filePath: string): ContractClass { + return filePath.includes('compiled_contract_class') + ? ContractClass.CASM + : ContractClass.SIERRA; + } + + /** + * @notice Retrieves paths of all relevant artifact files + */ + async getArtifactPaths() { + const sierraPattern = `${RELEASE_DIR}/**/*${CONFIG.CONTRACT_FILE_SUFFIXES.SIERRA_JSON}`; + const casmPattern = `${RELEASE_DIR}/**/*${CONFIG.CONTRACT_FILE_SUFFIXES.ASSEMBLY_JSON}`; + + const [sierraFiles, casmFiles] = await Promise.all([ + globby(sierraPattern), + globby(casmPattern), + ]); + + return { sierraFiles, casmFiles }; + } + + /** + * @notice Creates the output directory if it doesn't exist + */ + async createOutputDirectory() { + await fs.mkdir(ROOT_OUTPUT_DIR, { recursive: true }); + } + + /** + * @notice Reads and parses a JSON artifact file + */ + async readArtifactFile(filePath: string) { + const content = await fs.readFile(filePath, 'utf-8'); + return JSON.parse(content); + } + + /** + * @notice Generates JavaScript content for a contract artifact + */ + generateJavaScriptContent(name: string, artifact: any) { + return TEMPLATES.JS_ARTIFACT.replace('{name}', name).replace( + '{artifact}', + JSON.stringify(artifact), + ); + } + + /** + * @notice Generates TypeScript declaration content for a contract artifact + */ + generateDeclarationContent(name: string, isSierra: boolean) { + const type = isSierra ? 'CompiledContract' : 'CairoAssembly'; + return TEMPLATES.DTS_ARTIFACT.replace('{name}', name).replace( + '{type}', + type, + ); + } + + /** + * @notice Generates index file contents with categorized contracts + */ + generateIndexContents() { + const imports: string[] = []; + this.contractExports = []; + this.tokenExports = []; + this.mockExports = []; + + this.processedFiles.forEach((value, name) => { + const baseName = name.replace( + new RegExp(`^(${Object.values(ContractType).join('|')})`), + '', + ); + + if (value.sierra) { + // Add prefix to avoid naming conflicts + const sierraVarName = `${value.type}${baseName}Sierra`; + imports.push( + `import { ${name} as ${sierraVarName} } from './${name}.${ContractClass.SIERRA}.json.js';`, + ); + + const exportLine = ` ${baseName}: { contract_class: ${sierraVarName}`; + const targetExports = this.getExportArrayForType(value.type); + targetExports.push(value.casm ? `${exportLine},` : `${exportLine} },`); + } + + if (value.casm) { + // Add prefix to avoid naming conflicts + const casmVarName = `${value.type}${baseName}Casm`; + imports.push( + `import { ${name} as ${casmVarName} } from './${name}.${ContractClass.CASM}.json.js';`, + ); + + const exportLine = value.sierra + ? ` compiled_contract_class: ${casmVarName} },` + : ` ${baseName}: { compiled_contract_class: ${casmVarName} },`; + + this.getExportArrayForType(value.type).push(exportLine); + } + }); + + return { + jsContent: TEMPLATES.JS_INDEX.replace('{imports}', imports.join('\n')) + .replace('{contractExports}', this.contractExports.join('\n')) + .replace('{tokenExports}', this.tokenExports.join('\n')) + .replace('{mockExports}', this.mockExports.join('\n')), + dtsContent: TEMPLATES.DTS_INDEX, + }; + } + + private getExportArrayForType(type: ContractType): string[] { + switch (type) { + case ContractType.TOKEN: + return this.tokenExports; + case ContractType.MOCK: + return this.mockExports; + default: + return this.contractExports; + } + } + + /** + * @notice Processes a single artifact file + */ + async processArtifact(filePath: string) { + const contractType = this.getContractTypeFromPath(filePath); + const contractClass = this.getContractClassFromPath(filePath); + + const baseFileName = basename(filePath); + const name = baseFileName + .replace('.json', '') + .replace(`.${ContractClass.SIERRA}`, '') + .replace(`.${ContractClass.CASM}`, ''); + + const artifact = await this.readArtifactFile(filePath); + const fileInfo = this.processedFiles.get(name) || { + type: contractType, + sierra: false, + casm: false, + }; + + if (contractClass === ContractClass.SIERRA) { + fileInfo.sierra = true; + } else { + fileInfo.casm = true; + } + + this.processedFiles.set(name, fileInfo); + + // Generate and write files + const jsContent = this.generateJavaScriptContent(name, artifact); + const dtsContent = this.generateDeclarationContent( + name, + contractClass === ContractClass.SIERRA, + ); + + const outputFileName = `${name}.${contractClass}.json`; + await fs.writeFile( + join(ROOT_OUTPUT_DIR, outputFileName + '.js'), + jsContent, + ); + await fs.writeFile( + join(ROOT_OUTPUT_DIR, outputFileName + '.d.ts'), + dtsContent, + ); + } + + async generate() { + try { + await this.createOutputDirectory(); + + const { sierraFiles, casmFiles } = await this.getArtifactPaths(); + + await Promise.all([ + ...sierraFiles.map((file) => this.processArtifact(file)), + ...casmFiles.map((file) => this.processArtifact(file)), + ]); + + // Generate and write index files + const { jsContent, dtsContent } = this.generateIndexContents(); + await fs.writeFile(join(ROOT_OUTPUT_DIR, 'index.js'), jsContent); + await fs.writeFile(join(ROOT_OUTPUT_DIR, 'index.d.ts'), dtsContent); + + console.log( + `Successfully processed ${this.processedFiles.size} Starknet contracts`, + ); + } catch (error) { + console.error('Error processing Starknet artifacts:', error); + throw error; + } + } +} + +// Add to package.json scripts: +// "generate-artifacts": "tsx scripts/generate-artifacts.ts" +const generator = new StarknetArtifactGenerator(); +generator.generate().catch(console.error); diff --git a/starknet/src/artifacts/index.ts b/starknet/src/artifacts/index.ts new file mode 100644 index 00000000000..d541c99203b --- /dev/null +++ b/starknet/src/artifacts/index.ts @@ -0,0 +1,7 @@ +// Default empty artifact array when `yarn generate-artifacts` hasn't been run +// This file will be populated with contract artifacts in `dist/artifacts` directory after running the `generate-artifacts` command +export const starknetContracts = { + contracts: {}, + tokens: {}, + mocks: {}, +} as const; diff --git a/starknet/src/index.ts b/starknet/src/index.ts index bd32f273e30..7330cb6613f 100644 --- a/starknet/src/index.ts +++ b/starknet/src/index.ts @@ -1,98 +1,90 @@ -import { existsSync, readFileSync } from 'fs'; -import { dirname, join } from 'path'; import { CairoAssembly, CompiledContract } from 'starknet'; -import { fileURLToPath } from 'url'; +import { starknetContracts } from './artifacts/index.js'; import { CONFIG } from './config.js'; import { ContractError } from './errors.js'; -const currentDirectory = dirname(fileURLToPath(import.meta.url)); -const TARGET_DEV_PATH = join(currentDirectory, CONFIG.COMPILED_CONTRACTS_DIR); +export interface StarknetContractGroup { + [name: string]: { + contract_class: CompiledContract; + compiled_contract_class: CairoAssembly; + }; +} +/** + * @notice Contract file type enum + */ +export enum ContractType { + CONTRACT = 'contracts_', + TOKEN = 'token_', + MOCK = 'mock_', +} /** - * @notice Retrieves and parses the standard compiled contract data + * @notice Retrieves a compiled contract * @param name The name of the contract to retrieve - * @returns {CompiledContract} The parsed contract data - * @throws {ContractError} If the file is not found, cannot be parsed + * @returns {CompiledContract} The contract data + * @throws {ContractError} If the contract is not found */ -export const getCompiledContract = ( +export function getCompiledContract( name: string, - contractType?: ContractType, -): CompiledContract => { + contractType: ContractType = ContractType.CONTRACT, +): CompiledContract { try { - return JSON.parse( - readFileSync( - findContractFile(name, 'SIERRA_JSON', contractType), - 'utf-8', - ), - ); - } catch (error: unknown) { - if (error instanceof ContractError) throw error; - throw new ContractError(CONFIG.CONTRACT_ERROR_CODES.PARSE_ERROR, { + const group = getContractGroup(contractType); + const contract = group[name]; + + if (!contract?.contract_class) { + throw new Error('Contract not found or missing Sierra class'); + } + + return contract.contract_class; + } catch (_error) { + throw new ContractError(CONFIG.CONTRACT_ERROR_CODES.FILE_NOT_FOUND, { name, - error: (error as Error).message, + type: contractType, }); } -}; +} /** - * @notice Retrieves and parses the CASM compiled contract data + * @notice Retrieves a CASM compiled contract * @param name The name of the contract to retrieve - * @returns {CairoAssembly} The parsed CASM contract data - * @throws {ContractError} If the file is not found, cannot be parsed + * @returns {CairoAssembly} The CASM contract data + * @throws {ContractError} If the contract is not found */ -export const getCompiledContractCasm = ( +export function getCompiledContractCasm( name: string, - contractType?: ContractType, -): CairoAssembly => { + contractType: ContractType = ContractType.CONTRACT, +): CairoAssembly { try { - return JSON.parse( - readFileSync( - findContractFile(name, 'ASSEMBLY_JSON', contractType), - 'utf-8', - ), - ); - } catch (error: unknown) { - if (error instanceof ContractError) throw error; - throw new ContractError(CONFIG.CONTRACT_ERROR_CODES.PARSE_ERROR, { + const group = getContractGroup(contractType); + const contract = group[name]; + + if (!contract?.compiled_contract_class) { + throw new Error('Contract not found or missing CASM class'); + } + + return contract.compiled_contract_class; + } catch (_error) { + throw new ContractError(CONFIG.CONTRACT_ERROR_CODES.FILE_NOT_FOUND, { name, - error: (error as Error).message, + type: contractType, }); } -}; - -/** - * @notice Contract file type enum - */ -export enum ContractType { - CONTRACT = 'contracts_', - TOKEN = 'token_', - MOCK = 'mock_', } /** - * @notice Finds the path to a contract file based on predefined patterns - * @param name The base name of the contract to find - * @param suffix The type of contract file to look for (from CONFIG.CONTRACT_FILE_SUFFIXES) - * @param type Optional contract type prefix (defaults to CONTRACT) - * @returns {string} The full path to the contract file - * @throws {ContractError} If file is not found or the contract name is invalid + * @notice Helper function to get the correct contract group */ -function findContractFile( - name: string, - suffix: keyof typeof CONFIG.CONTRACT_FILE_SUFFIXES, - type: ContractType = ContractType.CONTRACT, -): string { - const suffixPath = CONFIG.CONTRACT_FILE_SUFFIXES[suffix]; - const path = `${TARGET_DEV_PATH}/${type}${name}${suffixPath}`; - - if (!existsSync(path)) { - throw new ContractError(CONFIG.CONTRACT_ERROR_CODES.FILE_NOT_FOUND, { - name, - suffix, - path, - }); +function getContractGroup(type: ContractType): StarknetContractGroup { + switch (type) { + case ContractType.CONTRACT: + return starknetContracts.contracts; + case ContractType.TOKEN: + return starknetContracts.tokens; + case ContractType.MOCK: + return starknetContracts.mocks; + default: + throw new Error('Invalid contract type'); } - - return path; } diff --git a/starknet/tsconfig.json b/starknet/tsconfig.json index 277f04966b2..0ba1b01ae9e 100644 --- a/starknet/tsconfig.json +++ b/starknet/tsconfig.json @@ -1,15 +1,8 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "importHelpers": true, - "noEmitHelpers": true, "outDir": "./dist", "rootDir": "./src" }, - "include": ["src/**/*"], - "ts-node": { - "experimentalSpecifierResolution": "node", - "experimentalResolver": true, - "files": true - } + "include": ["src/**/*"] } diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index a17a1f4abfd..69125388fdc 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -9,7 +9,7 @@ "@cosmjs/cosmwasm-stargate": "^0.32.4", "@cosmjs/stargate": "^0.32.4", "@hyperlane-xyz/core": "5.12.0", - "@hyperlane-xyz/starknet-core": "0.2.2", + "@hyperlane-xyz/starknet-core": "0.3.1", "@hyperlane-xyz/utils": "8.9.0", "@safe-global/api-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0", diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index 482d400a734..b4b2a3b2b9e 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -55,7 +55,7 @@ export class StarknetERC20WarpModule { const deployerAccountAddress = this.account[chain].address; const ismAddress = await this.getStarknetDeploymentISMAddress({ ismConfig: interchainSecurityModule, - mailbox: mailbox, + mailbox: mailbox!, chain, deployer, }); @@ -65,7 +65,7 @@ export class StarknetERC20WarpModule { 'HypErc20', { decimals: tokenMetadata.decimals, - mailbox: mailbox, + mailbox: mailbox!, total_supply: tokenMetadata.totalSupply, name: [byteArray.byteArrayFromString(tokenMetadata.name)], symbol: [byteArray.byteArrayFromString(tokenMetadata.symbol)], @@ -82,7 +82,7 @@ export class StarknetERC20WarpModule { const tokenAddress = await deployer.deployContract( 'HypNative', { - mailbox: mailbox, + mailbox: mailbox!, native_token: '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', // ETH address on Starknet chains hook: getChecksumAddress(0), @@ -107,7 +107,7 @@ export class StarknetERC20WarpModule { const tokenAddress = await deployer.deployContract( 'HypErc20Collateral', { - mailbox: mailbox, + mailbox: mailbox!, // @ts-ignore erc20: rest.token, owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner diff --git a/yarn.lock b/yarn.lock index 0b21496132c..a199b189e58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6211,7 +6211,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.6.1": +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1": version: 4.12.1 resolution: "@eslint-community/regexpp@npm:4.12.1" checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc @@ -6236,23 +6236,6 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.4": - version: 2.1.4 - resolution: "@eslint/eslintrc@npm:2.1.4" - dependencies: - ajv: "npm:^6.12.4" - debug: "npm:^4.3.2" - espree: "npm:^9.6.0" - globals: "npm:^13.19.0" - ignore: "npm:^5.2.0" - import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.0" - minimatch: "npm:^3.1.2" - strip-json-comments: "npm:^3.1.1" - checksum: 10/7a3b14f4b40fc1a22624c3f84d9f467a3d9ea1ca6e9a372116cb92507e485260359465b58e25bcb6c9981b155416b98c9973ad9b796053fd7b3f776a6946bce8 - languageName: node - linkType: hard - "@eslint/eslintrc@npm:^3.2.0": version: 3.2.0 resolution: "@eslint/eslintrc@npm:3.2.0" @@ -6270,13 +6253,6 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.57.1": - version: 8.57.1 - resolution: "@eslint/js@npm:8.57.1" - checksum: 10/7562b21be10c2adbfa4aa5bb2eccec2cb9ac649a3569560742202c8d1cb6c931ce634937a2f0f551e078403a1c1285d6c2c0aa345dafc986149665cd69fe8b59 - languageName: node - linkType: hard - "@eslint/js@npm:9.15.0, @eslint/js@npm:^9.15.0": version: 9.15.0 resolution: "@eslint/js@npm:9.15.0" @@ -7624,17 +7600,6 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.13.0": - version: 0.13.0 - resolution: "@humanwhocodes/config-array@npm:0.13.0" - dependencies: - "@humanwhocodes/object-schema": "npm:^2.0.3" - debug: "npm:^4.3.1" - minimatch: "npm:^3.0.5" - checksum: 10/524df31e61a85392a2433bf5d03164e03da26c03d009f27852e7dcfdafbc4a23f17f021dacf88e0a7a9fe04ca032017945d19b57a16e2676d9114c22a53a9d11 - languageName: node - linkType: hard - "@humanwhocodes/module-importer@npm:^1.0.1": version: 1.0.1 resolution: "@humanwhocodes/module-importer@npm:1.0.1" @@ -7642,13 +7607,6 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^2.0.3": - version: 2.0.3 - resolution: "@humanwhocodes/object-schema@npm:2.0.3" - checksum: 10/05bb99ed06c16408a45a833f03a732f59bf6184795d4efadd33238ff8699190a8c871ad1121241bb6501589a9598dc83bf25b99dcbcf41e155cdf36e35e937a3 - languageName: node - linkType: hard - "@humanwhocodes/retry@npm:^0.3.0": version: 0.3.1 resolution: "@humanwhocodes/retry@npm:0.3.1" @@ -7934,7 +7892,7 @@ __metadata: "@cosmjs/stargate": "npm:^0.32.4" "@eslint/js": "npm:^9.15.0" "@hyperlane-xyz/core": "npm:5.12.0" - "@hyperlane-xyz/starknet-core": "npm:0.2.2" + "@hyperlane-xyz/starknet-core": "npm:0.3.1" "@hyperlane-xyz/utils": "npm:8.9.0" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" @@ -7979,14 +7937,20 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/starknet-core@npm:0.2.2, @hyperlane-xyz/starknet-core@workspace:starknet": +"@hyperlane-xyz/starknet-core@npm:0.3.1, @hyperlane-xyz/starknet-core@workspace:starknet": version: 0.0.0-use.local resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" dependencies: - eslint: "npm:^8.57.0" + "@eslint/js": "npm:^9.15.0" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" + eslint: "npm:^9.15.0" eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" + globby: "npm:^14.1.0" prettier: "npm:^2.8.8" - starknet: "npm:6.11.0" + starknet: "npm:6.23.1" tsx: "npm:^4.7.1" typescript: "npm:5.3.3" languageName: unknown @@ -9561,6 +9525,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.7.0, @noble/curves@npm:~1.7.0": + version: 1.7.0 + resolution: "@noble/curves@npm:1.7.0" + dependencies: + "@noble/hashes": "npm:1.6.0" + checksum: 10/2a11ef4895907d0b241bd3b72f9e6ebe56f0e705949bfd5efe003f25233549f620d287550df2d24ad56a1f953b82ec5f7cf4bd7cb78b1b2e76eb6dd516d44cf8 + languageName: node + linkType: hard + "@noble/curves@npm:^1.0.0": version: 1.1.0 resolution: "@noble/curves@npm:1.1.0" @@ -9614,6 +9587,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.6.0": + version: 1.6.0 + resolution: "@noble/hashes@npm:1.6.0" + checksum: 10/b44b043b02adbecd33596adeed97d9f9864c24a2410f7ac3b847986c2ecf1f6f0df76024b3f1b14d6ea954932960d88898fe551fb9d39844a8b870e9f9044ea1 + languageName: node + linkType: hard + "@noble/hashes@npm:^1, @noble/hashes@npm:^1.0.0, @noble/hashes@npm:^1.3.1": version: 1.3.2 resolution: "@noble/hashes@npm:1.3.2" @@ -9621,7 +9601,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.5.0": +"@noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:~1.6.0": version: 1.6.1 resolution: "@noble/hashes@npm:1.6.1" checksum: 10/74d9ad7b1437a22ba3b877584add3367587fbf818113152f293025d20d425aa74c191d18d434797312f2270458bc9ab3241c34d14ec6115fb16438b3248f631f @@ -9652,7 +9632,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": +"@nodelib/fs.walk@npm:^1.2.3": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -13317,7 +13297,7 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.3": +"@scure/base@npm:1.2.1, @scure/base@npm:^1.1.3": version: 1.2.1 resolution: "@scure/base@npm:1.2.1" checksum: 10/f7bdd17618ccae7a74c8cbe410a235e4adbe54aa8afe4e2fb1294338aa92f6fd04b1f1f5dea60552f638b5f5e3e74902b7baf59d3954e5e42c0a36c6baa2ebe0 @@ -13401,6 +13381,16 @@ __metadata: languageName: node linkType: hard +"@scure/starknet@npm:1.1.0": + version: 1.1.0 + resolution: "@scure/starknet@npm:1.1.0" + dependencies: + "@noble/curves": "npm:~1.7.0" + "@noble/hashes": "npm:~1.6.0" + checksum: 10/88d846584ccf7ab213472382461d8db5a5b50e1b638205122935e3aff214cbba0703f75ed4d63ed4b1f820fe3c5b08b86013c9c0cc855fd197e36bda4d86f274 + languageName: node + linkType: hard + "@scure/starknet@npm:~1.0.0": version: 1.0.0 resolution: "@scure/starknet@npm:1.0.0" @@ -14771,13 +14761,6 @@ __metadata: languageName: node linkType: hard -"@starknet-io/types-js@npm:^0.7.7, starknet-types-07@npm:@starknet-io/types-js@^0.7.7": - version: 0.7.10 - resolution: "@starknet-io/types-js@npm:0.7.10" - checksum: 10/e7e10878d6d576dcd30c6910a819e8e9cbd72102c71a93be7e0282f229d75c5765d2b5d152f7ae0693987cdb00e3d04f02bad371d91f420ddbbed435aadfbe3e - languageName: node - linkType: hard - "@storybook/addon-actions@npm:7.6.20": version: 7.6.20 resolution: "@storybook/addon-actions@npm:7.6.20" @@ -16983,13 +16966,6 @@ __metadata: languageName: node linkType: hard -"@ungap/structured-clone@npm:^1.2.0": - version: 1.2.1 - resolution: "@ungap/structured-clone@npm:1.2.1" - checksum: 10/6770f71e8183311b2871601ddb02d62a26373be7cf2950cb546a345a2305c75b502e36ce80166120aa2f5f1ea1562141684651ebbfcc711c58acd32035d3e545 - languageName: node - linkType: hard - "@vanilla-extract/css-utils@npm:^0.1.4": version: 0.1.4 resolution: "@vanilla-extract/css-utils@npm:0.1.4" @@ -17722,7 +17698,7 @@ __metadata: languageName: node linkType: hard -"abi-wan-kanabi@npm:^2.2.2, abi-wan-kanabi@npm:^2.2.3": +"abi-wan-kanabi@npm:^2.2.3": version: 2.2.3 resolution: "abi-wan-kanabi@npm:2.2.3" dependencies: @@ -17882,7 +17858,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.14.0, acorn@npm:^8.9.0": +"acorn@npm:^8.14.0": version: 8.14.0 resolution: "acorn@npm:8.14.0" bin: @@ -21012,7 +20988,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.5": +"cross-spawn@npm:^7.0.5": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -23084,16 +23060,6 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.2.2": - version: 7.2.2 - resolution: "eslint-scope@npm:7.2.2" - dependencies: - esrecurse: "npm:^4.3.0" - estraverse: "npm:^5.2.0" - checksum: 10/5c660fb905d5883ad018a6fea2b49f3cb5b1cbf2cd4bd08e98646e9864f9bc2c74c0839bed2d292e90a4a328833accc197c8f0baed89cbe8d605d6f918465491 - languageName: node - linkType: hard - "eslint-scope@npm:^8.2.0": version: 8.2.0 resolution: "eslint-scope@npm:8.2.0" @@ -23118,13 +23084,6 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.4.3": - version: 3.4.3 - resolution: "eslint-visitor-keys@npm:3.4.3" - checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b - languageName: node - linkType: hard - "eslint-visitor-keys@npm:^4.2.0": version: 4.2.0 resolution: "eslint-visitor-keys@npm:4.2.0" @@ -23132,54 +23091,6 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.57.0": - version: 8.57.1 - resolution: "eslint@npm:8.57.1" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.6.1" - "@eslint/eslintrc": "npm:^2.1.4" - "@eslint/js": "npm:8.57.1" - "@humanwhocodes/config-array": "npm:^0.13.0" - "@humanwhocodes/module-importer": "npm:^1.0.1" - "@nodelib/fs.walk": "npm:^1.2.8" - "@ungap/structured-clone": "npm:^1.2.0" - ajv: "npm:^6.12.4" - chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" - debug: "npm:^4.3.2" - doctrine: "npm:^3.0.0" - escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^7.2.2" - eslint-visitor-keys: "npm:^3.4.3" - espree: "npm:^9.6.1" - esquery: "npm:^1.4.2" - esutils: "npm:^2.0.2" - fast-deep-equal: "npm:^3.1.3" - file-entry-cache: "npm:^6.0.1" - find-up: "npm:^5.0.0" - glob-parent: "npm:^6.0.2" - globals: "npm:^13.19.0" - graphemer: "npm:^1.4.0" - ignore: "npm:^5.2.0" - imurmurhash: "npm:^0.1.4" - is-glob: "npm:^4.0.0" - is-path-inside: "npm:^3.0.3" - js-yaml: "npm:^4.1.0" - json-stable-stringify-without-jsonify: "npm:^1.0.1" - levn: "npm:^0.4.1" - lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.1.2" - natural-compare: "npm:^1.4.0" - optionator: "npm:^0.9.3" - strip-ansi: "npm:^6.0.1" - text-table: "npm:^0.2.0" - bin: - eslint: bin/eslint.js - checksum: 10/5504fa24879afdd9f9929b2fbfc2ee9b9441a3d464efd9790fbda5f05738858530182029f13323add68d19fec749d3ab4a70320ded091ca4432b1e9cc4ed104c - languageName: node - linkType: hard - "eslint@npm:^9.15.0": version: 9.15.0 resolution: "eslint@npm:9.15.0" @@ -23240,17 +23151,6 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.6.0, espree@npm:^9.6.1": - version: 9.6.1 - resolution: "espree@npm:9.6.1" - dependencies: - acorn: "npm:^8.9.0" - acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/255ab260f0d711a54096bdeda93adff0eadf02a6f9b92f02b323e83a2b7fc258797919437ad331efec3930475feb0142c5ecaaf3cdab4befebd336d47d3f3134 - languageName: node - linkType: hard - "esprima@npm:2.7.x, esprima@npm:^2.7.1": version: 2.7.3 resolution: "esprima@npm:2.7.3" @@ -23271,7 +23171,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2, esquery@npm:^1.5.0": +"esquery@npm:^1.5.0": version: 1.6.0 resolution: "esquery@npm:1.6.0" dependencies: @@ -24037,6 +23937,19 @@ __metadata: languageName: node linkType: hard +"fast-glob@npm:^3.3.3": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.8" + checksum: 10/dcc6432b269762dd47381d8b8358bf964d8f4f60286ac6aa41c01ade70bda459ff2001b516690b96d5365f68a49242966112b5d5cc9cd82395fa8f9d017c90ad + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -24119,7 +24032,7 @@ __metadata: languageName: node linkType: hard -"fetch-cookie@npm:^3.0.0": +"fetch-cookie@npm:^3.0.0, fetch-cookie@npm:~3.0.0": version: 3.0.1 resolution: "fetch-cookie@npm:3.0.1" dependencies: @@ -24145,15 +24058,6 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^6.0.1": - version: 6.0.1 - resolution: "file-entry-cache@npm:6.0.1" - dependencies: - flat-cache: "npm:^3.0.4" - checksum: 10/099bb9d4ab332cb93c48b14807a6918a1da87c45dce91d4b61fd40e6505d56d0697da060cb901c729c90487067d93c9243f5da3dc9c41f0358483bfdebca736b - languageName: node - linkType: hard - "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -24324,17 +24228,6 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^3.0.4": - version: 3.2.0 - resolution: "flat-cache@npm:3.2.0" - dependencies: - flatted: "npm:^3.2.9" - keyv: "npm:^4.5.3" - rimraf: "npm:^3.0.2" - checksum: 10/02381c6ece5e9fa5b826c9bbea481d7fd77645d96e4b0b1395238124d581d10e56f17f723d897b6d133970f7a57f0fab9148cbbb67237a0a0ffe794ba60c0c70 - languageName: node - linkType: hard - "flat-cache@npm:^4.0.0": version: 4.0.1 resolution: "flat-cache@npm:4.0.1" @@ -25017,15 +24910,6 @@ __metadata: languageName: node linkType: hard -"get-starknet-core@npm:^4.0.0-next.3": - version: 4.0.0 - resolution: "get-starknet-core@npm:4.0.0" - dependencies: - "@starknet-io/types-js": "npm:^0.7.7" - checksum: 10/e1ae3a3b866dc86d0e8d8b59517ffcdc9b7b47105e5738068d36c957c7cf03e4d6a7c1d36eff355fb65114430b10e9defb909aba94a0df654a115961f90bbed9 - languageName: node - linkType: hard - "get-stream@npm:^3.0.0": version: 3.0.0 resolution: "get-stream@npm:3.0.0" @@ -25386,6 +25270,20 @@ __metadata: languageName: node linkType: hard +"globby@npm:^14.1.0": + version: 14.1.0 + resolution: "globby@npm:14.1.0" + dependencies: + "@sindresorhus/merge-streams": "npm:^2.1.0" + fast-glob: "npm:^3.3.3" + ignore: "npm:^7.0.3" + path-type: "npm:^6.0.0" + slash: "npm:^5.1.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10/e527ff54f0dddf60abfabd0d9e799768619d957feecd8b13ef60481f270bfdce0d28f6b09267c60f8064798fb3003b8ec991375f7fe0233fbce5304e1741368c + languageName: node + linkType: hard + "google-auth-library@npm:^9.3.0": version: 9.10.0 resolution: "google-auth-library@npm:9.10.0" @@ -26345,6 +26243,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^7.0.3": + version: 7.0.3 + resolution: "ignore@npm:7.0.3" + checksum: 10/ce5e812af3acd6607a3fe0a9f9b5f01d53f009a5ace8cbf5b6491d05a481b55d65186e6a7eaa13126e93f15276bcf3d1e8d6ff3ce5549c312f9bb313fff33365 + languageName: node + linkType: hard + "immediate@npm:^3.2.3": version: 3.3.0 resolution: "immediate@npm:3.3.0" @@ -26927,7 +26832,7 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.2, is-path-inside@npm:^3.0.3": +"is-path-inside@npm:^3.0.2": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" checksum: 10/abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 @@ -27215,7 +27120,7 @@ __metadata: languageName: node linkType: hard -"isomorphic-fetch@npm:^3.0.0": +"isomorphic-fetch@npm:^3.0.0, isomorphic-fetch@npm:~3.0.0": version: 3.0.0 resolution: "isomorphic-fetch@npm:3.0.0" dependencies: @@ -31406,6 +31311,13 @@ __metadata: languageName: node linkType: hard +"path-type@npm:^6.0.0": + version: 6.0.0 + resolution: "path-type@npm:6.0.0" + checksum: 10/b9f6eaf7795c48d5c9bc4c6bc3ac61315b8d36975a73497ab2e02b764c0836b71fb267ea541863153f633a069a1c2ed3c247cb781633842fc571c655ac57c00e + languageName: node + linkType: hard + "pathe@npm:^1.1.1, pathe@npm:^1.1.2": version: 1.1.2 resolution: "pathe@npm:1.1.2" @@ -35178,24 +35090,10 @@ __metadata: languageName: node linkType: hard -"starknet@npm:6.11.0": - version: 6.11.0 - resolution: "starknet@npm:6.11.0" - dependencies: - "@noble/curves": "npm:~1.4.0" - "@noble/hashes": "npm:^1.4.0" - "@scure/base": "npm:~1.1.3" - "@scure/starknet": "npm:~1.0.0" - abi-wan-kanabi: "npm:^2.2.2" - fetch-cookie: "npm:^3.0.0" - get-starknet-core: "npm:^4.0.0-next.3" - isomorphic-fetch: "npm:^3.0.0" - lossless-json: "npm:^4.0.1" - pako: "npm:^2.0.4" - starknet-types-07: "npm:@starknet-io/types-js@^0.7.7" - ts-mixer: "npm:^6.0.3" - url-join: "npm:^4.0.1" - checksum: 10/f2acd29277a908dbbb54dc6ebe533443d2449fc67d280ac0cec6d98ec06bbd83af497d438bb468eb3c6e7aec182c06ba134574183398361c63872ea67dbb2025 +"starknet-types-07@npm:@starknet-io/types-js@^0.7.10, starknet-types-07@npm:@starknet-io/types-js@^0.7.7": + version: 0.7.10 + resolution: "@starknet-io/types-js@npm:0.7.10" + checksum: 10/e7e10878d6d576dcd30c6910a819e8e9cbd72102c71a93be7e0282f229d75c5765d2b5d152f7ae0693987cdb00e3d04f02bad371d91f420ddbbed435aadfbe3e languageName: node linkType: hard @@ -35218,6 +35116,25 @@ __metadata: languageName: node linkType: hard +"starknet@npm:6.23.1": + version: 6.23.1 + resolution: "starknet@npm:6.23.1" + dependencies: + "@noble/curves": "npm:1.7.0" + "@noble/hashes": "npm:1.6.0" + "@scure/base": "npm:1.2.1" + "@scure/starknet": "npm:1.1.0" + abi-wan-kanabi: "npm:^2.2.3" + fetch-cookie: "npm:~3.0.0" + isomorphic-fetch: "npm:~3.0.0" + lossless-json: "npm:^4.0.1" + pako: "npm:^2.0.4" + starknet-types-07: "npm:@starknet-io/types-js@^0.7.10" + ts-mixer: "npm:^6.0.3" + checksum: 10/ad02fddad1f24bc9b107ca8ef894264176297a59b65b6db6f1d28c9af3900035373b01f002443295bcd2c70968665f206790e4eadd4a601a3639c5da6bb719a3 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -37269,6 +37186,13 @@ __metadata: languageName: node linkType: hard +"unicorn-magic@npm:^0.3.0": + version: 0.3.0 + resolution: "unicorn-magic@npm:0.3.0" + checksum: 10/bdd7d7c522f9456f32a0b77af23f8854f9a7db846088c3868ec213f9550683ab6a2bdf3803577eacbafddb4e06900974385841ccb75338d17346ccef45f9cb01 + languageName: node + linkType: hard + "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" @@ -37487,13 +37411,6 @@ __metadata: languageName: node linkType: hard -"url-join@npm:^4.0.1": - version: 4.0.1 - resolution: "url-join@npm:4.0.1" - checksum: 10/b53b256a9a36ed6b0f6768101e78ca97f32d7b935283fd29ce19d0bbfb6f88aa80aa6c03fd87f2f8978ab463a6539f597a63051e7086f3379685319a7495f709 - languageName: node - linkType: hard - "url-parse-lax@npm:^1.0.0": version: 1.0.0 resolution: "url-parse-lax@npm:1.0.0" From 06f4d5e24da60eafec6da146c0c88be8d189d3b8 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 13 Mar 2025 15:07:51 +0100 Subject: [PATCH 119/132] fix: update warpRouteConfig type to require mailbox --- typescript/cli/src/deploy/warp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index eecd4043cb7..1927e27ffb6 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -1253,7 +1253,7 @@ async function executeStarknetDeployments({ multiProvider, }: { starknetSigners: ChainMap; - warpRouteConfig: WarpRouteDeployConfig; + warpRouteConfig: WarpRouteDeployConfigMailboxRequired; multiProvider: MultiProvider; }): Promise> { validateStarknetWarpConfig(warpRouteConfig); @@ -1416,7 +1416,7 @@ interface EnrollRoutersParams { evmAddresses: ChainMap
; starknetAddresses: ChainMap
; context: WriteCommandContext; - warpRouteConfig: WarpRouteDeployConfig; + warpRouteConfig: WarpRouteDeployConfigMailboxRequired; deployments: WarpCoreConfig; multiProvider: MultiProvider; starknetSigners: ChainMap; From b565fb837d9005d8d4c05678ae37eb599608a336 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 10:05:13 +0200 Subject: [PATCH 120/132] chore: yarn install --- yarn.lock | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index a472ee989ac..9323b8d00e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7739,7 +7739,51 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/cosmos-types@workspace:typescript/cosmos-types": +"@hyperlane-xyz/core@npm:7.1.0": + version: 7.1.0 + resolution: "@hyperlane-xyz/core@npm:7.1.0" + dependencies: + "@arbitrum/nitro-contracts": "npm:^1.2.1" + "@chainlink/contracts-ccip": "npm:^1.5.0" + "@eth-optimism/contracts": "npm:^0.6.0" + "@hyperlane-xyz/utils": "npm:12.1.0" + "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" + "@matterlabs/hardhat-zksync-solc": "npm:1.2.5" + "@matterlabs/hardhat-zksync-verify": "npm:1.7.1" + "@openzeppelin/contracts": "npm:^4.9.3" + "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" + fx-portal: "npm:^1.0.3" + peerDependencies: + "@ethersproject/abi": "*" + "@ethersproject/providers": "*" + "@types/sinon-chai": "*" + checksum: 10/fe76fa68ccecdb1832b58e8d91fd89c3f66ff7f99b37302a5a57e5efd1d160e73fb2a34f2b36050cc40384cedfb933ffa94c8c4a0698ee8bdf0d2b79ea849980 + languageName: node + linkType: hard + +"@hyperlane-xyz/cosmos-sdk@workspace:typescript/cosmos-sdk": + version: 0.0.0-use.local + resolution: "@hyperlane-xyz/cosmos-sdk@workspace:typescript/cosmos-sdk" + dependencies: + "@cosmjs/stargate": "npm:^0.32.4" + "@eslint/js": "npm:^9.15.0" + "@hyperlane-xyz/cosmos-types": "npm:11.0.0" + "@types/mocha": "npm:^10.0.1" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" + eslint: "npm:^9.15.0" + eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" + mocha: "npm:^10.2.0" + mocha-steps: "npm:^1.3.0" + prettier: "npm:^2.8.8" + typescript: "npm:5.3.3" + typescript-eslint: "npm:^8.23.0" + languageName: unknown + linkType: soft + +"@hyperlane-xyz/cosmos-types@npm:11.0.0, @hyperlane-xyz/cosmos-types@workspace:typescript/cosmos-types": version: 0.0.0-use.local resolution: "@hyperlane-xyz/cosmos-types@workspace:typescript/cosmos-types" dependencies: @@ -7773,14 +7817,32 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/helloworld@npm:11.0.0, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": +"@hyperlane-xyz/helloworld@npm:11.0.0": + version: 11.0.0 + resolution: "@hyperlane-xyz/helloworld@npm:11.0.0" + dependencies: + "@hyperlane-xyz/core": "npm:6.1.0" + "@hyperlane-xyz/registry": "npm:11.1.0" + "@hyperlane-xyz/sdk": "npm:11.0.0" + "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" + ethers: "npm:^5.7.2" + peerDependencies: + "@ethersproject/abi": "*" + "@ethersproject/providers": "*" + "@types/node": "*" + "@types/sinon-chai": "*" + checksum: 10/09a412a062e3390807d89ce442af4b57f2eb0d9ff6297d7493898c9e4dc0208cd317980a3bc7906b2fca840fee6a5ad5400d05cd1eddfbe93d67d717a39b8b05 + languageName: node + linkType: hard + +"@hyperlane-xyz/helloworld@workspace:typescript/helloworld": version: 0.0.0-use.local resolution: "@hyperlane-xyz/helloworld@workspace:typescript/helloworld" dependencies: "@eslint/js": "npm:^9.15.0" - "@hyperlane-xyz/core": "npm:6.1.0" + "@hyperlane-xyz/core": "npm:7.1.0" "@hyperlane-xyz/registry": "npm:11.1.0" - "@hyperlane-xyz/sdk": "npm:11.0.0" + "@hyperlane-xyz/sdk": "npm:12.1.0" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" @@ -7959,6 +8021,38 @@ __metadata: languageName: unknown linkType: soft +"@hyperlane-xyz/sdk@npm:12.1.0": + version: 12.1.0 + resolution: "@hyperlane-xyz/sdk@npm:12.1.0" + dependencies: + "@arbitrum/sdk": "npm:^4.0.0" + "@aws-sdk/client-s3": "npm:^3.577.0" + "@chain-registry/types": "npm:^0.50.14" + "@cosmjs/cosmwasm-stargate": "npm:^0.32.4" + "@cosmjs/stargate": "npm:^0.32.4" + "@hyperlane-xyz/core": "npm:7.1.0" + "@hyperlane-xyz/utils": "npm:12.1.0" + "@safe-global/api-kit": "npm:1.3.0" + "@safe-global/protocol-kit": "npm:1.3.0" + "@safe-global/safe-deployments": "npm:1.37.23" + "@solana/spl-token": "npm:^0.4.9" + "@solana/web3.js": "npm:^1.95.4" + bignumber.js: "npm:^9.1.1" + cosmjs-types: "npm:^0.9.0" + cross-fetch: "npm:^3.1.5" + ethers: "npm:^5.7.2" + pino: "npm:^8.19.0" + starknet: "npm:^6.23.1" + viem: "npm:^2.21.45" + zksync-ethers: "npm:^5.10.0" + zod: "npm:^3.21.2" + peerDependencies: + "@ethersproject/abi": "*" + "@ethersproject/providers": "*" + checksum: 10/9416031867a5b4eb2bced1903c0b6c3e6c6d79e7c16bfd6e719505970b0a3f42d45dd3cf273f0f99fc40fc5da87220f77ae1dac9c7aab3d46f70a46c21337872 + languageName: node + linkType: hard + "@hyperlane-xyz/starknet-core@npm:0.3.1, @hyperlane-xyz/starknet-core@workspace:starknet": version: 0.0.0-use.local resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" @@ -8008,6 +8102,21 @@ __metadata: languageName: unknown linkType: soft +"@hyperlane-xyz/utils@npm:12.1.0": + version: 12.1.0 + resolution: "@hyperlane-xyz/utils@npm:12.1.0" + dependencies: + "@cosmjs/encoding": "npm:^0.32.4" + "@solana/web3.js": "npm:^1.95.4" + bignumber.js: "npm:^9.1.1" + ethers: "npm:^5.7.2" + lodash-es: "npm:^4.17.21" + pino: "npm:^8.19.0" + yaml: "npm:2.4.5" + checksum: 10/628225400fad5fe43db506939f8ecfc0dabfffc0c29f64a7ed2a47c45898abce7134e2d8787a971939c0416b660f06b9d2fbf30454dc32dce315a8dcb83174ed + languageName: node + linkType: hard + "@hyperlane-xyz/widgets@workspace:typescript/widgets": version: 0.0.0-use.local resolution: "@hyperlane-xyz/widgets@workspace:typescript/widgets" @@ -30171,6 +30280,13 @@ __metadata: languageName: node linkType: hard +"mocha-steps@npm:^1.3.0": + version: 1.3.0 + resolution: "mocha-steps@npm:1.3.0" + checksum: 10/ca36de467293b0c36290001cd0305df4a3e161fa52c20aff62fbca78566ec200610ff7b0f48eb720e76a3ffdbf91b04d478c27e15d31b1de7f0de787d63e774e + languageName: node + linkType: hard + "mocha@npm:7.1.2": version: 7.1.2 resolution: "mocha@npm:7.1.2" @@ -35583,6 +35699,25 @@ __metadata: languageName: node linkType: hard +"starknet@npm:^6.23.1": + version: 6.24.1 + resolution: "starknet@npm:6.24.1" + dependencies: + "@noble/curves": "npm:1.7.0" + "@noble/hashes": "npm:1.6.0" + "@scure/base": "npm:1.2.1" + "@scure/starknet": "npm:1.1.0" + abi-wan-kanabi: "npm:^2.2.3" + fetch-cookie: "npm:~3.0.0" + isomorphic-fetch: "npm:~3.0.0" + lossless-json: "npm:^4.0.1" + pako: "npm:^2.0.4" + starknet-types-07: "npm:@starknet-io/types-js@^0.7.10" + ts-mixer: "npm:^6.0.3" + checksum: 10/7feb01cfe9310a6669a7ca2926c525084f8ec137918db87e6087a0e0ca806a3bbaedb7a25da44cf1fddc3d87cf4e05a39e0180166af87a83c156a8f49bf67197 + languageName: node + linkType: hard + "starknetkit@npm:2.6.1": version: 2.6.1 resolution: "starknetkit@npm:2.6.1" From 12a42a7b0a595e39fa96323031d5238f692d9617 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 10:05:43 +0200 Subject: [PATCH 121/132] chore: update ccip-server --- typescript/ccip-server/CHANGELOG.md | 4 ++++ typescript/ccip-server/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/typescript/ccip-server/CHANGELOG.md b/typescript/ccip-server/CHANGELOG.md index cf6f66fc2f6..873cf4885b0 100644 --- a/typescript/ccip-server/CHANGELOG.md +++ b/typescript/ccip-server/CHANGELOG.md @@ -1,5 +1,9 @@ # @hyperlane-xyz/ccip-server +## 12.1.0 + +## 12.0.0 + ## 11.0.0 ## 10.0.0 diff --git a/typescript/ccip-server/package.json b/typescript/ccip-server/package.json index 4509223998e..d01f5485f55 100644 --- a/typescript/ccip-server/package.json +++ b/typescript/ccip-server/package.json @@ -1,6 +1,6 @@ { "name": "@hyperlane-xyz/ccip-server", - "version": "11.0.0", + "version": "12.1.0", "description": "CCIP server", "typings": "dist/index.d.ts", "typedocMain": "src/index.ts", From eefd1a538ef190583e02a077da992a4419d34d88 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 10:07:06 +0200 Subject: [PATCH 122/132] chore: merge typescript/infra & github-proxy packages from main --- typescript/github-proxy/CHANGELOG.md | 4 + typescript/github-proxy/package.json | 2 +- typescript/infra/CHANGELOG.md | 37 +++ typescript/infra/config/contexts.ts | 17 ++ .../config/environments/mainnet3/agent.ts | 188 ++++++++++++- .../mainnet3/aw-validators/rc.json | 3 - .../mainnet3/balances/dailyRelayerBurn.json | 145 +++++----- .../desiredRelayerBalanceOverrides.json | 4 +- .../balances/desiredRelayerBalances.json | 103 ++++--- .../balances/highUrgencyRelayerBalance.json | 118 ++++---- .../lowUrgencyEngKeyFunderBalance.json | 118 ++++---- .../balances/lowUrgencyKeyFunderBalance.json | 92 +++--- .../config/environments/mainnet3/chains.ts | 7 +- .../config/environments/mainnet3/funding.ts | 4 +- .../environments/mainnet3/gasPrices.json | 94 +++---- .../mainnet3/governance/ica/aw.ts | 169 +++++++++++ .../mainnet3/governance/ica/regular.ts | 110 ++++++++ .../mainnet3/governance/safe/aw.ts | 51 ++++ .../mainnet3/governance/safe/irregular.ts | 6 + .../mainnet3/governance/safe/regular.ts | 27 ++ .../mainnet3/governance/signers/aw.ts | 15 + .../mainnet3/governance/signers/irregular.ts | 13 + .../mainnet3/governance/signers/regular.ts | 16 ++ .../environments/mainnet3/governance/utils.ts | 68 +++++ .../infra/config/environments/mainnet3/igp.ts | 6 + .../environments/mainnet3/infrastructure.ts | 4 + .../config/environments/mainnet3/owners.ts | 230 +-------------- .../mainnet3/supportedChainNames.ts | 1 - .../environments/mainnet3/tokenPrices.json | 265 +++++++++--------- .../environments/mainnet3/validators.ts | 12 - ...umiaprismOptimismPolygonLUMIAWarpConfig.ts | 126 +++++++++ ...mLumiaprismOptimismPolygonETHWarpConfig.ts | 74 +++++ ...ptimismPolygonZeroNetworkUSDCWarpConfig.ts | 27 +- ...ePolygonScrollZeronetworkUSDTWarpConfig.ts | 29 +- .../getRenzoEZETHSTAGEWarpConfig.ts | 7 +- .../configGetters/getRenzoEZETHWarpConfig.ts | 32 +-- .../getRenzoPZETHSTAGEWarpConfig.ts | 7 + .../configGetters/getRenzoPZETHWarpConfig.ts | 10 +- .../configGetters/getSuperTokenWarpConfig.ts | 53 ++++ .../environments/mainnet3/warp/warpIds.ts | 16 +- .../environments/test/aggregationIsm.ts | 2 +- .../config/environments/testnet4/agent.ts | 233 +++++++++++++-- .../testnet4/aw-validators/rc.json | 8 +- .../config/environments/testnet4/chains.ts | 24 +- .../config/environments/testnet4/funding.ts | 4 +- .../environments/testnet4/gasPrices.json | 20 +- .../environments/testnet4/helloworld.ts | 6 +- .../environments/testnet4/infrastructure.ts | 2 + .../config/environments/testnet4/owners.ts | 3 + .../testnet4/supportedChainNames.ts | 1 + .../environments/testnet4/tokenPrices.json | 1 + .../environments/testnet4/validators.ts | 11 + typescript/infra/config/environments/utils.ts | 31 +- typescript/infra/config/relayer.json | 18 +- typescript/infra/config/warp.ts | 68 ++++- .../helm/key-funder/templates/cron-job.yaml | 6 + typescript/infra/helm/key-funder/values.yaml | 1 + typescript/infra/package.json | 8 +- typescript/infra/scripts/agent-utils.ts | 13 +- .../scripts/agents/update-agent-config.ts | 25 +- .../infra/scripts/check/check-owner-ica.ts | 42 ++- .../scripts/check/check-validator-announce.ts | 84 +++--- .../infra/scripts/check/check-warp-deploy.ts | 29 +- typescript/infra/scripts/deploy.ts | 4 +- .../funding/fund-keys-from-deployer.ts | 37 ++- typescript/infra/scripts/funding/vanguard.ts | 244 ++++++++++++++++ .../infra/scripts/funding/write-alert.ts | 102 ++++--- .../infra/scripts/keys/get-owner-ica.ts | 27 +- typescript/infra/scripts/print-gas-prices.ts | 25 +- .../infra/scripts/print-mailbox-owner.ts | 53 +--- typescript/infra/scripts/safes/create-safe.ts | 79 +++--- .../infra/scripts/safes/delete-pending-txs.ts | 20 +- typescript/infra/scripts/safes/delete-tx.ts | 28 +- .../infra/scripts/safes/get-pending-txs.ts | 13 +- .../safes/governance/check-safe-signers.ts | 149 ++++++++++ .../safes/governance/update-signers.ts | 99 +++++++ .../safes/migrate-to-typed-signatures.ts | 2 +- typescript/infra/scripts/safes/parse-txs.ts | 31 +- .../infra/scripts/send-test-messages.ts | 31 +- .../warp-routes/export-warp-configs.ts | 13 +- typescript/infra/src/agents/index.ts | 108 ++++++- typescript/infra/src/config/agent/relayer.ts | 70 ++++- typescript/infra/src/config/chain.ts | 11 +- typescript/infra/src/config/environment.ts | 29 +- typescript/infra/src/config/funding.ts | 1 + typescript/infra/src/config/gas-oracle.ts | 7 +- typescript/infra/src/funding/key-funder.ts | 1 + .../infra/src/govern/HyperlaneAppGovernor.ts | 163 ++++++----- .../infra/src/govern/HyperlaneCoreGovernor.ts | 3 +- typescript/infra/src/governance.ts | 72 +++++ .../external-secrets/external-secrets.ts | 38 ++- .../src/infrastructure/monitoring/grafana.ts | 12 +- .../infra/src/tx/govern-transaction-reader.ts | 86 +++--- typescript/infra/src/utils/gcloud.ts | 27 ++ typescript/infra/src/utils/safe.ts | 70 ++++- typescript/infra/src/utils/utils.ts | 4 - typescript/infra/src/warp/helm.ts | 2 +- typescript/infra/test/balance-alerts.test.ts | 81 ++++++ typescript/infra/test/warp-configs.test.ts | 42 +-- yarn.lock | 26 +- 100 files changed, 3467 insertions(+), 1293 deletions(-) create mode 100644 typescript/infra/config/environments/mainnet3/governance/ica/aw.ts create mode 100644 typescript/infra/config/environments/mainnet3/governance/ica/regular.ts create mode 100644 typescript/infra/config/environments/mainnet3/governance/safe/aw.ts create mode 100644 typescript/infra/config/environments/mainnet3/governance/safe/irregular.ts create mode 100644 typescript/infra/config/environments/mainnet3/governance/safe/regular.ts create mode 100644 typescript/infra/config/environments/mainnet3/governance/signers/aw.ts create mode 100644 typescript/infra/config/environments/mainnet3/governance/signers/irregular.ts create mode 100644 typescript/infra/config/environments/mainnet3/governance/signers/regular.ts create mode 100644 typescript/infra/config/environments/mainnet3/governance/utils.ts create mode 100644 typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIAWarpConfig.ts create mode 100644 typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumLumiaprismOptimismPolygonETHWarpConfig.ts create mode 100644 typescript/infra/scripts/funding/vanguard.ts create mode 100644 typescript/infra/scripts/safes/governance/check-safe-signers.ts create mode 100644 typescript/infra/scripts/safes/governance/update-signers.ts create mode 100644 typescript/infra/src/governance.ts create mode 100644 typescript/infra/test/balance-alerts.test.ts diff --git a/typescript/github-proxy/CHANGELOG.md b/typescript/github-proxy/CHANGELOG.md index 244f5c029d7..7933757440f 100644 --- a/typescript/github-proxy/CHANGELOG.md +++ b/typescript/github-proxy/CHANGELOG.md @@ -1,5 +1,9 @@ # @hyperlane-xyz/github-proxy +## 12.1.0 + +## 12.0.0 + ## 11.0.0 ## 10.0.0 diff --git a/typescript/github-proxy/package.json b/typescript/github-proxy/package.json index d85a80d9afd..c5afc67488c 100644 --- a/typescript/github-proxy/package.json +++ b/typescript/github-proxy/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/github-proxy", "description": "Github proxy that adds the API key to requests", - "version": "11.0.0", + "version": "12.1.0", "private": true, "scripts": { "deploy": "wrangler deploy", diff --git a/typescript/infra/CHANGELOG.md b/typescript/infra/CHANGELOG.md index ef994f636ea..7cd8eab6e2e 100644 --- a/typescript/infra/CHANGELOG.md +++ b/typescript/infra/CHANGELOG.md @@ -1,5 +1,42 @@ # @hyperlane-xyz/infra +## 12.1.0 + +### Minor Changes + +- acbf5936a: New check: HyperlaneRouterChecker now compares the list of domains + the Router is enrolled with against the warp route expectations. + It will raise a violation for missing remote domains. + `check-deploy` and `check-warp-deploy` scripts use this new check. + +### Patch Changes + +- Updated dependencies [acbf5936a] +- Updated dependencies [c757b6a18] +- Updated dependencies [a646f9ca1] +- Updated dependencies [3b615c892] + - @hyperlane-xyz/sdk@12.1.0 + - @hyperlane-xyz/helloworld@12.1.0 + - @hyperlane-xyz/utils@12.1.0 + +## 12.0.0 + +### Minor Changes + +- d478ffd08: updated warp ids and added new soon routes + +### Patch Changes + +- Updated dependencies [f7ca32315] +- Updated dependencies [4d3738d14] +- Updated dependencies [07321f6f0] +- Updated dependencies [59a087ded] +- Updated dependencies [59a087ded] +- Updated dependencies [337193305] + - @hyperlane-xyz/sdk@12.0.0 + - @hyperlane-xyz/helloworld@12.0.0 + - @hyperlane-xyz/utils@12.0.0 + ## 11.0.0 ### Minor Changes diff --git a/typescript/infra/config/contexts.ts b/typescript/infra/config/contexts.ts index 6c9e5517004..51d6c45d8b3 100644 --- a/typescript/infra/config/contexts.ts +++ b/typescript/infra/config/contexts.ts @@ -3,4 +3,21 @@ export enum Contexts { Hyperlane = 'hyperlane', ReleaseCandidate = 'rc', Neutron = 'neutron', + Vanguard0 = 'vanguard0', + Vanguard1 = 'vanguard1', + Vanguard2 = 'vanguard2', + Vanguard3 = 'vanguard3', + Vanguard4 = 'vanguard4', + Vanguard5 = 'vanguard5', +} + +function isValidContext(context: string): context is Contexts { + return Object.values(Contexts).includes(context as Contexts); +} + +export function mustBeValidContext(context: string): Contexts { + if (!isValidContext(context)) { + throw new Error(`Invalid context: ${context}`); + } + return context as Contexts; } diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 53644794327..764fea69805 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -7,7 +7,11 @@ import { ChainName, GasPaymentEnforcement, GasPaymentEnforcementPolicyType, + IsmCacheConfig, + IsmCachePolicy, + IsmCacheSelectorType, MatchingList, + ModuleType, RpcConsensusType, } from '@hyperlane-xyz/sdk'; @@ -19,6 +23,7 @@ import { } from '../../../src/config/agent/agent.js'; import { MetricAppContext, + chainMapMatchingList, consistentSenderRecipientMatchingList, routerMatchingList, senderMatchingList, @@ -26,10 +31,10 @@ import { } from '../../../src/config/agent/relayer.js'; import { BaseScraperConfig } from '../../../src/config/agent/scraper.js'; import { ALL_KEY_ROLES, Role } from '../../../src/roles.js'; -import { Contexts } from '../../contexts.js'; +import { Contexts, mustBeValidContext } from '../../contexts.js'; import { getDomainId } from '../../registry.js'; -import { environment } from './chains.js'; +import { environment, ethereumChainNames } from './chains.js'; import { helloWorld } from './helloworld.js'; import aaveSenderAddresses from './misc-artifacts/aave-sender-addresses.json'; import everclearSenderAddresses from './misc-artifacts/everclear-sender-addresses.json'; @@ -71,7 +76,6 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< artela: true, arthera: true, astar: true, - astarzkevm: false, aurora: true, avalanche: true, b3: true, @@ -212,7 +216,6 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< artela: true, arthera: true, astar: true, - astarzkevm: false, aurora: true, avalanche: true, b3: true, @@ -353,7 +356,6 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< artela: true, arthera: true, astar: true, - astarzkevm: false, aurora: true, avalanche: true, b3: true, @@ -579,6 +581,8 @@ const gasPaymentEnforcement: GasPaymentEnforcement[] = [ // Infinity VM is gasless, so enforcing min 1 wei here ensures outbound txs // outside of Solana are ignored. { originDomain: getDomainId('infinityvm') }, + // Temporary workaround due to funky Zeronetwork gas amounts. + { destinationDomain: getDomainId('zeronetwork') }, // Temporary workaround for some high gas amount estimates on Treasure ...warpRouteMatchingList(WarpRouteIds.ArbitrumTreasureMAGIC), ], @@ -588,6 +592,43 @@ const gasPaymentEnforcement: GasPaymentEnforcement[] = [ }, ]; +// HYPER - https://github.com/hyperlane-xyz/hyperlane-registry-private/blob/6f9ef6ca2805480312b75894cf030acde37c5527/deployments/warp_routes/HYPER/arbitrum-base-bsc-ethereum-optimism-config.yaml +const hyperMatchingList = chainMapMatchingList({ + arbitrum: '0xC9d23ED2ADB0f551369946BD377f8644cE1ca5c4', + base: '0xC9d23ED2ADB0f551369946BD377f8644cE1ca5c4', + bsc: '0xC9d23ED2ADB0f551369946BD377f8644cE1ca5c4', + ethereum: '0x93A2Db22B7c736B341C32Ff666307F4a9ED910F5', + optimism: '0x9923DB8d7FBAcC2E69E87fAd19b886C81cd74979', +}); + +// stHYPER - https://github.com/hyperlane-xyz/hyperlane-registry-private/blob/6f9ef6ca2805480312b75894cf030acde37c5527/deployments/warp_routes/stHYPER/bsc-ethereum-config.yaml#L1 +const stHyperMatchingList = chainMapMatchingList({ + bsc: '0x6E9804a08092D8ba4E69DaCF422Df12459F2599E', + ethereum: '0x9F6E6d150977dabc82d5D4EaaBDB1F1Ab0D25F92', +}); + +// Staging HYPER - https://github.com/hyperlane-xyz/hyperlane-registry-private/blob/38b91443b960a7887653445ef094c730bf708717/deployments/warp_routes/HYPER/arbitrum-base-bsc-ethereum-optimism-config.yaml +const stagingHyperMatchingList = chainMapMatchingList({ + arbitrum: '0xF80dcED2488Add147E60561F8137338F7f3976e1', + base: '0x830B15a1986C75EaF8e048442a13715693CBD8bD', + bsc: '0x9537c772c6092DB4B93cFBA93659bB5a8c0E133D', + ethereum: '0xC10c27afcb915439C27cAe54F5F46Da48cd71190', + optimism: '0x31cD131F5F6e1Cc0d6743F695Fc023B70D0aeAd8', +}); + +// Staging stHYPER - https://github.com/hyperlane-xyz/hyperlane-registry-private/blob/38b91443b960a7887653445ef094c730bf708717/deployments/warp_routes/stHYPER/bsc-ethereum-config.yaml +const stagingStHyperMatchingList = chainMapMatchingList({ + bsc: '0xf0c8c5fc69fCC3fA49C319Fdf422D8279756afE2', + ethereum: '0x0C919509663cb273E156B706f065b9F7e6331891', +}); + +const vanguardMatchingList = [ + ...hyperMatchingList, + ...stHyperMatchingList, + ...stagingHyperMatchingList, + ...stagingStHyperMatchingList, +]; + // Gets metric app contexts, including: // - helloworld // - all warp routes defined in WarpRouteIds, using addresses from the registry @@ -651,6 +692,23 @@ const metricAppContextsGetter = (): MetricAppContext[] => { name: 'everclear_gateway', matchingList: senderMatchingList(everclearSenderAddresses), }, + // Manually specified for now until things are public + { + name: 'HYPER/arbitrum-base-bsc-ethereum-optimism', + matchingList: hyperMatchingList, + }, + { + name: 'stHYPER/bsc-ethereum', + matchingList: stHyperMatchingList, + }, + { + name: 'HYPER-STAGING/arbitrum-base-bsc-ethereum-optimism', + matchingList: stagingHyperMatchingList, + }, + { + name: 'stHYPER-STAGING/bsc-ethereum', + matchingList: stagingStHyperMatchingList, + }, ]; }; @@ -740,6 +798,25 @@ const blacklist: MatchingList = [ })), ]; +const ismCacheConfigs: Array = [ + { + selector: { + type: IsmCacheSelectorType.DefaultIsm, + }, + moduleTypes: [ + ModuleType.AGGREGATION, + ModuleType.MERKLE_ROOT_MULTISIG, + ModuleType.MESSAGE_ID_MULTISIG, + // The relayer will cache these per-origin to accommodate DomainRoutingIsms + ModuleType.ROUTING, + ], + // SVM is explicitly not cached as the default ISM is a multisig ISM + // that routes internally. + chains: ethereumChainNames, + cachePolicy: IsmCachePolicy.IsmSpecific, + }, +]; + const hyperlane: RootAgentConfig = { ...contextBase, context: Contexts.Hyperlane, @@ -749,17 +826,23 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '45739bd-20250401-014114', + // Using an older image to ensure low-volume chains don't hit an edge case, + // see https://hyperlaneworkspace.slack.com/archives/C08GR6PBPGT/p1745272027027899?thread_ts=1745262374.703859&cid=C08GR6PBPGT + tag: '7b0f5a0-20250418-120540', }, - blacklist, + blacklist: [...blacklist, ...vanguardMatchingList], gasPaymentEnforcement: gasPaymentEnforcement, metricAppContextsGetter, + ismCacheConfigs, + cache: { + enabled: true, + }, resources: relayerResources, }, validators: { docker: { repo, - tag: '8ab7c80-20250326-191115', + tag: '385b307-20250418-150728', }, rpcConsensusType: RpcConsensusType.Quorum, chains: validatorChainConfig(Contexts.Hyperlane), @@ -785,20 +868,24 @@ const releaseCandidate: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '45739bd-20250401-014114', + tag: '7b0f5a0-20250418-120540', }, - blacklist, + blacklist: [...blacklist, ...vanguardMatchingList], // We're temporarily (ab)using the RC relayer as a way to increase // message throughput. // whitelist: releaseCandidateHelloworldMatchingList, gasPaymentEnforcement, metricAppContextsGetter, + ismCacheConfigs, + cache: { + enabled: true, + }, resources: relayerResources, }, validators: { docker: { repo, - tag: '11a4e95-20250116-145528', + tag: '385b307-20250418-150728', }, rpcConsensusType: RpcConsensusType.Quorum, chains: validatorChainConfig(Contexts.ReleaseCandidate), @@ -819,17 +906,92 @@ const neutron: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '45739bd-20250401-014114', + tag: '7b0f5a0-20250418-120540', }, - blacklist, + blacklist: [...blacklist, ...vanguardMatchingList], gasPaymentEnforcement, metricAppContextsGetter, + ismCacheConfigs, + cache: { + enabled: true, + }, resources: relayerResources, }, }; +const getVanguardRootAgentConfig = (index: number): RootAgentConfig => ({ + ...contextBase, + context: mustBeValidContext(`vanguard${index}`), + contextChainNames: { + validator: [], + relayer: ['bsc', 'arbitrum', 'optimism', 'ethereum', 'base'], + scraper: [], + }, + rolesWithKeys: [Role.Relayer], + relayer: { + rpcConsensusType: RpcConsensusType.Fallback, + docker: { + repo, + // includes gasPriceCap overrides + per-chain maxSubmitQueueLength + tag: '4569591-20250421-224434', + }, + whitelist: vanguardMatchingList, + // Not specifying a blacklist for optimization purposes -- all the message IDs + // in there are not vanguard-specific. + gasPaymentEnforcement: [ + { + type: GasPaymentEnforcementPolicyType.None, + matchingList: vanguardMatchingList, + }, + ], + metricAppContextsGetter, + ismCacheConfigs, + cache: { + enabled: true, + // Cache for 10 minutes + defaultExpirationSeconds: 10 * 60, + }, + resources: { + requests: { + // Big enough to claim a c3-standard-44 each + cpu: '35000m', + memory: '100Gi', + }, + }, + dbBootstrap: true, + mixing: { + enabled: true, + // Arbitrary salt to ensure different agents have different sorting behavior for pending messages + salt: 69690 + index, + }, + batch: { + defaultBatchSize: 32, + batchSizeOverrides: { + // Slightly lower to ideally fit within 5M + ethereum: 26, + }, + bypassBatchSimulation: true, + maxSubmitQueueLength: { + arbitrum: 350, + base: 350, + bsc: 350, + optimism: 350, + ethereum: 75, + }, + }, + txIdIndexingEnabled: false, + igpIndexingEnabled: false, + }, +}); + export const agents = { [Contexts.Hyperlane]: hyperlane, [Contexts.ReleaseCandidate]: releaseCandidate, [Contexts.Neutron]: neutron, + [Contexts.Vanguard0]: getVanguardRootAgentConfig(0), + [Contexts.Vanguard1]: getVanguardRootAgentConfig(1), + [Contexts.Vanguard2]: getVanguardRootAgentConfig(2), + [Contexts.Vanguard3]: getVanguardRootAgentConfig(3), + [Contexts.Vanguard4]: getVanguardRootAgentConfig(4), + [Contexts.Vanguard5]: getVanguardRootAgentConfig(5), }; diff --git a/typescript/infra/config/environments/mainnet3/aw-validators/rc.json b/typescript/infra/config/environments/mainnet3/aw-validators/rc.json index 6e790ce0b16..e24a7109449 100644 --- a/typescript/infra/config/environments/mainnet3/aw-validators/rc.json +++ b/typescript/infra/config/environments/mainnet3/aw-validators/rc.json @@ -12,9 +12,6 @@ "astar": { "validators": ["0x50792eee9574f1bcd002e2c6dddf2aa73b04e254"] }, - "astarzkevm": { - "validators": ["0x71e0a93f84548765cafc64d787eede19eea1ab06"] - }, "avalanche": { "validators": [ "0x2c7cf6d1796e37676ba95f056ff21bf536c6c2d3", diff --git a/typescript/infra/config/environments/mainnet3/balances/dailyRelayerBurn.json b/typescript/infra/config/environments/mainnet3/balances/dailyRelayerBurn.json index 5b8cc004aad..0ae62de58ae 100644 --- a/typescript/infra/config/environments/mainnet3/balances/dailyRelayerBurn.json +++ b/typescript/infra/config/environments/mainnet3/balances/dailyRelayerBurn.json @@ -1,139 +1,138 @@ { "abstract": 0.00428, - "alephzeroevmmainnet": 29.2, - "ancient8": 0.00204, - "apechain": 5.46, + "alephzeroevmmainnet": 33.9, + "ancient8": 0.00238, + "apechain": 7.04, "appchain": 0.0155, - "arbitrum": 0.0308, - "arbitrumnova": 0.00152, - "arcadia": 0.00152, - "artela": 671, - "arthera": 369, - "astar": 90.6, - "astarzkevm": 0.00152, - "aurora": 0.0015, - "avalanche": 0.136, + "arbitrum": 0.0344, + "arbitrumnova": 0.00195, + "arcadia": 0.00195, + "artela": 3350, + "arthera": 511, + "astar": 116, + "aurora": 0.00195, + "avalanche": 0.16, "b3": 0.00284, "base": 0.0931, - "berachain": 0.534, + "berachain": 0.916, "bitlayer": 0.00114, "blast": 0.00305, - "bob": 0.00177, - "boba": 0.00183, - "bouncebit": 21, + "bob": 0.00197, + "boba": 0.00195, + "bouncebit": 30.4, "bsc": 0.264, "bsquared": 0.00192, - "celo": 7.63, - "cheesechain": 6810, - "chilizmainnet": 63.3, - "conflux": 32.8, - "conwai": 1310, + "celo": 10.1, + "cheesechain": 10600, + "chilizmainnet": 84, + "conflux": 44.5, + "conwai": 1760, "coredao": 6.82, "corn": 0.0000356, - "coti": 34.8, + "coti": 47.3, "cyber": 0.00321, - "deepbrainchain": 1850, - "degenchain": 875, - "dogechain": 15.5, - "duckchain": 0.863, + "deepbrainchain": 3640, + "degenchain": 1230, + "dogechain": 19.7, + "duckchain": 1.05, "eclipsemainnet": 0.0232, - "endurance": 4.15, + "endurance": 6.29, "ethereum": 0.135, "everclear": 0.0081, - "evmos": 624, + "evmos": 994, "fantom": 7.39, - "flame": 1.03, + "flame": 1.3, "flare": 208, - "flowmainnet": 7.19, - "form": 0.00176, + "flowmainnet": 8.44, + "form": 0.00195, "fraxtal": 0.0217, "fusemainnet": 254, "glue": 28.8, "gnosis": 3.12, - "gravity": 174, - "guru": 758, - "harmony": 243, + "gravity": 219, + "guru": 1390, + "harmony": 285, "hemi": 0.00207, "hyperevm": 0.294, - "immutablezkevmmainnet": 4.69, - "inevm": 0.288, + "immutablezkevmmainnet": 6.74, + "inevm": 0.379, "infinityvm": 3.13, - "injective": 0.288, + "injective": 0.379, "ink": 0.0233, - "kaia": 27.8, - "kroma": 0.00177, + "kaia": 31.1, + "kroma": 0.00195, "linea": 0.0765, "lisk": 0.0104, "lukso": 3.66, - "lumia": 5.94, - "lumiaprism": 6.71, + "lumia": 9.31, + "lumiaprism": 11, "mantapacific": 0.00336, "mantle": 7.51, "matchain": 0.00537, "merlin": 0.000172, "metal": 0.00791, - "metis": 0.164, - "mint": 0.00253, + "metis": 0.229, + "mint": 0.00272, "mode": 0.0229, - "molten": 13.8, - "moonbeam": 34.1, + "molten": 24.4, + "moonbeam": 46.5, "morph": 0.178, "nero": 3.13, - "neutron": 20.2, - "nibiru": 148, - "oortmainnet": 52.2, - "opbnb": 0.00496, + "neutron": 25.5, + "nibiru": 183, + "oortmainnet": 73.6, + "opbnb": 0.00528, "optimism": 0.0383, - "orderly": 0.00249, - "osmosis": 11.2, + "orderly": 0.00281, + "osmosis": 14.4, "plume": 23, - "polygon": 13.1, + "polygon": 16.4, "polygonzkevm": 0.0244, "polynomialfi": 0.00275, "prom": 1.55, - "proofofplay": 0.00152, + "proofofplay": 0.00195, "rarichain": 0.00329, - "reactive": 35.5, - "real": 0.00152, + "reactive": 38.8, + "real": 0.00195, "redstone": 0.00217, - "rivalz": 0.00152, - "ronin": 3.89, + "rivalz": 0.00195, + "ronin": 6.23, "rootstockmainnet": 0.00165, - "sanko": 0.309, + "sanko": 0.483, "scroll": 0.00951, - "sei": 14.4, + "sei": 18.4, "shibarium": 11.3, - "snaxchain": 0.00222, + "snaxchain": 0.00283, "solanamainnet": 3.27, "soneium": 0.0394, "sonic": 7.39, "sonicsvm": 0.165, - "soon": 0.00152, + "soon": 0.00195, "sophon": 46.4, "story": 1.67, "stride": 15.5, "subtensor": 0.228, "superpositionmainnet": 0.0235, "superseed": 0.0126, - "swell": 0.00651, + "swell": 0.00856, "taiko": 0.005, "tangle": 3.13, "telos": 35.1, "torus": 22.7, - "treasure": 37.4, - "trumpchain": 0.264, + "treasure": 39.3, + "trumpchain": 0.376, "unichain": 0.00897, - "unitzero": 10.8, - "vana": 0.576, - "viction": 12.6, + "unitzero": 13.3, + "vana": 0.608, + "viction": 15.6, "worldchain": 0.0027, - "xai": 44.3, + "xai": 71.2, "xlayer": 0.114, - "xpla": 79.4, - "zeronetwork": 0.00152, - "zetachain": 10.7, + "xpla": 114, + "zeronetwork": 0.00195, + "zetachain": 13.3, "zircuit": 0.00424, - "zklink": 0.00152, - "zksync": 0.00159, + "zklink": 0.00195, + "zksync": 0.00195, "zoramainnet": 0.00876 } diff --git a/typescript/infra/config/environments/mainnet3/balances/desiredRelayerBalanceOverrides.json b/typescript/infra/config/environments/mainnet3/balances/desiredRelayerBalanceOverrides.json index 9fcc515a5bd..3861ccf7bd8 100644 --- a/typescript/infra/config/environments/mainnet3/balances/desiredRelayerBalanceOverrides.json +++ b/typescript/infra/config/environments/mainnet3/balances/desiredRelayerBalanceOverrides.json @@ -1,6 +1,6 @@ { + "artela": 2200, "arthera": 0.5, - "astarzkevm": 0, "coti": 0.1, "deepbrainchain": 100, "infinityvm": 0, @@ -8,7 +8,7 @@ "osmosis": 0, "plume": 0.05, "reactive": 0.1, - "sophon": 20, + "sophon": 50, "tangle": 6, "vana": 0.001 } diff --git a/typescript/infra/config/environments/mainnet3/balances/desiredRelayerBalances.json b/typescript/infra/config/environments/mainnet3/balances/desiredRelayerBalances.json index 5091fbdb899..7b83a2b9a42 100644 --- a/typescript/infra/config/environments/mainnet3/balances/desiredRelayerBalances.json +++ b/typescript/infra/config/environments/mainnet3/balances/desiredRelayerBalances.json @@ -1,72 +1,71 @@ { "abstract": 0.0342, - "alephzeroevmmainnet": 234, - "ancient8": 0.0163, - "apechain": 50, + "alephzeroevmmainnet": 271, + "ancient8": 0.019, + "apechain": 56.3, "appchain": 0.124, - "arbitrum": 0.246, - "arbitrumnova": 0.0122, - "arcadia": 0.0122, - "artela": 5370, + "arbitrum": 0.275, + "arbitrumnova": 0.0156, + "arcadia": 0.0156, + "artela": 2200, "arthera": 0.5, - "astar": 725, - "astarzkevm": 0, - "aurora": 0.012, - "avalanche": 1.09, + "astar": 928, + "aurora": 0.0156, + "avalanche": 1.28, "b3": 0.0227, "base": 0.745, - "berachain": 4.27, + "berachain": 7.33, "bitlayer": 0.00912, "blast": 0.0244, - "bob": 0.0142, - "boba": 0.0146, - "bouncebit": 168, + "bob": 0.0158, + "boba": 0.0156, + "bouncebit": 243, "bsc": 5, "bsquared": 0.0154, - "celo": 61, - "cheesechain": 54500, - "chilizmainnet": 506, - "conflux": 262, - "conwai": 10500, + "celo": 80.8, + "cheesechain": 84800, + "chilizmainnet": 672, + "conflux": 356, + "conwai": 14100, "coredao": 54.6, "corn": 0.000285, "coti": 0.1, "cyber": 0.0257, "deepbrainchain": 100, - "degenchain": 7000, - "dogechain": 124, - "duckchain": 6.9, + "degenchain": 9840, + "dogechain": 158, + "duckchain": 8.4, "eclipsemainnet": 0.2, - "endurance": 33.2, + "endurance": 50.3, "ethereum": 1.08, "everclear": 0.0648, - "evmos": 4990, + "evmos": 7950, "fantom": 59.1, - "flame": 8.24, + "flame": 10.4, "flare": 1660, - "flowmainnet": 57.5, - "form": 0.0141, + "flowmainnet": 67.5, + "form": 0.0156, "fraxtal": 0.2, "fusemainnet": 2030, "glue": 230, "gnosis": 25, - "gravity": 1390, - "guru": 6060, - "harmony": 1940, + "gravity": 1750, + "guru": 11100, + "harmony": 2280, "hemi": 0.0166, "hyperevm": 2.35, - "immutablezkevmmainnet": 37.5, - "inevm": 3, + "immutablezkevmmainnet": 53.9, + "inevm": 3.03, "infinityvm": 0, - "injective": 2.3, + "injective": 3.03, "ink": 0.186, "kaia": 250, "kroma": 0.05, "linea": 1, "lisk": 0.0832, "lukso": 29.3, - "lumia": 47.5, - "lumiaprism": 53.7, + "lumia": 74.5, + "lumiaprism": 88, "mantapacific": 0.2, "mantle": 60.1, "matchain": 0.05, @@ -75,19 +74,19 @@ "metis": 3, "mint": 0.05, "mode": 0.2, - "molten": 110, - "moonbeam": 273, + "molten": 195, + "moonbeam": 372, "morph": 1.42, "nero": 25, - "neutron": 162, + "neutron": 204, "nibiru": 10, "oortmainnet": 2000, - "opbnb": 0.0397, + "opbnb": 0.0422, "optimism": 0.5, "orderly": 0.05, "osmosis": 0, "plume": 0.05, - "polygon": 105, + "polygon": 131, "polygonzkevm": 0.5, "polynomialfi": 0.05, "prom": 18, @@ -97,11 +96,11 @@ "real": 0.1, "redstone": 0.2, "rivalz": 0.05, - "ronin": 31.1, + "ronin": 49.8, "rootstockmainnet": 0.0132, - "sanko": 2.47, + "sanko": 3.86, "scroll": 0.5, - "sei": 115, + "sei": 147, "shibarium": 90.4, "snaxchain": 0.05, "solanamainnet": 40, @@ -109,29 +108,29 @@ "sonic": 59.1, "sonicsvm": 1.32, "soon": 0.08, - "sophon": 20, + "sophon": 50, "story": 13.4, "stride": 124, "subtensor": 1.82, "superpositionmainnet": 0.188, "superseed": 0.101, - "swell": 0.0521, + "swell": 0.0685, "taiko": 0.2, "tangle": 6, "telos": 281, "torus": 182, "treasure": 900, - "trumpchain": 2.11, + "trumpchain": 3.01, "unichain": 0.0718, - "unitzero": 86.4, + "unitzero": 106, "vana": 0.001, - "viction": 101, + "viction": 125, "worldchain": 0.2, - "xai": 354, + "xai": 570, "xlayer": 0.912, - "xpla": 635, + "xpla": 912, "zeronetwork": 0.05, - "zetachain": 85.6, + "zetachain": 106, "zircuit": 0.0339, "zklink": 0.05, "zksync": 0.05, diff --git a/typescript/infra/config/environments/mainnet3/balances/highUrgencyRelayerBalance.json b/typescript/infra/config/environments/mainnet3/balances/highUrgencyRelayerBalance.json index a405059bd12..a57e55ab69b 100644 --- a/typescript/infra/config/environments/mainnet3/balances/highUrgencyRelayerBalance.json +++ b/typescript/infra/config/environments/mainnet3/balances/highUrgencyRelayerBalance.json @@ -1,134 +1,134 @@ { "abstract": 0.00856, - "alephzeroevmmainnet": 58.4, - "ancient8": 0.00408, - "apechain": 10.9, + "alephzeroevmmainnet": 67.8, + "ancient8": 0.00476, + "apechain": 14.1, "appchain": 0.031, - "arbitrum": 0.0616, - "arbitrumnova": 0.00304, - "arcadia": 0.00304, - "artela": 1340, + "arbitrum": 0.0688, + "arbitrumnova": 0.0039, + "arcadia": 0.0039, + "artela": 550, "arthera": 0.125, - "astar": 181, - "aurora": 0.003, - "avalanche": 0.272, + "astar": 232, + "aurora": 0.0039, + "avalanche": 0.32, "b3": 0.00568, "base": 0.186, - "berachain": 1.07, + "berachain": 1.83, "bitlayer": 0.00228, "blast": 0.0061, - "bob": 0.00354, - "boba": 0.00366, - "bouncebit": 42, + "bob": 0.00394, + "boba": 0.0039, + "bouncebit": 60.8, "bsc": 0.528, "bsquared": 0.00384, - "celo": 15.3, - "cheesechain": 13600, - "chilizmainnet": 127, - "conflux": 65.6, - "conwai": 2620, + "celo": 20.2, + "cheesechain": 21200, + "chilizmainnet": 168, + "conflux": 89, + "conwai": 3520, "coredao": 13.6, "corn": 0.0000712, "coti": 0.025, "cyber": 0.00642, "deepbrainchain": 25, - "degenchain": 1750, - "dogechain": 31, - "duckchain": 1.73, + "degenchain": 2460, + "dogechain": 39.4, + "duckchain": 2.1, "eclipsemainnet": 0.05, - "endurance": 8.3, + "endurance": 12.6, "ethereum": 0.27, "everclear": 0.0162, - "evmos": 1250, + "evmos": 1990, "fantom": 14.8, - "flame": 2.06, + "flame": 2.6, "flare": 416, - "flowmainnet": 14.4, - "form": 0.00352, + "flowmainnet": 16.9, + "form": 0.0039, "fraxtal": 0.0434, "fusemainnet": 508, "glue": 57.6, "gnosis": 6.24, - "gravity": 348, - "guru": 1520, - "harmony": 486, + "gravity": 438, + "guru": 2780, + "harmony": 570, "hemi": 0.00414, "hyperevm": 1, - "immutablezkevmmainnet": 9.38, - "inevm": 0.576, - "injective": 0.576, + "immutablezkevmmainnet": 13.5, + "inevm": 0.758, + "injective": 0.758, "ink": 0.0466, - "kaia": 55.6, - "kroma": 0.00354, + "kaia": 62.2, + "kroma": 0.0039, "linea": 0.22, "lisk": 0.0208, "lukso": 7.32, - "lumia": 11.9, - "lumiaprism": 13.4, + "lumia": 18.6, + "lumiaprism": 22, "mantapacific": 0.02, "mantle": 15, "matchain": 0.0107, "merlin": 0.000344, "metal": 0.0158, "metis": 1, - "mint": 0.00506, + "mint": 0.00544, "mode": 0.0458, - "molten": 27.6, - "moonbeam": 68.2, + "molten": 48.8, + "moonbeam": 93, "morph": 0.356, "nero": 6.26, - "neutron": 40.4, + "neutron": 51, "nibiru": 2.5, "oortmainnet": 400, - "opbnb": 0.00992, + "opbnb": 0.0106, "optimism": 0.083, - "orderly": 0.00498, + "orderly": 0.00562, "plume": 0.0125, - "polygon": 26.2, + "polygon": 32.8, "polygonzkevm": 0.0488, "polynomialfi": 0.0055, "prom": 3.1, - "proofofplay": 0.00304, + "proofofplay": 0.0039, "rarichain": 0.00658, "reactive": 0.025, - "real": 0.00304, + "real": 0.0039, "redstone": 0.00434, - "rivalz": 0.00304, - "ronin": 7.78, + "rivalz": 0.0039, + "ronin": 12.5, "rootstockmainnet": 0.0033, - "sanko": 0.618, + "sanko": 0.966, "scroll": 0.022, - "sei": 28.8, + "sei": 36.8, "shibarium": 22.6, - "snaxchain": 0.00444, + "snaxchain": 0.00566, "solanamainnet": 10, "soneium": 0.0788, "sonic": 14.8, "sonicsvm": 0.5, "soon": 0.02, - "sophon": 5, + "sophon": 12.5, "story": 3.34, "stride": 31, "subtensor": 0.456, "superpositionmainnet": 0.047, "superseed": 0.0252, - "swell": 0.013, + "swell": 0.0171, "taiko": 0.019, "tangle": 1.5, "telos": 70.2, "torus": 45.4, "treasure": 250, - "trumpchain": 0.528, + "trumpchain": 0.752, "unichain": 0.0179, - "unitzero": 21.6, + "unitzero": 26.6, "vana": 0.00025, - "viction": 25.2, + "viction": 31.2, "worldchain": 0.05, - "xai": 88.6, + "xai": 142, "xlayer": 0.228, - "xpla": 159, + "xpla": 228, "zeronetwork": 0.0125, - "zetachain": 21.4, + "zetachain": 26.6, "zircuit": 0.0094, "zklink": 0.0125, "zksync": 0.0125, diff --git a/typescript/infra/config/environments/mainnet3/balances/lowUrgencyEngKeyFunderBalance.json b/typescript/infra/config/environments/mainnet3/balances/lowUrgencyEngKeyFunderBalance.json index 8a3e541d658..d19b9f5f158 100644 --- a/typescript/infra/config/environments/mainnet3/balances/lowUrgencyEngKeyFunderBalance.json +++ b/typescript/infra/config/environments/mainnet3/balances/lowUrgencyEngKeyFunderBalance.json @@ -1,134 +1,134 @@ { "abstract": 0.0257, - "alephzeroevmmainnet": 175, - "ancient8": 0.0122, - "apechain": 32.8, + "alephzeroevmmainnet": 203, + "ancient8": 0.0143, + "apechain": 42.2, "appchain": 0.093, - "arbitrum": 0.185, - "arbitrumnova": 0.00912, - "arcadia": 0.00912, - "artela": 4030, + "arbitrum": 0.206, + "arbitrumnova": 0.0117, + "arcadia": 0.0117, + "artela": 1650, "arthera": 0.375, - "astar": 544, - "aurora": 0.009, - "avalanche": 0.816, + "astar": 696, + "aurora": 0.0117, + "avalanche": 0.96, "b3": 0.017, "base": 0.559, - "berachain": 3.2, + "berachain": 5.5, "bitlayer": 0.00684, "blast": 0.0183, - "bob": 0.0106, - "boba": 0.011, - "bouncebit": 126, + "bob": 0.0118, + "boba": 0.0117, + "bouncebit": 182, "bsc": 1.58, "bsquared": 0.0115, - "celo": 45.8, - "cheesechain": 40900, - "chilizmainnet": 380, - "conflux": 197, - "conwai": 7860, + "celo": 60.6, + "cheesechain": 63600, + "chilizmainnet": 504, + "conflux": 267, + "conwai": 10600, "coredao": 40.9, "corn": 0.000214, "coti": 0.075, "cyber": 0.0193, "deepbrainchain": 75, - "degenchain": 5250, - "dogechain": 93, - "duckchain": 5.18, + "degenchain": 7380, + "dogechain": 118, + "duckchain": 6.3, "eclipsemainnet": 0.15, - "endurance": 24.9, + "endurance": 37.7, "ethereum": 0.81, "everclear": 0.0486, - "evmos": 3740, + "evmos": 5960, "fantom": 44.3, - "flame": 6.18, + "flame": 7.8, "flare": 1250, - "flowmainnet": 43.1, - "form": 0.0106, + "flowmainnet": 50.6, + "form": 0.0117, "fraxtal": 0.13, "fusemainnet": 1520, "glue": 173, "gnosis": 18.7, - "gravity": 1040, - "guru": 4550, - "harmony": 1460, + "gravity": 1310, + "guru": 8340, + "harmony": 1710, "hemi": 0.0124, "hyperevm": 1.76, - "immutablezkevmmainnet": 28.1, - "inevm": 1.73, - "injective": 1.73, + "immutablezkevmmainnet": 40.4, + "inevm": 2.27, + "injective": 2.27, "ink": 0.14, - "kaia": 167, - "kroma": 0.0106, + "kaia": 187, + "kroma": 0.0117, "linea": 0.459, "lisk": 0.0624, "lukso": 22, - "lumia": 35.6, - "lumiaprism": 40.3, + "lumia": 55.9, + "lumiaprism": 66, "mantapacific": 0.06, "mantle": 45.1, "matchain": 0.0322, "merlin": 0.00103, "metal": 0.0475, "metis": 2.5, - "mint": 0.0152, + "mint": 0.0163, "mode": 0.137, - "molten": 82.8, - "moonbeam": 205, + "molten": 146, + "moonbeam": 279, "morph": 1.07, "nero": 18.8, - "neutron": 121, + "neutron": 153, "nibiru": 7.5, "oortmainnet": 1800, - "opbnb": 0.0298, + "opbnb": 0.0317, "optimism": 0.23, - "orderly": 0.0149, + "orderly": 0.0169, "plume": 0.0375, - "polygon": 78.6, + "polygon": 98.4, "polygonzkevm": 0.146, "polynomialfi": 0.0165, "prom": 9.3, - "proofofplay": 0.00912, + "proofofplay": 0.0117, "rarichain": 0.0197, "reactive": 0.075, - "real": 0.00912, + "real": 0.0117, "redstone": 0.013, - "rivalz": 0.00912, - "ronin": 23.3, + "rivalz": 0.0117, + "ronin": 37.4, "rootstockmainnet": 0.0099, - "sanko": 1.85, + "sanko": 2.9, "scroll": 0.0571, - "sei": 86.4, + "sei": 110, "shibarium": 67.8, - "snaxchain": 0.0133, + "snaxchain": 0.017, "solanamainnet": 30, "soneium": 0.236, "sonic": 44.3, "sonicsvm": 0.99, "soon": 0.05, - "sophon": 15, + "sophon": 37.5, "story": 10, "stride": 93, "subtensor": 1.37, "superpositionmainnet": 0.141, "superseed": 0.0756, - "swell": 0.0391, + "swell": 0.0514, "taiko": 0.03, "tangle": 4.5, "telos": 211, "torus": 136, "treasure": 618, - "trumpchain": 1.58, + "trumpchain": 2.26, "unichain": 0.0538, - "unitzero": 64.8, + "unitzero": 79.8, "vana": 0.00075, - "viction": 75.6, + "viction": 93.6, "worldchain": 0.15, - "xai": 266, + "xai": 427, "xlayer": 0.684, - "xpla": 476, + "xpla": 684, "zeronetwork": 0.0375, - "zetachain": 64.2, + "zetachain": 79.8, "zircuit": 0.0254, "zklink": 0.0375, "zksync": 0.0375, diff --git a/typescript/infra/config/environments/mainnet3/balances/lowUrgencyKeyFunderBalance.json b/typescript/infra/config/environments/mainnet3/balances/lowUrgencyKeyFunderBalance.json index 8dece748a66..007b0aa1ca5 100644 --- a/typescript/infra/config/environments/mainnet3/balances/lowUrgencyKeyFunderBalance.json +++ b/typescript/infra/config/environments/mainnet3/balances/lowUrgencyKeyFunderBalance.json @@ -1,70 +1,70 @@ { "abstract": 0.0514, - "alephzeroevmmainnet": 350, - "ancient8": 0.0245, + "alephzeroevmmainnet": 407, + "ancient8": 0.0286, "apechain": 100, "appchain": 0.186, - "arbitrum": 0.37, - "arbitrumnova": 0.0182, - "arcadia": 0.0182, - "artela": 8050, + "arbitrum": 0.413, + "arbitrumnova": 0.0234, + "arcadia": 0.0234, + "artela": 3300, "arthera": 0.75, - "astar": 1090, - "aurora": 0.018, - "avalanche": 1.63, + "astar": 1390, + "aurora": 0.0234, + "avalanche": 1.92, "b3": 0.0341, "base": 1.12, - "berachain": 6.41, + "berachain": 11, "bitlayer": 0.0137, "blast": 0.0366, - "bob": 0.0212, - "boba": 0.022, - "bouncebit": 252, + "bob": 0.0236, + "boba": 0.0234, + "bouncebit": 365, "bsc": 8, "bsquared": 0.023, "celo": 308, - "cheesechain": 81700, - "chilizmainnet": 760, - "conflux": 394, - "conwai": 15700, + "cheesechain": 127000, + "chilizmainnet": 1010, + "conflux": 534, + "conwai": 21100, "coredao": 81.8, "corn": 0.000427, "coti": 0.15, "cyber": 0.0385, "deepbrainchain": 150, - "degenchain": 10500, - "dogechain": 200, - "duckchain": 10.4, + "degenchain": 14800, + "dogechain": 236, + "duckchain": 12.6, "eclipsemainnet": 0.3, - "endurance": 49.8, + "endurance": 75.5, "ethereum": 1.8, "everclear": 0.1, - "evmos": 7490, + "evmos": 11900, "fantom": 88.7, - "flame": 12.4, + "flame": 15.6, "flare": 2500, - "flowmainnet": 86.3, - "form": 0.0211, + "flowmainnet": 101, + "form": 0.0234, "fraxtal": 0.4, "fusemainnet": 3050, "glue": 346, "gnosis": 37.4, - "gravity": 2090, - "guru": 9100, - "harmony": 2920, + "gravity": 2630, + "guru": 16700, + "harmony": 3420, "hemi": 0.0248, "hyperevm": 10, - "immutablezkevmmainnet": 56.3, + "immutablezkevmmainnet": 80.9, "inevm": 6.1, - "injective": 3.46, + "injective": 4.55, "ink": 0.28, "kaia": 500, "kroma": 0.1, "linea": 2, "lisk": 0.2, "lukso": 43.9, - "lumia": 71.3, - "lumiaprism": 80.5, + "lumia": 112, + "lumiaprism": 132, "mantapacific": 0.4, "mantle": 90.1, "matchain": 0.0644, @@ -73,14 +73,14 @@ "metis": 6, "mint": 0.1, "mode": 0.4, - "molten": 166, + "molten": 293, "moonbeam": 700, "morph": 2.14, "nero": 37.6, - "neutron": 242, + "neutron": 306, "nibiru": 15, "oortmainnet": 4000, - "opbnb": 0.0595, + "opbnb": 0.0634, "optimism": 1.2, "orderly": 0.1, "plume": 0.075, @@ -94,11 +94,11 @@ "real": 0.2, "redstone": 0.4, "rivalz": 0.1, - "ronin": 46.7, + "ronin": 74.8, "rootstockmainnet": 0.0198, - "sanko": 3.71, + "sanko": 5.8, "scroll": 1.1, - "sei": 173, + "sei": 221, "shibarium": 136, "snaxchain": 0.075, "solanamainnet": 60, @@ -106,29 +106,29 @@ "sonic": 88.7, "sonicsvm": 1.98, "soon": 0.1, - "sophon": 30, + "sophon": 75, "story": 20, "stride": 186, "subtensor": 2.74, "superpositionmainnet": 0.282, "superseed": 0.151, - "swell": 0.1, + "swell": 0.103, "taiko": 0.4, "tangle": 9, "telos": 421, "torus": 272, "treasure": 1800, - "trumpchain": 3.17, + "trumpchain": 4.51, "unichain": 0.2, - "unitzero": 130, + "unitzero": 160, "vana": 0.0015, - "viction": 151, + "viction": 187, "worldchain": 0.4, - "xai": 532, + "xai": 854, "xlayer": 1.37, - "xpla": 953, + "xpla": 1370, "zeronetwork": 0.1, - "zetachain": 128, + "zetachain": 160, "zircuit": 0.0509, "zklink": 0.075, "zksync": 0.1, diff --git a/typescript/infra/config/environments/mainnet3/chains.ts b/typescript/infra/config/environments/mainnet3/chains.ts index 0d7e9941dcb..bc040ff8014 100644 --- a/typescript/infra/config/environments/mainnet3/chains.ts +++ b/typescript/infra/config/environments/mainnet3/chains.ts @@ -74,7 +74,7 @@ export const chainMetadataOverrides: ChainMap> = { // }, // zircuit: { // blocks: { - // confirmations: 3, + // confirmations: 5, // }, // }, // degenchain: { @@ -101,6 +101,11 @@ export const chainMetadataOverrides: ChainMap> = { // confirmations: 5, // }, // }, + // cyber: { + // blocks: { + // confirmations: 3, + // }, + // }, }; export const getRegistry = async ( diff --git a/typescript/infra/config/environments/mainnet3/funding.ts b/typescript/infra/config/environments/mainnet3/funding.ts index 5bd4db8e679..d89c7e3c8d5 100644 --- a/typescript/infra/config/environments/mainnet3/funding.ts +++ b/typescript/infra/config/environments/mainnet3/funding.ts @@ -19,7 +19,7 @@ export const keyFunderConfig: KeyFunderConfig< > = { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: 'f2bf607-20250328-181346', + tag: '2d0234b-20250422-165314', }, // We're currently using the same deployer/key funder key as mainnet2. // To minimize nonce clobbering we offset the key funder cron @@ -32,7 +32,9 @@ export const keyFunderConfig: KeyFunderConfig< contextsAndRolesToFund: { [Contexts.Hyperlane]: [Role.Relayer, Role.Kathy], [Contexts.ReleaseCandidate]: [Role.Relayer, Role.Kathy], + [Contexts.Vanguard0]: [Role.Relayer], }, + chainsToSkip: [], // desired balance config, must be set for each chain desiredBalancePerChain: desiredRelayerBalancePerChain, // if not set, keyfunder defaults to 0 diff --git a/typescript/infra/config/environments/mainnet3/gasPrices.json b/typescript/infra/config/environments/mainnet3/gasPrices.json index 44bb6994933..bc265a7db57 100644 --- a/typescript/infra/config/environments/mainnet3/gasPrices.json +++ b/typescript/infra/config/environments/mainnet3/gasPrices.json @@ -20,7 +20,7 @@ "decimals": 9 }, "arbitrum": { - "amount": "0.068903", + "amount": "0.01", "decimals": 9 }, "arbitrumnova": { @@ -36,17 +36,13 @@ "decimals": 9 }, "arthera": { - "amount": "1.025041", + "amount": "1.02503", "decimals": 9 }, "astar": { "amount": "779.371350051", "decimals": 9 }, - "astarzkevm": { - "amount": "0.0137", - "decimals": 9 - }, "aurora": { "amount": "0.07", "decimals": 9 @@ -56,11 +52,11 @@ "decimals": 9 }, "flame": { - "amount": "20.0", + "amount": "12.1", "decimals": 9 }, "avalanche": { - "amount": "1.46913265", + "amount": "0.097870287", "decimals": 9 }, "b3": { @@ -68,11 +64,11 @@ "decimals": 9 }, "base": { - "amount": "0.002901959", + "amount": "0.003076709", "decimals": 9 }, "berachain": { - "amount": "0.002338637", + "amount": "0.000992963", "decimals": 9 }, "bitlayer": { @@ -80,7 +76,7 @@ "decimals": 9 }, "blast": { - "amount": "0.002047803", + "amount": "0.001450792", "decimals": 9 }, "bob": { @@ -88,7 +84,7 @@ "decimals": 9 }, "boba": { - "amount": "0.001000082", + "amount": "0.001000096", "decimals": 9 }, "bsc": { @@ -100,7 +96,7 @@ "decimals": 9 }, "celo": { - "amount": "50.0", + "amount": "25.001", "decimals": 9 }, "cheesechain": { @@ -108,7 +104,7 @@ "decimals": 9 }, "chilizmainnet": { - "amount": "5000.0", + "amount": "2501.0", "decimals": 9 }, "conflux": { @@ -128,7 +124,7 @@ "decimals": 9 }, "coti": { - "amount": "0.005000007", + "amount": "1.500000007", "decimals": 9 }, "cyber": { @@ -160,7 +156,7 @@ "decimals": 9 }, "ethereum": { - "amount": "1.718727762", + "amount": "7.5", "decimals": 9 }, "everclear": { @@ -168,11 +164,11 @@ "decimals": 9 }, "evmos": { - "amount": "27.5", + "amount": "33.375931531", "decimals": 9 }, "fantom": { - "amount": "1.023229", + "amount": "1.026999", "decimals": 9 }, "flare": { @@ -188,7 +184,7 @@ "decimals": 9 }, "fraxtal": { - "amount": "0.00100101", + "amount": "0.001000253", "decimals": 9 }, "fusemainnet": { @@ -200,7 +196,7 @@ "decimals": 9 }, "gnosis": { - "amount": "1.000000007", + "amount": "1.000000008", "decimals": 9 }, "gravity": { @@ -224,7 +220,7 @@ "decimals": 9 }, "immutablezkevmmainnet": { - "amount": "11.00000005", + "amount": "11.000000049", "decimals": 9 }, "inevm": { @@ -236,7 +232,7 @@ "decimals": 9 }, "ink": { - "amount": "0.001000253", + "amount": "0.001000257", "decimals": 9 }, "injective": { @@ -252,7 +248,7 @@ "decimals": 9 }, "linea": { - "amount": "0.1074276", + "amount": "0.055646648", "decimals": 9 }, "lisk": { @@ -260,19 +256,19 @@ "decimals": 9 }, "lukso": { - "amount": "0.243957052", + "amount": "0.100026017", "decimals": 9 }, "lumia": { - "amount": "1.71", + "amount": "1.0", "decimals": 9 }, "lumiaprism": { - "amount": "1.71", + "amount": "1.0", "decimals": 9 }, "mantapacific": { - "amount": "0.003026419", + "amount": "0.003000488", "decimals": 9 }, "mantle": { @@ -280,7 +276,7 @@ "decimals": 9 }, "matchain": { - "amount": "0.001000008", + "amount": "0.001", "decimals": 9 }, "merlin": { @@ -292,7 +288,7 @@ "decimals": 9 }, "metis": { - "amount": "3.003118323", + "amount": "3.76552424", "decimals": 9 }, "mint": { @@ -300,7 +296,7 @@ "decimals": 9 }, "mode": { - "amount": "0.00100032", + "amount": "0.001000314", "decimals": 9 }, "molten": { @@ -312,7 +308,7 @@ "decimals": 9 }, "morph": { - "amount": "0.901", + "amount": "0.063", "decimals": 9 }, "nero": { @@ -336,7 +332,7 @@ "decimals": 9 }, "optimism": { - "amount": "0.001345842", + "amount": "0.002404968", "decimals": 9 }, "orderly": { @@ -348,15 +344,15 @@ "decimals": 1 }, "plume": { - "amount": "0.01", + "amount": "100.0", "decimals": 9 }, "polygon": { - "amount": "91.760798759", + "amount": "30.000000034", "decimals": 9 }, "polygonzkevm": { - "amount": "0.0257", + "amount": "0.01", "decimals": 9 }, "polynomialfi": { @@ -404,15 +400,15 @@ "decimals": 9 }, "scroll": { - "amount": "0.039318533", + "amount": "0.046868564", "decimals": 9 }, "sei": { - "amount": "1.1", + "amount": "1.100326643", "decimals": 9 }, "shibarium": { - "amount": "3.417583731", + "amount": "2.523983809", "decimals": 9 }, "snaxchain": { @@ -424,7 +420,7 @@ "decimals": 1 }, "soneium": { - "amount": "0.069907155", + "amount": "0.001134841", "decimals": 9 }, "sonic": { @@ -440,11 +436,11 @@ "decimals": 1 }, "sophon": { - "amount": "2214.613224136", + "amount": "2022.206337798", "decimals": 9 }, "story": { - "amount": "0.001298711", + "amount": "0.001000186", "decimals": 9 }, "stride": { @@ -452,7 +448,7 @@ "decimals": 1 }, "subtensor": { - "amount": "10.0", + "amount": "10.14637", "decimals": 9 }, "superseed": { @@ -468,7 +464,7 @@ "decimals": 9 }, "taiko": { - "amount": "0.010517821", + "amount": "0.008988459", "decimals": 9 }, "tangle": { @@ -476,7 +472,7 @@ "decimals": 9 }, "telos": { - "amount": "525.424236562", + "amount": "527.745263691", "decimals": 9 }, "torus": { @@ -484,7 +480,7 @@ "decimals": 9 }, "treasure": { - "amount": "767.364249527", + "amount": "1023.72101162", "decimals": 9 }, "trumpchain": { @@ -492,15 +488,15 @@ "decimals": 9 }, "unichain": { - "amount": "0.001000254", + "amount": "0.001000253", "decimals": 9 }, "unitzero": { - "amount": "11.000000042", + "amount": "2.000000007", "decimals": 9 }, "vana": { - "amount": "0.312673957", + "amount": "0.001006296", "decimals": 9 }, "viction": { @@ -508,7 +504,7 @@ "decimals": 9 }, "worldchain": { - "amount": "0.001000292", + "amount": "0.001000319", "decimals": 9 }, "xai": { diff --git a/typescript/infra/config/environments/mainnet3/governance/ica/aw.ts b/typescript/infra/config/environments/mainnet3/governance/ica/aw.ts new file mode 100644 index 00000000000..77c0e572b5a --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/ica/aw.ts @@ -0,0 +1,169 @@ +// Found by running: +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +// yarn tsx ./scripts/keys/get-owner-ica.ts -e mainnet3 --ownerChain ethereum --destinationChains ... +export const awIcas: ChainMap
= { + viction: '0x23ed65DE22ac29Ec1C16E75EddB0cE3A187357b4', + inevm: '0xFDF9EDcb2243D51f5f317b9CEcA8edD2bEEE036e', + + // Jul 26, 2024 batch + // ---------------------------------------------------------- + xlayer: '0x1571c482fe9E76bbf50829912b1c746792966369', + cheesechain: '0xEe2C5320BE9bC7A1492187cfb289953b53E3ff1b', + worldchain: '0x1996DbFcFB433737fE404F58D2c32A7f5f334210', + // zircuit: '0x0d67c56E818a02ABa58cd2394b95EF26db999aA3', // already has a safe + + // Aug 5, 2024 batch + // ---------------------------------------------------------- + cyber: '0x984Fe5a45Ac4aaeC4E4655b50f776aB79c9Be19F', + degenchain: '0x22d952d3b9F493442731a3c7660aCaD98e55C00A', + kroma: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', + lisk: '0x22d952d3b9F493442731a3c7660aCaD98e55C00A', + lukso: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', + merlin: '0xCf867cEaeeE8CBe65C680c734D29d26440931D5b', + metis: '0xb51e63CD0842D670a13c88B159fCFc268DA652A3', + mint: '0xb51e63CD0842D670a13c88B159fCFc268DA652A3', + proofofplay: '0xb51e63CD0842D670a13c88B159fCFc268DA652A3', + real: '0xc761e68BF3A94326FD0D305e3ccb4cdaab2edA19', + sanko: '0x5DAcd2f1AafC749F2935A160865Ab1568eC23752', + tangle: '0xCC2aeb692197C7894E561d31ADFE8F79746f7d9F', + xai: '0x22d952d3b9F493442731a3c7660aCaD98e55C00A', + // taiko: '0x483D218D2FEe7FC7204ba15F00C7901acbF9697D', // renzo chain + + // Aug 26, 2024 batch + // ---------------------------------------------------------- + astar: '0x6b241544eBa7d89B51b72DF85a0342dAa37371Ca', + bitlayer: '0xe6239316cA60814229E801fF0B9DD71C9CA29008', + coredao: '0x84802CdF47565C95d8ffd59E7c4B1cf027F5452F', + dogechain: '0x84802CdF47565C95d8ffd59E7c4B1cf027F5452F', + flare: '0x689b8DaBBF2f9Fd83D37427A062B30edF463e20b', + molten: '0x84802CdF47565C95d8ffd59E7c4B1cf027F5452F', + shibarium: '0x6348FAe3a8374dbAAaE84EEe5458AE4063Fe2be7', + + // Sep 9, 2024 batch + // ---------------------------------------------------------- + everclear: '0x63B2075372D6d0565F51210D0A296D3c8a773aB6', + oortmainnet: '0x7021D11F9fAe455AB2f45D72dbc2C64d116Cb657', + + // Sep 19, 2024 SAFE --> ICA v1 Migration + // ---------------------------------------------------------- + celo: '0x3fA264c58E1365f1d5963B831b864EcdD2ddD19b', + avalanche: '0x8c8695cD9905e22d84E466804ABE55408A87e595', + polygon: '0xBDD25dd5203fedE33FD631e30fEF9b9eF2598ECE', + moonbeam: '0x480e5b5De6a29F07fe8295C60A1845d36b7BfdE6', + gnosis: '0xD42125a4889A7A36F32d7D12bFa0ae52B0AD106b', + scroll: '0x2a3fe2513F4A7813683d480724AB0a3683EfF8AC', + polygonzkevm: '0x66037De438a59C966214B78c1d377c4e93a5C7D1', + ancient8: '0xA9FD5BeB556AB1859D7625B381110a257f56F98C', + redstone: '0x5DAcd2f1AafC749F2935A160865Ab1568eC23752', + mantle: '0x08C880b88335CA3e85Ebb4E461245a7e899863c9', + bob: '0xc99e58b9A4E330e2E4d09e2c94CD3c553904F588', + zetachain: '0xc876B8e63c3ff5b636d9492715BE375644CaD345', + zoramainnet: '0x84977Eb15E0ff5824a6129c789F70e88352C230b', + fusemainnet: '0xbBdb1682B2922C282b56DD716C29db5EFbdb5632', + endurance: '0x470E04D8a3b7938b385093B93CeBd8Db7A1E557C', + // sei: '0xabad187003EdeDd6C720Fc633f929EA632996567', // renzo chain + + // Oct 16, 2024 batch + // ---------------------------------------------------------- + // lumia: '0x418E10Ac9e0b84022d0636228d05bc74172e0e41', + + // Oct 30, 2024 batch + // ---------------------------------------------------------- + apechain: '0xe68b0aB6BB8c11D855556A5d3539524f6DB3bdc6', + arbitrumnova: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', + b3: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', + fantom: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', + gravity: '0x3104ADE26e21AEbdB325321433541DfE8B5dCF23', + harmony: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', + kaia: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', + morph: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', + orderly: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', + snaxchain: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', + + // Nov 8, 2024 batch + // ---------------------------------------------------------- + alephzeroevmmainnet: '0xDE91AC081E12107a033728A287b06B1Fc640A637', + chilizmainnet: '0x54AF0FCDCD58428f8dF3f825267DfB58f2C710eb', + flowmainnet: '0x65528D447C93CC1A1A7186CB4449d9fE0d5C1928', + immutablezkevmmainnet: '0x54AF0FCDCD58428f8dF3f825267DfB58f2C710eb', + metal: '0xf1d25462e1f82BbF25b3ef7A4C94F738a30a968B', + polynomialfi: '0x6ACa36E710dC0C80400090EA0bC55dA913a3D20D', + rarichain: '0xD0A4Ad2Ca0251BBc6541f8c2a594F1A82b67F114', + rootstockmainnet: '0x0C15f7479E0B46868693568a3f1C747Fdec9f17d', + superpositionmainnet: '0x5F17Dc2e1fd1371dc6e694c51f22aBAF8E27667B', + flame: '0x4F3d85360840497Cd1bc34Ca55f27629eee2AA2e', + prom: '0x1cDd3C143387cD1FaE23e2B66bc3F409D073aC3D', + + // Nov 21, 2024 batch + // ---------------------------------------------------------- + boba: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', + duckchain: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', + unichain: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', + vana: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', + bsquared: '0xd9564EaaA68A327933f758A54450D3A0531E60BB', + superseed: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', + + // Dec 4, 2024 batch + // ---------------------------------------------------------- + // swell: '0xff8326468e7AaB51c53D3569cf7C45Dd54c11687', // already has a safe + lumiaprism: '0xAFfA863646D1bC74ecEC0dB1070f069Af065EBf5', + appchain: '0x4F25DFFd10A6D61C365E1a605d07B2ab0E82A7E6', + + // Dec 13, 2024 batch + // ---------------------------------------------------------- + arthera: '0x962e4E5F5e47e1Ab5361eE0B5108Ebeb9Fa5c99B', + aurora: '0x853f40c807cbb08EDd19B326b9b6A669bf3c274c', + conflux: '0xac8f0e306A126312C273080d149ca01d461603FE', + conwai: '0x5926599B8Aff45f1708b804B30213babdAD78C83', + corn: '0x5926599B8Aff45f1708b804B30213babdAD78C83', + evmos: '0x5926599B8Aff45f1708b804B30213babdAD78C83', + form: '0x5926599B8Aff45f1708b804B30213babdAD78C83', + ink: '0xDde4Ce691d1c0579d48BCdd3491aA71472b6cC38', + rivalz: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', + soneium: '0x5926599B8Aff45f1708b804B30213babdAD78C83', + sonic: '0x5926599B8Aff45f1708b804B30213babdAD78C83', + telos: '0xDde4Ce691d1c0579d48BCdd3491aA71472b6cC38', + + // Jan 13, 2025 batch + // ---------------------------------------------------------- + artela: '0x745CEA119757ea3e27093da590bC91f408bD4448', + guru: '0x825cF3d703F384E4aA846BA72eCf70f1985C91b6', + hemi: '0x8D18CBB212920e5ef070b23b813d82F8981cC276', + nero: '0xbBdb1682B2922C282b56DD716C29db5EFbdb5632', + torus: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', + xpla: '0x24832680dF0468967F413be1C83acfE24154F88D', + + // Feb 3, 2025 batch + // ---------------------------------------------------------- + glue: '0x24832680dF0468967F413be1C83acfE24154F88D', + matchain: '0x66af72e46b3e8DFc19992A2A88C05d9EEFE01ffB', + unitzero: '0x66af72e46b3e8DFc19992A2A88C05d9EEFE01ffB', + trumpchain: '0x56895bFa7f7dFA5743b2A0994B5B0f88b88350F9', + + // Q5, 2024 batch + // ---------------------------------------------------------- + // berachain: '0x56895bFa7f7dFA5743b2A0994B5B0f88b88350F9', + + // Feb 17, 2025 batch + // ---------------------------------------------------------- + bouncebit: '0x8768A14AA6eD2A62C77155501E742376cbE97981', + arcadia: '0xD2344a364b6Dc6B2Fe0f7D836fa344d83056cbaD', + ronin: '0x8768A14AA6eD2A62C77155501E742376cbE97981', + story: '0x8768A14AA6eD2A62C77155501E742376cbE97981', + subtensor: '0x61BFbb5FEC57f5470388A80946F0415138630b9c', + + // Mar 14, 2025 batch + // ---------------------------------------------------------- + infinityvm: '0x35460c519b7C71d49C64F060dF89AbAE463F3b9a', + plume: '0x61BFbb5FEC57f5470388A80946F0415138630b9c', + + // Mar 31, 2025 batch + // ---------------------------------------------------------- + coti: '0x294589E4913A132A49F7830a2A219363A25c0529', + deepbrainchain: '0xeFb7D10Da69A0a913485851ccec6B85cF98d9cab', + // nibiru: '0x40cD75e80d04663FAe0CE30687504074F163C346', // temporary while looking into decimals + opbnb: '0xeFb7D10Da69A0a913485851ccec6B85cF98d9cab', + reactive: '0x9312B04076efA12D69b95bcE7F4F0EA847073E6a', +} as const; diff --git a/typescript/infra/config/environments/mainnet3/governance/ica/regular.ts b/typescript/infra/config/environments/mainnet3/governance/ica/regular.ts new file mode 100644 index 00000000000..57e8add8029 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/ica/regular.ts @@ -0,0 +1,110 @@ +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +export const regularIcas: ChainMap
= { + ancient8: '0xf789D8a609247c448E28d3af5b8EFC6Cb786C4ee', + alephzeroevmmainnet: '0xA3dfBa2447F0A50706F22e38f2785e0Bf30BC069', + apechain: '0x9422838f6fA763354756a3Aba18f34015cB4bF74', + appchain: '0x8A4E9b5B445504727D7a9377eA329Bb38700F8FA', + arbitrumnova: '0x83be90021870A93ff794E00E8CFe454547877E3E', + arcadia: '0x32879753603c140E34D46375F43Db8BdC9a8c545', + artela: '0x2754282484EBC6103042BE6c2f17acFA96B2546a', + arthera: '0x33fab5e0290b7b7a0f19Ba9Db8b97C6e7f7a8848', + astar: '0x87433Bf4d46fB11C6c39c6F1876b95c9001D06E1', + aurora: '0x793C99f45CF63dd101D7D6819e02333Fb6cFd57f', + bouncebit: '0xa3EC6913675e9686bfC458F02a0F737EdD0362c8', + flame: '0x355e54B6D423db49735Cb701452bd98434A90BAd', + avalanche: '0x3a1014df0202477a1222999c72bD36395904e8AB', + b3: '0x83be90021870A93ff794E00E8CFe454547877E3E', + bitlayer: '0x39EBb0D2b62D623BBEB874505079a21d05A7Ab9d', + bob: '0x4FB145Da6407F4F485A209332a38A5327B61f83e', + boba: '0xEc65c06c606006Db682197230bF8E7740C2BcFF9', + bsquared: '0x7C475877fc180f5aAB628Ae1B506316a0F3ADE5A', + celo: '0x20D701Ac137BB131e735B403e0471b101423dDeC', + cheesechain: '0x5e4277969e2EEEbe04091dc270c2363b6e694F8d', + chilizmainnet: '0x97827F6191296077587929Ca870620bD5D79F9e7', + conflux: '0x04b7cBD37eeFe304655c7e8638BbE4ddEff576E8', + conwai: '0x4BAe702C0D1cC7d6d343594735812b5723041528', + coredao: '0x100D940B953b20378242Fe0FbC45792805d50556', + corn: '0x4BAe702C0D1cC7d6d343594735812b5723041528', + coti: '0x54b625734673868027c70F656B807588910f3a6f', + cyber: '0x162F8F6B70f9EE57fED16C4337B90e939851ECA1', + deepbrainchain: '0x1fbE174fc6B1d3123B890f34aBd982577377Bec8', + degenchain: '0xF01B503E5cC884f0EB6eF062F704A610FD69842D', + dogechain: '0x100D940B953b20378242Fe0FbC45792805d50556', + duckchain: '0xEc65c06c606006Db682197230bF8E7740C2BcFF9', + endurance: '0xd8b01D2fA7F50889eE0f51114D00ab4c8581A5F4', + everclear: '0x975866f773aC8f6d816089B0A5Ab9DC8A01c9462', + evmos: '0x4BAe702C0D1cC7d6d343594735812b5723041528', + fantom: '0x83be90021870A93ff794E00E8CFe454547877E3E', + flare: '0x68be5bC1b1E373f3c6a278D290F39461DDD21968', + flowmainnet: '0x8e9558D9bA4d3FB52ea75D18B337f778589AB1aF', + form: '0x4BAe702C0D1cC7d6d343594735812b5723041528', + fusemainnet: '0x4601d6260C532E73187248D0608cB88D446149AD', + glue: '0x0Fb0635eAbB9332eDc2544A57e7C15fBc5204C0B', + gnosis: '0x2984A89A1A0331ae200FE2Cb46cCa190e795672E', + gravity: '0x4Ce5ecE7349E467EBd77F39Eb4232Dd8C6a7314D', + guru: '0x562FF51582Ec53466c4B8506d3a2FBD3B2F675f4', + harmony: '0x83be90021870A93ff794E00E8CFe454547877E3E', + hemi: '0xa16C2860414e52945Bae3eb957cC0897A21f04a6', + immutablezkevmmainnet: '0x97827F6191296077587929Ca870620bD5D79F9e7', + inevm: '0x8427F6021a84a992008DA6a949cad986CEB0d0b3', + infinityvm: '0xf0c8600D83dC8B517C347cb1D45e02B1F95057bb', + ink: '0xf36dC13eE34034709390b6dF3de7305eA298BFec', + kaia: '0x83be90021870A93ff794E00E8CFe454547877E3E', + kroma: '0xBd887119d776f0c990e9a03B1A157A107CD45033', + lisk: '0xF01B503E5cC884f0EB6eF062F704A610FD69842D', + lukso: '0xBd887119d776f0c990e9a03B1A157A107CD45033', + lumiaprism: '0x60a51cB66CF6012A2adF27090fb7D51aE17369CF', + mantle: '0x9652721a385AF5B410AA0865Df959255a798F761', + matchain: '0x262E9A3eDA180fCb84846BaBa2aEB9EDa88e9BeF', + merlin: '0xe4a82d029b3aB2D8Ed12b6800e86452E979D8c5c', + metal: '0x9E359Bd54B59D28218a47E4C9a1e84568f09aFb4', + metis: '0x319aB070390C8f2A914a61f8DE366f3295bb44fF', + mint: '0x319aB070390C8f2A914a61f8DE366f3295bb44fF', + molten: '0x100D940B953b20378242Fe0FbC45792805d50556', + moonbeam: '0xb3178E470E959340206AD9d9eE4E53d0911b2ba9', + morph: '0x83be90021870A93ff794E00E8CFe454547877E3E', + nero: '0x4601d6260C532E73187248D0608cB88D446149AD', + // nibiru: '0x966A57078204CA6cA13f091eE66eE61Ada5D2318', // temporary while looking into decimals + oortmainnet: '0xBc2cc7372C018C2a0b1F78E29D8B49d21bc5f3bA', + opbnb: '0x1fbE174fc6B1d3123B890f34aBd982577377Bec8', + orderly: '0x83be90021870A93ff794E00E8CFe454547877E3E', + plume: '0xB9d20ea5f3bB574C923F0af67b06b8D87F111819', + polygon: '0xff5b2F0Ae1FFBf6b92Ea9B5851D7643C81102064', + polygonzkevm: '0x258B14038C610f7889C74162760A3A70Ce526CD5', + polynomialfi: '0x45EFBEAD9009C2662a633ee6F307C90B647B7793', + prom: '0x9123625687807F02FC92A9817cFb8db13A9a8B4d', + proofofplay: '0x319aB070390C8f2A914a61f8DE366f3295bb44fF', + rarichain: '0x80c7c5fF8F84BEeDEd18382eb45aA3246063fbc5', + reactive: '0x97B3BE8f063E98Da6ac254716DA194da4634aC80', + real: '0xdc13fF36978e860d2FC98EaFBA4fDeB64E1D864A', + redstone: '0x27C06C13E512Cdd80A2E49fc9c803cFd0de7Ba9e', + rivalz: '0xBd887119d776f0c990e9a03B1A157A107CD45033', + ronin: '0xa3EC6913675e9686bfC458F02a0F737EdD0362c8', + rootstockmainnet: '0x77d190423A18e78C95269f4D1349141D15E433f3', + sanko: '0x27C06C13E512Cdd80A2E49fc9c803cFd0de7Ba9e', + scroll: '0xDD2e306EE337952b4f7C0c4Eb44A8B5b9925Bc76', + shibarium: '0x778f0e4CD1434258A811ab493217f3DC8d501C1b', + snaxchain: '0x83be90021870A93ff794E00E8CFe454547877E3E', + soneium: '0x4BAe702C0D1cC7d6d343594735812b5723041528', + sonic: '0x4BAe702C0D1cC7d6d343594735812b5723041528', + story: '0xa3EC6913675e9686bfC458F02a0F737EdD0362c8', + subtensor: '0xB9d20ea5f3bB574C923F0af67b06b8D87F111819', + superseed: '0xEc65c06c606006Db682197230bF8E7740C2BcFF9', + superpositionmainnet: '0x9f921e87309829ecc61F6df0F2016fD08B80CDD1', + tangle: '0x60109e724a30E89fEB1877fAd006317f8b81768a', + telos: '0xf36dC13eE34034709390b6dF3de7305eA298BFec', + torus: '0xBd887119d776f0c990e9a03B1A157A107CD45033', + trumpchain: '0x3FAA6Ff31e4FC67957792303bA35092Ee0EBd367', + unichain: '0xEc65c06c606006Db682197230bF8E7740C2BcFF9', + unitzero: '0x262E9A3eDA180fCb84846BaBa2aEB9EDa88e9BeF', + vana: '0xEc65c06c606006Db682197230bF8E7740C2BcFF9', + viction: '0x426FC4C5CC60E5e47101fe30d4f8B94F1b7C1C70', + worldchain: '0x51E4E4b7317043A1D6e2fe6ccd9dA51844C385f0', + xai: '0xF01B503E5cC884f0EB6eF062F704A610FD69842D', + xlayer: '0x73581C1c03E64BDE5775b7236B57d78155B75fA7', + xpla: '0x0Fb0635eAbB9332eDc2544A57e7C15fBc5204C0B', + zetachain: '0x33d8605D4dF6661259640268DD32BC99A2F55a83', + zoramainnet: '0xdbB6E8FDe96dE3a1604b93884d8fF9Cb521059fC', +}; diff --git a/typescript/infra/config/environments/mainnet3/governance/safe/aw.ts b/typescript/infra/config/environments/mainnet3/governance/safe/aw.ts new file mode 100644 index 00000000000..f5f96e90736 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/safe/aw.ts @@ -0,0 +1,51 @@ +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +export const awSafes: ChainMap
= { + mantapacific: '0x03ed2D65f2742193CeD99D48EbF1F1D6F12345B6', // does not have a UI + celo: '0x879038d6Fc9F6D5e2BA73188bd078486d77e1156', + ethereum: '0x3965AC3D295641E452E0ea896a086A9cD7C6C5b6', + avalanche: '0x5bE94B17112B8F18eA9Ac8e559377B467556a3c3', + polygon: '0xf9cFD440CfBCfAB8473cc156485B7eE753b2913E', + bsc: '0x7bB2ADeDdC342ffb611dDC073095cc4B8C547170', + arbitrum: '0x03fD5BE9DF85F0017dC7F4DC3068dDF64fffF25e', + optimism: '0xbd7db3821806bc72D223F0AE521Bf82FcBd6Ef4d', + moonbeam: '0x594203849E52BF6ee0E511cD807Ca2D658893e37', + gnosis: '0x0Ac72fBc82c9c39F81242229631dfC38aA13031B', + inevm: '0x77F3863ea99F2360D84d4BA1A2E441857D0357fa', // caldera + injective + base: '0x3949eD0CD036D9FF662d97BD7aC1686051c4aeBF', + scroll: '0x6EeEbB9F7FB18DD5E59F82658c59B846715eD4F7', + polygonzkevm: '0x1610f578D4d77Fc4ae7ce2DD9AA0b98A5Cd0a9b2', + // injective: 'inj1632x8j35kenryam3mkrsez064sqg2y2fr0frzt', + // solana: 'EzppBFV2taxWw8kEjxNYvby6q7W1biJEqwP3iC7YgRe3', + blast: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', + linea: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', + mode: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', + ancient8: '0xD2BFA0F0654E3f2139b8cDC56c32eeC54D32b133', + taiko: '0xa4864301d3fa2a3e68256309F9F0F570270a1BD0', + fraxtal: '0x66e9f52800E9F89F0569fddc594Acd5EE609f762', + sei: '0xCed197FBc360C26C19889745Cf73511b71D03d5D', + redstone: '0xa1a50ff5FD859558E1899fEC5C3064483177FA23', + mantle: '0x8aFE6EECc6CcB02aA20DA8Fff7d29aadEBbc2DCd', + bob: '0x9e2fe7723b018d02cDE4f5cC1A9bC9C65b922Fc8', + zetachain: '0x9d399876522Fc5C044D048594de399A2349d6026', + zoramainnet: '0xF87018025575552889062De4b05bBC3DAe35Cd96', + fusemainnet: '0x29a526227CB864C90Cf078d03872da913B473139', + endurance: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', + zircuit: '0x9e2fe7723b018d02cDE4f5cC1A9bC9C65b922Fc8', + swell: '0x5F7771EA40546e2932754C263455Cb0023a55ca7', + + // Q5, 2024 batch + berachain: '0x5F7771EA40546e2932754C263455Cb0023a55ca7', + + // HyperEVM + hyperevm: '0x5F7771EA40546e2932754C263455Cb0023a55ca7', + + // zksync chains + zeronetwork: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', + abstract: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', + zksync: '0x9C81aA0cC233e9BddeA426F5d395Ab5B65135450', + zklink: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', + treasure: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', + sophon: '0x3D1baf8cA4935f416671640B1Aa9E17E005986eE', +}; diff --git a/typescript/infra/config/environments/mainnet3/governance/safe/irregular.ts b/typescript/infra/config/environments/mainnet3/governance/safe/irregular.ts new file mode 100644 index 00000000000..2b00e65a1ee --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/safe/irregular.ts @@ -0,0 +1,6 @@ +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +export const irregularSafes: ChainMap
= { + ethereum: '0xec2EdC01a2Fbade68dBcc80947F43a5B408cC3A0', +}; diff --git a/typescript/infra/config/environments/mainnet3/governance/safe/regular.ts b/typescript/infra/config/environments/mainnet3/governance/safe/regular.ts new file mode 100644 index 00000000000..acb47bbc64f --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/safe/regular.ts @@ -0,0 +1,27 @@ +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +export const regularSafes: ChainMap
= { + abstract: '0xcd81ccFe7D9306849136Fa96397113345a32ECf3', + arbitrum: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + base: '0x890ac177Fe3052B8676A65f32C1589Bc329f3d50', + berachain: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + blast: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + bsc: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + ethereum: '0x562Dfaac27A84be6C96273F5c9594DA1681C0DA7', + fraxtal: '0x890ac177Fe3052B8676A65f32C1589Bc329f3d50', + hyperevm: '0x290Eb7bbf939A36B2c350a668c04815E49757eDC', + linea: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + mantapacific: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + mode: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + optimism: '0x890ac177Fe3052B8676A65f32C1589Bc329f3d50', + sei: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + sophon: '0x113d3a19031Fe5DB58884D6aa54545dD4De499c0', + swell: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', + taiko: '0x890ac177Fe3052B8676A65f32C1589Bc329f3d50', + treasure: '0xcd81ccFe7D9306849136Fa96397113345a32ECf3', + zeronetwork: '0xcd81ccFe7D9306849136Fa96397113345a32ECf3', + zksync: '0xcd81ccFe7D9306849136Fa96397113345a32ECf3', + zklink: '0xcd81ccFe7D9306849136Fa96397113345a32ECf3', + zircuit: '0x7379D7bB2ccA68982E467632B6554fD4e72e9431', +}; diff --git a/typescript/infra/config/environments/mainnet3/governance/signers/aw.ts b/typescript/infra/config/environments/mainnet3/governance/signers/aw.ts new file mode 100644 index 00000000000..4f98960731f --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/signers/aw.ts @@ -0,0 +1,15 @@ +import { Address } from '@hyperlane-xyz/utils'; + +export const awSigners: Address[] = [ + '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba', // 1 + '0xc3E966E79eF1aA4751221F55fB8A36589C24C0cA', // 2 + '0x3b7f8f68A4FD0420FeA2F42a1eFc53422f205599', // 3 + '0x478be6076f31E9666123B9721D0B6631baD944AF', // 4 + '0x003DDD9eEAb62013b7332Ab4CC6B10077a8ca961', // 5 + '0xd00d6A31485C93c597D1d8231eeeE0ed17B9844B', // 6 + '0x483fd7284A696343FEc0819DDF2cf7E06E8A06E5', // 7 + '0x5b73A98165778BCCE72979B4EE3faCdb31728b8E', // 8 + '0x5dd9a0814022A61777938263308EBB336174f13D', // 9 +]; + +export const awThreshold = 4; diff --git a/typescript/infra/config/environments/mainnet3/governance/signers/irregular.ts b/typescript/infra/config/environments/mainnet3/governance/signers/irregular.ts new file mode 100644 index 00000000000..a8bfd5d5053 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/signers/irregular.ts @@ -0,0 +1,13 @@ +import { Address } from '@hyperlane-xyz/utils'; + +export const irregularSigners: Address[] = [ + '0x9f500df92175b2ac36f8d443382b219d211d354a', + '0x82950a6356316272dF1928C72F5F0A44D9673c88', + '0x861FC61a961F8AFDf115B8DE274101B9ECea2F26', + '0x3b548E88BA3259A6f45DEeA91449cdda5cF164b3', + '0xD5c0D17cCb9071D27a4F7eD8255F59989b9aee0d', + '0x3965AC3D295641E452E0ea896a086A9cD7C6C5b6', + '0x87fcEcb180E0275C22CEF213FF301816bB24E74B', +]; + +export const irregularThreshold = 5; diff --git a/typescript/infra/config/environments/mainnet3/governance/signers/regular.ts b/typescript/infra/config/environments/mainnet3/governance/signers/regular.ts new file mode 100644 index 00000000000..f06ddd4f8fd --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/signers/regular.ts @@ -0,0 +1,16 @@ +import { Address } from '@hyperlane-xyz/utils'; + +export const regularSigners: Address[] = [ + '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba', // 1 + '0xc3E966E79eF1aA4751221F55fB8A36589C24C0cA', // 2 + '0x2f43Ac3cD6A22E4Ba20d3d18d116b1f9420eD84B', // 3 + '0xfae231524539698f1d136d7b21e3b4144cdbf2a3', // 4 + '0x2C073004A6e4f37377F848193d6433260Ebe9b99', // 5 + '0x9f500df92175b2ac36f8d443382b219d211d354a', // 6 + '0x82950a6356316272dF1928C72F5F0A44D9673c88', // 7 + '0x861FC61a961F8AFDf115B8DE274101B9ECea2F26', // 8 + '0x3b548E88BA3259A6f45DEeA91449cdda5cF164b3', // 9 + '0xD5c0D17cCb9071D27a4F7eD8255F59989b9aee0d', // 10 +]; + +export const regularThreshold = 6; diff --git a/typescript/infra/config/environments/mainnet3/governance/utils.ts b/typescript/infra/config/environments/mainnet3/governance/utils.ts new file mode 100644 index 00000000000..e62602711a4 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/governance/utils.ts @@ -0,0 +1,68 @@ +import { ChainName } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +import { GovernanceType } from '../../../../src/governance.js'; + +import { awIcas } from './ica/aw.js'; +import { regularIcas } from './ica/regular.js'; +import { awSafes } from './safe/aw.js'; +import { irregularSafes } from './safe/irregular.js'; +import { regularSafes } from './safe/regular.js'; +import { awSigners, awThreshold } from './signers/aw.js'; +import { irregularSigners, irregularThreshold } from './signers/irregular.js'; +import { regularSigners, regularThreshold } from './signers/regular.js'; + +export function getGovernanceSafes(governanceType: GovernanceType) { + switch (governanceType) { + case GovernanceType.Regular: + return regularSafes; + case GovernanceType.AbacusWorks: + return awSafes; + case GovernanceType.Irregular: + return irregularSafes; + default: + throw new Error(`Unknown governance type: ${governanceType}`); + } +} + +export function getGovernanceIcas(governanceType: GovernanceType) { + switch (governanceType) { + case GovernanceType.Regular: + return regularIcas; + case GovernanceType.AbacusWorks: + return awIcas; + case GovernanceType.Irregular: + return {}; + default: + throw new Error(`Unknown governance type: ${governanceType}`); + } +} + +export function getGovernanceSigners(governanceType: GovernanceType): { + signers: Address[]; + threshold: number; +} { + switch (governanceType) { + case GovernanceType.Regular: + return { + signers: regularSigners, + threshold: regularThreshold, + }; + case GovernanceType.AbacusWorks: + return { + signers: awSigners, + threshold: awThreshold, + }; + case GovernanceType.Irregular: + return { + signers: irregularSigners, + threshold: irregularThreshold, + }; + } +} +export function getSafeChains(): Set { + return new Set( + ...Object.keys(getGovernanceSafes(GovernanceType.AbacusWorks)), + ...Object.keys(getGovernanceSafes(GovernanceType.Regular)), + ); +} diff --git a/typescript/infra/config/environments/mainnet3/igp.ts b/typescript/infra/config/environments/mainnet3/igp.ts index 7099d6aa712..23ef3b6ba83 100644 --- a/typescript/infra/config/environments/mainnet3/igp.ts +++ b/typescript/infra/config/environments/mainnet3/igp.ts @@ -39,6 +39,12 @@ export function getOverheadWithOverrides(local: ChainName, remote: ChainName) { // estimates. We double the overhead to help account for this. if (getChain(remote).technicalStack === ChainTechnicalStack.ZkSync) { overhead *= 2; + + // Zero Network gas usage has changed recently and now requires + // another 3x multiplier on top of the ZKSync overhead. + if (remote === 'zeronetwork') { + overhead *= 3; + } } return overhead; } diff --git a/typescript/infra/config/environments/mainnet3/infrastructure.ts b/typescript/infra/config/environments/mainnet3/infrastructure.ts index 36c15751c76..2d1e81f48ed 100644 --- a/typescript/infra/config/environments/mainnet3/infrastructure.ts +++ b/typescript/infra/config/environments/mainnet3/infrastructure.ts @@ -41,6 +41,10 @@ export const infrastructure: InfrastructureConfig = { 'hyperlane-mainnet3-', 'rc-mainnet3-', 'neutron-mainnet3-', + // All vanguard context secrets. There's a cap on the number of + // prefixes you can specify in a single IAM policy, so for convenience + // we just use a single prefix for all vanguard contexts. + 'vanguard', 'mainnet3-', ], }, diff --git a/typescript/infra/config/environments/mainnet3/owners.ts b/typescript/infra/config/environments/mainnet3/owners.ts index ea3ee54e975..b10d98a4c55 100644 --- a/typescript/infra/config/environments/mainnet3/owners.ts +++ b/typescript/infra/config/environments/mainnet3/owners.ts @@ -4,7 +4,10 @@ import { Address, objFilter, objMap } from '@hyperlane-xyz/utils'; import { getMainnetAddresses } from '../../registry.js'; import { ethereumChainNames } from './chains.js'; -import { supportedChainNames } from './supportedChainNames.js'; +import { awIcas } from './governance/ica/aw.js'; +import { regularIcas } from './governance/ica/regular.js'; +import { awSafes } from './governance/safe/aw.js'; +import { regularSafes } from './governance/safe/regular.js'; export const upgradeTimelocks: ChainMap
= { arbitrum: '0xAC98b0cD1B64EA4fe133C6D2EDaf842cE5cF4b01', @@ -28,224 +31,17 @@ export function localAccountRouters(): ChainMap
{ ); } -export const safes: ChainMap
= { - mantapacific: '0x03ed2D65f2742193CeD99D48EbF1F1D6F12345B6', // does not have a UI - celo: '0x879038d6Fc9F6D5e2BA73188bd078486d77e1156', - ethereum: '0x3965AC3D295641E452E0ea896a086A9cD7C6C5b6', - avalanche: '0x5bE94B17112B8F18eA9Ac8e559377B467556a3c3', - polygon: '0xf9cFD440CfBCfAB8473cc156485B7eE753b2913E', - bsc: '0x7bB2ADeDdC342ffb611dDC073095cc4B8C547170', - arbitrum: '0x03fD5BE9DF85F0017dC7F4DC3068dDF64fffF25e', - optimism: '0xbd7db3821806bc72D223F0AE521Bf82FcBd6Ef4d', - moonbeam: '0x594203849E52BF6ee0E511cD807Ca2D658893e37', - gnosis: '0x0Ac72fBc82c9c39F81242229631dfC38aA13031B', - inevm: '0x77F3863ea99F2360D84d4BA1A2E441857D0357fa', // caldera + injective - base: '0x3949eD0CD036D9FF662d97BD7aC1686051c4aeBF', - scroll: '0x6EeEbB9F7FB18DD5E59F82658c59B846715eD4F7', - polygonzkevm: '0x1610f578D4d77Fc4ae7ce2DD9AA0b98A5Cd0a9b2', - // injective: 'inj1632x8j35kenryam3mkrsez064sqg2y2fr0frzt', - // solana: 'EzppBFV2taxWw8kEjxNYvby6q7W1biJEqwP3iC7YgRe3', - blast: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', - linea: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', - mode: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', - ancient8: '0xD2BFA0F0654E3f2139b8cDC56c32eeC54D32b133', - taiko: '0xa4864301d3fa2a3e68256309F9F0F570270a1BD0', - fraxtal: '0x66e9f52800E9F89F0569fddc594Acd5EE609f762', - sei: '0xCed197FBc360C26C19889745Cf73511b71D03d5D', - redstone: '0xa1a50ff5FD859558E1899fEC5C3064483177FA23', - mantle: '0x8aFE6EECc6CcB02aA20DA8Fff7d29aadEBbc2DCd', - bob: '0x9e2fe7723b018d02cDE4f5cC1A9bC9C65b922Fc8', - zetachain: '0x9d399876522Fc5C044D048594de399A2349d6026', - zoramainnet: '0xF87018025575552889062De4b05bBC3DAe35Cd96', - fusemainnet: '0x29a526227CB864C90Cf078d03872da913B473139', - endurance: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', - zircuit: '0x9e2fe7723b018d02cDE4f5cC1A9bC9C65b922Fc8', - swell: '0x5F7771EA40546e2932754C263455Cb0023a55ca7', - - // Q5, 2024 batch - berachain: '0x5F7771EA40546e2932754C263455Cb0023a55ca7', - - // HyperEVM - hyperevm: '0x5F7771EA40546e2932754C263455Cb0023a55ca7', - - // zksync chains - zeronetwork: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', - abstract: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', - zksync: '0x9C81aA0cC233e9BddeA426F5d395Ab5B65135450', - zklink: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', - treasure: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', - sophon: '0x3D1baf8cA4935f416671640B1Aa9E17E005986eE', -}; - export const icaOwnerChain = 'ethereum'; - -// Found by running: -// yarn tsx ./scripts/keys/get-owner-ica.ts -e mainnet3 --ownerChain ethereum --destinationChains ... -export const icas: Partial< - Record<(typeof supportedChainNames)[number], Address> -> = { - viction: '0x23ed65DE22ac29Ec1C16E75EddB0cE3A187357b4', - inevm: '0xFDF9EDcb2243D51f5f317b9CEcA8edD2bEEE036e', - - // Jul 26, 2024 batch - // ---------------------------------------------------------- - xlayer: '0x1571c482fe9E76bbf50829912b1c746792966369', - cheesechain: '0xEe2C5320BE9bC7A1492187cfb289953b53E3ff1b', - worldchain: '0x1996DbFcFB433737fE404F58D2c32A7f5f334210', - // zircuit: '0x0d67c56E818a02ABa58cd2394b95EF26db999aA3', // already has a safe - - // Aug 5, 2024 batch - // ---------------------------------------------------------- - cyber: '0x984Fe5a45Ac4aaeC4E4655b50f776aB79c9Be19F', - degenchain: '0x22d952d3b9F493442731a3c7660aCaD98e55C00A', - kroma: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', - lisk: '0x22d952d3b9F493442731a3c7660aCaD98e55C00A', - lukso: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', - merlin: '0xCf867cEaeeE8CBe65C680c734D29d26440931D5b', - metis: '0xb51e63CD0842D670a13c88B159fCFc268DA652A3', - mint: '0xb51e63CD0842D670a13c88B159fCFc268DA652A3', - proofofplay: '0xb51e63CD0842D670a13c88B159fCFc268DA652A3', - real: '0xc761e68BF3A94326FD0D305e3ccb4cdaab2edA19', - sanko: '0x5DAcd2f1AafC749F2935A160865Ab1568eC23752', - tangle: '0xCC2aeb692197C7894E561d31ADFE8F79746f7d9F', - xai: '0x22d952d3b9F493442731a3c7660aCaD98e55C00A', - // taiko: '0x483D218D2FEe7FC7204ba15F00C7901acbF9697D', // renzo chain - - // Aug 26, 2024 batch - // ---------------------------------------------------------- - astar: '0x6b241544eBa7d89B51b72DF85a0342dAa37371Ca', - astarzkevm: '0x526c6DAee1175A1A2337E703B63593acb327Dde4', - bitlayer: '0xe6239316cA60814229E801fF0B9DD71C9CA29008', - coredao: '0x84802CdF47565C95d8ffd59E7c4B1cf027F5452F', - dogechain: '0x84802CdF47565C95d8ffd59E7c4B1cf027F5452F', - flare: '0x689b8DaBBF2f9Fd83D37427A062B30edF463e20b', - molten: '0x84802CdF47565C95d8ffd59E7c4B1cf027F5452F', - shibarium: '0x6348FAe3a8374dbAAaE84EEe5458AE4063Fe2be7', - - // Sep 9, 2024 batch - // ---------------------------------------------------------- - everclear: '0x63B2075372D6d0565F51210D0A296D3c8a773aB6', - oortmainnet: '0x7021D11F9fAe455AB2f45D72dbc2C64d116Cb657', - - // Sep 19, 2024 SAFE --> ICA v1 Migration - // ---------------------------------------------------------- - celo: '0x3fA264c58E1365f1d5963B831b864EcdD2ddD19b', - avalanche: '0x8c8695cD9905e22d84E466804ABE55408A87e595', - polygon: '0xBDD25dd5203fedE33FD631e30fEF9b9eF2598ECE', - moonbeam: '0x480e5b5De6a29F07fe8295C60A1845d36b7BfdE6', - gnosis: '0xD42125a4889A7A36F32d7D12bFa0ae52B0AD106b', - scroll: '0x2a3fe2513F4A7813683d480724AB0a3683EfF8AC', - polygonzkevm: '0x66037De438a59C966214B78c1d377c4e93a5C7D1', - ancient8: '0xA9FD5BeB556AB1859D7625B381110a257f56F98C', - redstone: '0x5DAcd2f1AafC749F2935A160865Ab1568eC23752', - mantle: '0x08C880b88335CA3e85Ebb4E461245a7e899863c9', - bob: '0xc99e58b9A4E330e2E4d09e2c94CD3c553904F588', - zetachain: '0xc876B8e63c3ff5b636d9492715BE375644CaD345', - zoramainnet: '0x84977Eb15E0ff5824a6129c789F70e88352C230b', - fusemainnet: '0xbBdb1682B2922C282b56DD716C29db5EFbdb5632', - endurance: '0x470E04D8a3b7938b385093B93CeBd8Db7A1E557C', - // sei: '0xabad187003EdeDd6C720Fc633f929EA632996567', // renzo chain - - // Oct 16, 2024 batch - // ---------------------------------------------------------- - // lumia: '0x418E10Ac9e0b84022d0636228d05bc74172e0e41', - - // Oct 30, 2024 batch - // ---------------------------------------------------------- - apechain: '0xe68b0aB6BB8c11D855556A5d3539524f6DB3bdc6', - arbitrumnova: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', - b3: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', - fantom: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', - gravity: '0x3104ADE26e21AEbdB325321433541DfE8B5dCF23', - harmony: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', - kaia: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', - morph: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', - orderly: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', - snaxchain: '0x8965d9f19336EB4e910d5f1B9070205FdBee6837', - - // Nov 8, 2024 batch - // ---------------------------------------------------------- - alephzeroevmmainnet: '0xDE91AC081E12107a033728A287b06B1Fc640A637', - chilizmainnet: '0x54AF0FCDCD58428f8dF3f825267DfB58f2C710eb', - flowmainnet: '0x65528D447C93CC1A1A7186CB4449d9fE0d5C1928', - immutablezkevmmainnet: '0x54AF0FCDCD58428f8dF3f825267DfB58f2C710eb', - metal: '0xf1d25462e1f82BbF25b3ef7A4C94F738a30a968B', - polynomialfi: '0x6ACa36E710dC0C80400090EA0bC55dA913a3D20D', - rarichain: '0xD0A4Ad2Ca0251BBc6541f8c2a594F1A82b67F114', - rootstockmainnet: '0x0C15f7479E0B46868693568a3f1C747Fdec9f17d', - superpositionmainnet: '0x5F17Dc2e1fd1371dc6e694c51f22aBAF8E27667B', - flame: '0x4F3d85360840497Cd1bc34Ca55f27629eee2AA2e', - prom: '0x1cDd3C143387cD1FaE23e2B66bc3F409D073aC3D', - - // Nov 21, 2024 batch - // ---------------------------------------------------------- - boba: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', - duckchain: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', - unichain: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', - vana: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', - bsquared: '0xd9564EaaA68A327933f758A54450D3A0531E60BB', - superseed: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', - - // Dec 4, 2024 batch - // ---------------------------------------------------------- - // swell: '0xff8326468e7AaB51c53D3569cf7C45Dd54c11687', // already has a safe - lumiaprism: '0xAFfA863646D1bC74ecEC0dB1070f069Af065EBf5', - appchain: '0x4F25DFFd10A6D61C365E1a605d07B2ab0E82A7E6', - - // Dec 13, 2024 batch - // ---------------------------------------------------------- - arthera: '0x962e4E5F5e47e1Ab5361eE0B5108Ebeb9Fa5c99B', - aurora: '0x853f40c807cbb08EDd19B326b9b6A669bf3c274c', - conflux: '0xac8f0e306A126312C273080d149ca01d461603FE', - conwai: '0x5926599B8Aff45f1708b804B30213babdAD78C83', - corn: '0x5926599B8Aff45f1708b804B30213babdAD78C83', - evmos: '0x5926599B8Aff45f1708b804B30213babdAD78C83', - form: '0x5926599B8Aff45f1708b804B30213babdAD78C83', - ink: '0xDde4Ce691d1c0579d48BCdd3491aA71472b6cC38', - rivalz: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', - soneium: '0x5926599B8Aff45f1708b804B30213babdAD78C83', - sonic: '0x5926599B8Aff45f1708b804B30213babdAD78C83', - telos: '0xDde4Ce691d1c0579d48BCdd3491aA71472b6cC38', - - // Jan 13, 2025 batch - // ---------------------------------------------------------- - artela: '0x745CEA119757ea3e27093da590bC91f408bD4448', - guru: '0x825cF3d703F384E4aA846BA72eCf70f1985C91b6', - hemi: '0x8D18CBB212920e5ef070b23b813d82F8981cC276', - nero: '0xbBdb1682B2922C282b56DD716C29db5EFbdb5632', - torus: '0xc1e20A0D78E79B94D71d4bDBC8FD0Af7c856Dd7A', - xpla: '0x24832680dF0468967F413be1C83acfE24154F88D', - - // Feb 3, 2025 batch - // ---------------------------------------------------------- - glue: '0x24832680dF0468967F413be1C83acfE24154F88D', - matchain: '0x66af72e46b3e8DFc19992A2A88C05d9EEFE01ffB', - unitzero: '0x66af72e46b3e8DFc19992A2A88C05d9EEFE01ffB', - trumpchain: '0x56895bFa7f7dFA5743b2A0994B5B0f88b88350F9', - - // Q5, 2024 batch - // ---------------------------------------------------------- - // berachain: '0x56895bFa7f7dFA5743b2A0994B5B0f88b88350F9', - - // Feb 17, 2025 batch - // ---------------------------------------------------------- - bouncebit: '0x8768A14AA6eD2A62C77155501E742376cbE97981', - arcadia: '0xD2344a364b6Dc6B2Fe0f7D836fa344d83056cbaD', - ronin: '0x8768A14AA6eD2A62C77155501E742376cbE97981', - story: '0x8768A14AA6eD2A62C77155501E742376cbE97981', - subtensor: '0x61BFbb5FEC57f5470388A80946F0415138630b9c', - - // Mar 14, 2025 batch - // ---------------------------------------------------------- - infinityvm: '0x35460c519b7C71d49C64F060dF89AbAE463F3b9a', - plume: '0x61BFbb5FEC57f5470388A80946F0415138630b9c', -} as const; - export const DEPLOYER = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; export const ethereumChainOwners: ChainMap = Object.fromEntries( ethereumChainNames.map((local) => { - const owner = icas[local] ?? safes[local] ?? DEPLOYER; + const owner = + regularIcas[local] ?? + regularSafes[local] ?? + awIcas[local] ?? + awSafes[local] ?? + DEPLOYER; return [ local, @@ -256,12 +52,6 @@ export const ethereumChainOwners: ChainMap = Object.fromEntries( validatorAnnounce: DEPLOYER, // unused testRecipient: DEPLOYER, fallbackRoutingHook: DEPLOYER, - // Because of the logic above of setting the owner to the Safe or ICA address, - // the checker/governor tooling does not know what type of owner it is. - // So we need to keep the Safe and ICA addresses somewhere in the config - // to be able to track down which addresses are SAFEs, ICAs, or standard SIGNERS. - ...(safes[local] && { _safeAddress: safes[local] }), - ...(icas[local] && { _icaAddress: icas[local] }), }, }, ]; diff --git a/typescript/infra/config/environments/mainnet3/supportedChainNames.ts b/typescript/infra/config/environments/mainnet3/supportedChainNames.ts index 48775ef6c95..30c8c5bf692 100644 --- a/typescript/infra/config/environments/mainnet3/supportedChainNames.ts +++ b/typescript/infra/config/environments/mainnet3/supportedChainNames.ts @@ -13,7 +13,6 @@ export const mainnet3SupportedChainNames = [ 'artela', 'arthera', 'astar', - 'astarzkevm', 'aurora', 'bouncebit', 'flame', diff --git a/typescript/infra/config/environments/mainnet3/tokenPrices.json b/typescript/infra/config/environments/mainnet3/tokenPrices.json index ac4857aa513..b5a3096bdd9 100644 --- a/typescript/infra/config/environments/mainnet3/tokenPrices.json +++ b/typescript/infra/config/environments/mainnet3/tokenPrices.json @@ -1,139 +1,138 @@ { - "abstract": "2054.11", - "ancient8": "2054.11", - "alephzeroevmmainnet": "0.106945", - "apechain": "0.57248", - "appchain": "2054.11", - "arbitrum": "2054.11", - "arbitrumnova": "2054.11", - "arcadia": "2054.11", - "artela": "0.00465492", - "arthera": "0.00845931", - "astar": "0.03447512", - "astarzkevm": "2054.11", - "aurora": "2054.11", - "bouncebit": "0.156235", - "flame": "3.65", - "avalanche": "22.58", - "b3": "2054.11", - "base": "2054.11", - "berachain": "7.87", - "bitlayer": "87747", - "blast": "2054.11", - "bob": "2054.11", - "boba": "2054.11", - "bsc": "629.83", - "bsquared": "87747", - "celo": "0.397064", - "cheesechain": "0.00051627", - "chilizmainnet": "0.04934281", - "conflux": "0.095348", - "conwai": "0.00239384", - "coredao": "0.458289", - "corn": "87747", - "coti": "0.089803", - "cyber": "2054.11", - "deepbrainchain": "0.00168509", - "degenchain": "0.00357109", - "dogechain": "0.201434", - "duckchain": "3.59", - "eclipsemainnet": "2054.11", - "endurance": "0.753059", - "ethereum": "2054.11", - "everclear": "2054.11", - "evmos": "0.00501066", - "fantom": "0.634786", - "flare": "0.01502485", - "flowmainnet": "0.434717", - "form": "2054.11", - "fraxtal": "2049.92", - "fusemainnet": "0.01229848", - "glue": "0.108488", - "gnosis": "1.003", - "gravity": "0.01714157", - "guru": "0.00412234", - "harmony": "0.01288063", - "hemi": "2054.11", - "hyperevm": "15.06", - "immutablezkevmmainnet": "0.665922", - "inevm": "10.84", + "abstract": "1599.53", + "ancient8": "1599.53", + "alephzeroevmmainnet": "0.092064", + "apechain": "0.443681", + "appchain": "1599.53", + "arbitrum": "1599.53", + "arbitrumnova": "1599.53", + "arcadia": "1599.53", + "artela": "0.00093234", + "arthera": "0.00611031", + "astar": "0.0268412", + "aurora": "1599.53", + "bouncebit": "0.102952", + "flame": "2.41", + "avalanche": "19.48", + "b3": "1599.53", + "base": "1599.53", + "berachain": "3.41", + "bitlayer": "85131", + "blast": "1599.53", + "bob": "1599.53", + "boba": "1599.53", + "bsc": "592.22", + "bsquared": "85131", + "celo": "0.308083", + "cheesechain": "0.00029572", + "chilizmainnet": "0.03718311", + "conflux": "0.070167", + "conwai": "0.00177459", + "coredao": "0.68447", + "corn": "85131", + "coti": "0.066086", + "cyber": "1599.53", + "deepbrainchain": "0.00085848", + "degenchain": "0.00254968", + "dogechain": "0.15876", + "duckchain": "2.99", + "eclipsemainnet": "1599.53", + "endurance": "0.496753", + "ethereum": "1599.53", + "everclear": "1599.53", + "evmos": "0.00314416", + "fantom": "0.464854", + "flare": "0.01602446", + "flowmainnet": "0.370122", + "form": "1599.53", + "fraxtal": "1595.81", + "fusemainnet": "0.01187583", + "glue": "0.123497", + "gnosis": "1", + "gravity": "0.0142485", + "guru": "0.00224974", + "harmony": "0.01097306", + "hemi": "1599.53", + "hyperevm": "18.16", + "immutablezkevmmainnet": "0.463869", + "inevm": "8.25", "infinityvm": "1", - "ink": "2054.11", - "injective": "10.84", - "kaia": "0.112228", - "kroma": "2054.11", - "linea": "2054.11", - "lisk": "2054.11", - "lukso": "0.8531", - "lumia": "0.52645", - "lumiaprism": "0.499733", - "mantapacific": "2054.11", - "mantle": "0.844786", - "matchain": "629.83", - "merlin": "87769", - "metal": "2054.11", - "metis": "19.03", - "mint": "2054.11", - "mode": "2054.11", - "molten": "0.2257", - "moonbeam": "0.091772", - "morph": "2054.11", + "ink": "1599.53", + "injective": "8.25", + "kaia": "0.100334", + "kroma": "1599.53", + "linea": "1599.53", + "lisk": "1599.53", + "lukso": "1.015", + "lumia": "0.335583", + "lumiaprism": "0.285301", + "mantapacific": "1599.53", + "mantle": "0.655372", + "matchain": "592.22", + "merlin": "85091", + "metal": "1599.53", + "metis": "13.62", + "mint": "1599.53", + "mode": "1599.53", + "molten": "0.127992", + "moonbeam": "0.067257", + "morph": "1599.53", "nero": "1", - "neutron": "0.154792", - "nibiru": "0.02117295", - "oortmainnet": "0.05927", - "opbnb": "629.83", - "optimism": "2054.11", - "orderly": "2054.11", - "osmosis": "0.3037", - "plume": "0.188632", - "polygon": "0.237997", - "polygonzkevm": "2054.11", - "polynomialfi": "2054.11", - "prom": "6.2", - "proofofplay": "2054.11", - "rarichain": "2054.11", - "reactive": "0.087976", - "real": "2054.11", - "redstone": "2054.11", - "rivalz": "2054.11", - "ronin": "0.802351", - "rootstockmainnet": "87076", - "sanko": "10.1", - "scroll": "2054.11", - "sei": "0.215432", - "shibarium": "0.399826", - "snaxchain": "2054.11", - "solanamainnet": "143.92", - "soneium": "2054.11", - "sonic": "0.634786", - "sonicsvm": "143.92", - "soon": "2054.11", + "neutron": "0.122576", + "nibiru": "0.01709542", + "oortmainnet": "0.0424777", + "opbnb": "592.22", + "optimism": "1599.53", + "orderly": "1599.53", + "osmosis": "0.216516", + "plume": "0.163739", + "polygon": "0.190008", + "polygonzkevm": "1599.53", + "polynomialfi": "1599.53", + "prom": "5.71", + "proofofplay": "1599.53", + "rarichain": "1599.53", + "reactive": "0.080498", + "real": "1599.53", + "redstone": "1599.53", + "rivalz": "1599.53", + "ronin": "0.501632", + "rootstockmainnet": "84707", + "sanko": "6.47", + "scroll": "1599.53", + "sei": "0.170216", + "shibarium": "0.27927", + "snaxchain": "1599.53", + "solanamainnet": "138.62", + "soneium": "1599.53", + "sonic": "0.464854", + "sonicsvm": "138.62", + "soon": "1599.53", "sophon": "1", - "story": "6.22", - "stride": "0.336439", - "subtensor": "273", - "superseed": "2054.11", - "superpositionmainnet": "2054.11", - "swell": "2054.11", - "taiko": "2054.11", + "story": "3.87", + "stride": "0.193807", + "subtensor": "280.37", + "superseed": "1599.53", + "superpositionmainnet": "1599.53", + "swell": "1599.53", + "taiko": "1599.53", "tangle": "1", - "telos": "0.105677", - "torus": "0.137549", - "treasure": "0.145629", - "trumpchain": "11.83", - "unichain": "2054.11", - "unitzero": "0.428111", - "vana": "7.18", - "viction": "0.248455", - "worldchain": "2054.11", - "xai": "0.07055", - "xlayer": "50.37", - "xpla": "0.03934581", - "zeronetwork": "2054.11", - "zetachain": "0.328577", - "zircuit": "2054.11", - "zklink": "2054.11", - "zksync": "2054.11", - "zoramainnet": "2054.11" + "telos": "0.086451", + "torus": "0.140374", + "treasure": "0.079584", + "trumpchain": "8.31", + "unichain": "1599.53", + "unitzero": "0.235709", + "vana": "5.14", + "viction": "0.200071", + "worldchain": "1599.53", + "xai": "0.04386052", + "xlayer": "50.69", + "xpla": "0.02738309", + "zeronetwork": "1599.53", + "zetachain": "0.234442", + "zircuit": "1599.53", + "zklink": "1599.53", + "zksync": "1599.53", + "zoramainnet": "1599.53" } diff --git a/typescript/infra/config/environments/mainnet3/validators.ts b/typescript/infra/config/environments/mainnet3/validators.ts index 80c5cb80f0c..8da0262591c 100644 --- a/typescript/infra/config/environments/mainnet3/validators.ts +++ b/typescript/infra/config/environments/mainnet3/validators.ts @@ -790,18 +790,6 @@ export const validatorChainConfig = ( 'astar', ), }, - astarzkevm: { - interval: 5, - reorgPeriod: getReorgPeriod('astarzkevm'), - validators: validatorsConfig( - { - [Contexts.Hyperlane]: ['0x89ecdd6caf138934bf3a2fb7b323984d72fd66de'], - [Contexts.ReleaseCandidate]: [''], - [Contexts.Neutron]: [], - }, - 'astarzkevm', - ), - }, bitlayer: { interval: 5, reorgPeriod: getReorgPeriod('bitlayer'), diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIAWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIAWarpConfig.ts new file mode 100644 index 00000000000..c180bc9f166 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIAWarpConfig.ts @@ -0,0 +1,126 @@ +import { + ChainMap, + ChainSubmissionStrategy, + HypTokenRouterConfig, + TokenType, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; + +import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; + +const safeOwners: Record = { + arbitrum: '0xc8A9Dea7359Bd6FDCAD3B8EDE108416C25cF4CE9', + avalanche: '0x6d5Cd9e6EB9a2E74bF9857c53aA44F659f0Cc332', + base: '0xcEC53d6fF9B4C7b8E77f0C0D3f8828Bb872f2377', + bsc: '0xa86C4AF592ddAa676f53De278dE9cfCD52Ae6B39', + ethereum: '0xa86C4AF592ddAa676f53De278dE9cfCD52Ae6B39', + lumiaprism: '0xa86C4AF592ddAa676f53De278dE9cfCD52Ae6B39', + optimism: '0x914931eBb5638108651455F50C1F784d3E5fd3EC', + polygon: '0x7a412dD3812369226cd42023FC9301A66788122e', +}; + +export const getArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIAWarpConfig = + async ( + routerConfig: ChainMap, + ): Promise> => { + const arbitrum: HypTokenRouterConfig = { + ...routerConfig.arbitrum, + owner: safeOwners.arbitrum, + type: TokenType.synthetic, + symbol: 'LUMIA', + }; + + const avalanche: HypTokenRouterConfig = { + ...routerConfig.avalanche, + owner: safeOwners.avalanche, + type: TokenType.synthetic, + symbol: 'LUMIA', + }; + + const base: HypTokenRouterConfig = { + ...routerConfig.base, + owner: safeOwners.base, + type: TokenType.synthetic, + symbol: 'LUMIA', + }; + + const bsc: HypTokenRouterConfig = { + ...routerConfig.bsc, + owner: safeOwners.bsc, + type: TokenType.synthetic, + proxyAdmin: { + address: '0x54bB5b12c67095eB3dabCD11FA74AAcfE46E7767', + owner: '0x8bBA07Ddc72455b55530C17e6f6223EF6E156863', + }, + }; + + const ethereum: HypTokenRouterConfig = { + ...routerConfig.ethereum, + owner: safeOwners.ethereum, + type: TokenType.collateral, + token: '0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7', + proxyAdmin: { + address: '0xdaB976ae358D4D2d25050b5A25f71520983C46c9', + owner: '0x8bBA07Ddc72455b55530C17e6f6223EF6E156863', + }, + }; + + const lumiaprism: HypTokenRouterConfig = { + ...routerConfig.lumiaprism, + owner: safeOwners.lumiaprism, + type: TokenType.native, + proxyAdmin: { + address: '0xBC53dACd8c0ac0d2bAC461479EAaf5519eCC8853', + owner: '0x8bBA07Ddc72455b55530C17e6f6223EF6E156863', + }, + }; + + const optimism: HypTokenRouterConfig = { + ...routerConfig.optimism, + owner: safeOwners.optimism, + type: TokenType.synthetic, + symbol: 'LUMIA', + }; + + const polygon: HypTokenRouterConfig = { + ...routerConfig.polygon, + owner: safeOwners.polygon, + type: TokenType.synthetic, + symbol: 'LUMIA', + }; + + return { + arbitrum, + avalanche, + base, + bsc, + ethereum, + lumiaprism, + optimism, + polygon, + }; + }; + +// Create a GnosisSafeBuilder Strategy for each safe address +export function getLUMIAGnosisSafeBuilderStrategyConfigGenerator( + lumiaSafes: Record, +) { + return (): ChainSubmissionStrategy => { + return Object.fromEntries( + Object.entries(lumiaSafes).map(([chain, safeAddress]) => [ + chain, + { + submitter: { + type: TxSubmitterType.GNOSIS_TX_BUILDER, + version: '1.0', + chain, + safeAddress, + }, + }, + ]), + ); + }; +} + +export const getLUMIAGnosisSafeBuilderStrategyConfig = + getLUMIAGnosisSafeBuilderStrategyConfigGenerator(safeOwners); diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumLumiaprismOptimismPolygonETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumLumiaprismOptimismPolygonETHWarpConfig.ts new file mode 100644 index 00000000000..11b60dc7cea --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumLumiaprismOptimismPolygonETHWarpConfig.ts @@ -0,0 +1,74 @@ +import { ethers } from 'ethers'; + +import { ChainMap, HypTokenRouterConfig, TokenType } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +import { + RouterConfigWithoutOwner, + tokens, +} from '../../../../../src/config/warp.js'; +import { getGnosisSafeBuilderStrategyConfigGenerator } from '../../../utils.js'; + +const safeOwners: ChainMap
= { + arbitrum: '0xc8A9Dea7359Bd6FDCAD3B8EDE108416C25cF4CE9', + ethereum: '0xb10B260fBf5F33CC5Ff81761e090aeCDffcb1fd5', + base: '0xC92aB408512defCf1938858E726dc5C0ada9175a', + lumiaprism: '0x1b06089dA471355F8F05C7A6d8DE1D9dAC397629', + optimism: '0x914931eBb5638108651455F50C1F784d3E5fd3EC', + polygon: '0x7a412dD3812369226cd42023FC9301A66788122e', +}; + +export const getArbitrumBaseEthereumLumiaprismOptimismPolygonETHWarpConfig = + async ( + routerConfig: ChainMap, + ): Promise> => { + const arbitrum: HypTokenRouterConfig = { + ...routerConfig.arbitrum, + owner: safeOwners.arbitrum, + type: TokenType.native, + }; + + const base: HypTokenRouterConfig = { + ...routerConfig.base, + owner: safeOwners.base, + type: TokenType.native, + }; + + const ethereum: HypTokenRouterConfig = { + ...routerConfig.ethereum, + owner: safeOwners.ethereum, + type: TokenType.native, + }; + + const lumiaprism: HypTokenRouterConfig = { + ...routerConfig.lumiaprism, + owner: safeOwners.lumiaprism, + type: TokenType.synthetic, + symbol: 'WETH', + }; + + const optimism: HypTokenRouterConfig = { + ...routerConfig.optimism, + owner: safeOwners.optimism, + type: TokenType.native, + }; + + const polygon: HypTokenRouterConfig = { + ...routerConfig.polygon, + owner: safeOwners.polygon, + token: tokens.polygon.WETH, + type: TokenType.collateral, + }; + + return { + arbitrum, + base, + ethereum, + lumiaprism, + optimism, + polygon, + }; + }; + +export const getArbitrumBaseEthereumLumiaprismOptimismPolygonETHGnosisSafeBuilderStrategyConfig = + getGnosisSafeBuilderStrategyConfigGenerator(safeOwners); diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDCWarpConfig.ts index 9dae492537a..49fc6289494 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDCWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDCWarpConfig.ts @@ -11,6 +11,7 @@ import { RouterConfigWithoutOwner, tokens, } from '../../../../../src/config/warp.js'; +import { timelocks } from '../../owners.js'; export const getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC = async ( routerConfig: ChainMap, @@ -20,10 +21,10 @@ export const getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC = async ( const arbitrum: HypTokenRouterConfig = { ...routerConfig.arbitrum, - ...abacusWorksEnvOwnerConfig.arbitrum, + owner: abacusWorksEnvOwnerConfig.arbitrum.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.arbitrum, address: '0x02317D525FA7ceb5ea388244b4618f0c8Ac1CeC2', + owner: timelocks.arbitrum, }, type: TokenType.collateral, token: tokens.arbitrum.USDC, @@ -32,9 +33,9 @@ export const getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC = async ( const base: HypTokenRouterConfig = { ...routerConfig.base, - ...abacusWorksEnvOwnerConfig.base, + owner: abacusWorksEnvOwnerConfig.base.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.base, + owner: abacusWorksEnvOwnerConfig.base.owner, address: '0xB6E9331576C5aBF69376AF6989eA61b7C7ea67F1', }, type: TokenType.collateral, @@ -44,9 +45,9 @@ export const getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC = async ( const optimism: HypTokenRouterConfig = { ...routerConfig.optimism, - ...abacusWorksEnvOwnerConfig.optimism, + owner: abacusWorksEnvOwnerConfig.optimism.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.optimism, + owner: abacusWorksEnvOwnerConfig.optimism.owner, address: '0xca9e64761C97b049901dF4E7a5926464969528b1', }, type: TokenType.collateral, @@ -56,9 +57,9 @@ export const getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC = async ( const polygon: HypTokenRouterConfig = { ...routerConfig.polygon, - ...abacusWorksEnvOwnerConfig.polygon, + owner: abacusWorksEnvOwnerConfig.polygon.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.polygon, + owner: abacusWorksEnvOwnerConfig.polygon.owner, address: '0x7fd5be37d560626625f395A2e6E30eA89150cc98', }, type: TokenType.collateral, @@ -68,9 +69,9 @@ export const getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC = async ( const zeronetwork: HypTokenRouterConfig = { ...routerConfig.zeronetwork, - ...abacusWorksEnvOwnerConfig.zeronetwork, + owner: abacusWorksEnvOwnerConfig.zeronetwork.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.zeronetwork, + owner: abacusWorksEnvOwnerConfig.zeronetwork.owner, address: '0x6E906d8AeEBE9025a410887EAafc58C2561705e0', }, type: TokenType.collateral, @@ -80,9 +81,9 @@ export const getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC = async ( const ethereum: HypTokenRouterConfig = { ...routerConfig.ethereum, - ...abacusWorksEnvOwnerConfig.ethereum, + owner: abacusWorksEnvOwnerConfig.ethereum.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.ethereum, + owner: abacusWorksEnvOwnerConfig.ethereum.owner, address: '0x81063D413Ed6Eac3FCf0521eea14906fD27fEb1A', }, type: TokenType.collateral, @@ -92,7 +93,7 @@ export const getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC = async ( const lisk: HypTokenRouterConfig = { ...routerConfig.lisk, - ...abacusWorksEnvOwnerConfig.lisk, + owner: abacusWorksEnvOwnerConfig.lisk.owner, proxyAdmin: { ...abacusWorksEnvOwnerConfig.lisk, address: '0x81Db8B4Bc6F2e95781eeA2a21D0A453Ac046eFc0', diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBscEthereumMantleModePolygonScrollZeronetworkUSDTWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBscEthereumMantleModePolygonScrollZeronetworkUSDTWarpConfig.ts index 8de2710ee2a..bb25b14036b 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBscEthereumMantleModePolygonScrollZeronetworkUSDTWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getArbitrumBscEthereumMantleModePolygonScrollZeronetworkUSDTWarpConfig.ts @@ -12,6 +12,7 @@ import { RouterConfigWithoutOwner, tokens, } from '../../../../../src/config/warp.js'; +import { timelocks } from '../../owners.js'; export const getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig = async ( @@ -22,10 +23,10 @@ export const getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig const arbitrum: HypTokenRouterConfig = { ...routerConfig.arbitrum, - ...abacusWorksEnvOwnerConfig.arbitrum, + owner: abacusWorksEnvOwnerConfig.arbitrum.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.arbitrum, address: '0x6701d503369cf6aA9e5EdFfEBFA40A2ffdf3dB21', + owner: timelocks.arbitrum, }, type: TokenType.collateral, token: tokens.arbitrum.USDT, @@ -34,9 +35,9 @@ export const getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig const ethereum: HypTokenRouterConfig = { ...routerConfig.ethereum, - ...abacusWorksEnvOwnerConfig.ethereum, + owner: abacusWorksEnvOwnerConfig.ethereum.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.ethereum, + owner: abacusWorksEnvOwnerConfig.ethereum.owner, address: '0xA92D6084709469A2B2339919FfC568b7C5D7888D', }, type: TokenType.collateral, @@ -46,9 +47,9 @@ export const getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig const mantle: HypTokenRouterConfig = { ...routerConfig.mantle, - ...abacusWorksEnvOwnerConfig.mantle, + owner: abacusWorksEnvOwnerConfig.mantle.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.mantle, + owner: abacusWorksEnvOwnerConfig.mantle.owner, address: '0x633268639892C73Fa7340Ec1da4e397cf3913c8C', }, type: TokenType.collateral, @@ -58,9 +59,9 @@ export const getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig const mode: HypTokenRouterConfig = { ...routerConfig.mode, - ...abacusWorksEnvOwnerConfig.mode, + owner: abacusWorksEnvOwnerConfig.mode.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.mode, + owner: abacusWorksEnvOwnerConfig.mode.owner, address: '0x633268639892C73Fa7340Ec1da4e397cf3913c8C', }, type: TokenType.collateral, @@ -70,9 +71,9 @@ export const getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig const polygon: HypTokenRouterConfig = { ...routerConfig.polygon, - ...abacusWorksEnvOwnerConfig.polygon, + owner: abacusWorksEnvOwnerConfig.polygon.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.polygon, + owner: abacusWorksEnvOwnerConfig.polygon.owner, address: '0x5DBeAEC137d1ef9a240599656073Ae3E717fae3c', }, type: TokenType.collateral, @@ -82,9 +83,9 @@ export const getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig const scroll: HypTokenRouterConfig = { ...routerConfig.scroll, - ...abacusWorksEnvOwnerConfig.scroll, + owner: abacusWorksEnvOwnerConfig.scroll.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.scroll, + owner: abacusWorksEnvOwnerConfig.scroll.owner, address: '0x81Db8B4Bc6F2e95781eeA2a21D0A453Ac046eFc0', }, type: TokenType.collateral, @@ -94,9 +95,9 @@ export const getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig const zeronetwork: HypTokenRouterConfig = { ...routerConfig.zeronetwork, - ...abacusWorksEnvOwnerConfig.zeronetwork, + owner: abacusWorksEnvOwnerConfig.zeronetwork.owner, proxyAdmin: { - ...abacusWorksEnvOwnerConfig.zeronetwork, + owner: abacusWorksEnvOwnerConfig.zeronetwork.owner, address: '0xa3F188BDd6e3894b393e12396347545bC47E7B0e', }, type: TokenType.synthetic, diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoEZETHSTAGEWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoEZETHSTAGEWarpConfig.ts index 352212e33e0..f7998b25550 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoEZETHSTAGEWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoEZETHSTAGEWarpConfig.ts @@ -1,7 +1,8 @@ +import { getGnosisSafeBuilderStrategyConfigGenerator } from '../../../utils.js'; + import { ezEthChainsToDeploy, ezEthValidators, - getRenzoGnosisSafeBuilderStrategyConfigGenerator, getRenzoWarpConfigGenerator, renzoTokenPrices, } from './getRenzoEZETHWarpConfig.js'; @@ -58,5 +59,5 @@ export const getRenzoEZETHSTAGEWarpConfig = getRenzoWarpConfigGenerator({ tokenPrices: renzoTokenPrices, }); -export const getRenzoGnosisSafeBuilderStagingStrategyConfig = - getRenzoGnosisSafeBuilderStrategyConfigGenerator(ezEthStagingSafes); +export const getEZETHSTAGEGnosisSafeBuilderStrategyConfig = + getGnosisSafeBuilderStrategyConfigGenerator(ezEthStagingSafes); diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoEZETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoEZETHWarpConfig.ts index 2d3ab01da66..c7eac5e935b 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoEZETHWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoEZETHWarpConfig.ts @@ -17,6 +17,7 @@ import { import { Address, assert, symmetricDifference } from '@hyperlane-xyz/utils'; import { getEnvironmentConfig } from '../../../../../scripts/core-utils.js'; +import { getGnosisSafeBuilderStrategyConfigGenerator } from '../../../utils.js'; import { getRegistry as getMainnet3Registry } from '../../chains.js'; export const ezEthChainsToDeploy = [ @@ -388,7 +389,7 @@ export function getRenzoWarpConfigGenerator(params: { if (tokenPriceDiff.size > 0) { throw new Error( - `chainsToDeploy !== xERC20Diff, diff is ${Array.from( + `chainsToDeploy !== tokenPriceDiff, diff is ${Array.from( tokenPriceDiff, ).join(', ')}`, ); @@ -443,7 +444,9 @@ export function getRenzoWarpConfigGenerator(params: { ], }, hook: getRenzoHook(defaultHook, chain, safes[chain]), - proxyAdmin: existingProxyAdmins?.[chain] ?? undefined, // when 'undefined' yaml will not include the field + ...(existingProxyAdmins?.[chain] + ? { proxyAdmin: existingProxyAdmins?.[chain] } + : {}), }, ]; @@ -467,26 +470,5 @@ export const getRenzoEZETHWarpConfig = getRenzoWarpConfigGenerator({ existingProxyAdmins: existingProxyAdmins, }); -// Create a GnosisSafeBuilder Strategy for each safe address -export function getRenzoGnosisSafeBuilderStrategyConfigGenerator( - ezEthSafes: Record, -) { - return (): ChainSubmissionStrategy => { - return Object.fromEntries( - Object.entries(ezEthSafes).map(([chain, safeAddress]) => [ - chain, - { - submitter: { - type: TxSubmitterType.GNOSIS_TX_BUILDER, - version: '1.0', - chain, - safeAddress, - }, - }, - ]), - ); - }; -} - -export const getRenzoGnosisSafeBuilderStrategyConfig = - getRenzoGnosisSafeBuilderStrategyConfigGenerator(ezEthSafes); +export const getEZETHGnosisSafeBuilderStrategyConfig = + getGnosisSafeBuilderStrategyConfigGenerator(ezEthSafes); diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoPZETHSTAGEWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoPZETHSTAGEWarpConfig.ts index a59e11df1e2..99a4ddde45c 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoPZETHSTAGEWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoPZETHSTAGEWarpConfig.ts @@ -1,5 +1,7 @@ import { pick } from '@hyperlane-xyz/utils'; +import { getGnosisSafeBuilderStrategyConfigGenerator } from '../../../utils.js'; + import { ezEthStagingSafes } from './getRenzoEZETHSTAGEWarpConfig.js'; import { ezEthValidators, @@ -13,6 +15,8 @@ const pzEthStagingAddresses = { ethereum: '0xDe9e4211087A43112b0e0e9d840459Acf1d9E6C8', zircuit: '0xDe9e4211087A43112b0e0e9d840459Acf1d9E6C8', swell: '0xDe9e4211087A43112b0e0e9d840459Acf1d9E6C8', + unichain: '0xDe9e4211087A43112b0e0e9d840459Acf1d9E6C8', + berachain: '0xDe9e4211087A43112b0e0e9d840459Acf1d9E6C8', }; const pzEthStagingValidators = pick(ezEthValidators, pzEthChainsToDeploy); @@ -30,3 +34,6 @@ export const getRenzoPZETHStagingWarpConfig = getRenzoWarpConfigGenerator({ xERC20Lockbox: pzEthStagingLockbox, tokenPrices: pzEthStagingTokenPrices, }); + +export const getPZETHSTAGEGnosisSafeBuilderStrategyConfig = + getGnosisSafeBuilderStrategyConfigGenerator(pzEthStagingSafes); diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoPZETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoPZETHWarpConfig.ts index 63e5ae8f661..0ee97fc5def 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoPZETHWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getRenzoPZETHWarpConfig.ts @@ -13,9 +13,17 @@ const pzEthAddresses = { ethereum: '0x9cb41CD74D01ae4b4f640EC40f7A60cA1bCF83E7', zircuit: '0x9cb41CD74D01ae4b4f640EC40f7A60cA1bCF83E7', swell: '0x9cb41CD74D01ae4b4f640EC40f7A60cA1bCF83E7', + unichain: '0x9cb41CD74D01ae4b4f640EC40f7A60cA1bCF83E7', + berachain: '0x9cb41CD74D01ae4b4f640EC40f7A60cA1bCF83E7', }; -export const pzEthChainsToDeploy = ['ethereum', 'swell', 'zircuit']; +export const pzEthChainsToDeploy = [ + 'ethereum', + 'swell', + 'zircuit', + 'unichain', + 'berachain', +]; const pzEthValidators = pick(ezEthValidators, pzEthChainsToDeploy); const pzEthSafes = pick(ezEthSafes, pzEthChainsToDeploy); diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getSuperTokenWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getSuperTokenWarpConfig.ts index d3b32a5ad29..70d5f089ce0 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getSuperTokenWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getSuperTokenWarpConfig.ts @@ -84,6 +84,55 @@ const productionOwnerByChain: TypedSuperTokenChainMap = { worldchain: '0x998238aF5A2DDC7ae08Dbe4B60b82EF63A1538cd', }; +const productionOwnerOverridesByChain: ChainMap< + Record<'collateralToken' | 'collateralProxyAdmin', string> +> = { + celo: { + collateralToken: productionOwnerByChain.celo, + collateralProxyAdmin: productionOwnerByChain.celo, + }, + optimism: { + collateralToken: productionOwnerByChain.optimism, + collateralProxyAdmin: productionOwnerByChain.optimism, + }, + base: { + collateralToken: productionOwnerByChain.base, + collateralProxyAdmin: productionOwnerByChain.base, + }, + unichain: { + collateralToken: productionOwnerByChain.unichain, + collateralProxyAdmin: productionOwnerByChain.unichain, + }, + ink: { + collateralToken: productionOwnerByChain.ink, + collateralProxyAdmin: productionOwnerByChain.ink, + }, + soneium: { + collateralToken: productionOwnerByChain.soneium, + collateralProxyAdmin: productionOwnerByChain.soneium, + }, + mode: { + collateralToken: productionOwnerByChain.mode, + collateralProxyAdmin: productionOwnerByChain.mode, + }, + fraxtal: { + collateralToken: productionOwnerByChain.fraxtal, + collateralProxyAdmin: productionOwnerByChain.fraxtal, + }, + superseed: { + collateralToken: productionOwnerByChain.superseed, + collateralProxyAdmin: productionOwnerByChain.superseed, + }, + lisk: { + collateralToken: productionOwnerByChain.lisk, + collateralProxyAdmin: productionOwnerByChain.lisk, + }, + worldchain: { + collateralToken: productionOwnerByChain.worldchain, + collateralProxyAdmin: productionOwnerByChain.worldchain, + }, +}; + const productionAmountRoutingThreshold = 250000000000; // 250k = 250 * 10^3 ^ 10^6 const productionXERC20LockboxAddress = '0x5e5F4d6B03db16E7f00dE7C9AFAA53b92C8d1D42'; @@ -338,6 +387,7 @@ function generateSuperTokenConfig( bufferCapPerChain: ChainMap, rateLimitPerSecondPerChain: ChainMap, extraLockboxes?: ChainMap<{ lockbox: Address; limits: XERC20LimitConfig }[]>, + ownerOverridesByChain?: ChainMap>, ): ChainMap { return Object.fromEntries( deploymentChains.map((chain) => [ @@ -375,6 +425,8 @@ function generateSuperTokenConfig( // This provides flexibility to use different hooks based on transfer amounts // If a destination chain is not CCIP enabled, then we use the default hook hook: generateHookConfig(chain, ownerByChain, amountRoutingThreshold), + // This is used to explicitly check the owners of each key (e.g. collateralProxyAdmin). + ownerOverrides: ownerOverridesByChain?.[chain] ?? undefined, }, ]), ); @@ -406,5 +458,6 @@ export const getSuperTokenProductionWarpConfig = async ( productionBufferCapByChain, productionRateLimitByChain, productionExtraLockboxes, + productionOwnerOverridesByChain, ); }; diff --git a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts index 011a4594ac5..44eb32b710a 100644 --- a/typescript/infra/config/environments/mainnet3/warp/warpIds.ts +++ b/typescript/infra/config/environments/mainnet3/warp/warpIds.ts @@ -39,12 +39,12 @@ export enum WarpRouteIds { EthereumVictionETH = 'ETH/ethereum-viction', EthereumVictionUSDC = 'USDC/ethereum-viction', EthereumVictionUSDT = 'USDT/ethereum-viction', - EthereumSwellZircuitPZETH = 'PZETH/ethereum-swell-zircuit', - // TODO: can be uncommented after merging this warp route into the registry - // EthereumSwellZircuitPZETHSTAGE = 'PZETHSTAGE/ethereum-swell-zircuit', + BerachainEthereumSwellUnichainZircuitPZETH = 'PZETH/berachain-ethereum-swell-unichain-zircuit', + BerachainEthereumSwellUnichainZircuitPZETHSTAGE = 'PZETHSTAGE/berachain-ethereum-swell-unichain-zircuit', EthereumBscLumiaprismLUMIA = 'LUMIA/bsc-ethereum-lumiaprism', EthereumZircuitRe7LRT = 'Re7LRT/ethereum-zircuit', InevmInjectiveINJ = 'INJ/inevm-injective', + ArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIA = 'LUMIA/arbitrum-avalanche-base-bsc-ethereum-lumiaprism-optimism-polygon', MantapacificNeutronTIA = 'TIA/mantapacific-neutron', BaseZeroNetworkCBBTC = 'CBBTC/base-zeronetwork', BaseEthereumREZ = 'REZ/base-ethereum', @@ -86,7 +86,7 @@ export enum WarpRouteIds { HyperevmSolanaSOL = 'SOL/hyperevm-solanamainnet', MintSolanaMINT = 'MINT/mint-solanamainnet', EthereumUnichainPumpBTC = 'pumpBTCuni/ethereum-unichain', - BaseEthereumLumiaprismETH = 'ETH/base-ethereum-lumiaprism', + ArbitrumBaseEthereumLumiaprismOptimismPolygonETH = 'ETH/arbitrum-base-ethereum-lumiaprism-optimism-polygon', BscHyperevmEnzoBTC = 'enzoBTC/bsc-hyperevm', BscHyperevmSTBTC = 'stBTC/bsc-hyperevm', // Soon Routes @@ -95,4 +95,12 @@ export enum WarpRouteIds { SolanaSoonMEW = 'MEW/solanamainnet-soon', SolanaSoonPnut = 'Pnut/solanamainnet-soon', SolanaSoonWIF = 'WIF/solanamainnet-soon', + SolanaSoonPOPCAT = 'POPCAT/solanamainnet-soon', + SolanaSoonGIGA = 'GIGA/solanamainnet-soon', + SolanaSoonGOAT = 'GOAT/solanamainnet-soon', + SolanaSoonSPORE = 'SPORE/solanamainnet-soon', + + // HYPER routes + HYPER = 'HYPER/arbitrum-base-bsc-ethereum-optimism', + stHYPER = 'stHYPER/bsc-ethereum', } diff --git a/typescript/infra/config/environments/test/aggregationIsm.ts b/typescript/infra/config/environments/test/aggregationIsm.ts index 3324e90bd64..d69f9220e31 100644 --- a/typescript/infra/config/environments/test/aggregationIsm.ts +++ b/typescript/infra/config/environments/test/aggregationIsm.ts @@ -9,6 +9,6 @@ export const aggregationIsm = (validatorKey: string): AggregationIsmConfig => { merkleRootMultisig(validatorKey), messageIdMultisig(validatorKey), ], - threshold: 2, + threshold: 1, }; }; diff --git a/typescript/infra/config/environments/testnet4/agent.ts b/typescript/infra/config/environments/testnet4/agent.ts index d5053e17719..9b2dbd0693a 100644 --- a/typescript/infra/config/environments/testnet4/agent.ts +++ b/typescript/infra/config/environments/testnet4/agent.ts @@ -1,6 +1,11 @@ import { GasPaymentEnforcement, GasPaymentEnforcementPolicyType, + IsmCacheConfig, + IsmCachePolicy, + IsmCacheSelectorType, + MatchingList, + ModuleType, RpcConsensusType, } from '@hyperlane-xyz/sdk'; @@ -11,13 +16,14 @@ import { } from '../../../src/config/agent/agent.js'; import { BaseRelayerConfig, + MetricAppContext, routerMatchingList, } from '../../../src/config/agent/relayer.js'; import { ALL_KEY_ROLES, Role } from '../../../src/roles.js'; -import { Contexts } from '../../contexts.js'; +import { Contexts, mustBeValidContext } from '../../contexts.js'; import { getDomainId } from '../../registry.js'; -import { environment } from './chains.js'; +import { environment, ethereumChainNames } from './chains.js'; import { helloWorld } from './helloworld.js'; import { supportedChainNames, @@ -62,6 +68,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< hyperliquidevmtestnet: true, infinityvmmonza: true, inksepolia: true, + kyvetestnet: false, modetestnet: true, monadtestnet: true, odysseytestnet: true, @@ -105,6 +112,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< hyperliquidevmtestnet: false, infinityvmmonza: true, inksepolia: true, + kyvetestnet: false, modetestnet: true, monadtestnet: true, odysseytestnet: true, @@ -148,6 +156,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< hyperliquidevmtestnet: false, infinityvmmonza: true, inksepolia: true, + kyvetestnet: false, modetestnet: true, monadtestnet: true, odysseytestnet: true, @@ -222,7 +231,90 @@ const scraperResources = { }, }; +// Kessel is a load test, these are contracts involved in the load +// test that we want to have certain relayers focus on or ignore. +const kesselMatchingList: MatchingList = [ + // classic kessel test recipient + { + recipientAddress: '0x492b3653A38e229482Bab2f7De4A094B18017246', + }, + // kessel run spice route + { + destinationDomain: getDomainId('basesepolia'), + recipientAddress: '0x4Cd2d5deD9D1ef5013fddCDceBeaCB32DFb5ad47', + }, + { + destinationDomain: getDomainId('bsctestnet'), + recipientAddress: '0x975B8Cf9501cBaD717812fcdE3b51a390AD77540', + }, + { + destinationDomain: getDomainId('optimismsepolia'), + recipientAddress: '0x554B0724432Ef42CB4a2C12E756F6F022e37aD8F', + }, + { + destinationDomain: getDomainId('arbitrumsepolia'), + recipientAddress: '0xdED2d823A5e4E82AfbBB68A3e9D947eE03EFbA9d', + }, + { + destinationDomain: getDomainId('sepolia'), + recipientAddress: '0x51BB50884Ec21063DEC3DCA0B2d4aCeF2559E65a', + }, +]; + +const kesselAppContext = 'kessel'; + +const metricAppContextsGetter = (): MetricAppContext[] => [ + { + name: 'helloworld', + matchingList: routerMatchingList(helloWorld[Contexts.Hyperlane].addresses), + }, + { + name: kesselAppContext, + matchingList: kesselMatchingList, + }, +]; + +const ismCacheConfigs: Array = [ + { + selector: { + type: IsmCacheSelectorType.DefaultIsm, + }, + // Default ISM Routing ISMs change configs based off message content, + // so they are not specified here. + moduleTypes: [ + ModuleType.AGGREGATION, + ModuleType.MERKLE_ROOT_MULTISIG, + ModuleType.MESSAGE_ID_MULTISIG, + ], + // SVM is explicitly not cached as the default ISM is a multisig ISM + // that routes internally. + chains: ethereumChainNames, + cachePolicy: IsmCachePolicy.IsmSpecific, + }, + { + selector: { + type: IsmCacheSelectorType.AppContext, + context: kesselAppContext, + }, + // Default ISM Routing ISMs change configs based off message content, + // so they are not specified here. + moduleTypes: [ + ModuleType.AGGREGATION, + ModuleType.MERKLE_ROOT_MULTISIG, + ModuleType.MESSAGE_ID_MULTISIG, + ModuleType.ROUTING, + ], + // SVM is explicitly not cached as the default ISM is a multisig ISM + // that routes internally. + chains: ethereumChainNames, + cachePolicy: IsmCachePolicy.IsmSpecific, + }, +]; + const relayBlacklist: BaseRelayerConfig['blacklist'] = [ + // Ignore kessel runner test recipients. + // All 5 test recipients have the same address. + ...kesselMatchingList, { // In an effort to reduce some giant retry queues that resulted // from spam txs to the old TestRecipient before we were charging for @@ -254,25 +346,22 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '8d76c56-20250328-185250', + tag: '385b307-20250418-150728', }, blacklist: [...releaseCandidateHelloworldMatchingList, ...relayBlacklist], gasPaymentEnforcement, - metricAppContextsGetter: () => [ - { - name: 'helloworld', - matchingList: routerMatchingList( - helloWorld[Contexts.Hyperlane].addresses, - ), - }, - ], + metricAppContextsGetter, + ismCacheConfigs, + cache: { + enabled: true, + }, resources: relayerResources, }, validators: { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '8d76c56-20250328-185250', + tag: '385b307-20250418-150728', }, chains: validatorChainConfig(Contexts.Hyperlane), resources: validatorResources, @@ -281,7 +370,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '8d76c56-20250328-185250', + tag: 'da3978b-20250414-155929', }, resources: scraperResources, }, @@ -296,26 +385,136 @@ const releaseCandidate: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '8c3e983-20250310-144838', + tag: '385b307-20250418-150728', }, - whitelist: [...releaseCandidateHelloworldMatchingList], blacklist: relayBlacklist, gasPaymentEnforcement, - transactionGasLimit: 750000, + metricAppContextsGetter, + ismCacheConfigs, + cache: { + enabled: true, + }, resources: relayerResources, }, validators: { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '73c232b-20240912-124300', + tag: '385b307-20250418-150728', }, chains: validatorChainConfig(Contexts.ReleaseCandidate), resources: validatorResources, }, }; +export const kesselRunnerNetworks = [ + 'basesepolia', + 'arbitrumsepolia', + 'sepolia', + 'bsctestnet', + 'optimismsepolia', +]; +const neutron: RootAgentConfig = { + ...contextBase, + context: Contexts.Neutron, + contextChainNames: { + validator: [], + relayer: kesselRunnerNetworks, + scraper: [], + }, + rolesWithKeys: [Role.Relayer], + relayer: { + rpcConsensusType: RpcConsensusType.Fallback, + docker: { + repo, + tag: '385b307-20250418-150728', + }, + whitelist: kesselMatchingList, + gasPaymentEnforcement, + metricAppContextsGetter, + ismCacheConfigs, + cache: { + enabled: true, + // Cache for 10 minutes + defaultExpirationSeconds: 10 * 60, + }, + resources: { + requests: { + cpu: '20000m', + memory: '32Gi', + }, + }, + }, +}; + +const getVanguardRootAgentConfig = (index: number): RootAgentConfig => ({ + ...contextBase, + context: mustBeValidContext(`vanguard${index}`), + contextChainNames: { + validator: [], + relayer: kesselRunnerNetworks, + scraper: [], + }, + rolesWithKeys: [Role.Relayer], + relayer: { + rpcConsensusType: RpcConsensusType.Fallback, + docker: { + repo, + // includes gasPriceCap overrides + per-chain maxSubmitQueueLength + tag: '9d20c65-20250418-220918', + }, + whitelist: kesselMatchingList, + gasPaymentEnforcement: [ + { + type: GasPaymentEnforcementPolicyType.None, + matchingList: kesselMatchingList, + }, + ], + metricAppContextsGetter, + ismCacheConfigs, + cache: { + enabled: true, + }, + resources: { + requests: { + cpu: '30000m', + memory: '100Gi', + }, + }, + dbBootstrap: true, + mixing: { + enabled: true, + // Arbitrary salt to ensure different agents have different sorting behavior for pending messages + salt: 69690 + index, + }, + batch: { + defaultBatchSize: 32, + batchSizeOverrides: { + // Slightly lower to ideally fit within 5M + sepolia: 26, + }, + bypassBatchSimulation: true, + maxSubmitQueueLength: { + arbitrumsepolia: 350, + basesepolia: 350, + bsctestnet: 350, + optimismsepolia: 350, + sepolia: 75, + }, + }, + txIdIndexingEnabled: false, + igpIndexingEnabled: false, + }, +}); + export const agents = { [Contexts.Hyperlane]: hyperlane, [Contexts.ReleaseCandidate]: releaseCandidate, + [Contexts.Neutron]: neutron, + [Contexts.Vanguard0]: getVanguardRootAgentConfig(0), + [Contexts.Vanguard1]: getVanguardRootAgentConfig(1), + [Contexts.Vanguard2]: getVanguardRootAgentConfig(2), + [Contexts.Vanguard3]: getVanguardRootAgentConfig(3), + [Contexts.Vanguard4]: getVanguardRootAgentConfig(4), + [Contexts.Vanguard5]: getVanguardRootAgentConfig(5), }; diff --git a/typescript/infra/config/environments/testnet4/aw-validators/rc.json b/typescript/infra/config/environments/testnet4/aw-validators/rc.json index bcecd3ec6df..980eece7d41 100644 --- a/typescript/infra/config/environments/testnet4/aw-validators/rc.json +++ b/typescript/infra/config/environments/testnet4/aw-validators/rc.json @@ -20,12 +20,8 @@ "0xbf86037899efe97bca4cea865607e10b849b5878" ] }, - "plumetestnet": { - "validators": [ - "0xe6e6aeecbf7755cdbc50c2683df9f2d100f6399d", - "0x27946c13a475233a3b1eb47f0bd0f7cdec3a3983", - "0x2596413213368475c96ddfb1ae26666d22093a8b" - ] + "holesky": { + "validators": ["0x9af51a9e6405451bb9caf12607e178db202d6821"] }, "scrollsepolia": { "validators": [ diff --git a/typescript/infra/config/environments/testnet4/chains.ts b/typescript/infra/config/environments/testnet4/chains.ts index 8d0251beb84..7a98bd984b8 100644 --- a/typescript/infra/config/environments/testnet4/chains.ts +++ b/typescript/infra/config/environments/testnet4/chains.ts @@ -15,9 +15,31 @@ export const ethereumChainNames = supportedChainNames.filter( export const chainMetadataOverrides: ChainMap> = { bsctestnet: { transactionOverrides: { - gasPrice: 8 * 10 ** 9, // 8 gwei + gasPrice: 1 * 10 ** 9, // 1 gwei + gasPriceCap: 100 * 10 ** 9, // 100 gwei cap }, }, + arbitrumsepolia: { + transactionOverrides: { + gasPriceCap: 100 * 10 ** 9, // 100 gwei cap + }, + }, + basesepolia: { + transactionOverrides: { + gasPriceCap: 100 * 10 ** 9, // 100 gwei cap + }, + }, + optimismsepolia: { + transactionOverrides: { + gasPriceCap: 100 * 10 ** 9, // 100 gwei cap + }, + }, + sepolia: { + transactionOverrides: { + gasPriceCap: 100 * 10 ** 9, // 100 gwei cap + }, + }, + // deploy-only overrides // scrollsepolia: { // transactionOverrides: { diff --git a/typescript/infra/config/environments/testnet4/funding.ts b/typescript/infra/config/environments/testnet4/funding.ts index a9e7039ba19..99d5d362a9e 100644 --- a/typescript/infra/config/environments/testnet4/funding.ts +++ b/typescript/infra/config/environments/testnet4/funding.ts @@ -10,7 +10,7 @@ export const keyFunderConfig: KeyFunderConfig< > = { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '8d76c56-20250328-185250', + tag: '4fd2990-20250414-150005', }, // We're currently using the same deployer key as testnet2. // To minimize nonce clobbering we offset the key funder cron @@ -24,6 +24,7 @@ export const keyFunderConfig: KeyFunderConfig< [Contexts.Hyperlane]: [Role.Relayer, Role.Kathy], [Contexts.ReleaseCandidate]: [Role.Relayer, Role.Kathy], }, + chainsToSkip: ['hyperliquidevmtestnet'], // desired balance config desiredBalancePerChain: { abstracttestnet: '0.1', @@ -49,6 +50,7 @@ export const keyFunderConfig: KeyFunderConfig< hyperliquidevmtestnet: '0.1', infinityvmmonza: '0', inksepolia: '0.1', + kyvetestnet: '0', modetestnet: '0.05', monadtestnet: '0.1', odysseytestnet: '0.1', diff --git a/typescript/infra/config/environments/testnet4/gasPrices.json b/typescript/infra/config/environments/testnet4/gasPrices.json index 42d61546168..32ffd9e5c55 100644 --- a/typescript/infra/config/environments/testnet4/gasPrices.json +++ b/typescript/infra/config/environments/testnet4/gasPrices.json @@ -72,7 +72,7 @@ "decimals": 9 }, "holesky": { - "amount": "0.001003929", + "amount": "0.035590357", "decimals": 9 }, "hyperliquidevmtestnet": { @@ -87,6 +87,10 @@ "amount": "0.001000252", "decimals": 9 }, + "kyvetestnet": { + "amount": "0.03", + "decimals": 1 + }, "modetestnet": { "amount": "0.001000252", "decimals": 9 @@ -96,11 +100,11 @@ "decimals": 9 }, "odysseytestnet": { - "amount": "57.569127248", + "amount": "1.000000252", "decimals": 9 }, "optimismsepolia": { - "amount": "0.001000299", + "amount": "0.001000336", "decimals": 9 }, "plumetestnet2": { @@ -108,15 +112,15 @@ "decimals": 9 }, "polygonamoy": { - "amount": "100.0", + "amount": "66.61600003", "decimals": 9 }, "scrollsepolia": { - "amount": "0.039203481", + "amount": "0.040772405", "decimals": 9 }, "sepolia": { - "amount": "0.02007895", + "amount": "9.024562066", "decimals": 9 }, "solanatestnet": { @@ -156,11 +160,11 @@ "decimals": 9 }, "unichaintestnet": { - "amount": "0.001000252", + "amount": "0.001000254", "decimals": 9 }, "weavevmtestnet": { - "amount": "1.2", + "amount": "1.000000007", "decimals": 9 } } diff --git a/typescript/infra/config/environments/testnet4/helloworld.ts b/typescript/infra/config/environments/testnet4/helloworld.ts index 059947b8788..b803840611a 100644 --- a/typescript/infra/config/environments/testnet4/helloworld.ts +++ b/typescript/infra/config/environments/testnet4/helloworld.ts @@ -4,7 +4,7 @@ import { } from '../../../src/config/helloworld/types.js'; import { Contexts } from '../../contexts.js'; -import { environment, ethereumChainNames } from './chains.js'; +import { environment } from './chains.js'; import hyperlaneAddresses from './helloworld/hyperlane/addresses.json'; import rcAddresses from './helloworld/rc/addresses.json'; @@ -13,7 +13,7 @@ export const hyperlaneHelloworld: HelloWorldConfig = { kathy: { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '857338e-20240716-165320', + tag: '3e27f07-20250415-081849', }, chainsToSkip: [], runEnv: environment, @@ -32,7 +32,7 @@ export const releaseCandidateHelloworld: HelloWorldConfig = { kathy: { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '857338e-20240716-165320', + tag: '3e27f07-20250415-081849', }, chainsToSkip: [], runEnv: environment, diff --git a/typescript/infra/config/environments/testnet4/infrastructure.ts b/typescript/infra/config/environments/testnet4/infrastructure.ts index eb747804c48..0449da0dbb5 100644 --- a/typescript/infra/config/environments/testnet4/infrastructure.ts +++ b/typescript/infra/config/environments/testnet4/infrastructure.ts @@ -41,6 +41,8 @@ export const infrastructure: InfrastructureConfig = { 'hyperlane-testnet4-', 'rc-testnet4-', 'testnet4-', + // All vanguard secrets + 'vanguard', ], }, }; diff --git a/typescript/infra/config/environments/testnet4/owners.ts b/typescript/infra/config/environments/testnet4/owners.ts index b960c75a5f8..162aaef6b31 100644 --- a/typescript/infra/config/environments/testnet4/owners.ts +++ b/typescript/infra/config/environments/testnet4/owners.ts @@ -22,6 +22,9 @@ export const owners: ChainMap = { sonicsvmtestnet: { owner: 'n/a - SVM not supported here', }, + kyvetestnet: { + owner: 'n/a - CSDK not supported here', + }, }; export const ethereumChainOwners: ChainMap = Object.fromEntries( diff --git a/typescript/infra/config/environments/testnet4/supportedChainNames.ts b/typescript/infra/config/environments/testnet4/supportedChainNames.ts index a29cd99638e..daa92765e01 100644 --- a/typescript/infra/config/environments/testnet4/supportedChainNames.ts +++ b/typescript/infra/config/environments/testnet4/supportedChainNames.ts @@ -22,6 +22,7 @@ export const testnet4SupportedChainNames = [ 'hyperliquidevmtestnet', 'infinityvmmonza', 'inksepolia', + 'kyvetestnet', 'modetestnet', 'monadtestnet', 'odysseytestnet', diff --git a/typescript/infra/config/environments/testnet4/tokenPrices.json b/typescript/infra/config/environments/testnet4/tokenPrices.json index 5dfc21974f2..9ab9776dc84 100644 --- a/typescript/infra/config/environments/testnet4/tokenPrices.json +++ b/typescript/infra/config/environments/testnet4/tokenPrices.json @@ -21,6 +21,7 @@ "hyperliquidevmtestnet": "10", "infinityvmmonza": "10", "inksepolia": "10", + "kyvetestnet": "10", "modetestnet": "10", "monadtestnet": "10", "odysseytestnet": "10", diff --git a/typescript/infra/config/environments/testnet4/validators.ts b/typescript/infra/config/environments/testnet4/validators.ts index 6fc846597c4..9d73bb9d047 100644 --- a/typescript/infra/config/environments/testnet4/validators.ts +++ b/typescript/infra/config/environments/testnet4/validators.ts @@ -522,5 +522,16 @@ export const validatorChainConfig = ( 'plumetestnet2', ), }, + + kyvetestnet: { + interval: 5, + reorgPeriod: getReorgPeriod('kyvetestnet'), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0x3c470ad2640bc0bcb6a790e8cf85e54d34ca92f5'], + }, + 'kyvetestnet', + ), + }, }; }; diff --git a/typescript/infra/config/environments/utils.ts b/typescript/infra/config/environments/utils.ts index 48830ad6112..3c23fc11c87 100644 --- a/typescript/infra/config/environments/utils.ts +++ b/typescript/infra/config/environments/utils.ts @@ -1,4 +1,8 @@ -import { ChainName } from '@hyperlane-xyz/sdk'; +import { + ChainName, + ChainSubmissionStrategy, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; import { CheckpointSyncerType, @@ -66,3 +70,28 @@ export const validatorBaseConfigsFn = chain, addresses[context], ); + +/** + * Create a GnosisSafeBuilder Strategy for each safe address + * @param safes Safe addresses for strategy + * @returns GnosisSafeBuilder Strategy for each safe address + */ +export function getGnosisSafeBuilderStrategyConfigGenerator( + safes: Record, +) { + return (): ChainSubmissionStrategy => { + return Object.fromEntries( + Object.entries(safes).map(([chain, safeAddress]) => [ + chain, + { + submitter: { + type: TxSubmitterType.GNOSIS_TX_BUILDER, + version: '1.0', + chain, + safeAddress, + }, + }, + ]), + ); + }; +} diff --git a/typescript/infra/config/relayer.json b/typescript/infra/config/relayer.json index 0d943716a70..4e7996f05e1 100644 --- a/typescript/infra/config/relayer.json +++ b/typescript/infra/config/relayer.json @@ -2,7 +2,13 @@ "mainnet3": { "hyperlane": "0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5", "neutron": "0x03787bc64a4f352b4ad172947473342028513ef3", - "rc": "0x09b96417602ed6ac76651f7a8c4860e60e3aa6d0" + "rc": "0x09b96417602ed6ac76651f7a8c4860e60e3aa6d0", + "vanguard0": "0xbe2e6b1ce045422a08a3662fffa3fc5f114efc3d", + "vanguard1": "0xdbcd22e5223f5d0040398e66dbb525308f27c655", + "vanguard2": "0x226b721316ea44aad50a10f4cc67fc30658ab4a9", + "vanguard3": "0xcdd728647ecd9d75413c9b780de303b1d1eb12a5", + "vanguard4": "0x5401627b69f317da9adf3d6e1e1214724ce49032", + "vanguard5": "0x6fd953d1cbdf3a79663b4238898147a6cf36d459" }, "test": { "hyperlane": "", @@ -11,7 +17,13 @@ }, "testnet4": { "hyperlane": "0x16626cd24fd1f228a031e48b77602ae25f8930db", - "neutron": "", - "rc": "0x7fe8c60ead4ab10be736f4de2b3090db5a851f16" + "neutron": "0xf2c72c0befa494d62949a1699a99e2c605a0b636", + "rc": "0x7fe8c60ead4ab10be736f4de2b3090db5a851f16", + "vanguard0": "0x2c9209efcaff2778d945e18fb24174e16845dc62", + "vanguard1": "0x939043d9db00f6ada1b742239beb7ddd5bf82096", + "vanguard2": "0x45b58e4d46a89c003cc7126bd971eb3794a66aeb", + "vanguard3": "0x1f4fdb150e8c9fda70687a2fd481e305af1e7f8e", + "vanguard4": "0xe41b227e7aaaf7bbd1d60258de0dd76a11a0c3fc", + "vanguard5": "0xb1d77c39166972c0873b6ae016d1a54ec3ce289b" } } diff --git a/typescript/infra/config/warp.ts b/typescript/infra/config/warp.ts index f9da8feabe9..d74e16e90b8 100644 --- a/typescript/infra/config/warp.ts +++ b/typescript/infra/config/warp.ts @@ -18,12 +18,19 @@ import { RouterConfigWithoutOwner } from '../src/config/warp.js'; import { getAncient8EthereumUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getAncient8EthereumUSDCWarpConfig.js'; import { getAppChainBaseUSDCWarpConfig } from './environments/mainnet3/warp/configGetters/getAppchainBaseUSDCWarpConfig.js'; +import { + getArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIAWarpConfig, + getLUMIAGnosisSafeBuilderStrategyConfig, +} from './environments/mainnet3/warp/configGetters/getArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIAWarpConfig.js'; import { getArbitrumBaseBlastBscEthereumGnosisMantleModeOptimismPolygonScrollZeroNetworkZoraMainnetETHWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumBaseBlastBscEthereumGnosisMantleModeOptimismPolygonScrollZeroNetworkZoraMainnetETHWarpConfig.js'; +import { + getArbitrumBaseEthereumLumiaprismOptimismPolygonETHGnosisSafeBuilderStrategyConfig, + getArbitrumBaseEthereumLumiaprismOptimismPolygonETHWarpConfig, +} from './environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumLumiaprismOptimismPolygonETHWarpConfig.js'; import { getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDC } from './environments/mainnet3/warp/configGetters/getArbitrumBaseEthereumOptimismPolygonZeroNetworkUSDCWarpConfig.js'; import { getArbitrumEthereumMantleModePolygonScrollZeroNetworkUSDTWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumBscEthereumMantleModePolygonScrollZeronetworkUSDTWarpConfig.js'; import { getArbitrumEthereumSolanaTreasureSMOLWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumEthereumSolanaTreasureSMOLWarpConfig.js'; import { getArbitrumNeutronTiaWarpConfig } from './environments/mainnet3/warp/configGetters/getArbitrumNeutronTiaWarpConfig.js'; -import { getBaseEthereumLumiaprismETHWarpConfig } from './environments/mainnet3/warp/configGetters/getBaseEthereumLumiaprismETHWarpConfig.js'; import { getBaseEthereumSuperseedCBBTCWarpConfig } from './environments/mainnet3/warp/configGetters/getBaseEthereumSuperseedCBBTCWarpConfig.js'; import { getTRUMPWarpConfig, @@ -51,13 +58,17 @@ import { getInevmInjectiveINJWarpConfig } from './environments/mainnet3/warp/con import { getMantapacificNeutronTiaWarpConfig } from './environments/mainnet3/warp/configGetters/getMantapacificNeutronTiaWarpConfig.js'; import { getMintSolanaMintWarpConfig } from './environments/mainnet3/warp/configGetters/getMintSolanaMintWarpConfig.js'; import { + getEZETHSTAGEGnosisSafeBuilderStrategyConfig, getRenzoEZETHSTAGEWarpConfig, - getRenzoGnosisSafeBuilderStagingStrategyConfig, } from './environments/mainnet3/warp/configGetters/getRenzoEZETHSTAGEWarpConfig.js'; import { + getEZETHGnosisSafeBuilderStrategyConfig, getRenzoEZETHWarpConfig, - getRenzoGnosisSafeBuilderStrategyConfig, } from './environments/mainnet3/warp/configGetters/getRenzoEZETHWarpConfig.js'; +import { + getPZETHSTAGEGnosisSafeBuilderStrategyConfig, + getRenzoPZETHStagingWarpConfig, +} from './environments/mainnet3/warp/configGetters/getRenzoPZETHSTAGEWarpConfig.js'; import { getRenzoPZETHWarpConfig } from './environments/mainnet3/warp/configGetters/getRenzoPZETHWarpConfig.js'; import { getREZBaseEthereumWarpConfig } from './environments/mainnet3/warp/configGetters/getRenzoREZBaseEthereum.js'; import { getSuperTokenProductionWarpConfig } from './environments/mainnet3/warp/configGetters/getSuperTokenWarpConfig.js'; @@ -88,7 +99,10 @@ export const warpConfigGetterMap: Record = { [WarpRouteIds.EthereumVictionETH]: getEthereumVictionETHWarpConfig, [WarpRouteIds.EthereumVictionUSDC]: getEthereumVictionUSDCWarpConfig, [WarpRouteIds.EthereumVictionUSDT]: getEthereumVictionUSDTWarpConfig, - [WarpRouteIds.EthereumSwellZircuitPZETH]: getRenzoPZETHWarpConfig, + [WarpRouteIds.BerachainEthereumSwellUnichainZircuitPZETH]: + getRenzoPZETHWarpConfig, + [WarpRouteIds.BerachainEthereumSwellUnichainZircuitPZETHSTAGE]: + getRenzoPZETHStagingWarpConfig, [WarpRouteIds.MantapacificNeutronTIA]: getMantapacificNeutronTiaWarpConfig, [WarpRouteIds.EclipseEthereumSolanaUSDT]: getEclipseEthereumSolanaUSDTWarpConfig, @@ -117,19 +131,29 @@ export const warpConfigGetterMap: Record = { // [WarpRouteIds.SuperTokenStaging]: getSuperTokenStagingWarpConfig, [WarpRouteIds.SuperUSDT]: getSuperTokenProductionWarpConfig, [WarpRouteIds.MintSolanaMINT]: getMintSolanaMintWarpConfig, - [WarpRouteIds.BaseEthereumLumiaprismETH]: - getBaseEthereumLumiaprismETHWarpConfig, + [WarpRouteIds.ArbitrumBaseEthereumLumiaprismOptimismPolygonETH]: + getArbitrumBaseEthereumLumiaprismOptimismPolygonETHWarpConfig, [WarpRouteIds.BscHyperevmEnzoBTC]: getBscHyperevmEnzoBTCWarpConfig, [WarpRouteIds.BscHyperevmSTBTC]: getBscHyperevmSTBTCWarpConfig, [WarpRouteIds.EthereumLineaTURTLE]: getEthereumLineaTurtleWarpConfig, + [WarpRouteIds.ArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIA]: + getArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIAWarpConfig, }; type StrategyConfigGetter = () => ChainSubmissionStrategy; export const strategyConfigGetterMap: Record = { + [WarpRouteIds.ArbitrumAvalancheBaseBscEthereumLumiaprismOptimismPolygonLUMIA]: + getLUMIAGnosisSafeBuilderStrategyConfig, [WarpRouteIds.ArbitrumBaseBerachainBlastBscEthereumFraxtalLineaModeOptimismSeiSwellTaikoUnichainZircuitEZETH]: - getRenzoGnosisSafeBuilderStrategyConfig, + getEZETHGnosisSafeBuilderStrategyConfig, [WarpRouteIds.ArbitrumBaseBerachainBlastBscEthereumFraxtalLineaModeOptimismSeiSwellTaikoUnichainZircuitEZETHSTAGE]: - getRenzoGnosisSafeBuilderStagingStrategyConfig, + getEZETHSTAGEGnosisSafeBuilderStrategyConfig, + [WarpRouteIds.ArbitrumBaseEthereumLumiaprismOptimismPolygonETH]: + getArbitrumBaseEthereumLumiaprismOptimismPolygonETHGnosisSafeBuilderStrategyConfig, + [WarpRouteIds.BerachainEthereumSwellUnichainZircuitPZETH]: + getEZETHGnosisSafeBuilderStrategyConfig, + [WarpRouteIds.BerachainEthereumSwellUnichainZircuitPZETHSTAGE]: + getPZETHSTAGEGnosisSafeBuilderStrategyConfig, }; /** @@ -151,6 +175,32 @@ async function getConfigFromMergedRegistry( return populateWarpRouteMailboxAddresses(warpRoute, registry); } +/** + * Retrieves all Warp configurations for the specified Warp route ID by fetching it from the MergedRegistry + * Also, populates their mailbox + * Will return in the form { [warRouteId]: { ...config } } + */ +export async function getWarpConfigMapFromMergedRegistry( + registryUris: string[], +): Promise>> { + const registry = getRegistry({ + registryUris, + enableProxy: true, + }); + const warpRouteMap = await registry.getWarpDeployConfigs(); + assert( + warpRouteMap, + `Warp route Configs not found for registry URIs: ${registryUris.join( + ', ', + )}`, + ); + return promiseObjAll( + objMap(warpRouteMap, async (_, warpRouteConfig) => + populateWarpRouteMailboxAddresses(warpRouteConfig, registry), + ), + ); +} + /** * Populates warp route configuration by filling in mailbox addresses for each chain entry * @param warpRoute The warp route configuration @@ -200,7 +250,7 @@ export async function getWarpConfig( const { owner, ownerOverrides } = config; return { owner, - ownerOverrides, + ...(ownerOverrides ? { ownerOverrides } : {}), }; }); diff --git a/typescript/infra/helm/key-funder/templates/cron-job.yaml b/typescript/infra/helm/key-funder/templates/cron-job.yaml index a94884272b6..33479c1c1f8 100644 --- a/typescript/infra/helm/key-funder/templates/cron-job.yaml +++ b/typescript/infra/helm/key-funder/templates/cron-job.yaml @@ -43,6 +43,12 @@ spec: {{- range $chain, $balance := .Values.hyperlane.igpClaimThresholdPerChain }} - --igp-claim-threshold-per-chain - {{ $chain }}={{ $balance }} +{{- end }} +{{- if .Values.hyperlane.chainsToSkip }} + - --chain-skip-override +{{- range $index, $chain := .Values.hyperlane.chainsToSkip }} + - {{ $chain }} +{{- end }} {{- end }} env: - name: PROMETHEUS_PUSH_GATEWAY diff --git a/typescript/infra/helm/key-funder/values.yaml b/typescript/infra/helm/key-funder/values.yaml index 0fd652b97e0..6006dd3c544 100644 --- a/typescript/infra/helm/key-funder/values.yaml +++ b/typescript/infra/helm/key-funder/values.yaml @@ -5,6 +5,7 @@ hyperlane: runEnv: testnet2 # Used for fetching secrets chains: [] + chainsToSkip: [] contextFundingFrom: hyperlane # key = context, value = array of roles to fund contextsAndRolesToFund: diff --git a/typescript/infra/package.json b/typescript/infra/package.json index f6aba0adf04..125b2247328 100644 --- a/typescript/infra/package.json +++ b/typescript/infra/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/infra", "description": "Infrastructure utilities for the Hyperlane Network", - "version": "11.0.0", + "version": "12.1.0", "dependencies": { "@arbitrum/sdk": "^4.0.0", "@aws-sdk/client-iam": "^3.74.0", @@ -13,10 +13,10 @@ "@ethersproject/hardware-wallets": "^5.7.0", "@ethersproject/providers": "*", "@google-cloud/secret-manager": "^5.5.0", - "@hyperlane-xyz/helloworld": "11.0.0", + "@hyperlane-xyz/helloworld": "12.1.0", "@hyperlane-xyz/registry": "11.1.0", - "@hyperlane-xyz/sdk": "11.0.0", - "@hyperlane-xyz/utils": "11.0.0", + "@hyperlane-xyz/sdk": "12.1.0", + "@hyperlane-xyz/utils": "12.1.0", "@inquirer/prompts": "3.3.2", "@nomiclabs/hardhat-etherscan": "^3.0.3", "@safe-global/api-kit": "1.3.0", diff --git a/typescript/infra/scripts/agent-utils.ts b/typescript/infra/scripts/agent-utils.ts index 459222620cd..149bef1e51e 100644 --- a/typescript/infra/scripts/agent-utils.ts +++ b/typescript/infra/scripts/agent-utils.ts @@ -15,6 +15,7 @@ import { import { Address, ProtocolType, + inCIMode, objFilter, objMap, promiseObjAll, @@ -51,7 +52,6 @@ import { assertRole, filterRemoteDomainMetadata, getInfraPath, - inCIMode, readJSONAtPath, writeMergedJSONAtPath, } from '../src/utils/utils.js'; @@ -345,6 +345,13 @@ export function withSkipReview(args: Argv) { .default('skipReview', false); } +export function withPropose(args: Argv) { + return args + .describe('propose', 'Propose') + .boolean('propose') + .default('propose', false); +} + // Interactively gets a single warp route ID export async function getWarpRouteIdInteractive() { const choices = Object.values(WarpRouteIds) @@ -376,7 +383,7 @@ export async function getWarpRouteIdsInteractive() { pageSize: 30, }); if (!selection.length) { - console.log('Please select at least one Warp Route ID'); + rootLogger.info('Please select at least one Warp Route ID'); } } @@ -675,7 +682,7 @@ export async function assertCorrectKubeContext(coreConfig: EnvironmentConfig) { !currentKubeContext.endsWith(`${coreConfig.infra.kubernetes.clusterName}`) ) { const cluster = coreConfig.infra.kubernetes.clusterName; - console.error( + rootLogger.error( `Cowardly refusing to deploy using current k8s context ${currentKubeContext}; are you sure you have the right k8s context active?`, `Want clusterName ${cluster}`, `Run gcloud container clusters get-credentials ${cluster} --zone us-east1-c`, diff --git a/typescript/infra/scripts/agents/update-agent-config.ts b/typescript/infra/scripts/agents/update-agent-config.ts index 3ee6adece1d..ca87040bc83 100644 --- a/typescript/infra/scripts/agents/update-agent-config.ts +++ b/typescript/infra/scripts/agents/update-agent-config.ts @@ -25,6 +25,8 @@ import { } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts.js'; +import mainnet3GasPrices from '../../config/environments/mainnet3/gasPrices.json' assert { type: 'json' }; +import testnet4GasPrices from '../../config/environments/testnet4/gasPrices.json' assert { type: 'json' }; import { getCombinedChainsToScrape } from '../../src/config/agent/scraper.js'; import { DeployEnvironment, @@ -67,12 +69,23 @@ export async function writeAgentConfig( await Promise.all( environmentChains .filter((chain) => chainIsProtocol(chain, ProtocolType.Cosmos)) - .map(async (chain) => [ - chain, - { - gasPrice: await getCosmosChainGasPrice(chain, multiProvider), - }, - ]), + .map(async (chain) => { + try { + const gasPrice = await getCosmosChainGasPrice(chain, multiProvider); + return [chain, { gasPrice }]; + } catch (error) { + rootLogger.error(`Error getting gas price for ${chain}:`, error); + const { denom } = await multiProvider.getNativeToken(chain); + assert(denom, `No nativeToken.denom found for chain ${chain}`); + const amount = + environment === 'mainnet3' + ? mainnet3GasPrices[chain as keyof typeof mainnet3GasPrices] + .amount + : testnet4GasPrices[chain as keyof typeof testnet4GasPrices] + .amount; + return [chain, { gasPrice: { denom, amount } }]; + } + }), ), ); diff --git a/typescript/infra/scripts/check/check-owner-ica.ts b/typescript/infra/scripts/check/check-owner-ica.ts index 99d13ed3a2e..f508b793502 100644 --- a/typescript/infra/scripts/check/check-owner-ica.ts +++ b/typescript/infra/scripts/check/check-owner-ica.ts @@ -6,16 +6,21 @@ import { configureRootLogger, eqAddress, isZeroishAddress, + rootLogger, } from '@hyperlane-xyz/utils'; -import { icas } from '../../config/environments/mainnet3/owners.js'; +import { + getGovernanceIcas, + getGovernanceSafes, +} from '../../config/environments/mainnet3/governance/utils.js'; import { chainsToSkip } from '../../src/config/chain.js'; +import { withGovernanceType } from '../../src/governance.js'; import { isEthereumProtocolChain } from '../../src/utils/utils.js'; import { getArgs as getEnvArgs, withChains } from '../agent-utils.js'; import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js'; function getArgs() { - return withChains(getEnvArgs()).option('ownerChain', { + return withGovernanceType(withChains(getEnvArgs())).option('ownerChain', { type: 'string', description: 'Origin chain where the Safe owner lives', default: 'ethereum', @@ -25,20 +30,25 @@ function getArgs() { async function main() { configureRootLogger(LogFormat.Pretty, LogLevel.Info); - const { environment, ownerChain, chains } = await getArgs(); + const { environment, ownerChain, chains, governanceType } = await getArgs(); const config = getEnvironmentConfig(environment); const multiProvider = await config.getMultiProvider(); - const owner = config.owners[ownerChain].ownerOverrides?._safeAddress; + const owner = getGovernanceSafes(governanceType)[ownerChain]; if (!owner) { - console.error(`No Safe owner found for ${ownerChain}`); + rootLogger.error(`No Safe owner found for ${ownerChain}`); process.exit(1); } - console.log(`Safe owner on ${ownerChain}: ${owner}`); + rootLogger.info(`Safe owner on ${ownerChain}: ${owner}`); const { chainAddresses } = await getHyperlaneCore(environment, multiProvider); - const ica = InterchainAccount.fromAddressesMap(chainAddresses, multiProvider); + const interchainAccountApp = InterchainAccount.fromAddressesMap( + chainAddresses, + multiProvider, + ); + + const icas = getGovernanceIcas(governanceType); const checkOwnerIcaChains = ( chains?.length ? chains : Object.keys(icas) @@ -51,10 +61,11 @@ async function main() { owner: owner, }; const ownerChainInterchainAccountRouter = - ica.contractsMap[ownerChain].interchainAccountRouter.address; + interchainAccountApp.contractsMap[ownerChain].interchainAccountRouter + .address; if (isZeroishAddress(ownerChainInterchainAccountRouter)) { - console.error(`Interchain account router address is zero`); + rootLogger.error(`Interchain account router address is zero`); process.exit(1); } @@ -63,12 +74,12 @@ async function main() { { Expected: Address; Actual: Address } > = {}; for (const chain of checkOwnerIcaChains) { - const expectedAddress = icas[chain as keyof typeof icas]; + const expectedAddress = icas[chain]; if (!expectedAddress) { - console.error(`No expected address found for ${chain}`); + rootLogger.error(`No expected address found for ${chain}`); continue; } - const actualAccount = await ica.getAccount( + const actualAccount = await interchainAccountApp.getAccount( chain, ownerConfig, ownerChainInterchainAccountRouter, @@ -82,16 +93,17 @@ async function main() { } if (Object.keys(mismatchedResults).length > 0) { - console.error('\nMismatched ICAs found:'); + rootLogger.error('\nMismatched ICAs found:'); + // eslint-disable-next-line no-console console.table(mismatchedResults); process.exit(1); } else { - console.log('✅ All ICAs match the expected addresses.'); + rootLogger.info('✅ All ICAs match the expected addresses.'); } process.exit(0); } main().catch((err) => { - console.error('Error:', err); + rootLogger.error('Error:', err); process.exit(1); }); diff --git a/typescript/infra/scripts/check/check-validator-announce.ts b/typescript/infra/scripts/check/check-validator-announce.ts index a57e89e8f7c..fb5f606b1b5 100644 --- a/typescript/infra/scripts/check/check-validator-announce.ts +++ b/typescript/infra/scripts/check/check-validator-announce.ts @@ -32,43 +32,55 @@ async function main() { const results = await Promise.all( targetNetworks.map(async (chain) => { - const validatorAnnounce = core.getContracts(chain).validatorAnnounce; - const announcedValidators = - await validatorAnnounce.getAnnouncedValidators(); - - const defaultValidatorConfigs = - defaultMultisigConfigs[chain].validators || []; - const validators = defaultValidatorConfigs.map((v) => v.address); - const unannouncedValidators = validators.filter( - (validator) => - !announcedValidators.some((x) => eqAddress(x, validator)), - ); - - if (unannouncedValidators.length > 0) { - chainsWithUnannouncedValidators[chain] = unannouncedValidators; + try { + const validatorAnnounce = core.getContracts(chain).validatorAnnounce; + const announcedValidators = + await validatorAnnounce.getAnnouncedValidators(); + + const defaultValidatorConfigs = + defaultMultisigConfigs[chain].validators || []; + const validators = defaultValidatorConfigs.map((v) => v.address); + const unannouncedValidators = validators.filter( + (validator) => + !announcedValidators.some((x) => eqAddress(x, validator)), + ); + + if (unannouncedValidators.length > 0) { + chainsWithUnannouncedValidators[chain] = unannouncedValidators; + } + + const validatorCount = validators.length; + const unannouncedValidatorCount = unannouncedValidators.length; + + const threshold = defaultMultisigConfigs[chain].threshold; + const minimumThreshold = getMinimumThreshold(validatorCount); + + return { + chain, + threshold, + [thresholdOK]: + threshold < minimumThreshold || threshold > validatorCount + ? CheckResult.WARNING + : CheckResult.OK, + total: validatorCount, + [totalOK]: + validatorCount < minimumValidatorCount + ? CheckResult.WARNING + : CheckResult.OK, + unannounced: + unannouncedValidatorCount > 0 ? unannouncedValidatorCount : '', + }; + } catch (error) { + console.error(`Error processing chain ${chain}:`, error); + return { + chain, + threshold: 'ERROR', + [thresholdOK]: CheckResult.WARNING, + total: 0, + [totalOK]: CheckResult.WARNING, + unannounced: 'ERROR', + }; } - - const validatorCount = validators.length; - const unannouncedValidatorCount = unannouncedValidators.length; - - const threshold = defaultMultisigConfigs[chain].threshold; - const minimumThreshold = getMinimumThreshold(validatorCount); - - return { - chain, - threshold, - [thresholdOK]: - threshold < minimumThreshold || threshold > validatorCount - ? CheckResult.WARNING - : CheckResult.OK, - total: validatorCount, - [totalOK]: - validatorCount < minimumValidatorCount - ? CheckResult.WARNING - : CheckResult.OK, - unannounced: - unannouncedValidatorCount > 0 ? unannouncedValidatorCount : '', - }; }), ); diff --git a/typescript/infra/scripts/check/check-warp-deploy.ts b/typescript/infra/scripts/check/check-warp-deploy.ts index d80f06dc193..27d1f38d281 100644 --- a/typescript/infra/scripts/check/check-warp-deploy.ts +++ b/typescript/infra/scripts/check/check-warp-deploy.ts @@ -1,11 +1,13 @@ import chalk from 'chalk'; import { Gauge, Registry } from 'prom-client'; +import { DEFAULT_GITHUB_REGISTRY } from '@hyperlane-xyz/registry'; import { ChainName } from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; import { WarpRouteIds } from '../../config/environments/mainnet3/warp/warpIds.js'; -import { getWarpAddresses } from '../../config/registry.js'; -import { warpConfigGetterMap } from '../../config/warp.js'; +import { DEFAULT_REGISTRY_URI } from '../../config/registry.js'; +import { getWarpConfigMapFromMergedRegistry } from '../../config/warp.js'; import { submitMetrics } from '../../src/utils/metrics.js'; import { Modules, getWarpRouteIdsInteractive } from '../agent-utils.js'; import { getEnvironmentConfig } from '../core-utils.js'; @@ -40,6 +42,11 @@ async function main() { WarpRouteIds.ArbitrumBaseBlastBscEthereumGnosisLiskMantleModeOptimismPolygonScrollZeroNetworkZoraMainnet, ]; + const registries = [DEFAULT_GITHUB_REGISTRY, DEFAULT_REGISTRY_URI]; + const warpCoreConfigMap = await getWarpConfigMapFromMergedRegistry( + registries, + ); + let warpIdsToCheck: string[]; if (interactive) { warpIdsToCheck = await getWarpRouteIdsInteractive(); @@ -47,22 +54,21 @@ async function main() { console.log(chalk.yellow('Skipping the following warp routes:')); routesToSkip.forEach((route) => console.log(chalk.yellow(`- ${route}`))); - warpIdsToCheck = Object.keys(warpConfigGetterMap).filter( + warpIdsToCheck = Object.keys(warpCoreConfigMap).filter( (warpRouteId) => !routesToSkip.includes(warpRouteId), ); } - // Determine which chains have warp configs - const chainsWithWarpConfigs = warpIdsToCheck.reduce((chains, warpRouteId) => { - const warpAddresses = getWarpAddresses(warpRouteId); - Object.keys(warpAddresses).forEach((chain) => chains.add(chain)); + // Get all the chains from warpCoreConfigMap. Used to initialize the MultiProvider. + const warpConfigChains = warpIdsToCheck.reduce((chains, warpRouteId) => { + const warpConfigs = warpCoreConfigMap[warpRouteId]; + assert(warpConfigs, `Config not found in registry for ${warpRouteId}`); + Object.keys(warpConfigs).forEach((chain) => chains.add(chain)); return chains; }, new Set()); console.log( - `Found warp configs for chains: ${Array.from(chainsWithWarpConfigs).join( - ', ', - )}`, + `Found warp configs for chains: ${Array.from(warpConfigChains).join(', ')}`, ); // Get the multiprovider once to avoid recreating it for each warp route @@ -74,7 +80,7 @@ async function main() { undefined, undefined, undefined, - Array.from(chainsWithWarpConfigs), + Array.from(warpConfigChains), ); // TODO: consider retrying this if check throws an error @@ -93,6 +99,7 @@ async function main() { fork, false, multiProvider, + registries, ); await governor.check(); diff --git a/typescript/infra/scripts/deploy.ts b/typescript/infra/scripts/deploy.ts index 994bc6ebd9d..99ad6aefb49 100644 --- a/typescript/infra/scripts/deploy.ts +++ b/typescript/infra/scripts/deploy.ts @@ -22,7 +22,7 @@ import { LiquidityLayerDeployer, TestRecipientDeployer, } from '@hyperlane-xyz/sdk'; -import { objFilter, objMap } from '@hyperlane-xyz/utils'; +import { inCIMode, objFilter, objMap } from '@hyperlane-xyz/utils'; import { Contexts } from '../config/contexts.js'; import { core as coreConfig } from '../config/environments/mainnet3/core.js'; @@ -37,7 +37,7 @@ import { } from '../src/deployment/verify.js'; import { Role } from '../src/roles.js'; import { impersonateAccount, useLocalProvider } from '../src/utils/fork.js'; -import { inCIMode, writeYamlAtPath } from '../src/utils/utils.js'; +import { writeYamlAtPath } from '../src/utils/utils.js'; import { Modules, diff --git a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts index 247cddf0844..03537689af6 100644 --- a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts +++ b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts @@ -91,6 +91,10 @@ const RC_FUNDING_DISCOUNT_DENOMINATOR = ethers.BigNumber.from(10); const CONTEXT_FUNDING_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes const CHAIN_FUNDING_TIMEOUT_MS = 1 * 60 * 1000; // 1 minute +// Need to ensure we don't fund non-vanguard chains in the vanguard contexts +const VANGUARD_CHAINS = ['base', 'arbitrum', 'optimism', 'ethereum', 'bsc']; +const VANGUARD_CONTEXTS: Contexts[] = [Contexts.Vanguard0]; + // Funds key addresses for multiple contexts from the deployer key of the context // specified via the `--context` flag. // The --contexts-and-roles flag is used to specify the contexts and the key roles @@ -152,7 +156,11 @@ async function main() { .boolean('skip-igp-claim') .describe('skip-igp-claim', 'If true, never claims funds from the IGP') - .default('skip-igp-claim', false).argv; + .default('skip-igp-claim', false) + + .array('chain-skip-override') + .describe('chain-skip-override', 'Array of chains to skip funding for') + .default('chain-skip-override', []).argv; constMetricLabels.hyperlane_deployment = environment; const config = getEnvironmentConfig(environment); @@ -170,6 +178,7 @@ async function main() { multiProvider, argv.contextsAndRoles, argv.skipIgpClaim, + argv.chainSkipOverride, argv.desiredBalancePerChain, argv.desiredKathyBalancePerChain ?? {}, argv.igpClaimThresholdPerChain ?? {}, @@ -186,6 +195,7 @@ async function main() { context, argv.contextsAndRoles[context]!, argv.skipIgpClaim, + argv.chainSkipOverride, argv.desiredBalancePerChain, argv.desiredKathyBalancePerChain ?? {}, argv.igpClaimThresholdPerChain ?? {}, @@ -238,6 +248,7 @@ class ContextFunder { public readonly context: Contexts, public readonly rolesToFund: FundableRole[], public readonly skipIgpClaim: boolean, + public readonly chainSkipOverride: ChainName[], public readonly desiredBalancePerChain: KeyFunderConfig< ChainName[] >['desiredBalancePerChain'], @@ -252,6 +263,18 @@ class ContextFunder { roleKeysPerChain = objFilter( roleKeysPerChain, (chain, _roleKeys): _roleKeys is Record => { + // Skip funding for vanguard contexts on non-vanguard chains + if ( + VANGUARD_CONTEXTS.includes(this.context) && + !VANGUARD_CHAINS.includes(chain) + ) { + logger.warn( + { chain, context: this.context }, + 'Skipping funding for vanguard context on non-vanguard chain', + ); + return false; + } + const valid = isEthereumProtocolChain(chain) && multiProvider.tryGetChainName(chain) !== null; @@ -290,6 +313,7 @@ class ContextFunder { multiProvider: MultiProvider, contextsAndRolesToFund: ContextAndRolesMap, skipIgpClaim: boolean, + chainSkipOverride: ChainName[], desiredBalancePerChain: KeyFunderConfig< ChainName[] >['desiredBalancePerChain'], @@ -365,6 +389,7 @@ class ContextFunder { context, contextsAndRolesToFund[context]!, skipIgpClaim, + chainSkipOverride, desiredBalancePerChain, desiredKathyBalancePerChain, igpClaimThresholdPerChain, @@ -378,6 +403,7 @@ class ContextFunder { context: Contexts, rolesToFund: FundableRole[], skipIgpClaim: boolean, + chainSkipOverride: ChainName[], desiredBalancePerChain: KeyFunderConfig< ChainName[] >['desiredBalancePerChain'], @@ -430,6 +456,7 @@ class ContextFunder { context, rolesToFund, skipIgpClaim, + chainSkipOverride, desiredBalancePerChain, desiredKathyBalancePerChain, igpClaimThresholdPerChain, @@ -450,6 +477,14 @@ class ContextFunder { } private async fundChain(chain: string, keys: BaseAgentKey[]): Promise { + if (this.chainSkipOverride.includes(chain)) { + logger.warn( + { chain }, + `Configured to skip funding operations for chain ${chain}, skipping`, + ); + return; + } + const { promise, cleanup } = createTimeoutPromise( CHAIN_FUNDING_TIMEOUT_MS, `Timed out funding chain ${chain} after ${ diff --git a/typescript/infra/scripts/funding/vanguard.ts b/typescript/infra/scripts/funding/vanguard.ts new file mode 100644 index 00000000000..0f8a65484c9 --- /dev/null +++ b/typescript/infra/scripts/funding/vanguard.ts @@ -0,0 +1,244 @@ +import chalk from 'chalk'; +import { formatUnits, parseUnits } from 'ethers/lib/utils.js'; +import yargs from 'yargs'; + +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { Address, rootLogger } from '@hyperlane-xyz/utils'; + +import { Contexts } from '../../config/contexts.js'; +import { DEPLOYER } from '../../config/environments/mainnet3/owners.js'; +import { Role } from '../../src/roles.js'; +import { getEnvironmentConfig } from '../core-utils.js'; + +const VANGUARDS = [ + 'vanguard0', // regular + 'vanguard1', // regular + 'vanguard2', // regular + 'vanguard3', // 5x gas cap + 'vanguard4', // 5x gas cap + 'vanguard5', // 10x gas cap +] as const; + +type VanguardName = (typeof VANGUARDS)[number]; +type VanguardBalance = { + chain: string; + vanguard: VanguardName; + balance: string; +}; + +const TOKEN_DECIMALS = 18; +const MIN_FUNDING_AMOUNT = parseUnits('0.05', TOKEN_DECIMALS); + +const VANGUARD_ENVIRONMENT = 'mainnet3'; +const VANGUARD_ADDRESSES: Record = { + vanguard0: '0xbe2e6b1ce045422a08a3662fffa3fc5f114efc3d', + vanguard1: '0xdbcd22e5223f5d0040398e66dbb525308f27c655', + vanguard2: '0x226b721316ea44aad50a10f4cc67fc30658ab4a9', + vanguard3: '0xcdd728647ecd9d75413c9b780de303b1d1eb12a5', + vanguard4: '0x5401627b69f317da9adf3d6e1e1214724ce49032', + vanguard5: '0x6fd953d1cbdf3a79663b4238898147a6cf36d459', +}; + +const VANGUARD_NETWORKS = [ + 'base', + 'arbitrum', + 'optimism', + 'ethereum', + 'bsc', +] as const; + +const VANGUARD_FUNDING_CONFIGS: Record< + (typeof VANGUARD_NETWORKS)[number], + Record +> = { + base: { + vanguard0: '1', + vanguard1: '1', + vanguard2: '1', + vanguard3: '1', + vanguard4: '1', + vanguard5: '1', + }, + arbitrum: { + vanguard0: '1', + vanguard1: '1', + vanguard2: '1', + vanguard3: '1', + vanguard4: '1', + vanguard5: '1', + }, + optimism: { + vanguard0: '1', + vanguard1: '1', + vanguard2: '1', + vanguard3: '1', + vanguard4: '1', + vanguard5: '1', + }, + ethereum: { + vanguard0: '5', + vanguard1: '5', + vanguard2: '5', + vanguard3: '5', + vanguard4: '5', + vanguard5: '5', + }, + bsc: { + vanguard0: '10', + vanguard1: '10', + vanguard2: '5', + vanguard3: '5', + vanguard4: '5', + vanguard5: '5', + }, +} as const; + +const ACTIVE_VANGUARDS: VanguardName[] = ['vanguard0']; + +async function fundVanguards() { + const { fund } = await yargs(process.argv.slice(2)) + .describe('fund', 'Fund vanguards') + .boolean('fund') + .default('fund', false).argv; + + const envConfig = getEnvironmentConfig(VANGUARD_ENVIRONMENT); + const multiProvider = await envConfig.getMultiProvider( + Contexts.Hyperlane, + Role.Deployer, + true, + ); + + // Print balances before funding + const currentBalances: ChainMap> = {}; + for (const chain of VANGUARD_NETWORKS) { + currentBalances[chain] = {}; + const provider = multiProvider.getProvider(chain); + const deployerBalance = await provider.getBalance(DEPLOYER); + currentBalances[chain]['deployer'] = Number( + formatUnits(deployerBalance, TOKEN_DECIMALS), + ).toFixed(3); // Round to 3 decimal places + + for (const vanguard of VANGUARDS) { + const address = VANGUARD_ADDRESSES[vanguard]; + const currentBalance = await provider.getBalance(address); + currentBalances[chain][vanguard] = Number( + formatUnits(currentBalance, TOKEN_DECIMALS), + ).toFixed(3); // Round to 3 decimal places + } + } + + rootLogger.info('\nCurrent balances:'); + // eslint-disable-next-line no-console + console.table(currentBalances); + + // Track which vanguards were topped up + const topUpsNeeded: ChainMap = {}; + + await Promise.all( + VANGUARD_NETWORKS.map(async (chain) => { + for (const vanguard of ACTIVE_VANGUARDS) { + const address = VANGUARD_ADDRESSES[vanguard]; + try { + const provider = multiProvider.getProvider(chain); + const currentBalance = await provider.getBalance(address); + const desiredBalance = parseUnits( + VANGUARD_FUNDING_CONFIGS[chain][vanguard], + TOKEN_DECIMALS, + ); + const delta = desiredBalance.sub(currentBalance); + + if (delta.gt(MIN_FUNDING_AMOUNT)) { + topUpsNeeded[chain] = topUpsNeeded[chain] || []; + topUpsNeeded[chain].push({ + chain, + vanguard, + balance: Number(formatUnits(delta, TOKEN_DECIMALS)).toFixed(3), + }); + } + } catch (error) { + rootLogger.error( + chalk.bold.red( + `Error topping up ${vanguard} on chain ${chain}:`, + error, + ), + ); + } + } + }), + ); + + // Print summary of topped up vanguards + if (Object.keys(topUpsNeeded).length > 0) { + rootLogger.info('\nTop ups needed for the following:'); + // eslint-disable-next-line no-console + console.table( + Object.entries(topUpsNeeded).reduce((acc, [chain, topUps]) => { + const chainEntries: Record = {} as Record< + VanguardName, + string + >; + VANGUARDS.forEach((vanguard) => { + const match = topUps.find((t) => t.vanguard === vanguard); + chainEntries[vanguard] = match ? match.balance : '-'; + }); + acc[chain] = chainEntries; + return acc; + }, {} as Record>), + ); + + if (fund) { + rootLogger.info(chalk.italic.blue('\nFunding vanguards...')); + } else { + rootLogger.info(chalk.italic.yellow('\nDry run - not funding vanguards')); + process.exit(0); + } + + await Promise.all( + Object.entries(topUpsNeeded).map(async ([chain, topUps]) => { + for (const { vanguard, balance: topUpAmount } of topUps) { + try { + const signer = multiProvider.getSigner(chain); + const signerBalance = await signer.getBalance(); + + // Convert balance to a BigNumber by using parseUnits + const amount = parseUnits(topUpAmount, TOKEN_DECIMALS); + + if (signerBalance.lt(amount)) { + rootLogger.warn( + chalk.bold.yellow( + `Insufficient balance for ${vanguard} on ${chain}. Required: ${formatUnits( + amount, + TOKEN_DECIMALS, + )}, Available: ${formatUnits(signerBalance, TOKEN_DECIMALS)}`, + ), + ); + continue; + } + + await multiProvider.sendTransaction(chain, { + to: VANGUARD_ADDRESSES[vanguard], + value: amount, + }); + } catch (error) { + rootLogger.error( + chalk.bold.red( + `Error checking balance for ${vanguard} on ${chain}:`, + error, + ), + ); + continue; + } + } + }), + ); + } else { + rootLogger.info(chalk.bold.green('\nNo vanguards needed topping up')); + } + + process.exit(0); +} + +fundVanguards().catch((error) => { + rootLogger.error(chalk.bold.red('Error funding agents:', error)); + process.exit(1); +}); diff --git a/typescript/infra/scripts/funding/write-alert.ts b/typescript/infra/scripts/funding/write-alert.ts index 27cae18d193..cc09ae64f6a 100644 --- a/typescript/infra/scripts/funding/write-alert.ts +++ b/typescript/infra/scripts/funding/write-alert.ts @@ -1,5 +1,6 @@ import { confirm } from '@inquirer/prompts'; import { ChildProcess } from 'child_process'; +import yargs from 'yargs'; import { ChainMap } from '@hyperlane-xyz/sdk'; import { rootLogger } from '@hyperlane-xyz/utils'; @@ -30,6 +31,7 @@ import { portForwardPrometheusServer, } from '../../src/infrastructure/monitoring/prometheus.js'; import { readJSONAtPath } from '../../src/utils/utils.js'; +import { withDryRun } from '../agent-utils.js'; interface AlertUpdateInfo { alertType: AlertType; @@ -44,6 +46,8 @@ interface RegressionError { } async function main() { + const { dryRun } = await withDryRun(yargs(process.argv.slice(2))).argv; + // runs a validation check to ensure the threshold configs are valid relative to each other await validateBalanceThresholdConfigs(); @@ -89,12 +93,6 @@ async function main() { alertType: alert, missingChains, }); - rootLogger.error( - `Missing thresholds for chains: ${missingChains.join( - ', ', - )} for ${alert} config, skipping updating this alert`, - ); - continue; } // generate a table of the differences in the thresholds, prompt the user to confirm the changes @@ -141,10 +139,14 @@ async function main() { } // abort if there are any missing thresholds in the config to avoid introducing a regression - handleMissingChainErrors(missingChainErrors); + await handleMissingChainErrors(missingChainErrors); - // update the alerts with the new thresholds via the Grafana API - await updateAlerts(alertUpdateInfo, saToken, portForwardProcess); + if (!dryRun) { + // update the alerts with the new thresholds via the Grafana API + await updateAlerts(alertUpdateInfo, saToken, portForwardProcess); + } else { + rootLogger.info('Dry run, not updating alerts'); + } } finally { portForwardProcess.kill(); } @@ -228,49 +230,63 @@ function generateDiffTable( currentThresholds: ChainMap, proposedThresholds: ChainMap, ) { - const diffTable = Object.entries(proposedThresholds).reduce( - (acc, [chain, newThreshold]) => { - const currentThreshold = currentThresholds[chain]; - if (currentThreshold !== proposedThresholds[chain]) { - acc.push({ - chain, - current: currentThreshold, - new: newThreshold, - change: - currentThreshold === undefined - ? 'new' - : currentThreshold < newThreshold - ? 'increase' - : 'decrease', - }); - } - return acc; - }, - [] as { - chain: string; - current: number; - new: number; - change: 'increase' | 'decrease' | 'new'; - }[], - ); + const diffTable = []; + + // Check for changes and additions + for (const [chain, newThreshold] of Object.entries(proposedThresholds)) { + const currentThreshold = currentThresholds[chain]; + if (currentThreshold !== newThreshold) { + diffTable.push({ + chain, + current: currentThreshold, + new: newThreshold, + change: + currentThreshold === undefined + ? 'new' + : currentThreshold < newThreshold + ? 'increase' + : 'decrease', + }); + } + } + + // Check for removals + for (const chain of Object.keys(currentThresholds)) { + if (!(chain in proposedThresholds)) { + diffTable.push({ + chain, + current: currentThresholds[chain], + new: undefined, + change: 'removed', + }); + } + } return diffTable; } -function handleMissingChainErrors(missingChainErrors: RegressionError[]) { +async function handleMissingChainErrors(missingChainErrors: RegressionError[]) { if (missingChainErrors.length === 0) return; for (const error of missingChainErrors) { - rootLogger.error( - `Missing thresholds for chains: ${error.missingChains.join(', ')} for ${ - error.alertType - } config`, - ); + for (const chain of error.missingChains) { + const confirmed = await confirm({ + message: `Is the missing chain "${chain}" for ${error.alertType} expected?`, + default: false, + }); + + if (!confirmed) { + rootLogger.error( + `Aborting updating alerts due to unexpected missing chain: ${chain} for ${error.alertType}`, + ); + process.exit(1); + } + } } - rootLogger.error( - `Aborting updating alerts due to missing thresholds in config`, + + rootLogger.info( + 'Proceeding with alert updates - all missing chains confirmed as expected', ); - process.exit(1); } async function confirmFiringAlerts( diff --git a/typescript/infra/scripts/keys/get-owner-ica.ts b/typescript/infra/scripts/keys/get-owner-ica.ts index 730e5e7bdda..7728dfbd20c 100644 --- a/typescript/infra/scripts/keys/get-owner-ica.ts +++ b/typescript/infra/scripts/keys/get-owner-ica.ts @@ -6,19 +6,24 @@ import { configureRootLogger, eqAddress, isZeroishAddress, + rootLogger, } from '@hyperlane-xyz/utils'; +import { getGovernanceSafes } from '../../config/environments/mainnet3/governance/utils.js'; +import { icaOwnerChain } from '../../config/environments/mainnet3/owners.js'; import { chainsToSkip } from '../../src/config/chain.js'; +import { withGovernanceType } from '../../src/governance.js'; import { isEthereumProtocolChain } from '../../src/utils/utils.js'; import { getArgs as getEnvArgs, withChains } from '../agent-utils.js'; import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js'; function getArgs() { - return withChains(getEnvArgs()) + return withGovernanceType(withChains(getEnvArgs())) .option('ownerChain', { type: 'string', description: 'Origin chain where the governing owner lives', demandOption: true, + default: icaOwnerChain, }) .option('owner', { type: 'string', @@ -43,16 +48,19 @@ async function main() { chains, deploy, owner: ownerOverride, + governanceType, } = await getArgs(); const config = getEnvironmentConfig(environment); const multiProvider = await config.getMultiProvider(); - const originOwner = ownerOverride ?? config.owners[ownerChain]?.owner; + // Get the safe owner for the given governance type + const governanceOwner = getGovernanceSafes(governanceType)[ownerChain]; + const originOwner = ownerOverride ?? governanceOwner; if (!originOwner) { throw new Error(`No owner found for ${ownerChain}`); } - console.log(`Governance owner on ${ownerChain}: ${originOwner}`); + rootLogger.info(`Governance owner on ${ownerChain}: ${originOwner}`); const { chainAddresses } = await getHyperlaneCore(environment, multiProvider); const ica = InterchainAccount.fromAddressesMap(chainAddresses, multiProvider); @@ -65,7 +73,7 @@ async function main() { ica.contractsMap[ownerChain].interchainAccountRouter.address; if (isZeroishAddress(ownerChainInterchainAccountRouter)) { - console.error(`Interchain account router address is zero`); + rootLogger.error(`Interchain account router address is zero`); process.exit(1); } @@ -94,7 +102,7 @@ async function main() { ); result.Deployed = eqAddress(account, deployedAccount) ? '✅' : '❌'; if (result.Deployed === '❌') { - console.warn( + rootLogger.warn( `Mismatch between account and deployed account for ${chain}`, ); } @@ -102,7 +110,7 @@ async function main() { return { chain, result }; } catch (error) { - console.error(`Error processing chain ${chain}:`, error); + rootLogger.error(`Error processing chain ${chain}:`, error); return { chain, error }; } }), @@ -112,15 +120,16 @@ async function main() { if (settledResult.status === 'fulfilled') { const { chain, result, error } = settledResult.value; if (error || !result) { - console.error(`Failed to process ${chain}:`, error); + rootLogger.error(`Failed to process ${chain}:`, error); } else { results[chain] = result; } } else { - console.error(`Promise rejected:`, settledResult.reason); + rootLogger.error(`Promise rejected:`, settledResult.reason); } }); + // eslint-disable-next-line no-console console.table(results); process.exit(0); } @@ -128,6 +137,6 @@ async function main() { main() .then() .catch((err) => { - console.error('Error:', err); + rootLogger.error('Error:', err); process.exit(1); }); diff --git a/typescript/infra/scripts/print-gas-prices.ts b/typescript/infra/scripts/print-gas-prices.ts index 040e9b9a338..48070726fc0 100644 --- a/typescript/infra/scripts/print-gas-prices.ts +++ b/typescript/infra/scripts/print-gas-prices.ts @@ -84,11 +84,26 @@ async function getGasPrice( }; } case ProtocolType.Cosmos: { - const { amount } = await getCosmosChainGasPrice(chain, mpp); - return { - amount, - decimals: 1, - }; + try { + const { amount } = await getCosmosChainGasPrice(chain, mpp); + return { + amount, + decimals: 1, + }; + } catch (error) { + console.error( + `Error getting gas price for cosmos chain ${chain}:`, + error, + ); + if (currentGasPrice) { + return currentGasPrice; + } else { + return { + amount: 'PLEASE SET A GAS PRICE FOR COSMOS CHAIN', + decimals: 1, + }; + } + } } case ProtocolType.Sealevel: // Return the gas price from the config if it exists, otherwise return some default diff --git a/typescript/infra/scripts/print-mailbox-owner.ts b/typescript/infra/scripts/print-mailbox-owner.ts index 88b04598d3d..a59f56f0a40 100644 --- a/typescript/infra/scripts/print-mailbox-owner.ts +++ b/typescript/infra/scripts/print-mailbox-owner.ts @@ -1,5 +1,3 @@ -import { ethers } from 'ethers'; - import { ChainAddresses } from '@hyperlane-xyz/registry'; import { ChainMap, @@ -8,15 +6,14 @@ import { HyperlaneCore, } from '@hyperlane-xyz/sdk'; import { - Address, - eqAddressEvm, objFilter, objMap, promiseObjAll, + rootLogger, } from '@hyperlane-xyz/utils'; import { Contexts } from '../config/contexts.js'; -import { DeployEnvironment } from '../src/config/environment.js'; +import { Owner, determineGovernanceType } from '../src/governance.js'; import { Role } from '../src/roles.js'; import { filterRemoteDomainMetadata, @@ -33,19 +30,6 @@ import { } from './agent-utils.js'; import { getEnvironmentConfig } from './core-utils.js'; -const DEPLOYERS: Record = { - mainnet3: '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba', - testnet4: '0xfaD1C94469700833717Fa8a3017278BC1cA8031C', - test: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', -}; - -export enum Owner { - ICA = 'ICA', - SAFE = 'SAFE', - DEPLOYER = 'DEPLOYER KEY', - UNKNOWN = 'UNKNOWN', -} - async function main() { const { context = Contexts.Hyperlane, @@ -80,47 +64,32 @@ async function main() { isEthereumProtocolChain(chain), ); - const deployer = DEPLOYERS[environment]; - const mailboxOwners = await promiseObjAll( objMap( evmContractsMap, async (chain: string, contracts: HyperlaneContracts) => { - // get possible owners from config - const ownerConfig = envConfig.owners[chain]; - const safeAddress = - ownerConfig.ownerOverrides?._safeAddress ?? - ethers.constants.AddressZero; - const icaAddress = - ownerConfig.ownerOverrides?._icaAddress ?? - ethers.constants.AddressZero; - // get actual onchain owner const ownerAddress = await contracts.mailbox.owner(); - - // determine owner type - let ownerType = Owner.UNKNOWN; - if (eqAddressEvm(ownerAddress, deployer)) { - ownerType = Owner.DEPLOYER; - } else if (eqAddressEvm(ownerAddress, safeAddress)) { - ownerType = Owner.SAFE; - } else if (eqAddressEvm(ownerAddress, icaAddress)) { - ownerType = Owner.ICA; - } - + const { ownerType, governanceType } = await determineGovernanceType( + chain, + ownerAddress, + ); return { owner: ownerAddress, type: ownerType, + governance: governanceType, }; }, ), ); + // eslint-disable-next-line no-console console.table(mailboxOwners); const totalChains = Object.keys(mailboxOwners).length; - console.log(`\nTotal chains: ${totalChains}`); + rootLogger.info(`\nTotal chains: ${totalChains}`); + // eslint-disable-next-line no-console console.table( Object.values(Owner).map((ownerType) => ({ 'Owner Type': ownerType, @@ -134,6 +103,6 @@ async function main() { main() .then() .catch((e) => { - console.error(e); + rootLogger.error(e); process.exit(1); }); diff --git a/typescript/infra/scripts/safes/create-safe.ts b/typescript/infra/scripts/safes/create-safe.ts index febf00cb570..f21ae84f86f 100644 --- a/typescript/infra/scripts/safes/create-safe.ts +++ b/typescript/infra/scripts/safes/create-safe.ts @@ -1,35 +1,42 @@ -import { EthersAdapter, SafeFactory } from '@safe-global/protocol-kit'; -import { SafeAccountConfig } from '@safe-global/protocol-kit'; +import { + EthersAdapter, + SafeAccountConfig, + SafeFactory, +} from '@safe-global/protocol-kit'; import { ethers } from 'ethers'; +import { rootLogger } from '@hyperlane-xyz/utils'; + import { Contexts } from '../../config/contexts.js'; -import { getChain } from '../../config/registry.js'; +import { getGovernanceSigners } from '../../config/environments/mainnet3/governance/utils.js'; +import { withGovernanceType } from '../../src/governance.js'; import { Role } from '../../src/roles.js'; -import { readJSONAtPath } from '../../src/utils/utils.js'; import { getArgs, - getKeyForRole, withChainRequired, withSafeHomeUrlRequired, withThreshold, } from '../agent-utils.js'; - -const OWNERS_FILE_PATH = 'config/environments/mainnet3/safe/safeSigners.json'; +import { getEnvironmentConfig } from '../core-utils.js'; async function main() { - const { chain, safeHomeUrl, threshold } = await withThreshold( - withSafeHomeUrlRequired(withChainRequired(getArgs())), - ).argv; - - const chainMetadata = await getChain(chain); - const rpcUrls = chainMetadata.rpcUrls; - const deployerPrivateKey = await getDeployerPrivateKey(); - - // Create ethers signer with deployer key - const provider = new ethers.providers.JsonRpcProvider(rpcUrls[0].http); + const { chain, safeHomeUrl, threshold, governanceType } = + await withGovernanceType( + withThreshold(withSafeHomeUrlRequired(withChainRequired(getArgs()))), + ).argv; + + const envConfig = getEnvironmentConfig('mainnet3'); + const multiProvider = await envConfig.getMultiProvider( + Contexts.Hyperlane, + Role.Deployer, + true, + [chain], + ); + + const signer = multiProvider.getSigner(chain); const ethAdapter = new EthersAdapter({ ethers, - signerOrProvider: new ethers.Wallet(deployerPrivateKey, provider), + signerOrProvider: signer, }); let safeFactory; @@ -38,31 +45,30 @@ async function main() { ethAdapter, }); } catch (e) { - console.error(`Error initializing SafeFactory: ${e}`); + rootLogger.error(`Error initializing SafeFactory: ${e}`); process.exit(1); } - const ownersConfig = readJSONAtPath(OWNERS_FILE_PATH); - const owners = ownersConfig.signers; - + const { signers, threshold: defaultThreshold } = + getGovernanceSigners(governanceType); const safeAccountConfig: SafeAccountConfig = { - owners, - threshold, + owners: signers, + threshold: threshold ?? defaultThreshold, }; let safe; try { safe = await safeFactory.deploySafe({ safeAccountConfig }); } catch (e) { - console.error(`Error deploying Safe: ${e}`); + rootLogger.error(`Error deploying Safe: ${e}`); process.exit(1); } const safeAddress = await safe.getAddress(); - console.log(`Safe address: ${safeAddress}`); - console.log(`Safe url: ${safeHomeUrl}/home?safe=${chain}:${safeAddress}`); - console.log('url may not be correct, please check by following the link'); + rootLogger.info(`Safe address: ${safeAddress}`); + rootLogger.info(`Safe url: ${safeHomeUrl}/home?safe=${chain}:${safeAddress}`); + rootLogger.info('url may not be correct, please check by following the link'); try { // TODO: check https://app.safe.global for officially supported chains, filter by chain id @@ -70,31 +76,24 @@ async function main() { 'https://', 'https://gateway.', )}/v1/chains`; - console.log(`Fetching chain data from ${chainsUrl}`); + rootLogger.info(`Fetching chain data from ${chainsUrl}`); const response = await fetch(chainsUrl); const resultsJson = await response.json(); const transactionService = resultsJson.results[0].transactionService; - console.log(`Chains: ${JSON.stringify(transactionService)}`); - console.log( + rootLogger.info(`Chains: ${JSON.stringify(transactionService)}`); + rootLogger.info( `Add the transaction service url ${transactionService} as gnosisSafeTransactionServiceUrl to the metadata.yml in the registry`, ); } catch (e) { - console.error(`Could not fetch safe tx service url: ${e}`); + rootLogger.error(`Could not fetch safe tx service url: ${e}`); } } -const getDeployerPrivateKey = async () => { - const key = getKeyForRole('mainnet3', Contexts.Hyperlane, Role.Deployer); - await key.fetch(); - - return key.privateKey; -}; - main() .then() .catch((e) => { - console.error(e); + rootLogger.error(e); process.exit(1); }); diff --git a/typescript/infra/scripts/safes/delete-pending-txs.ts b/typescript/infra/scripts/safes/delete-pending-txs.ts index 8fe9187a589..f452d76a47e 100644 --- a/typescript/infra/scripts/safes/delete-pending-txs.ts +++ b/typescript/infra/scripts/safes/delete-pending-txs.ts @@ -1,17 +1,22 @@ import yargs from 'yargs'; +import { rootLogger } from '@hyperlane-xyz/utils'; + import { Contexts } from '../../config/contexts.js'; -import { safes } from '../../config/environments/mainnet3/owners.js'; +import { getGovernanceSafes } from '../../config/environments/mainnet3/governance/utils.js'; +import { withGovernanceType } from '../../src/governance.js'; import { Role } from '../../src/roles.js'; import { deleteAllPendingSafeTxs } from '../../src/utils/safe.js'; import { withChains } from '../agent-utils.js'; import { getEnvironmentConfig } from '../core-utils.js'; async function main() { - const { chains } = await withChains(yargs(process.argv.slice(2))).argv; + const { chains, governanceType } = await withGovernanceType( + withChains(yargs(process.argv.slice(2))), + ).argv; if (!chains || chains.length === 0) { - console.error('No chains provided'); + rootLogger.error('No chains provided'); process.exit(1); } @@ -23,11 +28,16 @@ async function main() { chains, ); + const safes = getGovernanceSafes(governanceType); + for (const chain of chains) { try { await deleteAllPendingSafeTxs(chain, multiProvider, safes[chain]); } catch (error) { - console.error(`Error deleting pending transactions for ${chain}:`, error); + rootLogger.error( + `Error deleting pending transactions for ${chain}:`, + error, + ); } } } @@ -35,6 +45,6 @@ async function main() { main() .then() .catch((e) => { - console.error(e); + rootLogger.error(e); process.exit(1); }); diff --git a/typescript/infra/scripts/safes/delete-tx.ts b/typescript/infra/scripts/safes/delete-tx.ts index bca35efcbe3..0ecfe2f4483 100644 --- a/typescript/infra/scripts/safes/delete-tx.ts +++ b/typescript/infra/scripts/safes/delete-tx.ts @@ -1,28 +1,33 @@ import yargs from 'yargs'; +import { rootLogger } from '@hyperlane-xyz/utils'; + import { Contexts } from '../../config/contexts.js'; -import { safes } from '../../config/environments/mainnet3/owners.js'; +import { getGovernanceSafes } from '../../config/environments/mainnet3/governance/utils.js'; +import { withGovernanceType } from '../../src/governance.js'; import { Role } from '../../src/roles.js'; import { deleteSafeTx } from '../../src/utils/safe.js'; import { withChains } from '../agent-utils.js'; import { getEnvironmentConfig } from '../core-utils.js'; async function main() { - const { chains, tx } = await withChains( - yargs(process.argv.slice(2)).option('tx', { - type: 'string', - description: 'Transaction hash to delete', - demandOption: true, - }), + const { chains, tx, governanceType } = await withGovernanceType( + withChains( + yargs(process.argv.slice(2)).option('tx', { + type: 'string', + description: 'Transaction hash to delete', + demandOption: true, + }), + ), ).argv; if (!chains || chains.length === 0) { - console.error('No chains provided'); + rootLogger.error('No chains provided'); process.exit(1); } if (!tx) { - console.error('No transaction hash provided'); + rootLogger.error('No transaction hash provided'); process.exit(1); } @@ -34,11 +39,12 @@ async function main() { chains, ); + const safes = getGovernanceSafes(governanceType); for (const chain of chains) { try { await deleteSafeTx(chain, multiProvider, safes[chain], tx); } catch (error) { - console.error(`Error deleting transaction ${tx} for ${chain}:`, error); + rootLogger.error(`Error deleting transaction ${tx} for ${chain}:`, error); } } } @@ -46,6 +52,6 @@ async function main() { main() .then() .catch((e) => { - console.error(e); + rootLogger.error(e); process.exit(1); }); diff --git a/typescript/infra/scripts/safes/get-pending-txs.ts b/typescript/infra/scripts/safes/get-pending-txs.ts index ead1b69c74c..89ce203eddb 100644 --- a/typescript/infra/scripts/safes/get-pending-txs.ts +++ b/typescript/infra/scripts/safes/get-pending-txs.ts @@ -10,7 +10,8 @@ import { } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts.js'; -import { safes } from '../../config/environments/mainnet3/owners.js'; +import { getGovernanceSafes } from '../../config/environments/mainnet3/governance/utils.js'; +import { withGovernanceType } from '../../src/governance.js'; import { Role } from '../../src/roles.js'; import { SafeTxStatus, @@ -21,11 +22,10 @@ import { withChains } from '../agent-utils.js'; import { getEnvironmentConfig } from '../core-utils.js'; async function main() { - const safeChains = Object.keys(safes); configureRootLogger(LogFormat.Pretty, LogLevel.Info); - const { chains, fullTxHash } = await withChains( - yargs(process.argv.slice(2)), - safeChains, + + const { chains, fullTxHash, governanceType } = await withGovernanceType( + withChains(yargs(process.argv.slice(2))), ) .describe( 'fullTxHash', @@ -34,6 +34,9 @@ async function main() { .boolean('fullTxHash') .default('fullTxHash', false).argv; + const safes = getGovernanceSafes(governanceType); + const safeChains = Object.keys(safes); + const chainsToCheck = chains || safeChains; if (chainsToCheck.length === 0) { rootLogger.error('No chains provided'); diff --git a/typescript/infra/scripts/safes/governance/check-safe-signers.ts b/typescript/infra/scripts/safes/governance/check-safe-signers.ts new file mode 100644 index 00000000000..d0fdb2630cb --- /dev/null +++ b/typescript/infra/scripts/safes/governance/check-safe-signers.ts @@ -0,0 +1,149 @@ +import Safe from '@safe-global/protocol-kit'; +import yargs from 'yargs'; + +import { rootLogger } from '@hyperlane-xyz/utils'; + +import { Contexts } from '../../../config/contexts.js'; +import { + getGovernanceSafes, + getGovernanceSigners, +} from '../../../config/environments/mainnet3/governance/utils.js'; +import { GovernanceType, withGovernanceType } from '../../../src/governance.js'; +import { Role } from '../../../src/roles.js'; +import { getOwnerChanges, getSafeAndService } from '../../../src/utils/safe.js'; +import { getEnvironmentConfig } from '../../core-utils.js'; + +enum SafeConfigViolationType { + missingOwners = 'missingOwners', + unexpectedOwners = 'unexpectedOwners', + thresholdMismatch = 'thresholdMismatch', +} + +interface SafeConfigViolation { + type: SafeConfigViolationType; + chain: string; + safeAddress: string; + owners?: string[]; + expected?: string; + actual?: string; +} + +async function main() { + const { governanceType = GovernanceType.Regular } = await withGovernanceType( + yargs(process.argv.slice(2)), + ).argv; + + const violations: SafeConfigViolation[] = []; + + const { signers, threshold } = getGovernanceSigners(governanceType); + const safes = getGovernanceSafes(governanceType); + + const multiProvider = await getEnvironmentConfig('mainnet3').getMultiProvider( + Contexts.Hyperlane, + Role.Deployer, + true, + Object.keys(safes), + ); + + for (const [chain, safeAddress] of Object.entries(safes)) { + let safeSdk: Safe.default; + try { + ({ safeSdk } = await getSafeAndService( + chain, + multiProvider, + safeAddress, + )); + } catch (error) { + rootLogger.error(`[${chain}] could not get safe: ${error}`); + continue; + } + + const currentOwners = await safeSdk.getOwners(); + const currentThreshold = await safeSdk.getThreshold(); + const expectedOwners = signers; + const { ownersToRemove, ownersToAdd } = await getOwnerChanges( + currentOwners, + expectedOwners, + ); + + if (ownersToRemove.length > 0) { + violations.push({ + type: SafeConfigViolationType.unexpectedOwners, + chain, + safeAddress, + owners: ownersToRemove, + }); + } + + if (ownersToAdd.length > 0) { + violations.push({ + type: SafeConfigViolationType.missingOwners, + chain, + safeAddress, + owners: ownersToAdd, + }); + } + + if (threshold !== currentThreshold) { + violations.push({ + type: SafeConfigViolationType.thresholdMismatch, + chain, + safeAddress, + expected: threshold.toString(), + actual: currentThreshold.toString(), + }); + } + } + + if (violations.length > 0) { + // Display threshold mismatches in a table + const thresholdViolations = violations.filter( + (v) => v.type === SafeConfigViolationType.thresholdMismatch, + ); + if (thresholdViolations.length > 0) { + console.table(thresholdViolations, [ + 'chain', + 'safeAddress', + 'expected', + 'actual', + ]); + } + + // Group other violations by chain + const violationsByChain = violations + .filter((v) => v.type !== SafeConfigViolationType.thresholdMismatch) + .reduce((acc, v) => { + if (!acc[v.chain]) acc[v.chain] = []; + acc[v.chain].push(v); + return acc; + }, {} as Record); + + // Display chain-specific violations as bulleted lists + for (const [chain, chainViolations] of Object.entries(violationsByChain)) { + rootLogger.info(`\nChain: ${chain}`); + + const missingSigs = chainViolations.find( + (v) => v.type === SafeConfigViolationType.missingOwners, + ); + if (missingSigs?.owners?.length) { + rootLogger.info('Missing signers:'); + missingSigs.owners.forEach((owner) => rootLogger.info(` • ${owner}`)); + } + + const extraSigs = chainViolations.find( + (v) => v.type === SafeConfigViolationType.unexpectedOwners, + ); + if (extraSigs?.owners?.length) { + rootLogger.info('Extraneous signers:'); + extraSigs.owners.forEach((owner) => rootLogger.info(` • ${owner}`)); + } + } + } else { + rootLogger.info('No violations found'); + } +} + +main().catch((error) => { + rootLogger.error(error); + process.exit(1); +}); diff --git a/typescript/infra/scripts/safes/governance/update-signers.ts b/typescript/infra/scripts/safes/governance/update-signers.ts new file mode 100644 index 00000000000..d51025d0af7 --- /dev/null +++ b/typescript/infra/scripts/safes/governance/update-signers.ts @@ -0,0 +1,99 @@ +import Safe from '@safe-global/protocol-kit'; +import yargs from 'yargs'; + +import { ChainName } from '@hyperlane-xyz/sdk'; +import { rootLogger } from '@hyperlane-xyz/utils'; + +import { Contexts } from '../../../config/contexts.js'; +import { + getGovernanceSafes, + getGovernanceSigners, +} from '../../../config/environments/mainnet3/governance/utils.js'; +import { AnnotatedCallData } from '../../../src/govern/HyperlaneAppGovernor.js'; +import { SafeMultiSend } from '../../../src/govern/multisend.js'; +import { GovernanceType, withGovernanceType } from '../../../src/governance.js'; +import { Role } from '../../../src/roles.js'; +import { getSafeAndService, updateSafeOwner } from '../../../src/utils/safe.js'; +import { withPropose } from '../../agent-utils.js'; +import { getEnvironmentConfig } from '../../core-utils.js'; + +async function main() { + const { propose, governanceType = GovernanceType.Regular } = + await withGovernanceType(withPropose(yargs(process.argv.slice(2)))).argv; + + const { signers, threshold } = getGovernanceSigners(governanceType); + const safes = getGovernanceSafes(governanceType); + + const envConfig = getEnvironmentConfig('mainnet3'); + const multiProvider = await envConfig.getMultiProvider( + Contexts.Hyperlane, + Role.Deployer, + true, + Object.keys(safes), + ); + + for (const [chain, safeAddress] of Object.entries(safes)) { + let safeSdk: Safe.default; + try { + ({ safeSdk } = await getSafeAndService( + chain, + multiProvider, + safeAddress, + )); + } catch (error) { + rootLogger.error(`[${chain}] could not get safe: ${error}`); + continue; + } + + let safeMultiSend: SafeMultiSend; + try { + safeMultiSend = new SafeMultiSend( + multiProvider, + chain as ChainName, + safeAddress, + ); + } catch (error) { + rootLogger.error(`[${chain}] could not get safe multi send: ${error}`); + continue; + } + + let transactions: AnnotatedCallData[]; + try { + transactions = await updateSafeOwner({ + safeSdk, + owners: signers, + threshold, + }); + } catch (error) { + rootLogger.error(`[${chain}] could not update safe owner: ${error}`); + continue; + } + + rootLogger.info(`[${chain}] Generated transactions for updating signers`); + rootLogger.info(`[${chain}] ${JSON.stringify(transactions, null, 2)}`); + + if (propose) { + try { + await safeMultiSend.sendTransactions( + transactions.map((call) => ({ + to: call.to, + data: call.data, + value: call.value, + })), + ); + rootLogger.info(`[${chain}] Successfully sent transactions`); + } catch (error) { + rootLogger.error(`[${chain}] could not send transactions: ${error}`); + } + } + } + + if (!propose) { + rootLogger.info('Skipping sending transactions, pass --propose to send'); + } +} + +main().catch((error) => { + rootLogger.error(error); + process.exit(1); +}); diff --git a/typescript/infra/scripts/safes/migrate-to-typed-signatures.ts b/typescript/infra/scripts/safes/migrate-to-typed-signatures.ts index d2f7dd5540d..06a455980dc 100644 --- a/typescript/infra/scripts/safes/migrate-to-typed-signatures.ts +++ b/typescript/infra/scripts/safes/migrate-to-typed-signatures.ts @@ -13,7 +13,7 @@ import { } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts.js'; -import { safes } from '../../config/environments/mainnet3/owners.js'; +import { awSafes as safes } from '../../config/environments/mainnet3/governance/safe/aw.js'; import { Role } from '../../src/roles.js'; import { createSafeTransaction, diff --git a/typescript/infra/scripts/safes/parse-txs.ts b/typescript/infra/scripts/safes/parse-txs.ts index 4483dc271ae..02c64c349eb 100644 --- a/typescript/infra/scripts/safes/parse-txs.ts +++ b/typescript/infra/scripts/safes/parse-txs.ts @@ -11,7 +11,11 @@ import { stringifyObject, } from '@hyperlane-xyz/utils'; -import { safes } from '../../config/environments/mainnet3/owners.js'; +import { + getGovernanceIcas, + getGovernanceSafes, +} from '../../config/environments/mainnet3/governance/utils.js'; +import { withGovernanceType } from '../../src/governance.js'; import { GovernTransactionReader } from '../../src/tx/govern-transaction-reader.js'; import { getPendingTxsForChains, getSafeTx } from '../../src/utils/safe.js'; import { writeYamlAtPath } from '../../src/utils/utils.js'; @@ -19,11 +23,11 @@ import { withChains } from '../agent-utils.js'; import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js'; const environment = 'mainnet3'; -const safeChains = Object.keys(safes); async function main() { - const { chains } = await withChains(yargs(process.argv.slice(2)), safeChains) - .argv; + const { chains, governanceType } = await withGovernanceType( + withChains(yargs(process.argv.slice(2))), + ).argv; configureRootLogger(LogFormat.Pretty, LogLevel.Info); const config = getEnvironmentConfig(environment); @@ -33,16 +37,24 @@ async function main() { const registry = await config.getRegistry(); const warpRoutes = await registry.getWarpRoutes(); + // Get the relevant set of governance safes and icas + const safes = getGovernanceSafes(governanceType); + const icas = getGovernanceIcas(governanceType); + + // Initialize the transaction reader with the relevant safes and icas const reader = new GovernTransactionReader( environment, multiProvider, chainAddresses, config.core, warpRoutes, + safes, + icas, ); + // Get the pending transactions for the relevant chains, for the chosen governance type const pendingTxs = await getPendingTxsForChains( - !chains || chains.length === 0 ? safeChains : chains, + !chains || chains.length === 0 ? Object.keys(safes) : chains, multiProvider, safes, ); @@ -90,11 +102,12 @@ async function main() { process.exit(1); } else { rootLogger.info('✅✅✅✅✅ No fatal errors ✅✅✅✅✅'); - const chainResults = Object.fromEntries(chainResultEntries); - const resultsPath = `safe-tx-results-${Date.now()}.yaml`; - writeYamlAtPath(resultsPath, chainResults); - rootLogger.info(`Results written to ${resultsPath}`); } + + const chainResults = Object.fromEntries(chainResultEntries); + const resultsPath = `safe-tx-results-${Date.now()}.yaml`; + writeYamlAtPath(resultsPath, chainResults); + rootLogger.info(`Results written to ${resultsPath}`); } main().catch((err) => { diff --git a/typescript/infra/scripts/send-test-messages.ts b/typescript/infra/scripts/send-test-messages.ts index 41d9501e3e5..592ccfbc591 100644 --- a/typescript/infra/scripts/send-test-messages.ts +++ b/typescript/infra/scripts/send-test-messages.ts @@ -1,5 +1,5 @@ import { Provider } from '@ethersproject/providers'; -import { BigNumber, Wallet } from 'ethers'; +import { Wallet } from 'ethers'; import fs from 'fs'; import yargs from 'yargs'; @@ -9,6 +9,7 @@ import { Mailbox, StorageGasOracle, StorageGasOracle__factory, + TestSendReceiver, TestSendReceiver__factory, } from '@hyperlane-xyz/core'; import { @@ -127,6 +128,11 @@ function getArgs() { default: false, describe: 'Mine forever after sending messages', }) + .option('body', { + type: 'string', + default: '0x1234', + describe: 'send custom body', + }) .option('singleOrigin', { type: 'string', default: undefined, @@ -161,8 +167,14 @@ function getArgs() { async function main() { const args = await getArgs(); - const { timeout, defaultHook, requiredHook, mineforever, singleOrigin } = - args; + const { + timeout, + defaultHook, + requiredHook, + mineforever, + singleOrigin, + body, + } = args; let messages = args.messages; // Limit the test chains to a subset of the known chains @@ -236,24 +248,15 @@ async function main() { await setIgpConfig(remoteId, signer, provider, mailbox, addresses, local); } - const message = formatMessage( - 1, - 0, - multiProvider.getDomainId(local), - recipient.address, - multiProvider.getDomainId(remote), - recipient.address, - '0x1234', - ); const quote = await mailbox['quoteDispatch(uint32,bytes32,bytes)']( remoteId, addressToBytes32(recipient.address), - message, + body, ); await mailbox['dispatch(uint32,bytes32,bytes)']( remoteId, addressToBytes32(recipient.address), - message, + body, { value: quote, }, diff --git a/typescript/infra/scripts/warp-routes/export-warp-configs.ts b/typescript/infra/scripts/warp-routes/export-warp-configs.ts index 9fee0505774..139f5a1971e 100644 --- a/typescript/infra/scripts/warp-routes/export-warp-configs.ts +++ b/typescript/infra/scripts/warp-routes/export-warp-configs.ts @@ -1,3 +1,6 @@ +import { WarpRouteDeployConfig } from '@hyperlane-xyz/sdk'; +import { objMap } from '@hyperlane-xyz/utils'; + import { getRegistry } from '../../config/registry.js'; import { getWarpConfig, warpConfigGetterMap } from '../../config/warp.js'; import { getArgs, withOutputFile } from '../agent-utils.js'; @@ -20,8 +23,16 @@ async function main() { warpRouteId, ); + const registryConfig: WarpRouteDeployConfig = objMap( + warpConfig, + (_, config) => { + const { mailbox: _mailbox, ...rest } = config; + return rest; + }, + ); + const configFileName = `${warpRouteId}-deploy.yaml`; - registry.addWarpRouteConfig(warpConfig, configFileName); + registry.addWarpRouteConfig(registryConfig, configFileName); // TODO: Use registry.getWarpRoutesPath() to dynamically generate path by removing "protected" console.log( diff --git a/typescript/infra/src/agents/index.ts b/typescript/infra/src/agents/index.ts index cd1b1b206d8..fc0510cd066 100644 --- a/typescript/infra/src/agents/index.ts +++ b/typescript/infra/src/agents/index.ts @@ -22,6 +22,7 @@ import { import { RelayerConfigHelper, RelayerConfigMapConfig, + RelayerDbBootstrapConfig, RelayerEnvConfig, } from '../config/agent/relayer.js'; import { ScraperConfigHelper } from '../config/agent/scraper.js'; @@ -29,9 +30,12 @@ import { ValidatorConfigHelper } from '../config/agent/validator.js'; import { DeployEnvironment } from '../config/environment.js'; import { AgentRole, Role } from '../roles.js'; import { + createServiceAccountIfNotExists, + createServiceAccountKey, fetchGCPSecret, gcpSecretExistsUsingClient, getGcpSecretLatestVersionName, + grantServiceAccountStorageRoleIfNotExists, setGCPSecretUsingClient, } from '../utils/gcloud.js'; import { HelmManager } from '../utils/helm.js'; @@ -48,6 +52,12 @@ const HELM_CHART_PATH = join( '/../../rust/main/helm/hyperlane-agent/', ); +export interface BatchConfig { + maxBatchSize: number; + bypassBatchSimulation: boolean; + maxSubmitQueueLength?: number; +} + export abstract class AgentHelmManager extends HelmManager { abstract readonly role: AgentRole; readonly helmChartPath: string = HELM_CHART_PATH; @@ -106,12 +116,18 @@ export abstract class AgentHelmManager extends HelmManager ); } + const batchConfig = this.batchConfig(chain); + return { name: chain, rpcConsensusType: this.rpcConsensusType(chain), protocol: metadata.protocol, blocks: { reorgPeriod }, - maxBatchSize: 32, + maxBatchSize: batchConfig.maxBatchSize, + bypassBatchSimulation: batchConfig.bypassBatchSimulation, + ...(batchConfig.maxSubmitQueueLength + ? { maxSubmitQueueLength: batchConfig.maxSubmitQueueLength } + : {}), priorityFeeOracle, transactionSubmitter, }; @@ -136,6 +152,13 @@ export abstract class AgentHelmManager extends HelmManager kubernetesResources(): KubernetesResources | undefined { return this.config.agentRoleConfig.resources; } + + batchConfig(_: ChainName): BatchConfig { + return { + maxBatchSize: 32, + bypassBatchSimulation: false, + }; + } } abstract class OmniscientAgentHelmManager extends AgentHelmManager { @@ -185,6 +208,7 @@ export class RelayerHelmManager extends OmniscientAgentHelmManager { addressBlacklist: config.addressBlacklist, metricAppContexts: config.metricAppContexts, gasPaymentEnforcement: config.gasPaymentEnforcement, + ismCacheConfigs: config.ismCacheConfigs, }; const envConfig = objOmitKeys( config, @@ -197,6 +221,15 @@ export class RelayerHelmManager extends OmniscientAgentHelmManager { envConfig, configMapConfig, resources: this.kubernetesResources(), + dbBootstrap: await this.dbBootstrapConfig( + this.config.relayerConfig.dbBootstrap, + ), + mixing: this.config.relayerConfig.mixing ?? { enabled: false }, + // Enable by default in our infra + environmentVariableEndpointEnabled: + this.config.relayerConfig.environmentVariableEndpointEnabled ?? true, + cacheDefaultExpirationSeconds: + this.config.relayerConfig.cache?.defaultExpirationSeconds, }; const signers = await this.config.signers(); @@ -218,8 +251,81 @@ export class RelayerHelmManager extends OmniscientAgentHelmManager { effect: 'NoSchedule', }); + if (this.context.includes('vanguard')) { + values.tolerations.push({ + key: 'context-family', + operator: 'Equal', + value: 'vanguard', + effect: 'NoSchedule', + }); + } + return values; } + + batchConfig(chain: ChainName): BatchConfig { + const defaultBatchConfig = super.batchConfig(chain); + + let maxBatchSize = + this.config.relayerConfig.batch?.defaultBatchSize ?? + defaultBatchConfig.maxBatchSize; + const chainBatchSizeOverride = + this.config.relayerConfig.batch?.batchSizeOverrides?.[chain]; + if (chainBatchSizeOverride) { + maxBatchSize = chainBatchSizeOverride; + } + + return { + maxBatchSize, + bypassBatchSimulation: + this.config.relayerConfig.batch?.bypassBatchSimulation ?? + defaultBatchConfig.bypassBatchSimulation, + maxSubmitQueueLength: + this.config.relayerConfig.batch?.maxSubmitQueueLength?.[chain], + }; + } + + async dbBootstrapConfig( + enabled: boolean = false, + ): Promise { + if (!enabled) { + return undefined; + } + + await this.ensureDbBootstrapGcpServiceAccount('relayer-db-backups'); + + return { + enabled: true, + bucket: 'relayer-db-backups', + object_targz: `${this.environment}-latest.tar.gz`, + }; + } + + async ensureDbBootstrapGcpServiceAccount(bucket: string) { + const secretName = this.dbBootstrapServiceAccountKeySecretName(); + + if (await gcpSecretExistsUsingClient(secretName)) { + // The secret already exists, no need to create it again + return; + } + + const STORAGE_OBJECT_VIEWER_ROLE = 'roles/storage.objectViewer'; + + const serviceAccountEmail = await createServiceAccountIfNotExists( + `${this.environment}-db-bootstrap-reader`, + ); + await grantServiceAccountStorageRoleIfNotExists( + serviceAccountEmail, + bucket, + STORAGE_OBJECT_VIEWER_ROLE, + ); + const key = await createServiceAccountKey(serviceAccountEmail); + await setGCPSecretUsingClient(secretName, JSON.stringify(key)); + } + + dbBootstrapServiceAccountKeySecretName(): string { + return `${this.environment}-relayer-db-bootstrap-viewer-key`; + } } export class ScraperHelmManager extends OmniscientAgentHelmManager { diff --git a/typescript/infra/src/config/agent/relayer.ts b/typescript/infra/src/config/agent/relayer.ts index a633eb6ccca..961712321c6 100644 --- a/typescript/infra/src/config/agent/relayer.ts +++ b/typescript/infra/src/config/agent/relayer.ts @@ -9,6 +9,7 @@ import { HyperlaneAddresses, HyperlaneAddressesMap, HyperlaneFactories, + IsmCacheConfig, MatchingList, RelayerConfig as RelayerAgentConfig, } from '@hyperlane-xyz/sdk'; @@ -17,6 +18,7 @@ import { ProtocolType, addressToBytes32, isValidAddressEvm, + objMap, rootLogger, } from '@hyperlane-xyz/utils'; @@ -41,6 +43,23 @@ export interface MetricAppContext { matchingList: MatchingList; } +export interface RelayerMixingConfig { + enabled: boolean; + salt?: number; +} + +export interface RelayerCacheConfig { + enabled: boolean; + defaultExpirationSeconds?: number; +} + +export interface RelayerBatchConfig { + bypassBatchSimulation?: boolean; + defaultBatchSize?: number; + batchSizeOverrides?: ChainMap; + maxSubmitQueueLength?: ChainMap; +} + // Incomplete basic relayer agent config export interface BaseRelayerConfig { gasPaymentEnforcement: GasPaymentEnforcement[]; @@ -50,6 +69,14 @@ export interface BaseRelayerConfig { transactionGasLimit?: BigNumberish; skipTransactionGasLimitFor?: string[]; metricAppContextsGetter?: () => MetricAppContext[]; + ismCacheConfigs?: Array; + dbBootstrap?: boolean; + mixing?: RelayerMixingConfig; + environmentVariableEndpointEnabled?: boolean; + cache?: RelayerCacheConfig; + batch?: RelayerBatchConfig; + txIdIndexingEnabled?: boolean; + igpIndexingEnabled?: boolean; } // Full relayer-specific agent config for a single chain @@ -58,7 +85,10 @@ export type RelayerConfig = Omit; // and are intended to derisk hitting max env var length limits. export type RelayerConfigMapConfig = Pick< RelayerConfig, - 'addressBlacklist' | 'gasPaymentEnforcement' | 'metricAppContexts' + | 'addressBlacklist' + | 'gasPaymentEnforcement' + | 'metricAppContexts' + | 'ismCacheConfigs' >; // The rest of the config is intended to be set as env vars. export type RelayerEnvConfig = Omit< @@ -74,6 +104,20 @@ export interface HelmRelayerValues extends HelmStatefulSetValues { envConfig?: RelayerEnvConfig; // Config intended to be set as configMap values configMapConfig?: RelayerConfigMapConfig; + // Config for setting up the database + dbBootstrap?: RelayerDbBootstrapConfig; + // Config for setting up the mixing service + mixing?: RelayerMixingConfig; + // Config for the environment variable endpoint + environmentVariableEndpointEnabled?: boolean; + // Config for the cache + cacheDefaultExpirationSeconds?: number; +} + +export interface RelayerDbBootstrapConfig { + enabled: boolean; + bucket: string; + object_targz: string; } // See rust/main/helm/values.yaml for the full list of options and their defaults. @@ -84,7 +128,7 @@ export interface HelmRelayerChainValues { } export class RelayerConfigHelper extends AgentConfigHelper { - readonly #relayerConfig: BaseRelayerConfig; + readonly relayerConfig: BaseRelayerConfig; readonly logger: Logger; constructor(agentConfig: RootAgentConfig) { @@ -92,12 +136,12 @@ export class RelayerConfigHelper extends AgentConfigHelper { throw Error('Relayer is not defined for this context'); super(agentConfig, agentConfig.relayer); - this.#relayerConfig = agentConfig.relayer; + this.relayerConfig = agentConfig.relayer; this.logger = rootLogger.child({ module: 'RelayerConfigHelper' }); } async buildConfig(): Promise { - const baseConfig = this.#relayerConfig!; + const baseConfig = this.relayerConfig!; const relayerConfig: RelayerConfig = { relayChains: this.relayChains.join(','), @@ -128,6 +172,12 @@ export class RelayerConfigHelper extends AgentConfigHelper { baseConfig.metricAppContextsGetter(), ); } + if (baseConfig.ismCacheConfigs) { + relayerConfig.ismCacheConfigs = baseConfig.ismCacheConfigs; + } + relayerConfig.allowContractCallCaching = baseConfig.cache?.enabled ?? false; + relayerConfig.txIdIndexingEnabled = baseConfig.txIdIndexingEnabled ?? true; + relayerConfig.igpIndexingEnabled = baseConfig.igpIndexingEnabled ?? true; return relayerConfig; } @@ -213,7 +263,7 @@ export class RelayerConfigHelper extends AgentConfigHelper { get requiresAwsCredentials(): boolean { // If AWS is present on the agentConfig, we are using AWS keys and need credentials regardless. if (!this.aws) { - console.warn( + this.logger.warn( `Relayer does not have AWS credentials. Be sure this is a non-k8s-based environment!`, ); return false; @@ -275,6 +325,16 @@ export function consistentSenderRecipientMatchingList( ]; } +export function chainMapMatchingList( + chainMap: ChainMap
, +): MatchingList { + // Convert to a router matching list + const routers = objMap(chainMap, (chain, address) => ({ + router: address, + })); + return routerMatchingList(routers); +} + // Create a matching list for the given contract addresses export function matchingList( addressesMap: HyperlaneAddressesMap, diff --git a/typescript/infra/src/config/chain.ts b/typescript/infra/src/config/chain.ts index 65a4bda00d9..a2b2dc213ac 100644 --- a/typescript/infra/src/config/chain.ts +++ b/typescript/infra/src/config/chain.ts @@ -8,11 +8,15 @@ import { HyperlaneSmartProvider, ProviderRetryOptions, } from '@hyperlane-xyz/sdk'; -import { ProtocolType, objFilter, objMerge } from '@hyperlane-xyz/utils'; +import { + ProtocolType, + inCIMode, + objFilter, + objMerge, +} from '@hyperlane-xyz/utils'; import { getChain, getRegistryWithOverrides } from '../../config/registry.js'; import { getSecretRpcEndpoints } from '../agents/index.js'; -import { inCIMode } from '../utils/utils.js'; import { DeployEnvironment } from './environment.js'; @@ -20,6 +24,9 @@ import { DeployEnvironment } from './environment.js'; // Used by scripts like check-owner-ica.ts to exclude chains that are temporarily // unsupported (e.g. zksync, zeronetwork) or have known issues (e.g. lumia). export const chainsToSkip: ChainName[] = [ + // TODO: complete work when RPC is available again + 'infinityvm', + // TODO: remove once zksync PR is merged into main // mainnets 'zksync', diff --git a/typescript/infra/src/config/environment.ts b/typescript/infra/src/config/environment.ts index 90761e1adc2..4166cdb5eaa 100644 --- a/typescript/infra/src/config/environment.ts +++ b/typescript/infra/src/config/environment.ts @@ -14,6 +14,12 @@ import { mustGet, objKeys, objMap, objMerge } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts.js'; import { environments } from '../../config/environments/index.js'; +import { awIcas } from '../../config/environments/mainnet3/governance/ica/aw.js'; +import { awSafes } from '../../config/environments/mainnet3/governance/safe/aw.js'; +import { + DEPLOYER, + upgradeTimelocks, +} from '../../config/environments/mainnet3/owners.js'; import { getHyperlaneCore } from '../../scripts/core-utils.js'; import { CloudAgentKey } from '../agents/keys.js'; import { Role } from '../roles.js'; @@ -89,7 +95,28 @@ export async function getRouterConfigsForAllVms( envConfig.environment, multiProvider, ); - const evmRouterConfig = core.getRouterConfig(envConfig.owners); + + // Core deployment governance is changing. + // For now stick with the previous ownership setup. + const ownerConfigs: ChainMap = objMap( + envConfig.owners, + (chain, _) => { + const owner = awIcas[chain] ?? awSafes[chain] ?? DEPLOYER; + return { + owner, + ownerOverrides: { + proxyAdmin: upgradeTimelocks[chain] ?? owner, + validatorAnnounce: DEPLOYER, + testRecipient: DEPLOYER, + fallbackRoutingHook: DEPLOYER, + ...(awSafes[chain] && { _safeAddress: awSafes[chain] }), + ...(awIcas[chain] && { _icaAddress: awIcas[chain] }), + }, + }; + }, + ); + + const evmRouterConfig = core.getRouterConfig(ownerConfigs); const allRouterConfigs: ChainMap = objMap( chainAddresses, diff --git a/typescript/infra/src/config/funding.ts b/typescript/infra/src/config/funding.ts index 279a6842ad7..d656ac724f9 100644 --- a/typescript/infra/src/config/funding.ts +++ b/typescript/infra/src/config/funding.ts @@ -27,6 +27,7 @@ export interface KeyFunderConfig desiredBalancePerChain: Record; desiredKathyBalancePerChain: ChainMap; igpClaimThresholdPerChain: ChainMap; + chainsToSkip: ChainName[]; } export interface CheckWarpDeployConfig extends CronJobConfig {} diff --git a/typescript/infra/src/config/gas-oracle.ts b/typescript/infra/src/config/gas-oracle.ts index 279ccaa482e..ddebc4ce768 100644 --- a/typescript/infra/src/config/gas-oracle.ts +++ b/typescript/infra/src/config/gas-oracle.ts @@ -167,6 +167,8 @@ function getMinUsdCost(local: ChainName, remote: ChainName): number { } const remoteMinCostOverrides: ChainMap = { + ethereum: 0.5, + // For Ethereum L2s, we need to account for the L1 DA costs that // aren't accounted for directly in the gas price. arbitrum: 0.5, @@ -179,11 +181,11 @@ function getMinUsdCost(local: ChainName, remote: ChainName): number { polygonzkevm: 0.5, // op stack chains - base: 0.2, + base: 0.5, fraxtal: 0.2, lisk: 0.2, mode: 0.2, - optimism: 0.2, + optimism: 0.5, soneium: 0.2, superseed: 0.2, unichain: 0.2, @@ -195,6 +197,7 @@ function getMinUsdCost(local: ChainName, remote: ChainName): number { neutron: 0.5, // For Solana, special min cost solanamainnet: 1.2, + bsc: 0.5, }; const override = remoteMinCostOverrides[remote]; if (override !== undefined) { diff --git a/typescript/infra/src/funding/key-funder.ts b/typescript/infra/src/funding/key-funder.ts index ad63128ecf7..4281edf4318 100644 --- a/typescript/infra/src/funding/key-funder.ts +++ b/typescript/infra/src/funding/key-funder.ts @@ -46,6 +46,7 @@ export class KeyFunderHelmManager extends HelmManager { desiredBalancePerChain: this.config.desiredBalancePerChain, desiredKathyBalancePerChain: this.config.desiredKathyBalancePerChain, igpClaimThresholdPerChain: this.config.igpClaimThresholdPerChain, + chainsToSkip: this.config.chainsToSkip, }, image: { repository: this.config.docker.repo, diff --git a/typescript/infra/src/govern/HyperlaneAppGovernor.ts b/typescript/infra/src/govern/HyperlaneAppGovernor.ts index ecbd9c9ddc4..a9f90cf5ec7 100644 --- a/typescript/infra/src/govern/HyperlaneAppGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneAppGovernor.ts @@ -2,8 +2,7 @@ import chalk from 'chalk'; import { BigNumber } from 'ethers'; import prompts from 'prompts'; -import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; -import { Ownable__factory } from '@hyperlane-xyz/core'; +import { Ownable__factory, ProxyAdmin__factory } from '@hyperlane-xyz/core'; import { ChainMap, ChainName, @@ -14,9 +13,8 @@ import { OwnableConfig, OwnerViolation, ProxyAdminViolation, + canProposeSafeTransactions, } from '@hyperlane-xyz/sdk'; -// @ts-ignore -import { canProposeSafeTransactions } from '@hyperlane-xyz/sdk'; import { Address, CallData, @@ -24,9 +22,11 @@ import { eqAddress, objMap, retryAsync, + rootLogger, } from '@hyperlane-xyz/utils'; -import { getSafeAndService, updateSafeOwner } from '../utils/safe.js'; +import { getGovernanceSafes } from '../../config/environments/mainnet3/governance/utils.js'; +import { GovernanceType, determineGovernanceType } from '../governance.js'; import { ManualMultiSend, @@ -46,6 +46,7 @@ export type AnnotatedCallData = CallData & { description: string; expandedDescription?: string; icaTargetChain?: ChainName; + governanceType?: GovernanceType; }; export type InferredCall = { @@ -106,9 +107,17 @@ export abstract class HyperlaneAppGovernor< protected async sendCalls(chain: ChainName, requestConfirmation: boolean) { const calls = this.calls[chain] || []; - console.log(`\nFound ${calls.length} transactions for ${chain}`); - const filterCalls = (submissionType: SubmissionType) => - calls.filter((call) => call.submissionType == submissionType); + rootLogger.info(`\nFound ${calls.length} transactions for ${chain}`); + const filterCalls = ( + submissionType: SubmissionType, + governanceType?: GovernanceType, + ) => + calls.filter( + (call) => + call.submissionType == submissionType && + (governanceType === undefined || + call.governanceType == governanceType), + ); const summarizeCalls = async ( submissionType: SubmissionType, callsForSubmissionType: AnnotatedCallData[], @@ -117,17 +126,17 @@ export abstract class HyperlaneAppGovernor< return false; } - console.log( + rootLogger.info( `${SubmissionType[submissionType]} calls: ${callsForSubmissionType.length}`, ); callsForSubmissionType.map( ({ icaTargetChain, description, expandedDescription, ...call }) => { // Print a blank line to separate calls - console.log(''); + rootLogger.info(''); // Print the ICA call header if it exists if (icaTargetChain) { - console.log( + rootLogger.info( chalk.bold( `> INTERCHAIN ACCOUNT CALL: ${chain} -> ${icaTargetChain}`, ), @@ -135,14 +144,14 @@ export abstract class HyperlaneAppGovernor< } // Print the call details - console.log(chalk.bold(`> ${description.trimEnd()}`)); + rootLogger.info(chalk.bold(`> ${description.trimEnd()}`)); if (expandedDescription) { - console.info(chalk.gray(`${expandedDescription.trimEnd()}`)); + rootLogger.info(chalk.gray(`${expandedDescription.trimEnd()}`)); } - console.info(chalk.gray(`to: ${call.to}`)); - console.info(chalk.gray(`data: ${call.data}`)); - console.info(chalk.gray(`value: ${call.value}`)); + rootLogger.info(chalk.gray(`to: ${call.to}`)); + rootLogger.info(chalk.gray(`data: ${call.data}`)); + rootLogger.info(chalk.gray(`value: ${call.value}`)); }, ); if (!requestConfirmation) return true; @@ -160,25 +169,10 @@ export abstract class HyperlaneAppGovernor< const sendCallsForType = async ( submissionType: SubmissionType, multiSend: MultiSend, + governanceType?: GovernanceType, ) => { const callsForSubmissionType = []; - const filteredCalls = filterCalls(submissionType); - - // If calls are being submitted via a safe, we need to check for any safe owner changes first - if (submissionType === SubmissionType.SAFE) { - try { - const { safeSdk } = await getSafeAndService( - chain, - this.checker.multiProvider, - (multiSend as SafeMultiSend).safeAddress, - ); - const updateOwnerCalls = await updateSafeOwner(safeSdk); - callsForSubmissionType.push(...updateOwnerCalls); - } catch (error) { - // Catch but don't throw because we want to try submitting any remaining calls - console.error(`Error updating safe owner: ${error}`); - } - } + const filteredCalls = filterCalls(submissionType, governanceType); // Add the filtered calls to the calls for submission type callsForSubmissionType.push(...filteredCalls); @@ -190,7 +184,7 @@ export abstract class HyperlaneAppGovernor< callsForSubmissionType, ); if (confirmed) { - console.info( + rootLogger.info( chalk.italic( `Submitting calls on ${chain} via ${SubmissionType[submissionType]}`, ), @@ -204,12 +198,12 @@ export abstract class HyperlaneAppGovernor< })), ); } catch (error) { - console.error( + rootLogger.error( chalk.red(`Error submitting calls on ${chain}: ${error}`), ); } } else { - console.info( + rootLogger.info( chalk.italic( `Skipping submission of calls on ${chain} via ${SubmissionType[submissionType]}`, ), @@ -218,31 +212,36 @@ export abstract class HyperlaneAppGovernor< } }; + // Do all SIGNER calls first await sendCallsForType( SubmissionType.SIGNER, new SignerMultiSend(this.checker.multiProvider, chain), ); - const safeOwner = - this.checker.configMap[chain].ownerOverrides?._safeAddress; - if (safeOwner) { - await retryAsync( - () => - sendCallsForType( - SubmissionType.SAFE, - new SafeMultiSend(this.checker.multiProvider, chain, safeOwner), - ), - 10, - ); + // Then propose transactions on safes for all governance types + for (const governanceType of Object.values(GovernanceType)) { + const safeOwner = getGovernanceSafes(governanceType)[chain]; + if (safeOwner) { + await retryAsync( + () => + sendCallsForType( + SubmissionType.SAFE, + new SafeMultiSend(this.checker.multiProvider, chain, safeOwner), + governanceType, + ), + 10, + ); + } } + // Then finally submit remaining calls manually await sendCallsForType(SubmissionType.MANUAL, new ManualMultiSend(chain)); this.printSeparator(); } private printSeparator() { - console.log( + rootLogger.info( `-------------------------------------------------------------------------------------------------------------------`, ); } @@ -299,7 +298,7 @@ export abstract class HyperlaneAppGovernor< ), ); } catch (error) { - console.error( + rootLogger.error( chalk.red( `Error inferring call submission types for chain ${chain}: ${error}`, ), @@ -354,7 +353,7 @@ export abstract class HyperlaneAppGovernor< // If the account's owner is not the ICA router, default to manual submission if (!eqAddress(localOwner, this.interchainAccount.routerAddress(chain))) { - console.info( + rootLogger.info( chalk.gray( `Account's owner ${localOwner} is not ICA router. Defaulting to manual submission.`, ), @@ -374,7 +373,7 @@ export abstract class HyperlaneAppGovernor< const origin = this.interchainAccount.multiProvider.getChainName( accountConfig.origin, ); - console.info( + rootLogger.info( chalk.gray( `Inferred call for ICA remote owner ${bytes32ToAddress( accountConfig.owner, @@ -405,6 +404,11 @@ export abstract class HyperlaneAppGovernor< }; } + const { governanceType } = await determineGovernanceType( + origin, + accountConfig.owner, + ); + // If the call to the remote ICA is valid, infer the submission type const { description, expandedDescription } = call; const encodedCall: AnnotatedCallData = { @@ -413,6 +417,7 @@ export abstract class HyperlaneAppGovernor< value: callRemote.value, description, expandedDescription, + governanceType, }; // Try to infer the submission type for the ICA call @@ -512,42 +517,54 @@ export abstract class HyperlaneAppGovernor< try { await multiProvider.estimateGas(chain, call, submitterAddress); return true; - } catch (e) { + } catch (_) { return false; } }; // Check if the transaction will succeed with the SIGNER if (await checkTransactionSuccess(chain, signerAddress)) { - return { type: SubmissionType.SIGNER, chain, call }; + return { + type: SubmissionType.SIGNER, + chain, + call, + }; } // Check if the transaction will succeed with a SAFE - const safeAddress = - this.checker.configMap[chain].ownerOverrides?._safeAddress; - if (typeof safeAddress === 'string') { - // Check if the safe can propose transactions - const canProposeSafe = await this.checkSafeProposalEligibility( - chain, - signerAddress, - safeAddress, - ); - if ( - canProposeSafe && - (await checkTransactionSuccess(chain, safeAddress)) - ) { - // If the transaction will succeed with the safe, return the inferred call - return { type: SubmissionType.SAFE, chain, call }; + // Need to check all governance types because the safe address is different for each type + for (const governanceType of Object.values(GovernanceType)) { + const safeAddress = getGovernanceSafes(governanceType)[chain]; + if (typeof safeAddress === 'string') { + // Check if the safe can propose transactions + const canProposeSafe = await this.checkSafeProposalEligibility( + chain, + signerAddress, + safeAddress, + ); + if ( + canProposeSafe && + (await checkTransactionSuccess(chain, safeAddress)) + ) { + call.governanceType = governanceType; + // If the transaction will succeed with the safe, return the inferred call + return { type: SubmissionType.SAFE, chain, call }; + } } } // If we're not already an ICA call, try to infer an ICA call + // We'll also infer the governance type for the ICA call if (!isICACall) { return this.inferICAEncodedSubmissionType(chain, call); } // If the transaction will not succeed with SIGNER, SAFE or ICA, default to MANUAL submission - return { type: SubmissionType.MANUAL, chain, call }; + return { + type: SubmissionType.MANUAL, + chain, + call, + }; } private async checkSubmitterBalance( @@ -559,7 +576,7 @@ export abstract class HyperlaneAppGovernor< .getProvider(chain) .getBalance(submitterAddress); if (submitterBalance.lt(requiredValue)) { - console.warn( + rootLogger.warn( chalk.yellow( `Submitter ${submitterAddress} has an insufficient balance for the call and is likely to fail. Balance: ${submitterBalance}, Balance required: ${requiredValue}`, ), @@ -590,7 +607,7 @@ export abstract class HyperlaneAppGovernor< errorMessage.includes('invalid multisend contract address') || errorMessage.includes('invalid multisendcallonly contract address') ) { - console.warn(chalk.yellow(`Invalid contract: ${errorMessage}.`)); + rootLogger.warn(chalk.yellow(`Invalid contract: ${errorMessage}.`)); return false; } @@ -599,7 +616,7 @@ export abstract class HyperlaneAppGovernor< errorMessage.includes('service unavailable') || errorMessage.includes('too many requests') ) { - console.warn( + rootLogger.warn( chalk.yellow( `Safe service error for ${safeAddress} on ${chain}: ${errorMessage}. ${retries} retries left.`, ), @@ -618,7 +635,7 @@ export abstract class HyperlaneAppGovernor< } // Handle all other errors - console.error( + rootLogger.error( chalk.red( `Failed to determine if signer can propose safe transactions on ${chain}. Error: ${error}`, ), diff --git a/typescript/infra/src/govern/HyperlaneCoreGovernor.ts b/typescript/infra/src/govern/HyperlaneCoreGovernor.ts index f4b9c745591..cd0667b52e3 100644 --- a/typescript/infra/src/govern/HyperlaneCoreGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneCoreGovernor.ts @@ -15,6 +15,7 @@ import { ProxyAdminViolation, ViolationType, } from '@hyperlane-xyz/sdk'; +import { rootLogger } from '@hyperlane-xyz/utils'; import { HyperlaneAppGovernor } from '../govern/HyperlaneAppGovernor.js'; @@ -77,7 +78,7 @@ export class HyperlaneCoreGovernor extends HyperlaneAppGovernor< return this.handleMailboxViolation(violation as MailboxViolation); } case CoreViolationType.ValidatorAnnounce: { - console.warn(chalk.yellow('Ignoring ValidatorAnnounce violation')); + rootLogger.warn(chalk.yellow('Ignoring ValidatorAnnounce violation')); return undefined; } case ViolationType.ProxyAdmin: { diff --git a/typescript/infra/src/governance.ts b/typescript/infra/src/governance.ts new file mode 100644 index 00000000000..ce95e00d7bb --- /dev/null +++ b/typescript/infra/src/governance.ts @@ -0,0 +1,72 @@ +import { Argv } from 'yargs'; + +import { ChainName } from '@hyperlane-xyz/sdk'; +import { Address, eqAddressEvm } from '@hyperlane-xyz/utils'; + +import { + getGovernanceIcas, + getGovernanceSafes, +} from '../config/environments/mainnet3/governance/utils.js'; + +import { DeployEnvironment } from './config/environment.js'; + +export enum GovernanceType { + AbacusWorks = 'abacusWorks', + Regular = 'regular', + Irregular = 'irregular', +} + +export enum Owner { + ICA = 'ICA', + SAFE = 'SAFE', + DEPLOYER = 'DEPLOYER KEY', + UNKNOWN = 'UNKNOWN', +} + +export const DEPLOYERS: Record = { + mainnet3: '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba', + testnet4: '0xfaD1C94469700833717Fa8a3017278BC1cA8031C', + test: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', +}; + +export function withGovernanceType(args: Argv) { + return args.option('governanceType', { + type: 'string', + description: 'Type of governance to use', + choices: Object.values(GovernanceType), + default: GovernanceType.Regular, + }); +} + +export async function determineGovernanceType( + chain: ChainName, + address: Address, +): Promise<{ + ownerType: Owner | null; + governanceType: GovernanceType; +}> { + if ( + Object.values(DEPLOYERS).some((deployer) => eqAddressEvm(deployer, address)) + ) { + return { + ownerType: Owner.DEPLOYER, + governanceType: GovernanceType.AbacusWorks, + }; + } + + for (const governanceType of Object.values(GovernanceType)) { + const icas = getGovernanceIcas(governanceType); + if (icas[chain] && icas[chain].includes(address)) { + return { ownerType: Owner.ICA, governanceType }; + } + const safes = getGovernanceSafes(governanceType); + if (safes[chain] && safes[chain].includes(address)) { + return { ownerType: Owner.SAFE, governanceType }; + } + } + + return { + ownerType: Owner.UNKNOWN, + governanceType: GovernanceType.AbacusWorks, + }; +} diff --git a/typescript/infra/src/infrastructure/external-secrets/external-secrets.ts b/typescript/infra/src/infrastructure/external-secrets/external-secrets.ts index 24569a6e9b7..7f691de5ef2 100644 --- a/typescript/infra/src/infrastructure/external-secrets/external-secrets.ts +++ b/typescript/infra/src/infrastructure/external-secrets/external-secrets.ts @@ -85,18 +85,42 @@ async function getGcpExternalSecretsConfig( }; } +async function isExternalSecretsReleaseInstalled( + infraConfig: InfrastructureConfig, +): Promise { + try { + // Gives a non-zero exit code if the release is not installed + await execCmd( + `helm status external-secrets --namespace ${infraConfig.externalSecrets.namespace}`, + ); + return true; + } catch (e) { + return false; + } +} + // Ensures the core `external-secrets` release (with all the CRDs etc) is up to date. async function ensureExternalSecretsRelease(infraConfig: InfrastructureConfig) { // Prometheus's helm chart requires a repository to be added await addHelmRepoIfRequired(infraConfig.externalSecrets.helmChart); - // The name passed in must be in the form `repo/chartName` - const chartName = getDeployableHelmChartName( - infraConfig.externalSecrets.helmChart, - ); - await execCmd( - `helm upgrade external-secrets ${chartName} --namespace ${infraConfig.externalSecrets.namespace} --create-namespace --version ${infraConfig.externalSecrets.helmChart.version} --install --set installCRDs=true `, - ); + // Only install the release if it doesn't already exist. We've observed + // some issues attempting an upgrade when the external-secrets release + // already exists. Doing so could result in the CRD being deleted and + // recreated, which would cause all existing external-secrets CRDs to be + // deleted! + if (!(await isExternalSecretsReleaseInstalled(infraConfig))) { + // The name passed in must be in the form `repo/chartName` + const chartName = getDeployableHelmChartName( + infraConfig.externalSecrets.helmChart, + ); + + await execCmd( + `helm upgrade external-secrets ${chartName} --namespace ${infraConfig.externalSecrets.namespace} --create-namespace --version ${infraConfig.externalSecrets.helmChart.version} --install --set installCRDs=true `, + ); + } else { + console.log('External-secrets release already installed.'); + } // Wait for the external-secrets-webhook deployment to have a ready replica. // The webhook deployment is required in order for subsequent deployments diff --git a/typescript/infra/src/infrastructure/monitoring/grafana.ts b/typescript/infra/src/infrastructure/monitoring/grafana.ts index 5966c81970c..bdd57301bf9 100644 --- a/typescript/infra/src/infrastructure/monitoring/grafana.ts +++ b/typescript/infra/src/infrastructure/monitoring/grafana.ts @@ -1,5 +1,5 @@ import { ChainMap } from '@hyperlane-xyz/sdk'; -import { rootLogger } from '@hyperlane-xyz/utils'; +import { inCIMode, rootLogger } from '@hyperlane-xyz/utils'; import { AlertType, @@ -64,6 +64,16 @@ export async function exportGrafanaAlert( export async function fetchGrafanaServiceAccountToken(): Promise { let saToken: string | undefined; + if (inCIMode()) { + saToken = process.env.GRAFANA_SERVICE_ACCOUNT_TOKEN; + if (!saToken) { + throw new Error( + 'GRAFANA_SERVICE_ACCOUNT_TOKEN is not set in CI environment', + ); + } + return saToken; + } + try { saToken = (await fetchGCPSecret( 'grafana-balance-alert-thresholds-token', diff --git a/typescript/infra/src/tx/govern-transaction-reader.ts b/typescript/infra/src/tx/govern-transaction-reader.ts index cb63eb97daf..89c04a2254c 100644 --- a/typescript/infra/src/tx/govern-transaction-reader.ts +++ b/typescript/infra/src/tx/govern-transaction-reader.ts @@ -4,6 +4,10 @@ import { MetaTransactionData, OperationType, } from '@safe-global/safe-core-sdk-types'; +import { + getMultiSendCallOnlyDeployments, + getMultiSendDeployments, +} from '@safe-global/safe-deployments'; import assert from 'assert'; import chalk from 'chalk'; import { BigNumber, ethers } from 'ethers'; @@ -29,6 +33,7 @@ import { normalizeConfig, } from '@hyperlane-xyz/sdk'; import { + Address, addressToBytes32, bytes32ToAddress, deepEquals, @@ -38,12 +43,9 @@ import { import { icaOwnerChain, - icas, - safes, timelocks, } from '../../config/environments/mainnet3/owners.js'; import { DeployEnvironment } from '../config/environment.js'; -import { getSafeAndService } from '../utils/safe.js'; interface GovernTransaction extends Record { chain: ChainName; @@ -96,12 +98,17 @@ export class GovernTransactionReader { Record > = {}; + readonly multiSendCallOnlyDeployments: Address[] = []; + readonly multiSendDeployments: Address[] = []; + constructor( readonly environment: DeployEnvironment, readonly multiProvider: MultiProvider, readonly chainAddresses: ChainMap>, readonly coreConfig: ChainMap, warpRoutes: Record, + readonly safes: ChainMap, + readonly icas: ChainMap, ) { // Populate maps with warp route addresses and additional token details for (const warpRoute of Object.values(warpRoutes)) { @@ -113,6 +120,28 @@ export class GovernTransactionReader { this.warpRouteIndex[token.chainName][address] = token; } } + + // Get deployments for each version + const versions = ['1.3.0', '1.4.1']; + for (const version of versions) { + const multiSendCallOnlyDeployments = getMultiSendCallOnlyDeployments({ + version, + }); + const multiSendDeployments = getMultiSendDeployments({ + version, + }); + assert( + multiSendCallOnlyDeployments && multiSendDeployments, + `MultiSend and MultiSendCallOnly deployments not found for version ${version}`, + ); + + Object.values(multiSendCallOnlyDeployments.deployments).forEach((d) => { + this.multiSendCallOnlyDeployments.push(d.address); + }); + Object.values(multiSendDeployments.deployments).forEach((d) => { + this.multiSendDeployments.push(d.address); + }); + } } async read( @@ -139,8 +168,8 @@ export class GovernTransactionReader { return this.readTimelockControllerTransaction(chain, tx); } - // If it's a Multisend - if (await this.isMultisendTransaction(chain, tx)) { + // If it's a Multisend or MultisendCallOnly transaction + if (await this.isMultisendTransaction(tx)) { return this.readMultisendTransaction(chain, tx); } @@ -640,12 +669,12 @@ export class GovernTransactionReader { this.chainAddresses, this.multiProvider, ).getAccount(remoteChainName, { - owner: safes[icaOwnerChain], + owner: this.safes[icaOwnerChain], origin: icaOwnerChain, routerOverride: router, ismOverride: ism, }); - const expectedRemoteIcaAddress = icas[remoteChainName as keyof typeof icas]; + const expectedRemoteIcaAddress = this.icas[remoteChainName]; let remoteIcaInsight = '✅ matches expected ICA'; if ( !expectedRemoteIcaAddress || @@ -794,21 +823,17 @@ export class GovernTransactionReader { ); } - async isMultisendTransaction( - chain: ChainName, - tx: AnnotatedEV5Transaction, - ): Promise { + async isMultisendTransaction(tx: AnnotatedEV5Transaction): Promise { if (tx.to === undefined) { return false; } - const multiSendCallOnlyAddress = await this.getMultiSendCallOnlyAddress( - chain, - ); - if (!multiSendCallOnlyAddress) { - return false; - } - return eqAddress(multiSendCallOnlyAddress, tx.to); + // Check if the transaction is to a MultiSend or MultiSendCallOnly deployment + return ( + this.multiSendCallOnlyDeployments.some((addr) => + eqAddress(addr, tx.to!), + ) || this.multiSendDeployments.some((addr) => eqAddress(addr, tx.to!)) + ); } async isOwnableTransaction( @@ -827,31 +852,6 @@ export class GovernTransactionReader { return false; } } - - private multiSendCallOnlyAddressCache: ChainMap = {}; - - async getMultiSendCallOnlyAddress( - chain: ChainName, - ): Promise { - if (this.multiSendCallOnlyAddressCache[chain]) { - return this.multiSendCallOnlyAddressCache[chain]; - } - - const safe = safes[chain]; - if (!safe) { - return undefined; - } - - const { safeSdk } = await getSafeAndService( - chain, - this.multiProvider, - safe, - ); - - this.multiSendCallOnlyAddressCache[chain] = - safeSdk.getMultiSendCallOnlyAddress(); - return this.multiSendCallOnlyAddressCache[chain]; - } } function metaTransactionDataToEV5Transaction( diff --git a/typescript/infra/src/utils/gcloud.ts b/typescript/infra/src/utils/gcloud.ts index 3fc157b4ce4..960a0f9e91e 100644 --- a/typescript/infra/src/utils/gcloud.ts +++ b/typescript/infra/src/utils/gcloud.ts @@ -273,6 +273,33 @@ export async function grantServiceAccountRoleIfNotExists( debugLog(`Granted role ${role} to service account ${serviceAccountEmail}`); } +export async function grantServiceAccountStorageRoleIfNotExists( + serviceAccountEmail: string, + bucketName: string, + role: string, +) { + const bucketUri = `gs://${bucketName}`; + const existingPolicies = await execCmdAndParseJson( + `gcloud storage buckets get-iam-policy ${bucketUri} --format="json"`, + ); + const existingBindings = existingPolicies.bindings || []; + const hasRole = existingBindings.some( + (binding: any) => + binding.role === role && + binding.members && + binding.members.includes(`serviceAccount:${serviceAccountEmail}`), + ); + if (hasRole) { + debugLog( + `Service account ${serviceAccountEmail} already has role ${role} on bucket ${bucketName}`, + ); + return; + } + await execCmd( + `gcloud storage buckets add-iam-policy-binding ${bucketUri} --member="serviceAccount:${serviceAccountEmail}" --role="${role}"`, + ); +} + export async function createServiceAccountKey(serviceAccountEmail: string) { const localKeyFile = '/tmp/tmp_key.json'; await execCmd( diff --git a/typescript/infra/src/utils/safe.ts b/typescript/infra/src/utils/safe.ts index 258fc48ca0c..f3fb37a35e1 100644 --- a/typescript/infra/src/utils/safe.ts +++ b/typescript/infra/src/utils/safe.ts @@ -23,7 +23,6 @@ import { rootLogger, } from '@hyperlane-xyz/utils'; -import safeSigners from '../../config/environments/mainnet3/safe/safeSigners.json' assert { type: 'json' }; // eslint-disable-next-line import/no-cycle import { AnnotatedCallData } from '../govern/HyperlaneAppGovernor.js'; @@ -308,17 +307,41 @@ export async function deleteSafeTx( } } -export async function updateSafeOwner( - safeSdk: Safe.default, -): Promise { - const threshold = await safeSdk.getThreshold(); - const owners = await safeSdk.getOwners(); - const newOwners = safeSigners.signers; - const ownersToRemove = owners.filter( - (owner) => !newOwners.some((newOwner) => eqAddress(owner, newOwner)), +export async function getOwnerChanges( + currentOwners: Address[], + expectedOwners: Address[], +): Promise<{ + ownersToRemove: Address[]; + ownersToAdd: Address[]; +}> { + const ownersToRemove = currentOwners.filter( + (owner) => !expectedOwners.some((newOwner) => eqAddress(owner, newOwner)), ); - const ownersToAdd = newOwners.filter( - (newOwner) => !owners.some((owner) => eqAddress(newOwner, owner)), + const ownersToAdd = expectedOwners.filter( + (newOwner) => !currentOwners.some((owner) => eqAddress(newOwner, owner)), + ); + + return { ownersToRemove, ownersToAdd }; +} + +export async function updateSafeOwner({ + safeSdk, + owners, + threshold, +}: { + safeSdk: Safe.default; + owners?: Address[]; + threshold?: number; +}): Promise { + const currentThreshold = await safeSdk.getThreshold(); + const newThreshold = threshold ?? currentThreshold; + + const currentOwners = await safeSdk.getOwners(); + const expectedOwners = owners ?? currentOwners; + + const { ownersToRemove, ownersToAdd } = await getOwnerChanges( + currentOwners, + expectedOwners, ); rootLogger.info(chalk.magentaBright('Owners to remove:', ownersToRemove)); @@ -329,7 +352,7 @@ export async function updateSafeOwner( for (const ownerToRemove of ownersToRemove) { const { data: removeTxData } = await safeSdk.createRemoveOwnerTx({ ownerAddress: ownerToRemove, - threshold, + threshold: newThreshold, }); transactions.push({ to: removeTxData.to, @@ -342,7 +365,7 @@ export async function updateSafeOwner( for (const ownerToAdd of ownersToAdd) { const { data: addTxData } = await safeSdk.createAddOwnerTx({ ownerAddress: ownerToAdd, - threshold, + threshold: newThreshold, }); transactions.push({ to: addTxData.to, @@ -352,6 +375,27 @@ export async function updateSafeOwner( }); } + if ( + ownersToRemove.length === 0 && + ownersToAdd.length === 0 && + currentThreshold !== newThreshold + ) { + rootLogger.info( + chalk.magentaBright( + `Threshold change ${currentThreshold} => ${newThreshold}`, + ), + ); + const { data: thresholdTxData } = await safeSdk.createChangeThresholdTx( + newThreshold, + ); + transactions.push({ + to: thresholdTxData.to, + data: thresholdTxData.data, + value: BigNumber.from(thresholdTxData.value), + description: `Change safe threshold to ${newThreshold}`, + }); + } + return transactions; } diff --git a/typescript/infra/src/utils/utils.ts b/typescript/infra/src/utils/utils.ts index db1830f4448..ee498581f41 100644 --- a/typescript/infra/src/utils/utils.ts +++ b/typescript/infra/src/utils/utils.ts @@ -290,10 +290,6 @@ export function getInfraPath() { return join(dirname(fileURLToPath(import.meta.url)), '../../'); } -export function inCIMode() { - return process.env.CI === 'true'; -} - // Filter out chains that are not supported by the multiProvider // Filter out any value that is not a string e.g. remote domain metadata export function filterRemoteDomainMetadata( diff --git a/typescript/infra/src/warp/helm.ts b/typescript/infra/src/warp/helm.ts index 89339b514a9..792aeaec3da 100644 --- a/typescript/infra/src/warp/helm.ts +++ b/typescript/infra/src/warp/helm.ts @@ -73,7 +73,7 @@ export class WarpRouteMonitorHelmManager extends HelmManager { return { image: { repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: 'ad6f664-20250401-202427', + tag: '8b985a4-20250421-152953', }, warpRouteId: this.warpRouteId, fullnameOverride: this.helmReleaseName, diff --git a/typescript/infra/test/balance-alerts.test.ts b/typescript/infra/test/balance-alerts.test.ts new file mode 100644 index 00000000000..9533116eb81 --- /dev/null +++ b/typescript/infra/test/balance-alerts.test.ts @@ -0,0 +1,81 @@ +import { expect } from 'chai'; + +import { retryAsync } from '@hyperlane-xyz/utils'; + +import { THRESHOLD_CONFIG_PATH } from '../src/config/funding/balances.js'; +import { + AlertType, + alertConfigMapping, +} from '../src/config/funding/grafanaAlerts.js'; +import { parseBalancesPromQLQuery } from '../src/funding/alerts.js'; +import { + fetchGrafanaAlert, + fetchGrafanaServiceAccountToken, +} from '../src/infrastructure/monitoring/grafana.js'; +import { readJSONAtPath } from '../src/utils/utils.js'; + +const DEFAULT_TIMEOUT = 30_000; + +describe('Balance Alert Thresholds', async function () { + this.timeout(DEFAULT_TIMEOUT); + + it('should have matching thresholds between Grafana alerts and threshold config files', async function () { + let saToken: string; + try { + saToken = await fetchGrafanaServiceAccountToken(); + } catch (error) { + console.log( + 'Error fetching grafana service account token, skipping test', + error, + ); + this.skip(); + } + const alertsToCheck = Object.values(AlertType); + const mismatches: string[] = []; + + for (const alert of alertsToCheck) { + // Fetch alert rule from Grafana + const alertRule = await retryAsync( + () => fetchGrafanaAlert(alert, saToken), + 3, // 3 attempts + ); + const existingQuery = alertRule.queries[0]; + + // Parse current thresholds from the query + const currentThresholds = parseBalancesPromQLQuery( + existingQuery, + alertConfigMapping[alert].walletName, + ); + + // Read proposed thresholds from config file + const proposedThresholds = readJSONAtPath( + `${THRESHOLD_CONFIG_PATH}/${alertConfigMapping[alert].configFileName}`, + ); + + // Compare thresholds + const allChains = new Set([ + ...Object.keys(currentThresholds), + ...Object.keys(proposedThresholds), + ]); + + for (const chain of allChains) { + const current = currentThresholds[chain]; + const proposed = proposedThresholds[chain]; + + if (current !== proposed) { + mismatches.push( + `${alert} - ${chain}: current=${current}, proposed=${proposed}`, + ); + } + } + } + + if (mismatches.length > 0) { + expect.fail( + 'Found mismatches between Grafana alerts and config files:\n' + + mismatches.join('\n') + + '\nThis is either due to your branch being out of date with the main branch or you have made changes to the threshold config files. Once your changes to the threshold config files have been reviewed, run the write-alerts script to update the grafana queries', + ); + } + }); +}); diff --git a/typescript/infra/test/warp-configs.test.ts b/typescript/infra/test/warp-configs.test.ts index c95baede4ef..bb66ac3aea4 100644 --- a/typescript/infra/test/warp-configs.test.ts +++ b/typescript/infra/test/warp-configs.test.ts @@ -3,8 +3,8 @@ import chaiAsPromised from 'chai-as-promised'; import { DEFAULT_GITHUB_REGISTRY } from '@hyperlane-xyz/registry'; import { getRegistry } from '@hyperlane-xyz/registry/fs'; -import { MultiProvider } from '@hyperlane-xyz/sdk'; -import { diffObjMerge, rootLogger } from '@hyperlane-xyz/utils'; +import { HypTokenRouterConfig, MultiProvider } from '@hyperlane-xyz/sdk'; +import { rootLogger } from '@hyperlane-xyz/utils'; import { getWarpConfig, warpConfigGetterMap } from '../config/warp.js'; import { @@ -15,7 +15,7 @@ import { const { expect } = chai; chai.use(chaiAsPromised); chai.should(); -const DEFAULT_TIMEOUT = 60000; +const DEFAULT_TIMEOUT = 100000; const warpIdsToSkip = [ 'EZETH/arbitrum-base-blast-bsc-ethereum-fraxtal-linea-mode-optimism-sei-swell-taiko-zircuit', @@ -47,24 +47,28 @@ describe('Warp Configs', async function () { for (const warpRouteId of warpIdsToCheck) { it(`should match Github Registry configs for ${warpRouteId}`, async function () { - const warpConfig = await getWarpConfig( - multiProvider, - envConfig, - warpRouteId, - ); - const { mergedObject, isInvalid } = diffObjMerge( - warpConfig, - configsFromGithub![warpRouteId], - ); - - if (isInvalid) { - console.log('Differences', JSON.stringify(mergedObject, null, 2)); + const warpConfig: Record< + string, + Partial + > = await getWarpConfig(multiProvider, envConfig, warpRouteId); + for (const key in warpConfig) { + if (warpConfig[key].mailbox) { + delete warpConfig[key].mailbox; + } + } + const expectedConfig = configsFromGithub![warpRouteId]; + for (const key in expectedConfig) { + if (expectedConfig[key].mailbox) { + delete expectedConfig[key].mailbox; + } } - expect( - isInvalid, - `Registry config does not match Getter for ${warpRouteId}`, - ).to.be.false; + expect(warpConfig).to.have.keys(Object.keys(expectedConfig)); + for (const key in warpConfig) { + if (warpConfig[key]) { + expect(warpConfig[key]).to.deep.equal(expectedConfig[key]); + } + } }); } diff --git a/yarn.lock b/yarn.lock index 9323b8d00e4..a774951dc1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7817,25 +7817,7 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/helloworld@npm:11.0.0": - version: 11.0.0 - resolution: "@hyperlane-xyz/helloworld@npm:11.0.0" - dependencies: - "@hyperlane-xyz/core": "npm:6.1.0" - "@hyperlane-xyz/registry": "npm:11.1.0" - "@hyperlane-xyz/sdk": "npm:11.0.0" - "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" - ethers: "npm:^5.7.2" - peerDependencies: - "@ethersproject/abi": "*" - "@ethersproject/providers": "*" - "@types/node": "*" - "@types/sinon-chai": "*" - checksum: 10/09a412a062e3390807d89ce442af4b57f2eb0d9ff6297d7493898c9e4dc0208cd317980a3bc7906b2fca840fee6a5ad5400d05cd1eddfbe93d67d717a39b8b05 - languageName: node - linkType: hard - -"@hyperlane-xyz/helloworld@workspace:typescript/helloworld": +"@hyperlane-xyz/helloworld@npm:12.1.0, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": version: 0.0.0-use.local resolution: "@hyperlane-xyz/helloworld@workspace:typescript/helloworld" dependencies: @@ -7891,10 +7873,10 @@ __metadata: "@ethersproject/hardware-wallets": "npm:^5.7.0" "@ethersproject/providers": "npm:*" "@google-cloud/secret-manager": "npm:^5.5.0" - "@hyperlane-xyz/helloworld": "npm:11.0.0" + "@hyperlane-xyz/helloworld": "npm:12.1.0" "@hyperlane-xyz/registry": "npm:11.1.0" - "@hyperlane-xyz/sdk": "npm:11.0.0" - "@hyperlane-xyz/utils": "npm:11.0.0" + "@hyperlane-xyz/sdk": "npm:12.1.0" + "@hyperlane-xyz/utils": "npm:12.1.0" "@inquirer/prompts": "npm:3.3.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-etherscan": "npm:^3.0.3" From a2f4928537f9ecc4a60d99c4d2fd3d71ee21cb24 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 10:07:59 +0200 Subject: [PATCH 123/132] chore: merge utils package from main --- typescript/utils/CHANGELOG.md | 4 +++ typescript/utils/package.json | 2 +- typescript/utils/src/env.ts | 4 +++ typescript/utils/src/index.ts | 2 +- typescript/utils/src/validator.ts | 32 +++++++++++++++++++++ yarn.lock | 47 ++++++++++++++++++++----------- 6 files changed, 73 insertions(+), 18 deletions(-) diff --git a/typescript/utils/CHANGELOG.md b/typescript/utils/CHANGELOG.md index a6e11f782d0..4aa277d315b 100644 --- a/typescript/utils/CHANGELOG.md +++ b/typescript/utils/CHANGELOG.md @@ -1,5 +1,9 @@ # @hyperlane-xyz/utils +## 12.1.0 + +## 12.0.0 + ## 11.0.0 ### Major Changes diff --git a/typescript/utils/package.json b/typescript/utils/package.json index 0bbb2492efb..07adc835b50 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/utils", "description": "General utilities and types for the Hyperlane network", - "version": "11.0.0", + "version": "12.1.0", "dependencies": { "@cosmjs/encoding": "^0.32.4", "@solana/web3.js": "^1.95.4", diff --git a/typescript/utils/src/env.ts b/typescript/utils/src/env.ts index 8841f56a805..bfa7ecfae7b 100644 --- a/typescript/utils/src/env.ts +++ b/typescript/utils/src/env.ts @@ -7,3 +7,7 @@ export function safelyAccessEnvVar(name: string, toLowerCase = false) { return undefined; } } + +export function inCIMode() { + return process.env.CI === 'true'; +} diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index f1612b6c834..8e4880d6831 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -80,7 +80,7 @@ export { isS3CheckpointWithId, } from './checkpoints.js'; export { domainHash } from './domains.js'; -export { safelyAccessEnvVar } from './env.js'; +export { safelyAccessEnvVar, inCIMode } from './env.js'; export { canonizeId, evmId } from './ids.js'; export { LogFormat, diff --git a/typescript/utils/src/validator.ts b/typescript/utils/src/validator.ts index 63f135909d6..4876bc3869e 100644 --- a/typescript/utils/src/validator.ts +++ b/typescript/utils/src/validator.ts @@ -2,6 +2,7 @@ import { ethers } from 'ethers'; import { eqAddress } from './addresses.js'; import { domainHash } from './domains.js'; +import { fromHexString, toHexString } from './strings.js'; import { Address, Checkpoint, @@ -106,3 +107,34 @@ export class BaseValidator { throw new Error('Not implemented'); } } + +/** + * Create signature for validator announce + */ +export const createAnnounce = async ( + validatorPrivKey: string, + storageLocation: string, + mailboxId: string, + localDomain: number, +) => { + const domainIdBytes = Buffer.alloc(4); + domainIdBytes.writeUInt32BE(localDomain); + + const domainHashBytes = toHexString( + Buffer.concat([ + domainIdBytes, + fromHexString(mailboxId), + Buffer.from('HYPERLANE_ANNOUNCEMENT'), + ]), + ); + const domainHash = ethers.utils.keccak256(domainHashBytes); + + const announcementDigestBytes = toHexString( + Buffer.concat([fromHexString(domainHash), Buffer.from(storageLocation)]), + ); + const announcementDigest = ethers.utils.keccak256(announcementDigestBytes); + + return new ethers.Wallet(validatorPrivKey).signMessage( + fromHexString(announcementDigest), + ); +}; diff --git a/yarn.lock b/yarn.lock index a774951dc1f..2cc7e1a725c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8054,7 +8054,37 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/utils@npm:11.0.0, @hyperlane-xyz/utils@workspace:typescript/utils": +"@hyperlane-xyz/utils@npm:11.0.0": + version: 11.0.0 + resolution: "@hyperlane-xyz/utils@npm:11.0.0" + dependencies: + "@cosmjs/encoding": "npm:^0.32.4" + "@solana/web3.js": "npm:^1.95.4" + bignumber.js: "npm:^9.1.1" + ethers: "npm:^5.7.2" + lodash-es: "npm:^4.17.21" + pino: "npm:^8.19.0" + yaml: "npm:2.4.5" + checksum: 10/1f605c1280b23877ff696a31d325c0869af4671f7b3658ae0a31c2359b971c970e180cb91257732cf23f239fc5e70f6b1bc9dd05d959b698a2599e466c04fca0 + languageName: node + linkType: hard + +"@hyperlane-xyz/utils@npm:12.1.0": + version: 12.1.0 + resolution: "@hyperlane-xyz/utils@npm:12.1.0" + dependencies: + "@cosmjs/encoding": "npm:^0.32.4" + "@solana/web3.js": "npm:^1.95.4" + bignumber.js: "npm:^9.1.1" + ethers: "npm:^5.7.2" + lodash-es: "npm:^4.17.21" + pino: "npm:^8.19.0" + yaml: "npm:2.4.5" + checksum: 10/628225400fad5fe43db506939f8ecfc0dabfffc0c29f64a7ed2a47c45898abce7134e2d8787a971939c0416b660f06b9d2fbf30454dc32dce315a8dcb83174ed + languageName: node + linkType: hard + +"@hyperlane-xyz/utils@workspace:typescript/utils": version: 0.0.0-use.local resolution: "@hyperlane-xyz/utils@workspace:typescript/utils" dependencies: @@ -8084,21 +8114,6 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/utils@npm:12.1.0": - version: 12.1.0 - resolution: "@hyperlane-xyz/utils@npm:12.1.0" - dependencies: - "@cosmjs/encoding": "npm:^0.32.4" - "@solana/web3.js": "npm:^1.95.4" - bignumber.js: "npm:^9.1.1" - ethers: "npm:^5.7.2" - lodash-es: "npm:^4.17.21" - pino: "npm:^8.19.0" - yaml: "npm:2.4.5" - checksum: 10/628225400fad5fe43db506939f8ecfc0dabfffc0c29f64a7ed2a47c45898abce7134e2d8787a971939c0416b660f06b9d2fbf30454dc32dce315a8dcb83174ed - languageName: node - linkType: hard - "@hyperlane-xyz/widgets@workspace:typescript/widgets": version: 0.0.0-use.local resolution: "@hyperlane-xyz/widgets@workspace:typescript/widgets" From 1c261f32751edc14fca18c8154a3e8c7986ce534 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 10:08:49 +0200 Subject: [PATCH 124/132] chore: merge solidity from main --- solidity/CHANGELOG.md | 64 ++++++++ solidity/contracts/PackageVersioned.sol | 2 +- solidity/contracts/test/TestSendReceiver.sol | 13 +- solidity/contracts/token/HypERC20.sol | 2 +- solidity/contracts/token/extensions/4626.md | 80 ++++++++++ .../contracts/token/extensions/HypERC4626.sol | 87 ++++------ .../token/extensions/HypERC4626Collateral.sol | 20 ++- .../token/libs/FungibleTokenRouter.sol | 4 + solidity/contracts/token/libs/TokenRouter.sol | 4 +- solidity/coverage.sh | 4 +- solidity/generate-artifact-exports.mjs | 148 ++++++++++-------- solidity/package.json | 4 +- solidity/test/test/TestSendReceiver.t.sol | 11 ++ solidity/test/token/HypERC4626Test.t.sol | 27 ++++ solidity/zk-hardhat.config.cts | 10 +- yarn.lock | 70 ++++++--- 16 files changed, 388 insertions(+), 162 deletions(-) create mode 100644 solidity/contracts/token/extensions/4626.md diff --git a/solidity/CHANGELOG.md b/solidity/CHANGELOG.md index d37fb6a1806..162ffdadb95 100644 --- a/solidity/CHANGELOG.md +++ b/solidity/CHANGELOG.md @@ -1,5 +1,69 @@ # @hyperlane-xyz/core +## 7.1.0 + +### Minor Changes + +- e6f6d61a0: Refactor ZKsync artifact generation and validation logic + +### Patch Changes + +- @hyperlane-xyz/utils@12.1.0 + +## 7.0.0 + +### Major Changes + +- 59a087ded: Remove unused FastTokenRouter +- 59a087ded: ## Changes + + Add immutable `scale` parameter to all warp route variants which scales outbound amounts **down** and inbound amounts **up**. This is useful when different chains of the route have different decimal places to unify semantics of amounts in messages. + + Removes `HypNativeScaled` in favor of `HypNative` with `scale` parameter. + + ## Migration + + If you want to keep the same behavior as before, you can set `scale` to `1` in all your routes. + + ### `HypNativeScaled` Usage + + ```diff + - HypNativeScaled(scale, mailbox) + + HypNative(scale, mailbox) + ``` + + ### `HypERC20` Usage + + ```diff + - HypERC20(decimals, mailbox) + + HypERC20(decimals, scale, mailbox) + ``` + + ### `HypERC20Collateral` Usage + + ```diff + - HypERC20Collateral(erc20, mailbox) + + HypERC20Collateral(erc20, scale, mailbox) + ``` + +### Minor Changes + +- 07321f6f0: Add ZKSync support and restructure build artifacts: + + - Add ZKSync compilation support + - Restructure typechain directory location to core-utils/typechain + - Add ZKSync-specific artifact generation and exports + - Update build process to handle both standard and ZKSync artifacts + - Add new exports for ZKSync build artifacts and contract types + +- 59a087ded: Fixed misuse of aggregation hook funds for relaying messages by making sure msg.value is adequate and refunding if excess. + +### Patch Changes + +- 59a087ded: Refactor TokenRouter internal amount accounting for use in scaling Warp Routes +- 59a087ded: Fix yield route (`HypERC4626`/`HypERC4626Collateral`) decimal scaling by leveraging `FungibleTokenRouter` + - @hyperlane-xyz/utils@12.0.0 + ## 6.1.0 ### Minor Changes diff --git a/solidity/contracts/PackageVersioned.sol b/solidity/contracts/PackageVersioned.sol index 381e583830c..b479179c235 100644 --- a/solidity/contracts/PackageVersioned.sol +++ b/solidity/contracts/PackageVersioned.sol @@ -7,5 +7,5 @@ pragma solidity >=0.6.11; **/ abstract contract PackageVersioned { // GENERATED CODE - DO NOT EDIT - string public constant PACKAGE_VERSION = "6.1.0"; + string public constant PACKAGE_VERSION = "7.1.0"; } diff --git a/solidity/contracts/test/TestSendReceiver.sol b/solidity/contracts/test/TestSendReceiver.sol index ef5be37a1c0..cb9e0667a7f 100644 --- a/solidity/contracts/test/TestSendReceiver.sol +++ b/solidity/contracts/test/TestSendReceiver.sol @@ -47,10 +47,21 @@ contract TestSendReceiver is IMessageRecipient { ); } - function handle(uint32, bytes32, bytes calldata) external payable override { + function handle( + uint32, + bytes32, + bytes calldata _messageBody + ) external payable override { + bytes memory hardcodedFail = "failMessageBody"; + require( + keccak256(_messageBody) != keccak256(hardcodedFail), + "failMessageBody" + ); + bytes32 blockHash = previousBlockHash(); bool isBlockHashEndIn0 = uint256(blockHash) % 16 == 0; require(!isBlockHashEndIn0, "block hash ends in 0"); + emit Handled(blockHash); } diff --git a/solidity/contracts/token/HypERC20.sol b/solidity/contracts/token/HypERC20.sol index 43a1131c46a..843625b6d3e 100644 --- a/solidity/contracts/token/HypERC20.sol +++ b/solidity/contracts/token/HypERC20.sol @@ -64,7 +64,7 @@ contract HypERC20 is ERC20Upgradeable, FungibleTokenRouter { */ function _transferFromSender( uint256 _amount - ) internal override returns (bytes memory) { + ) internal virtual override returns (bytes memory) { _burn(msg.sender, _amount); return bytes(""); // no metadata } diff --git a/solidity/contracts/token/extensions/4626.md b/solidity/contracts/token/extensions/4626.md new file mode 100644 index 00000000000..013b334d6b3 --- /dev/null +++ b/solidity/contracts/token/extensions/4626.md @@ -0,0 +1,80 @@ +# Yield Routes + +Yield routes are a mechanism to transfer yield-bearing assets across chains using the ERC4626 standard. + +`HypERC4626Collateral` is a contract that implements `ERC4626` deposits upon transfer and withdrawal upon transfer back. + +`HypERC4646` is a contract that implements a rebasing `ERC20`. Balances are virtualized as vault shares times the total assets over the total shares in the `ERC4626` vault (on the collateral chain). + +## `HypERC4626Collateral.transferRemote` + +```mermaid +sequenceDiagram + participant User + participant ERC20 + participant ERC4626 + participant WarpRoute + participant Mailbox + + User->>WarpRoute: transferRemote(dest, recipient, amount) + WarpRoute->>ERC20: transferFrom(User, WarpRoute, amount) + WarpRoute->>ERC20: approve(ERC4626, amount) + WarpRoute->>ERC4626: deposit(amount, WarpRoute) + ERC4626-->>WarpRoute: shares + WarpRoute->>ERC4626: convertToAssets(PRECISION) + ERC4626-->>WarpRoute: exchangeRate + WarpRoute->>WarpRoute: nonce += 1 + + WarpRoute->>Mailbox: dispatch(dest, router[dest], {recipient, shares, exchangeRate, nonce}) +``` + +## `HypERC4626.handle` + +```mermaid +sequenceDiagram + participant Recipient + participant WarpRoute + participant Mailbox + + Mailbox->>WarpRoute: handle(origin, sender, {recipient, shares, exchangeRate, nonce}) + alt nonce > latestNonce && origin = collateralChain + WarpRoute->>WarpRoute: latestNonce = nonce + WarpRoute->>WarpRoute: latestExchangeRate = exchangeRate + end + + WarpRoute->>WarpRoute: mint(shares, recipient) + WarpRoute->>WarpRoute: balance[recipient] = shares + Recipient->>WarpRoute: balanceOf(recipient) + WarpRoute-->>WarpRoute: balance[recipient] * latestExchangeRate + WarpRoute-->>Recipient: amount +``` + +## `HypERC4626.transferRemote` + +```mermaid +sequenceDiagram + participant User + participant WarpRoute + participant Mailbox + + User->>WarpRoute: transferRemote(dest, recipient, amount) + WarpRoute-->>WarpRoute: burn(shares, User) + WarpRoute->>Mailbox: dispatch(dest, router[dest], {recipient, shares}) +``` + +## `HypERC4626Collateral.handle` + +```mermaid +sequenceDiagram + participant Recipient + participant ERC20 + participant ERC4626 + participant WarpRoute + participant Mailbox + + Mailbox->>WarpRoute: handle(orgn, sender, {recipient, shares}) + WarpRoute->>ERC4626: redeem(shares, recipient, WarpRoute) + ERC4626-->>WarpRoute: amount + WarpRoute->>ERC20: transfer(recipient, amount) + ERC20-->>Recipient: amount +``` diff --git a/solidity/contracts/token/extensions/HypERC4626.sol b/solidity/contracts/token/extensions/HypERC4626.sol index aa30578c698..57e9f8db606 100644 --- a/solidity/contracts/token/extensions/HypERC4626.sol +++ b/solidity/contracts/token/extensions/HypERC4626.sol @@ -19,6 +19,7 @@ import {HypERC20} from "../HypERC20.sol"; import {Message} from "../../libs/Message.sol"; import {TokenMessage} from "../libs/TokenMessage.sol"; import {TokenRouter} from "../libs/TokenRouter.sol"; +import {FungibleTokenRouter} from "../libs/FungibleTokenRouter.sol"; // ============ External Imports ============ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -29,6 +30,10 @@ import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ * @title Hyperlane ERC20 Rebasing Token * @author Abacus Works * @notice This contract implements a rebasing token that reflects yields from the origin chain + * @dev Messages contain amounts as shares of ERC4626 and exchange rate of assets per share. + * @dev internal ERC20 balances storage mapping is in share units + * @dev internal ERC20 allowances storage mapping is in asset units + * @dev public ERC20 interface is in asset units */ contract HypERC4626 is HypERC20 { using Math for uint256; @@ -55,31 +60,6 @@ contract HypERC4626 is HypERC20 { // ============ Public Functions ============ - /// Override transfer to handle underlying amounts while using shares internally - /// @inheritdoc ERC20Upgradeable - /// @dev the Transfer event emitted from ERC20Upgradeable will be in terms of shares not assets, so it may be misleading - function transfer( - address to, - uint256 amount - ) public virtual override returns (bool) { - _transfer(_msgSender(), to, assetsToShares(amount)); - return true; - } - - /// Override transferFrom to handle underlying amounts while using shares internally - /// @inheritdoc ERC20Upgradeable - function transferFrom( - address sender, - address recipient, - uint256 amount - ) public virtual override returns (bool) { - address spender = _msgSender(); - uint256 shares = assetsToShares(amount); - _spendAllowance(sender, spender, amount); - _transfer(sender, recipient, shares); - return true; - } - /// Override totalSupply to return the total assets instead of shares. This reflects the actual circulating supply in terms of assets, accounting for rebasing /// @inheritdoc ERC20Upgradeable function totalSupply() public view virtual override returns (uint256) { @@ -112,38 +92,37 @@ contract HypERC4626 is HypERC20 { return _shares.mulDiv(exchangeRate, PRECISION); } - // ============ Internal Functions ============ + // @inheritdoc HypERC20 + // @dev Amount specified by the user is in assets, but the internal accounting is in shares + function _transferFromSender( + uint256 _amount + ) internal virtual override returns (bytes memory) { + return HypERC20._transferFromSender(assetsToShares(_amount)); + } - /// Override to send shares instead of assets from synthetic - /// @inheritdoc TokenRouter - function _transferRemote( - uint32 _destination, - bytes32 _recipient, - uint256 _amountOrId, - uint256 _value, - bytes memory _hookMetadata, - address _hook - ) internal virtual override returns (bytes32 messageId) { - uint256 _shares = assetsToShares(_amountOrId); - _transferFromSender(_shares); - bytes memory _tokenMessage = TokenMessage.format( - _recipient, - _shares, - bytes("") - ); - - messageId = _Router_dispatch( - _destination, - _value, - _tokenMessage, - _hookMetadata, - _hook - ); - - emit SentTransferRemote(_destination, _recipient, _amountOrId); + // @inheritdoc FungibleTokenRouter + // @dev Amount specified by user is in assets, but the message accounting is in shares + function _outboundAmount( + uint256 _localAmount + ) internal view virtual override returns (uint256) { + return + FungibleTokenRouter._outboundAmount(assetsToShares(_localAmount)); + } + + // @inheritdoc ERC20Upgradeable + // @dev Amount specified by user is in assets, but the internal accounting is in shares + function _transfer( + address _from, + address _to, + uint256 _amount + ) internal virtual override { + super._transfer(_from, _to, assetsToShares(_amount)); } - /// override _handle to update exchange rate + // `_inboundAmount` implementation reused from `FungibleTokenRouter` unchanged because message + // accounting is in shares + + // ========== TokenRouter extensions ============ /// @inheritdoc TokenRouter function _handle( uint32 _origin, diff --git a/solidity/contracts/token/extensions/HypERC4626Collateral.sol b/solidity/contracts/token/extensions/HypERC4626Collateral.sol index 774bb2c10c7..8dbe737d787 100644 --- a/solidity/contracts/token/extensions/HypERC4626Collateral.sol +++ b/solidity/contracts/token/extensions/HypERC4626Collateral.sol @@ -17,6 +17,7 @@ pragma solidity >=0.8.0; import {TokenMessage} from "../libs/TokenMessage.sol"; import {HypERC20Collateral} from "../HypERC20Collateral.sol"; import {TypeCasts} from "../../libs/TypeCasts.sol"; +import {TokenRouter} from "../libs/TokenRouter.sol"; // ============ External Imports ============ import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; @@ -56,6 +57,12 @@ contract HypERC4626Collateral is HypERC20Collateral { _MailboxClient_initialize(_hook, _interchainSecurityModule, _owner); } + /** + * @inheritdoc TokenRouter + * @dev Override `_transferRemote` to send shares as amount and append {exchange rate, nonce} in the message. + * This is preferred for readability and to avoid confusion with the amount of shares. The scaling factor + * is applied to the shares returned by the deposit before sending the message. + */ function _transferRemote( uint32 _destination, bytes32 _recipient, @@ -67,6 +74,7 @@ contract HypERC4626Collateral is HypERC20Collateral { // Can't override _transferFromSender only because we need to pass shares in the token message _transferFromSender(_amount); uint256 _shares = _depositIntoVault(_amount); + uint256 _exchangeRate = vault.convertToAssets(PRECISION); rateUpdateNonce++; @@ -75,9 +83,10 @@ contract HypERC4626Collateral is HypERC20Collateral { rateUpdateNonce ); + uint256 _outboundAmount = _outboundAmount(_shares); bytes memory _tokenMessage = TokenMessage.format( _recipient, - _shares, + _outboundAmount, _tokenMetadata ); @@ -89,7 +98,7 @@ contract HypERC4626Collateral is HypERC20Collateral { _hook ); - emit SentTransferRemote(_destination, _recipient, _shares); + emit SentTransferRemote(_destination, _recipient, _outboundAmount); } /** @@ -102,16 +111,15 @@ contract HypERC4626Collateral is HypERC20Collateral { } /** - * @dev Transfers `_amount` of `wrappedToken` from this contract to `_recipient`, and withdraws from vault + * @dev Withdraws `_shares` of `wrappedToken` from this contract to `_recipient` * @inheritdoc HypERC20Collateral */ function _transferTo( address _recipient, - uint256 _amount, + uint256 _shares, bytes calldata ) internal virtual override { - // withdraw with the specified amount of shares - vault.redeem(_amount, _recipient, address(this)); + vault.redeem(_shares, _recipient, address(this)); } /** diff --git a/solidity/contracts/token/libs/FungibleTokenRouter.sol b/solidity/contracts/token/libs/FungibleTokenRouter.sol index 6faf12a0256..7579f3287e2 100644 --- a/solidity/contracts/token/libs/FungibleTokenRouter.sol +++ b/solidity/contracts/token/libs/FungibleTokenRouter.sol @@ -3,6 +3,10 @@ pragma solidity >=0.8.0; import {TokenRouter} from "./TokenRouter.sol"; +/** + * @title Hyperlane Fungible Token Router that extends TokenRouter with scaling logic for fungible tokens with different decimals. + * @author Abacus Works + */ abstract contract FungibleTokenRouter is TokenRouter { uint256 public immutable scale; diff --git a/solidity/contracts/token/libs/TokenRouter.sol b/solidity/contracts/token/libs/TokenRouter.sol index a78a007dc7a..65d2bd13f1a 100644 --- a/solidity/contracts/token/libs/TokenRouter.sol +++ b/solidity/contracts/token/libs/TokenRouter.sol @@ -20,7 +20,7 @@ abstract contract TokenRouter is GasRouter { * @dev Emitted on `transferRemote` when a transfer message is dispatched. * @param destination The identifier of the destination chain. * @param recipient The address of the recipient on the destination chain. - * @param amount The amount of tokens burnt on the origin chain. + * @param amount The amount of tokens sent in to the remote recipient. */ event SentTransferRemote( uint32 indexed destination, @@ -32,7 +32,7 @@ abstract contract TokenRouter is GasRouter { * @dev Emitted on `_handle` when a transfer message is processed. * @param origin The identifier of the origin chain. * @param recipient The address of the recipient on the destination chain. - * @param amount The amount of tokens minted on the destination chain. + * @param amount The amount of tokens received from the remote sender. */ event ReceivedTransferRemote( uint32 indexed origin, diff --git a/solidity/coverage.sh b/solidity/coverage.sh index 4675ed84a21..3a785017909 100755 --- a/solidity/coverage.sh +++ b/solidity/coverage.sh @@ -3,9 +3,9 @@ # exit on error set -e -# exclude FastTokenRouter until https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2806 is resolved forge coverage \ --report lcov \ --report summary \ - --no-match-coverage "(test|mock|node_modules|script|Fast)" \ + --no-match-coverage "(test|mock|node_modules|script)" \ + --no-match-test "Fork" \ --ir-minimum # https://github.com/foundry-rs/foundry/issues/3357 diff --git a/solidity/generate-artifact-exports.mjs b/solidity/generate-artifact-exports.mjs index 69327ffda46..f55f463c807 100755 --- a/solidity/generate-artifact-exports.mjs +++ b/solidity/generate-artifact-exports.mjs @@ -3,41 +3,58 @@ import { basename, dirname, join } from 'path'; import { glob } from 'typechain'; import { fileURLToPath } from 'url'; -const cwd = process.cwd(); +const CONFIG = { + cwd: process.cwd(), + outputDir: 'dist/zksync/', + artifactsDir: 'artifacts', + artifactGlobs: [ + `!./artifacts-zk/!(build-info)/**/*.dbg.json`, + `./artifacts-zk/!(build-info)/**/+([a-zA-Z0-9_]).json`, + ], + formatIdentifier: 'hh-zksolc-artifact-1', +}; + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const ROOT_OUTPUT_DIR = join(__dirname, 'dist/zksync/'); -const ARTIFACTS_OUTPUT_DIR = join(ROOT_OUTPUT_DIR, 'artifacts'); +const ROOT_OUTPUT_DIR = join(__dirname, CONFIG.outputDir); +const ARTIFACTS_OUTPUT_DIR = join(ROOT_OUTPUT_DIR, CONFIG.artifactsDir); /** * @notice Templates for TypeScript artifact generation */ -const TEMPLATES = { - JS_ARTIFACT: `\ -export const {name} = {artifact}; -`, - - DTS_ARTIFACT: `\ -import type { ZKSyncArtifact } from '../types.js'; - -export declare const {name}: ZKSyncArtifact; -`, +class Templates { + static jsArtifact(name, artifact) { + return `export const ${name} = ${JSON.stringify(artifact)};`; + } - JS_INDEX: `\ -{imports} + static dtsArtifact(name) { + return `import type { ZKSyncArtifact } from '../types.js'; +export declare const ${name}: ZKSyncArtifact;`; + } + static jsIndex(imports, exports) { + return `${imports} export const zkSyncContractArtifacts = [ -{exports} -]; -`, +${exports} +];`; + } - DTS_INDEX: `\ -import type { ZKSyncArtifact } from './types.js'; + static dtsIndex() { + return `import type { ZKSyncArtifact } from './types.js'; +export declare const zkSyncContractArtifacts: readonly ZKSyncArtifact[];`; + } -export declare const zkSyncContractArtifacts: readonly ZKSyncArtifact[]; -`, -}; + // Generates a single import line for a contract in index file + static importLine(name) { + return `import { ${name} } from './artifacts/${name}.js';`; + } + + // Generates a single export line for a contract in index file + static exportLine(name) { + return ` ${name},`; + } +} class ArtifactGenerator { constructor() { @@ -50,10 +67,7 @@ class ArtifactGenerator { * @return {string[]} Array of file paths matching the glob pattern */ getArtifactPaths() { - return glob(cwd, [ - `!./artifacts-zk/!(build-info)/**/*.dbg.json`, - `./artifacts-zk/!(build-info)/**/+([a-zA-Z0-9_]).json`, - ]); + return glob(CONFIG.cwd, CONFIG.artifactGlobs); } /** @@ -73,45 +87,6 @@ class ArtifactGenerator { return JSON.parse(content); } - /** - * @notice Generates JavaScript content for a contract artifact - */ - generateJavaScriptContent(name, artifact) { - return TEMPLATES.JS_ARTIFACT.replace('{name}', name).replace( - '{artifact}', - JSON.stringify(artifact, null, 2), - ); - } - - /** - * @notice Generates TypeScript declaration content for a contract artifact - */ - generateDeclarationContent(name) { - return TEMPLATES.DTS_ARTIFACT.replace('{name}', name); - } - - /** - * @notice Generates index file contents - */ - generateIndexContents(artifactNames) { - const imports = artifactNames - .map((name) => `import { ${name} } from './artifacts/${name}.js';`) - .join('\n'); - const exports = artifactNames.map((name) => ` ${name},`).join('\n'); - - const jsContent = TEMPLATES.JS_INDEX.replace('{imports}', imports).replace( - '{exports}', - exports, - ); - - const dtsContent = TEMPLATES.DTS_INDEX.replace( - '{imports}', - imports, - ).replace('{exports}', exports); - - return { jsContent, dtsContent }; - } - /** * @notice Processes a single artifact file */ @@ -124,15 +99,35 @@ class ArtifactGenerator { const artifact = await this.readArtifactFile(filePath); + /** + * @notice Validates that the artifact was compiled with zksolc + * + * Format examples: + * - Valid: "_format": "hh-zksolc-artifact-1" (compiled with zksolc) + * - Invalid: "_format": "hh-sol-artifact-1" (standard Solidity compilation) + */ + if ( + !artifact._format || + !artifact._format.includes(CONFIG.formatIdentifier) + ) { + throw new Error( + `Artifact ${name} validation failed: invalid _format property. Expected ${ + CONFIG.formatIdentifier + } but got '${ + artifact._format || 'undefined' + }'. It may not be properly compiled with zksolc.`, + ); + } + // Generate and write .js file - const jsContent = this.generateJavaScriptContent(name, artifact); + const jsContent = Templates.jsArtifact(name, artifact); await fs.writeFile( join(ROOT_OUTPUT_DIR, 'artifacts', `${name}.js`), jsContent, ); // Generate and write .d.ts file - const dtsContent = this.generateDeclarationContent(name); + const dtsContent = Templates.dtsArtifact(name); await fs.writeFile( join(ROOT_OUTPUT_DIR, 'artifacts', `${name}.d.ts`), dtsContent, @@ -141,6 +136,23 @@ class ArtifactGenerator { this.processedFiles.add(name); } + /** + * @notice Generates index file contents + */ + generateIndexContents(artifactNames) { + const imports = artifactNames + .map((name) => Templates.importLine(name)) + .join('\n'); + const exports = artifactNames + .map((name) => Templates.exportLine(name)) + .join('\n'); + + const jsContent = Templates.jsIndex(imports, exports); + const dtsContent = Templates.dtsIndex(); + + return { jsContent, dtsContent }; + } + async generate() { try { await this.createOutputDirectory(); diff --git a/solidity/package.json b/solidity/package.json index 9c001dd5555..47e6db51788 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -1,12 +1,12 @@ { "name": "@hyperlane-xyz/core", "description": "Core solidity contracts for Hyperlane", - "version": "6.1.0", + "version": "7.1.0", "dependencies": { "@arbitrum/nitro-contracts": "^1.2.1", "@chainlink/contracts-ccip": "^1.5.0", "@eth-optimism/contracts": "^0.6.0", - "@hyperlane-xyz/utils": "11.0.0", + "@hyperlane-xyz/utils": "12.1.0", "@layerzerolabs/lz-evm-oapp-v2": "2.0.2", "@matterlabs/hardhat-zksync-solc": "1.2.5", "@matterlabs/hardhat-zksync-verify": "1.7.1", diff --git a/solidity/test/test/TestSendReceiver.t.sol b/solidity/test/test/TestSendReceiver.t.sol index 09b584a8bb7..e8d4566891e 100644 --- a/solidity/test/test/TestSendReceiver.t.sol +++ b/solidity/test/test/TestSendReceiver.t.sol @@ -111,4 +111,15 @@ contract TestSendReceiverTest is Test { "0x1234" ); } + + function testHandle_withHardcodedBody() public { + bytes memory hardcodedFail = "failMessageBody"; + + vm.expectRevert("failMessageBody"); + testSendReceiver.handle( + 0, + address(testSendReceiver).addressToBytes32(), + hardcodedFail + ); + } } diff --git a/solidity/test/token/HypERC4626Test.t.sol b/solidity/test/token/HypERC4626Test.t.sol index 71f9fc37c63..60a202f2623 100644 --- a/solidity/test/token/HypERC4626Test.t.sol +++ b/solidity/test/token/HypERC4626Test.t.sol @@ -542,6 +542,33 @@ contract HypERC4626CollateralTest is HypTokenTest { ); // asserting that the exchange rate is set finally by the collateral variant } + function test_rebasingERC20() public { + _performRemoteTransferWithoutExpectation(0, transferAmount); + assertEq(remoteToken.balanceOf(BOB), transferAmount); + + _accrueYield(); + localRebasingToken.rebase(DESTINATION, bytes(""), address(0)); // yield is added + remoteMailbox.processNextInboundMessage(); + + uint256 balance = remoteToken.balanceOf(BOB); + assertApproxEqRelDecimal( + balance, + transferAmount + _discountedYield(), + 1e14, + 0 + ); + + vm.prank(BOB); + remoteToken.approve(ALICE, balance); + + vm.prank(ALICE); + remoteToken.transferFrom(BOB, CAROL, balance); + + assertEq(remoteToken.allowance(BOB, ALICE), 0); + assertEq(remoteToken.balanceOf(BOB), 0); + assertEq(remoteToken.balanceOf(CAROL), balance); + } + function test_cyclicTransfers() public { // ALICE: local -> remote(BOB) _performRemoteTransferWithoutExpectation(0, transferAmount); diff --git a/solidity/zk-hardhat.config.cts b/solidity/zk-hardhat.config.cts index 645359382ae..2e6caed3043 100644 --- a/solidity/zk-hardhat.config.cts +++ b/solidity/zk-hardhat.config.cts @@ -10,10 +10,18 @@ import { rootHardhatConfig } from './rootHardhatConfig.cjs'; module.exports = { ...rootHardhatConfig, zksolc: { - version: '1.5.3', + version: '1.5.12', compilerSource: 'binary', enableEraVMExtensions: true, }, + defaultNetwork: 'ZKsyncInMemoryNode', + networks: { + ZKsyncInMemoryNode: { + url: 'http://127.0.0.1:8011', + ethNetwork: '', + zksync: true, + }, + }, paths: { sources: './contracts', cache: './cache-zk', diff --git a/yarn.lock b/yarn.lock index 2cc7e1a725c..295317a7c16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7692,14 +7692,58 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/core@npm:6.1.0, @hyperlane-xyz/core@workspace:solidity": +"@hyperlane-xyz/core@npm:6.1.0": + version: 6.1.0 + resolution: "@hyperlane-xyz/core@npm:6.1.0" + dependencies: + "@arbitrum/nitro-contracts": "npm:^1.2.1" + "@chainlink/contracts-ccip": "npm:^1.5.0" + "@eth-optimism/contracts": "npm:^0.6.0" + "@hyperlane-xyz/utils": "npm:11.0.0" + "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" + "@matterlabs/hardhat-zksync-solc": "npm:1.2.5" + "@matterlabs/hardhat-zksync-verify": "npm:1.7.1" + "@openzeppelin/contracts": "npm:^4.9.3" + "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" + fx-portal: "npm:^1.0.3" + peerDependencies: + "@ethersproject/abi": "*" + "@ethersproject/providers": "*" + "@types/sinon-chai": "*" + checksum: 10/a45eb1ae30b240f345b8873d6d8f2ee98cca6701fcbe1ef2bcd62f9d47ce7399137b2d4c5ab6abee41ffde6ae5892af54158b9e39394509781a9a9e6acd8ece1 + languageName: node + linkType: hard + +"@hyperlane-xyz/core@npm:7.1.0": + version: 7.1.0 + resolution: "@hyperlane-xyz/core@npm:7.1.0" + dependencies: + "@arbitrum/nitro-contracts": "npm:^1.2.1" + "@chainlink/contracts-ccip": "npm:^1.5.0" + "@eth-optimism/contracts": "npm:^0.6.0" + "@hyperlane-xyz/utils": "npm:12.1.0" + "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" + "@matterlabs/hardhat-zksync-solc": "npm:1.2.5" + "@matterlabs/hardhat-zksync-verify": "npm:1.7.1" + "@openzeppelin/contracts": "npm:^4.9.3" + "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" + fx-portal: "npm:^1.0.3" + peerDependencies: + "@ethersproject/abi": "*" + "@ethersproject/providers": "*" + "@types/sinon-chai": "*" + checksum: 10/fe76fa68ccecdb1832b58e8d91fd89c3f66ff7f99b37302a5a57e5efd1d160e73fb2a34f2b36050cc40384cedfb933ffa94c8c4a0698ee8bdf0d2b79ea849980 + languageName: node + linkType: hard + +"@hyperlane-xyz/core@workspace:solidity": version: 0.0.0-use.local resolution: "@hyperlane-xyz/core@workspace:solidity" dependencies: "@arbitrum/nitro-contracts": "npm:^1.2.1" "@chainlink/contracts-ccip": "npm:^1.5.0" "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:11.0.0" + "@hyperlane-xyz/utils": "npm:12.1.0" "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" "@layerzerolabs/solidity-examples": "npm:^1.1.0" "@matterlabs/hardhat-zksync-solc": "npm:1.2.5" @@ -7739,28 +7783,6 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/core@npm:7.1.0": - version: 7.1.0 - resolution: "@hyperlane-xyz/core@npm:7.1.0" - dependencies: - "@arbitrum/nitro-contracts": "npm:^1.2.1" - "@chainlink/contracts-ccip": "npm:^1.5.0" - "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:12.1.0" - "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" - "@matterlabs/hardhat-zksync-solc": "npm:1.2.5" - "@matterlabs/hardhat-zksync-verify": "npm:1.7.1" - "@openzeppelin/contracts": "npm:^4.9.3" - "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" - fx-portal: "npm:^1.0.3" - peerDependencies: - "@ethersproject/abi": "*" - "@ethersproject/providers": "*" - "@types/sinon-chai": "*" - checksum: 10/fe76fa68ccecdb1832b58e8d91fd89c3f66ff7f99b37302a5a57e5efd1d160e73fb2a34f2b36050cc40384cedfb933ffa94c8c4a0698ee8bdf0d2b79ea849980 - languageName: node - linkType: hard - "@hyperlane-xyz/cosmos-sdk@workspace:typescript/cosmos-sdk": version: 0.0.0-use.local resolution: "@hyperlane-xyz/cosmos-sdk@workspace:typescript/cosmos-sdk" From cc4d65060ba18c7af2627ed0b0690e73e0427774 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 10:09:44 +0200 Subject: [PATCH 125/132] chore: merge rust from main --- rust/Dockerfile | 106 +-- rust/main/Cargo.lock | 754 ++++++++++++++++-- rust/main/Cargo.toml | 20 +- rust/main/agents/relayer/Cargo.toml | 5 + rust/main/agents/relayer/src/lib.rs | 1 + rust/main/agents/relayer/src/metrics.rs | 1 + .../relayer/src/metrics/message_submission.rs | 78 ++ .../relayer/src/msg/metadata/aggregation.rs | 167 +++- .../agents/relayer/src/msg/metadata/base.rs | 381 ++++++++- .../relayer/src/msg/metadata/base_builder.rs | 170 ++-- .../validator_announced_storages.rs | 74 ++ .../validator_announced_storages/tests.rs | 217 +++++ .../src/msg/metadata/ccip_read/cache_types.rs | 37 + .../relayer/src/msg/metadata/ccip_read/mod.rs | 175 ++++ .../src/msg/metadata/message_builder.rs | 222 ++++-- .../agents/relayer/src/msg/metadata/mod.rs | 3 +- .../relayer/src/msg/metadata/multisig/base.rs | 97 ++- .../metadata/multisig/merkle_root_multisig.rs | 7 +- .../metadata/multisig/message_id_multisig.rs | 7 +- .../relayer/src/msg/metadata/routing.rs | 89 ++- rust/main/agents/relayer/src/msg/mod.rs | 2 + rust/main/agents/relayer/src/msg/op_batch.rs | 452 +++++++++++ rust/main/agents/relayer/src/msg/op_queue.rs | 48 +- .../agents/relayer/src/msg/op_submitter.rs | 499 ++++++++---- .../agents/relayer/src/msg/pending_message.rs | 412 +++++++--- rust/main/agents/relayer/src/msg/processor.rs | 96 ++- rust/main/agents/relayer/src/msg/utils.rs | 55 ++ rust/main/agents/relayer/src/relayer.rs | 501 +++++++++--- .../src/server/environment_variable.rs | 188 +++++ rust/main/agents/relayer/src/server/mod.rs | 11 + rust/main/agents/relayer/src/settings/mod.rs | 85 +- .../src/test_utils/mock_aggregation_ism.rs | 22 +- .../src/test_utils/mock_base_builder.rs | 91 ++- .../agents/relayer/src/test_utils/mock_ism.rs | 50 +- .../src/test_utils/mock_routing_ism.rs | 29 +- rust/main/agents/scraper/Cargo.toml | 3 +- rust/main/agents/scraper/src/agent.rs | 17 +- rust/main/agents/validator/Cargo.toml | 5 + rust/main/agents/validator/src/main.rs | 2 +- rust/main/agents/validator/src/settings.rs | 200 ++++- rust/main/agents/validator/src/submit.rs | 216 ++++- rust/main/agents/validator/src/validator.rs | 75 +- .../hyperlane-cosmos-native/src/mailbox.rs | 6 +- .../src/providers/grpc.rs | 22 +- .../src/trait_builder.rs | 6 +- .../hyperlane-cosmos/src/mailbox/contract.rs | 6 +- .../hyperlane-cosmos/src/providers/grpc.rs | 37 +- .../src/providers/grpc/tests.rs | 5 +- .../hyperlane-cosmos/src/trait_builder.rs | 8 +- .../main/chains/hyperlane-ethereum/Cargo.toml | 3 +- .../chains/hyperlane-ethereum/src/config.rs | 24 +- .../src/contracts/mailbox.rs | 249 ++++-- .../src/contracts/multicall.rs | 81 +- .../src/contracts/validator_announce.rs | 2 + .../src/rpc_clients/fallback.rs | 2 +- .../src/rpc_clients/trait_builder.rs | 50 +- rust/main/chains/hyperlane-ethereum/src/tx.rs | 248 ++++-- .../main/chains/hyperlane-fuel/src/mailbox.rs | 6 +- .../chains/hyperlane-sealevel/src/mailbox.rs | 9 +- .../chains/hyperlane-sealevel/src/provider.rs | 7 +- .../hyperlane-sealevel/src/trait_builder.rs | 4 +- rust/main/config/mainnet_config.json | 309 +++---- rust/main/config/test_sealevel_config.json | 2 + rust/main/config/testnet_config.json | 164 +++- .../templates/relayer-external-secret.yaml | 8 + .../templates/relayer-statefulset.yaml | 51 ++ rust/main/helm/hyperlane-agent/values.yaml | 4 + rust/main/hyperlane-base/Cargo.toml | 7 +- rust/main/hyperlane-base/src/agent.rs | 28 +- rust/main/hyperlane-base/src/cache/error.rs | 13 + .../hyperlane-base/src/cache/metered_cache.rs | 107 +++ rust/main/hyperlane-base/src/cache/mod.rs | 42 + .../src/cache/moka/dynamic_expiry.rs | 103 +++ .../src/cache/moka/local_cache.rs | 56 ++ .../main/hyperlane-base/src/cache/moka/mod.rs | 345 ++++++++ .../src/cache/optional_cache.rs | 55 ++ .../hyperlane-base/src/contract_sync/mod.rs | 8 +- rust/main/hyperlane-base/src/db/mod.rs | 24 +- .../src/db/rocks/hyperlane_db.rs | 27 +- rust/main/hyperlane-base/src/lib.rs | 2 + rust/main/hyperlane-base/src/metadata.rs | 17 + .../src/metrics/agent_metrics.rs | 14 +- rust/main/hyperlane-base/src/metrics/cache.rs | 12 + rust/main/hyperlane-base/src/metrics/core.rs | 137 +++- rust/main/hyperlane-base/src/metrics/mod.rs | 1 + rust/main/hyperlane-base/src/settings/base.rs | 33 +- .../hyperlane-base/src/settings/chains.rs | 23 +- .../src/settings/checkpoint_syncer.rs | 14 +- .../src/settings/parser/connection_parser.rs | 60 +- .../hyperlane-base/src/settings/parser/mod.rs | 36 +- .../hyperlane-base/src/settings/trace/mod.rs | 2 + .../src/traits/checkpoint_syncer.rs | 3 +- .../hyperlane-base/src/types/gcs_storage.rs | 8 +- .../hyperlane-base/src/types/local_storage.rs | 6 +- .../main/hyperlane-base/src/types/multisig.rs | 253 +++++- .../hyperlane-base/src/types/s3_storage.rs | 229 ++++-- rust/main/hyperlane-core/src/chain.rs | 29 +- rust/main/hyperlane-core/src/config/mod.rs | 9 +- .../src/traits/interchain_security_module.rs | 1 + .../main/hyperlane-core/src/traits/mailbox.rs | 27 +- .../src/traits/pending_operation.rs | 63 +- .../src/traits/pending_operation/tests.rs | 154 ++++ rust/main/hyperlane-metric/Cargo.toml | 2 +- rust/main/hyperlane-test/src/mocks/mailbox.rs | 40 +- rust/main/submitter/Cargo.toml | 2 + rust/main/submitter/src/chain_tx_adapter.rs | 3 +- .../submitter/src/chain_tx_adapter/adapter.rs | 41 +- .../submitter/src/chain_tx_adapter/chains.rs | 1 - .../src/chain_tx_adapter/chains/cosmos.rs | 27 +- .../src/chain_tx_adapter/chains/ethereum.rs | 27 +- .../src/chain_tx_adapter/chains/factory.rs | 12 +- .../src/chain_tx_adapter/chains/sealevel.rs | 1 - .../chains/sealevel/adapter.rs | 127 +-- .../chains/sealevel/adapter/tests.rs | 318 +------- .../chains/sealevel/adapter/tests/build.rs | 40 + .../chains/sealevel/adapter/tests/common.rs | 237 ++++++ .../chains/sealevel/adapter/tests/config.rs | 52 ++ .../chains/sealevel/adapter/tests/estimate.rs | 23 + .../chains/sealevel/adapter/tests/simulate.rs | 18 + .../chains/sealevel/adapter/tests/status.rs | 18 + .../chains/sealevel/adapter/tests/submit.rs | 18 + .../chains/sealevel/payload.rs | 17 +- rust/main/submitter/src/error.rs | 58 ++ rust/main/submitter/src/lib.rs | 12 + rust/main/submitter/src/payload.rs | 2 - rust/main/submitter/src/payload/types.rs | 67 +- rust/main/submitter/src/payload_dispatcher.rs | 8 +- .../submitter/src/payload_dispatcher/db.rs | 7 + .../src/payload_dispatcher/db/loader.rs | 317 ++++++++ .../src/payload_dispatcher/db/payload.rs | 5 + .../payload_dispatcher/db/payload/loader.rs | 60 ++ .../db/payload/payload_db.rs | 246 ++++++ .../src/payload_dispatcher/db/transaction.rs | 5 + .../db/transaction/loader.rs | 65 ++ .../db/transaction/transaction_db.rs | 221 +++++ .../src/payload_dispatcher/dispatcher.rs | 149 +++- .../src/payload_dispatcher/entrypoint.rs | 134 +++- .../src/payload_dispatcher/stages.rs | 4 + .../stages/building_stage.rs | 354 +++++--- .../stages/finality_stage.rs | 541 +++++++++++++ .../stages/inclusion_stage.rs | 428 ++++++++++ .../src/payload_dispatcher/stages/state.rs | 59 +- .../src/payload_dispatcher/stages/utils.rs | 45 ++ .../src/payload_dispatcher/test_utils.rs | 148 ++-- .../submitter/src/payload_dispatcher/tests.rs | 128 +++ rust/main/submitter/src/transaction.rs | 2 - rust/main/submitter/src/transaction/types.rs | 8 + rust/main/utils/abigen/src/lib.rs | 2 + .../utils/run-locally/src/cosmosnative/cli.rs | 8 +- .../src/ethereum/termination_invariants.rs | 19 +- .../src/invariants/termination_invariants.rs | 84 +- rust/main/utils/run-locally/src/main.rs | 64 +- .../utils/run-locally/src/sealevel/mod.rs | 2 + .../src/sealevel/termination_invariants.rs | 14 +- rust/sealevel/.gitignore | 3 +- .../testwarproute/program-ids.json | 8 +- .../GIGA-solanamainnet-soon/program-ids.json | 10 + .../GIGA-solanamainnet-soon/token-config.json | 18 + .../GOAT-solanamainnet-soon/program-ids.json | 10 + .../GOAT-solanamainnet-soon/token-config.json | 18 + .../program-ids.json | 10 + .../token-config.json | 18 + .../SPORE-solanamainnet-soon/program-ids.json | 10 + .../token-config.json | 18 + 164 files changed, 11182 insertions(+), 2162 deletions(-) create mode 100644 rust/main/agents/relayer/src/metrics.rs create mode 100644 rust/main/agents/relayer/src/metrics/message_submission.rs create mode 100644 rust/main/agents/relayer/src/msg/metadata/base_builder/validator_announced_storages.rs create mode 100644 rust/main/agents/relayer/src/msg/metadata/base_builder/validator_announced_storages/tests.rs create mode 100644 rust/main/agents/relayer/src/msg/metadata/ccip_read/cache_types.rs create mode 100644 rust/main/agents/relayer/src/msg/metadata/ccip_read/mod.rs create mode 100644 rust/main/agents/relayer/src/msg/op_batch.rs create mode 100644 rust/main/agents/relayer/src/msg/utils.rs create mode 100644 rust/main/agents/relayer/src/server/environment_variable.rs create mode 100644 rust/main/hyperlane-base/src/cache/error.rs create mode 100644 rust/main/hyperlane-base/src/cache/metered_cache.rs create mode 100644 rust/main/hyperlane-base/src/cache/mod.rs create mode 100644 rust/main/hyperlane-base/src/cache/moka/dynamic_expiry.rs create mode 100644 rust/main/hyperlane-base/src/cache/moka/local_cache.rs create mode 100644 rust/main/hyperlane-base/src/cache/moka/mod.rs create mode 100644 rust/main/hyperlane-base/src/cache/optional_cache.rs create mode 100644 rust/main/hyperlane-base/src/metrics/cache.rs create mode 100644 rust/main/hyperlane-core/src/traits/pending_operation/tests.rs create mode 100644 rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/build.rs create mode 100644 rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/common.rs create mode 100644 rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/config.rs create mode 100644 rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/estimate.rs create mode 100644 rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/simulate.rs create mode 100644 rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/status.rs create mode 100644 rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/submit.rs create mode 100644 rust/main/submitter/src/error.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/db.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/db/loader.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/db/payload.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/db/payload/loader.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/db/payload/payload_db.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/db/transaction.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/db/transaction/loader.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/db/transaction/transaction_db.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/stages/utils.rs create mode 100644 rust/main/submitter/src/payload_dispatcher/tests.rs create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/GIGA-solanamainnet-soon/program-ids.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/GIGA-solanamainnet-soon/token-config.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/GOAT-solanamainnet-soon/program-ids.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/GOAT-solanamainnet-soon/token-config.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/POPCAT-solanamainnet-soon/program-ids.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/POPCAT-solanamainnet-soon/token-config.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/SPORE-solanamainnet-soon/program-ids.json create mode 100644 rust/sealevel/environments/mainnet3/warp-routes/SPORE-solanamainnet-soon/token-config.json diff --git a/rust/Dockerfile b/rust/Dockerfile index fb3f5cc185a..1bde4548612 100644 --- a/rust/Dockerfile +++ b/rust/Dockerfile @@ -1,66 +1,80 @@ -# syntax=docker/dockerfile:experimental +# syntax=docker/dockerfile:1.4 +# Dockerfile for multi-stage build of Hyperlane's Rust components +# https://docs.docker.com/build/building/multi-stage/#use-multi-stage-builds +# https://depot.dev/docs/container-builds/how-to-guides/optimal-dockerfiles/rust-dockerfile -FROM rust:1.80.1 as builder -WORKDIR /usr/src - -# 1a: Prepare for static linking +# -------- Base Image with Tools -------- +# Base image containing all necessary build tools and dependencies +FROM rust:1.80.1 AS base RUN apt-get update && \ - apt-get dist-upgrade -y && \ apt-get install -y musl-tools clang && \ - rustup target add x86_64-unknown-linux-musl + rustup target add x86_64-unknown-linux-musl && \ + cargo install --locked sccache + +# Configure sccache for faster builds +# https://github.com/mozilla/sccache +ENV RUSTC_WRAPPER=sccache +ENV SCCACHE_DIR=/sccache -RUN mkdir -p rust/main -RUN mkdir -p rust/sealevel +# -------- Builder Stage -------- +# This stage compiles the Rust binaries +FROM base AS builder -# Add workspace to workdir -COPY rust/main/agents rust/main/agents -COPY rust/main/applications rust/main/applications -COPY rust/main/chains rust/main/chains -COPY rust/main/hyperlane-base rust/main/hyperlane-base -COPY rust/main/hyperlane-core rust/main/hyperlane-core -COPY rust/main/hyperlane-metric rust/main/hyperlane-metric -COPY rust/main/hyperlane-test rust/main/hyperlane-test -COPY rust/main/submitter rust/main/submitter -COPY rust/main/ethers-prometheus rust/main/ethers-prometheus -COPY rust/main/utils rust/main/utils -COPY rust/sealevel rust/sealevel +# Set up the workspace structure +WORKDIR /usr/src/rust/main -COPY rust/main/Cargo.toml rust/main/. -COPY rust/main/Cargo.lock rust/main/. +# Copy git metadata for version information +# Required by vergen for build-time git information +COPY .git ../../.git -# Required for VERGEN_GIT_SHA to be populated -COPY .git .git +# Copy all main workspace crates +# Each directory represents a different component of the Hyperlane system +COPY rust/main/agents ./agents +COPY rust/main/applications ./applications +COPY rust/main/chains ./chains +COPY rust/main/ethers-prometheus ./ethers-prometheus +COPY rust/main/hyperlane-base ./hyperlane-base +COPY rust/main/hyperlane-core ./hyperlane-core +COPY rust/main/hyperlane-metric ./hyperlane-metric +COPY rust/main/hyperlane-test ./hyperlane-test +COPY rust/main/submitter ./submitter +COPY rust/main/utils ./utils +COPY rust/main/Cargo.toml ./ +COPY rust/main/Cargo.lock ./ -WORKDIR /usr/src/rust/main +# Copy sealevel workspace into correct relative location +COPY rust/sealevel ../sealevel -# Build binaries -RUN \ - RUSTFLAGS="--cfg tokio_unstable" cargo build --release --bin validator --bin relayer --bin scraper && \ +# Build the release binaries with caching enabled +# Documentation: https://doc.rust-lang.org/cargo/commands/cargo-build.html +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ + RUSTFLAGS="--cfg tokio_unstable" \ + cargo build --release --bin validator --bin relayer --bin scraper && \ mkdir -p /release && \ - cp /usr/src/rust/main/target/release/validator /release && \ - cp /usr/src/rust/main/target/release/relayer /release && \ - cp /usr/src/rust/main/target/release/scraper /release + cp target/release/validator /release && \ + cp target/release/relayer /release && \ + cp target/release/scraper /release -## 2: Copy the binaries to release image +# -------- Runtime Image -------- +# Minimal runtime image containing config, binaries, and runtime dependencies FROM ubuntu:22.04 -RUN apt-get update && \ - apt-get install -y \ - openssl \ - ca-certificates \ - tini && \ - rm -rf /var/lib/apt/lists/* - WORKDIR /app -RUN mkdir -p /app/config COPY rust/main/config /app/config COPY --from=builder /release/* . -RUN chmod 777 /app && \ - mkdir /usr/share/hyperlane/ && \ - chmod 1000 /usr/share/hyperlane && \ - mkdir /data/ && \ - chown -R 1000 /data/ +# Install runtime dependencies +# remove /var/lib/apt/lists/* to clean up the package lists +RUN apt-get update && \ + apt-get install -y openssl ca-certificates tini && \ + rm -rf /var/lib/apt/lists/* && \ + chmod 777 /app && \ + mkdir -p /usr/share/hyperlane && chmod 1000 /usr/share/hyperlane && \ + mkdir -p /data && chown -R 1000 /data +# Run as non-root user for security +# Use tini as init system for proper process management USER 1000 ENTRYPOINT ["tini", "--"] CMD ["./validator"] diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index 4ff5e8bb49b..18323263f60 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -383,13 +383,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-mutex" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" dependencies = [ - "event-listener", + "event-listener 2.5.3", ] [[package]] @@ -399,7 +410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "261803dcc39ba9e72760ba6e16d0199b1eef9fc44e81bffabbebb9f5aea3906c" dependencies = [ "async-mutex", - "event-listener", + "event-listener 2.5.3", ] [[package]] @@ -510,6 +521,438 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-config" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b96342ea8948ab9bef3e6234ea97fc32e2d8a88d8fb6a084e52267317f94b6b" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http 0.60.12", + "aws-smithy-json 0.60.7", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex 0.4.3", + "http 0.2.12", + "hyper 0.14.30", + "ring 0.17.8", + "time", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4471bef4c22a06d2c7a1b6492493d3fdf24a805323109d6874f9c94d5906ac14" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-runtime" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aff45ffe35196e593ea3b9dd65b320e51e2dda95aff4390bc459e461d09c6ad" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http 0.62.0", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid 1.11.0", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3ba2c5c0f2618937ce3d4a5ad574b86775576fa24006bcb3128c6e2cbf3c34e" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http 0.60.12", + "aws-smithy-json 0.61.3", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex 0.4.3", + "hmac 0.12.1", + "http 0.2.12", + "http-body 0.4.6", + "lru", + "once_cell", + "percent-encoding", + "regex-lite", + "sha2 0.10.8", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ca43a4ef210894f93096039ef1d6fa4ad3edfabb3be92b80908b9f2e4b4eab" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.60.12", + "aws-smithy-json 0.61.3", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fea2f3a8bb3bd10932ae7ad59cc59f65f270fc9183a7e91f501dc5efbef7ee" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.60.12", + "aws-smithy-json 0.60.7", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ada54e5f26ac246dc79727def52f7f8ed38915cb47781e2a72213957dc3a7d5" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.60.12", + "aws-smithy-json 0.60.7", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d03c3c05ff80d54ff860fe38c726f6f494c639ae975203a101335f223386db" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http 0.62.0", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex 0.4.3", + "hmac 0.12.1", + "http 0.2.12", + "http 1.2.0", + "once_cell", + "p256 0.11.1", + "percent-encoding", + "ring 0.17.8", + "sha2 0.10.8", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795" +dependencies = [ + "aws-smithy-http 0.60.12", + "aws-smithy-types", + "bytes", + "crc32c", + "crc32fast", + "hex 0.4.3", + "http 0.2.12", + "http-body 0.4.6", + "md-5 0.10.6", + "pin-project-lite", + "sha1", + "sha2 0.10.8", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c45d3dddac16c5c59d553ece225a88870cf81b7b813c9cc17b78cf4685eac7a" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7809c27ad8da6a6a68c454e651d4962479e81472aa19ae99e59f9aba1f9713cc" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5949124d11e538ca21142d1fba61ab0a2a2c1bc3ed323cdb3e4b878bfb83166" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.2.0", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aff1159006441d02e57204bf57a1b890ba68bedb6904ffd2873c1c4c11c546b" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.4.7", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-rustls", + "pin-project-lite", + "rustls 0.21.12", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445d065e76bc1ef54963db400319f1dd3ebb3e0a74af20f7f7630625b0cc7cc0" +dependencies = [ + "aws-smithy-runtime-api", + "once_cell", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0152749e17ce4d1b47c7747bdfec09dac1ccafdcbc741ebf9daa2a373356730f" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http 0.62.0", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.2.0", + "http-body 0.4.6", + "http-body 1.0.1", + "once_cell", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da37cf5d57011cb1753456518ec76e31691f1f474b73934a284eb2a1c76510f" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.2.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836155caafba616c0ff9b07944324785de2ab016141c3550bd1c07882f8cee8f" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.2.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3873f8deed8927ce8d04487630dc9ff73193bab64742a61d050e57a68dec4125" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.6.20" @@ -708,6 +1151,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -1097,13 +1550,23 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "bzip2-sys" version = "0.1.11+1.0.8" @@ -1442,6 +1905,15 @@ dependencies = [ "unreachable", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.13.4" @@ -1685,7 +2157,7 @@ dependencies = [ "ed25519-zebra 4.0.3", "k256 0.13.4", "num-traits", - "p256", + "p256 0.13.2", "rand_core 0.6.4", "rayon", "sha2 0.10.8", @@ -1833,6 +2305,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -2250,6 +2731,19 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -2991,7 +3485,7 @@ dependencies = [ [[package]] name = "ethers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -3005,7 +3499,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "ethers-core", "once_cell", @@ -3016,7 +3510,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", @@ -3034,7 +3528,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "Inflector", "cfg-if", @@ -3058,7 +3552,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "ethers-contract-abigen", "ethers-core", @@ -3072,7 +3566,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "arrayvec", "bytes", @@ -3102,7 +3596,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "ethers-core", "getrandom 0.2.15", @@ -3118,7 +3612,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "async-trait", "auto_impl 0.5.0", @@ -3127,6 +3621,7 @@ dependencies = [ "ethers-etherscan", "ethers-providers", "ethers-signers", + "eyre", "futures-locks", "futures-util", "instant", @@ -3168,7 +3663,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "async-trait", "auto_impl 1.2.0", @@ -3204,7 +3699,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-02-03#a6cd47a09f4ba16f7ac12242d598b6ac6a328694" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2025-04-18#bae8199df9d2a02b0393bedfde1e5114d5badb80" dependencies = [ "async-trait", "coins-bip32 0.7.0", @@ -3234,6 +3729,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "eventsource-client" version = "0.12.2" @@ -3268,9 +3784,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "feature-probe" @@ -3375,6 +3891,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -3589,7 +4111,7 @@ dependencies = [ "fuel-types", "k256 0.13.4", "lazy_static", - "p256", + "p256 0.13.2", "rand 0.8.5", "secp256k1", "serde", @@ -3872,9 +4394,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -4156,6 +4678,17 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashers" version = "1.0.1" @@ -4460,9 +4993,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -4514,7 +5047,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", @@ -4545,7 +5078,7 @@ dependencies = [ "futures-util", "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.2", + "hyper 1.6.0", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -4568,15 +5101,20 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "aws-config", + "aws-sdk-s3", "axum 0.6.20", "backtrace", "backtrace-oneline", "bs58 0.5.1", + "chrono", "color-eyre", "config", "console-subscriber", "convert_case 0.6.0", + "dashmap", "derive-new", + "derive_builder", "ed25519-dalek 1.0.1", "ethers", "ethers-prometheus", @@ -4596,13 +5134,13 @@ dependencies = [ "itertools 0.12.1", "maplit", "mockall", + "moka", "paste", "prometheus", "reqwest", "rocksdb", "rusoto_core", "rusoto_kms", - "rusoto_s3", "rusoto_sts", "serde", "serde_json", @@ -4795,6 +5333,7 @@ version = "0.1.0" dependencies = [ "abigen", "async-trait", + "dashmap", "derive-new", "ethers", "ethers-contract", @@ -5603,6 +6142,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.2", +] + [[package]] name = "lz4-sys" version = "1.10.0" @@ -5805,6 +6353,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener 5.3.1", + "futures-util", + "once_cell", + "parking_lot 0.12.3", + "quanta", + "rustc_version", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid 1.11.0", +] + [[package]] name = "multer" version = "2.1.0" @@ -6172,9 +6744,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" @@ -6296,6 +6868,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "overload" version = "0.1.1" @@ -6308,6 +6886,17 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.8", +] + [[package]] name = "p256" version = "0.13.2" @@ -6346,6 +6935,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.11.2" @@ -6986,6 +7581,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "quanta" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773ce68d0bb9bc7ef20be3536ffe94e223e1f365bd374108b2659fac0c65cfe6" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quinn" version = "0.8.5" @@ -7177,6 +7787,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -7270,6 +7889,12 @@ dependencies = [ "regex-syntax 0.8.4", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -7311,9 +7936,11 @@ dependencies = [ "hyperlane-base", "hyperlane-core", "hyperlane-ethereum", + "hyperlane-metric", "hyperlane-operation-verifier", "hyperlane-test", "itertools 0.12.1", + "maplit", "mockall", "num-derive 0.4.2", "num-traits", @@ -7325,12 +7952,15 @@ dependencies = [ "serde", "serde_json", "strum 0.26.3", + "submitter", + "tempfile", "thiserror", "tokio", "tokio-metrics", "tokio-test", "tracing", "tracing-futures", + "tracing-subscriber", "tracing-test", "typetag", "uuid 1.11.0", @@ -7670,19 +8300,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "rusoto_s3" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aae4677183411f6b0b412d66194ef5403293917d66e70ab118f07cc24c5b14d" -dependencies = [ - "async-trait", - "bytes", - "futures", - "rusoto_core", - "xml-rs", -] - [[package]] name = "rusoto_signature" version = "0.48.0" @@ -7825,9 +8442,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ "log", "once_cell", @@ -7891,9 +8508,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" @@ -8375,9 +8992,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.209" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -8430,9 +9047,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2 1.0.93", "quote 1.0.37", @@ -9488,7 +10105,7 @@ dependencies = [ "dirs", "dotenvy", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -9685,8 +10302,10 @@ dependencies = [ "solana-sdk", "solana-transaction-status", "tempfile", + "thiserror", "tokio", "tracing", + "tracing-subscriber", "uuid 1.11.0", ] @@ -9812,6 +10431,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tai64" version = "4.0.0" @@ -10190,7 +10815,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.20", + "rustls 0.23.19", "tokio", ] @@ -10359,7 +10984,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-timeout 0.5.2", "hyper-util", "percent-encoding", @@ -10539,6 +11164,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "try-lock" version = "0.2.5" @@ -10782,6 +11413,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -10813,6 +11450,7 @@ name = "validator" version = "0.1.0" dependencies = [ "async-trait", + "aws-config", "axum 0.6.20", "chrono", "config", @@ -10828,10 +11466,12 @@ dependencies = [ "hyperlane-cosmos", "hyperlane-ethereum", "hyperlane-test", + "itertools 0.10.5", "k256 0.13.4", "mockall", "prometheus", "reqwest", + "rusoto_core", "serde", "serde_json", "thiserror", @@ -10883,6 +11523,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -11375,6 +12021,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "ya-gcp" version = "0.11.3" diff --git a/rust/main/Cargo.toml b/rust/main/Cargo.toml index 20a0564d6de..70428cc8fcd 100644 --- a/rust/main/Cargo.toml +++ b/rust/main/Cargo.toml @@ -14,6 +14,7 @@ members = [ "ethers-prometheus", "hyperlane-base", "hyperlane-core", + "hyperlane-metric", "hyperlane-test", "submitter", "utils/abigen", @@ -22,6 +23,7 @@ members = [ "utils/hex", "utils/run-locally", ] +resolver = "2" [workspace.package] documentation = "https://docs.hyperlane.xyz" @@ -37,6 +39,12 @@ anyhow = "1.0" async-trait = "0.1" async-rwlock = "1.3" auto_impl = "1.0" +aws-config = { version = "1.1.7", features = ["behavior-version-latest"] } +# AWS deps are pinned to be compatible with rustc 1.80.1 +aws-sdk-s3 = "=1.65.0" +aws-sdk-sso = "=1.50.0" +aws-sdk-ssooidc = "=1.50.0" +aws-sdk-sts = "=1.50.0" axum = "0.6.1" backtrace = "0.3" base64 = "0.21.2" @@ -61,6 +69,7 @@ cosmwasm-std = "*" crunchy = "0.2" ctrlc = "3.2" curve25519-dalek = { version = "~3.2", features = ["serde"] } +dashmap = "5" derive-new = "0.5" derive_builder = "0.12" derive_more = "0.99" @@ -84,6 +93,7 @@ hyper-tls = "0.5.0" hyperlane-cosmwasm-interface = "=0.0.6-rc6" ibc-proto = "0.51.1" injective-protobuf = "0.2.2" +moka = "0.12.8" injective-std = "1.13.6" itertools = "*" jobserver = "=0.1.26" @@ -208,27 +218,27 @@ overflow-checks = true [workspace.dependencies.ethers] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2025-02-03" +tag = "2025-04-18" [workspace.dependencies.ethers-contract] features = ["legacy"] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2025-02-03" +tag = "2025-04-18" [workspace.dependencies.ethers-core] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2025-02-03" +tag = "2025-04-18" [workspace.dependencies.ethers-providers] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2025-02-03" +tag = "2025-04-18" [workspace.dependencies.ethers-signers] features = ["aws"] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2025-02-03" +tag = "2025-04-18" [patch.crates-io.curve25519-dalek] branch = "v3.2.2-relax-zeroize" diff --git a/rust/main/agents/relayer/Cargo.toml b/rust/main/agents/relayer/Cargo.toml index 41c86820617..b37ff4c9bec 100644 --- a/rust/main/agents/relayer/Cargo.toml +++ b/rust/main/agents/relayer/Cargo.toml @@ -25,6 +25,7 @@ eyre.workspace = true futures.workspace = true futures-util.workspace = true itertools.workspace = true +maplit.workspace = true num-derive.workspace = true num-traits.workspace = true prometheus.workspace = true @@ -53,7 +54,9 @@ hyperlane-core = { path = "../../hyperlane-core", features = [ "async", ] } hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" } +hyperlane-metric = { path = "../../hyperlane-metric" } hyperlane-operation-verifier = { path = "../../applications/hyperlane-operation-verifier" } +submitter = { path = "../../submitter" } [dev-dependencies] axum = { workspace = true, features = ["macros"] } @@ -61,10 +64,12 @@ once_cell.workspace = true mockall.workspace = true tokio-test.workspace = true tracing-test.workspace = true +tracing-subscriber.workspace = true hyperlane-test = { path = "../../hyperlane-test" } hyperlane-base = { path = "../../hyperlane-base", features = ["test-utils"] } hyperlane-core = { path = "../../hyperlane-core", features = ["agent", "async", "test-utils"] } ethers-prometheus = { path = "../../ethers-prometheus", features = ["serde"] } +tempfile.workspace = true [features] default = ["color-eyre", "oneline-errors"] diff --git a/rust/main/agents/relayer/src/lib.rs b/rust/main/agents/relayer/src/lib.rs index 8b46b7e5a6e..54c051b418d 100644 --- a/rust/main/agents/relayer/src/lib.rs +++ b/rust/main/agents/relayer/src/lib.rs @@ -1,6 +1,7 @@ pub mod msg; mod merkle_tree; +mod metrics; mod processor; mod prover; mod relayer; diff --git a/rust/main/agents/relayer/src/metrics.rs b/rust/main/agents/relayer/src/metrics.rs new file mode 100644 index 00000000000..e48fb809b5b --- /dev/null +++ b/rust/main/agents/relayer/src/metrics.rs @@ -0,0 +1 @@ +pub mod message_submission; diff --git a/rust/main/agents/relayer/src/metrics/message_submission.rs b/rust/main/agents/relayer/src/metrics/message_submission.rs new file mode 100644 index 00000000000..64700718a5e --- /dev/null +++ b/rust/main/agents/relayer/src/metrics/message_submission.rs @@ -0,0 +1,78 @@ +use std::time::Duration; + +use maplit::hashmap; +use prometheus::{CounterVec, IntCounter, IntCounterVec, IntGauge}; + +use hyperlane_base::CoreMetrics; +use hyperlane_core::{HyperlaneDomain, HyperlaneMessage}; + +#[derive(Clone, Debug)] +pub struct MetadataBuildMetric { + pub app_context: Option, + pub success: bool, + pub duration: Duration, +} + +#[derive(Debug)] +pub struct MessageSubmissionMetrics { + // Origin and destination chain names + pub origin: String, + pub destination: String, + + // Fields are public for testing purposes + pub last_known_nonce: IntGauge, + pub messages_processed: IntCounter, + + /// Number of times we've built metadata + pub metadata_build_count: IntCounterVec, + /// Total number of seconds spent building different types of metadata. + pub metadata_build_duration: CounterVec, +} + +impl MessageSubmissionMetrics { + pub fn new( + metrics: &CoreMetrics, + origin: &HyperlaneDomain, + destination: &HyperlaneDomain, + ) -> Self { + let origin = origin.name(); + let destination = destination.name(); + Self { + origin: origin.to_string(), + destination: destination.to_string(), + last_known_nonce: metrics.last_known_message_nonce().with_label_values(&[ + "message_processed", + origin, + destination, + ]), + messages_processed: metrics + .messages_processed_count() + .with_label_values(&[origin, destination]), + metadata_build_count: metrics.metadata_build_count(), + metadata_build_duration: metrics.metadata_build_duration(), + } + } + + pub fn update_nonce(&self, msg: &HyperlaneMessage) { + // this is technically a race condition between `.get` and `.set` but worst case + // the gauge should get corrected on the next update and is not an issue + // with a ST runtime + self.last_known_nonce + .set(std::cmp::max(self.last_known_nonce.get(), msg.nonce as i64)); + } + + /// Add metrics on how long metadata building took for + /// a specific ISM + pub fn insert_metadata_build_metric(&self, params: MetadataBuildMetric) { + let labels = hashmap! { + "app_context" => params.app_context.as_deref().unwrap_or("Unknown"), + "origin" => self.origin.as_str(), + "remote" => self.destination.as_str(), + "status" => if params.success { "success" } else { "failure" }, + }; + self.metadata_build_count.with(&labels).inc(); + self.metadata_build_duration + .with(&labels) + .inc_by(params.duration.as_secs_f64()); + } +} diff --git a/rust/main/agents/relayer/src/msg/metadata/aggregation.rs b/rust/main/agents/relayer/src/msg/metadata/aggregation.rs index 8560586f4e3..e5df0ccffba 100644 --- a/rust/main/agents/relayer/src/msg/metadata/aggregation.rs +++ b/rust/main/agents/relayer/src/msg/metadata/aggregation.rs @@ -5,12 +5,17 @@ use futures_util::future::join_all; use derive_new::new; use itertools::{Either, Itertools}; use tracing::{info, instrument}; +use {hyperlane_base::cache::FunctionCallCache, tracing::warn}; -use hyperlane_core::{HyperlaneMessage, InterchainSecurityModule, ModuleType, H256, U256}; +use hyperlane_core::{ + AggregationIsm, HyperlaneMessage, InterchainSecurityModule, ModuleType, H256, U256, +}; use crate::msg::metadata::{base::MetadataBuildError, message_builder}; -use super::{MessageMetadataBuildParams, MessageMetadataBuilder, Metadata, MetadataBuilder}; +use super::{ + IsmCachePolicy, MessageMetadataBuildParams, MessageMetadataBuilder, Metadata, MetadataBuilder, +}; /// Bytes used to store one member of the (start, end) range tuple /// Copied from `AggregationIsmMetadata.sol` @@ -116,6 +121,135 @@ impl AggregationIsmMetadataBuilder { } Ok(Self::n_cheapest_metas(metas_and_gas, threshold)) } + + /// Returns modules and threshold from the aggregation ISM. + /// This method will attempt to get the value from cache first. If it is a cache miss, + /// it will request it from the ISM contract. The result will be cached for future use. + /// + /// Implicit contract in this method: function name `modules_and_threshold` matches + /// the name of the method `modules_and_threshold`. + async fn call_modules_and_threshold( + &self, + ism: Box, + message: &HyperlaneMessage, + ) -> Result<(Vec, u8), MetadataBuildError> { + let ism_domain = ism.domain().name(); + let fn_key = "modules_and_threshold"; + + // Depending on the cache policy, make use of the message ID + let params_cache_key = match self + .base_builder() + .ism_cache_policy_classifier() + .get_cache_policy( + self.root_ism, + ism.domain(), + ModuleType::Aggregation, + self.base.app_context.as_ref(), + ) + .await + { + // To have the cache key be more succinct, we use the message id + IsmCachePolicy::MessageSpecific => (ism.address(), message.id()), + IsmCachePolicy::IsmSpecific => (ism.address(), H256::zero()), + }; + + let cache_result = self + .base_builder() + .cache() + .get_cached_call_result::<(Vec, u8)>(ism_domain, fn_key, ¶ms_cache_key) + .await + .map_err(|err| { + warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok() + .flatten(); + + match cache_result { + Some(result) => Ok(result), + None => { + let result = ism + .modules_and_threshold(message) + .await + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + + self.base_builder() + .cache() + .cache_call_result(ism_domain, fn_key, ¶ms_cache_key, &result) + .await + .map_err(|err| { + warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok(); + Ok(result) + } + } + } + + async fn try_build_fast_path( + &self, + message: &HyperlaneMessage, + params: MessageMetadataBuildParams, + threshold: usize, + ism_addresses: Vec, + ) -> Result { + if threshold > 1 { + return Err(MetadataBuildError::FastPathError( + "Aggregation ISM threshold > 1".to_string(), + )); + } + let sub_isms = join_all(ism_addresses.iter().map(|sub_ism_address| { + message_builder::ism_and_module_type(self.base.clone(), *sub_ism_address) + })) + .await; + let (message_id_multisig_ism_index, message_id_multisig_ism) = sub_isms + .into_iter() + .enumerate() + .find_map(|(index, ism)| { + if let Ok((ism, ModuleType::MessageIdMultisig)) = ism { + Some((index, ism)) + } else { + None + } + }) + .ok_or(MetadataBuildError::FastPathError( + "No MessageIdMultisigIsm submodule in aggregation ISM".to_string(), + ))?; + let message_id_multisig_ism_address = ism_addresses + .get(message_id_multisig_ism_index) + .ok_or(MetadataBuildError::FastPathError(format!( + "No ism address found for messageIdMultisig index {}", + message_id_multisig_ism_index + )))?; + let sub_module_and_meta = message_builder::build_message_metadata( + self.base.clone(), + *message_id_multisig_ism_address, + message, + params.clone(), + Some((message_id_multisig_ism, ModuleType::MessageIdMultisig)), + ) + .await?; + + let metadata = sub_module_and_meta.metadata.to_vec(); + + // return an error if delivering with this metadata fails + if sub_module_and_meta + .ism + .dry_run_verify(message, &metadata) + .await + .map_err(|err| MetadataBuildError::FastPathError(err.to_string()))? + .is_none() + { + return Err(MetadataBuildError::FastPathError( + "Fast path metadata failed dry run (returned None)".to_string(), + )); + } + let sub_module_metadata = SubModuleMetadata::new(message_id_multisig_ism_index, metadata); + let metadata = Metadata::new(Self::format_metadata( + &mut [sub_module_metadata], + ism_addresses.len(), + )); + Ok(metadata) + } } #[async_trait] @@ -133,19 +267,34 @@ impl MetadataBuilder for AggregationIsmMetadataBuilder { .build_aggregation_ism(ism_address) .await .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; - let (ism_addresses, threshold) = ism - .modules_and_threshold(message) - .await - .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + + let (ism_addresses, threshold) = self.call_modules_and_threshold(ism, message).await?; let threshold = threshold as usize; - let sub_modules_and_metas = join_all(ism_addresses.iter().map(|ism_address| { + match self + .try_build_fast_path(message, params.clone(), threshold, ism_addresses.clone()) + .await + { + Ok(metadata) => { + info!("Built metadata using fast path"); + return Ok(metadata); + } + Err(err) => { + warn!( + ?err, + "Fast path failed, falling back to the other submodules in the aggregation ISM" + ); + } + } + + let sub_modules_and_metas = join_all(ism_addresses.iter().map(|sub_ism_address| { message_builder::build_message_metadata( self.base.clone(), - *ism_address, + *sub_ism_address, message, params.clone(), + None, ) })) .await; @@ -158,7 +307,7 @@ impl MetadataBuilder for AggregationIsmMetadataBuilder { } // Partitions things into - // 1. ok_sub_modules: ISMs with metadata with valid metadata + // 1. ok_sub_modules: ISMs with valid metadata // 2. err_sub_modules: ISMs with invalid metadata let (ok_sub_modules, err_sub_modules): (Vec<_>, Vec<_>) = sub_modules_and_metas .into_iter() diff --git a/rust/main/agents/relayer/src/msg/metadata/base.rs b/rust/main/agents/relayer/src/msg/metadata/base.rs index af2f7b94afd..3fe4284a1db 100644 --- a/rust/main/agents/relayer/src/msg/metadata/base.rs +++ b/rust/main/agents/relayer/src/msg/metadata/base.rs @@ -2,6 +2,7 @@ #![allow(clippy::unnecessary_get_then_check)] // TODO: `rustc` 1.80.1 clippy issue use std::{ + collections::HashSet, fmt::Debug, sync::Arc, time::{Duration, Instant}, @@ -9,9 +10,13 @@ use std::{ use derive_new::new; use eyre::Result; +use num_traits::cast::FromPrimitive; +use serde::{Deserialize, Deserializer}; use tokio::sync::{Mutex, RwLock}; -use hyperlane_core::{HyperlaneMessage, InterchainSecurityModule, Mailbox, ModuleType, H256}; +use hyperlane_core::{ + HyperlaneDomain, HyperlaneMessage, InterchainSecurityModule, Mailbox, ModuleType, H256, +}; use crate::settings::matching_list::MatchingList; @@ -33,8 +38,12 @@ pub enum MetadataBuildError { MaxIsmDepthExceeded(u32), #[error("Exceeded max count when building metadata ({0})")] MaxIsmCountReached(u32), + #[error("Exceeded max validator count when building metadata ({0})")] + MaxValidatorCountReached(u32), #[error("Aggregation threshold not met ({0})")] AggregationThresholdNotMet(u32), + #[error("Fast path error ({0})")] + FastPathError(String), } #[derive(Clone, Debug, new)] @@ -79,9 +88,9 @@ pub struct IsmWithMetadataAndType { /// Allows fetching the default ISM, caching the value for a period of time /// to avoid fetching it all the time. /// TODO: make this generic -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct DefaultIsmCache { - value: RwLock>, + value: Arc>>, mailbox: Arc, } @@ -91,7 +100,7 @@ impl DefaultIsmCache { pub fn new(mailbox: Arc) -> Self { Self { - value: RwLock::new(None), + value: Arc::new(RwLock::new(None)), mailbox, } } @@ -128,17 +137,17 @@ impl DefaultIsmCache { #[derive(Debug)] pub struct IsmAwareAppContextClassifier { - default_ism: DefaultIsmCache, + default_ism_getter: DefaultIsmCache, app_context_classifier: AppContextClassifier, } impl IsmAwareAppContextClassifier { pub fn new( - destination_mailbox: Arc, + default_ism_getter: DefaultIsmCache, app_matching_lists: Vec<(MatchingList, String)>, ) -> Self { Self { - default_ism: DefaultIsmCache::new(destination_mailbox), + default_ism_getter, app_context_classifier: AppContextClassifier::new(app_matching_lists), } } @@ -152,7 +161,7 @@ impl IsmAwareAppContextClassifier { return Ok(Some(app_context)); } - if root_ism == self.default_ism.get().await? { + if root_ism == self.default_ism_getter.get().await? { return Ok(Some("default_ism".to_string())); } @@ -187,3 +196,359 @@ impl AppContextClassifier { Ok(None) } } + +/// An ISM caching policy. +#[derive(Copy, Clone, Debug, PartialEq, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum IsmCachePolicy { + /// Default cache policy, includes the message in the cache key + /// when querying config that may be message-specific. + /// This is the default because it makes the fewest assumptions + /// about the mutability of an ISM's config. + #[default] + MessageSpecific, + /// Even if an ISM's config interface is message-specific, we + /// ignore the message and use the same config for all messages. + IsmSpecific, +} + +#[derive(Debug, Clone, Default, Deserialize, PartialEq)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum IsmCacheSelector { + #[default] + DefaultIsm, + AppContext { + context: String, + }, +} + +/// Configuration for ISM caching behavior. +/// Fields are renamed to be all lowercase / without underscores to match +/// the format expected by the settings parsing. +#[derive(Debug, Clone, Default, Deserialize)] +pub struct IsmCacheConfig { + selector: IsmCacheSelector, + #[serde(deserialize_with = "deserialize_module_types", rename = "moduletypes")] + module_types: HashSet, + chains: Option>, + #[serde(default, rename = "cachepolicy")] + cache_policy: IsmCachePolicy, +} + +/// To deserialize the module types from a list of numbers +/// into a set of `ModuleType` enums. +fn deserialize_module_types<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let nums: Vec = Vec::deserialize(deserializer)?; + let mut set = HashSet::new(); + for num in nums { + let module = ModuleType::from_u8(num).ok_or_else(|| { + serde::de::Error::custom(format!("Invalid module type value: {}", num)) + })?; + set.insert(module); + } + Ok(set) +} + +impl IsmCacheConfig { + fn matches_chain(&self, domain_name: &str) -> bool { + if let Some(chains) = &self.chains { + chains.contains(domain_name) + } else { + // If no domains are specified, match all domains + true + } + } + + fn matches_module_type(&self, module_type: ModuleType) -> bool { + self.module_types.contains(&module_type) + } +} + +/// Classifies messages into an ISM cache policy based on the +/// default ISM and the configured cache policy. +#[derive(Debug, new)] +pub struct IsmCachePolicyClassifier { + default_ism_getter: DefaultIsmCache, + ism_cache_configs: Vec, +} + +impl IsmCachePolicyClassifier { + /// Returns the cache policy for the given app context. + pub async fn get_cache_policy( + &self, + root_ism: H256, + domain: &HyperlaneDomain, + ism_module_type: ModuleType, + app_context: Option<&String>, + ) -> IsmCachePolicy { + for config in &self.ism_cache_configs { + let matches_module = match &config.selector { + IsmCacheSelector::DefaultIsm => { + let default_ism = match self.default_ism_getter.get().await { + Ok(default_ism) => default_ism, + Err(err) => { + tracing::warn!(?err, "Error fetching default ISM for ISM cache policy, attempting next config"); + continue; + } + }; + root_ism == default_ism + } + IsmCacheSelector::AppContext { + context: selector_app_context, + } => app_context.map_or(false, |app_context| app_context == selector_app_context), + }; + + if matches_module + && config.matches_chain(domain.name()) + && config.matches_module_type(ism_module_type) + { + tracing::trace!( + ?domain, + ism_cache_config =? config, + "Using configured default ISM cache policy" + ); + return config.cache_policy; + } + } + + IsmCachePolicy::default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hyperlane_test::mocks::MockMailboxContract; + + #[test] + fn test_ism_cache_config() { + let config = IsmCacheConfig { + selector: IsmCacheSelector::DefaultIsm, + module_types: HashSet::from([ModuleType::Aggregation]), + chains: Some(HashSet::from(["foochain".to_owned()])), + cache_policy: IsmCachePolicy::IsmSpecific, + }; + + assert_eq!(config.matches_chain("foochain"), true); + assert_eq!(config.matches_chain("barchain"), false); + + assert_eq!(config.matches_module_type(ModuleType::Aggregation), true); + assert_eq!(config.matches_module_type(ModuleType::Routing), false); + } + + #[test] + fn test_ism_cache_config_deserialize() { + // Module type 2 is the numeric version of ModuleType::Aggregation + let json = r#" + { + "selector": { + "type": "defaultIsm" + }, + "moduletypes": [2], + "chains": ["foochain"], + "cachepolicy": "ismSpecific" + } + "#; + let config: IsmCacheConfig = serde_json::from_str(json).unwrap(); + + assert_eq!(config.selector, IsmCacheSelector::DefaultIsm); + assert_eq!( + config.module_types, + HashSet::from([ModuleType::Aggregation]) + ); + assert_eq!(config.chains, Some(HashSet::from(["foochain".to_owned()]))); + assert_eq!(config.cache_policy, IsmCachePolicy::IsmSpecific); + + let json = r#" + { + "selector": { + "type": "appContext", + "context": "foo" + }, + "moduletypes": [2], + "chains": ["foochain"], + "cachepolicy": "ismSpecific" + } + "#; + let config: IsmCacheConfig = serde_json::from_str(json).unwrap(); + assert_eq!( + config.selector, + IsmCacheSelector::AppContext { + context: "foo".to_string(), + }, + ); + } + + #[tokio::test] + async fn test_ism_cache_policy_classifier_default_ism() { + let default_ism = H256::zero(); + + let mock_mailbox = MockMailboxContract::new_with_default_ism(default_ism); + let mailbox: Arc = Arc::new(mock_mailbox); + + let default_ism_getter = DefaultIsmCache::new(mailbox); + let default_ism_cache_config = IsmCacheConfig { + selector: IsmCacheSelector::DefaultIsm, + module_types: HashSet::from([ModuleType::Aggregation]), + chains: Some(HashSet::from(["foochain".to_owned()])), + cache_policy: IsmCachePolicy::IsmSpecific, + }; + + let classifier = + IsmCachePolicyClassifier::new(default_ism_getter, vec![default_ism_cache_config]); + + // We meet the criteria for the cache policy + let domain = HyperlaneDomain::new_test_domain("foochain"); + let cache_policy = classifier + .get_cache_policy(default_ism, &domain, ModuleType::Aggregation, None) + .await; + assert_eq!(cache_policy, IsmCachePolicy::IsmSpecific); + + // Different ISM module type, should not match + let cache_policy = classifier + .get_cache_policy(default_ism, &domain, ModuleType::Routing, None) + .await; + assert_eq!(cache_policy, IsmCachePolicy::MessageSpecific); + + // ISM not default ISM, should not match + let cache_policy = classifier + .get_cache_policy(H256::repeat_byte(0xfe), &domain, ModuleType::Routing, None) + .await; + assert_eq!(cache_policy, IsmCachePolicy::MessageSpecific); + + // Different domain, should not match + let domain = HyperlaneDomain::new_test_domain("barchain"); + let cache_policy = classifier + .get_cache_policy(default_ism, &domain, ModuleType::Routing, None) + .await; + assert_eq!(cache_policy, IsmCachePolicy::MessageSpecific); + } + + #[tokio::test] + async fn test_ism_cache_policy_classifier_app_context() { + let default_ism = H256::zero(); + let mock_mailbox = MockMailboxContract::new_with_default_ism(default_ism); + let mailbox: Arc = Arc::new(mock_mailbox); + // Unused for this test + let default_ism_getter = DefaultIsmCache::new(mailbox); + + let app_context_cache_config = IsmCacheConfig { + selector: IsmCacheSelector::AppContext { + context: "foo".to_string(), + }, + module_types: HashSet::from([ModuleType::Aggregation]), + chains: Some(HashSet::from(["foochain".to_owned()])), + cache_policy: IsmCachePolicy::IsmSpecific, + }; + + let classifier = + IsmCachePolicyClassifier::new(default_ism_getter, vec![app_context_cache_config]); + + // We meet the criteria for the cache policy + let domain = HyperlaneDomain::new_test_domain("foochain"); + let cache_policy = classifier + .get_cache_policy( + // To make extra sure we're testing the app context match, + // let's use a different ISM address + H256::repeat_byte(0xfe), + &domain, + ModuleType::Aggregation, + Some(&"foo".to_string()), + ) + .await; + assert_eq!(cache_policy, IsmCachePolicy::IsmSpecific); + + // Different app context, should not match + let cache_policy = classifier + .get_cache_policy( + default_ism, + &domain, + ModuleType::Routing, + Some(&"bar".to_string()), + ) + .await; + assert_eq!(cache_policy, IsmCachePolicy::MessageSpecific); + + // No app context, should not match + let cache_policy = classifier + .get_cache_policy(H256::repeat_byte(0xfe), &domain, ModuleType::Routing, None) + .await; + assert_eq!(cache_policy, IsmCachePolicy::MessageSpecific); + + // Different domain, should not match + let domain = HyperlaneDomain::new_test_domain("barchain"); + let cache_policy = classifier + .get_cache_policy( + default_ism, + &domain, + ModuleType::Routing, + Some(&"foo".to_string()), + ) + .await; + assert_eq!(cache_policy, IsmCachePolicy::MessageSpecific); + } + + #[tokio::test] + async fn test_ism_cache_policy_classifier_multiple_policies() { + let default_ism = H256::zero(); + let mock_mailbox = MockMailboxContract::new_with_default_ism(default_ism); + let mailbox: Arc = Arc::new(mock_mailbox); + // Unused for this test + let default_ism_getter = DefaultIsmCache::new(mailbox); + + let app_context_cache_config = IsmCacheConfig { + selector: IsmCacheSelector::AppContext { + context: "foo".to_string(), + }, + module_types: HashSet::from([ModuleType::Aggregation]), + chains: Some(HashSet::from(["foochain".to_owned()])), + cache_policy: IsmCachePolicy::IsmSpecific, + }; + + let default_ism_cache_config = IsmCacheConfig { + selector: IsmCacheSelector::DefaultIsm, + module_types: HashSet::from([ModuleType::Routing]), + chains: Some(HashSet::from(["foochain".to_owned()])), + cache_policy: IsmCachePolicy::IsmSpecific, + }; + + let classifier = IsmCachePolicyClassifier::new( + default_ism_getter, + vec![app_context_cache_config, default_ism_cache_config], + ); + + // We meet the criteria for the app context cache policy + let domain = HyperlaneDomain::new_test_domain("foochain"); + let cache_policy = classifier + .get_cache_policy( + // To make extra sure we're testing the app context match, + // let's use a different ISM address + H256::repeat_byte(0xfe), + &domain, + ModuleType::Aggregation, + Some(&"foo".to_string()), + ) + .await; + assert_eq!(cache_policy, IsmCachePolicy::IsmSpecific); + + // We meet the criteria for the default ISM cache policy + let cache_policy = classifier + .get_cache_policy(default_ism, &domain, ModuleType::Routing, None) + .await; + assert_eq!(cache_policy, IsmCachePolicy::IsmSpecific); + + // Different app context and not default ISM, should not match + let cache_policy = classifier + .get_cache_policy( + H256::repeat_byte(0xfe), + &domain, + ModuleType::Routing, + Some(&"bar".to_string()), + ) + .await; + assert_eq!(cache_policy, IsmCachePolicy::MessageSpecific); + } +} diff --git a/rust/main/agents/relayer/src/msg/metadata/base_builder.rs b/rust/main/agents/relayer/src/msg/metadata/base_builder.rs index 73f45607cda..4973e53728f 100644 --- a/rust/main/agents/relayer/src/msg/metadata/base_builder.rs +++ b/rust/main/agents/relayer/src/msg/metadata/base_builder.rs @@ -3,15 +3,16 @@ use std::{collections::HashMap, fmt::Debug, str::FromStr, sync::Arc}; -use crate::merkle_tree::builder::MerkleTreeBuilder; use derive_new::new; use eyre::Context; +use futures::{stream, StreamExt}; +use tokio::sync::RwLock; +use tracing::{debug, info, warn}; + use hyperlane_base::{ + cache::{LocalCache, MeteredCache, OptionalCache}, db::{HyperlaneDb, HyperlaneRocksDB}, - settings::CheckpointSyncerBuildError, -}; -use hyperlane_base::{ - settings::{ChainConf, CheckpointSyncerConf}, + settings::{ChainConf, CheckpointSyncerBuildError, CheckpointSyncerConf}, CheckpointSyncer, CoreMetrics, MultisigCheckpointSyncer, }; use hyperlane_core::{ @@ -20,10 +21,12 @@ use hyperlane_core::{ H256, }; -use tokio::sync::RwLock; -use tracing::{debug, info, warn}; +use crate::merkle_tree::builder::MerkleTreeBuilder; +use crate::msg::metadata::base_builder::validator_announced_storages::fetch_storage_locations_helper; + +use super::{base::IsmCachePolicyClassifier, IsmAwareAppContextClassifier}; -use super::IsmAwareAppContextClassifier; +mod validator_announced_storages; /// Base metadata builder with types used by higher level metadata builders. #[allow(clippy::too_many_arguments)] @@ -35,8 +38,10 @@ pub struct BaseMetadataBuilder { origin_validator_announce: Arc, allow_local_checkpoint_syncers: bool, metrics: Arc, + cache: OptionalCache>, db: HyperlaneRocksDB, app_context_classifier: IsmAwareAppContextClassifier, + ism_cache_policy_classifier: IsmCachePolicyClassifier, } impl Debug for BaseMetadataBuilder { @@ -54,6 +59,8 @@ pub trait BuildsBaseMetadata: Send + Sync + Debug { fn origin_domain(&self) -> &HyperlaneDomain; fn destination_domain(&self) -> &HyperlaneDomain; fn app_context_classifier(&self) -> &IsmAwareAppContextClassifier; + fn ism_cache_policy_classifier(&self) -> &IsmCachePolicyClassifier; + fn cache(&self) -> &OptionalCache>; async fn get_proof(&self, leaf_index: u32, checkpoint: Checkpoint) -> eyre::Result; async fn highest_known_leaf_index(&self) -> Option; @@ -85,6 +92,14 @@ impl BuildsBaseMetadata for BaseMetadataBuilder { &self.app_context_classifier } + fn ism_cache_policy_classifier(&self) -> &IsmCachePolicyClassifier { + &self.ism_cache_policy_classifier + } + + fn cache(&self) -> &OptionalCache> { + &self.cache + } + async fn get_proof(&self, leaf_index: u32, checkpoint: Checkpoint) -> eyre::Result { const CTX: &str = "When fetching message proof"; let proof = self @@ -154,10 +169,7 @@ impl BuildsBaseMetadata for BaseMetadataBuilder { validators: &[H256], app_context: Option, ) -> Result { - let storage_locations = self - .origin_validator_announce - .get_announced_storage_locations(validators) - .await?; + let storage_locations = self.fetch_storage_locations(validators).await?; debug!( hyp_message=?message, @@ -169,67 +181,103 @@ impl BuildsBaseMetadata for BaseMetadataBuilder { // Only use the most recently announced location for now. let mut checkpoint_syncers: HashMap> = HashMap::new(); - for (&validator, validator_storage_locations) in validators.iter().zip(storage_locations) { - debug!(hyp_message=?message, ?validator, ?validator_storage_locations, "Validator and its storage locations for message"); - for storage_location in validator_storage_locations.iter().rev() { - let Ok(config) = CheckpointSyncerConf::from_str(storage_location) else { - debug!( - ?validator, - ?storage_location, - "Could not parse checkpoint syncer config for validator" - ); - continue; - }; - // If this is a LocalStorage based checkpoint syncer and it's not - // allowed, ignore it - if !self.allow_local_checkpoint_syncers - && matches!(config, CheckpointSyncerConf::LocalStorage { .. }) - { - debug!( - ?config, - "Ignoring disallowed LocalStorage based checkpoint syncer" - ); - continue; + let result = validators + .iter() + .zip(storage_locations) + .filter_map(|(validator, validator_storage_locations)| { + debug!(hyp_message=?message, ?validator, ?validator_storage_locations, "Validator and its storage locations for message"); + if validator_storage_locations.is_empty() { + // If the validator has not announced any storage locations, we skip it + // and log a warning. + warn!(?validator, "Validator has not announced any storage locations; see https://docs.hyperlane.xyz/docs/operators/validators/announcing-your-validator"); + return None; } - match config.build_and_validate(None).await { - Ok(checkpoint_syncer) => { - // found the syncer for this validator - checkpoint_syncers.insert(validator.into(), checkpoint_syncer.into()); - break; - } - Err(CheckpointSyncerBuildError::ReorgEvent(reorg_event)) => { - // If a reorg event has been posted to a checkpoint syncer, - // we refuse to build - return Err(CheckpointSyncerBuildError::ReorgEvent(reorg_event)); - } - Err(err) => { - debug!( - error=%err, - ?config, - ?validator, - "Error when loading checkpoint syncer; will attempt to use the next config" - ); + let future = async move { + // Reverse the order of storage locations to prefer the most recently announced + for storage_location in validator_storage_locations.iter().rev() { + let Ok(config) = CheckpointSyncerConf::from_str(storage_location) else { + debug!( + ?validator, + ?storage_location, + "Could not parse checkpoint syncer config for validator" + ); + continue; + }; + + // If this is a LocalStorage based checkpoint syncer and it's not + // allowed, ignore it + if !self.allow_local_checkpoint_syncers + && matches!(config, CheckpointSyncerConf::LocalStorage { .. }) + { + debug!( + ?config, + "Ignoring disallowed LocalStorage based checkpoint syncer" + ); + continue; + } + + match config.build_and_validate(None).await { + Ok(checkpoint_syncer) => { + // found the syncer for this validator + return Ok(Some((*validator, checkpoint_syncer))); + } + Err(CheckpointSyncerBuildError::ReorgEvent(reorg_event)) => { + // If a reorg event has been posted to a checkpoint syncer, + // we refuse to build + // This will result in a short circuit and return an error for the entire build process of all syncers + return Err(CheckpointSyncerBuildError::ReorgEvent(reorg_event)); + } + Err(err) => { + debug!( + error=%err, + ?config, + ?validator, + "Error when loading checkpoint syncer; will attempt to use the next config" + ); + } + } } - } - } - if checkpoint_syncers.get(&validator.into()).is_none() { - if validator_storage_locations.is_empty() { - warn!(?validator, "Validator has not announced any storage locations; see https://docs.hyperlane.xyz/docs/operators/validators/announcing-your-validator"); - } else { warn!( ?validator, ?validator_storage_locations, "No valid checkpoint syncer configs for validator" ); - } - } + Ok(None) + }; + Some(future) + }) + .collect::>(); + + let checkpoint_syncers_results = stream::iter(result) + .buffer_unordered(10) // Limit the number of concurrent tasks + .collect::>() + .await + .into_iter() + .collect::, _>>()? // Collect results into a single vector and return if any of them returns an error + .into_iter() + .flatten() // Flatten Option<_> + .collect::>(); + + for (validator, checkpoint_syncer) in checkpoint_syncers_results { + checkpoint_syncers.insert(validator.into(), checkpoint_syncer.into()); } + Ok(MultisigCheckpointSyncer::new( checkpoint_syncers, - self.metrics.clone(), - app_context, + app_context.map(|ctx| (self.metrics.clone(), ctx)), )) } } + +impl BaseMetadataBuilder { + /// Fetches storage locations for validators with caching. + pub async fn fetch_storage_locations( + &self, + validators: &[H256], + ) -> eyre::Result>> { + fetch_storage_locations_helper(validators, &self.cache, &*self.origin_validator_announce) + .await + } +} diff --git a/rust/main/agents/relayer/src/msg/metadata/base_builder/validator_announced_storages.rs b/rust/main/agents/relayer/src/msg/metadata/base_builder/validator_announced_storages.rs new file mode 100644 index 00000000000..afa7de204e5 --- /dev/null +++ b/rust/main/agents/relayer/src/msg/metadata/base_builder/validator_announced_storages.rs @@ -0,0 +1,74 @@ +use eyre::Context; + +use hyperlane_base::cache::FunctionCallCache; +use hyperlane_core::{ValidatorAnnounce, H256}; + +const METHOD_NAME: &str = "get_announced_storage_locations"; + +/// Helper function to fetch storage locations for validators. +pub async fn fetch_storage_locations_helper( + validators: &[H256], + cache: &impl FunctionCallCache, + validator_announce: &dyn ValidatorAnnounce, +) -> eyre::Result>> { + const CTX: &str = "When fetching storage locations"; + + let origin = validator_announce.domain().name(); // Dynamically fetch domain name + + let mut storage_locations = Vec::new(); + let mut missing_validators = Vec::new(); + + for (index, validator) in validators.iter().enumerate() { + let key = generate_cache_key(validator); + + // Attempt to retrieve from cache + if let Some(cached) = cache + .get_cached_call_result::>(origin, METHOD_NAME, &key) + .await? + { + storage_locations.push(cached); + } else { + missing_validators.push((index, *validator)); + storage_locations.push(Vec::new()); // Placeholder for missing validator + } + } + + if missing_validators.is_empty() { + // Cache contains storage locations for all validators + return Ok(storage_locations); + } + + // Fetch from validator_announce for missing validators + let fetched_locations = validator_announce + .get_announced_storage_locations( + &missing_validators + .iter() + .map(|(_, v)| *v) + .collect::>(), + ) + .await + .context(CTX)?; + + for (fetched_index, (index, validator)) in missing_validators.iter().enumerate() { + let key = generate_cache_key(validator); + let locations = &fetched_locations[fetched_index]; + + // Store in cache + cache + .cache_call_result(origin, METHOD_NAME, &key, locations) + .await?; + + // Update the placeholder in storage_locations + storage_locations[*index] = locations.clone(); + } + + Ok(storage_locations) +} + +/// Generates a cache key for a given validator. +fn generate_cache_key(validator: &H256) -> String { + format!("storage_location:{:?}", validator) +} + +#[cfg(test)] +mod tests; diff --git a/rust/main/agents/relayer/src/msg/metadata/base_builder/validator_announced_storages/tests.rs b/rust/main/agents/relayer/src/msg/metadata/base_builder/validator_announced_storages/tests.rs new file mode 100644 index 00000000000..8a44ca2ccf1 --- /dev/null +++ b/rust/main/agents/relayer/src/msg/metadata/base_builder/validator_announced_storages/tests.rs @@ -0,0 +1,217 @@ +use async_trait::async_trait; +use mockall::mock; + +use hyperlane_base::cache::LocalCache; +use hyperlane_core::{ + Announcement, ChainCommunicationError, ChainResult, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneProvider, KnownHyperlaneDomain, SignedType, TxOutcome, + ValidatorAnnounce, H256, U256, +}; + +use super::*; + +// Mock implementation for ValidatorAnnounce +mock! { + #[derive(Debug)] + pub ValidatorAnnounceMock {} + + #[async_trait] + impl ValidatorAnnounce for ValidatorAnnounceMock { + async fn get_announced_storage_locations( + &self, + validators: &[H256], + ) -> ChainResult>>; + + async fn announce(&self, announcement: SignedType) -> ChainResult; + + async fn announce_tokens_needed(&self, announcement: SignedType) -> Option; + } + + impl HyperlaneContract for ValidatorAnnounceMock { + fn address(&self) -> H256; + } + + impl HyperlaneChain for ValidatorAnnounceMock { + fn domain(&self) -> &HyperlaneDomain; + fn provider(&self) -> Box; + } +} + +#[tokio::test] +async fn test_fetch_storage_locations_helper_with_cache_hit() { + let origin = HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum); + let cache = LocalCache::new("test_cache"); + let validators = vec![H256::from_low_u64_be(1), H256::from_low_u64_be(2)]; + + let mut validator_announce = MockValidatorAnnounceMock::new(); + + let key1 = generate_cache_key(&validators[0]); + let key2 = generate_cache_key(&validators[1]); + + // Mock the response from the validator announce contract + validator_announce + .expect_domain() + .return_const(origin.clone()); + + // Prepopulate the cache with storage locations + let location1 = vec!["location1".to_string()]; + let location2 = vec!["location2".to_string()]; + cache + .cache_call_result(&origin.name(), METHOD_NAME, &key1, &location1) + .await + .unwrap(); + cache + .cache_call_result(&origin.name(), METHOD_NAME, &key2, &location2) + .await + .unwrap(); + + let result = fetch_storage_locations_helper(&validators, &cache, &validator_announce).await; + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec![location1, location2]); +} + +#[tokio::test] +async fn test_fetch_storage_locations_helper_with_cache_miss() { + let origin = HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum); + let cache = LocalCache::new("test_cache"); + let validators = vec![H256::from_low_u64_be(1), H256::from_low_u64_be(2)]; + + let mut validator_announce = MockValidatorAnnounceMock::new(); + + // Mock the response from the validator announce contract + validator_announce + .expect_get_announced_storage_locations() + .returning(move |_| { + Ok(vec![ + vec!["location1".to_string()], + vec!["location2".to_string()], + ]) + }); + validator_announce + .expect_domain() + .return_const(origin.clone()); + + let result = fetch_storage_locations_helper(&validators, &cache, &validator_announce).await; + + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + vec![vec!["location1".to_string()], vec!["location2".to_string()]] + ); +} + +#[tokio::test] +async fn test_fetch_storage_locations_helper_with_error() { + let origin = HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum); + let cache = LocalCache::new("test_cache"); + let validators = vec![H256::from_low_u64_be(1), H256::from_low_u64_be(2)]; + + let mut validator_announce = MockValidatorAnnounceMock::new(); + + // Mock an error response from the validator announce contract + validator_announce + .expect_get_announced_storage_locations() + .returning(move |_| { + Err(ChainCommunicationError::CustomError( + "Error fetching storage locations".to_string(), + )) + }); + validator_announce + .expect_domain() + .return_const(origin.clone()); + + let result = fetch_storage_locations_helper(&validators, &cache, &validator_announce).await; + + assert!(result.is_err()); +} + +#[tokio::test] +async fn test_fetch_storage_locations_helper_with_partial_cache_hit() { + let origin = HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum); + let cache = LocalCache::new("test_cache"); + let validators = vec![ + H256::from_low_u64_be(1), + H256::from_low_u64_be(2), + H256::from_low_u64_be(3), + ]; + + let mut validator_announce = MockValidatorAnnounceMock::new(); + + let key1 = generate_cache_key(&validators[0]); + let key2 = generate_cache_key(&validators[1]); + + // Prepopulate the cache with storage locations for some validators + let location1 = vec!["location1".to_string()]; + let location2 = vec!["location2".to_string()]; + cache + .cache_call_result(&origin.name(), METHOD_NAME, &key1, &location1) + .await + .unwrap(); + cache + .cache_call_result(&origin.name(), METHOD_NAME, &key2, &location2) + .await + .unwrap(); + + // Mock the response from the validator announce contract for the missing validator + let location3 = "location3"; + validator_announce + .expect_get_announced_storage_locations() + .returning(move |_| Ok(vec![vec![location3.to_string()]])); + validator_announce + .expect_domain() + .return_const(origin.clone()); + + let result = fetch_storage_locations_helper(&validators, &cache, &validator_announce).await; + + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + vec![location1, location2, vec!["location3".to_string()]] + ); +} + +#[tokio::test] +async fn test_fetch_storage_locations_helper_with_different_domains() { + let origin1 = HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum); + let origin2 = HyperlaneDomain::Known(KnownHyperlaneDomain::Optimism); + let cache = LocalCache::new("test_cache"); + let validators = vec![H256::from_low_u64_be(1)]; + + let mut validator_announce1 = MockValidatorAnnounceMock::new(); + let mut validator_announce2 = MockValidatorAnnounceMock::new(); + + let key = generate_cache_key(&validators[0]); + + // Mock the response from the validator announce contracts + validator_announce1 + .expect_domain() + .return_const(origin1.clone()); + validator_announce2 + .expect_domain() + .return_const(origin2.clone()); + + // Prepopulate the cache with storage locations for origin1 + let location1 = vec!["location1_origin1".to_string()]; + cache + .cache_call_result(&origin1.name(), METHOD_NAME, &key, &location1) + .await + .unwrap(); + + // Prepopulate the cache with storage locations for origin2 + let location2 = vec!["location1_origin2".to_string()]; + cache + .cache_call_result(&origin2.name(), METHOD_NAME, &key, &location2) + .await + .unwrap(); + + // Fetch storage locations for origin1 + let result1 = fetch_storage_locations_helper(&validators, &cache, &validator_announce1).await; + assert!(result1.is_ok()); + assert_eq!(result1.unwrap(), vec![location1]); + + // Fetch storage locations for origin2 + let result2 = fetch_storage_locations_helper(&validators, &cache, &validator_announce2).await; + assert!(result2.is_ok()); + assert_eq!(result2.unwrap(), vec![location2]); +} diff --git a/rust/main/agents/relayer/src/msg/metadata/ccip_read/cache_types.rs b/rust/main/agents/relayer/src/msg/metadata/ccip_read/cache_types.rs new file mode 100644 index 00000000000..f1b6d40693f --- /dev/null +++ b/rust/main/agents/relayer/src/msg/metadata/ccip_read/cache_types.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; + +use hyperlane_ethereum::OffchainLookup; + +#[allow(missing_docs)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializedOffchainLookup { + pub sender: ethers::core::types::Address, + pub urls: ::std::vec::Vec, + pub call_data: ethers::core::types::Bytes, + pub callback_function: [u8; 4], + pub extra_data: ethers::core::types::Bytes, +} + +impl From for SerializedOffchainLookup { + fn from(lookup: OffchainLookup) -> Self { + Self { + sender: lookup.sender, + urls: lookup.urls, + call_data: lookup.call_data, + callback_function: lookup.callback_function, + extra_data: lookup.extra_data, + } + } +} + +impl From for OffchainLookup { + fn from(lookup: SerializedOffchainLookup) -> Self { + Self { + sender: lookup.sender, + urls: lookup.urls, + call_data: lookup.call_data, + callback_function: lookup.callback_function, + extra_data: lookup.extra_data, + } + } +} diff --git a/rust/main/agents/relayer/src/msg/metadata/ccip_read/mod.rs b/rust/main/agents/relayer/src/msg/metadata/ccip_read/mod.rs new file mode 100644 index 00000000000..db0c90dc810 --- /dev/null +++ b/rust/main/agents/relayer/src/msg/metadata/ccip_read/mod.rs @@ -0,0 +1,175 @@ +#![allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue +use async_trait::async_trait; +use cache_types::SerializedOffchainLookup; +use derive_more::Deref; +use derive_new::new; +use ethers::{abi::AbiDecode, core::utils::hex::decode as hex_decode}; +use hyperlane_base::cache::FunctionCallCache; +use regex::Regex; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use tracing::{info, instrument, warn}; + +use hyperlane_core::{ + utils::bytes_to_hex, CcipReadIsm, HyperlaneMessage, RawHyperlaneMessage, H256, +}; +use hyperlane_ethereum::OffchainLookup; + +use super::{ + base::{MessageMetadataBuildParams, MetadataBuildError}, + message_builder::MessageMetadataBuilder, + Metadata, MetadataBuilder, +}; + +mod cache_types; + +#[derive(Serialize, Deserialize)] +struct OffchainResponse { + data: String, +} + +#[derive(Clone, Debug, new, Deref)] +pub struct CcipReadIsmMetadataBuilder { + base: MessageMetadataBuilder, +} + +impl CcipReadIsmMetadataBuilder { + /// Returns info on how to query for offchain information + /// This method will attempt to get the value from cache first. If it is a cache miss, + /// it will request it from the ISM contract. The result will be cached for future use. + /// + /// Implicit contract in this method: function name `get_offchain_verify_info` matches + /// the name of the method `get_offchain_verify_info`. + async fn call_get_offchain_verify_info( + &self, + ism: Box, + message: &HyperlaneMessage, + ) -> Result { + let ism_domain = ism.domain().name(); + let fn_key = "get_offchain_verify_info"; + // To have the cache key be more succinct, we use the message id + let call_params = (ism.address(), message.id()); + + let info_from_cache = self + .base_builder() + .cache() + .get_cached_call_result::(ism_domain, fn_key, &call_params) + .await + .map_err(|err| { + warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok() + .flatten(); + + let info: OffchainLookup = match info_from_cache { + Some(info) => info.into(), + None => { + let response = ism + .get_offchain_verify_info(RawHyperlaneMessage::from(message).to_vec()) + .await; + + match response { + Ok(_) => { + info!("incorrectly configured getOffchainVerifyInfo, expected revert"); + return Err(MetadataBuildError::CouldNotFetch); + } + Err(raw_error) => { + let matching_regex = Regex::new(r"0x[[:xdigit:]]+") + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + if let Some(matching) = &matching_regex.captures(&raw_error.to_string()) { + let hex_val = hex_decode(&matching[0][2..]).map_err(|err| { + MetadataBuildError::FailedToBuild(err.to_string()) + })?; + OffchainLookup::decode(hex_val) + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))? + } else { + info!(?raw_error, "unable to parse custom error out of revert"); + return Err(MetadataBuildError::CouldNotFetch); + } + } + } + } + }; + + self.base_builder() + .cache() + .cache_call_result( + ism_domain, + fn_key, + &call_params, + &SerializedOffchainLookup::from(info.clone()), + ) + .await + .map_err(|err| { + warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok(); + + Ok(info) + } +} + +#[async_trait] +impl MetadataBuilder for CcipReadIsmMetadataBuilder { + #[instrument(err, skip(self, message, _params))] + async fn build( + &self, + ism_address: H256, + message: &HyperlaneMessage, + _params: MessageMetadataBuildParams, + ) -> Result { + let ism = self + .base_builder() + .build_ccip_read_ism(ism_address) + .await + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + + let info = self.call_get_offchain_verify_info(ism, message).await?; + + for url in info.urls.iter() { + // Need to explicitly convert the sender H160 the hex because the `ToString` implementation + // for `H160` truncates the output. (e.g. `0xc66a…7b6f` instead of returning + // the full address) + let sender_as_bytes = &bytes_to_hex(info.sender.as_bytes()); + let data_as_bytes = &info.call_data.to_string(); + let interpolated_url = url + .replace("{sender}", sender_as_bytes) + .replace("{data}", data_as_bytes); + let res = if !url.contains("{data}") { + let body = json!({ + "sender": sender_as_bytes, + "data": data_as_bytes + }); + Client::new() + .post(interpolated_url) + .header("Content-Type", "application/json") + .json(&body) + .send() + .await + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))? + } else { + reqwest::get(interpolated_url) + .await + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))? + }; + + let json: Result = res.json().await; + + match json { + Ok(result) => { + // remove leading 0x which hex_decode doesn't like + let metadata = hex_decode(&result.data[2..]) + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + return Ok(Metadata::new(metadata)); + } + Err(_err) => { + // try the next URL + } + } + } + + // No metadata endpoints or endpoints down + Err(MetadataBuildError::CouldNotFetch) + } +} diff --git a/rust/main/agents/relayer/src/msg/metadata/message_builder.rs b/rust/main/agents/relayer/src/msg/metadata/message_builder.rs index 9f3b1a0d8ca..b1ca182d9ef 100644 --- a/rust/main/agents/relayer/src/msg/metadata/message_builder.rs +++ b/rust/main/agents/relayer/src/msg/metadata/message_builder.rs @@ -6,6 +6,10 @@ use std::{fmt::Debug, sync::Arc}; use async_trait::async_trait; use eyre::Result; use hyperlane_core::{HyperlaneMessage, InterchainSecurityModule, ModuleType, H256}; +use { + hyperlane_base::cache::{FunctionCallCache, NoParams}, + tracing::warn, +}; use tracing::instrument; @@ -29,6 +33,7 @@ use super::{ pub struct MessageMetadataBuilder { pub base: Arc, pub app_context: Option, + pub root_ism: H256, pub max_ism_depth: u32, pub max_ism_count: u32, } @@ -56,7 +61,7 @@ impl MetadataBuilder for MessageMetadataBuilder { message: &HyperlaneMessage, params: MessageMetadataBuildParams, ) -> Result { - build_message_metadata(self.clone(), ism_address, message, params) + build_message_metadata(self.clone(), ism_address, message, params, None) .await .map(|res| res.metadata) } @@ -75,6 +80,7 @@ impl MessageMetadataBuilder { Ok(Self { base, app_context, + root_ism: ism_address, max_ism_depth: ISM_MAX_DEPTH, max_ism_count: ISM_MAX_COUNT, }) @@ -83,26 +89,78 @@ impl MessageMetadataBuilder { pub fn base_builder(&self) -> &Arc { &self.base } + + /// Returns the module type of the ISM. + /// This method will attempt to get the value from cache first. If it is a cache miss, + /// it will request it from the ISM contract. The result will be cached for future use. + /// + /// Implicit contract in this method: function name `module_type` matches + /// the name of the method `module_type`. + async fn call_module_type( + &self, + ism: &dyn InterchainSecurityModule, + ) -> Result { + let ism_domain = ism.domain().name(); + let fn_key = "module_type"; + let call_params = (ism.address(), NoParams); + + match self + .base_builder() + .cache() + .get_cached_call_result::(ism_domain, fn_key, &call_params) + .await + .map_err(|err| { + warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok() + .flatten() + { + Some(module_type) => Ok(module_type), + None => { + let module_type = ism + .module_type() + .await + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + + self.base_builder() + .cache() + .cache_call_result(ism_domain, fn_key, &call_params, &module_type) + .await + .map_err(|err| { + warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok(); + Ok(module_type) + } + } + } } -/// Builds metadata for a message. -pub async fn build_message_metadata( +pub async fn ism_and_module_type( message_builder: MessageMetadataBuilder, ism_address: H256, - message: &HyperlaneMessage, - mut params: MessageMetadataBuildParams, -) -> Result { - let ism: Box = message_builder +) -> Result<(Box, ModuleType), MetadataBuildError> { + let ism = message_builder .base_builder() .build_ism(ism_address) .await .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + let module_type = message_builder.call_module_type(&ism).await?; + Ok((ism, module_type)) +} - let module_type = ism - .module_type() - .await - .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; - +/// Builds metadata for a message. +pub async fn build_message_metadata( + message_builder: MessageMetadataBuilder, + ism_address: H256, + message: &HyperlaneMessage, + mut params: MessageMetadataBuildParams, + maybe_ism_and_module_type: Option<(Box, ModuleType)>, +) -> Result { + let (ism, module_type) = match maybe_ism_and_module_type { + Some((ism, module_type)) => (ism, module_type), + None => ism_and_module_type(message_builder.clone(), ism_address).await?, + }; // check if max depth is reached if params.ism_depth >= message_builder.max_ism_depth { tracing::error!( @@ -155,15 +213,21 @@ pub async fn build_message_metadata( mod test { use std::sync::Arc; + use hyperlane_base::cache::{ + LocalCache, MeteredCache, MeteredCacheConfig, MeteredCacheMetrics, OptionalCache, + }; use hyperlane_core::{ HyperlaneDomain, HyperlaneMessage, KnownHyperlaneDomain, Mailbox, ModuleType, H256, U256, }; use hyperlane_test::mocks::MockMailboxContract; + use prometheus::IntCounterVec; use crate::{ msg::metadata::{ - base::MetadataBuildError, message_builder::build_message_metadata, - IsmAwareAppContextClassifier, MessageMetadataBuildParams, + base::MetadataBuildError, + message_builder::{build_message_metadata, ism_and_module_type}, + DefaultIsmCache, IsmAwareAppContextClassifier, IsmCachePolicyClassifier, + MessageMetadataBuildParams, }, settings::matching_list::{Filter, ListElement, MatchingList}, test_utils::{ @@ -174,18 +238,44 @@ mod test { use super::MessageMetadataBuilder; + const TEST_DOMAIN: HyperlaneDomain = HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum); + + fn dummy_cache_metrics() -> MeteredCacheMetrics { + MeteredCacheMetrics { + hit_count: IntCounterVec::new( + prometheus::Opts::new("dummy_hit_count", "help string"), + &["cache_name", "chain", "method", "status"], + ) + .ok(), + miss_count: IntCounterVec::new( + prometheus::Opts::new("dummy_miss_count", "help string"), + &["cache_name", "chain", "method", "status"], + ) + .ok(), + } + } + fn build_mock_base_builder() -> MockBaseMetadataBuilder { let origin_domain = HyperlaneDomain::Known(KnownHyperlaneDomain::Optimism); let destination_domain = HyperlaneDomain::Known(KnownHyperlaneDomain::Ethereum); + let cache = OptionalCache::new(Some(MeteredCache::new( + LocalCache::new("test-cache"), + dummy_cache_metrics(), + MeteredCacheConfig { + cache_name: "test-cache".to_owned(), + }, + ))); let mut base_builder = MockBaseMetadataBuilder::new(); base_builder.responses.origin_domain = Some(origin_domain.clone()); base_builder.responses.destination_domain = Some(destination_domain); + base_builder.responses.cache = Some(cache); - let mock_mailbox = MockMailboxContract::new(); + let mock_mailbox = MockMailboxContract::new_with_default_ism(H256::zero()); let mailbox: Arc = Arc::new(mock_mailbox); + let default_ism_getter = DefaultIsmCache::new(mailbox); let app_context_classifier = IsmAwareAppContextClassifier::new( - mailbox, + default_ism_getter.clone(), vec![( MatchingList(Some(vec![ListElement::new( Filter::Wildcard, @@ -198,18 +288,20 @@ mod test { )], ); base_builder.responses.app_context_classifier = Some(app_context_classifier); + base_builder.responses.ism_cache_policy_classifier = Some(IsmCachePolicyClassifier::new( + default_ism_getter, + Default::default(), + )); base_builder } fn insert_null_isms(base_builder: &MockBaseMetadataBuilder, addresses: &[H256]) { for ism_address in addresses { - let mock_ism = MockInterchainSecurityModule::new(*ism_address); - mock_ism - .responses - .module_type - .lock() - .unwrap() - .push_back(Ok(ModuleType::Null)); + let mock_ism = MockInterchainSecurityModule::new( + *ism_address, + TEST_DOMAIN.clone(), + ModuleType::Null, + ); mock_ism .responses .dry_run_verify @@ -227,13 +319,11 @@ mod test { addresses: &[(H256, H256)], ) { for (ism_address, route_address) in addresses { - let mock_ism = MockInterchainSecurityModule::new(*ism_address); - mock_ism - .responses - .module_type - .lock() - .unwrap() - .push_back(Ok(ModuleType::Routing)); + let mock_ism = MockInterchainSecurityModule::new( + *ism_address, + TEST_DOMAIN.clone(), + ModuleType::Routing, + ); mock_ism .responses .dry_run_verify @@ -244,7 +334,7 @@ mod test { .responses .push_build_ism_response(*ism_address, Ok(Box::new(mock_ism))); - let routing_ism = MockRoutingIsm::default(); + let routing_ism = MockRoutingIsm::new(*ism_address, TEST_DOMAIN.clone()); routing_ism .responses .route @@ -253,10 +343,7 @@ mod test { .push_back(Ok(*route_address)); base_builder .responses - .build_routing_ism - .lock() - .unwrap() - .push_back(Ok(Box::new(routing_ism))); + .push_build_routing_ism_response(*ism_address, Ok(Box::new(routing_ism))); } } @@ -265,13 +352,11 @@ mod test { addresses: Vec<(H256, Vec, u8)>, ) { for (ism_address, aggregation_addresses, threshold) in addresses { - let mock_ism = MockInterchainSecurityModule::new(ism_address); - mock_ism - .responses - .module_type - .lock() - .unwrap() - .push_back(Ok(ModuleType::Aggregation)); + let mock_ism = MockInterchainSecurityModule::new( + ism_address, + TEST_DOMAIN.clone(), + ModuleType::Aggregation, + ); mock_ism .responses .dry_run_verify @@ -282,7 +367,7 @@ mod test { .responses .push_build_ism_response(ism_address, Ok(Box::new(mock_ism))); - let agg_ism = MockAggregationIsm::default(); + let agg_ism = MockAggregationIsm::new(ism_address, TEST_DOMAIN.clone()); agg_ism .responses .modules_and_threshold @@ -291,32 +376,29 @@ mod test { .push_back(Ok((aggregation_addresses, threshold))); base_builder .responses - .build_aggregation_ism - .lock() - .unwrap() - .push_back(Ok(Box::new(agg_ism))); + .push_build_aggregation_ism_response(ism_address, Ok(Box::new(agg_ism))); } } - /// 0x0 + /// 0 /// | - /// +---> 0x100 + /// +---> 100 /// | | - /// | +----> 0x110 -> 0x1100 + /// | +----> 110 -> 1100 /// | | - /// | +----> 0x120 -> 0x1200 + /// | +----> 120 -> 1200 /// | - /// +---> 0x200 + /// +---> 200 /// | | - /// | +----> 0x210 -> 0x2100 + /// | +----> 210 -> 2100 /// | | /// | +----> 0x220 -> 0x2200 /// | - /// +---> 0x300 + /// +---> 300 /// | - /// +----> 0x310 -> 0x3100 + /// +----> 310 -> 3100 /// | - /// +----> 0x320 -> 0x3200 + /// +----> 320 -> 3200 fn insert_ism_test_data(base_builder: &MockBaseMetadataBuilder) { insert_mock_aggregation_isms( base_builder, @@ -390,9 +472,10 @@ mod test { builder }; let params = MessageMetadataBuildParams::default(); - let err = build_message_metadata(message_builder, ism_address, &message, params.clone()) - .await - .expect_err("Metadata found when it should have failed"); + let err = + build_message_metadata(message_builder, ism_address, &message, params.clone(), None) + .await + .expect_err("Metadata found when it should have failed"); assert_eq!(err, MetadataBuildError::MaxIsmDepthExceeded(0)); assert_eq!(*(params.ism_count.lock().await), 0); @@ -418,9 +501,10 @@ mod test { }; let params = MessageMetadataBuildParams::default(); - let err = build_message_metadata(message_builder, ism_address, &message, params.clone()) - .await - .expect_err("Metadata found when it should have failed"); + let err = + build_message_metadata(message_builder, ism_address, &message, params.clone(), None) + .await + .expect_err("Metadata found when it should have failed"); assert_eq!(err, MetadataBuildError::MaxIsmCountReached(0)); assert_eq!(*(params.ism_count.lock().await), 0); @@ -446,9 +530,10 @@ mod test { }; let params = MessageMetadataBuildParams::default(); - let err = build_message_metadata(message_builder, ism_address, &message, params.clone()) - .await - .expect_err("Metadata found when it should have failed"); + let err = + build_message_metadata(message_builder, ism_address, &message, params.clone(), None) + .await + .expect_err("Metadata found when it should have failed"); assert_eq!(err, MetadataBuildError::AggregationThresholdNotMet(2)); assert!(*(params.ism_count.lock().await) <= 4); assert!(logs_contain("Max ISM depth reached ism_depth=2")); @@ -473,9 +558,10 @@ mod test { }; let params = MessageMetadataBuildParams::default(); - let err = build_message_metadata(message_builder, ism_address, &message, params.clone()) - .await - .expect_err("Metadata found when it should have failed"); + let err = + build_message_metadata(message_builder, ism_address, &message, params.clone(), None) + .await + .expect_err("Metadata found when it should have failed"); assert_eq!(err, MetadataBuildError::AggregationThresholdNotMet(2)); assert_eq!(*(params.ism_count.lock().await), 5); assert!(logs_contain("Max ISM count reached ism_count=5")); diff --git a/rust/main/agents/relayer/src/msg/metadata/mod.rs b/rust/main/agents/relayer/src/msg/metadata/mod.rs index 942160d81b9..f960e2a82f0 100644 --- a/rust/main/agents/relayer/src/msg/metadata/mod.rs +++ b/rust/main/agents/relayer/src/msg/metadata/mod.rs @@ -8,7 +8,8 @@ mod null_metadata; mod routing; pub(crate) use base::{ - AppContextClassifier, IsmAwareAppContextClassifier, MessageMetadataBuildParams, Metadata, + AppContextClassifier, DefaultIsmCache, IsmAwareAppContextClassifier, IsmCacheConfig, + IsmCachePolicy, IsmCachePolicyClassifier, MessageMetadataBuildParams, Metadata, MetadataBuildError, MetadataBuilder, }; pub(crate) use base_builder::{BaseMetadataBuilder, BuildsBaseMetadata}; diff --git a/rust/main/agents/relayer/src/msg/metadata/multisig/base.rs b/rust/main/agents/relayer/src/msg/metadata/multisig/base.rs index 91f78b93c2b..fe83f29d414 100644 --- a/rust/main/agents/relayer/src/msg/metadata/multisig/base.rs +++ b/rust/main/agents/relayer/src/msg/metadata/multisig/base.rs @@ -6,16 +6,17 @@ use derive_new::new; use ethers::abi::Token; use eyre::{Context, Result}; +use hyperlane_base::cache::FunctionCallCache; use hyperlane_base::settings::CheckpointSyncerBuildError; use hyperlane_base::MultisigCheckpointSyncer; use hyperlane_core::accumulator::merkle::Proof; -use hyperlane_core::{HyperlaneMessage, MultisigSignedCheckpoint, H256}; +use hyperlane_core::{HyperlaneMessage, ModuleType, MultisigIsm, MultisigSignedCheckpoint, H256}; use strum::Display; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use crate::msg::metadata::base::MetadataBuildError; use crate::msg::metadata::message_builder::MessageMetadataBuilder; -use crate::msg::metadata::{MessageMetadataBuildParams, Metadata, MetadataBuilder}; +use crate::msg::metadata::{IsmCachePolicy, MessageMetadataBuildParams, Metadata, MetadataBuilder}; #[derive(new, AsRef, Deref)] pub struct MultisigMetadata { @@ -37,8 +38,12 @@ pub enum MetadataToken { Signatures, } +const MAX_VALIDATOR_SET_SIZE: usize = 50; + #[async_trait] pub trait MultisigIsmMetadataBuilder: AsRef + Send + Sync { + fn module_type(&self) -> ModuleType; + async fn fetch_metadata( &self, validators: &[H256], @@ -90,6 +95,72 @@ pub trait MultisigIsmMetadataBuilder: AsRef + Send + Syn let metas: Result>> = self.token_layout().iter().map(build_token).collect(); Ok(metas?.into_iter().flatten().collect()) } + + /// Returns the validators and threshold for the given multisig ISM. + /// This method will attempt to get the value from cache first. If it is a cache miss, + /// it will request it from ISM contract. The result will be cached for future use. + /// + /// Implicit contract in this method: function name `validators_and_threshold` matches + /// the name of the method `validators_and_threshold`. + async fn call_validators_and_threshold( + &self, + multisig_ism: &dyn MultisigIsm, + message: &HyperlaneMessage, + ) -> Result<(Vec, u8), MetadataBuildError> { + let ism_domain = multisig_ism.domain().name(); + let fn_key = "validators_and_threshold"; + + // Depending on the cache policy, make use of the message ID + let params_cache_key = match self + .as_ref() + .base_builder() + .ism_cache_policy_classifier() + .get_cache_policy( + self.as_ref().root_ism, + multisig_ism.domain(), + self.module_type(), + self.as_ref().app_context.as_ref(), + ) + .await + { + // To have the cache key be more succinct, we use the message id + IsmCachePolicy::MessageSpecific => (multisig_ism.address(), message.id()), + IsmCachePolicy::IsmSpecific => (multisig_ism.address(), H256::zero()), + }; + + let cache_result = self + .as_ref() + .base_builder() + .cache() + .get_cached_call_result::<(Vec, u8)>(ism_domain, fn_key, ¶ms_cache_key) + .await + .map_err(|err| { + warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok() + .flatten(); + + match cache_result { + Some(result) => Ok(result), + None => { + let result = multisig_ism + .validators_and_threshold(message) + .await + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + + self.as_ref() + .base_builder() + .cache() + .cache_call_result(ism_domain, fn_key, ¶ms_cache_key, &result) + .await + .map_err(|err| { + warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok(); + Ok(result) + } + } + } } #[async_trait] @@ -108,16 +179,28 @@ impl MetadataBuilder for T { .await .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; - let (validators, threshold) = multisig_ism - .validators_and_threshold(message) - .await - .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + let (validators, threshold) = self + .call_validators_and_threshold(&multisig_ism, message) + .await?; if validators.is_empty() { info!("Could not fetch metadata: No validator set found for ISM"); return Err(MetadataBuildError::CouldNotFetch); } + // Dismiss large validator sets + if validators.len() > MAX_VALIDATOR_SET_SIZE { + info!( + ?ism_address, + validator_count = validators.len(), + max_validator_count = MAX_VALIDATOR_SET_SIZE, + "Skipping metadata: Too many validators in ISM" + ); + return Err(MetadataBuildError::MaxValidatorCountReached( + validators.len() as u32, + )); + } + info!(hyp_message=?message, ?validators, threshold, "List of validators and threshold for message"); let checkpoint_syncer = match self diff --git a/rust/main/agents/relayer/src/msg/metadata/multisig/merkle_root_multisig.rs b/rust/main/agents/relayer/src/msg/metadata/multisig/merkle_root_multisig.rs index 5c4c813ca0d..72dd9884615 100644 --- a/rust/main/agents/relayer/src/msg/metadata/multisig/merkle_root_multisig.rs +++ b/rust/main/agents/relayer/src/msg/metadata/multisig/merkle_root_multisig.rs @@ -6,7 +6,7 @@ use derive_new::new; use eyre::{Context, Result}; use hyperlane_base::MultisigCheckpointSyncer; -use hyperlane_core::{unwrap_or_none_result, HyperlaneMessage, H256}; +use hyperlane_core::{unwrap_or_none_result, HyperlaneMessage, ModuleType, H256}; use tracing::debug; use crate::msg::metadata::MessageMetadataBuilder; @@ -15,8 +15,13 @@ use super::base::{MetadataToken, MultisigIsmMetadataBuilder, MultisigMetadata}; #[derive(Debug, Clone, Deref, new, AsRef)] pub struct MerkleRootMultisigMetadataBuilder(MessageMetadataBuilder); + #[async_trait] impl MultisigIsmMetadataBuilder for MerkleRootMultisigMetadataBuilder { + fn module_type(&self) -> ModuleType { + ModuleType::MerkleRootMultisig + } + fn token_layout(&self) -> Vec { vec![ MetadataToken::CheckpointMerkleTreeHook, diff --git a/rust/main/agents/relayer/src/msg/metadata/multisig/message_id_multisig.rs b/rust/main/agents/relayer/src/msg/metadata/multisig/message_id_multisig.rs index dc6262768e2..247c00d2df5 100644 --- a/rust/main/agents/relayer/src/msg/metadata/multisig/message_id_multisig.rs +++ b/rust/main/agents/relayer/src/msg/metadata/multisig/message_id_multisig.rs @@ -6,7 +6,7 @@ use derive_new::new; use eyre::{Context, Result}; use hyperlane_base::MultisigCheckpointSyncer; -use hyperlane_core::{unwrap_or_none_result, HyperlaneMessage, H256}; +use hyperlane_core::{unwrap_or_none_result, HyperlaneMessage, ModuleType, H256}; use tracing::{debug, warn}; use super::base::{MetadataToken, MultisigIsmMetadataBuilder, MultisigMetadata}; @@ -17,6 +17,10 @@ pub struct MessageIdMultisigMetadataBuilder(MessageMetadataBuilder); #[async_trait] impl MultisigIsmMetadataBuilder for MessageIdMultisigMetadataBuilder { + fn module_type(&self) -> ModuleType { + ModuleType::MessageIdMultisig + } + fn token_layout(&self) -> Vec { vec![ MetadataToken::CheckpointMerkleTreeHook, @@ -34,7 +38,6 @@ impl MultisigIsmMetadataBuilder for MessageIdMultisigMetadataBuilder { checkpoint_syncer: &MultisigCheckpointSyncer, ) -> Result> { let message_id = message.id(); - const CTX: &str = "When fetching MessageIdMultisig metadata"; let leaf_index = unwrap_or_none_result!( self.base_builder() diff --git a/rust/main/agents/relayer/src/msg/metadata/routing.rs b/rust/main/agents/relayer/src/msg/metadata/routing.rs index 3fedd4eb3a2..e0eb23df795 100644 --- a/rust/main/agents/relayer/src/msg/metadata/routing.rs +++ b/rust/main/agents/relayer/src/msg/metadata/routing.rs @@ -1,13 +1,14 @@ use async_trait::async_trait; use derive_more::Deref; use derive_new::new; +use hyperlane_base::cache::FunctionCallCache; use tracing::instrument; -use hyperlane_core::{HyperlaneMessage, H256}; +use hyperlane_core::{HyperlaneMessage, ModuleType, H256}; use super::{ - base::MessageMetadataBuildParams, MessageMetadataBuilder, Metadata, MetadataBuildError, - MetadataBuilder, + base::MessageMetadataBuildParams, IsmCachePolicy, MessageMetadataBuilder, Metadata, + MetadataBuildError, MetadataBuilder, }; #[derive(Clone, Debug, new, Deref)] @@ -30,10 +31,84 @@ impl MetadataBuilder for RoutingIsmMetadataBuilder { .build_routing_ism(ism_address) .await .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; - let module = ism - .route(message) - .await - .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + + let ism_domain = ism.domain().name(); + let message_domain = self.base.base_builder().origin_domain(); + let fn_key = "route"; + + let cache_policy = self + .base_builder() + .ism_cache_policy_classifier() + .get_cache_policy( + self.root_ism, + ism.domain(), + ModuleType::Routing, + self.base.app_context.as_ref(), + ) + .await; + + let cache_result: Option = match cache_policy { + // if cache is ISM specific, we use the message origin for caching + IsmCachePolicy::IsmSpecific => { + let params_cache_key = (ism.address(), message.origin); + self.base_builder() + .cache() + .get_cached_call_result(ism_domain, fn_key, ¶ms_cache_key) + .await + } + // if cache is Message specific, we use the message id for caching + IsmCachePolicy::MessageSpecific => { + let params_cache_key = (ism.address(), message.id()); + self.base_builder() + .cache() + .get_cached_call_result(ism_domain, fn_key, ¶ms_cache_key) + .await + } + } + .map_err(|err| { + tracing::warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok() + .flatten(); + + let module = + match cache_result { + Some(result) => result, + None => { + let module = ism + .route(message) + .await + .map_err(|err| MetadataBuildError::FailedToBuild(err.to_string()))?; + + // store result in cache + match cache_policy { + IsmCachePolicy::IsmSpecific => { + let params_cache_key = (ism.address(), message.origin); + self.base_builder().cache().cache_call_result( + message_domain.name(), + fn_key, + ¶ms_cache_key, + &module, + ).await + } + IsmCachePolicy::MessageSpecific => { + let params_cache_key = (ism.address(), message.id()); + self.base_builder().cache().cache_call_result( + message_domain.name(), + fn_key, + ¶ms_cache_key, + &module, + ).await + } + } + .map_err(|err| { + tracing::warn!(error = %err, "Error when caching call result for {:?}", fn_key); + }) + .ok(); + module + } + }; + self.base.build(module, message, params).await } } diff --git a/rust/main/agents/relayer/src/msg/mod.rs b/rust/main/agents/relayer/src/msg/mod.rs index 2f138327194..c95337526b6 100644 --- a/rust/main/agents/relayer/src/msg/mod.rs +++ b/rust/main/agents/relayer/src/msg/mod.rs @@ -30,9 +30,11 @@ pub(crate) mod blacklist; pub(crate) mod gas_payment; pub(crate) mod metadata; +pub(crate) mod op_batch; pub(crate) mod op_queue; pub(crate) mod op_submitter; pub(crate) mod processor; +mod utils; pub mod pending_message; diff --git a/rust/main/agents/relayer/src/msg/op_batch.rs b/rust/main/agents/relayer/src/msg/op_batch.rs new file mode 100644 index 00000000000..aeec6050c7d --- /dev/null +++ b/rust/main/agents/relayer/src/msg/op_batch.rs @@ -0,0 +1,452 @@ +use std::{sync::Arc, time::Duration}; + +use derive_new::new; +use hyperlane_core::{ + rpc_clients::DEFAULT_MAX_RPC_RETRIES, total_estimated_cost, BatchResult, + ChainCommunicationError, ChainResult, ConfirmReason, HyperlaneDomain, Mailbox, + PendingOperation, PendingOperationStatus, QueueOperation, TxOutcome, +}; +use itertools::{Either, Itertools}; +use tokio::time::sleep; +use tracing::{info, instrument, warn}; + +use super::{ + op_queue::OpQueue, + op_submitter::{submit_single_operation, SerialSubmitterMetrics}, + pending_message::CONFIRM_DELAY, +}; + +const BATCH_RETRY_SLEEP_DURATION: Duration = Duration::from_millis(100); + +#[derive(new, Debug)] +pub(crate) struct OperationBatch { + operations: Vec, + #[allow(dead_code)] + domain: HyperlaneDomain, +} + +impl OperationBatch { + #[instrument(skip_all, fields(domain=%self.domain, batch_size=self.operations.len()))] + pub async fn submit( + self, + prepare_queue: &mut OpQueue, + confirm_queue: &mut OpQueue, + metrics: &SerialSubmitterMetrics, + ) { + let excluded_ops = match self.try_submit_as_batch(metrics).await { + Ok(batch_result) => { + Self::handle_batch_result(self.operations, batch_result, confirm_queue).await + } + Err(e) => { + warn!(error=?e, batch=?self.operations, "Error when submitting batch"); + self.operations + } + }; + + if !excluded_ops.is_empty() { + warn!(excluded_ops=?excluded_ops, "Either operations reverted in the batch or the txid wasn't included. Falling back to serial submission."); + OperationBatch::new(excluded_ops, self.domain) + .submit_serially(prepare_queue, confirm_queue, metrics) + .await; + } + } + + #[instrument(skip(self, metrics), ret, level = "debug")] + async fn try_submit_as_batch( + &self, + metrics: &SerialSubmitterMetrics, + ) -> ChainResult { + // We already assume that the relayer submits to a single mailbox per destination. + // So it's fine to use the first item in the batch to get the mailbox. + let Some(first_item) = self.operations.first() else { + return Err(ChainCommunicationError::BatchIsEmpty); + }; + let Some(mailbox) = first_item.try_get_mailbox() else { + // no need to update the metrics since all operations are excluded + return Ok(BatchResult::failed(self.operations.len())); + }; + let outcome = self + .submit_batch_with_retry(mailbox, DEFAULT_MAX_RPC_RETRIES, BATCH_RETRY_SLEEP_DURATION) + .await?; + let ops_submitted = self.operations.len() - outcome.failed_indexes.len(); + metrics.ops_submitted.inc_by(ops_submitted as u64); + Ok(outcome) + } + + async fn submit_batch_with_retry( + &self, + mailbox: Arc, + max_retries: usize, + sleep_period: Duration, + ) -> ChainResult { + if !mailbox.supports_batching() { + return Ok(BatchResult::failed(self.operations.len())); + } + let mut last_error = None; + let ops = self.operations.iter().collect_vec(); + let op_ids = ops.iter().map(|op| op.id()).collect_vec(); + for retry_number in 1..=max_retries { + match mailbox.process_batch(ops.clone()).await { + Ok(res) => return Ok(res), + Err(err) => { + warn!(retries=retry_number, ?max_retries, error=?err, ids=?op_ids, "Retrying batch submission"); + last_error = Some(err); + sleep(sleep_period).await; + } + } + } + let error = last_error.unwrap_or(ChainCommunicationError::BatchingFailed); + Err(error) + } + + /// Process the operations sent by a batch. + /// Returns the operations that were not sent + async fn handle_batch_result( + operations: Vec, + batch_result: BatchResult, + confirm_queue: &mut OpQueue, + ) -> Vec> { + let (sent_ops, excluded_ops): (Vec<_>, Vec<_>) = + operations.into_iter().enumerate().partition_map(|(i, op)| { + if !batch_result.failed_indexes.contains(&i) { + Either::Left(op) + } else { + Either::Right(op) + } + }); + + if let Some(outcome) = batch_result.outcome { + info!(batch_size=sent_ops.len(), outcome=?outcome, batch=?sent_ops, ?excluded_ops, "Submitted transaction batch"); + Self::update_sent_ops_state(sent_ops, outcome, confirm_queue).await; + } + excluded_ops + } + + async fn update_sent_ops_state( + sent_ops: Vec>, + outcome: TxOutcome, + confirm_queue: &mut OpQueue, + ) { + let total_estimated_cost = total_estimated_cost(sent_ops.as_slice()); + for mut op in sent_ops { + op.set_operation_outcome(outcome.clone(), total_estimated_cost); + op.set_next_attempt_after(CONFIRM_DELAY); + confirm_queue + .push( + op, + Some(PendingOperationStatus::Confirm( + ConfirmReason::SubmittedBySelf, + )), + ) + .await; + } + } + + async fn submit_serially( + self, + prepare_queue: &mut OpQueue, + confirm_queue: &mut OpQueue, + metrics: &SerialSubmitterMetrics, + ) { + for op in self.operations.into_iter() { + submit_single_operation(op, prepare_queue, confirm_queue, metrics).await; + } + } +} + +#[cfg(test)] +mod tests { + + use std::{str::FromStr, sync::Arc}; + + use crate::{ + merkle_tree::builder::MerkleTreeBuilder, + msg::{ + gas_payment::GasPaymentEnforcer, + metadata::{ + BaseMetadataBuilder, DefaultIsmCache, IsmAwareAppContextClassifier, + IsmCachePolicyClassifier, + }, + op_queue::test::MockPendingOperation, + pending_message::{MessageContext, PendingMessage}, + processor::test::{ + dummy_cache_metrics, dummy_submission_metrics, DummyApplicationOperationVerifier, + }, + }, + settings::{ + matching_list::MatchingList, GasPaymentEnforcementConf, GasPaymentEnforcementPolicy, + }, + }; + use ethers::utils::hex; + use hyperlane_base::{ + cache::{LocalCache, MeteredCache, MeteredCacheConfig, OptionalCache}, + db::{HyperlaneRocksDB, DB}, + settings::{ChainConf, ChainConnectionConf, CoreContractAddresses}, + CoreMetrics, + }; + use hyperlane_core::{ + config::OpSubmissionConfig, Decode, HyperlaneMessage, KnownHyperlaneDomain, + MessageSubmissionData, ReorgPeriod, SubmitterType, H160, U256, + }; + use hyperlane_ethereum::{ConnectionConf, RpcConnectionConf}; + use hyperlane_test::mocks::{MockMailboxContract, MockValidatorAnnounceContract}; + use tokio::sync::RwLock; + + use super::*; + + fn dummy_pending_operation( + mailbox: Arc, + domain: HyperlaneDomain, + ) -> Box { + let seconds_to_next_attempt = 10; + let mut mock_pending_operation = + MockPendingOperation::new(seconds_to_next_attempt, domain.clone()); + mock_pending_operation.mailbox = Some(mailbox); + Box::new(mock_pending_operation) as Box + } + + #[tokio::test] + async fn test_handle_batch_result_succeeds() { + let mut mock_mailbox = MockMailboxContract::new(); + let dummy_domain: HyperlaneDomain = KnownHyperlaneDomain::Alfajores.into(); + + mock_mailbox.expect_supports_batching().return_const(true); + mock_mailbox.expect_process_batch().returning(move |_ops| { + let batch_result = BatchResult::new(None, vec![]); + Ok(batch_result) + }); + let mock_mailbox = Arc::new(mock_mailbox) as Arc; + let operation = dummy_pending_operation(mock_mailbox.clone(), dummy_domain.clone()); + + let operations = vec![operation]; + let op_batch = OperationBatch::new(operations, dummy_domain); + let batch_result = op_batch + .submit_batch_with_retry(mock_mailbox, 1, Duration::from_secs(0)) + .await + .unwrap(); + assert!( + batch_result.failed_indexes.is_empty(), + "Batch result should not have failed indexes" + ) + } + + #[tokio::test] + async fn test_handle_batch_result_fails() { + let mut mock_mailbox = MockMailboxContract::new(); + let dummy_domain: HyperlaneDomain = KnownHyperlaneDomain::Alfajores.into(); + + mock_mailbox.expect_supports_batching().return_const(true); + mock_mailbox + .expect_process_batch() + .returning(move |_ops| Err(ChainCommunicationError::BatchingFailed)); + let mock_mailbox = Arc::new(mock_mailbox) as Arc; + let operation = dummy_pending_operation(mock_mailbox.clone(), dummy_domain.clone()); + + let operations = vec![operation]; + let op_batch = OperationBatch::new(operations, dummy_domain); + let result = op_batch + .submit_batch_with_retry(mock_mailbox, 1, Duration::from_secs(0)) + .await; + assert!(matches!( + result, + Err(ChainCommunicationError::BatchingFailed) + )); + } + + #[tokio::test] + async fn test_handle_batch_succeeds_eventually() { + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .try_init(); + let mut mock_mailbox = MockMailboxContract::new(); + let dummy_domain: HyperlaneDomain = KnownHyperlaneDomain::Alfajores.into(); + + let mut counter = 0; + mock_mailbox.expect_supports_batching().return_const(true); + mock_mailbox.expect_process_batch().returning(move |_ops| { + counter += 1; + if counter < 5 { + return Err(ChainCommunicationError::BatchingFailed); + } + let batch_result = BatchResult::new(None, vec![]); + Ok(batch_result) + }); + let mock_mailbox = Arc::new(mock_mailbox) as Arc; + let operation = dummy_pending_operation(mock_mailbox.clone(), dummy_domain.clone()); + + let operations = vec![operation]; + let op_batch = OperationBatch::new(operations, dummy_domain); + let batch_result = op_batch + .submit_batch_with_retry(mock_mailbox, 10, Duration::from_secs(0)) + .await + .unwrap(); + assert!( + batch_result.failed_indexes.is_empty(), + "Batch result should not have failed indexes" + ); + } + + #[tokio::test] + async fn test_handle_batch_result_fails_if_not_supported() { + let mut mock_mailbox = MockMailboxContract::new(); + let dummy_domain: HyperlaneDomain = KnownHyperlaneDomain::Alfajores.into(); + + mock_mailbox.expect_supports_batching().return_const(false); + mock_mailbox.expect_process_batch().returning(move |_ops| { + let batch_result = BatchResult::new(None, vec![]); + Ok(batch_result) + }); + let mock_mailbox = Arc::new(mock_mailbox) as Arc; + let operation = dummy_pending_operation(mock_mailbox.clone(), dummy_domain.clone()); + + let operations = vec![operation]; + let op_batch = OperationBatch::new(operations, dummy_domain); + let batch_result = op_batch + .submit_batch_with_retry(mock_mailbox, 1, Duration::from_secs(0)) + .await + .unwrap(); + assert!( + batch_result.failed_indexes.len() == 1, + "Batching should fail if not supported" + ) + } + + #[tokio::test] + #[ignore] + async fn benchmarking_with_real_rpcs() { + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .try_init(); + + let arb_chain_conf = ChainConf { + domain: HyperlaneDomain::Known(hyperlane_core::KnownHyperlaneDomain::Arbitrum), + // TODO + signer: None, + submitter: SubmitterType::Classic, + estimated_block_time: Duration::from_secs(1), + reorg_period: ReorgPeriod::from_blocks(10), + addresses: CoreContractAddresses { + mailbox: H160::from_str("0x979Ca5202784112f4738403dBec5D0F3B9daabB9") + .unwrap() + .into(), + validator_announce: H160::from_str("0x1df063280C4166AF9a725e3828b4dAC6c7113B08") + .unwrap() + .into(), + ..Default::default() + }, + connection: ChainConnectionConf::Ethereum(ConnectionConf { + rpc_connection: RpcConnectionConf::HttpFallback { + urls: vec![ + "https://arbitrum.drpc.org".parse().unwrap(), + "https://endpoints.omniatech.io/v1/arbitrum/one/public" + .parse() + .unwrap(), + ], + }, + transaction_overrides: Default::default(), + op_submission_config: OpSubmissionConfig { + batch_contract_address: None, + max_batch_size: 32, + bypass_batch_simulation: false, + ..Default::default() + }, + }), + metrics_conf: Default::default(), + index: Default::default(), + }; + + // https://explorer.hyperlane.xyz/message/0x29160a18c6e27c2f14ebe021207ac3f90664507b9c5aacffd802b2afcc15788a + // Base -> Arbitrum, uses the default ISM + let message_bytes = hex::decode("0300139ebf000021050000000000000000000000005454cf5584939f7f884e95dba33fecd6d40b8fe20000a4b1000000000000000000000000fd34afdfbac1e47afc539235420e4be4a206f26d0000000000000000000000008650ee37ba2b0a8ac5954a04b46ee07093eab7f90000000000000000000000000000000000000000000000004563918244f40000").unwrap(); + let message = HyperlaneMessage::read_from(&mut &message_bytes[..]).unwrap(); + let base_domain = HyperlaneDomain::new_test_domain("base"); + let temp_dir = tempfile::tempdir().unwrap(); + let db = DB::from_path(temp_dir.path()).unwrap(); + let base_db = HyperlaneRocksDB::new(&base_domain, db); + + let core_metrics = CoreMetrics::new("test", 9090, Default::default()).unwrap(); + let arb_mailbox: Arc = arb_chain_conf + .build_mailbox(&core_metrics) + .await + .unwrap() + .into(); + + let cache = OptionalCache::new(Some(MeteredCache::new( + LocalCache::new("test-cache"), + dummy_cache_metrics(), + MeteredCacheConfig { + cache_name: "test-cache".to_owned(), + }, + ))); + let base_va = Arc::new(MockValidatorAnnounceContract::default()); + let default_ism_getter = DefaultIsmCache::new(arb_mailbox.clone()); + let core_metrics = Arc::new(core_metrics); + let metadata_builder = BaseMetadataBuilder::new( + base_domain.clone(), + arb_chain_conf.clone(), + Arc::new(RwLock::new(MerkleTreeBuilder::new())), + base_va, + false, + core_metrics.clone(), + cache.clone(), + base_db.clone(), + IsmAwareAppContextClassifier::new(default_ism_getter.clone(), vec![]), + IsmCachePolicyClassifier::new(default_ism_getter, Default::default()), + ); + let message_context = Arc::new(MessageContext { + destination_mailbox: arb_mailbox, + origin_db: Arc::new(base_db.clone()), + cache: cache.clone(), + metadata_builder: Arc::new(metadata_builder), + origin_gas_payment_enforcer: Arc::new(GasPaymentEnforcer::new( + vec![GasPaymentEnforcementConf { + policy: GasPaymentEnforcementPolicy::None, + matching_list: MatchingList::default(), + }], + base_db.clone(), + )), + transaction_gas_limit: Default::default(), + metrics: dummy_submission_metrics(), + application_operation_verifier: Some(Arc::new(DummyApplicationOperationVerifier {})), + }); + + let attempts = 2; + let batch_size = 32; + + let mut pending_messages = vec![]; + // Message found here https://basescan.org/tx/0x65345812a1f7df6236292d52d50418a090c84e2c901912bede6cadb9810a9882#eventlog + let metadata = + "0x000000100000001000000010000001680000000000000000000000100000015800000000000000000000000019dc38aeae620380430c200a6e990d5af5480117dbd3d5e656de9dcf604fcc90b52a3b97d9f3573b4a0733e824f1358e515698cf00139eaa5452e030aa937f6b14162a44ec3327f6832bbf16e4b0d6df452524af1c1a04e875b4ce7ac0da92aa08838a89f2a126eef23f6b6a08b6cdbe9e9e804b321088b91b034f9466eed2da1dcc36cb220b887b15f3e111a179142c27e4a0b6d6b7a291e22577d6296d82b7c3f29e8989ec1161d853aba0982b2db28b9a9917226c2c27111c41c99e6a84e7717740f901528062385e659b4330e7227593a334be532d27bcf24f3f13bf4fc1a860e96f8d6937984ea83ef61c8ea30d48cc903f6ff725406a4d1ce73f46064b3403ea4c720b770f4389d7259b275f085c6a98cef9a04880a249b42c382ba34a63031debbfb5b9b232ffd9ee45ff63a7249e83c7e9720f9e978a431b".as_bytes().to_vec(); + + for b in 0..batch_size { + let mut pending_message = PendingMessage::new( + message.clone(), + message_context.clone(), + PendingOperationStatus::FirstPrepareAttempt, + Some(format!("test-{}", b)), + attempts, + ); + pending_message.submission_data = Some(Box::new(MessageSubmissionData { + metadata: metadata.clone(), + gas_limit: U256::from(615293), + })); + pending_messages.push(pending_message); + } + + let arb_domain = HyperlaneDomain::new_test_domain("arbitrum"); + let serial_submitter_metrics = + SerialSubmitterMetrics::new(core_metrics.clone(), &arb_domain); + + let operation_batch = OperationBatch::new( + pending_messages + .into_iter() + .map(|msg| Box::new(msg) as Box) + .collect(), + arb_domain, + ); + operation_batch + .try_submit_as_batch(&serial_submitter_metrics) + .await + .unwrap(); + } +} diff --git a/rust/main/agents/relayer/src/msg/op_queue.rs b/rust/main/agents/relayer/src/msg/op_queue.rs index 7f382f6dc15..9cdabfc8eb8 100644 --- a/rust/main/agents/relayer/src/msg/op_queue.rs +++ b/rust/main/agents/relayer/src/msg/op_queue.rs @@ -153,29 +153,37 @@ impl OpQueue { queue.append(&mut reprioritized_queue); retry_responses } + + pub async fn len(&self) -> usize { + let queue = self.queue.lock().await; + queue.len() + } } #[cfg(test)] pub mod test { - use crate::{ - server::ENDPOINT_MESSAGES_QUEUE_SIZE, - settings::matching_list::{Filter, ListElement, MatchingList}, - }; - - use super::*; - use hyperlane_core::{ - HyperlaneDomain, HyperlaneDomainProtocol, HyperlaneDomainTechnicalStack, - HyperlaneDomainType, HyperlaneMessage, KnownHyperlaneDomain, PendingOperationResult, - TryBatchAs, TxOutcome, H256, U256, - }; - use serde::Serialize; use std::{ collections::VecDeque, str::FromStr, time::{Duration, Instant}, }; + + use serde::Serialize; use tokio::sync::{self, mpsc}; + use hyperlane_core::{ + ChainResult, HyperlaneDomain, HyperlaneDomainProtocol, HyperlaneDomainTechnicalStack, + HyperlaneDomainType, HyperlaneMessage, KnownHyperlaneDomain, Mailbox, + PendingOperationResult, ReprepareReason, TryBatchAs, TxOutcome, H256, U256, + }; + + use crate::{ + server::ENDPOINT_MESSAGES_QUEUE_SIZE, + settings::matching_list::{Filter, ListElement, MatchingList}, + }; + + use super::*; + #[derive(Debug, Clone, Serialize)] pub struct MockPendingOperation { id: H256, @@ -186,6 +194,8 @@ pub mod test { seconds_to_next_attempt: u64, destination_domain: HyperlaneDomain, retry_count: u32, + #[serde(skip)] + pub mailbox: Option>, } impl MockPendingOperation { @@ -199,6 +209,7 @@ pub mod test { recipient_address: H256::random(), origin_domain_id: 0, retry_count: 0, + mailbox: None, } } @@ -218,6 +229,7 @@ pub mod test { domain_protocol: HyperlaneDomainProtocol::Ethereum, domain_technical_stack: HyperlaneDomainTechnicalStack::Other, }, + mailbox: None, } } @@ -367,6 +379,18 @@ pub mod test { fn get_retries(&self) -> u32 { self.retry_count } + + async fn payload(&self) -> ChainResult> { + todo!() + } + + fn on_reprepare( + &mut self, + _err_msg: Option, + _: ReprepareReason, + ) -> PendingOperationResult { + todo!() + } } pub fn dummy_metrics_and_label() -> (IntGaugeVec, String) { diff --git a/rust/main/agents/relayer/src/msg/op_submitter.rs b/rust/main/agents/relayer/src/msg/op_submitter.rs index f3fd266bad1..7ae0d9f1364 100644 --- a/rust/main/agents/relayer/src/msg/op_submitter.rs +++ b/rust/main/agents/relayer/src/msg/op_submitter.rs @@ -1,39 +1,33 @@ #![allow(clippy::doc_markdown)] // TODO: `rustc` 1.80.1 clippy issue #![allow(clippy::doc_lazy_continuation)] // TODO: `rustc` 1.80.1 clippy issue +use std::fmt::Debug; use std::sync::Arc; use std::time::Duration; -use derive_new::new; use futures::future::join_all; use futures_util::future::try_join_all; -use hyperlane_core::total_estimated_cost; -use hyperlane_core::BatchResult; -use hyperlane_core::ConfirmReason::*; -use hyperlane_core::PendingOperation; -use hyperlane_core::PendingOperationStatus; -use hyperlane_core::ReprepareReason; -use itertools::Either; -use itertools::Itertools; +use num_traits::Zero; use prometheus::{IntCounter, IntGaugeVec}; -use tokio::sync::broadcast::Sender; -use tokio::sync::mpsc; -use tokio::sync::Mutex; +use tokio::sync::{broadcast::Sender, mpsc, Mutex}; use tokio::task::JoinHandle; use tokio::time::sleep; use tokio_metrics::TaskMonitor; -use tracing::{debug, info_span, instrument, instrument::Instrumented, trace, Instrument}; -use tracing::{info, warn}; +use tracing::{debug, error, info_span, instrument, trace, warn, Instrument}; +use hyperlane_base::db::{HyperlaneDb, HyperlaneRocksDB}; use hyperlane_base::CoreMetrics; use hyperlane_core::{ - ChainCommunicationError, ChainResult, HyperlaneDomain, HyperlaneDomainProtocol, - PendingOperationResult, QueueOperation, TxOutcome, + ConfirmReason::{self, *}, + HyperlaneDomain, HyperlaneDomainProtocol, PendingOperationResult, PendingOperationStatus, + QueueOperation, ReprepareReason, }; +use submitter::{Entrypoint, FullPayload, PayloadDispatcherEntrypoint, PayloadId}; use crate::msg::pending_message::CONFIRM_DELAY; use crate::server::MessageRetryRequest; +use super::op_batch::OperationBatch; use super::op_queue::OpQueue; use super::op_queue::OperationPriorityQueue; @@ -90,7 +84,6 @@ pub const SUBMITTER_QUEUE_COUNT: usize = 3; /// eligible for submission, we should be working on it within reason. This /// must be balanced with the cost of making RPCs that will almost certainly /// fail and potentially block new messages from being sent immediately. -#[derive(Debug)] pub struct SerialSubmitter { /// Domain this submitter delivers to. domain: HyperlaneDomain, @@ -100,21 +93,28 @@ pub struct SerialSubmitter { metrics: SerialSubmitterMetrics, /// Max batch size for submitting messages max_batch_size: u32, + max_submit_queue_len: Option, /// tokio task monitor task_monitor: TaskMonitor, prepare_queue: OpQueue, submit_queue: OpQueue, confirm_queue: OpQueue, + payload_dispatcher_entrypoint: Option, + db: Arc, } impl SerialSubmitter { + #[allow(clippy::too_many_arguments)] pub fn new( domain: HyperlaneDomain, rx: mpsc::UnboundedReceiver, retry_op_transmitter: &Sender, metrics: SerialSubmitterMetrics, max_batch_size: u32, + max_submit_queue_len: Option, task_monitor: TaskMonitor, + payload_dispatcher_entrypoint: Option, + db: HyperlaneRocksDB, ) -> Self { let prepare_queue = OpQueue::new( metrics.submitter_queue_length.clone(), @@ -138,10 +138,13 @@ impl SerialSubmitter { rx: Some(rx), metrics, max_batch_size, + max_submit_queue_len, task_monitor, prepare_queue, submit_queue, confirm_queue, + payload_dispatcher_entrypoint, + db: Arc::new(db), } } @@ -149,31 +152,43 @@ impl SerialSubmitter { self.prepare_queue.queue.clone() } - pub fn spawn(self) -> Instrumented> { + pub fn spawn(self) -> JoinHandle<()> { let span = info_span!("SerialSubmitter", destination=%self.domain); let task_monitor = self.task_monitor.clone(); let name = Self::task_name("", &self.domain); tokio::task::Builder::new() .name(&name) - .spawn(TaskMonitor::instrument(&task_monitor, async move { - self.run().await - })) + .spawn(TaskMonitor::instrument( + &task_monitor, + async move { self.run().await }.instrument(span), + )) .expect("spawning tokio task from Builder is infallible") - .instrument(span) } async fn run(mut self) { let rx_prepare = self.rx.take().expect("rx should be initialised"); + let entrypoint = self.payload_dispatcher_entrypoint.take().map(Arc::new); + + let submit_task = match &entrypoint { + None => self.create_classic_submit_task(), + Some(entrypoint) => self.create_lander_submit_task(entrypoint.clone()), + }; + + let confirm_task = match &entrypoint { + None => self.create_classic_confirm_task(), + Some(entrypoint) => self.create_lander_confirm_task(entrypoint.clone()), + }; + let tasks = [ self.create_receive_task(rx_prepare), self.create_prepare_task(), - self.create_submit_task(), - self.create_confirm_task(), + submit_task, + confirm_task, ]; if let Err(err) = try_join_all(tasks).await { - tracing::error!( + error!( error=?err, domain=?self.domain, "SerialSubmitter task panicked for domain" @@ -207,19 +222,20 @@ impl SerialSubmitter { self.submit_queue.clone(), self.confirm_queue.clone(), self.max_batch_size, + self.max_submit_queue_len, self.metrics.clone(), ), )) .expect("spawning tokio task from Builder is infallible") } - fn create_submit_task(&self) -> JoinHandle<()> { - let name = Self::task_name("submit::", &self.domain); + fn create_classic_submit_task(&self) -> JoinHandle<()> { + let name = Self::task_name("submit_classic::", &self.domain); tokio::task::Builder::new() .name(&name) .spawn(TaskMonitor::instrument( &self.task_monitor, - submit_task( + submit_classic_task( self.domain.clone(), self.prepare_queue.clone(), self.submit_queue.clone(), @@ -231,18 +247,63 @@ impl SerialSubmitter { .expect("spawning tokio task from Builder is infallible") } - fn create_confirm_task(&self) -> JoinHandle<()> { - let name = Self::task_name("confirm::", &self.domain); + fn create_classic_confirm_task(&self) -> JoinHandle<()> { + let name = Self::task_name("confirm_classic::", &self.domain); + tokio::task::Builder::new() + .name(&name) + .spawn(TaskMonitor::instrument( + &self.task_monitor, + confirm_classic_task( + self.domain.clone(), + self.prepare_queue.clone(), + self.confirm_queue.clone(), + self.max_batch_size, + self.metrics.clone(), + ), + )) + .expect("spawning tokio task from Builder is infallible") + } + + fn create_lander_submit_task( + &self, + entrypoint: Arc, + ) -> JoinHandle<()> { + let name = Self::task_name("submit_lander::", &self.domain); + tokio::task::Builder::new() + .name(&name) + .spawn(TaskMonitor::instrument( + &self.task_monitor, + submit_lander_task( + entrypoint, + self.domain.clone(), + self.prepare_queue.clone(), + self.submit_queue.clone(), + self.confirm_queue.clone(), + self.max_batch_size, + self.metrics.clone(), + self.db.clone(), + ), + )) + .expect("spawning tokio task from Builder is infallible") + } + + fn create_lander_confirm_task( + &self, + entrypoint: Arc, + ) -> JoinHandle<()> { + let name = Self::task_name("confirm_lander::", &self.domain); tokio::task::Builder::new() .name(&name) .spawn(TaskMonitor::instrument( &self.task_monitor, - confirm_task( + confirm_lander_task( + entrypoint, self.domain.clone(), self.prepare_queue.clone(), self.confirm_queue.clone(), self.max_batch_size, self.metrics.clone(), + self.db.clone(), ), )) .expect("spawning tokio task from Builder is infallible") @@ -277,11 +338,26 @@ async fn prepare_task( submit_queue: OpQueue, confirm_queue: OpQueue, max_batch_size: u32, + max_submit_queue_len: Option, metrics: SerialSubmitterMetrics, ) { // Prepare at most `max_batch_size` ops at a time to avoid getting rate-limited let ops_to_prepare = max_batch_size as usize; loop { + // Apply backpressure to the prepare queue if the submit queue is too long. + if let Some(max_len) = max_submit_queue_len { + let submit_queue_len = submit_queue.len().await as u32; + if submit_queue_len >= max_len { + debug!( + %submit_queue_len, + max_submit_queue_len=%max_len, + "Submit queue is too long, waiting to prepare more ops" + ); + // The submit queue is too long, so give some time before checking again + sleep(Duration::from_millis(150)).await; + continue; + } + } // Pop messages here according to the configured batch. let mut batch = prepare_queue.pop_many(ops_to_prepare).await; if batch.is_empty() { @@ -346,7 +422,7 @@ async fn prepare_task( } #[instrument(skip_all, fields(%domain))] -async fn submit_task( +async fn submit_classic_task( domain: HyperlaneDomain, mut prepare_queue: OpQueue, mut submit_queue: OpQueue, @@ -377,8 +453,96 @@ async fn submit_task( } } +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all, fields(domain=%_domain))] +async fn submit_lander_task( + entrypoint: Arc, + _domain: HyperlaneDomain, // used for instrumentation only + prepare_queue: OpQueue, + mut submit_queue: OpQueue, + confirm_queue: OpQueue, + max_batch_size: u32, + metrics: SerialSubmitterMetrics, + db: Arc, +) { + let recv_limit = max_batch_size as usize; + loop { + let batch = submit_queue.pop_many(recv_limit).await; + for op in batch.into_iter() { + submit_via_lander( + op, + &entrypoint, + &prepare_queue, + &confirm_queue, + &metrics, + db.clone(), + ) + .await; + } + } +} + +async fn submit_via_lander( + op: QueueOperation, + entrypoint: &Arc, + prepare_queue: &OpQueue, + confirm_queue: &OpQueue, + metrics: &SerialSubmitterMetrics, + db: Arc, +) { + let operation_payload = match op.payload().await { + Ok(p) => p, + Err(e) => { + let reason = ReprepareReason::ErrorCreatingPayload; + let msg = "Error creating payload"; + prepare_op(op, prepare_queue, e, msg, reason).await; + return; + } + }; + + let message_id = op.id(); + let metadata = message_id.to_string(); + let mailbox = op + .try_get_mailbox() + .expect("Operation should contain Mailbox address") + .address(); + let payload_id = PayloadId::random(); + let payload = FullPayload::new(payload_id, metadata, operation_payload, mailbox); + + if let Err(e) = entrypoint.send_payload(&payload).await { + let reason = ReprepareReason::ErrorSubmitting; + let msg = "Error sending payload"; + prepare_op(op, prepare_queue, e, msg, reason).await; + return; + } + + if let Err(e) = db.store_payload_id_by_message_id(&message_id, &payload.details.id) { + let reason = ReprepareReason::ErrorStoringPayloadIdByMessageId; + let msg = "Error storing mapping from message id to payload id"; + prepare_op(op, prepare_queue, e, msg, reason).await; + return; + } + + confirm_op(op, confirm_queue, metrics).await; +} + +async fn prepare_op( + mut op: QueueOperation, + prepare_queue: &OpQueue, + err: impl Debug, + msg: &str, + reason: ReprepareReason, +) { + use PendingOperationStatus::Retry; + + let status = Retry(reason.clone()); + let result = op.on_reprepare(Some(format!("{:?}", err)), reason); + warn!(?err, ?status, ?result, msg); + prepare_queue.push(op, Some(status)).await; +} + #[instrument(skip(prepare_queue, confirm_queue, metrics), ret, level = "debug")] -async fn submit_single_operation( +pub(crate) async fn submit_single_operation( mut op: QueueOperation, prepare_queue: &mut OpQueue, confirm_queue: &mut OpQueue, @@ -415,7 +579,7 @@ async fn submit_single_operation( async fn confirm_op( mut op: QueueOperation, - confirm_queue: &mut OpQueue, + confirm_queue: &OpQueue, metrics: &SerialSubmitterMetrics, ) { let destination = op.destination_domain().clone(); @@ -438,7 +602,7 @@ async fn confirm_op( } #[instrument(skip_all, fields(%domain))] -async fn confirm_task( +async fn confirm_classic_task( domain: HyperlaneDomain, prepare_queue: OpQueue, mut confirm_queue: OpQueue, @@ -466,9 +630,9 @@ async fn confirm_task( ) }); let op_results = join_all(futures).await; - if op_results.iter().all(|op| { + if op_results.iter().all(|op_result| { matches!( - op, + op_result, PendingOperationResult::NotReady | PendingOperationResult::Confirm(_) ) }) { @@ -479,6 +643,114 @@ async fn confirm_task( } } +#[instrument(skip_all, fields(%domain))] +async fn confirm_lander_task( + entrypoint: Arc, + domain: HyperlaneDomain, + prepare_queue: OpQueue, + mut confirm_queue: OpQueue, + max_batch_size: u32, + metrics: SerialSubmitterMetrics, + db: Arc, +) { + let recv_limit = max_batch_size as usize; + loop { + // Pick the next message to try confirming. + let batch = confirm_queue.pop_many(recv_limit).await; + + if batch.is_empty() { + // queue is empty so give some time before checking again to prevent burning CPU + sleep(Duration::from_millis(200)).await; + continue; + } + // cannot use `join_all` here because db reads are blocking + let payload_id_results = batch + .into_iter() + .map(|op| { + let message_id = op.id(); + (op, db.retrieve_payload_id_by_message_id(&message_id)) + }) + .collect::>(); + + let payload_status_result_futures = payload_id_results + .into_iter() + .map(|(op, result)| async { + let message_id = op.id(); + match result { + Ok(Some(payload_id)) => Some((op, entrypoint.payload_status(payload_id).await)), + Ok(None) | Err(_) => { + error!( + ?op, + %message_id, + "Error retrieving payload id by message id", + ); + send_back_on_failed_submisison( + op, + prepare_queue.clone(), + &metrics, + Some(&ReprepareReason::ErrorRetrievingPayloadId), + ) + .await; + None + } + } + }) + .collect::>(); + + let payload_status_results = join_all(payload_status_result_futures) + .await + .into_iter() + .flatten() + .collect::>(); + + let confirmed_operations = Arc::new(Mutex::new(0)); + let confirm_futures = payload_status_results + .into_iter() + .map(|(op, status_result)| async { + let Ok(payload_status) = status_result else { + send_back_on_failed_submisison( + op, + prepare_queue.clone(), + &metrics, + Some(&ReprepareReason::ErrorRetrievingPayloadId), + ) + .await; + return; + }; + if payload_status.is_finalized() { + { + let mut lock = confirmed_operations.lock().await; + *lock += 1; + } + confirm_operation( + op, + domain.clone(), + prepare_queue.clone(), + confirm_queue.clone(), + metrics.clone(), + ) + .await; + } else { + process_confirm_result( + op, + prepare_queue.clone(), + confirm_queue.clone(), + metrics.clone(), + PendingOperationResult::Confirm(ConfirmReason::SubmittedBySelf), + ) + .await; + } + }) + .collect::>(); + let _ = join_all(confirm_futures).await; + if confirmed_operations.lock().await.is_zero() { + // None of the operations are ready, so wait for a little bit + // before checking again to prevent burning CPU + sleep(Duration::from_millis(500)).await; + } + } +} + async fn confirm_operation( mut op: QueueOperation, domain: HyperlaneDomain, @@ -490,6 +762,16 @@ async fn confirm_operation( debug_assert_eq!(*op.destination_domain(), domain); let operation_result = op.confirm().await; + process_confirm_result(op, prepare_queue, confirm_queue, metrics, operation_result).await +} + +async fn process_confirm_result( + op: QueueOperation, + prepare_queue: OpQueue, + confirm_queue: OpQueue, + metrics: SerialSubmitterMetrics, + operation_result: PendingOperationResult, +) -> PendingOperationResult { match &operation_result { PendingOperationResult::Success => { debug!(?op, "Operation confirmed"); @@ -506,10 +788,7 @@ async fn confirm_operation( .await; } PendingOperationResult::Reprepare(reason) => { - metrics.ops_failed.inc(); - prepare_queue - .push(op, Some(PendingOperationStatus::Retry(reason.clone()))) - .await; + send_back_on_failed_submisison(op, prepare_queue.clone(), &metrics, Some(reason)).await; } PendingOperationResult::Drop => { metrics.ops_dropped.inc(); @@ -519,140 +798,54 @@ async fn confirm_operation( operation_result } +async fn send_back_on_failed_submisison( + op: QueueOperation, + prepare_queue: OpQueue, + metrics: &SerialSubmitterMetrics, + maybe_reason: Option<&ReprepareReason>, +) { + metrics.ops_failed.inc(); + let reason = maybe_reason.unwrap_or(&ReprepareReason::ErrorSubmitting); + prepare_queue + .push(op, Some(PendingOperationStatus::Retry(reason.clone()))) + .await; +} + #[derive(Debug, Clone)] pub struct SerialSubmitterMetrics { - submitter_queue_length: IntGaugeVec, - ops_prepared: IntCounter, - ops_submitted: IntCounter, - ops_confirmed: IntCounter, - ops_failed: IntCounter, - ops_dropped: IntCounter, + pub(crate) submitter_queue_length: IntGaugeVec, + pub(crate) ops_prepared: IntCounter, + pub(crate) ops_submitted: IntCounter, + pub(crate) ops_confirmed: IntCounter, + pub(crate) ops_failed: IntCounter, + pub(crate) ops_dropped: IntCounter, } impl SerialSubmitterMetrics { - pub fn new(metrics: &CoreMetrics, destination: &HyperlaneDomain) -> Self { + pub fn new(metrics: impl AsRef, destination: &HyperlaneDomain) -> Self { let destination = destination.name(); Self { - submitter_queue_length: metrics.submitter_queue_length(), + submitter_queue_length: metrics.as_ref().submitter_queue_length(), ops_prepared: metrics + .as_ref() .operations_processed_count() .with_label_values(&["prepared", destination]), ops_submitted: metrics + .as_ref() .operations_processed_count() .with_label_values(&["submitted", destination]), ops_confirmed: metrics + .as_ref() .operations_processed_count() .with_label_values(&["confirmed", destination]), ops_failed: metrics + .as_ref() .operations_processed_count() .with_label_values(&["failed", destination]), ops_dropped: metrics + .as_ref() .operations_processed_count() .with_label_values(&["dropped", destination]), } } } - -#[derive(new, Debug)] -struct OperationBatch { - operations: Vec, - #[allow(dead_code)] - domain: HyperlaneDomain, -} - -impl OperationBatch { - async fn submit( - self, - prepare_queue: &mut OpQueue, - confirm_queue: &mut OpQueue, - metrics: &SerialSubmitterMetrics, - ) { - let excluded_ops = match self.try_submit_as_batch(metrics).await { - Ok(batch_result) => { - Self::handle_batch_result(self.operations, batch_result, confirm_queue).await - } - Err(e) => { - warn!(error=?e, batch=?self.operations, "Error when submitting batch"); - self.operations - } - }; - - if !excluded_ops.is_empty() { - warn!(excluded_ops=?excluded_ops, "Either operations reverted in the batch or the txid wasn't included. Falling back to serial submission."); - OperationBatch::new(excluded_ops, self.domain) - .submit_serially(prepare_queue, confirm_queue, metrics) - .await; - } - } - - #[instrument(skip(metrics), ret, level = "debug")] - async fn try_submit_as_batch( - &self, - metrics: &SerialSubmitterMetrics, - ) -> ChainResult { - // We already assume that the relayer submits to a single mailbox per destination. - // So it's fine to use the first item in the batch to get the mailbox. - let Some(first_item) = self.operations.first() else { - return Err(ChainCommunicationError::BatchIsEmpty); - }; - let outcome = if let Some(mailbox) = first_item.try_get_mailbox() { - mailbox - .try_process_batch(self.operations.iter().collect_vec()) - .await? - } else { - BatchResult::failed(self.operations.len()) - }; - let ops_submitted = self.operations.len() - outcome.failed_indexes.len(); - metrics.ops_submitted.inc_by(ops_submitted as u64); - Ok(outcome) - } - - /// Process the operations sent by a batch. - /// Returns the operations that were not sent - async fn handle_batch_result( - operations: Vec, - batch_result: BatchResult, - confirm_queue: &mut OpQueue, - ) -> Vec> { - let (sent_ops, excluded_ops): (Vec<_>, Vec<_>) = - operations.into_iter().enumerate().partition_map(|(i, op)| { - if !batch_result.failed_indexes.contains(&i) { - Either::Left(op) - } else { - Either::Right(op) - } - }); - - if let Some(outcome) = batch_result.outcome { - info!(batch_size=sent_ops.len(), outcome=?outcome, batch=?sent_ops, ?excluded_ops, "Submitted transaction batch"); - Self::update_sent_ops_state(sent_ops, outcome, confirm_queue).await; - } - excluded_ops - } - - async fn update_sent_ops_state( - sent_ops: Vec>, - outcome: TxOutcome, - confirm_queue: &mut OpQueue, - ) { - let total_estimated_cost = total_estimated_cost(sent_ops.as_slice()); - for mut op in sent_ops { - op.set_operation_outcome(outcome.clone(), total_estimated_cost); - op.set_next_attempt_after(CONFIRM_DELAY); - confirm_queue - .push(op, Some(PendingOperationStatus::Confirm(SubmittedBySelf))) - .await; - } - } - - async fn submit_serially( - self, - prepare_queue: &mut OpQueue, - confirm_queue: &mut OpQueue, - metrics: &SerialSubmitterMetrics, - ) { - for op in self.operations.into_iter() { - submit_single_operation(op, prepare_queue, confirm_queue, metrics).await; - } - } -} diff --git a/rust/main/agents/relayer/src/msg/pending_message.rs b/rust/main/agents/relayer/src/msg/pending_message.rs index 3a833af21db..5f7c09b024b 100644 --- a/rust/main/agents/relayer/src/msg/pending_message.rs +++ b/rust/main/agents/relayer/src/msg/pending_message.rs @@ -9,11 +9,14 @@ use std::{ use async_trait::async_trait; use derive_new::new; use eyre::Result; -use prometheus::{IntCounter, IntGauge}; -use serde::Serialize; +use prometheus::IntGauge; +use serde::{de::DeserializeOwned, Serialize}; use tracing::{debug, error, info, info_span, instrument, trace, warn, Instrument, Level}; -use hyperlane_base::{db::HyperlaneDb, CoreMetrics}; +use hyperlane_base::{ + cache::{FunctionCallCache, LocalCache, MeteredCache, OptionalCache}, + db::HyperlaneDb, +}; use hyperlane_core::{ gas_used_by_operation, BatchItem, ChainCommunicationError, ChainResult, ConfirmReason, FixedPointNumber, HyperlaneChain, HyperlaneDomain, HyperlaneMessage, Mailbox, @@ -22,11 +25,14 @@ use hyperlane_core::{ }; use hyperlane_operation_verifier::ApplicationOperationVerifier; -use crate::msg::metadata::{MessageMetadataBuildParams, MetadataBuildError}; +use crate::{ + metrics::message_submission::{MessageSubmissionMetrics, MetadataBuildMetric}, + msg::metadata::{MessageMetadataBuildParams, MetadataBuildError}, +}; use super::{ gas_payment::{GasPaymentEnforcer, GasPolicyStatus}, - metadata::{BuildsBaseMetadata, MessageMetadataBuilder, MetadataBuilder}, + metadata::{BuildsBaseMetadata, MessageMetadataBuilder, Metadata, MetadataBuilder}, }; /// a default of 66 is picked, so messages are retried for 2 weeks (period confirmed by @nambrot) before being skipped. @@ -42,6 +48,8 @@ pub const CONFIRM_DELAY: Duration = if cfg!(any(test, feature = "test-utils")) { }; pub const RETRIEVED_MESSAGE_LOG: &str = "Message status retrieved from db"; +pub const USE_CACHE_METADATA_LOG: &str = "Reusing cached metadata"; +pub const INVALIDATE_CACHE_METADATA_LOG: &str = "Invalidating cached metadata"; pub const ISM_MAX_DEPTH: u32 = 13; pub const ISM_MAX_COUNT: u32 = 100; @@ -58,6 +66,8 @@ pub struct MessageContext { pub destination_mailbox: Arc, /// Origin chain database to verify gas payments. pub origin_db: Arc, + /// Cache to store commonly used data calls. + pub cache: OptionalCache>, /// Used to construct the ISM metadata needed to verify a message from the /// origin. pub metadata_builder: Arc, @@ -86,7 +96,7 @@ pub struct PendingMessage { submitted: bool, #[new(default)] #[serde(skip_serializing)] - submission_data: Option>, + pub(crate) submission_data: Option>, #[new(default)] num_retries: u32, #[new(value = "Instant::now()")] @@ -236,17 +246,10 @@ impl PendingOperation for PendingMessage { return PendingOperationResult::Confirm(ConfirmReason::AlreadySubmitted); } - let provider = self.ctx.destination_mailbox.provider(); - // We cannot deliver to an address that is not a contract so check and drop if it isn't. - let is_contract = match provider.is_contract(&self.message.recipient).await { + let is_contract = match self.is_recipient_contract().await { Ok(is_contract) => is_contract, - Err(err) => { - return self.on_reprepare( - Some(err), - ReprepareReason::ErrorCheckingIfRecipientIsContract, - ); - } + Err(reprepare_reason) => return reprepare_reason, }; if !is_contract { info!( @@ -266,101 +269,69 @@ impl PendingOperation for PendingMessage { return op_result; } - let ism_address = match self - .ctx - .destination_mailbox - .recipient_ism(self.message.recipient) - .await - { - Ok(ism_address) => ism_address, - Err(err) => { - return self.on_reprepare(Some(err), ReprepareReason::ErrorFetchingIsmAddress); - } - }; - - let message_metadata_builder = match MessageMetadataBuilder::new( - self.ctx.metadata_builder.clone(), - ism_address, - &self.message, - ) - .await - { - Ok(message_metadata_builder) => message_metadata_builder, - Err(err) => { - return self.on_reprepare(Some(err), ReprepareReason::ErrorGettingMetadataBuilder); + // If metadata is already built, check gas estimation works. + // If gas estimation fails, invalidate cache and rebuild it again. + let tx_cost_estimate = match self.metadata.as_ref() { + Some(metadata) => { + match self + .ctx + .destination_mailbox + .process_estimate_costs(&self.message, metadata) + .await + { + Ok(s) => { + tracing::debug!(USE_CACHE_METADATA_LOG); + Some(s) + } + Err(_) => { + self.clear_metadata(); + None + } + } } + None => None, }; - let params = MessageMetadataBuildParams::default(); - - let metadata_bytes = match message_metadata_builder - .build(ism_address, &self.message, params) - .await - { - Ok(metadata) => { - let metadata_bytes = metadata.to_vec(); - self.metadata = Some(metadata_bytes.clone()); - metadata_bytes + let metadata_bytes = match self.metadata.as_ref() { + Some(metadata) => { + tracing::debug!(USE_CACHE_METADATA_LOG); + metadata.clone() } - Err(err) => { - match &err { - MetadataBuildError::FailedToBuild(_) => { - return self - .on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata); - } - MetadataBuildError::CouldNotFetch => { - return self - .on_reprepare::(None, ReprepareReason::CouldNotFetchMetadata); - } - // If the metadata building is refused, we still allow it to be retried later. - MetadataBuildError::Refused(reason) => { - warn!(?reason, "Metadata building refused"); - return self - .on_reprepare::(None, ReprepareReason::MessageMetadataRefused); - } - // These errors cannot be recovered from, so we drop them - MetadataBuildError::UnsupportedModuleType(reason) => { - warn!(?reason, "Unsupported module type"); - return self - .on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata); - } - MetadataBuildError::MaxIsmDepthExceeded(depth) => { - warn!(depth, "Max ISM depth reached"); - return self - .on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata); - } - MetadataBuildError::MaxIsmCountReached(count) => { - warn!(count, "Max ISM count reached"); - return self - .on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata); - } - MetadataBuildError::AggregationThresholdNotMet(threshold) => { - warn!(threshold, "Aggregation threshold not met"); - return self - .on_reprepare(Some(err), ReprepareReason::CouldNotFetchMetadata); - } + _ => match self.build_metadata().await { + Ok(metadata) => { + let metadata_bytes = metadata.to_vec(); + self.metadata = Some(metadata_bytes.clone()); + metadata_bytes } - } + Err(err) => { + return err; + } + }, }; // Estimate transaction costs for the process call. If there are issues, it's // likely that gas estimation has failed because the message is // reverting. This is defined behavior, so we just log the error and // move onto the next tick. - let tx_cost_estimate = match self - .ctx - .destination_mailbox - .process_estimate_costs(&self.message, &metadata_bytes) - .await - { - Ok(tx_cost_estimate) => tx_cost_estimate, - Err(err) => { - let reason = self - .clarify_reason(ReprepareReason::ErrorEstimatingGas) - .await - .unwrap_or(ReprepareReason::ErrorEstimatingGas); - return self.on_reprepare(Some(err), reason); - } + let tx_cost_estimate = match tx_cost_estimate { + // reuse old gas cost estimate if it succeeded + Some(cost) => cost, + None => match self + .ctx + .destination_mailbox + .process_estimate_costs(&self.message, &metadata_bytes) + .await + { + Ok(cost) => cost, + Err(err) => { + let reason = self + .clarify_reason(ReprepareReason::ErrorEstimatingGas) + .await + .unwrap_or(ReprepareReason::ErrorEstimatingGas); + self.clear_metadata(); + return self.on_reprepare(Some(err), reason); + } + }, }; // Get the gas_limit if the gas payment requirement has been met, @@ -383,6 +354,7 @@ impl PendingOperation for PendingMessage { if let Some(max_limit) = self.ctx.transaction_gas_limit { if gas_limit > max_limit { // TODO: consider dropping instead of repreparing in this case + self.clear_metadata(); return self.on_reprepare::(None, ReprepareReason::ExceedsMaxGasLimit); } } @@ -419,6 +391,7 @@ impl PendingOperation for PendingMessage { .clarify_reason(ReprepareReason::ErrorEstimatingGas) .await .unwrap_or(ReprepareReason::ErrorEstimatingGas); + self.clear_metadata(); return self.on_reprepare::(None, reason); } } @@ -437,6 +410,7 @@ impl PendingOperation for PendingMessage { } Err(e) => { error!(error=?e, "Error when processing message"); + self.clear_metadata(); return PendingOperationResult::Reprepare(ReprepareReason::ErrorSubmitting); } } @@ -565,6 +539,26 @@ impl PendingOperation for PendingMessage { fn set_metric(&mut self, metric: Arc) { self.metric = Some(metric); } + + async fn payload(&self) -> ChainResult> { + let mailbox = &self.ctx.destination_mailbox; + let message = &self.message; + let submission_data = self + .submission_data + .as_ref() + .expect("Pending message must be prepared before we can create payload for it"); + let metadata = &submission_data.metadata; + let payload = mailbox.process_calldata(message, metadata).await?; + Ok(payload) + } + + fn on_reprepare( + &mut self, + err: Option, + reason: ReprepareReason, + ) -> PendingOperationResult { + self.on_reprepare(err, reason) + } } impl PendingMessage { @@ -651,6 +645,115 @@ impl PendingMessage { PendingOperationStatus::FirstPrepareAttempt } + /// Checks if the recipient is a contract. + /// This method will attempt to get the value from cache first. If it is a cache miss, + /// it will request it from the provider. The result will be cached for future use. + /// + /// Implicit contract in this method: function name `is_contract` matches + /// the name of the method `is_contract`. + async fn is_recipient_contract(&mut self) -> Result { + let mailbox = self.ctx.destination_mailbox.clone(); + let domain_name = mailbox.domain().name(); + let fn_key = "is_contract"; + let fn_params = self.message.recipient; + let provider = self.ctx.destination_mailbox.provider(); + + // Check cache for recipient contract status + if let Some(is_contract) = self + .get_from_cache::(domain_name, fn_key, &fn_params) + .await + { + return Ok(is_contract); + } + + // Check if the recipient is a contract + let is_contract = provider.is_contract(&fn_params).await.map_err(|err| { + self.on_reprepare( + Some(err), + ReprepareReason::ErrorCheckingIfRecipientIsContract, + ) + })?; + + // Cache the recipient contract status + self.store_to_cache(domain_name, fn_key, &fn_params, &is_contract) + .await; + + Ok(is_contract) + } + + /// Fetches the recipient ISM address. + /// This method will attempt to get the value from cache first. If it is a cache miss, + /// it will request it from the Mailbox contract. The result will be cached for future use. + /// + /// Implicit contract in this method: function name `recipient_ism` matches + /// the name of the method `recipient_ism`. + async fn recipient_ism_address(&mut self) -> Result { + let domain = self.ctx.destination_mailbox.domain().name(); + let fn_key = "recipient_ism"; + let fn_params = self.message.recipient; + + // Check cache for recipient ISM address + if let Some(ism_address) = self + .get_from_cache::(domain, fn_key, &fn_params) + .await + { + return Ok(ism_address); + } + + // Fetch the recipient ISM address + let ism_address = match self + .ctx + .destination_mailbox + .recipient_ism(self.message.recipient) + .await + { + Ok(ism_address) => ism_address, + Err(err) => { + return Err(self.on_reprepare(Some(err), ReprepareReason::ErrorFetchingIsmAddress)); + } + }; + + // Cache the recipient ISM address + self.store_to_cache(domain, fn_key, &fn_params, &ism_address) + .await; + + Ok(ism_address) + } + + async fn get_from_cache( + &self, + domain_name: &str, + fn_key: &str, + fn_params: &(impl Serialize + Send + Sync), + ) -> Option { + self.ctx + .cache + .get_cached_call_result::(domain_name, fn_key, fn_params) + .await + .map_err(|err| { + warn!(error=?err, ?fn_key, "Error checking cache stored result"); + err + }) + .ok() + .flatten() + } + + async fn store_to_cache( + &self, + domain_name: &str, + fn_key: &str, + fn_params: &(impl Serialize + Send + Sync), + result: &(impl Serialize + Send + Sync), + ) { + if let Err(err) = self + .ctx + .cache + .cache_call_result(domain_name, fn_key, fn_params, result) + .await + { + warn!(error=?err, ?fn_key, "Error caching result"); + } + } /// A preflight check to see if a message could possibly meet /// a gas payment requirement prior to undertaking expensive operations /// like metadata building or gas estimation. @@ -857,41 +960,85 @@ impl PendingMessage { None => None, } } -} -#[derive(Debug)] -pub struct MessageSubmissionMetrics { - // Fields are public for testing purposes - pub last_known_nonce: IntGauge, - pub messages_processed: IntCounter, -} + /// Builds metadata + async fn build_metadata(&mut self) -> Result { + let ism_address = self.recipient_ism_address().await?; -impl MessageSubmissionMetrics { - pub fn new( - metrics: &CoreMetrics, - origin: &HyperlaneDomain, - destination: &HyperlaneDomain, - ) -> Self { - let origin = origin.name(); - let destination = destination.name(); - Self { - last_known_nonce: metrics.last_known_message_nonce().with_label_values(&[ - "message_processed", - origin, - destination, - ]), - messages_processed: metrics - .messages_processed_count() - .with_label_values(&[origin, destination]), - } + let message_metadata_builder = match MessageMetadataBuilder::new( + self.ctx.metadata_builder.clone(), + ism_address, + &self.message, + ) + .await + { + Ok(message_metadata_builder) => message_metadata_builder, + Err(err) => { + return Err( + self.on_reprepare(Some(err), ReprepareReason::ErrorGettingMetadataBuilder) + ); + } + }; + + let params = MessageMetadataBuildParams::default(); + + let build_metadata_start = Instant::now(); + let metadata_res = message_metadata_builder + .build(ism_address, &self.message, params) + .await + .map_err(|err| match &err { + MetadataBuildError::FailedToBuild(_) | MetadataBuildError::FastPathError(_) => { + self.on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata) + } + MetadataBuildError::CouldNotFetch => { + self.on_reprepare::(None, ReprepareReason::CouldNotFetchMetadata) + } + // If the metadata building is refused, we still allow it to be retried later. + MetadataBuildError::Refused(reason) => { + warn!(?reason, "Metadata building refused"); + self.on_reprepare::(None, ReprepareReason::MessageMetadataRefused) + } + // These errors cannot be recovered from, so we drop them + MetadataBuildError::UnsupportedModuleType(reason) => { + warn!(?reason, "Unsupported module type"); + self.on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata) + } + MetadataBuildError::MaxIsmDepthExceeded(depth) => { + warn!(depth, "Max ISM depth reached"); + self.on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata) + } + MetadataBuildError::MaxIsmCountReached(count) => { + warn!(count, "Max ISM count reached"); + self.on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata) + } + MetadataBuildError::AggregationThresholdNotMet(threshold) => { + warn!(threshold, "Aggregation threshold not met"); + self.on_reprepare(Some(err), ReprepareReason::CouldNotFetchMetadata) + } + MetadataBuildError::MaxValidatorCountReached(count) => { + warn!(count, "Max validator count reached"); + self.on_reprepare(Some(err), ReprepareReason::ErrorBuildingMetadata) + } + }); + let build_metadata_end = Instant::now(); + + let metrics_params = MetadataBuildMetric { + app_context: self.app_context.clone(), + success: metadata_res.is_ok(), + duration: build_metadata_end.saturating_duration_since(build_metadata_start), + }; + + self.ctx + .metrics + .insert_metadata_build_metric(metrics_params); + + metadata_res } - fn update_nonce(&self, msg: &HyperlaneMessage) { - // this is technically a race condition between `.get` and `.set` but worst case - // the gauge should get corrected on the next update and is not an issue - // with a ST runtime - self.last_known_nonce - .set(std::cmp::max(self.last_known_nonce.get(), msg.nonce as i64)); + /// clear metadata cache + fn clear_metadata(&mut self) { + tracing::debug!(id=?self.message.id(), INVALIDATE_CACHE_METADATA_LOG); + self.metadata = None; } } @@ -905,7 +1052,7 @@ mod test { use chrono::TimeDelta; use hyperlane_base::db::*; - use hyperlane_core::*; + use hyperlane_core::{identifiers::UniqueIdentifier, *}; use crate::msg::pending_message::DEFAULT_MAX_MESSAGE_RETRIES; @@ -1022,7 +1169,8 @@ mod test { ) -> DbResult>; fn store_highest_seen_message_nonce_number(&self, nonce: &u32) -> DbResult<()>; fn retrieve_highest_seen_message_nonce_number(&self) -> DbResult>; - + fn store_payload_id_by_message_id(&self, message_id: &H256, payload_id: &UniqueIdentifier) -> DbResult<()>; + fn retrieve_payload_id_by_message_id(&self, message_id: &H256) -> DbResult>; } } diff --git a/rust/main/agents/relayer/src/msg/processor.rs b/rust/main/agents/relayer/src/msg/processor.rs index 7a6dffa5324..9b6943d0836 100644 --- a/rust/main/agents/relayer/src/msg/processor.rs +++ b/rust/main/agents/relayer/src/msg/processor.rs @@ -294,6 +294,17 @@ impl ProcessorExt for MessageProcessor { return Ok(()); } + // Skip if message is intended for a destination we don't have message context for + let destination_msg_ctx = if let Some(ctx) = self.destination_ctxs.get(&destination) { + ctx + } else { + debug!( + ?msg, + "Message destined for unknown message context, skipping", + ); + return Ok(()); + }; + debug!(%msg, "Sending message to submitter"); let app_context_classifier = @@ -303,7 +314,7 @@ impl ProcessorExt for MessageProcessor { // Finally, build the submit arg and dispatch it to the submitter. let pending_msg = PendingMessage::maybe_from_persisted_retries( msg, - self.destination_ctxs[&destination].clone(), + destination_msg_ctx.clone(), app_context, self.max_retries, ); @@ -393,10 +404,10 @@ impl MessageProcessorMetrics { } #[cfg(test)] -mod test { +pub mod test { use std::time::Instant; - use prometheus::{IntCounter, Registry}; + use prometheus::{CounterVec, IntCounter, IntCounterVec, Opts, Registry}; use tokio::{ sync::{ mpsc::{self, UnboundedReceiver}, @@ -407,6 +418,7 @@ mod test { use tokio_metrics::TaskMonitor; use hyperlane_base::{ + cache::{LocalCache, MeteredCache, MeteredCacheConfig, MeteredCacheMetrics, OptionalCache}, db::{ test_utils, DbResult, HyperlaneRocksDB, InterchainGasExpenditureData, InterchainGasPaymentData, @@ -414,8 +426,9 @@ mod test { settings::{ChainConf, ChainConnectionConf, Settings}, }; use hyperlane_core::{ - test_utils::dummy_domain, GasPaymentKey, InterchainGasPayment, InterchainGasPaymentMeta, - MerkleTreeInsertion, PendingOperationStatus, H256, + identifiers::UniqueIdentifier, test_utils::dummy_domain, GasPaymentKey, + InterchainGasPayment, InterchainGasPaymentMeta, MerkleTreeInsertion, + PendingOperationStatus, H256, }; use hyperlane_operation_verifier::{ ApplicationOperationVerifier, ApplicationOperationVerifierReport, @@ -425,16 +438,20 @@ mod test { use crate::{ merkle_tree::builder::MerkleTreeBuilder, + metrics::message_submission::MessageSubmissionMetrics, msg::{ gas_payment::GasPaymentEnforcer, - metadata::{BaseMetadataBuilder, IsmAwareAppContextClassifier}, + metadata::{ + BaseMetadataBuilder, DefaultIsmCache, IsmAwareAppContextClassifier, + IsmCachePolicyClassifier, + }, }, processor::Processor, }; use super::*; - struct DummyApplicationOperationVerifier {} + pub struct DummyApplicationOperationVerifier {} #[async_trait] impl ApplicationOperationVerifier for DummyApplicationOperationVerifier { @@ -447,7 +464,7 @@ mod test { } } - fn dummy_processor_metrics(domain_id: u32) -> MessageProcessorMetrics { + pub fn dummy_processor_metrics(domain_id: u32) -> MessageProcessorMetrics { MessageProcessorMetrics { max_last_known_message_nonce_gauge: IntGauge::new( "dummy_max_last_known_message_nonce_gauge", @@ -461,10 +478,37 @@ mod test { } } - fn dummy_submission_metrics() -> MessageSubmissionMetrics { + pub fn dummy_cache_metrics() -> MeteredCacheMetrics { + MeteredCacheMetrics { + hit_count: IntCounterVec::new( + prometheus::Opts::new("dummy_hit_count", "help string"), + &["cache_name", "method", "status"], + ) + .ok(), + miss_count: IntCounterVec::new( + prometheus::Opts::new("dummy_miss_count", "help string"), + &["cache_name", "method", "status"], + ) + .ok(), + } + } + + pub fn dummy_submission_metrics() -> MessageSubmissionMetrics { MessageSubmissionMetrics { + origin: "".to_string(), + destination: "".to_string(), last_known_nonce: IntGauge::new("last_known_nonce_gauge", "help string").unwrap(), messages_processed: IntCounter::new("message_processed_gauge", "help string").unwrap(), + metadata_build_count: IntCounterVec::new( + Opts::new("metadata_build_count", "help string"), + &["app_context", "origin", "remote", "status"], + ) + .unwrap(), + metadata_build_duration: CounterVec::new( + Opts::new("metadata_build_duration", "help string"), + &["app_context", "origin", "remote", "status"], + ) + .unwrap(), } } @@ -472,6 +516,8 @@ mod test { ChainConf { domain: domain.clone(), signer: Default::default(), + submitter: Default::default(), + estimated_block_time: Duration::from_secs_f64(1.1), reorg_period: Default::default(), addresses: Default::default(), connection: ChainConnectionConf::Ethereum(hyperlane_ethereum::ConnectionConf { @@ -479,7 +525,7 @@ mod test { url: "http://example.com".parse().unwrap(), }, transaction_overrides: Default::default(), - operation_batch: Default::default(), + op_submission_config: Default::default(), }), metrics_conf: Default::default(), index: Default::default(), @@ -490,6 +536,7 @@ mod test { origin_domain: &HyperlaneDomain, destination_domain: &HyperlaneDomain, db: &HyperlaneRocksDB, + cache: OptionalCache>, ) -> BaseMetadataBuilder { let mut settings = Settings::default(); settings.chains.insert( @@ -502,6 +549,9 @@ mod test { ); let destination_chain_conf = settings.chain_setup(destination_domain).unwrap(); let core_metrics = CoreMetrics::new("dummy_relayer", 37582, Registry::new()).unwrap(); + let default_ism_getter = DefaultIsmCache::new(Arc::new( + MockMailboxContract::new_with_default_ism(H256::zero()), + )); BaseMetadataBuilder::new( origin_domain.clone(), destination_chain_conf.clone(), @@ -509,8 +559,10 @@ mod test { Arc::new(MockValidatorAnnounceContract::default()), false, Arc::new(core_metrics), + cache, db.clone(), - IsmAwareAppContextClassifier::new(Arc::new(MockMailboxContract::default()), vec![]), + IsmAwareAppContextClassifier::new(default_ism_getter.clone(), vec![]), + IsmCachePolicyClassifier::new(default_ism_getter, Default::default()), ) } @@ -518,11 +570,14 @@ mod test { origin_domain: &HyperlaneDomain, destination_domain: &HyperlaneDomain, db: &HyperlaneRocksDB, + cache: OptionalCache>, ) -> (MessageProcessor, UnboundedReceiver) { - let base_metadata_builder = dummy_metadata_builder(origin_domain, destination_domain, db); + let base_metadata_builder = + dummy_metadata_builder(origin_domain, destination_domain, db, cache.clone()); let message_context = Arc::new(MessageContext { - destination_mailbox: Arc::new(MockMailboxContract::default()), + destination_mailbox: Arc::new(MockMailboxContract::new_with_default_ism(H256::zero())), origin_db: Arc::new(db.clone()), + cache, metadata_builder: Arc::new(base_metadata_builder), origin_gas_payment_enforcer: Arc::new(GasPaymentEnforcer::new([], db.clone())), transaction_gas_limit: Default::default(), @@ -590,10 +645,11 @@ mod test { origin_domain: &HyperlaneDomain, destination_domain: &HyperlaneDomain, db: &HyperlaneRocksDB, + cache: OptionalCache>, num_operations: usize, ) -> Vec { let (message_processor, mut receive_channel) = - dummy_message_processor(origin_domain, destination_domain, db); + dummy_message_processor(origin_domain, destination_domain, db, cache); let processor = Processor::new(Box::new(message_processor), TaskMonitor::new()); let process_fut = processor.spawn(info_span!("MessageProcessor")); @@ -768,6 +824,9 @@ mod test { /// Retrieve the nonce of the highest processed message we're aware of fn retrieve_highest_seen_message_nonce_number(&self) -> DbResult>; + fn store_payload_id_by_message_id(&self, message_id: &H256, payload_id: &UniqueIdentifier) -> DbResult<()>; + + fn retrieve_payload_id_by_message_id(&self, message_id: &H256) -> DbResult>; } } @@ -777,6 +836,13 @@ mod test { let origin_domain = dummy_domain(0, "dummy_origin_domain"); let destination_domain = dummy_domain(1, "dummy_destination_domain"); let db = HyperlaneRocksDB::new(&origin_domain, db); + let cache = OptionalCache::new(Some(MeteredCache::new( + LocalCache::new("test-cache"), + dummy_cache_metrics(), + MeteredCacheConfig { + cache_name: "test-cache".to_owned(), + }, + ))); // Assume the message syncer stored some new messages in HyperlaneDB let msg_retries = vec![0, 0, 0]; @@ -787,6 +853,7 @@ mod test { &origin_domain, &destination_domain, &db, + cache.clone(), msg_retries.len(), ) .await; @@ -803,6 +870,7 @@ mod test { &origin_domain, &destination_domain, &db, + cache.clone(), msg_retries.len(), ) .await; diff --git a/rust/main/agents/relayer/src/msg/utils.rs b/rust/main/agents/relayer/src/msg/utils.rs new file mode 100644 index 00000000000..8449955d5ee --- /dev/null +++ b/rust/main/agents/relayer/src/msg/utils.rs @@ -0,0 +1,55 @@ +// TODO: uncomment if needed +// fn from_payload_status_into_result_for_confirmation( +// status: &PayloadStatus, +// ) -> PendingOperationResult { +// use submitter::PayloadDropReason::{ +// FailedSimulation, FailedToBuildAsTransaction, Reverted, UnhandledError, +// }; +// use submitter::PayloadRetryReason::Reorged; +// use submitter::PayloadStatus::{ +// Dropped as PayloadDropped, InTransaction, ReadyToSubmit, Retry, +// }; +// use submitter::TransactionStatus::{ +// Dropped as TransactionDropped, Finalized, Included, Mempool, PendingInclusion, +// }; + +// use PendingOperationResult::*; + +// match status { +// ReadyToSubmit => Confirm(SubmittedBySelf), +// InTransaction(t) => match t { +// Included | Mempool | PendingInclusion => Confirm(SubmittedBySelf), +// TransactionDropped(_) => Reprepare(ReprepareReason::RevertedOrReorged), +// Finalized => Success, +// }, +// PayloadDropped(d) => match d { +// FailedToBuildAsTransaction | FailedSimulation | UnhandledError => { +// Reprepare(ReprepareReason::ErrorEstimatingGas) +// } +// Reverted => Reprepare(ReprepareReason::RevertedOrReorged), +// }, +// Retry(r) => match r { +// Reorged => Confirm(SubmittedBySelf), +// }, +// } +// } + +// fn from_submitter_error_into_result_for_confirmation( +// error: &SubmitterError, +// ) -> PendingOperationResult { +// use submitter::SubmitterError::*; + +// use PendingOperationResult::*; + +// match error { +// TxAlreadyExists | TxSubmissionError(_) => Confirm(SubmittedBySelf), +// TxReverted => Reprepare(ReprepareReason::RevertedOrReorged), +// NetworkError(_) +// | ChannelSendFailure(_) +// | ChannelClosed +// | EyreError(_) +// | PayloadNotFound +// | DbError(_) +// | ChainCommunicationError(_) => Reprepare(ReprepareReason::ErrorSubmitting), +// } +// } diff --git a/rust/main/agents/relayer/src/relayer.rs b/rust/main/agents/relayer/src/relayer.rs index b883dfa8c5b..623d43648f5 100644 --- a/rust/main/agents/relayer/src/relayer.rs +++ b/rust/main/agents/relayer/src/relayer.rs @@ -2,6 +2,7 @@ use std::{ collections::{HashMap, HashSet}, fmt::{Debug, Formatter}, sync::Arc, + time::Instant, }; use async_trait::async_trait; @@ -17,10 +18,11 @@ use tokio::{ task::JoinHandle, }; use tokio_metrics::TaskMonitor; -use tracing::{error, info, info_span, warn, Instrument}; +use tracing::{debug, error, info, info_span, warn, Instrument}; use hyperlane_base::{ broadcast::BroadcastMpscSender, + cache::{LocalCache, MeteredCache, MeteredCacheConfig, OptionalCache}, db::{HyperlaneRocksDB, DB}, metrics::{AgentMetrics, ChainSpecificMetricsUpdater}, settings::{ChainConf, IndexSettings}, @@ -30,18 +32,25 @@ use hyperlane_base::{ use hyperlane_core::{ rpc_clients::call_and_retry_n_times, ChainCommunicationError, ChainResult, ContractSyncCursor, HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, Mailbox, MerkleTreeInsertion, - QueueOperation, ValidatorAnnounce, H512, U256, + QueueOperation, SubmitterType, ValidatorAnnounce, H512, U256, }; use hyperlane_operation_verifier::ApplicationOperationVerifier; +use submitter::{ + DatabaseOrPath, PayloadDispatcher, PayloadDispatcherEntrypoint, PayloadDispatcherSettings, +}; use crate::{ merkle_tree::builder::MerkleTreeBuilder, + metrics::message_submission::MessageSubmissionMetrics, msg::{ blacklist::AddressBlacklist, gas_payment::GasPaymentEnforcer, - metadata::{BaseMetadataBuilder, IsmAwareAppContextClassifier}, + metadata::{ + BaseMetadataBuilder, DefaultIsmCache, IsmAwareAppContextClassifier, + IsmCachePolicyClassifier, + }, op_submitter::{SerialSubmitter, SerialSubmitterMetrics}, - pending_message::{MessageContext, MessageSubmissionMetrics}, + pending_message::MessageContext, processor::{MessageProcessor, MessageProcessorMetrics}, }, server::{self as relayer_server}, @@ -71,13 +80,15 @@ pub struct Relayer { core: HyperlaneAgentCore, message_syncs: HashMap>>, interchain_gas_payment_syncs: - HashMap>>, + Option>>>, /// Context data for each (origin, destination) chain pair a message can be /// sent between msg_ctxs: HashMap>, prover_syncs: HashMap>>, merkle_tree_hook_syncs: HashMap>>, dbs: HashMap, + /// The original reference to the relayer cache + _cache: OptionalCache>, message_whitelist: Arc, message_blacklist: Arc, address_blacklist: Arc, @@ -94,6 +105,8 @@ pub struct Relayer { runtime_metrics: RuntimeMetrics, /// Tokio console server pub tokio_console_server: Option, + payload_dispatcher_entrypoints: HashMap, + payload_dispatchers: HashMap, } impl Debug for Relayer { @@ -119,9 +132,10 @@ impl BaseAgent for Relayer { const AGENT_NAME: &'static str = "relayer"; type Settings = RelayerSettings; + type Metadata = AgentMetadata; async fn from_settings( - _agent_metadata: AgentMetadata, + _agent_metadata: Self::Metadata, settings: Self::Settings, core_metrics: Arc, agent_metrics: AgentMetrics, @@ -132,25 +146,71 @@ impl BaseAgent for Relayer { where Self: Sized, { + Self::reset_critical_errors(&settings, &chain_metrics); + + let start = Instant::now(); + let mut start_entity_init = Instant::now(); + let core = settings.build_hyperlane_core(core_metrics.clone()); let db = DB::from_path(&settings.db)?; + let cache_name = "relayer_cache"; + let inner_cache = if settings.allow_contract_call_caching { + Some(MeteredCache::new( + LocalCache::new(cache_name), + core_metrics.cache_metrics(), + MeteredCacheConfig { + cache_name: cache_name.to_owned(), + }, + )) + } else { + None + }; + let cache = OptionalCache::new(inner_cache); let dbs = settings .origin_chains .iter() .map(|origin| (origin.clone(), HyperlaneRocksDB::new(origin, db.clone()))) .collect::>(); + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized databases", "Relayer startup duration measurement"); + start_entity_init = Instant::now(); let application_operation_verifiers = Self::build_application_operation_verifiers(&settings, &core_metrics, &chain_metrics) .await; + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized application operation verifiers", "Relayer startup duration measurement"); + start_entity_init = Instant::now(); let mailboxes = Self::build_mailboxes(&settings, &core_metrics, &chain_metrics).await; + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized mailbox", "Relayer startup duration measurement"); + start_entity_init = Instant::now(); let validator_announces = Self::build_validator_announces(&settings, &core_metrics, &chain_metrics).await; + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized validator announces", "Relayer startup duration measurement"); + + start_entity_init = Instant::now(); + let dispatcher_entrypoints = Self::build_payload_dispatcher_entrypoints( + &settings, + core_metrics.clone(), + &chain_metrics, + db.clone(), + ) + .await; + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized dispatcher entrypoints", "Relayer startup duration measurement"); + + start_entity_init = Instant::now(); + let dispatchers = Self::build_payload_dispatchers( + &settings, + core_metrics.clone(), + &chain_metrics, + db.clone(), + ) + .await; + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized dipatchers", "Relayer startup duration measurement"); let contract_sync_metrics = Arc::new(ContractSyncMetrics::new(&core_metrics)); + start_entity_init = Instant::now(); let message_syncs: HashMap<_, Arc>> = settings .contract_syncs::( settings.origin_chains.iter(), @@ -160,27 +220,39 @@ impl BaseAgent for Relayer { .map(|(d, db)| (d.clone(), Arc::new(db.clone()))) .collect(), false, + settings.tx_id_indexing_enabled, ) .await? .into_iter() .map(|(k, v)| (k, v as _)) .collect(); - - let interchain_gas_payment_syncs = settings - .contract_syncs::( - settings.origin_chains.iter(), - &core_metrics, - &contract_sync_metrics, - dbs.iter() - .map(|(d, db)| (d.clone(), Arc::new(db.clone()))) + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized message syncs", "Relayer startup duration measurement"); + + start_entity_init = Instant::now(); + let interchain_gas_payment_syncs = if settings.igp_indexing_enabled { + Some( + settings + .contract_syncs::( + settings.origin_chains.iter(), + &core_metrics, + &contract_sync_metrics, + dbs.iter() + .map(|(d, db)| (d.clone(), Arc::new(db.clone()))) + .collect(), + false, + settings.tx_id_indexing_enabled, + ) + .await? + .into_iter() + .map(|(k, v)| (k, v as _)) .collect(), - false, ) - .await? - .into_iter() - .map(|(k, v)| (k, v as _)) - .collect(); + } else { + None + }; + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized IGP syncs", "Relayer startup duration measurement"); + start_entity_init = Instant::now(); let merkle_tree_hook_syncs = settings .contract_syncs::( settings.origin_chains.iter(), @@ -190,11 +262,13 @@ impl BaseAgent for Relayer { .map(|(d, db)| (d.clone(), Arc::new(db.clone()))) .collect(), false, + settings.tx_id_indexing_enabled, ) .await? .into_iter() .map(|(k, v)| (k, v as _)) .collect(); + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized merkle tree hook syncs", "Relayer startup duration measurement"); let message_whitelist = Arc::new(settings.whitelist); let message_blacklist = Arc::new(settings.blacklist); @@ -212,6 +286,7 @@ impl BaseAgent for Relayer { ); // provers by origin chain + start_entity_init = Instant::now(); let prover_syncs = settings .origin_chains .iter() @@ -222,11 +297,13 @@ impl BaseAgent for Relayer { ) }) .collect::>(); + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized prover syncs", "Relayer startup duration measurement"); info!(gas_enforcement_policies=?settings.gas_payment_enforcement, "Gas enforcement configuration"); // need one of these per origin chain due to the database scoping even though // the config itself is the same + start_entity_init = Instant::now(); let gas_payment_enforcers: HashMap<_, _> = settings .origin_chains .iter() @@ -240,11 +317,13 @@ impl BaseAgent for Relayer { ) }) .collect(); + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized gas payment enforcers", "Relayer startup duration measurement"); let mut msg_ctxs = HashMap::new(); let mut destination_chains = HashMap::new(); // only iterate through destination chains that were successfully instantiated + start_entity_init = Instant::now(); for (destination, dest_mailbox) in mailboxes.iter() { let destination_chain_setup = core.settings.chain_setup(destination).unwrap().clone(); destination_chains.insert(destination.clone(), destination_chain_setup.clone()); @@ -260,6 +339,7 @@ impl BaseAgent for Relayer { // only iterate through origin chains that were successfully instantiated for (origin, validator_announce) in validator_announces.iter() { let db = dbs.get(origin).unwrap().clone(); + let default_ism_getter = DefaultIsmCache::new(dest_mailbox.clone()); let metadata_builder = BaseMetadataBuilder::new( origin.clone(), destination_chain_setup.clone(), @@ -267,11 +347,16 @@ impl BaseAgent for Relayer { validator_announce.clone(), settings.allow_local_checkpoint_syncers, core.metrics.clone(), + cache.clone(), db, IsmAwareAppContextClassifier::new( - dest_mailbox.clone(), + default_ism_getter.clone(), settings.metric_app_contexts.clone(), ), + IsmCachePolicyClassifier::new( + default_ism_getter.clone(), + settings.ism_cache_configs.clone(), + ), ); msg_ctxs.insert( @@ -282,6 +367,7 @@ impl BaseAgent for Relayer { Arc::new(MessageContext { destination_mailbox: dest_mailbox.clone(), origin_db: Arc::new(dbs.get(origin).unwrap().clone()), + cache: cache.clone(), metadata_builder: Arc::new(metadata_builder), origin_gas_payment_enforcer: gas_payment_enforcers[origin].clone(), transaction_gas_limit, @@ -291,9 +377,13 @@ impl BaseAgent for Relayer { ); } } + debug!(elapsed = ?start_entity_init.elapsed(), event = "initialized message contexts", "Relayer startup duration measurement"); + + debug!(elapsed = ?start.elapsed(), event = "fully initialized", "Relayer startup duration measurement"); Ok(Self { dbs, + _cache: cache, origin_chains: settings.origin_chains, destination_chains, msg_ctxs, @@ -315,11 +405,16 @@ impl BaseAgent for Relayer { chain_metrics, runtime_metrics, tokio_console_server: Some(tokio_console_server), + payload_dispatcher_entrypoints: dispatcher_entrypoints, + payload_dispatchers: dispatchers, }) } #[allow(clippy::async_yields_async)] async fn run(mut self) { + let start = Instant::now(); + let mut start_entity_init = Instant::now(); + let mut tasks = vec![]; let task_monitor = tokio_metrics::TaskMonitor::new(); @@ -339,13 +434,26 @@ impl BaseAgent for Relayer { .expect("spawning tokio task from Builder is infallible"); tasks.push(console_server); } + debug!(elapsed = ?start_entity_init.elapsed(), event = "started tokio console server", "Relayer startup duration measurement"); + let sender = BroadcastSender::new(ENDPOINT_MESSAGES_QUEUE_SIZE); // send channels by destination chain let mut send_channels = HashMap::with_capacity(self.destination_chains.len()); let mut prep_queues = HashMap::with_capacity(self.destination_chains.len()); + start_entity_init = Instant::now(); for (dest_domain, dest_conf) in &self.destination_chains { let (send_channel, receive_channel) = mpsc::unbounded_channel::(); send_channels.insert(dest_domain.id(), send_channel); + + let payload_dispatcher_entrypoint = + self.payload_dispatcher_entrypoints.remove(dest_domain); + + let db = self + .dbs + .get(dest_domain) + .expect("DB should be created for every chain") + .clone(); + let serial_submitter = SerialSubmitter::new( dest_domain.clone(), receive_channel, @@ -354,10 +462,16 @@ impl BaseAgent for Relayer { // Default to submitting one message at a time if there is no batch config self.core.settings.chains[dest_domain.name()] .connection - .operation_batch_config() + .operation_submission_config() .map(|c| c.max_batch_size) .unwrap_or(1), + self.core.settings.chains[dest_domain.name()] + .connection + .operation_submission_config() + .and_then(|c| c.max_submit_queue_length), task_monitor.clone(), + payload_dispatcher_entrypoint, + db, ); prep_queues.insert(dest_domain.id(), serial_submitter.prepare_queue().await); @@ -367,6 +481,10 @@ impl BaseAgent for Relayer { task_monitor.clone(), )); + if let Some(dispatcher) = self.payload_dispatchers.remove(dest_domain) { + tasks.push(dispatcher.spawn().await); + } + let metrics_updater = ChainSpecificMetricsUpdater::new( dest_conf, self.core_metrics.clone(), @@ -380,22 +498,26 @@ impl BaseAgent for Relayer { }); tasks.push(metrics_updater.spawn()); } + debug!(elapsed = ?start_entity_init.elapsed(), event = "started submitters", "Relayer startup duration measurement"); + start_entity_init = Instant::now(); for origin in &self.origin_chains { - self.chain_metrics.set_critical_error(origin.name(), false); let maybe_broadcaster = self .message_syncs .get(origin) .and_then(|sync| sync.get_broadcaster()); tasks.push(self.run_message_sync(origin, task_monitor.clone()).await); - tasks.push( - self.run_interchain_gas_payment_sync( - origin, - BroadcastMpscSender::map_get_receiver(maybe_broadcaster.as_ref()).await, - task_monitor.clone(), - ) - .await, - ); + if let Some(interchain_gas_payment_syncs) = &self.interchain_gas_payment_syncs { + tasks.push( + self.run_interchain_gas_payment_sync( + origin, + interchain_gas_payment_syncs, + BroadcastMpscSender::map_get_receiver(maybe_broadcaster.as_ref()).await, + task_monitor.clone(), + ) + .await, + ); + } tasks.push( self.run_merkle_tree_hook_sync( origin, @@ -404,13 +526,21 @@ impl BaseAgent for Relayer { ) .await, ); + tasks.push(self.run_message_processor( + origin, + send_channels.clone(), + task_monitor.clone(), + )); + tasks.push(self.run_merkle_tree_processor(origin, task_monitor.clone())); } + debug!(elapsed = ?start_entity_init.elapsed(), event = "started message, IGP, merkle tree hook syncs, and message and merkle tree processors", "Relayer startup duration measurement"); + // run server + start_entity_init = Instant::now(); let custom_routes = relayer_server::Server::new(self.destination_chains.len()) .with_op_retry(sender.clone()) .with_message_queue(prep_queues) .routes(); - let server = self .core .settings @@ -423,19 +553,12 @@ impl BaseAgent for Relayer { .instrument(info_span!("Relayer server")), ); tasks.push(server_task); - - // each message process attempts to send messages from a chain - for origin in &self.origin_chains { - tasks.push(self.run_message_processor( - origin, - send_channels.clone(), - task_monitor.clone(), - )); - tasks.push(self.run_merkle_tree_processor(origin, task_monitor.clone())); - } + debug!(elapsed = ?start_entity_init.elapsed(), event = "started relayer server", "Relayer startup duration measurement"); tasks.push(self.runtime_metrics.spawn()); + debug!(elapsed = ?start.elapsed(), event = "fully started", "Relayer startup duration measurement"); + if let Err(err) = try_join_all(tasks).await { tracing::error!( error=?err, @@ -447,13 +570,13 @@ impl BaseAgent for Relayer { impl Relayer { fn record_critical_error( - &self, origin: &HyperlaneDomain, err: ChainCommunicationError, message: &str, + chain_metrics: ChainMetrics, ) { error!(?err, origin=?origin, "{message}"); - self.chain_metrics.set_critical_error(origin.name(), true); + chain_metrics.set_critical_error(origin.name(), true); } async fn instantiate_cursor_with_retries( @@ -479,93 +602,116 @@ impl Relayer { origin: &HyperlaneDomain, task_monitor: TaskMonitor, ) -> JoinHandle<()> { + let origin = origin.clone(); + let contract_sync = self.message_syncs.get(&origin).unwrap().clone(); let index_settings = self.as_ref().settings.chains[origin.name()].index_settings(); - let contract_sync = self.message_syncs.get(origin).unwrap().clone(); - let cursor_instantiation_result = - Self::instantiate_cursor_with_retries(contract_sync.clone(), index_settings.clone()) - .await; - let cursor = match cursor_instantiation_result { - Ok(cursor) => cursor, - Err(err) => { - self.record_critical_error(origin, err, CURSOR_BUILDING_ERROR); - return tokio::spawn(async {}.instrument(info_span!("MessageSync"))); - } - }; - let origin_name = origin.name().to_string(); - let name = Self::contract_sync_task_name("message::", &origin_name); + let chain_metrics = self.chain_metrics.clone(); + + let name = Self::contract_sync_task_name("message::", origin.name()); tokio::task::Builder::new() .name(&name) .spawn(TaskMonitor::instrument( &task_monitor, async move { - let label = "dispatched_messages"; - contract_sync.clone().sync(label, cursor.into()).await; - info!(chain = origin_name, label, "contract sync task exit"); + Self::message_sync_task(&origin, contract_sync, index_settings, chain_metrics) + .await; } .instrument(info_span!("MessageSync")), )) .expect("spawning tokio task from Builder is infallible") } - async fn run_interchain_gas_payment_sync( - &self, + async fn message_sync_task( origin: &HyperlaneDomain, - tx_id_receiver: Option>, - task_monitor: TaskMonitor, - ) -> JoinHandle<()> { - let index_settings = self.as_ref().settings.chains[origin.name()].index_settings(); - let contract_sync = self - .interchain_gas_payment_syncs - .get(origin) - .unwrap() - .clone(); + contract_sync: Arc>, + index_settings: IndexSettings, + chain_metrics: ChainMetrics, + ) { let cursor_instantiation_result = - Self::instantiate_cursor_with_retries(contract_sync.clone(), index_settings.clone()) - .await; + Self::instantiate_cursor_with_retries(contract_sync.clone(), index_settings).await; let cursor = match cursor_instantiation_result { Ok(cursor) => cursor, Err(err) => { - self.record_critical_error(origin, err, CURSOR_BUILDING_ERROR); - return tokio::spawn(async {}.instrument(info_span!("IgpSync"))); + Self::record_critical_error(origin, err, CURSOR_BUILDING_ERROR, chain_metrics); + return; } }; - let origin_name = origin.name().to_string(); - let name = Self::contract_sync_task_name("gas_payment::", &origin_name); + let label = "dispatched_messages"; + contract_sync.clone().sync(label, cursor.into()).await; + info!(chain = origin.name(), label, "contract sync task exit"); + } + + async fn run_interchain_gas_payment_sync( + &self, + origin: &HyperlaneDomain, + interchain_gas_payment_syncs: &HashMap< + HyperlaneDomain, + Arc>, + >, + tx_id_receiver: Option>, + task_monitor: TaskMonitor, + ) -> JoinHandle<()> { + let origin = origin.clone(); + let index_settings = self.as_ref().settings.chains[origin.name()].index_settings(); + let contract_sync = interchain_gas_payment_syncs.get(&origin).unwrap().clone(); + let chain_metrics = self.chain_metrics.clone(); + + let name = Self::contract_sync_task_name("gas_payment::", origin.name()); tokio::task::Builder::new() .name(&name) .spawn(TaskMonitor::instrument( &task_monitor, async move { - let label = "gas_payments"; - contract_sync - .clone() - .sync(label, SyncOptions::new(Some(cursor), tx_id_receiver)) - .await; - info!(chain = origin_name, label, "contract sync task exit"); + Self::interchain_gas_payments_sync_task( + &origin, + index_settings, + contract_sync, + chain_metrics, + tx_id_receiver, + ) + .await; } .instrument(info_span!("IgpSync")), )) .expect("spawning tokio task from Builder is infallible") } - async fn run_merkle_tree_hook_sync( - &self, + async fn interchain_gas_payments_sync_task( origin: &HyperlaneDomain, + index_settings: IndexSettings, + contract_sync: Arc>, + chain_metrics: ChainMetrics, tx_id_receiver: Option>, - task_monitor: TaskMonitor, - ) -> JoinHandle<()> { - let index_settings = self.as_ref().settings.chains[origin.name()].index.clone(); - let contract_sync = self.merkle_tree_hook_syncs.get(origin).unwrap().clone(); + ) { let cursor_instantiation_result = Self::instantiate_cursor_with_retries(contract_sync.clone(), index_settings.clone()) .await; let cursor = match cursor_instantiation_result { Ok(cursor) => cursor, Err(err) => { - self.record_critical_error(origin, err, CURSOR_BUILDING_ERROR); - return tokio::spawn(async {}.instrument(info_span!("MerkleTreeHookSync"))); + Self::record_critical_error(origin, err, CURSOR_BUILDING_ERROR, chain_metrics); + return; } }; + let label = "gas_payments"; + contract_sync + .clone() + .sync(label, SyncOptions::new(Some(cursor), tx_id_receiver)) + .await; + info!(chain = origin.name(), label, "contract sync task exit"); + } + + async fn run_merkle_tree_hook_sync( + &self, + origin: &HyperlaneDomain, + tx_id_receiver: Option>, + task_monitor: TaskMonitor, + ) -> JoinHandle<()> { + let origin = origin.clone(); + let index_settings = self.as_ref().settings.chains[origin.name()].index.clone(); + let contract_sync = self.merkle_tree_hook_syncs.get(&origin).unwrap().clone(); + let chain_metrics = self.chain_metrics.clone(); + let origin_name = origin.name().to_string(); let name = Self::contract_sync_task_name("merkle_tree::", &origin_name); tokio::task::Builder::new() @@ -573,18 +719,45 @@ impl Relayer { .spawn(TaskMonitor::instrument( &task_monitor, async move { - let label = "merkle_tree_hook"; - contract_sync - .clone() - .sync(label, SyncOptions::new(Some(cursor), tx_id_receiver)) - .await; - info!(chain = origin_name, label, "contract sync task exit"); + Self::merkle_tree_hook_sync_task( + &origin, + index_settings, + contract_sync, + chain_metrics, + tx_id_receiver, + ) + .await; } .instrument(info_span!("MerkleTreeHookSync")), )) .expect("spawning tokio task from Builder is infallible") } + async fn merkle_tree_hook_sync_task( + origin: &HyperlaneDomain, + index_settings: IndexSettings, + contract_sync: Arc>, + chain_metrics: ChainMetrics, + tx_id_receiver: Option>, + ) { + let cursor_instantiation_result = + Self::instantiate_cursor_with_retries(contract_sync.clone(), index_settings.clone()) + .await; + let cursor = match cursor_instantiation_result { + Ok(cursor) => cursor, + Err(err) => { + Self::record_critical_error(origin, err, CURSOR_BUILDING_ERROR, chain_metrics); + return; + } + }; + let label = "merkle_tree_hook"; + contract_sync + .clone() + .sync(label, SyncOptions::new(Some(cursor), tx_id_receiver)) + .await; + info!(chain = origin.name(), label, "contract sync task exit"); + } + fn contract_sync_task_name(prefix: &str, domain: &str) -> String { format!("contract::sync::{}{}", prefix, domain) } @@ -603,15 +776,31 @@ impl Relayer { let destination_ctxs: HashMap<_, _> = self .destination_chains .keys() - .map(|destination| { - ( - destination.id(), - self.msg_ctxs[&ContextKey { - origin: origin.id(), - destination: destination.id(), - }] - .clone(), - ) + .filter_map(|destination| { + let key = ContextKey { + origin: origin.id(), + destination: destination.id(), + }; + let context = self + .msg_ctxs + .get(&key) + .map(|c| (destination.id(), c.clone())); + + if context.is_none() { + let err_msg = format!( + "No message context found for origin {} and destination {}", + origin.name(), + destination.name() + ); + Self::record_critical_error( + origin, + ChainCommunicationError::CustomError(err_msg.clone()), + &err_msg, + self.chain_metrics.clone(), + ); + } + + context }) .collect(); @@ -703,6 +892,78 @@ impl Relayer { .collect() } + /// Helper function to build and return a hashmap of payload dispatchers. + /// Any chains that fail to build payload dispatcher will not be included + /// in the hashmap. Errors will be logged and chain metrics + /// will be updated for chains that fail to build payload dispatcher. + pub async fn build_payload_dispatcher_entrypoints( + settings: &RelayerSettings, + core_metrics: Arc, + chain_metrics: &ChainMetrics, + db: DB, + ) -> HashMap { + settings.destination_chains.iter() + .filter(|chain| SubmitterType::Lander == settings.chains[&chain.to_string()].submitter) + .map(|chain| (chain.clone(), PayloadDispatcherSettings { + chain_conf: settings.chains[&chain.to_string()].clone(), + raw_chain_conf: Default::default(), + domain: chain.clone(), + db: DatabaseOrPath::Database(db.clone()), + metrics: core_metrics.clone(), + })) + .map(|(chain, s)| (chain, PayloadDispatcherEntrypoint::try_from_settings(s))) + .filter_map(|(chain, result)| match result { + Ok(entrypoint) => Some((chain, entrypoint)), + Err(err) => { + error!(?err, origin=?chain, "Critical error when building payload dispatcher endpoint"); + chain_metrics.set_critical_error(chain.name(), true); + None + } + }) + .collect::>() + } + + /// Helper function to build and return a hashmap of payload dispatchers. + /// Any chains that fail to build payload dispatcher will not be included + /// in the hashmap. Errors will be logged and chain metrics + /// will be updated for chains that fail to build payload dispatcher. + pub async fn build_payload_dispatchers( + settings: &RelayerSettings, + core_metrics: Arc, + chain_metrics: &ChainMetrics, + db: DB, + ) -> HashMap { + settings + .destination_chains + .iter() + .filter(|chain| SubmitterType::Lander == settings.chains[&chain.to_string()].submitter) + .map(|chain| { + ( + chain.clone(), + PayloadDispatcherSettings { + chain_conf: settings.chains[&chain.to_string()].clone(), + raw_chain_conf: Default::default(), + domain: chain.clone(), + db: DatabaseOrPath::Database(db.clone()), + metrics: core_metrics.clone(), + }, + ) + }) + .map(|(chain, s)| { + let chain_name = chain.to_string(); + (chain, PayloadDispatcher::try_from_settings(s, chain_name)) + }) + .filter_map(|(chain, result)| match result { + Ok(entrypoint) => Some((chain, entrypoint)), + Err(err) => { + error!(?err, origin=?chain, "Critical error when building payload dispatcher"); + chain_metrics.set_critical_error(chain.name(), true); + None + } + }) + .collect::>() + } + /// Helper function to build and return a hashmap of validator announces. /// Any chains that fail to build validator announce will not be included /// in the hashmap. Errors will be logged and chain metrics @@ -750,6 +1011,13 @@ impl Relayer { }) .collect() } + + fn reset_critical_errors(settings: &RelayerSettings, chain_metrics: &ChainMetrics) { + settings + .origin_chains + .iter() + .for_each(|origin| chain_metrics.set_critical_error(origin.name(), false)); + } } #[cfg(test)] @@ -757,11 +1025,14 @@ mod test { use std::{ collections::{HashMap, HashSet}, path::PathBuf, + time::Duration, }; - use crate::settings::{matching_list::MatchingList, RelayerSettings}; use ethers::utils::hex; use ethers_prometheus::middleware::PrometheusMiddlewareConf; + use prometheus::{opts, IntGaugeVec, Registry}; + use reqwest::Url; + use hyperlane_base::{ settings::{ ChainConf, ChainConnectionConf, CoreContractAddresses, IndexSettings, Settings, @@ -771,12 +1042,12 @@ mod test { CRITICAL_ERROR_LABELS, }; use hyperlane_core::{ - config::OperationBatchConfig, HyperlaneDomain, IndexMode, KnownHyperlaneDomain, - ReorgPeriod, H256, + config::OpSubmissionConfig, HyperlaneDomain, IndexMode, KnownHyperlaneDomain, ReorgPeriod, + H256, }; use hyperlane_ethereum as h_eth; - use prometheus::{opts, IntGaugeVec, Registry}; - use reqwest::Url; + + use crate::settings::{matching_list::MatchingList, RelayerSettings}; use super::Relayer; @@ -787,6 +1058,8 @@ mod test { ChainConf { domain: HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum), signer: None, + submitter: Default::default(), + estimated_block_time: Duration::from_secs_f64(1.1), reorg_period: ReorgPeriod::None, addresses: CoreContractAddresses { mailbox: H256::from_slice( @@ -827,10 +1100,12 @@ mod test { gas_limit: None, max_fee_per_gas: None, max_priority_fee_per_gas: None, + ..Default::default() }, - operation_batch: OperationBatchConfig { + op_submission_config: OpSubmissionConfig { batch_contract_address: None, max_batch_size: 1, + ..Default::default() }, }), metrics_conf: PrometheusMiddlewareConf { @@ -874,7 +1149,11 @@ mod test { skip_transaction_gas_limit_for: HashSet::new(), allow_local_checkpoint_syncers: true, metric_app_contexts: Vec::new(), + allow_contract_call_caching: true, + ism_cache_configs: Default::default(), max_retries: 1, + tx_id_indexing_enabled: true, + igp_indexing_enabled: true, } } diff --git a/rust/main/agents/relayer/src/server/environment_variable.rs b/rust/main/agents/relayer/src/server/environment_variable.rs new file mode 100644 index 00000000000..ded358ef5a9 --- /dev/null +++ b/rust/main/agents/relayer/src/server/environment_variable.rs @@ -0,0 +1,188 @@ +use std::env; + +use axum::{extract::State, routing, Json, Router}; +use derive_new::new; +use serde::{Deserialize, Serialize}; + +const ENVIRONMENT_VARIABLE: &str = "/environment_variable"; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub struct SetEnvironmentVariableRequest { + name: String, + value: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct EnvironmentVariableResponse { + name: String, + value: Option, + message: String, +} + +#[derive(new, Clone)] +pub struct EnvironmentVariableApi {} + +async fn get_environment_variable( + State(_): State, + Json(body): Json, +) -> Result, String> { + let value = env::var(&body.name).ok(); + + let response = EnvironmentVariableResponse { + name: body.name, + value, + message: "got".to_string(), + }; + + Ok(Json(response)) +} + +async fn set_environment_variable( + State(_): State, + Json(body): Json, +) -> Result, String> { + let message = match &body.value { + None => { + env::remove_var(&body.name); + "unset" + } + Some(value) => { + env::set_var(&body.name, value); + "set" + } + }; + + let response = EnvironmentVariableResponse { + name: body.name, + value: body.value, + message: message.to_string(), + }; + + Ok(Json(response)) +} + +impl EnvironmentVariableApi { + pub fn router(&self) -> Router { + Router::new() + .route("/", routing::get(get_environment_variable)) + .route("/", routing::post(set_environment_variable)) + .with_state(self.clone()) + } + + pub fn get_route(&self) -> (&'static str, Router) { + (ENVIRONMENT_VARIABLE, self.router()) + } +} + +#[cfg(test)] +mod tests { + use std::env::VarError::NotPresent; + use std::net::SocketAddr; + + use axum::http::StatusCode; + use serde_json::{json, Value}; + + use super::*; + + const NAME: &str = "TEST_ENVIRONMENT_VAR"; + const VALUE: &str = "TEST_VALUE"; + + #[derive(Debug)] + struct TestServerSetup { + pub socket_address: SocketAddr, + } + + fn setup_test_server() -> TestServerSetup { + let api = EnvironmentVariableApi::new(); + let (path, router) = api.get_route(); + + let app = Router::new().nest(path, router); + + let server = + axum::Server::bind(&"127.0.0.1:0".parse().unwrap()).serve(app.into_make_service()); + let addr = server.local_addr(); + tokio::spawn(server); + + TestServerSetup { + socket_address: addr, + } + } + + #[tracing_test::traced_test] + #[tokio::test] + async fn test_environment_variable() { + let TestServerSetup { + socket_address: addr, + .. + } = setup_test_server(); + + let set = set(); + let response = request(addr, &set, true).await; + assert_eq!(NAME, response.name); + assert_eq!(Some(VALUE.to_string()), response.value); + assert_eq!("set", response.message); + assert_eq!(VALUE, env::var(NAME).unwrap()); + + let get = get_or_remove(); + let response = request(addr, &get, false).await; + assert_eq!(NAME, response.name); + assert_eq!(Some(VALUE.to_string()), response.value); + assert_eq!("got", response.message); + assert_eq!(VALUE, env::var(NAME).unwrap()); + + let remove = get_or_remove(); + let response = request(addr, &remove, true).await; + assert_eq!(NAME, response.name); + assert_eq!(None, response.value); + assert_eq!("unset", response.message); + assert_eq!(Err(NotPresent), env::var(NAME)); + + let get = get_or_remove(); + let response = request(addr, &get, false).await; + assert_eq!(NAME, response.name); + assert_eq!(None, response.value); + assert_eq!("got", response.message); + assert_eq!(Err(NotPresent), env::var(NAME)); + } + + async fn request(addr: SocketAddr, body: &Value, post: bool) -> EnvironmentVariableResponse { + let client = reqwest::Client::new(); + + let builder = if post { + client.post(format!("http://{}{}", addr, ENVIRONMENT_VARIABLE)) + } else { + client.get(format!("http://{}{}", addr, ENVIRONMENT_VARIABLE)) + }; + + let request = builder.json(&body).build().unwrap(); + let response = tokio::spawn(client.execute(request)) + .await + .unwrap() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let response = response + .json::() + .await + .unwrap(); + response + } + + fn set() -> Value { + json!( + { + "name": NAME, + "value": VALUE, + } + ) + } + + fn get_or_remove() -> Value { + json!( + { + "name": NAME, + } + ) + } +} diff --git a/rust/main/agents/relayer/src/server/mod.rs b/rust/main/agents/relayer/src/server/mod.rs index 03019d98353..c01af7310c5 100644 --- a/rust/main/agents/relayer/src/server/mod.rs +++ b/rust/main/agents/relayer/src/server/mod.rs @@ -1,6 +1,7 @@ use axum::Router; use derive_new::new; use std::collections::HashMap; +use std::env; use tokio::sync::broadcast::Sender; use crate::msg::op_queue::OperationPriorityQueue; @@ -10,6 +11,9 @@ pub const ENDPOINT_MESSAGES_QUEUE_SIZE: usize = 100; pub use list_messages::*; pub use message_retry::*; +use crate::server::environment_variable::EnvironmentVariableApi; + +mod environment_variable; mod list_messages; mod message_retry; @@ -44,6 +48,13 @@ impl Server { routes.push(ListOperationsApi::new(op_queues).get_route()); } + let expose_environment_variable_endpoint = + env::var("HYPERLANE_RELAYER_ENVIRONMENT_VARIABLE_ENDPOINT_ENABLED") + .map_or(false, |v| v == "true"); + if expose_environment_variable_endpoint { + routes.push(EnvironmentVariableApi::new().get_route()); + } + routes } } diff --git a/rust/main/agents/relayer/src/settings/mod.rs b/rust/main/agents/relayer/src/settings/mod.rs index 4d7bcecd1b2..91c8535213e 100644 --- a/rust/main/agents/relayer/src/settings/mod.rs +++ b/rust/main/agents/relayer/src/settings/mod.rs @@ -23,7 +23,8 @@ use serde::Deserialize; use serde_json::Value; use crate::{ - msg::pending_message::DEFAULT_MAX_MESSAGE_RETRIES, settings::matching_list::MatchingList, + msg::{metadata::IsmCacheConfig, pending_message::DEFAULT_MAX_MESSAGE_RETRIES}, + settings::matching_list::MatchingList, }; pub mod matching_list; @@ -63,8 +64,16 @@ pub struct RelayerSettings { pub allow_local_checkpoint_syncers: bool, /// App contexts used for metrics. pub metric_app_contexts: Vec<(MatchingList, String)>, + /// Whether to allow contract call caching at all. + pub allow_contract_call_caching: bool, + /// The ISM cache policies to use + pub ism_cache_configs: Vec, /// Maximum number of retries per operation pub max_retries: u32, + /// Whether to enable indexing of hook events given tx ids from indexed messages. + pub tx_id_indexing_enabled: bool, + /// Whether to enable IGP indexing. + pub igp_indexing_enabled: bool, } /// Config for gas payment enforcement @@ -318,12 +327,36 @@ impl FromRawConf for RelayerSettings { }) .unwrap_or_default(); + let allow_contract_call_caching = p + .chain(&mut err) + .get_opt_key("allowLocalCheckpointSyncers") + .parse_bool() + .unwrap_or(true); + + let ism_cache_configs = p + .chain(&mut err) + .get_opt_key("ismCacheConfigs") + .and_then(parse_ism_cache_configs) + .unwrap_or_default(); + let max_message_retries = p .chain(&mut err) .get_opt_key("maxMessageRetries") .parse_u32() .unwrap_or(DEFAULT_MAX_MESSAGE_RETRIES); + let tx_id_indexing_enabled = p + .chain(&mut err) + .get_opt_key("txIdIndexingEnabled") + .parse_bool() + .unwrap_or(true); + + let igp_indexing_enabled = p + .chain(&mut err) + .get_opt_key("igpIndexingEnabled") + .parse_bool() + .unwrap_or(true); + err.into_result(RelayerSettings { base, db, @@ -337,7 +370,11 @@ impl FromRawConf for RelayerSettings { skip_transaction_gas_limit_for, allow_local_checkpoint_syncers, metric_app_contexts, + allow_contract_call_caching, + ism_cache_configs, max_retries: max_message_retries, + tx_id_indexing_enabled, + igp_indexing_enabled, }) } } @@ -378,6 +415,22 @@ fn parse_matching_list(p: ValueParser) -> ConfigResult { err.into_result(ml) } +fn parse_ism_cache_configs(p: ValueParser) -> ConfigResult> { + let mut err = ConfigParsingError::default(); + + let raw_list = parse_json_array(p.clone()).map(|(_, v)| v); + let Some(raw_list) = raw_list else { + return err.into_result(Default::default()); + }; + let p = ValueParser::new(p.cwp.clone(), &raw_list); + let ml = p + .parse_value::>("Expected ISM cache configs") + .take_config_err(&mut err) + .unwrap_or_default(); + + err.into_result(ml) +} + fn parse_address_list( str: &str, err: &mut ConfigParsingError, @@ -426,4 +479,34 @@ mod test { assert_eq!(res, vec![valid_address1, valid_address2]); assert!(!err.is_ok()); } + + #[test] + fn test_parse_ism_cache_configs() { + let raw = r#" + [ + { + "selector": { + "type": "defaultIsm" + }, + "moduletypes": [2], + "chains": ["foochain"], + "cachepolicy": "ismSpecific" + }, + { + "selector": { + "type": "appContext", + "context": "foo" + }, + "moduletypes": [2], + "chains": ["foochain"], + "cachepolicy": "ismSpecific" + } + ] + "#; + + let value = serde_json::from_str::(raw).unwrap(); + let p = ValueParser::new(ConfigPath::default(), &value); + let configs = parse_ism_cache_configs(p).unwrap(); + assert_eq!(configs.len(), 2); + } } diff --git a/rust/main/agents/relayer/src/test_utils/mock_aggregation_ism.rs b/rust/main/agents/relayer/src/test_utils/mock_aggregation_ism.rs index 0e435f4197b..724d86227d5 100644 --- a/rust/main/agents/relayer/src/test_utils/mock_aggregation_ism.rs +++ b/rust/main/agents/relayer/src/test_utils/mock_aggregation_ism.rs @@ -13,14 +13,25 @@ type ResponseList = Arc>>; #[derive(Debug, Default)] pub struct MockAggregationIsmResponses { pub modules_and_threshold: ResponseList, u8)>>, - pub domain: Option, } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct MockAggregationIsm { + pub address: H256, + pub domain: HyperlaneDomain, pub responses: MockAggregationIsmResponses, } +impl MockAggregationIsm { + pub fn new(address: H256, domain: HyperlaneDomain) -> Self { + Self { + address, + domain, + responses: MockAggregationIsmResponses::default(), + } + } +} + #[async_trait::async_trait] impl AggregationIsm for MockAggregationIsm { async fn modules_and_threshold( @@ -38,16 +49,13 @@ impl AggregationIsm for MockAggregationIsm { impl HyperlaneContract for MockAggregationIsm { fn address(&self) -> H256 { - H256::zero() + self.address } } impl HyperlaneChain for MockAggregationIsm { fn domain(&self) -> &hyperlane_core::HyperlaneDomain { - self.responses - .domain - .as_ref() - .expect("No mock domain response set") + &self.domain } fn provider(&self) -> Box { unimplemented!() diff --git a/rust/main/agents/relayer/src/test_utils/mock_base_builder.rs b/rust/main/agents/relayer/src/test_utils/mock_base_builder.rs index 577e4aedf67..401bde37de3 100644 --- a/rust/main/agents/relayer/src/test_utils/mock_base_builder.rs +++ b/rust/main/agents/relayer/src/test_utils/mock_base_builder.rs @@ -3,13 +3,19 @@ use std::{ sync::{Arc, Mutex}, }; -use hyperlane_base::{settings::CheckpointSyncerBuildError, MultisigCheckpointSyncer}; +use hyperlane_base::{ + cache::{LocalCache, MeteredCache, OptionalCache}, + settings::CheckpointSyncerBuildError, + MultisigCheckpointSyncer, +}; use hyperlane_core::{ accumulator::merkle::Proof, AggregationIsm, CcipReadIsm, Checkpoint, HyperlaneDomain, HyperlaneMessage, InterchainSecurityModule, MultisigIsm, RoutingIsm, H256, }; -use crate::msg::metadata::{BuildsBaseMetadata, IsmAwareAppContextClassifier}; +use crate::msg::metadata::{ + BuildsBaseMetadata, IsmAwareAppContextClassifier, IsmCachePolicyClassifier, +}; type ResponseList = Arc>>; @@ -18,6 +24,8 @@ pub struct MockBaseMetadataBuilderResponses { pub origin_domain: Option, pub destination_domain: Option, pub app_context_classifier: Option, + pub ism_cache_policy_classifier: Option, + pub cache: Option>>, pub get_proof: ResponseList>, pub highest_known_leaf_index: ResponseList>, pub get_merkle_leaf_id_by_message_id: ResponseList>>, @@ -31,9 +39,11 @@ pub struct MockBaseMetadataBuilderResponses { #[allow(clippy::type_complexity)] pub build_ism: Arc>>>>>, - pub build_routing_ism: ResponseList>>, + pub build_routing_ism: Arc>>>>>, + pub build_aggregation_ism: + Arc>>>>>, + // TODO: migrate these to be keyed by address as well pub build_multisig_ism: ResponseList>>, - pub build_aggregation_ism: ResponseList>>, pub build_ccip_read_ism: ResponseList>>, pub build_checkpoint_syncer: ResponseList>, @@ -52,6 +62,32 @@ impl MockBaseMetadataBuilderResponses { .or_default() .push_back(ism); } + + pub fn push_build_aggregation_ism_response( + &self, + address: H256, + ism: eyre::Result>, + ) { + self.build_aggregation_ism + .lock() + .unwrap() + .entry(address) + .or_default() + .push_back(ism); + } + + pub fn push_build_routing_ism_response( + &self, + address: H256, + ism: eyre::Result>, + ) { + self.build_routing_ism + .lock() + .unwrap() + .entry(address) + .or_default() + .push_back(ism); + } } #[derive(Debug, Default)] @@ -87,6 +123,18 @@ impl BuildsBaseMetadata for MockBaseMetadataBuilder { .as_ref() .expect("No mock app_context_classifier response set") } + fn ism_cache_policy_classifier(&self) -> &crate::msg::metadata::IsmCachePolicyClassifier { + self.responses + .ism_cache_policy_classifier + .as_ref() + .expect("No mock app_context_classifier response set") + } + fn cache(&self) -> &OptionalCache> { + self.responses + .cache + .as_ref() + .expect("No mock cache response set") + } async fn get_proof(&self, _leaf_index: u32, _checkpoint: Checkpoint) -> eyre::Result { self.responses @@ -125,13 +173,15 @@ impl BuildsBaseMetadata for MockBaseMetadataBuilder { .pop_front() .expect("No mock build_ism response set") } - async fn build_routing_ism(&self, _address: H256) -> eyre::Result> { + async fn build_routing_ism(&self, address: H256) -> eyre::Result> { self.responses .build_routing_ism .lock() .unwrap() + .get_mut(&address) + .expect("No mock build_aggregation_ism response set") .pop_front() - .expect("No mock build_routing_ism response set") + .expect("No mock build_aggregation_ism response set") } async fn build_multisig_ism(&self, _address: H256) -> eyre::Result> { self.responses @@ -141,11 +191,13 @@ impl BuildsBaseMetadata for MockBaseMetadataBuilder { .pop_front() .expect("No mock build_multisig_ism response set") } - async fn build_aggregation_ism(&self, _address: H256) -> eyre::Result> { + async fn build_aggregation_ism(&self, address: H256) -> eyre::Result> { self.responses .build_aggregation_ism .lock() .unwrap() + .get_mut(&address) + .expect("No mock build_aggregation_ism response set") .pop_front() .expect("No mock build_aggregation_ism response set") } @@ -184,26 +236,23 @@ mod tests { #[tokio::test] async fn test_mock_works() { let base_builder = MockBaseMetadataBuilder::default(); + let domain = HyperlaneDomain::Known(hyperlane_core::KnownHyperlaneDomain::Arbitrum); { - let mock_ism = MockInterchainSecurityModule::new(H256::zero()); - mock_ism - .responses - .module_type - .lock() - .unwrap() - .push_back(Ok(ModuleType::Routing)); + let mock_ism = MockInterchainSecurityModule::new( + H256::zero(), + domain.clone(), + ModuleType::Routing, + ); base_builder .responses .push_build_ism_response(H256::zero(), Ok(Box::new(mock_ism))); } { - let mock_ism = MockInterchainSecurityModule::new(H256::zero()); - mock_ism - .responses - .module_type - .lock() - .unwrap() - .push_back(Ok(ModuleType::Aggregation)); + let mock_ism = MockInterchainSecurityModule::new( + H256::zero(), + domain.clone(), + ModuleType::Aggregation, + ); base_builder .responses .push_build_ism_response(H256::from_low_u64_be(10), Ok(Box::new(mock_ism))); diff --git a/rust/main/agents/relayer/src/test_utils/mock_ism.rs b/rust/main/agents/relayer/src/test_utils/mock_ism.rs index 2910891c12a..05603e049d9 100644 --- a/rust/main/agents/relayer/src/test_utils/mock_ism.rs +++ b/rust/main/agents/relayer/src/test_utils/mock_ism.rs @@ -12,14 +12,14 @@ type ResponseList = Arc>>; #[derive(Debug, Default)] pub struct MockInterchainSecurityModuleResponses { - pub module_type: ResponseList>, pub dry_run_verify: ResponseList>>, - pub domain: Option, } pub struct MockInterchainSecurityModule { - pub responses: MockInterchainSecurityModuleResponses, pub address: H256, + pub domain: HyperlaneDomain, + pub module_type: ModuleType, + pub responses: MockInterchainSecurityModuleResponses, } impl std::fmt::Debug for MockInterchainSecurityModule { @@ -33,10 +33,12 @@ impl std::fmt::Debug for MockInterchainSecurityModule { } impl MockInterchainSecurityModule { - pub fn new(address: H256) -> Self { + pub fn new(address: H256, domain: HyperlaneDomain, module_type: ModuleType) -> Self { Self { - responses: MockInterchainSecurityModuleResponses::default(), address, + domain, + module_type, + responses: MockInterchainSecurityModuleResponses::default(), } } } @@ -44,12 +46,7 @@ impl MockInterchainSecurityModule { #[async_trait::async_trait] impl InterchainSecurityModule for MockInterchainSecurityModule { async fn module_type(&self) -> ChainResult { - self.responses - .module_type - .lock() - .unwrap() - .pop_front() - .expect("No mock module_type response set") + Ok(self.module_type) } /// Dry runs the `verify()` ISM call and returns `Some(gas_estimate)` if the call @@ -70,16 +67,13 @@ impl InterchainSecurityModule for MockInterchainSecurityModule { impl HyperlaneContract for MockInterchainSecurityModule { fn address(&self) -> H256 { - H256::zero() + self.address } } impl HyperlaneChain for MockInterchainSecurityModule { fn domain(&self) -> &hyperlane_core::HyperlaneDomain { - self.responses - .domain - .as_ref() - .expect("No mock domain response set") + &self.domain } fn provider(&self) -> Box { unimplemented!() @@ -88,6 +82,8 @@ impl HyperlaneChain for MockInterchainSecurityModule { #[cfg(test)] mod tests { + use hyperlane_core::KnownHyperlaneDomain; + use crate::test_utils::mock_ism::MockInterchainSecurityModule; use super::*; @@ -95,24 +91,12 @@ mod tests { /// Just to test mock structs work #[tokio::test] async fn test_mock_works() { - let mock_ism = MockInterchainSecurityModule::new(H256::zero()); - mock_ism - .responses - .module_type - .lock() - .unwrap() - .push_back(Ok(ModuleType::Routing)); - mock_ism - .responses - .module_type - .lock() - .unwrap() - .push_back(Ok(ModuleType::Aggregation)); - + let mock_ism = MockInterchainSecurityModule::new( + H256::zero(), + HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum), + ModuleType::Routing, + ); let module_type = mock_ism.module_type().await.expect("No response"); assert_eq!(module_type, ModuleType::Routing); - - let module_type = mock_ism.module_type().await.expect("No response"); - assert_eq!(module_type, ModuleType::Aggregation); } } diff --git a/rust/main/agents/relayer/src/test_utils/mock_routing_ism.rs b/rust/main/agents/relayer/src/test_utils/mock_routing_ism.rs index 0ac3530a5e9..7a7ffee1818 100644 --- a/rust/main/agents/relayer/src/test_utils/mock_routing_ism.rs +++ b/rust/main/agents/relayer/src/test_utils/mock_routing_ism.rs @@ -13,14 +13,25 @@ type ResponseList = Arc>>; #[derive(Debug, Default)] pub struct MockRoutingIsmResponses { pub route: ResponseList>, - pub domain: Option, } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct MockRoutingIsm { + pub address: H256, + pub domain: HyperlaneDomain, pub responses: MockRoutingIsmResponses, } +impl MockRoutingIsm { + pub fn new(address: H256, domain: HyperlaneDomain) -> Self { + Self { + address, + domain, + responses: MockRoutingIsmResponses::default(), + } + } +} + #[async_trait::async_trait] impl RoutingIsm for MockRoutingIsm { async fn route(&self, _message: &HyperlaneMessage) -> ChainResult { @@ -35,16 +46,13 @@ impl RoutingIsm for MockRoutingIsm { impl HyperlaneContract for MockRoutingIsm { fn address(&self) -> H256 { - H256::zero() + self.address } } impl HyperlaneChain for MockRoutingIsm { fn domain(&self) -> &hyperlane_core::HyperlaneDomain { - self.responses - .domain - .as_ref() - .expect("No mock domain response set") + &self.domain } fn provider(&self) -> Box { unimplemented!() @@ -53,12 +61,17 @@ impl HyperlaneChain for MockRoutingIsm { #[cfg(test)] mod tests { + use hyperlane_core::KnownHyperlaneDomain; + use super::*; /// Just to test mock structs work #[tokio::test] async fn test_mock_works() { - let mock_ism = MockRoutingIsm::default(); + let mock_ism = MockRoutingIsm::new( + H256::zero(), + HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum), + ); mock_ism .responses .route diff --git a/rust/main/agents/scraper/Cargo.toml b/rust/main/agents/scraper/Cargo.toml index 937d4b09a02..e5b9a40f99d 100644 --- a/rust/main/agents/scraper/Cargo.toml +++ b/rust/main/agents/scraper/Cargo.toml @@ -20,7 +20,7 @@ itertools.workspace = true num-bigint.workspace = true num-traits.workspace = true prometheus.workspace = true -sea-orm = { workspace = true } +sea-orm = { workspace = true, features = ["mock"] } serde.workspace = true serde_json.workspace = true thiserror.workspace = true @@ -35,7 +35,6 @@ migration = { path = "migration" } [dev-dependencies] reqwest.workspace = true -sea-orm = { workspace = true, features = ["mock"]} tokio-test = "0.4" tracing-test.workspace = true ethers-prometheus = { path = "../../ethers-prometheus", features = ["serde"] } diff --git a/rust/main/agents/scraper/src/agent.rs b/rust/main/agents/scraper/src/agent.rs index 43244a730b5..67847d343e9 100644 --- a/rust/main/agents/scraper/src/agent.rs +++ b/rust/main/agents/scraper/src/agent.rs @@ -41,9 +41,10 @@ struct ChainScraper { impl BaseAgent for Scraper { const AGENT_NAME: &'static str = "scraper"; type Settings = ScraperSettings; + type Metadata = AgentMetadata; async fn from_settings( - _agent_metadata: AgentMetadata, + _agent_metadata: Self::Metadata, settings: Self::Settings, metrics: Arc, agent_metrics: AgentMetrics, @@ -272,6 +273,7 @@ impl Scraper { &contract_sync_metrics.clone(), store.into(), true, + true, ) .await .map_err(|err| { @@ -308,6 +310,7 @@ impl Scraper { &contract_sync_metrics.clone(), Arc::new(store.clone()) as _, true, + true, ) .await .map_err(|err| { @@ -346,6 +349,7 @@ impl Scraper { &contract_sync_metrics.clone(), Arc::new(store.clone()) as _, true, + true, ) .await .map_err(|err| { @@ -371,11 +375,13 @@ impl Scraper { #[cfg(test)] mod test { use std::collections::BTreeMap; + use std::time::Duration; use ethers::utils::hex; use ethers_prometheus::middleware::PrometheusMiddlewareConf; use prometheus::{opts, IntGaugeVec, Registry}; use reqwest::Url; + use sea_orm::{DatabaseBackend, MockDatabase}; use hyperlane_base::{ settings::{ @@ -384,10 +390,9 @@ mod test { BLOCK_HEIGHT_HELP, BLOCK_HEIGHT_LABELS, CRITICAL_ERROR_HELP, CRITICAL_ERROR_LABELS, }; use hyperlane_core::{ - config::OperationBatchConfig, IndexMode, KnownHyperlaneDomain, ReorgPeriod, H256, + config::OpSubmissionConfig, IndexMode, KnownHyperlaneDomain, ReorgPeriod, H256, }; use hyperlane_ethereum as h_eth; - use sea_orm::{DatabaseBackend, MockDatabase}; use super::*; @@ -397,6 +402,8 @@ mod test { ChainConf { domain: HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum), signer: None, + submitter: Default::default(), + estimated_block_time: Duration::from_secs_f64(1.1), reorg_period: ReorgPeriod::None, addresses: CoreContractAddresses { mailbox: H256::from_slice( @@ -437,10 +444,12 @@ mod test { gas_limit: None, max_fee_per_gas: None, max_priority_fee_per_gas: None, + ..Default::default() }, - operation_batch: OperationBatchConfig { + op_submission_config: OpSubmissionConfig { batch_contract_address: None, max_batch_size: 1, + ..Default::default() }, }), metrics_conf: PrometheusMiddlewareConf { diff --git a/rust/main/agents/validator/Cargo.toml b/rust/main/agents/validator/Cargo.toml index 7228ad69cb4..7c9ac96dea6 100644 --- a/rust/main/agents/validator/Cargo.toml +++ b/rust/main/agents/validator/Cargo.toml @@ -10,6 +10,7 @@ version.workspace = true [dependencies] async-trait.workspace = true +aws-config.workspace = true axum.workspace = true chrono.workspace = true config.workspace = true @@ -27,6 +28,7 @@ thiserror.workspace = true tokio = { workspace = true, features = ["rt", "macros", "parking_lot"] } tracing-futures.workspace = true tracing.workspace = true +itertools.workspace = true hyperlane-core = { path = "../../hyperlane-core", features = [ "agent", @@ -36,6 +38,9 @@ hyperlane-base = { path = "../../hyperlane-base" } hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" } hyperlane-cosmos = { path = "../../chains/hyperlane-cosmos" } +# Dependency version is determined by ethers +rusoto_core = '*' + [dev-dependencies] mockall.workspace = true tokio-test.workspace = true diff --git a/rust/main/agents/validator/src/main.rs b/rust/main/agents/validator/src/main.rs index 14056046e0c..7f9923d679b 100644 --- a/rust/main/agents/validator/src/main.rs +++ b/rust/main/agents/validator/src/main.rs @@ -14,7 +14,7 @@ mod settings; mod submit; mod validator; -#[tokio::main(flavor = "current_thread")] +#[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { // Logging is not initialised at this point, so, using `println!` println!("Validator starting up..."); diff --git a/rust/main/agents/validator/src/settings.rs b/rust/main/agents/validator/src/settings.rs index d464be5a926..ddf23dd7560 100644 --- a/rust/main/agents/validator/src/settings.rs +++ b/rust/main/agents/validator/src/settings.rs @@ -6,6 +6,7 @@ use std::{collections::HashSet, path::PathBuf, time::Duration}; +use aws_config::Region; use derive_more::{AsMut, AsRef, Deref, DerefMut}; use eyre::{eyre, Context}; use hyperlane_base::{ @@ -18,9 +19,17 @@ use hyperlane_base::{ use hyperlane_core::{ cfg_unwrap_all, config::*, HyperlaneDomain, HyperlaneDomainProtocol, ReorgPeriod, }; +use itertools::Itertools; use serde::Deserialize; use serde_json::Value; +/// Settings for RPCs +#[derive(Debug)] +pub struct RpcConfig { + pub url: String, + pub public: bool, +} + /// Settings for `Validator` #[derive(Debug, AsRef, AsMut, Deref, DerefMut)] pub struct ValidatorSettings { @@ -42,6 +51,12 @@ pub struct ValidatorSettings { pub reorg_period: ReorgPeriod, /// How frequently to check for new checkpoints pub interval: Duration, + /// A list of RPCs that the validator uses + pub rpcs: Vec, + /// If the validator oped into public RPCs + pub allow_public_rpcs: bool, + /// Max sign concurrency + pub max_sign_concurrency: usize, } #[derive(Debug, Deserialize)] @@ -66,6 +81,12 @@ impl FromRawConf for ValidatorSettings { .parse_string() .end(); + let allow_public_rpcs = p + .chain(&mut err) + .get_opt_key("allowPublicRpcs") + .parse_bool() + .unwrap_or(false); + let origin_chain_name_set = origin_chain_name.map(|s| HashSet::from([s])); let base: Option = p @@ -127,6 +148,23 @@ impl FromRawConf for ValidatorSettings { .parse_value("Invalid reorgPeriod") .unwrap_or(ReorgPeriod::from_blocks(1)); + let chain = p + .chain(&mut err) + .get_key("chains") + .get_key(origin_chain_name) + .end() + .unwrap(); + + let max_sign_concurrency = p + .chain(&mut err) + .get_opt_key("maxSignConcurrency") + .parse_u64() + .unwrap_or(50) as usize; + + let mut rpcs = get_rpc_urls(&chain, "rpcUrls", "customRpcUrls", &mut err); + // this is only relevant for cosmos + rpcs.extend(get_rpc_urls(&chain, "grpcUrls", "customGrpcUrls", &mut err)); + cfg_unwrap_all!(cwp, err: [base, origin_chain, validator, checkpoint_syncer]); let mut base: Settings = base; @@ -145,10 +183,70 @@ impl FromRawConf for ValidatorSettings { checkpoint_syncer, reorg_period, interval, + rpcs, + allow_public_rpcs, + max_sign_concurrency, }) } } +/// Extracts all of the rpc urls +/// +/// rpcKey is either grpcUrls or rpcUrls +/// overrideKey is either customGrpcUrls or customRpcUrls +fn get_rpc_urls( + chain: &ValueParser, + rpc_key: &str, + override_key: &str, + err: &mut ConfigParsingError, +) -> Vec { + // struct looks like the following + // ```rust + // { + // rpc: [ + // { + // "http": "http://my-rpc-url.com", + // "public": true + // } + // ] + // } + // ``` + let base = chain + .chain(err) + .get_opt_key(rpc_key) + .into_array_iter() + .map(|urls| { + urls.filter_map(|v| { + let public = v + .chain(err) + .get_opt_key("public") + .parse_bool() + .unwrap_or(false); + let url: Option<&str> = v.chain(err).get_key("http").parse_string().end(); + url.map(|url| RpcConfig { + url: url.to_owned(), + public, + }) + }) + .collect_vec() + }) + .unwrap_or_default(); + let overrides = chain + .chain(err) + .get_opt_key(override_key) + .parse_string() + .end() + .map(|urls| { + urls.split(',') + .map(|url| RpcConfig { + url: url.to_owned(), + public: false, + }) + .collect_vec() + }); + overrides.unwrap_or(base) +} + /// Expects ValidatorAgentConfig.checkpointSyncer fn parse_checkpoint_syncer(syncer: ValueParser) -> ConfigResult { let mut err = ConfigParsingError::default(); @@ -171,7 +269,8 @@ fn parse_checkpoint_syncer(syncer: ValueParser) -> ConfigResult = syncer .chain(&mut err) .get_key("region") .parse_from_str("Expected aws region") @@ -186,7 +285,7 @@ fn parse_checkpoint_syncer(syncer: ValueParser) -> ConfigResult ConfigResult Err(err), } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_get_rpc_urls_explicit() { + let expected = vec![ + RpcConfig { + url: "http://my-rpc-url.com".to_string(), + public: true, + }, + RpcConfig { + url: "http://my-rpc-url-2.com".to_string(), + public: false, + }, + ]; + + let rpcs = expected + .iter() + .map(|rpc| { + serde_json::json!({ + "http": rpc.url, + "public": rpc.public + }) + }) + .collect::>(); + let rpcs = serde_json::json!({ + "rpcurls": rpcs + }); + + let mut err = ConfigParsingError::default(); + let value_parser = ValueParser::new(ConfigPath::default(), &rpcs); + let parsed = get_rpc_urls(&value_parser, "rpcUrls", "customRpcUrls", &mut err); // why does it convert to lowercase? + + assert_eq!(parsed.len(), expected.len()); + for (i, rpc) in expected.iter().enumerate() { + assert_eq!(parsed[i].url, rpc.url); + assert_eq!(parsed[i].public, rpc.public); + } + } + + #[test] + fn test_get_rpc_urls_implicit_private() { + let rpcs = r#" + { + "rpcurls": [ + { + "http": "http://my-rpc-url.com" + }, + { + "http": "http://my-rpc-url-2.com", + "public": false + } + ] + } + "#; + let rpcs = serde_json::from_str(rpcs).unwrap(); + let mut err = ConfigParsingError::default(); + let value_parser = ValueParser::new(ConfigPath::default(), &rpcs); + let parsed = get_rpc_urls(&value_parser, "rpcUrls", "customRpcUrls", &mut err); + + assert_eq!(parsed.len(), 2); + assert_eq!(parsed[0].url, "http://my-rpc-url.com"); + assert_eq!(parsed[0].public, false); + assert_eq!(parsed[1].url, "http://my-rpc-url-2.com"); + assert_eq!(parsed[1].public, false); + } + + #[test] + fn test_get_rpc_urls_overrides() { + let rpcs = r#" + { + "rpcurls": [ + { + "http": "http://my-rpc-url.com" + }, + { + "http": "http://my-rpc-url-2.com", + "public": false + } + ], + "customrpcurls": "http://my-rpc-url-3.com,http://my-rpc-url-4.com" + } + "#; + let rpcs = serde_json::from_str(rpcs).unwrap(); + let mut err = ConfigParsingError::default(); + let value_parser = ValueParser::new(ConfigPath::default(), &rpcs); + let parsed = get_rpc_urls(&value_parser, "rpcUrls", "customRpcUrls", &mut err); + + assert_eq!(parsed.len(), 2); + assert_eq!(parsed[0].url, "http://my-rpc-url-3.com"); + assert_eq!(parsed[0].public, false); + assert_eq!(parsed[1].url, "http://my-rpc-url-4.com"); + assert_eq!(parsed[1].public, false); + } +} diff --git a/rust/main/agents/validator/src/submit.rs b/rust/main/agents/validator/src/submit.rs index f4779c66568..f7ce326f036 100644 --- a/rust/main/agents/validator/src/submit.rs +++ b/rust/main/agents/validator/src/submit.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use std::vec; +use futures::future::join_all; use prometheus::IntGauge; use tokio::time::sleep; use tracing::{debug, error, info}; @@ -13,38 +14,46 @@ use hyperlane_core::{ accumulator::incremental::IncrementalMerkle, Checkpoint, CheckpointWithMessageId, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneSignerExt, }; -use hyperlane_core::{ChainResult, MerkleTreeHook, ReorgEvent, ReorgPeriod}; -use hyperlane_ethereum::SingletonSignerHandle; +use hyperlane_core::{ChainResult, MerkleTreeHook, ReorgEvent, ReorgPeriod, SignedType}; +use hyperlane_ethereum::{Signers, SingletonSignerHandle}; #[derive(Clone)] pub(crate) struct ValidatorSubmitter { interval: Duration, reorg_period: ReorgPeriod, - signer: SingletonSignerHandle, + #[allow(unused)] + singleton_signer: SingletonSignerHandle, + signer: Signers, merkle_tree_hook: Arc, checkpoint_syncer: Arc, db: Arc, metrics: ValidatorSubmitterMetrics, + max_sign_concurrency: usize, } impl ValidatorSubmitter { + #[allow(clippy::too_many_arguments)] pub(crate) fn new( interval: Duration, reorg_period: ReorgPeriod, merkle_tree_hook: Arc, - signer: SingletonSignerHandle, + singleton_signer: SingletonSignerHandle, + signer: Signers, checkpoint_syncer: Arc, db: Arc, metrics: ValidatorSubmitterMetrics, + max_sign_concurrency: usize, ) -> Self { Self { reorg_period, interval, merkle_tree_hook, + singleton_signer, signer, checkpoint_syncer, db, metrics, + max_sign_concurrency, } } @@ -68,6 +77,9 @@ impl ValidatorSubmitter { ?target_checkpoint, "Backfill checkpoint submitter successfully reached target checkpoint" ); + + // Set that backfill is completed in metrics + self.metrics.backfill_complete.set(1); } /// Submits signed checkpoints indefinitely, starting from the `tree`. @@ -131,6 +143,9 @@ impl ValidatorSubmitter { .latest_checkpoint_processed .set(latest_checkpoint.index as i64); + // Set that initial consistency has been reached on first loop run. Subsequent runs are idempotent. + self.metrics.reached_initial_consistency.set(1); + sleep(self.interval).await; } } @@ -142,6 +157,7 @@ impl ValidatorSubmitter { tree: &mut IncrementalMerkle, correctness_checkpoint: &Checkpoint, ) { + let start = Instant::now(); // This should never be called with a tree that is ahead of the correctness checkpoint. assert!( !tree_exceeds_checkpoint(correctness_checkpoint, tree), @@ -234,6 +250,12 @@ impl ValidatorSubmitter { panic!("{panic_message}"); } + tracing::info!( + elapsed=?start.elapsed(), + checkpoint_queue_len = checkpoint_queue.len(), + "Checkpoint submitter reached correctness checkpoint" + ); + if !checkpoint_queue.is_empty() { info!( index = checkpoint.index, @@ -249,22 +271,73 @@ impl ValidatorSubmitter { } } + async fn sign_checkpoint( + &self, + checkpoint: CheckpointWithMessageId, + ) -> ChainResult> { + let signer_retries = 5; + + for i in 0..signer_retries { + match self.signer.sign(checkpoint).await { + Ok(signed_checkpoint) => return Ok(signed_checkpoint), + Err(err) => { + tracing::warn!( + ?checkpoint, + attempt = i, + retries = signer_retries, + ?err, + "Error signing checkpoint with direct signer" + ); + sleep(Duration::from_millis(100)).await; + } + } + } + + tracing::warn!( + ?checkpoint, + retries = signer_retries, + "Error signing checkpoint with direct signer after all retries, falling back to singleton signer" + ); + + // Now try the singleton signer as a last resort + Ok(self.singleton_signer.sign(checkpoint).await?) + } + async fn sign_and_submit_checkpoint( &self, checkpoint: CheckpointWithMessageId, ) -> ChainResult<()> { + let start = Instant::now(); let existing = self .checkpoint_syncer .fetch_checkpoint(checkpoint.index) .await?; + tracing::trace!( + elapsed=?start.elapsed(), + "Fetched checkpoint from checkpoint storage", + ); + if existing.is_some() { debug!(index = checkpoint.index, "Checkpoint already submitted"); return Ok(()); } - let signed_checkpoint = self.signer.sign(checkpoint).await?; + + let start = Instant::now(); + let signed_checkpoint = self.sign_checkpoint(checkpoint).await?; + tracing::trace!( + elapsed=?start.elapsed(), + "Signed checkpoint", + ); + + let start = Instant::now(); self.checkpoint_syncer .write_checkpoint(&signed_checkpoint) .await?; + tracing::trace!( + elapsed=?start.elapsed(), + "Stored checkpoint", + ); + debug!(index = checkpoint.index, "Signed and submitted checkpoint"); // TODO: move these into S3 implementations @@ -274,36 +347,81 @@ impl ValidatorSubmitter { } /// Signs and submits any previously unsubmitted checkpoints. - async fn sign_and_submit_checkpoints(&self, checkpoints: Vec) { - let last_checkpoint = checkpoints.as_slice()[checkpoints.len() - 1]; - // Submits checkpoints to the store in reverse order. This speeds up processing historic checkpoints (those before the validator is spun up), - // since those are the most likely to make messages become processable. - // A side effect is that new checkpoints will also be submitted in reverse order. - for queued_checkpoint in checkpoints.into_iter().rev() { - // certain checkpoint stores rate limit very aggressively, so we retry indefinitely - call_and_retry_indefinitely(|| { - let self_clone = self.clone(); - Box::pin(async move { - self_clone - .sign_and_submit_checkpoint(queued_checkpoint) - .await?; - Ok(()) + async fn sign_and_submit_checkpoints(&self, mut checkpoints: Vec) { + // The checkpoints are ordered by index, so the last one is the highest index. + let last_checkpoint_index = checkpoints[checkpoints.len() - 1].index; + + let arc_self = Arc::new(self.clone()); + + let mut first_chunk = true; + + while !checkpoints.is_empty() { + let start = Instant::now(); + + // Take a chunk of checkpoints, starting with the highest index. + // This speeds up processing historic checkpoints (those before the validator is spun up), + // since those are the most likely to make messages become processable. + // A side effect is that new checkpoints will also be submitted in reverse order. + + // This logic is a bit awkward, but we want control over the chunks so we can also + // write the latest index to the checkpoint storage after the first chunk is successful. + let mut chunk = Vec::with_capacity(self.max_sign_concurrency); + for _ in 0..self.max_sign_concurrency { + if let Some(cp) = checkpoints.pop() { + chunk.push(cp); + } else { + break; + } + } + + let chunk_len = chunk.len(); + + let futures = chunk.into_iter().map(|checkpoint| { + let self_clone = arc_self.clone(); + call_and_retry_indefinitely(move || { + let self_clone = self_clone.clone(); + Box::pin(async move { + let start = Instant::now(); + self_clone.sign_and_submit_checkpoint(checkpoint).await?; + tracing::info!( + elapsed=?start.elapsed(), + "Signed and submitted checkpoint", + ); + Ok(()) + }) }) - }) - .await; - } + }); - call_and_retry_indefinitely(|| { - let self_clone = self.clone(); - Box::pin(async move { - self_clone - .checkpoint_syncer - .update_latest_index(last_checkpoint.index) - .await?; - Ok(()) - }) - }) - .await; + join_all(futures).await; + + tracing::info!( + elapsed=?start.elapsed(), + chunk_len, + remaining_checkpoints = checkpoints.len(), + "Signed and submitted checkpoint chunk", + ); + + // If it's the first chunk, update the latest index + if first_chunk { + call_and_retry_indefinitely(|| { + let self_clone = self.clone(); + Box::pin(async move { + let start = Instant::now(); + self_clone + .checkpoint_syncer + .update_latest_index(last_checkpoint_index) + .await?; + tracing::trace!( + elapsed=?start.elapsed(), + "Updated latest index", + ); + Ok(()) + }) + }) + .await; + first_chunk = false; + } + } } } @@ -318,6 +436,8 @@ fn tree_exceeds_checkpoint(checkpoint: &Checkpoint, tree: &IncrementalMerkle) -> pub(crate) struct ValidatorSubmitterMetrics { latest_checkpoint_observed: IntGauge, latest_checkpoint_processed: IntGauge, + backfill_complete: IntGauge, + reached_initial_consistency: IntGauge, } impl ValidatorSubmitterMetrics { @@ -330,6 +450,10 @@ impl ValidatorSubmitterMetrics { latest_checkpoint_processed: metrics .latest_checkpoint() .with_label_values(&["validator_processed", chain_name]), + backfill_complete: metrics.backfill_complete().with_label_values(&[chain_name]), + reached_initial_consistency: metrics + .reached_initial_consistency() + .with_label_values(&[chain_name]), } } } @@ -339,15 +463,15 @@ mod test { use super::*; use async_trait::async_trait; use eyre::Result; - use hyperlane_base::{ - db::{DbResult, HyperlaneDb, InterchainGasExpenditureData, InterchainGasPaymentData}, - AgentMetadata, + use hyperlane_base::db::{ + DbResult, HyperlaneDb, InterchainGasExpenditureData, InterchainGasPaymentData, }; use hyperlane_core::{ - test_utils::dummy_domain, GasPaymentKey, HyperlaneChain, HyperlaneContract, - HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, InterchainGasPayment, - InterchainGasPaymentMeta, MerkleTreeHook, MerkleTreeInsertion, PendingOperationStatus, - ReorgEvent, SignedAnnouncement, SignedCheckpointWithMessageId, H160, H256, + identifiers::UniqueIdentifier, test_utils::dummy_domain, GasPaymentKey, HyperlaneChain, + HyperlaneContract, HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, + InterchainGasPayment, InterchainGasPaymentMeta, MerkleTreeHook, MerkleTreeInsertion, + PendingOperationStatus, ReorgEvent, SignedAnnouncement, SignedCheckpointWithMessageId, + H160, H256, }; use prometheus::Registry; use std::{fmt::Debug, sync::Arc, time::Duration}; @@ -464,7 +588,8 @@ mod test { ) -> DbResult>; fn store_highest_seen_message_nonce_number(&self, nonce: &u32) -> DbResult<()>; fn retrieve_highest_seen_message_nonce_number(&self) -> DbResult>; - + fn store_payload_id_by_message_id(&self, message_id: &H256, payload_id: &UniqueIdentifier) -> DbResult<()>; + fn retrieve_payload_id_by_message_id(&self, message_id: &H256) -> DbResult>; } } @@ -509,7 +634,7 @@ mod test { &self, signed_checkpoint: &SignedCheckpointWithMessageId, ) -> Result<()>; - async fn write_metadata(&self, metadata: &AgentMetadata) -> Result<()>; + async fn write_metadata(&self, metadata: &str) -> Result<()>; async fn write_announcement(&self, signed_announcement: &SignedAnnouncement) -> Result<()>; fn announcement_location(&self) -> String; async fn write_reorg_status(&self, reorg_event: &ReorgEvent) -> Result<()>; @@ -620,15 +745,22 @@ mod test { Ok(()) }); + let signer: Signers = "1111111111111111111111111111111111111111111111111111111111111111" + .parse::() + .unwrap() + .into(); + // instantiate the validator submitter let validator_submitter = ValidatorSubmitter::new( Duration::from_secs(1), ReorgPeriod::from_blocks(expected_reorg_period), Arc::new(mock_merkle_tree_hook), dummy_singleton_handle(), + signer, Arc::new(mock_checkpoint_syncer), Arc::new(db), dummy_metrics(), + 50, ); // mock the correctness checkpoint response diff --git a/rust/main/agents/validator/src/validator.rs b/rust/main/agents/validator/src/validator.rs index 671194aeca9..5dd1c316377 100644 --- a/rust/main/agents/validator/src/validator.rs +++ b/rust/main/agents/validator/src/validator.rs @@ -3,18 +3,21 @@ use std::{sync::Arc, time::Duration}; use crate::server as validator_server; use async_trait::async_trait; use derive_more::AsRef; -use eyre::Result; - +use ethers::utils::keccak256; +use eyre::{eyre, Result}; use futures_util::future::try_join_all; +use itertools::Itertools; +use serde::Serialize; use tokio::{task::JoinHandle, time::sleep}; use tracing::{error, info, info_span, warn, Instrument}; use hyperlane_base::{ db::{HyperlaneDb, HyperlaneRocksDB, DB}, + git_sha, metrics::AgentMetrics, settings::ChainConf, - AgentMetadata, BaseAgent, ChainMetrics, ChainSpecificMetricsUpdater, CheckpointSyncer, - ContractSyncMetrics, ContractSyncer, CoreMetrics, HyperlaneAgentCore, RuntimeMetrics, + BaseAgent, ChainMetrics, ChainSpecificMetricsUpdater, CheckpointSyncer, ContractSyncMetrics, + ContractSyncer, CoreMetrics, HyperlaneAgentCore, MetadataFromSettings, RuntimeMetrics, SequencedDataContractSync, }; @@ -23,7 +26,7 @@ use hyperlane_core::{ HyperlaneSignerExt, Mailbox, MerkleTreeHook, MerkleTreeInsertion, ReorgPeriod, TxOutcome, ValidatorAnnounce, H256, U256, }; -use hyperlane_ethereum::{SingletonSigner, SingletonSignerHandle}; +use hyperlane_ethereum::{Signers, SingletonSigner, SingletonSignerHandle}; use crate::{ settings::ValidatorSettings, @@ -43,6 +46,7 @@ pub struct Validator { merkle_tree_hook: Arc, validator_announce: Arc, signer: SingletonSignerHandle, + raw_signer: Signers, // temporary holder until `run` is called signer_instance: Option>, reorg_period: ReorgPeriod, @@ -52,7 +56,33 @@ pub struct Validator { agent_metrics: AgentMetrics, chain_metrics: ChainMetrics, runtime_metrics: RuntimeMetrics, - agent_metadata: AgentMetadata, + agent_metadata: ValidatorMetadata, + max_sign_concurrency: usize, +} + +/// Metadata for `validator` +#[derive(Debug, Serialize)] +pub struct ValidatorMetadata { + git_sha: String, + rpcs: Vec, + allows_public_rpcs: bool, +} + +impl MetadataFromSettings for ValidatorMetadata { + /// Create a new instance of the agent metadata from the settings + fn build_metadata(settings: &ValidatorSettings) -> ValidatorMetadata { + // Hash all the RPCs for the metadata + let rpcs = settings + .rpcs + .iter() + .map(|rpc| H256::from_slice(&keccak256(&rpc.url))) + .collect(); + ValidatorMetadata { + git_sha: git_sha(), + rpcs, + allows_public_rpcs: settings.allow_public_rpcs, + } + } } #[async_trait] @@ -60,9 +90,10 @@ impl BaseAgent for Validator { const AGENT_NAME: &'static str = "validator"; type Settings = ValidatorSettings; + type Metadata = ValidatorMetadata; async fn from_settings( - agent_metadata: AgentMetadata, + agent_metadata: Self::Metadata, settings: Self::Settings, metrics: Arc, agent_metrics: AgentMetrics, @@ -73,11 +104,23 @@ impl BaseAgent for Validator { where Self: Sized, { + // Check for public rpcs in the config + if settings.rpcs.iter().any(|x| x.public) && !settings.allow_public_rpcs { + return Err( + eyre!( + "Public RPC endpoints detected: {}. Using public RPCs can compromise security and reliability. If you understand the risks and still want to proceed, set `--allowPublicRpcs true`. We strongly recommend using private RPC endpoints for production validators.", + settings.rpcs.iter().filter_map(|x| if x.public { Some(x.url.clone()) } else { None }).join(", ") + ) + ); + } + let db = DB::from_path(&settings.db)?; let msg_db = HyperlaneRocksDB::new(&settings.origin_chain, db); + let raw_signer: Signers = settings.validator.build().await?; + // Intentionally using hyperlane_ethereum for the validator's signer - let (signer_instance, signer) = SingletonSigner::new(settings.validator.build().await?); + let (signer_instance, signer) = SingletonSigner::new(raw_signer.clone()); let core = settings.build_hyperlane_core(metrics.clone()); // Be extra sure to panic checkpoint syncer fails, which indicates @@ -116,6 +159,7 @@ impl BaseAgent for Validator { &contract_sync_metrics, msg_db.clone().into(), false, + false, ) .await?; @@ -129,6 +173,7 @@ impl BaseAgent for Validator { merkle_tree_hook_sync, validator_announce: validator_announce.into(), signer, + raw_signer, signer_instance: Some(Box::new(signer_instance)), reorg_period: settings.reorg_period, interval: settings.interval, @@ -138,6 +183,7 @@ impl BaseAgent for Validator { core_metrics: metrics, runtime_metrics, agent_metadata, + max_sign_concurrency: settings.max_sign_concurrency, }) } @@ -255,9 +301,11 @@ impl Validator { self.reorg_period.clone(), self.merkle_tree_hook.clone(), self.signer.clone(), + self.raw_signer.clone(), self.checkpoint_syncer.clone(), Arc::new(self.db.clone()) as Arc, ValidatorSubmitterMetrics::new(&self.core.metrics, &self.origin_chain), + self.max_sign_concurrency, ); let tip_tree = self @@ -265,6 +313,7 @@ impl Validator { .tree(&self.reorg_period) .await .expect("failed to get merkle tree"); + // This function is only called after we have already checked that the // merkle tree hook has count > 0, but we assert to be extra sure this is // the case. @@ -321,11 +370,10 @@ impl Validator { } async fn metadata(&self) -> Result<()> { + let serialized_metadata = serde_json::to_string_pretty(&self.agent_metadata)?; self.checkpoint_syncer - .write_metadata(&self.agent_metadata) - .await?; - - Ok(()) + .write_metadata(&serialized_metadata) + .await } async fn announce(&self) -> Result<()> { @@ -363,6 +411,9 @@ impl Validator { ?announcement_location, "Validator has announced signature storage location" ); + + self.core_metrics.set_announced(self.origin_chain.clone()); + break; } info!( diff --git a/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs b/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs index 0e4e182bfa7..76ae7253739 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs @@ -175,7 +175,11 @@ impl Mailbox for CosmosNativeMailbox { /// Get the calldata for a transaction to process a message with a proof /// against the provided signed checkpoint - fn process_calldata(&self, _message: &HyperlaneMessage, _metadata: &[u8]) -> Vec { + async fn process_calldata( + &self, + _message: &HyperlaneMessage, + _metadata: &[u8], + ) -> ChainResult> { todo!() // we dont need this for now } } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs index cb739542551..49468aa051a 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs @@ -1,3 +1,6 @@ +use std::time::Duration; + +use derive_new::new; use hyperlane_cosmos_rs::cosmos::base::tendermint::v1beta1::service_client::ServiceClient; use hyperlane_cosmos_rs::cosmos::base::tendermint::v1beta1::GetLatestBlockRequest; use hyperlane_cosmos_rs::hyperlane::core::interchain_security::v1::{ @@ -25,13 +28,15 @@ use hyperlane_metric::prometheus_metric::{ use crate::prometheus::metrics_channel::MetricsChannel; use crate::{ConnectionConf, HyperlaneCosmosError}; +const REQUEST_TIMEOUT: u64 = 30; + /// Grpc Provider #[derive(Clone, Debug)] pub struct GrpcProvider { fallback: FallbackProvider, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, new)] struct CosmosGrpcClient { channel: MetricsChannel, } @@ -40,7 +45,8 @@ struct CosmosGrpcClient { impl BlockNumberGetter for CosmosGrpcClient { async fn get_block_number(&self) -> Result { let mut client = ServiceClient::new(self.channel.clone()); - let request = tonic::Request::new(GetLatestBlockRequest {}); + let mut request = tonic::Request::new(GetLatestBlockRequest {}); + request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); let response = client .get_latest_block(request) .await @@ -70,13 +76,10 @@ impl GrpcProvider { let metrics_config = PrometheusConfig::from_url(url, ClientConnectionType::Grpc, chain.clone()); Endpoint::new(url.to_string()) - .map(|e| { - let metrics_channel = - MetricsChannel::new(e.connect_lazy(), metrics.clone(), metrics_config); - CosmosGrpcClient { - channel: metrics_channel, - } - }) + .map(|e| e.timeout(Duration::from_secs(REQUEST_TIMEOUT))) + .map(|e| e.connect_timeout(Duration::from_secs(REQUEST_TIMEOUT))) + .map(|e| MetricsChannel::new(e.connect_lazy(), metrics.clone(), metrics_config)) + .map(CosmosGrpcClient::new) .map_err(Into::::into) }) .collect::, _>>() @@ -91,6 +94,7 @@ impl GrpcProvider { height: Option, ) -> tonic::Request { let mut request = request.into_request(); + request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); if let Some(height) = height { request .metadata_mut() diff --git a/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs b/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs index 046207085f0..c529f63e505 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs @@ -4,7 +4,7 @@ use derive_new::new; use url::Url; use hyperlane_core::{ - config::OperationBatchConfig, ChainCommunicationError, FixedPointNumber, NativeToken, + config::OpSubmissionConfig, ChainCommunicationError, FixedPointNumber, NativeToken, }; /// Cosmos connection configuration @@ -31,7 +31,7 @@ pub struct ConnectionConf { /// bech32 with the appropriate length. contract_address_bytes: usize, /// Operation batching configuration - pub operation_batch: OperationBatchConfig, + pub operation_batch: OpSubmissionConfig, /// Native Token native_token: NativeToken, } @@ -141,7 +141,7 @@ impl ConnectionConf { minimum_gas_price: RawCosmosAmount, gas_multiplier: f64, contract_address_bytes: usize, - operation_batch: OperationBatchConfig, + operation_batch: OpSubmissionConfig, native_token: NativeToken, ) -> Self { Self { diff --git a/rust/main/chains/hyperlane-cosmos/src/mailbox/contract.rs b/rust/main/chains/hyperlane-cosmos/src/mailbox/contract.rs index 29beb5c4df0..7bb17989729 100644 --- a/rust/main/chains/hyperlane-cosmos/src/mailbox/contract.rs +++ b/rust/main/chains/hyperlane-cosmos/src/mailbox/contract.rs @@ -199,7 +199,11 @@ impl Mailbox for CosmosMailbox { Ok(result) } - fn process_calldata(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Vec { + async fn process_calldata( + &self, + _message: &HyperlaneMessage, + _metadata: &[u8], + ) -> ChainResult> { todo!() // not required } } diff --git a/rust/main/chains/hyperlane-cosmos/src/providers/grpc.rs b/rust/main/chains/hyperlane-cosmos/src/providers/grpc.rs index 607ee139fc3..ff525bebb56 100644 --- a/rust/main/chains/hyperlane-cosmos/src/providers/grpc.rs +++ b/rust/main/chains/hyperlane-cosmos/src/providers/grpc.rs @@ -1,4 +1,6 @@ -use std::{fmt::Debug, future::Future, time::Instant}; +use std::fmt::Debug; +use std::future::Future; +use std::time::{Duration, Instant}; use async_trait::async_trait; use cosmrs::{ @@ -61,6 +63,8 @@ const GAS_ESTIMATE_MULTIPLIER: f64 = 1.25; /// The number of blocks in the future in which a transaction will /// be valid for. const TIMEOUT_BLOCKS: u64 = 1000; +/// gRPC request timeout +const REQUEST_TIMEOUT: u64 = 30; #[derive(Debug, Clone, new)] struct CosmosChannel { @@ -73,7 +77,8 @@ struct CosmosChannel { impl CosmosChannel { async fn latest_block_height(&self) -> ChainResult { let mut client = ServiceClient::new(self.channel.clone()); - let request = tonic::Request::new(GetLatestBlockRequest {}); + let mut request = tonic::Request::new(GetLatestBlockRequest {}); + request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); let response = client .get_latest_block(request) .await @@ -86,7 +91,8 @@ impl CosmosChannel { let tx_bytes = payload.to_vec(); let mut client = TxServiceClient::new(self.channel.clone()); #[allow(deprecated)] - let sim_req = tonic::Request::new(SimulateRequest { tx: None, tx_bytes }); + let mut sim_req = tonic::Request::new(SimulateRequest { tx: None, tx_bytes }); + sim_req.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); let response = client .simulate(sim_req) .await @@ -100,7 +106,8 @@ impl CosmosChannel { async fn account_query(&self, account: String) -> ChainResult { let address = account.clone(); let mut client = QueryAccountClient::new(self.channel.clone()); - let request = tonic::Request::new(QueryAccountRequest { address }); + let mut request = tonic::Request::new(QueryAccountRequest { address }); + request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); let response = client .account(request) .await @@ -114,9 +121,10 @@ impl CosmosChannel { account: String, ) -> ChainResult { let address = account.clone(); - let request = tonic::Request::new( + let mut request = tonic::Request::new( injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest { address }, ); + request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); // Borrowed from the logic of `QueryAccountClient` in `cosmrs`, but using injective types. let mut grpc_client = tonic::client::Grpc::new(self.channel.clone()); @@ -152,7 +160,8 @@ impl CosmosChannel { let denom = denom.clone(); let mut client = QueryBalanceClient::new(self.channel.clone()); - let balance_request = tonic::Request::new(QueryBalanceRequest { address, denom }); + let mut balance_request = tonic::Request::new(QueryBalanceRequest { address, denom }); + balance_request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); let response = client .balance(balance_request) .await @@ -173,6 +182,7 @@ impl CosmosChannel { address: to, query_data: payload.clone(), }); + request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); if let Some(block_height) = block_height { request .metadata_mut() @@ -189,7 +199,8 @@ impl CosmosChannel { let to = contract_address.clone(); let mut client = WasmQueryClient::new(self.channel.clone()); - let request = tonic::Request::new(QueryContractInfoRequest { address: to }); + let mut request = tonic::Request::new(QueryContractInfoRequest { address: to }); + request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); let response = client .contract_info(request) @@ -228,7 +239,8 @@ impl CosmosChannel { impl BlockNumberGetter for CosmosChannel { async fn get_block_number(&self) -> ChainResult { let mut client = ServiceClient::new(self.channel.clone()); - let request = tonic::Request::new(GetLatestBlockRequest {}); + let mut request = tonic::Request::new(GetLatestBlockRequest {}); + request.set_timeout(Duration::from_secs(REQUEST_TIMEOUT)); let response = client .get_latest_block(request) @@ -317,11 +329,10 @@ impl WasmGrpcProvider { let metrics_config = PrometheusConfig::from_url(&url, ClientConnectionType::Grpc, chain.clone()); Endpoint::new(url.to_string()) - .map(|e| { - let metrics_channel = - MetricsChannel::new(e.connect_lazy(), metrics.clone(), metrics_config); - CosmosChannel::new(metrics_channel, url) - }) + .map(|e| e.timeout(Duration::from_secs(REQUEST_TIMEOUT))) + .map(|e| e.connect_timeout(Duration::from_secs(REQUEST_TIMEOUT))) + .map(|e| MetricsChannel::new(e.connect_lazy(), metrics.clone(), metrics_config)) + .map(|m| CosmosChannel::new(m, url)) .map_err(Into::::into) }) .collect(); diff --git a/rust/main/chains/hyperlane-cosmos/src/providers/grpc/tests.rs b/rust/main/chains/hyperlane-cosmos/src/providers/grpc/tests.rs index a3fa5cf8014..5c7926ee687 100644 --- a/rust/main/chains/hyperlane-cosmos/src/providers/grpc/tests.rs +++ b/rust/main/chains/hyperlane-cosmos/src/providers/grpc/tests.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use hyperlane_metric::prometheus_metric::PrometheusClientMetrics; use url::Url; -use hyperlane_core::config::OperationBatchConfig; +use hyperlane_core::config::OpSubmissionConfig; use hyperlane_core::{ContractLocator, HyperlaneDomain, KnownHyperlaneDomain, NativeToken}; use crate::grpc::{WasmGrpcProvider, WasmProvider}; @@ -61,9 +61,10 @@ fn provider(address: &str) -> WasmGrpcProvider { "untrn".to_owned(), RawCosmosAmount::new("untrn".to_owned(), "0".to_owned()), 32, - OperationBatchConfig { + OpSubmissionConfig { batch_contract_address: None, max_batch_size: 1, + ..Default::default() }, NativeToken { decimals: 6, diff --git a/rust/main/chains/hyperlane-cosmos/src/trait_builder.rs b/rust/main/chains/hyperlane-cosmos/src/trait_builder.rs index 139fcd91eb7..dc0cd5528f0 100644 --- a/rust/main/chains/hyperlane-cosmos/src/trait_builder.rs +++ b/rust/main/chains/hyperlane-cosmos/src/trait_builder.rs @@ -4,7 +4,7 @@ use derive_new::new; use url::Url; use hyperlane_core::{ - config::OperationBatchConfig, ChainCommunicationError, FixedPointNumber, NativeToken, + config::OpSubmissionConfig, ChainCommunicationError, FixedPointNumber, NativeToken, }; /// Cosmos connection configuration @@ -29,7 +29,7 @@ pub struct ConnectionConf { /// bech32 with the appropriate length. contract_address_bytes: usize, /// Operation batching configuration - pub operation_batch: OperationBatchConfig, + pub op_submission_config: OpSubmissionConfig, /// Native Token native_token: NativeToken, } @@ -133,7 +133,7 @@ impl ConnectionConf { canonical_asset: String, minimum_gas_price: RawCosmosAmount, contract_address_bytes: usize, - operation_batch: OperationBatchConfig, + op_submission_config: OpSubmissionConfig, native_token: NativeToken, ) -> Self { Self { @@ -144,7 +144,7 @@ impl ConnectionConf { canonical_asset, gas_price: minimum_gas_price, contract_address_bytes, - operation_batch, + op_submission_config, native_token, } } diff --git a/rust/main/chains/hyperlane-ethereum/Cargo.toml b/rust/main/chains/hyperlane-ethereum/Cargo.toml index a5dccaa9658..b1b4fbd01fd 100644 --- a/rust/main/chains/hyperlane-ethereum/Cargo.toml +++ b/rust/main/chains/hyperlane-ethereum/Cargo.toml @@ -10,6 +10,7 @@ version.workspace = true [dependencies] # Main block async-trait.workspace = true +dashmap.workspace = true derive-new.workspace = true ethers-contract.workspace = true ethers-core.workspace = true @@ -42,4 +43,4 @@ hyperlane-core = { path = "../../hyperlane-core", features = ["test-utils"] } [features] default = [] -test-utils = [] \ No newline at end of file +test-utils = [] diff --git a/rust/main/chains/hyperlane-ethereum/src/config.rs b/rust/main/chains/hyperlane-ethereum/src/config.rs index e1375e790d8..5623896edb7 100644 --- a/rust/main/chains/hyperlane-ethereum/src/config.rs +++ b/rust/main/chains/hyperlane-ethereum/src/config.rs @@ -1,7 +1,7 @@ use ethers::providers::Middleware; use ethers_core::types::{BlockId, BlockNumber}; use hyperlane_core::{ - config::OperationBatchConfig, ChainCommunicationError, ChainResult, ReorgPeriod, U256, + config::OpSubmissionConfig, ChainCommunicationError, ChainResult, ReorgPeriod, U256, }; use url::Url; @@ -38,7 +38,7 @@ pub struct ConnectionConf { /// Transaction overrides to use when sending transactions. pub transaction_overrides: TransactionOverrides, /// Operation batching configuration - pub operation_batch: OperationBatchConfig, + pub op_submission_config: OpSubmissionConfig, } /// Ethereum transaction overrides. @@ -55,6 +55,26 @@ pub struct TransactionOverrides { pub max_fee_per_gas: Option, /// Max priority fee per gas to use for EIP-1559 transactions. pub max_priority_fee_per_gas: Option, + + /// Min gas price to use for Legacy transactions, in wei. + pub min_gas_price: Option, + /// Min fee per gas to use for EIP-1559 transactions. + pub min_fee_per_gas: Option, + /// Min priority fee per gas to use for EIP-1559 transactions. + pub min_priority_fee_per_gas: Option, + + /// Gas limit multiplier denominator to use for transactions, eg 110 + pub gas_limit_multiplier_denominator: Option, + /// Gas limit multiplier numerator to use for transactions, eg 100 + pub gas_limit_multiplier_numerator: Option, + + /// Gas price multiplier denominator to use for transactions, eg 110 + pub gas_price_multiplier_denominator: Option, + /// Gas price multiplier numerator to use for transactions, eg 100 + pub gas_price_multiplier_numerator: Option, + + /// Gas price cap, in wei. + pub gas_price_cap: Option, } /// Ethereum reorg period diff --git a/rust/main/chains/hyperlane-ethereum/src/contracts/mailbox.rs b/rust/main/chains/hyperlane-ethereum/src/contracts/mailbox.rs index 13e18740fc0..6d896ad0c20 100644 --- a/rust/main/chains/hyperlane-ethereum/src/contracts/mailbox.rs +++ b/rust/main/chains/hyperlane-ethereum/src/contracts/mailbox.rs @@ -7,15 +7,17 @@ use std::sync::Arc; use async_trait::async_trait; use derive_new::new; -use ethers::abi::AbiEncode; use ethers::prelude::Middleware; +use ethers::types::transaction::eip2718::TypedTransaction; +use ethers::types::{Block, H256 as TxHash}; use ethers_contract::builders::ContractCall; use ethers_contract::{Multicall, MulticallResult}; use ethers_core::utils::WEI_IN_ETHER; use futures_util::future::join_all; use hyperlane_core::rpc_clients::call_and_retry_indefinitely; use hyperlane_core::{BatchResult, QueueOperation, ReorgPeriod, H512}; -use itertools::Itertools; +use tokio::join; +use tokio::sync::Mutex; use tracing::instrument; use hyperlane_core::{ @@ -27,11 +29,11 @@ use hyperlane_core::{ use crate::error::HyperlaneEthereumError; use crate::interfaces::arbitrum_node_interface::ArbitrumNodeInterface; -use crate::interfaces::i_mailbox::{ - IMailbox as EthereumMailboxInternal, ProcessCall, IMAILBOX_ABI, -}; +use crate::interfaces::i_mailbox::{IMailbox as EthereumMailboxInternal, IMAILBOX_ABI}; use crate::interfaces::mailbox::DispatchFilter; -use crate::tx::{call_with_reorg_period, fill_tx_gas_params, report_tx}; +use crate::tx::{ + call_with_reorg_period, estimate_eip1559_fees, fill_tx_gas_params, report_tx, Eip1559Fee, +}; use crate::{ BuildableWithProvider, ConnectionConf, EthereumProvider, EthereumReorgPeriod, TransactionOverrides, @@ -278,6 +280,14 @@ where provider: Arc, arbitrum_node_interface: Option>>, conn: ConnectionConf, + cache: Arc>, +} + +#[derive(Debug, Default)] +pub struct EthereumMailboxCache { + pub is_contract: HashMap, + pub latest_block: Option>, + pub eip1559_fee: Option, } impl EthereumMailbox @@ -307,16 +317,15 @@ where provider, arbitrum_node_interface, conn: conn.clone(), + cache: Default::default(), } } - /// Returns a ContractCall that processes the provided message. - async fn process_contract_call( + fn contract_call( &self, message: &HyperlaneMessage, metadata: &[u8], tx_gas_estimate: Option, - with_gas_estimate_buffer: bool, ) -> ChainResult> { let mut tx = self.contract.process( metadata.to_vec().into(), @@ -325,6 +334,18 @@ where if let Some(gas_estimate) = tx_gas_estimate { tx = tx.gas(gas_estimate); } + Ok(tx) + } + + /// Returns a ContractCall that processes the provided message. + async fn process_contract_call( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + tx_gas_estimate: Option, + with_gas_estimate_buffer: bool, + ) -> ChainResult> { + let tx = self.contract_call(message, metadata, tx_gas_estimate)?; fill_tx_gas_params( tx, @@ -332,6 +353,7 @@ where &self.conn.transaction_overrides.clone(), &self.domain, with_gas_estimate_buffer, + self.cache.clone(), ) .await } @@ -341,35 +363,22 @@ where multicall: &mut Multicall, contract_calls: Vec>, ) -> ChainResult> { - let batch = multicall::batch::<_, ()>(multicall, contract_calls.clone()).await?; + let batch = multicall::batch::<_, ()>(multicall, contract_calls.clone()); let call_results = batch.call().await?; - let failed_calls = contract_calls - .iter() - .zip(call_results.iter()) - .enumerate() - .filter_map( - |(index, (_, result))| { - if !result.success { - Some(index) - } else { - None - } - }, - ) - .collect_vec(); - - // only send a batch if there are at least two successful calls - let call_count = contract_calls.len(); - let successful_calls = call_count - failed_calls.len(); - if successful_calls >= 2 { - Ok(BatchSimulation::new( - Some(self.submittable_batch(batch)), - failed_calls, - )) - } else { - Ok(BatchSimulation::failed(call_count)) + let (successful, failed) = multicall::filter_failed(contract_calls, call_results); + + if successful.is_empty() { + return Ok(BatchSimulation::failed(failed.len())); } + + let successful_batch = multicall::batch::<_, ()>(multicall, successful.clone()); + + Ok(BatchSimulation::new( + Some(self.submittable_batch(successful_batch)), + successful, + failed, + )) } fn submittable_batch( @@ -383,11 +392,56 @@ where domain: self.domain.clone(), } } + + async fn _submit_multicall( + &self, + multicall: &mut Multicall, + contract_calls: Vec>, + cache: Arc>, + ) -> ChainResult { + let batch = multicall::batch::<_, ()>(multicall, contract_calls.clone()); + let call_with_gas_overrides = fill_tx_gas_params( + batch, + self.provider.clone(), + &self.conn.transaction_overrides.clone(), + &self.domain, + true, + cache, + ) + .await?; + let outcome = report_tx(call_with_gas_overrides).await?; + Ok(BatchResult::new(Some(outcome.into()), vec![])) + } + + async fn _simulate_and_submit_batch( + &self, + multicall: &mut Multicall, + contract_calls: Vec>, + cache: Arc>, + ) -> ChainResult { + let batch_simulation = self.simulate_batch(multicall, contract_calls).await?; + batch_simulation.try_submit(cache).await + } + + async fn refresh_block_and_fee_cache(&self, tx: &TypedTransaction) { + let Some((eip1559_fee, latest_block)) = + estimate_eip1559_fees(self.provider.clone(), None, &self.domain, tx) + .await + .ok() + else { + return; + }; + let mut cache = self.cache.lock().await; + cache.latest_block = Some(latest_block); + cache.eip1559_fee = Some(eip1559_fee); + } } #[derive(new)] pub struct BatchSimulation { pub call: Option>, + /// Successful individual calls + pub successful: Vec>, /// Indexes of excluded calls in the batch (because they either failed the simulation /// or they were the only successful call) pub excluded_call_indexes: Vec, @@ -395,14 +449,19 @@ pub struct BatchSimulation { impl BatchSimulation { pub fn failed(ops_count: usize) -> Self { - Self::new(None, (0..ops_count).collect()) + Self::new(None, vec![], (0..ops_count).collect()) } } impl BatchSimulation { - pub async fn try_submit(self) -> ChainResult { - if let Some(submittable_batch) = self.call { - let batch_outcome = submittable_batch.submit().await?; + pub async fn try_submit( + self, + cache: Arc>, + ) -> ChainResult { + if let Some(mut submittable_batch) = self.call { + let estimated = multicall::estimate(submittable_batch.call, self.successful).await?; + submittable_batch.call = estimated; + let batch_outcome = submittable_batch.submit(cache).await?; Ok(BatchResult::new( Some(batch_outcome), self.excluded_call_indexes, @@ -421,13 +480,14 @@ pub struct SubmittableBatch { } impl SubmittableBatch { - pub async fn submit(self) -> ChainResult { + pub async fn submit(self, cache: Arc>) -> ChainResult { let call_with_gas_overrides = fill_tx_gas_params( self.call, self.provider, &self.transaction_overrides, &self.domain, true, + cache, ) .await?; let outcome = report_tx(call_with_gas_overrides).await?; @@ -507,37 +567,65 @@ where Ok(receipt.into()) } + /// Returns true if the mailbox supports batching + fn supports_batching(&self) -> bool { + true + } + #[instrument(skip(self, ops), fields(size=%ops.len()))] - async fn try_process_batch<'a>( - &self, - ops: Vec<&'a QueueOperation>, - ) -> ChainResult { + async fn process_batch<'a>(&self, ops: Vec<&'a QueueOperation>) -> ChainResult { let messages = ops .iter() .map(|op| op.try_batch()) .collect::>>>()?; - let mut multicall = build_multicall(self.provider.clone(), &self.conn, self.domain.clone()) - .await - .map_err(|e| HyperlaneEthereumError::MulticallError(e.to_string()))?; - let contract_call_futures = messages + let mut multicall = build_multicall( + self.provider.clone(), + &self.conn, + self.domain.clone(), + self.cache.clone(), + ) + .await + .map_err(|e| HyperlaneEthereumError::MulticallError(e.to_string()))?; + + let contract_calls = messages .iter() - .map(|batch_item| async { - self.process_contract_call( + .map(|batch_item| { + self.contract_call( &batch_item.data, &batch_item.submission_data.metadata, Some(batch_item.submission_data.gas_limit), - true, ) - .await }) - .collect::>(); - let contract_calls = join_all(contract_call_futures) + .collect::>>()?; + + let simulate_future = self.simulate_batch(&mut multicall, contract_calls.clone()); + let refresh_cache_future = async { + if let Some(contract_call) = contract_calls.first() { + self.refresh_block_and_fee_cache(&contract_call.tx).await + } + }; + + let (simulate_result, _) = join!(simulate_future, refresh_cache_future); + let mut simulation = simulate_result?; + + let filled_tx_params_futures = simulation.successful.iter().map(|tx| { + fill_tx_gas_params( + tx.clone(), + self.provider.clone(), + &self.conn.transaction_overrides, + &self.domain, + false, + self.cache.clone(), + ) + }); + let contract_calls = join_all(filled_tx_params_futures) .await .into_iter() .collect::>>()?; - let batch_simulation = self.simulate_batch(&mut multicall, contract_calls).await?; - batch_simulation.try_submit().await + simulation.successful = contract_calls; + + simulation.try_submit(self.cache.clone()).await } #[instrument(skip(self), fields(msg=%message, metadata=%bytes_to_hex(metadata)))] @@ -593,13 +681,12 @@ where }) } - fn process_calldata(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Vec { - let process_call = ProcessCall { - message: RawHyperlaneMessage::from(message).to_vec().into(), - metadata: metadata.to_vec().into(), - }; - - AbiEncode::encode(process_call) + async fn process_calldata( + &self, + _message: &HyperlaneMessage, + _metadata: &[u8], + ) -> ChainResult> { + todo!() } } @@ -621,7 +708,7 @@ mod test { providers::{MockProvider, Provider}, types::{Block, Transaction, U256 as EthersU256}, }; - + use ethers_core::types::FeeHistory; use hyperlane_core::{ ContractLocator, HyperlaneDomain, HyperlaneMessage, KnownHyperlaneDomain, Mailbox, TxCostEstimate, H160, H256, U256, @@ -642,7 +729,7 @@ mod test { url: "http://127.0.0.1:8545".parse().unwrap(), }, transaction_overrides: Default::default(), - operation_batch: Default::default(), + op_submission_config: Default::default(), }; let mailbox = EthereumMailbox::new( @@ -683,14 +770,28 @@ mod test { EthersU256::from(ethers::utils::parse_units("15", "gwei").unwrap()).into(); mock_provider.push(gas_price).unwrap(); - // RPC 4: eth_estimateGas to the ArbitrumNodeInterface's estimateRetryableTicket function by process_estimate_costs + // RPC 6: eth_estimateGas to the ArbitrumNodeInterface's estimateRetryableTicket function by process_estimate_costs let l2_gas_limit = U256::from(200000); // 200k gas mock_provider.push(l2_gas_limit).unwrap(); + let fee_history = FeeHistory { + oldest_block: ethers::types::U256::zero(), + base_fee_per_gas: vec![], + gas_used_ratio: vec![], + reward: vec![vec![]], + }; + + // RPC 5: eth_feeHistory from the estimate_eip1559_fees_default + mock_provider.push(fee_history).unwrap(); + let latest_block: Block = Block { gas_limit: ethers::types::U256::MAX, ..Block::::default() }; + + // RPC 4: eth_getBlockByNumber from the estimate_eip1559_fees_default + mock_provider.push(latest_block.clone()).unwrap(); + // RPC 3: eth_getBlockByNumber from the fill_tx_gas_params call in process_contract_call // to get the latest block gas limit and for eip 1559 fee estimation mock_provider.push(latest_block).unwrap(); @@ -727,17 +828,31 @@ mod test { // order, so we start with the final RPCs and work toward the first // RPCs - // RPC 4: eth_gasPrice by process_estimate_costs + // RPC 6: eth_gasPrice by process_estimate_costs // Return 15 gwei let gas_price: U256 = EthersU256::from(ethers::utils::parse_units("15", "gwei").unwrap()).into(); mock_provider.push(gas_price).unwrap(); + let fee_history = FeeHistory { + oldest_block: ethers::types::U256::zero(), + base_fee_per_gas: vec![], + gas_used_ratio: vec![], + reward: vec![vec![]], + }; + + // RPC 5: eth_feeHistory from the estimate_eip1559_fees_default + mock_provider.push(fee_history).unwrap(); + let latest_block_gas_limit = U256::from(12345u32); let latest_block: Block = Block { gas_limit: latest_block_gas_limit.into(), ..Block::::default() }; + + // RPC 4: eth_getBlockByNumber from the estimate_eip1559_fees_default + mock_provider.push(latest_block.clone()).unwrap(); + // RPC 3: eth_getBlockByNumber from the fill_tx_gas_params call in process_contract_call // to get the latest block gas limit and for eip 1559 fee estimation mock_provider.push(latest_block).unwrap(); diff --git a/rust/main/chains/hyperlane-ethereum/src/contracts/multicall.rs b/rust/main/chains/hyperlane-ethereum/src/contracts/multicall.rs index 68d5703e7d3..92ef7fda4d7 100644 --- a/rust/main/chains/hyperlane-ethereum/src/contracts/multicall.rs +++ b/rust/main/chains/hyperlane-ethereum/src/contracts/multicall.rs @@ -2,13 +2,20 @@ use std::sync::Arc; use ethers::{abi::Detokenize, providers::Middleware}; use ethers_contract::{builders::ContractCall, Multicall, MulticallResult, MulticallVersion}; +use itertools::{Either, Itertools}; +use tokio::sync::Mutex; +use tracing::warn; + use hyperlane_core::{ utils::hex_or_base58_to_h256, ChainResult, HyperlaneDomain, HyperlaneProvider, U256, }; -use tracing::warn; use crate::{ConnectionConf, EthereumProvider}; +use super::EthereumMailboxCache; + +const MULTICALL_GAS_LIMIT_MULTIPLIER_DENOMINATOR: u64 = 80; +const MULTICALL_GAS_LIMIT_MULTIPLIER_NUMERATOR: u64 = 100; const ALLOW_BATCH_FAILURES: bool = true; /// Conservative estimate picked by subtracting the gas used by individual calls from the total cost of `aggregate3` @@ -21,13 +28,27 @@ pub async fn build_multicall( provider: Arc, conn: &ConnectionConf, domain: HyperlaneDomain, + cache: Arc>, ) -> eyre::Result> { let address = conn - .operation_batch + .op_submission_config .batch_contract_address .unwrap_or(hex_or_base58_to_h256("0xcA11bde05977b3631167028862bE2a173976CA11").unwrap()); - let ethereum_provider = EthereumProvider::new(provider.clone(), domain); - if !ethereum_provider.is_contract(&address).await? { + let is_contract_cache = { + let cache = cache.lock().await; + cache.is_contract.get(&address).cloned() + }; + let is_contract = match is_contract_cache { + Some(is_contract) => is_contract, + None => { + let ethereum_provider = EthereumProvider::new(provider.clone(), domain); + let is_contract = ethereum_provider.is_contract(&address).await?; + cache.lock().await.is_contract.insert(address, is_contract); + is_contract + } + }; + + if !is_contract { return Err(eyre::eyre!("Multicall contract not found at address")); } let multicall = match Multicall::new(provider.clone(), Some(address.into())).await { @@ -43,17 +64,49 @@ pub async fn build_multicall( Ok(multicall) } -pub async fn batch( +pub fn batch( multicall: &mut Multicall, calls: Vec>, -) -> ChainResult>> +) -> ContractCall> where - M: Middleware + 'static, + M: Middleware, D: Detokenize, { // clear any calls that were in the multicall beforehand multicall.clear_calls(); + calls.into_iter().for_each(|call| { + multicall.add_call(call, ALLOW_BATCH_FAILURES); + }); + + multicall.as_aggregate_3_value() +} + +pub fn filter_failed( + calls: Vec>, + results: Vec, +) -> (Vec>, Vec) { + calls + .into_iter() + .zip(results) + .enumerate() + .partition_map(|(index, (call, result))| { + if result.success { + Either::Left(call) + } else { + Either::Right(index) + } + }) +} + +pub async fn estimate( + batch: ContractCall, + calls: Vec>, +) -> ChainResult> +where + M: Middleware + 'static, + D: Detokenize, +{ let mut individual_estimates_sum = Some(U256::zero()); let overhead_per_call: U256 = MULTICALL_OVERHEAD_PER_CALL.into(); @@ -68,15 +121,19 @@ where None } }; - multicall.add_call(call, ALLOW_BATCH_FAILURES); }); - let mut batch_call = multicall.as_aggregate_3_value(); - let mut gas_limit: U256 = batch_call.estimate_gas().await?.into(); + let mut gas_limit: U256 = batch.estimate_gas().await?.into(); // Use the max of the sum of individual estimates and the estimate for the entire batch if let Some(gas_sum) = individual_estimates_sum { gas_limit = gas_limit.max(gas_sum) } - batch_call = batch_call.gas(gas_limit); - Ok(batch_call) + + // in practice, even when the full batch lands, no more than 65% of the gas limit is used. + // this sets the limit lower, but still allows for some overhead, to make it more likely + // that the tx gets included (due to the lower gas limit) + let scaled_down_gas_limit = gas_limit * MULTICALL_GAS_LIMIT_MULTIPLIER_DENOMINATOR + / MULTICALL_GAS_LIMIT_MULTIPLIER_NUMERATOR; + + Ok(batch.gas(scaled_down_gas_limit)) } diff --git a/rust/main/chains/hyperlane-ethereum/src/contracts/validator_announce.rs b/rust/main/chains/hyperlane-ethereum/src/contracts/validator_announce.rs index 099a5c5caef..a51a255e439 100644 --- a/rust/main/chains/hyperlane-ethereum/src/contracts/validator_announce.rs +++ b/rust/main/chains/hyperlane-ethereum/src/contracts/validator_announce.rs @@ -98,6 +98,8 @@ where &self.conn.transaction_overrides, &self.domain, true, + // pass an empty value as the cache + Default::default(), ) .await } diff --git a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs index a252635aa91..5750bc7b150 100644 --- a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs +++ b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs @@ -98,7 +98,7 @@ where type Error = ProviderError; // TODO: Refactor to use `FallbackProvider::call` - #[instrument] + #[instrument(skip(self, params))] async fn request(&self, method: &str, params: T) -> Result where T: Debug + Serialize + Send + Sync, diff --git a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/trait_builder.rs b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/trait_builder.rs index 7cd78eb5907..b135b382249 100644 --- a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/trait_builder.rs +++ b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/trait_builder.rs @@ -1,9 +1,10 @@ use std::fmt::Debug; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use std::time::Duration; use async_trait::async_trait; +use dashmap::DashMap; use ethers::middleware::gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}; use ethers::middleware::gas_oracle::{ GasCategory, GasOracle, GasOracleMiddleware, Polygon, ProviderOracle, @@ -32,6 +33,7 @@ use hyperlane_metric::prometheus_metric::{ use tracing::instrument; use crate::signer::Signers; +use crate::tx::PENDING_TX_TIMEOUT_SECS; use crate::{ConnectionConf, EthereumFallbackProvider, RetryingProvider, RpcConnectionConf}; // This should be whatever the prometheus scrape interval is @@ -282,11 +284,14 @@ fn wrap_with_gas_escalator(provider: M) -> GasEscalatorMiddleware where M: Middleware + 'static, { - // Increase the gas price by 12.5% every 90 seconds - // (These are the default values from ethers doc comments) - const COEFFICIENT: f64 = 1.125; - const EVERY_SECS: u64 = 90u64; - // a 3k gwei limit is chosen to account for `treasure` chain, where the highest gas price observed is 1.2k gwei + // Increase the gas price by 25% every 90 seconds + const COEFFICIENT: f64 = 1.25; + + // escalating creates a new tx hash, and the submitter tracks each tx hash for at most + // `PENDING_TX_TIMEOUT_SECS`. So the escalator will send a new tx when the initial + // tx hash stops being tracked. + const EVERY_SECS: u64 = PENDING_TX_TIMEOUT_SECS; + // a 50k gwei limit is chosen to account for `treasure` chain, where the highest gas price observed is 1.2k gwei const MAX_GAS_PRICE: u128 = 3_000 * 10u128.pow(9); let escalator = GeometricGasPrice::new(COEFFICIENT, EVERY_SECS, MAX_GAS_PRICE.into()); // Check the status of sent txs every eth block or so. The alternative is to subscribe to new blocks and check then, @@ -295,7 +300,27 @@ where GasEscalatorMiddleware::new(provider, escalator, FREQUENCY) } +/// Builds a new HTTP provider with the given URL. fn build_http_provider(url: Url) -> ChainResult { + let client = get_reqwest_client(&url)?; + Ok(Http::new_with_client(url, client)) +} + +/// Gets a cached reqwest client for the given URL, or builds a new one if it doesn't exist. +fn get_reqwest_client(url: &Url) -> ChainResult { + let client_cache = get_reqwest_client_cache(); + if let Some(client) = client_cache.get(url) { + return Ok(client.clone()); + } + let client = build_new_reqwest_client(url.clone())?; + client_cache.insert(url.clone(), client.clone()); + Ok(client) +} + +/// Builds a new reqwest client with the given URL. +/// Generally `get_reqwest_client` should be used instead of this function, +/// as it caches the client for reuse. +fn build_new_reqwest_client(url: Url) -> ChainResult { let mut queries_to_keep = vec![]; let mut headers = reqwest::header::HeaderMap::new(); @@ -325,11 +350,20 @@ fn build_http_provider(url: Url) -> ChainResult { .clear() .extend_pairs(queries_to_keep); - let http_client = Client::builder() + let client = Client::builder() .timeout(HTTP_CLIENT_TIMEOUT) .default_headers(headers) .build() .map_err(EthereumProviderConnectionError::from)?; - Ok(Http::new_with_client(url, http_client)) + Ok(client) +} + +/// A cache for reqwest clients, indexed by URL. +/// Generally creating a new Reqwest client is expensive due to some DNS +/// resolutions, so we cache them for reuse. +static REQWEST_CLIENT_CACHE: OnceLock> = OnceLock::new(); + +fn get_reqwest_client_cache() -> &'static DashMap { + REQWEST_CLIENT_CACHE.get_or_init(DashMap::new) } diff --git a/rust/main/chains/hyperlane-ethereum/src/tx.rs b/rust/main/chains/hyperlane-ethereum/src/tx.rs index 830d75df45a..ce17acaf23f 100644 --- a/rust/main/chains/hyperlane-ethereum/src/tx.rs +++ b/rust/main/chains/hyperlane-ethereum/src/tx.rs @@ -18,28 +18,36 @@ use ethers_core::{ EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE, }, }; +use tokio::sync::Mutex; +use tokio::try_join; +use tracing::{debug, error, info, instrument, warn}; + use hyperlane_core::{ - utils::bytes_to_hex, ChainCommunicationError, ChainResult, HyperlaneDomain, ReorgPeriod, H256, - U256, + ChainCommunicationError, ChainResult, HyperlaneDomain, ReorgPeriod, H256, U256, }; -use tracing::{debug, error, info, warn}; -use crate::{EthereumReorgPeriod, Middleware, TransactionOverrides}; +use crate::{EthereumMailboxCache, EthereumReorgPeriod, Middleware, TransactionOverrides}; + +/// An amount of gas to add to the estimated gas limit +pub const GAS_LIMIT_BUFFER: u32 = 75_000; + +// A multiplier to apply to the estimated gas limit, i.e. 10%. +pub const DEFAULT_GAS_LIMIT_MULTIPLIER_NUMERATOR: u32 = 11; +pub const DEFAULT_GAS_LIMIT_MULTIPLIER_DENOMINATOR: u32 = 10; -/// An amount of gas to add to the estimated gas -pub const GAS_ESTIMATE_BUFFER: u32 = 75_000; +// A multiplier to apply to the estimated gas price, i.e. 10%. +pub const DEFAULT_GAS_PRICE_MULTIPLIER_NUMERATOR: u32 = 11; +pub const DEFAULT_GAS_PRICE_MULTIPLIER_DENOMINATOR: u32 = 10; -// A multiplier to apply to the estimated gas, i.e. 10%. -pub const GAS_ESTIMATE_MULTIPLIER_NUMERATOR: u32 = 11; -pub const GAS_ESTIMATE_MULTIPLIER_DENOMINATOR: u32 = 10; +pub const PENDING_TX_TIMEOUT_SECS: u64 = 90; pub fn apply_gas_estimate_buffer(gas: U256, domain: &HyperlaneDomain) -> ChainResult { // Arbitrum Nitro chains use 2d fees are especially prone to costs increasing // by the time the transaction lands on chain, requiring a higher gas limit. // In this case, we apply a multiplier to the gas estimate. let gas = if domain.is_arbitrum_nitro() { - gas.saturating_mul(GAS_ESTIMATE_MULTIPLIER_NUMERATOR.into()) - .checked_div(GAS_ESTIMATE_MULTIPLIER_DENOMINATOR.into()) + gas.saturating_mul(DEFAULT_GAS_LIMIT_MULTIPLIER_NUMERATOR.into()) + .checked_div(DEFAULT_GAS_LIMIT_MULTIPLIER_DENOMINATOR.into()) .ok_or_else(|| { ChainCommunicationError::from_other_str("Gas estimate buffer divide by zero") })? @@ -48,7 +56,30 @@ pub fn apply_gas_estimate_buffer(gas: U256, domain: &HyperlaneDomain) -> ChainRe }; // Always add a flat buffer - Ok(gas.saturating_add(GAS_ESTIMATE_BUFFER.into())) + Ok(gas.saturating_add(GAS_LIMIT_BUFFER.into())) +} + +pub fn apply_gas_price_multiplier( + gas_price: EthersU256, + transaction_overrides: &TransactionOverrides, +) -> EthersU256 { + let numerator: EthersU256 = transaction_overrides + .gas_price_multiplier_numerator + .map(Into::into) + .unwrap_or(DEFAULT_GAS_PRICE_MULTIPLIER_NUMERATOR.into()); + let denominator: EthersU256 = transaction_overrides + .gas_price_multiplier_denominator + .map(Into::into) + .unwrap_or(DEFAULT_GAS_PRICE_MULTIPLIER_DENOMINATOR.into()); + let multiplied = gas_price.saturating_mul(numerator).checked_div(denominator); + let Some(multiplied) = multiplied else { + warn!( + ?gas_price, + "Gas price multiplier divide by zero, using original gas price" + ); + return gas_price; + }; + multiplied } const PENDING_TRANSACTION_POLLING_INTERVAL: Duration = Duration::from_secs(2); @@ -60,19 +91,13 @@ where M: Middleware + 'static, D: Detokenize, { - let data = tx - .tx - .data() - .map(|b| bytes_to_hex(b)) - .unwrap_or_else(|| "None".into()); - let to = tx .tx .to() .cloned() .unwrap_or_else(|| NameOrAddress::Address(Default::default())); - info!(?to, %data, tx=?tx.tx, "Dispatching transaction"); + info!(?to, from=?tx.tx.from(), gas_limit=?tx.tx.gas(), gas_price=?tx.tx.gas_price(), nonce=?tx.tx.nonce(), "Dispatching transaction"); let dispatch_fut = tx.send(); let dispatched = dispatch_fut .await? @@ -80,6 +105,7 @@ where track_pending_tx(dispatched).await } +#[instrument(skip(pending_tx))] pub(crate) async fn track_pending_tx( pending_tx: PendingTransaction<'_, P>, ) -> ChainResult { @@ -87,7 +113,7 @@ pub(crate) async fn track_pending_tx( info!(?tx_hash, "Dispatched tx"); - match tokio::time::timeout(Duration::from_secs(150), pending_tx).await { + match tokio::time::timeout(Duration::from_secs(PENDING_TX_TIMEOUT_SECS), pending_tx).await { // all good Ok(Ok(Some(receipt))) => { info!(?tx_hash, "confirmed transaction"); @@ -116,6 +142,7 @@ pub(crate) async fn fill_tx_gas_params( transaction_overrides: &TransactionOverrides, domain: &HyperlaneDomain, with_gas_limit_overrides: bool, + cache: Arc>, ) -> ChainResult> where M: Middleware + 'static, @@ -134,13 +161,20 @@ where } } let gas_limit = estimated_gas_limit; + let (cached_latest_block, cached_eip1559_fee) = { + let cache = cache.lock().await; + (cache.latest_block.clone(), cache.eip1559_fee) + }; // Cap the gas limit to the block gas limit - let latest_block = provider - .get_block(BlockNumber::Latest) - .await - .map_err(ChainCommunicationError::from_other)? - .ok_or_else(|| ProviderError::CustomError("Latest block not found".into()))?; + let latest_block = match cached_latest_block { + Some(block) => block, + None => provider + .get_block(BlockNumber::Latest) + .await + .map_err(ChainCommunicationError::from_other)? + .ok_or_else(|| ProviderError::CustomError("Latest block not found".into()))?, + }; let block_gas_limit: U256 = latest_block.gas_limit.into(); let gas_limit = if gas_limit > block_gas_limit { warn!( @@ -159,11 +193,17 @@ where return Ok(tx.gas_price(gas_price).gas(gas_limit)); } - let Ok((base_fee, max_fee, max_priority_fee)) = - estimate_eip1559_fees(provider, None, &latest_block, domain, &tx.tx).await - else { - // Is not EIP 1559 chain - return Ok(tx.gas(gas_limit)); + let eip1559_fee_result = match cached_eip1559_fee { + Some(fee) => Ok((fee, latest_block)), + None => estimate_eip1559_fees(provider.clone(), None, domain, &tx.tx).await, + }; + let ((base_fee, max_fee, max_priority_fee), _) = match eip1559_fee_result { + Ok(result) => result, + Err(err) => { + warn!(?err, "Failed to estimate EIP-1559 fees"); + // Assume it's not an EIP 1559 chain + return Ok(apply_legacy_overrides(tx, transaction_overrides).gas(gas_limit)); + } }; // If the base fee is zero, just treat the chain as a non-EIP-1559 chain. @@ -172,18 +212,12 @@ where // fee lower than 3 gwei because of privileged transactions being included by block // producers that have a lower priority fee. if base_fee.is_zero() { - return Ok(tx.gas(gas_limit)); + return Ok(apply_legacy_overrides(tx, transaction_overrides).gas(gas_limit)); } // Apply overrides for EIP 1559 tx params if they exist. - let max_fee = transaction_overrides - .max_fee_per_gas - .map(Into::into) - .unwrap_or(max_fee); - let max_priority_fee = transaction_overrides - .max_priority_fee_per_gas - .map(Into::into) - .unwrap_or(max_priority_fee); + let (max_fee, max_priority_fee) = + apply_1559_multipliers_and_overrides(max_fee, max_priority_fee, transaction_overrides); // Is EIP 1559 chain let mut request = Eip1559TransactionRequest::new(); @@ -206,34 +240,119 @@ where Ok(eip_1559_tx.gas(gas_limit)) } +fn apply_legacy_overrides( + tx: ContractCall, + transaction_overrides: &TransactionOverrides, +) -> ContractCall +where + M: Middleware + 'static, + D: Detokenize, +{ + let gas_price = tx.tx.gas_price(); + // if no gas price was set in the tx, leave the tx as is and return early + let Some(mut gas_price) = gas_price else { + return tx; + }; + + let min_price_override = transaction_overrides + .min_gas_price + .map(Into::into) + .unwrap_or(0.into()); + gas_price = gas_price.max(min_price_override); + gas_price = apply_gas_price_cap(gas_price, transaction_overrides); + tx.gas_price(gas_price) +} + +fn apply_1559_multipliers_and_overrides( + max_fee: EthersU256, + max_priority_fee: EthersU256, + transaction_overrides: &TransactionOverrides, +) -> (EthersU256, EthersU256) { + let max_fee = transaction_overrides + .max_fee_per_gas + .map(Into::into) + .unwrap_or(max_fee); + let mut max_fee = apply_gas_price_multiplier(max_fee, transaction_overrides); + if let Some(min_fee) = transaction_overrides.min_fee_per_gas { + max_fee = max_fee.max(min_fee.into()); + } + + let max_priority_fee = transaction_overrides + .max_priority_fee_per_gas + .map(Into::into) + .unwrap_or(max_priority_fee); + let mut max_priority_fee = apply_gas_price_multiplier(max_priority_fee, transaction_overrides); + + if let Some(min_priority_fee) = transaction_overrides.min_priority_fee_per_gas { + max_priority_fee = max_priority_fee.max(min_priority_fee.into()); + } + max_fee = apply_gas_price_cap(max_fee, transaction_overrides); + (max_fee, max_priority_fee) +} + +fn apply_gas_price_cap( + gas_price: EthersU256, + transaction_overrides: &TransactionOverrides, +) -> EthersU256 { + if let Some(gas_price_cap) = transaction_overrides.gas_price_cap { + if gas_price > gas_price_cap.into() { + warn!( + ?gas_price, + ?gas_price_cap, + "Gas price for transaction is higher than the gas price cap. Capping it to the gas price cap." + ); + return gas_price_cap.into(); + } + } + gas_price +} + type FeeEstimator = fn(EthersU256, Vec>) -> (EthersU256, EthersU256); +pub type Eip1559Fee = ( + EthersU256, // base fee + EthersU256, // max fee + EthersU256, // max priority fee +); + +async fn latest_block(provider: Arc) -> ChainResult> +where + M: Middleware + 'static, +{ + let latest_block = provider + .get_block(BlockNumber::Latest) + .await + .map_err(ChainCommunicationError::from_other)? + .ok_or_else(|| ProviderError::CustomError("Latest block not found".into()))?; + Ok(latest_block) +} + /// Use this to estimate EIP 1559 fees with some chain-specific logic. -async fn estimate_eip1559_fees( +pub(crate) async fn estimate_eip1559_fees( provider: Arc, estimator: Option, - latest_block: &Block, domain: &HyperlaneDomain, tx: &TypedTransaction, -) -> ChainResult<(EthersU256, EthersU256, EthersU256)> +) -> ChainResult<(Eip1559Fee, Block)> where M: Middleware + 'static, { if domain.is_zksync_stack() { - estimate_eip1559_fees_zksync(provider, latest_block, tx).await + estimate_eip1559_fees_zksync(provider, tx).await } else { - estimate_eip1559_fees_default(provider, estimator, latest_block).await + estimate_eip1559_fees_default(provider, estimator).await } } async fn estimate_eip1559_fees_zksync( provider: Arc, - latest_block: &Block, tx: &TypedTransaction, -) -> ChainResult<(EthersU256, EthersU256, EthersU256)> +) -> ChainResult<(Eip1559Fee, Block)> where M: Middleware + 'static, { + let latest_block = latest_block(provider.clone()).await?; + let base_fee_per_gas = latest_block .base_fee_per_gas .ok_or_else(|| ProviderError::CustomError("EIP-1559 not activated".into()))?; @@ -242,7 +361,10 @@ where let max_fee_per_gas = response.max_fee_per_gas; let max_priority_fee_per_gas = response.max_priority_fee_per_gas; - Ok((base_fee_per_gas, max_fee_per_gas, max_priority_fee_per_gas)) + Ok(( + (base_fee_per_gas, max_fee_per_gas, max_priority_fee_per_gas), + latest_block, + )) } async fn zksync_estimate_fee( @@ -289,24 +411,29 @@ struct ZksyncEstimateFeeResponse { async fn estimate_eip1559_fees_default( provider: Arc, estimator: Option, - latest_block: &Block, -) -> ChainResult<(EthersU256, EthersU256, EthersU256)> +) -> ChainResult<((EthersU256, EthersU256, EthersU256), Block)> where M: Middleware + 'static, { + let latest_block = latest_block(provider.clone()); + + let fee_history = async { + provider + .fee_history( + EIP1559_FEE_ESTIMATION_PAST_BLOCKS, + BlockNumber::Latest, + &[EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await + .map_err(ChainCommunicationError::from_other) + }; + + let (latest_block, fee_history) = try_join!(latest_block, fee_history)?; + let base_fee_per_gas = latest_block .base_fee_per_gas .ok_or_else(|| ProviderError::CustomError("EIP-1559 not activated".into()))?; - let fee_history = provider - .fee_history( - EIP1559_FEE_ESTIMATION_PAST_BLOCKS, - BlockNumber::Latest, - &[EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], - ) - .await - .map_err(ChainCommunicationError::from_other)?; - // use the provided fee estimator function, or fallback to the default implementation. let (max_fee_per_gas, max_priority_fee_per_gas) = if let Some(es) = estimator { es(base_fee_per_gas, fee_history.reward) @@ -314,7 +441,10 @@ where eip1559_default_estimator(base_fee_per_gas, fee_history.reward) }; - Ok((base_fee_per_gas, max_fee_per_gas, max_priority_fee_per_gas)) + Ok(( + (base_fee_per_gas, max_fee_per_gas, max_priority_fee_per_gas), + latest_block, + )) } pub(crate) async fn call_with_reorg_period( diff --git a/rust/main/chains/hyperlane-fuel/src/mailbox.rs b/rust/main/chains/hyperlane-fuel/src/mailbox.rs index fbe951abef3..7c2342e5ae9 100644 --- a/rust/main/chains/hyperlane-fuel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-fuel/src/mailbox.rs @@ -212,7 +212,11 @@ impl Mailbox for FuelMailbox { }) } - fn process_calldata(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Vec { + async fn process_calldata( + &self, + _message: &HyperlaneMessage, + _metadata: &[u8], + ) -> ChainResult> { // Seems like this is not needed for Fuel, as it's only used in mocks todo!() } diff --git a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs index e9d959aa693..69020d2fced 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs @@ -544,7 +544,12 @@ impl Mailbox for SealevelMailbox { }) } - fn process_calldata(&self, _message: &HyperlaneMessage, _metadata: &[u8]) -> Vec { - todo!() + async fn process_calldata( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + ) -> ChainResult> { + let process_instruction = self.get_process_instruction(message, metadata).await?; + serde_json::to_vec(&process_instruction).map_err(Into::into) } } diff --git a/rust/main/chains/hyperlane-sealevel/src/provider.rs b/rust/main/chains/hyperlane-sealevel/src/provider.rs index 364e4b164be..2d365e35432 100644 --- a/rust/main/chains/hyperlane-sealevel/src/provider.rs +++ b/rust/main/chains/hyperlane-sealevel/src/provider.rs @@ -156,6 +156,8 @@ impl SealevelProviderForSubmitter for SealevelProvider { } /// Gets the estimated costs for a given instruction. + /// The return value is `Some(SealevelTxCostEstimate)` if the instruction was successfully simulated, + /// `None` if the simulation failed. async fn get_estimated_costs_for_instruction( &self, instruction: Instruction, @@ -245,7 +247,10 @@ impl SealevelProviderForSubmitter for SealevelProvider { /// expires. /// Standalone logic stolen from Solana's non-blocking client, /// decoupled from the sending of a transaction. - async fn wait_for_transaction_confirmation(&self, transaction: &Transaction) -> ChainResult { + async fn wait_for_transaction_confirmation( + &self, + transaction: &Transaction, + ) -> ChainResult<()> { let signature = transaction.get_signature(); const GET_STATUS_RETRIES: usize = usize::MAX; diff --git a/rust/main/chains/hyperlane-sealevel/src/trait_builder.rs b/rust/main/chains/hyperlane-sealevel/src/trait_builder.rs index dd518e438e4..15d945de54e 100644 --- a/rust/main/chains/hyperlane-sealevel/src/trait_builder.rs +++ b/rust/main/chains/hyperlane-sealevel/src/trait_builder.rs @@ -1,4 +1,4 @@ -use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError, NativeToken}; +use hyperlane_core::{config::OpSubmissionConfig, ChainCommunicationError, NativeToken}; use serde::Serialize; use url::Url; @@ -13,7 +13,7 @@ pub struct ConnectionConf { /// A list of urls to connect to pub urls: Vec, /// Operation batching configuration - pub operation_batch: OperationBatchConfig, + pub op_submission_config: OpSubmissionConfig, /// Native token and its denomination pub native_token: NativeToken, /// Priority fee oracle configuration diff --git a/rust/main/config/mainnet_config.json b/rust/main/config/mainnet_config.json index 6482dcf8786..9a5141f41b7 100644 --- a/rust/main/config/mainnet_config.json +++ b/rust/main/config/mainnet_config.json @@ -34,7 +34,7 @@ "interchainAccountIsm": "0xd766e7C7517f2d0D92754b2fe4aE7AdEf7bDEC3e", "interchainAccountRouter": "0x25C87e735021F72d8728438C2130b02E3141f2cb", "interchainGasPaymaster": "0x8F1E22d309baa69D398a03cc88E9b46037e988AA", - "interchainSecurityModule": "0x93906EE1E709836ABf858cD8cCD9f0dB47715169", + "interchainSecurityModule": "0xf036EFd98d4d4f559f39B7281360A4956484305f", "isTestnet": false, "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0x811808Dd29ba8B0FC6C0ec0b5537035E59745162", @@ -100,7 +100,7 @@ "interchainAccountIsm": "0x2A7574358Ec53522CE2452887661AB4c86F7d400", "interchainAccountRouter": "0x91874Dbed74925dFe6059B90385EEb90DdE0B2E6", "interchainGasPaymaster": "0x3b6044acd6767f017e99318AA6Ef93b7B06A5a22", - "interchainSecurityModule": "0xCC0955be246DAB4B9cb2c36258C8B927fE1e7955", + "interchainSecurityModule": "0x3486c63bc98d46Bf49Edc2e4d51822EBaC9893D2", "mailbox": "0x979Ca5202784112f4738403dBec5D0F3B9daabB9", "merkleTreeHook": "0x748040afB89B8FdBb992799808215419d36A0930", "name": "arbitrum", @@ -172,7 +172,7 @@ "interchainAccountIsm": "0x27a3233c05C1Df7c163123301D14bE9349E3Cb48", "interchainAccountRouter": "0xa82a0227e6d6db53AF4B264A852bfF91C6504a51", "interchainGasPaymaster": "0x95519ba800BBd0d34eeAE026fEc620AD978176C0", - "interchainSecurityModule": "0x7eB10cc72D8828e5aa25cF108F209DfcC0F1d9f4", + "interchainSecurityModule": "0x430e9C6f19aa66a0d699a6547288dd4c5C173EeB", "mailbox": "0xFf06aFcaABaDDd1fb08371f9ccA15D73D51FeBD6", "merkleTreeHook": "0x84eea61D679F42D92145fA052C89900CBAccE95A", "name": "avalanche", @@ -245,7 +245,7 @@ "interchainAccountIsm": "0x223F7D3f27E6272266AE4B5B91Fd5C7A2d798cD8", "interchainAccountRouter": "0x4767D22117bBeeb295413000B620B93FD8522d53", "interchainGasPaymaster": "0xc3F23848Ed2e04C0c6d41bd7804fa8f89F940B94", - "interchainSecurityModule": "0xa0b0a0015561abDdDD32118c81c6D5f822a7369e", + "interchainSecurityModule": "0x3236Ad296df3389E1916DD6D8410dAF36361F869", "mailbox": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", "merkleTreeHook": "0x19dc38aeae620380430C200a6E990D5Af5480117", "name": "base", @@ -320,7 +320,7 @@ "interchainAccountIsm": "0xe93f2f409ad8B5000431D234472973fe848dcBEC", "interchainAccountRouter": "0x2f4Eb04189e11Af642237Da62d163Ab714614498", "interchainGasPaymaster": "0xB3fCcD379ad66CED0c91028520C64226611A48c9", - "interchainSecurityModule": "0x7414266C063420385B50cAAe157aBba3E008F275", + "interchainSecurityModule": "0xE6DCad3Cb356b9278c2597b27f8005818EcDe35e", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0xC9B8ea6230d6687a4b13fD3C0b8f0Ec607B26465", "name": "blast", @@ -388,7 +388,7 @@ "interchainAccountIsm": "0x451dF8AB0936D85526D816f0b4dCaDD934A034A4", "interchainAccountRouter": "0x5C02157068a52cEcfc98EDb6115DE6134EcB4764", "interchainGasPaymaster": "0x62B7592C1B6D1E43f4630B8e37f4377097840C05", - "interchainSecurityModule": "0xB6417D6dCd2B07110031FFfAe971146698b2b49E", + "interchainSecurityModule": "0x722e40abcbCa22b026D2B7CaAE3f045cD74BC36F", "mailbox": "0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147", "merkleTreeHook": "0x781bE492F1232E66990d83a9D3AC3Ec26f56DAfB", "name": "bob", @@ -454,7 +454,7 @@ "interchainAccountIsm": "0x9e22945bE593946618383B108CC5bce09eBA4C26", "interchainAccountRouter": "0x32A07c1B7a7fe8D4A0e44B0181873aB9d64C16c1", "interchainGasPaymaster": "0x78E25e7f84416e69b9339B0A6336EB6EFfF6b451", - "interchainSecurityModule": "0x04e755515dcCF415785a0569b774d729545644D8", + "interchainSecurityModule": "0xd53e71905018cC84118F71aD01e6e7D04ce54331", "mailbox": "0x2971b9Aec44bE4eb673DF1B88cDB57b96eefe8a4", "merkleTreeHook": "0xFDb9Cd5f9daAA2E4474019405A328a88E7484f26", "name": "bsc", @@ -535,7 +535,7 @@ "interchainAccountIsm": "0xB732c83aeE29596E3163Da2260710eAB67Bc0B29", "interchainAccountRouter": "0x27a6cAe33378bB6A6663b382070427A01fc9cB37", "interchainGasPaymaster": "0x571f1435613381208477ac5d6974310d88AC7cB7", - "interchainSecurityModule": "0x79D33fd54d950836452a32D28dEE680C274481aa", + "interchainSecurityModule": "0x5698f94618093Bb122e946E2804d91017def3Ed4", "mailbox": "0x50da3B3907A08a24fe4999F4Dcf337E8dC7954bb", "merkleTreeHook": "0x04dB778f05854f26E67e0a66b740BBbE9070D366", "name": "celo", @@ -600,7 +600,7 @@ "interchainAccountIsm": "0x4Eb82Ee35b0a1c1d776E3a3B547f9A9bA6FCC9f2", "interchainAccountRouter": "0xEF9A332Ec1fD233Bf9344A58be56ff9E104B4f60", "interchainGasPaymaster": "0x7E27456a839BFF31CA642c060a2b68414Cb6e503", - "interchainSecurityModule": "0xe81735F4a3aF24A8D970e5Df9D095Af29b770a5a", + "interchainSecurityModule": "0x2bBB4d8337eaeFB6dA75b1647403722cf2Fe8801", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0x0054D19613f20dD72721A146ED408971a2CCA9BD", "name": "cheesechain", @@ -663,7 +663,7 @@ "from": 4842212 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0xe330a492e8010147BB3C34149F660f0960F96303", + "interchainSecurityModule": "0xA3A8aF70997949B138aBEd936D270Fd95bE60cF5", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "cyber", @@ -730,7 +730,7 @@ "from": 23783929 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0x4130098643F029b7d22909E991aeF16E2577Adae", + "interchainSecurityModule": "0x7F14B16C446884E06aF21AF2340C6B91D9cD4AF6", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "degenchain", @@ -843,7 +843,7 @@ "interchainAccountIsm": "0xCeafc098e5c3c7768b9229Be2FEC275862A81Abd", "interchainAccountRouter": "0xed9a722c543883FB7e07E78F3879762DE09eA7D5", "interchainGasPaymaster": "0xB30EAB08aa87138D57168D0e236850A530f49921", - "interchainSecurityModule": "0xa3133C4E7D94e2479c63D30742C5B210EB66c3C7", + "interchainSecurityModule": "0x438e8B0d9fDdB16E29Fa22AfE3a63cD7Bf0D4909", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xC831271c1fB212012811a91Dd43e5926C1020563", "name": "endurance", @@ -914,7 +914,7 @@ "interchainAccountIsm": "0x292C614ED53DaaDBf971521bc2C652d1ca51cB47", "interchainAccountRouter": "0x5E532F7B610618eE73C2B462978e94CB1F7995Ce", "interchainGasPaymaster": "0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611", - "interchainSecurityModule": "0x0115b6ea2933C11540079E55E16A940fd9856c83", + "interchainSecurityModule": "0x59f2ba715A3d4bA4beC4e7ea7988eC3c579ecE77", "mailbox": "0xc005dc82818d67AF737725bD4bf75435d065D239", "merkleTreeHook": "0x48e6c30B97748d1e2e03bf3e9FbE3890ca5f8CCA", "name": "ethereum", @@ -930,7 +930,7 @@ "proxyAdmin": "0x75EE15Ee1B4A75Fa3e2fDF5DF3253c25599cc659", "rpcUrls": [ { - "http": "https://rpc.ankr.com/eth" + "http": "https://eth.llamarpc.com" }, { "http": "https://ethereum.publicnode.com" @@ -989,7 +989,7 @@ "interchainAccountIsm": "0x7C012DCA02C42cfA3Fd7Da3B0ED7234B52AE68eF", "interchainAccountRouter": "0xbed53B5C5BCE9433f25A2A702e6df13E22d84Ae9", "interchainGasPaymaster": "0x2Fca7f6eC3d4A0408900f2BB30004d4616eE985E", - "interchainSecurityModule": "0x5deE9A82730A6624f9Fc8051971ef2413248dcfB", + "interchainSecurityModule": "0xDa87175A8D1d6C5102228fB02b0D74AA13f496fb", "mailbox": "0x2f9DB5616fa3fAd1aB06cB2C906830BA63d135e3", "merkleTreeHook": "0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147", "name": "fraxtal", @@ -1057,7 +1057,7 @@ "interchainAccountIsm": "0x9629c28990F11c31735765A6FD59E1E1bC197DbD", "interchainAccountRouter": "0x2351FBe24C1212F253b7a300ff0cBCFd97952a19", "interchainGasPaymaster": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", - "interchainSecurityModule": "0x5056E14dc4db177d8bdd2d6a3D6b26cF7b54A207", + "interchainSecurityModule": "0xcBE8A3d5A9714A7ae5acFA3185e27b8859149AF7", "mailbox": "0x3071D4DA6020C956Fe15Bfd0a9Ca8D4574f16696", "merkleTreeHook": "0xfBc08389224d23b79cb21cDc16c5d42F0ad0F57f", "name": "fusemainnet", @@ -1131,7 +1131,7 @@ "interchainAccountIsm": "0x07E2062A1bC66a2C1d05cb5C3870a4AF86e0056E", "interchainAccountRouter": "0xBE70Ab882D1F7E37e04a70CDd9Ec23b37a234064", "interchainGasPaymaster": "0xDd260B99d302f0A3fF885728c086f729c06f227f", - "interchainSecurityModule": "0xF142247dE475528587c6Ec07A94f14719a01F1fd", + "interchainSecurityModule": "0x9B35A8A81Af682178c501ca2A03759582E663cc8", "mailbox": "0xaD09d78f4c6b9dA2Ae82b1D34107802d380Bb74f", "merkleTreeHook": "0x2684C6F89E901987E1FdB7649dC5Be0c57C61645", "name": "gnosis", @@ -1202,7 +1202,7 @@ "interchainAccountIsm": "0x708E002637792FDC031E6B62f23DD60014AC976a", "interchainAccountRouter": "0xfB8cea1c7F45608Da30655b50bbF355D123A4358", "interchainGasPaymaster": "0x19dc38aeae620380430C200a6E990D5Af5480117", - "interchainSecurityModule": "0x7544825B817eea456DE8e20627922C6eC870Df98", + "interchainSecurityModule": "0xB5A787404B071415CDba76Aa6823C4313D326e6e", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0x0972954923a1e2b2aAb04Fa0c4a0797e5989Cd65", "name": "inevm", @@ -1333,7 +1333,7 @@ "from": 14616307 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0xBddA3A94f6c8E99ad5F49A44D8e4122157296c73", + "interchainSecurityModule": "0xD75EddFf11a00b86Ac6d7Af98C090FfA928FceEC", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "kroma", @@ -1406,7 +1406,7 @@ "interchainAccountIsm": "0xdcA646C56E7768DD11654956adE24bfFf9Ba4893", "interchainAccountRouter": "0xD59dA396F162Ed93a41252Cebb8d5DD4F093238C", "interchainGasPaymaster": "0x8105a095368f1a184CceA86cCe21318B5Ee5BE28", - "interchainSecurityModule": "0x0Eb6D5192d6a3a25Ee913F1439148E1B3D5c83D0", + "interchainSecurityModule": "0xe13288068F0D7b0c0FE12C4fDb0B3FD3F4F7E101", "mailbox": "0x02d16BC51af6BfD153d67CA61754cF912E82C4d9", "merkleTreeHook": "0xC077A0Cc408173349b1c9870C667B40FE3C01dd7", "name": "linea", @@ -1477,7 +1477,7 @@ "from": 4195553 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0xf978e697c1eF5F079948515212dD34Dda5683025", + "interchainSecurityModule": "0x1F444C08D2Dc01cE735fC11F8986008d7B611001", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "lisk", @@ -1542,7 +1542,7 @@ "from": 3088760 }, "interchainGasPaymaster": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", - "interchainSecurityModule": "0xAAe0d8E08452f9a1a5aeF8D62F07105888cf99b6", + "interchainSecurityModule": "0x2A609D036b0a30F87340726c0E29e67c0a424201", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0x062200d92dF6bB7bA89Ce4D6800110450f94784e", "name": "lukso", @@ -1616,7 +1616,7 @@ "interchainAccountIsm": "0x8Ea50255C282F89d1A14ad3F159437EE5EF0507f", "interchainAccountRouter": "0x693A4cE39d99e46B04cb562329e3F0141cA17331", "interchainGasPaymaster": "0x0D63128D887159d63De29497dfa45AFc7C699AE4", - "interchainSecurityModule": "0x354Fd68D5009BCe9a8e8Ce3Ff05A7fEC0866F9dF", + "interchainSecurityModule": "0x10d65CAF7B1c764b77015913d4bF69aEfdd57f06", "isTestnet": false, "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x149db7afD694722747035d5AEC7007ccb6F8f112", @@ -1686,7 +1686,7 @@ "interchainAccountIsm": "0xe039DA3A0071BEd087A12660D7b03cf669c7776E", "interchainAccountRouter": "0x45285463352c53a481e882cD5E2AF2E25BBdAd0D", "interchainGasPaymaster": "0x8105a095368f1a184CceA86cCe21318B5Ee5BE28", - "interchainSecurityModule": "0x0F2ca75C53A7029C33c7Ca55459bEC1b6F06CF44", + "interchainSecurityModule": "0x1104Fb7e5CA092aBB9154bB2B1E1905ff35332fe", "mailbox": "0x398633D19f4371e1DB5a8EFE90468eB70B1176AA", "merkleTreeHook": "0x5332D1AC0A626D265298c14ff681c0A8D28dB86d", "name": "mantle", @@ -1748,7 +1748,7 @@ "from": 13523607 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0x99b3c360f2Cf6C9139F005D39CB34A7cD6ceC118", + "interchainSecurityModule": "0x6828457a0727cE2DEe5c9f6f4a7498565844067f", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "merlin", @@ -1815,7 +1815,7 @@ "from": 17966274 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0xE6aF8e780401023a6b9694Bf3b9fC6960065A0e6", + "interchainSecurityModule": "0x231B529b4f34FBfF2c609D4F721e9343fDA7B97f", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "metis", @@ -1880,7 +1880,7 @@ "from": 3752032 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0xB6027D4d56863d59760c77449C068cF1B31Ee1Ec", + "interchainSecurityModule": "0x4761669387477aB606C9aF717accFe395F4b4699", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "mint", @@ -1947,7 +1947,7 @@ "interchainAccountIsm": "0xa377b8269e0A47cdd2fD5AAeAe860b45623c6d82", "interchainAccountRouter": "0x6e1B9f776bd415d7cC3C7458A5f0d801016918f8", "interchainGasPaymaster": "0x931dFCc8c1141D6F532FD023bd87DAe0080c835d", - "interchainSecurityModule": "0x5024F80A462be9dBBfbFEd2b71edDd0e7c55Bd23", + "interchainSecurityModule": "0xC0BA461D612361F374066F8Ffa850717d3eE003b", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xE2ee936bEa8e42671c400aC96dE198E06F2bA2A6", "name": "mode", @@ -2019,7 +2019,7 @@ "interchainAccountIsm": "0x79b3730CE3685f65802aF1771319992bA960EB9D", "interchainAccountRouter": "0xc4482f66191754a8629D35289043C4EB0285F10E", "interchainGasPaymaster": "0x14760E32C0746094cF14D97124865BC7F0F7368F", - "interchainSecurityModule": "0x8D026C1fe04E1CB080594AfA02A1966DCA7CEb18", + "interchainSecurityModule": "0x981974103BC1C6D40D9E7b79F3dAaEA672913E3e", "mailbox": "0x094d03E751f49908080EFf000Dd6FD177fd44CC3", "merkleTreeHook": "0x87403b85f6f316e7ba91ba1fa6C3Fb7dD4095547", "name": "moonbeam", @@ -2161,7 +2161,7 @@ "interchainAccountIsm": "0x2c46BF14641d00549ECa4779BF5CBf91602C1DEd", "interchainAccountRouter": "0x03D6cC17d45E9EA27ED757A8214d1F07F7D901aD", "interchainGasPaymaster": "0xD8A76C4D91fCbB7Cc8eA795DFDF870E48368995C", - "interchainSecurityModule": "0xBa2Ed586ddC1aAfDc231E56533EC8174F54EBe0D", + "interchainSecurityModule": "0x66B72EA92140d727f9B831dA997a7bf2B4241CbA", "mailbox": "0xd4C1905BB1D26BC93DAC913e13CaCC278CdCC80D", "merkleTreeHook": "0x68eE9bec9B4dbB61f69D9D293Ae26a5AACb2e28f", "name": "optimism", @@ -2300,7 +2300,7 @@ "interchainAccountIsm": "0xBAC4529cdfE7CCe9E858BF706e41F8Ed096C1BAd", "interchainAccountRouter": "0xF163949AD9F88977ebF649D0461398Ca752E64B9", "interchainGasPaymaster": "0x0071740Bf129b05C4684abfbBeD248D80971cce2", - "interchainSecurityModule": "0xEe59cE6943c3c2C462C73eDf2f57B9131091B250", + "interchainSecurityModule": "0x46640247fd58Ddd6FFd669842EAa8f1126653b47", "mailbox": "0x5d934f4e2f797775e53561bB72aca21ba36B96BB", "merkleTreeHook": "0x73FbD25c3e817DC4B4Cd9d00eff6D83dcde2DfF6", "name": "polygon", @@ -2374,7 +2374,7 @@ "interchainAccountIsm": "0xc1198e241DAe48BF5AEDE5DCE49Fe4A6064cF7a7", "interchainAccountRouter": "0x20a0A32a110362920597F72974E1E0d7e25cA20a", "interchainGasPaymaster": "0x0D63128D887159d63De29497dfa45AFc7C699AE4", - "interchainSecurityModule": "0x4490b10C94310AE485F57A73eaCF43e5A3f808D2", + "interchainSecurityModule": "0x8E3221dfeB876698AE86DD24fF8C9668AFa1a365", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x149db7afD694722747035d5AEC7007ccb6F8f112", "name": "polygonzkevm", @@ -2442,7 +2442,7 @@ "from": 32018468 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0x5C066Bf0616B6b7c2cBAEc72752c9b7bC9DC59FC", + "interchainSecurityModule": "0x7d86188dad2e53078E78CaFEdaAaec85df850a32", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "proofofplay", @@ -2506,7 +2506,7 @@ "from": 363159 }, "interchainGasPaymaster": "0x3071D4DA6020C956Fe15Bfd0a9Ca8D4574f16696", - "interchainSecurityModule": "0x68443a13969367E940AA52d73c01883fA7E396DD", + "interchainSecurityModule": "0x541c30815Ae4268EfD2c71356E0eF7b954BA81EC", "mailbox": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", "merkleTreeHook": "0x55E4F0bc6b7Bb493D50839A8592e7ad8d5e93cf7", "name": "real", @@ -2573,7 +2573,7 @@ "interchainAccountIsm": "0x5DA60220C5dDe35b7aE91c042ff5979047FA0785", "interchainAccountRouter": "0x7a4d31a686A36285d68e14EDD53631417eB19603", "interchainGasPaymaster": "0x2Fa570E83009eaEef3a1cbd496a9a30F05266634", - "interchainSecurityModule": "0xeF08221d4442d85e8db38caC16DD6b10287801fF", + "interchainSecurityModule": "0x79461FE1BedB824630b7E3c896409cb41FAC2bAd", "mailbox": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D", "merkleTreeHook": "0x8F1E22d309baa69D398a03cc88E9b46037e988AA", "name": "redstone", @@ -2635,7 +2635,7 @@ "from": 937117 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0x5Ec54C72c5Df5D2f02A4e4aCBEE9631c6866517A", + "interchainSecurityModule": "0x3eC59Fcd81b2542aa8661A8e25011Fda4b342c69", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "sanko", @@ -2703,7 +2703,7 @@ "interchainAccountIsm": "0x32af5Df81fEd5E26119F6640FBB13f3d63a94CDe", "interchainAccountRouter": "0x0B48a744698ba8dFa514742dFEB6728f52fD66f7", "interchainGasPaymaster": "0xBF12ef4B9f307463D3FB59c3604F294dDCe287E2", - "interchainSecurityModule": "0x40B5F901675B5E911C07E01987Db8a346DcAD259", + "interchainSecurityModule": "0x22bC113c8279393E5995c64e6Af78b0397437f43", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0x6119E37Bd66406A1Db74920aC79C15fB8411Ba76", "name": "scroll", @@ -2784,7 +2784,7 @@ "interchainAccountIsm": "0xf35dc7B9eE4Ebf0cd3546Bd6EE3b403dE2b9F5D6", "interchainAccountRouter": "0xBcaedE97a98573A88242B3b0CB0A255F3f90d4d5", "interchainGasPaymaster": "0xFC62DeF1f08793aBf0E67f69257c6be258194F72", - "interchainSecurityModule": "0xAb0e1AeA5a57EfF6DD7C77D2934c745ab8CCEF2E", + "interchainSecurityModule": "0x8bB29A4c9F4985b2b18d02da19994E1f8f49a456", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xca1b69fA4c4a7c7fD839bC50867c589592bcfe49", "name": "sei", @@ -2899,7 +2899,7 @@ "interchainAccountIsm": "0xAE557e108b3336130370aC74836f1356B4b30Cf2", "interchainAccountRouter": "0x1F8CF09F060A2AE962c0Bb1F92e209a1E7b0E10B", "interchainGasPaymaster": "0x273Bc6b01D9E88c064b6E5e409BdF998246AEF42", - "interchainSecurityModule": "0x9241FBE9611799eE0bAd18C57f83Baa40dc9ebDb", + "interchainSecurityModule": "0x52FA98bdbf1217f3670387d8587487Bb620aBdC5", "mailbox": "0x28EFBCadA00A7ed6772b3666F3898d276e88CAe3", "merkleTreeHook": "0x6A55822cf11f9fcBc4c75BC2638AfE8Eb942cAdd", "name": "taiko", @@ -3029,7 +3029,7 @@ "interchainAccountIsm": "0x551BbEc45FD665a8C95ca8731CbC32b7653Bc59B", "interchainAccountRouter": "0xc11f8Cf2343d3788405582F65B8af6A4F7a6FfC8", "interchainGasPaymaster": "0x0D63128D887159d63De29497dfa45AFc7C699AE4", - "interchainSecurityModule": "0x3656cAaFC7Fe916E75767DF2c8ACeB81D3a57A21", + "interchainSecurityModule": "0x04Df0F875eBF8b7E81Df0c8014C2c88c08d47375", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0x149db7afD694722747035d5AEC7007ccb6F8f112", "name": "viction", @@ -3097,7 +3097,7 @@ "interchainAccountIsm": "0xCB9f90EE5d83Ea52ABd922BD70898f0155D54798", "interchainAccountRouter": "0x473884010F0C1742DA8Ad01E7E295624B931076b", "interchainGasPaymaster": "0x7E27456a839BFF31CA642c060a2b68414Cb6e503", - "interchainSecurityModule": "0xDBD0bFC0F4594D697290d4F83293b743FdF4c90F", + "interchainSecurityModule": "0x3bd1AbAe2c79c522Bb1724EcCDE64a5B3EFD7306", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0x0054D19613f20dD72721A146ED408971a2CCA9BD", "name": "worldchain", @@ -3160,7 +3160,7 @@ "from": 24395308 }, "interchainGasPaymaster": "0x9844aFFaBE17c37F791ff99ABa58B0FbB75e22AF", - "interchainSecurityModule": "0xe84F868bC5bc6F620fDe2556E7DC4B303c1daf73", + "interchainSecurityModule": "0x8d6885498eaFe02248578d19143538F56CF9c0e1", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "name": "xai", @@ -3228,7 +3228,7 @@ "interchainAccountIsm": "0x29B37088724B745C0ABcE591449Cf042772160C2", "interchainAccountRouter": "0x03cF708E42C89623bd83B281A56935cB562b9258", "interchainGasPaymaster": "0x7E27456a839BFF31CA642c060a2b68414Cb6e503", - "interchainSecurityModule": "0x2b973956070C0DC62ed2c65fBaEcD631A668f4B1", + "interchainSecurityModule": "0x34881587eE748410d4adE581c88D19520b4405F5", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0x0054D19613f20dD72721A146ED408971a2CCA9BD", "name": "xlayer", @@ -3296,7 +3296,7 @@ "interchainAccountIsm": "0x2b6d3F7d28B5EC8C3C028fBCAdcf774D9709Dd29", "interchainAccountRouter": "0x3AdCBc94ab8C48EC52D06dc65Bb787fD1981E3d5", "interchainGasPaymaster": "0x931dFCc8c1141D6F532FD023bd87DAe0080c835d", - "interchainSecurityModule": "0x382bd3B7E7B567FdA24Ef4E1d885bFB5ebD77Af5", + "interchainSecurityModule": "0xdA297907c3700234d7deDE5d346D8c7376d77584", "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", "merkleTreeHook": "0xE2ee936bEa8e42671c400aC96dE198E06F2bA2A6", "name": "zetachain", @@ -3362,7 +3362,7 @@ "from": 1511458 }, "interchainGasPaymaster": "0x03cF708E42C89623bd83B281A56935cB562b9258", - "interchainSecurityModule": "0x09264F8a74d0217DcC2a18bcbf873855F2603d8C", + "interchainSecurityModule": "0x48793Ed9b3fb48a759Fb2e0D4988d2BE71c4F141", "mailbox": "0xc2FbB9411186AB3b1a6AFCCA702D1a80B48b197c", "merkleTreeHook": "0x4C97D35c668EE5194a13c8DE8Afc18cce40C9F28", "name": "zircuit", @@ -3435,7 +3435,7 @@ "interchainAccountIsm": "0xb2674E213019972f937CCFc5e23BF963D915809e", "interchainAccountRouter": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "interchainGasPaymaster": "0x18B0688990720103dB63559a3563f7E8d0f63EDb", - "interchainSecurityModule": "0x3CB581058a67B5Bb516B074BC027Dea481283132", + "interchainSecurityModule": "0xa752ADc2a1b2bcCBfB08216f567b8ad4a6B1c54D", "mailbox": "0xF5da68b2577EF5C0A0D98aA2a58483a68C2f232a", "merkleTreeHook": "0x886BB0f329781b98f98FDeb1ce7a8957F2d43B9F", "name": "zoramainnet", @@ -3506,7 +3506,7 @@ "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", "fallbackRoutingHook": "0xc401e251CCa7A364114504A994D6fC7cb1c243AB", "interchainGasPaymaster": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", - "interchainSecurityModule": "0x4cCCF749af950C3bD26bA943D5C6A37359cD7401", + "interchainSecurityModule": "0x79891a4B04cfcD56932102D69d6cbd57E77b51A4", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", "pausableHook": "0x5Ed813B8b41f25c8002B01A72bbDBe6A0232Fe27", @@ -3531,73 +3531,6 @@ "timelockController": "0x0000000000000000000000000000000000000000", "technicalStack": "polkadotsubstrate" }, - "astarzkevm": { - "blockExplorers": [ - { - "apiUrl": "https://astar-zkevm.explorer.startale.com/api", - "family": "blockscout", - "name": "Astar zkEVM Explorer", - "url": "https://astar-zkevm.explorer.startale.com" - } - ], - "blocks": { - "confirmations": 1, - "estimateBlockTime": 3, - "reorgPeriod": 5 - }, - "chainId": 3776, - "deployer": { - "name": "Abacus Works", - "url": "https://www.hyperlane.xyz" - }, - "displayName": "Astar zkEVM", - "domainId": 3776, - "gasCurrencyCoinGeckoId": "ethereum", - "name": "astarzkevm", - "nativeToken": { - "decimals": 18, - "name": "Ethereum", - "symbol": "ETH" - }, - "protocol": "ethereum", - "rpcUrls": [ - { - "http": "https://rpc.startale.com/astar-zkevm" - }, - { - "http": "https://astar-zkevm-rpc.dwellir.com" - } - ], - "aggregationHook": "0x9aA6A7525Aa42f861AB07F8556654971dd4525ED", - "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", - "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", - "fallbackRoutingHook": "0xc401e251CCa7A364114504A994D6fC7cb1c243AB", - "interchainGasPaymaster": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", - "interchainSecurityModule": "0x0F1cd0ab7Aee598474E5D121a8cc85c550f4e970", - "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", - "merkleTreeHook": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", - "pausableHook": "0x5Ed813B8b41f25c8002B01A72bbDBe6A0232Fe27", - "pausableIsm": "0x61594D2cA900C44ab51d07776465397FefC643C6", - "protocolFee": "0xb4fc9B5fD57499Ef6FfF3995728a55F7A618ef86", - "proxyAdmin": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", - "staticAggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", - "staticAggregationIsm": "0x78A31fF00D79158c22C09DC6D7Fa7188e21925E4", - "staticAggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", - "staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", - "staticMerkleRootWeightedMultisigIsmFactory": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", - "staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", - "staticMessageIdWeightedMultisigIsmFactory": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", - "storageGasOracle": "0xA38D1D7F217A52A27b0e6BF50E0a9ddAD05798C0", - "testRecipient": "0xC49aF4965264FA7BB6424CE37aA06773ad177224", - "validatorAnnounce": "0x59C2dB903937EbE55B59c3415FD55e970FF5f2DC", - "index": { - "from": 5253856 - }, - "interchainAccountIsm": "0xa35cbc2d169284580d82AecED883d0800aa7fbfC", - "interchainAccountRouter": "0x7621e04860F0bDe63311db9D5D8b589AD3458A1f", - "timelockController": "0x0000000000000000000000000000000000000000", - "technicalStack": "polygoncdk" - }, "bitlayer": { "blockExplorers": [ { @@ -3643,7 +3576,7 @@ "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", "fallbackRoutingHook": "0xc401e251CCa7A364114504A994D6fC7cb1c243AB", "interchainGasPaymaster": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", - "interchainSecurityModule": "0x3cF48484617AF065B3EF13f4Fa9c4c0B4BA0Cf16", + "interchainSecurityModule": "0xcA0dEbdcd926cf06507a2e19ca7f5c3087577F09", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", "pausableHook": "0x5Ed813B8b41f25c8002B01A72bbDBe6A0232Fe27", @@ -3719,7 +3652,7 @@ "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", "fallbackRoutingHook": "0xc401e251CCa7A364114504A994D6fC7cb1c243AB", "interchainGasPaymaster": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", - "interchainSecurityModule": "0x5BA207e5d1095Ea9BdeF6E8331B194253E965B93", + "interchainSecurityModule": "0xf71E5802FeFDdd09CcF131362c8cfD195EeB3C4E", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", "pausableHook": "0x5Ed813B8b41f25c8002B01A72bbDBe6A0232Fe27", @@ -3783,7 +3716,7 @@ "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", "fallbackRoutingHook": "0xc401e251CCa7A364114504A994D6fC7cb1c243AB", "interchainGasPaymaster": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", - "interchainSecurityModule": "0xEa043414d0910b2E34bd9fa2AB20Ce5724c0d156", + "interchainSecurityModule": "0x239A0567841a1489DfD134089a5806bE7823A38C", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", "pausableHook": "0x5Ed813B8b41f25c8002B01A72bbDBe6A0232Fe27", @@ -3856,7 +3789,7 @@ "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", "fallbackRoutingHook": "0xc401e251CCa7A364114504A994D6fC7cb1c243AB", "interchainGasPaymaster": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", - "interchainSecurityModule": "0xba83B88Ad0b746908ADa44612d0D27470B7bAA1B", + "interchainSecurityModule": "0xf813C66da07b10438CE199AAD100F14C00D050aF", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", "pausableHook": "0x5Ed813B8b41f25c8002B01A72bbDBe6A0232Fe27", @@ -3925,7 +3858,7 @@ "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", "fallbackRoutingHook": "0xc401e251CCa7A364114504A994D6fC7cb1c243AB", "interchainGasPaymaster": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", - "interchainSecurityModule": "0x90f37dEAADEB97247aaD9F4351D1e1605B14Be08", + "interchainSecurityModule": "0x97B619407F1EFEA56bA0da09cDfdf2B88B5dC360", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", "pausableHook": "0x5Ed813B8b41f25c8002B01A72bbDBe6A0232Fe27", @@ -3988,7 +3921,7 @@ "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", "fallbackRoutingHook": "0xc401e251CCa7A364114504A994D6fC7cb1c243AB", "interchainGasPaymaster": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", - "interchainSecurityModule": "0x2780874be44b38D32aef5149f2153E48A8E3ab1F", + "interchainSecurityModule": "0xF730bB6EAA4c07aAA3C247d4276457cb14A7C6B6", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575", "pausableHook": "0x5Ed813B8b41f25c8002B01A72bbDBe6A0232Fe27", @@ -4058,7 +3991,7 @@ "interchainAccountIsm": "0xcd9D3744512F07AE844c40E27912092d7c503565", "interchainAccountRouter": "0x92cdbF0Ccdf8E93467FA858fb986fa650A02f2A8", "interchainGasPaymaster": "0xb58257cc81E47EC72fD38aE16297048de23163b4", - "interchainSecurityModule": "0x8ac8dA1D80D1B7A942E0ec47FC17376581B09D6C", + "interchainSecurityModule": "0xd03554d5e47B129F843Dad6fCB01335cF2bB956b", "mailbox": "0x7f50C5776722630a0024fAE05fDe8b47571D7B39", "merkleTreeHook": "0xCC3D1659D50461d27a2F025dDb2c9B06B584B7e1", "pausableHook": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd", @@ -4118,7 +4051,7 @@ "interchainAccountIsm": "0xc23BaF5Eb5848D19701BbE7f139645e6bd58a319", "interchainAccountRouter": "0x7c58Cadcc2b60ACF794eE1843488d6f5703f76BE", "interchainGasPaymaster": "0xb4fc9B5fD57499Ef6FfF3995728a55F7A618ef86", - "interchainSecurityModule": "0xBdd7D5e713f22568b96935fE28d91A043AbAC868", + "interchainSecurityModule": "0xE3ce115F5a0561364e4576BE408AEB3d362e77ee", "mailbox": "0xb129828B9EDa48192D0B2db35D0E40dCF51B3594", "merkleTreeHook": "0x3E969bA938E6A993eeCD6F65b0dd8712B07dFe59", "pausableHook": "0x6Fb36672365C7c797028C400A61c58c0ECc53cD2", @@ -4249,7 +4182,7 @@ "interchainAccountIsm": "0x6119B76720CcfeB3D256EC1b91218EEfFD6756E1", "interchainAccountRouter": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", "interchainGasPaymaster": "0x18B0688990720103dB63559a3563f7E8d0f63EDb", - "interchainSecurityModule": "0x9746fB7d2567f087342283AA613dFa204DAd19ce", + "interchainSecurityModule": "0xca63656D17e9a368c527edeAE562a653bc1700fb", "mailbox": "0x7f50C5776722630a0024fAE05fDe8b47571D7B39", "merkleTreeHook": "0x886BB0f329781b98f98FDeb1ce7a8957F2d43B9F", "pausableHook": "0x2F619Ac5122689180AeBB930ADccdae215d538a9", @@ -4313,7 +4246,7 @@ "interchainAccountIsm": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", "interchainAccountRouter": "0xeE8C0E1EeBfFCC451a013336386eA53E42a44451", "interchainGasPaymaster": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", - "interchainSecurityModule": "0x244B331e7D1Cb096fD222f7D8437b19abbf9e429", + "interchainSecurityModule": "0xE124bDAc9235650406363165cBcE61bA32c1461C", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x6963480b05EB58f4d624B014ab92e9aD4d21df6D", "pausableHook": "0xD0dca420feFda68537695A8D887080eeF4030AF7", @@ -4374,7 +4307,7 @@ "interchainAccountIsm": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", "interchainAccountRouter": "0xeE8C0E1EeBfFCC451a013336386eA53E42a44451", "interchainGasPaymaster": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", - "interchainSecurityModule": "0xc3C3cCD63f52816FAf7525ED42B30B1ac3a5211A", + "interchainSecurityModule": "0x8c2306B2c5E8D6fBba256e5De1AF1bDae46c9772", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x6963480b05EB58f4d624B014ab92e9aD4d21df6D", "pausableHook": "0xD0dca420feFda68537695A8D887080eeF4030AF7", @@ -4453,7 +4386,7 @@ "interchainAccountIsm": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", "interchainAccountRouter": "0xeE8C0E1EeBfFCC451a013336386eA53E42a44451", "interchainGasPaymaster": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", - "interchainSecurityModule": "0x36A3E7d097c22b4b34790eCecc109B113863beE5", + "interchainSecurityModule": "0x6a696868Bb1676FD6F621F47826b5B94EAaD2992", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x6963480b05EB58f4d624B014ab92e9aD4d21df6D", "pausableHook": "0xD0dca420feFda68537695A8D887080eeF4030AF7", @@ -4520,7 +4453,7 @@ "interchainAccountIsm": "0x783EC5e105234a570eB90f314284E5dBe53bdd90", "interchainAccountRouter": "0xc5D6aCaafBCcEC6D7fD7d92F4509befce641c563", "interchainGasPaymaster": "0xf3dFf6747E7FC74B431C943961054B7BF6309d8a", - "interchainSecurityModule": "0xA45f45eA1C30C20dabc2d27bAEb172baF97544D3", + "interchainSecurityModule": "0xc7F5fFadDb69a9BB79e7Bdd9707F8a964DA12ED2", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x5090dF2FBDa7127c7aDa41f60B79F5c55D380Dd8", "pausableHook": "0x886BB0f329781b98f98FDeb1ce7a8957F2d43B9F", @@ -4591,7 +4524,7 @@ "interchainAccountIsm": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", "interchainAccountRouter": "0xeE8C0E1EeBfFCC451a013336386eA53E42a44451", "interchainGasPaymaster": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", - "interchainSecurityModule": "0xb13A746F29444A6AB647c30189cF9A7651379294", + "interchainSecurityModule": "0x5A8b89A37500f5d30Aa6A8855714e48eEA513928", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x6963480b05EB58f4d624B014ab92e9aD4d21df6D", "pausableHook": "0xD0dca420feFda68537695A8D887080eeF4030AF7", @@ -4656,7 +4589,7 @@ "interchainAccountIsm": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", "interchainAccountRouter": "0xeE8C0E1EeBfFCC451a013336386eA53E42a44451", "interchainGasPaymaster": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", - "interchainSecurityModule": "0x24C784dCe9619D785D3cD186EcFc0d7D2725cBBE", + "interchainSecurityModule": "0x68d516c4eD1545b9530f909c5875332eac6435A7", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x6963480b05EB58f4d624B014ab92e9aD4d21df6D", "pausableHook": "0xD0dca420feFda68537695A8D887080eeF4030AF7", @@ -4720,7 +4653,7 @@ "interchainAccountIsm": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", "interchainAccountRouter": "0xeE8C0E1EeBfFCC451a013336386eA53E42a44451", "interchainGasPaymaster": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", - "interchainSecurityModule": "0xF803499c61b388c0916D567545F5ef912b0866CD", + "interchainSecurityModule": "0x5cd17cc5F5802aa63645d3AF8A9071bE3F9B5E0c", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x6963480b05EB58f4d624B014ab92e9aD4d21df6D", "pausableHook": "0xD0dca420feFda68537695A8D887080eeF4030AF7", @@ -4790,7 +4723,7 @@ "interchainAccountIsm": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", "interchainAccountRouter": "0xeE8C0E1EeBfFCC451a013336386eA53E42a44451", "interchainGasPaymaster": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", - "interchainSecurityModule": "0x40f38d88a1C602Cf368905968881EffcBbbeE520", + "interchainSecurityModule": "0x1f62894DDec2FA56581D483E0DFAC93cc0051dD5", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x6963480b05EB58f4d624B014ab92e9aD4d21df6D", "pausableHook": "0xD0dca420feFda68537695A8D887080eeF4030AF7", @@ -4854,7 +4787,7 @@ "interchainAccountIsm": "0xFB9e40D811Cea562cc8a322b029eF2BDcC3ef6ed", "interchainAccountRouter": "0xeE8C0E1EeBfFCC451a013336386eA53E42a44451", "interchainGasPaymaster": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", - "interchainSecurityModule": "0x4186D7f90Cd5f0402aC7124c63a0Cc8Ff1DeeC87", + "interchainSecurityModule": "0xFf828bCb21b15A5c9aA172331dd48e317B728B64", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x6963480b05EB58f4d624B014ab92e9aD4d21df6D", "pausableHook": "0xD0dca420feFda68537695A8D887080eeF4030AF7", @@ -5041,7 +4974,7 @@ "interchainAccountIsm": "0x4d264424905535E97396Db83bd553D0d73A4EF9d", "interchainAccountRouter": "0x26A29486480BD74f9B830a9B8dB33cb43C40f496", "interchainGasPaymaster": "0x9c2214467Daf9e2e1F45b36d08ce0b9C65BFeA88", - "interchainSecurityModule": "0x75177B4869d1DD30A1717db94d999cBe5C228211", + "interchainSecurityModule": "0xAF6B4222dB762a21E5DB86e5a096A9f61ff8536B", "mailbox": "0x5bdADEAD721Eb4C4038fF7c989E3C7BbBA302435", "merkleTreeHook": "0x2684C6F89E901987E1FdB7649dC5Be0c57C61645", "pausableHook": "0xC8E323036AAFB4B4201e7B640E79C4Db285A3FC8", @@ -5105,7 +5038,7 @@ "interchainAccountIsm": "0x545E289B88c6d97b74eC0B96e308cae46Bf5f832", "interchainAccountRouter": "0x4ef363Da5bb09CC6aeA16973786963d0C8820778", "interchainGasPaymaster": "0x561BcA8D862536CD9C88f332C1A1Da0fC8F96e40", - "interchainSecurityModule": "0xc0d622A92C3B3497bD3eD082eA6f34d6e88cE4cC", + "interchainSecurityModule": "0x2ed1BED972266d0873734B9439A671A8aa84fEA2", "mailbox": "0x248aDe14C0489E20C9a7Fea5F86DBfC3702208eF", "merkleTreeHook": "0x9c2214467Daf9e2e1F45b36d08ce0b9C65BFeA88", "pausableHook": "0x2f536FB7a37bd817Af644072a904Ddc02Dae429f", @@ -5172,7 +5105,7 @@ "interchainAccountIsm": "0x60bB6D060393D3C206719A7bD61844cC82891cfB", "interchainAccountRouter": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", "interchainGasPaymaster": "0x61374178e45F65fF9D6252d017Cd580FC60B7654", - "interchainSecurityModule": "0x2b157b578A3944fEBd4860A40C9b48Ea00f9004d", + "interchainSecurityModule": "0x436add4839EEFA86c68fC3411ACdFA30CF522Ba9", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xEe08043cf22c80b27BF24d19999231dF4a3fC256", "pausableHook": "0x145566181A18E23bB6a8A3eC6D87765542A7F754", @@ -5237,7 +5170,7 @@ "interchainAccountIsm": "0xcdc31BA959DE8C035A03167ebAE1961208CDf172", "interchainAccountRouter": "0x349831a180eE4265008C5FFB9465Ff97c1CF0028", "interchainGasPaymaster": "0x6AA10748a036a49Cb290C0e12B77319b76792D5E", - "interchainSecurityModule": "0x2ae5aD85fc727D93e02f38433FDd8baBe12eAF67", + "interchainSecurityModule": "0x1f9bB28a9f79B9F1464DC894C39490acdB208bc8", "mailbox": "0xd9Cc2e652A162bb93173d1c44d46cd2c0bbDA59D", "merkleTreeHook": "0x2783D98CC073dbcDa90241C804d16982D3d75821", "pausableHook": "0x3bb2D0a828f7dD91bA786091F421f6d7cF376445", @@ -5307,7 +5240,7 @@ "interchainAccountIsm": "0x545E289B88c6d97b74eC0B96e308cae46Bf5f832", "interchainAccountRouter": "0x4ef363Da5bb09CC6aeA16973786963d0C8820778", "interchainGasPaymaster": "0xc6835e52C1b976F1ebC71Bc8919738E02849FdA9", - "interchainSecurityModule": "0xE01A20C71fd636553ca81FD7C7B565F130284C60", + "interchainSecurityModule": "0x5cA9D5bad6E75F5062bab703E2828E7D1D007FbD", "mailbox": "0x1c6f404800bA49Ed581af734eA0d25c0c7d017B2", "merkleTreeHook": "0xdAa1B65547fB969c9ff5678956AB2FF9771B883D", "pausableHook": "0xA0e0829DA397CcF55d5B779C31728f21Cb8219DF", @@ -5420,7 +5353,7 @@ "interchainAccountIsm": "0x8c794a781327b819416E7b67908f1D22397f1E67", "interchainAccountRouter": "0x16625230dD6cFe1B2bec3eCaEc7d43bA3A902CD6", "interchainGasPaymaster": "0x2b79328DA089E89A9E9c08732b56dd31F01011Db", - "interchainSecurityModule": "0x634593bb0DA3Bd90b96504846071BbBA41871DCA", + "interchainSecurityModule": "0x294127e3D5Cf438430e7ce6e7822aF07EB528A6b", "mailbox": "0x730f8a4128Fa8c53C777B62Baa1abeF94cAd34a9", "merkleTreeHook": "0x9c64f327F0140DeBd430aab3E2F1d6cbcA921227", "pausableHook": "0x2684C6F89E901987E1FdB7649dC5Be0c57C61645", @@ -5484,7 +5417,7 @@ "interchainAccountIsm": "0xE67Dc24970B482579923551Ede52BD35a2858989", "interchainAccountRouter": "0xDDE46032Baf4da13fDD79BF9dfbaA2749615C409", "interchainGasPaymaster": "0x2f536FB7a37bd817Af644072a904Ddc02Dae429f", - "interchainSecurityModule": "0x2c6cA9897bF9FE1d9877f8C7FB01D2a0BFcBc697", + "interchainSecurityModule": "0x3cB87AAa6CaA54cf14BB4D0063B79a7CDDeBCD1A", "mailbox": "0x2f0E57527Bb37E5E064EF243fad56CCE6241906c", "merkleTreeHook": "0xC8E323036AAFB4B4201e7B640E79C4Db285A3FC8", "pausableHook": "0xdAa1B65547fB969c9ff5678956AB2FF9771B883D", @@ -5548,7 +5481,7 @@ "interchainAccountIsm": "0x20a0A32a110362920597F72974E1E0d7e25cA20a", "interchainAccountRouter": "0x5b3EeADcc0E2d4284eA6816e2E503c24d30a9E54", "interchainGasPaymaster": "0x282629Af1A2f9b8e2c5Cbc54C35C7989f21950c6", - "interchainSecurityModule": "0xc2E273cc16C7291380fA01b02f8B150EcC819dB1", + "interchainSecurityModule": "0xBA5683c94dc66dCE0CB5489B15C2E9eed0C31924", "mailbox": "0x5C02157068a52cEcfc98EDb6115DE6134EcB4764", "merkleTreeHook": "0xf147bBD944C610F86DaE6C7668497D22932C1E4A", "pausableHook": "0x872Bd98057931c8809927c6dE2ef39738a80Eb0C", @@ -5615,7 +5548,7 @@ "interchainAccountIsm": "0xf40eE9FF75Fa34910b7C4C8d68d4850B3bD184D3", "interchainAccountRouter": "0xf6fB78dc009C1A4286c0E7d90C10c9E8906a62Ea", "interchainGasPaymaster": "0xDDE46032Baf4da13fDD79BF9dfbaA2749615C409", - "interchainSecurityModule": "0x79189dC0003c2f3836a0Ce09f1A24E97C1847bDB", + "interchainSecurityModule": "0x0Af112173cCd9375080DFd11c92Ccfb4E8eA0041", "mailbox": "0x65dCf8F6b3f6a0ECEdf3d0bdCB036AEa47A1d615", "merkleTreeHook": "0x8c794a781327b819416E7b67908f1D22397f1E67", "pausableHook": "0x4d264424905535E97396Db83bd553D0d73A4EF9d", @@ -5682,7 +5615,7 @@ "interchainAccountIsm": "0xd9Cc2e652A162bb93173d1c44d46cd2c0bbDA59D", "interchainAccountRouter": "0x7279B1e11142078b8dC9e69620200f4C84FB8aaa", "interchainGasPaymaster": "0x5ae1ECA065aC8ee92Ce98E584fc3CE43070020e7", - "interchainSecurityModule": "0x9826fe75fa80DCD5c2CD7fc68965D043B7Bc9501", + "interchainSecurityModule": "0x4cD5E75A30d225c1E4Bf1838F6F8F27E480F4083", "mailbox": "0x96D51cc3f7500d501bAeB1A2a62BB96fa03532F8", "merkleTreeHook": "0x086c3947F71BE98A0bDf4AB7239955e7542b0CbA", "pausableHook": "0x9C6e8d989ea7F212e679191BEb44139d83ac927a", @@ -5752,7 +5685,7 @@ "interchainAccountIsm": "0x8a733038eF4BbC314eE0F7595257D8d3799B6aA9", "interchainAccountRouter": "0xCE8260c1b5cF2fAD15bb4B6542716b050Fdf35c9", "interchainGasPaymaster": "0xa1c3884EbE24Cccb120B2E98a55f85140563aa4C", - "interchainSecurityModule": "0x5DA2EDFFfE00c7e1FFe74D884b3F1110289a866c", + "interchainSecurityModule": "0x6825B2925953128090597695694926077e1D42B0", "mailbox": "0x5e8a0fCc0D1DF583322943e01F02cB243e5300f6", "merkleTreeHook": "0x2f536FB7a37bd817Af644072a904Ddc02Dae429f", "pausableHook": "0xc6835e52C1b976F1ebC71Bc8919738E02849FdA9", @@ -5813,7 +5746,7 @@ "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", - "interchainSecurityModule": "0x5d38dbFde6F6bbAC6d3554FA7633192836152b13", + "interchainSecurityModule": "0x394F2bF9E6e6CCB0b26f3970ee4C178860d2e961", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", @@ -5883,7 +5816,7 @@ "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", - "interchainSecurityModule": "0x8F3cdCA573b922Ae0c651a88C312d8f5ebb9618E", + "interchainSecurityModule": "0xB16b9E925C454cDbf8b63832F9402b6484E26787", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", @@ -5944,7 +5877,7 @@ "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", - "interchainSecurityModule": "0xcE3996198496Df93EDebFf43491F938428d426cF", + "interchainSecurityModule": "0x07291a67F6EF475C0713495fd58339E39C7C85E3", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", @@ -6009,7 +5942,7 @@ "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", - "interchainSecurityModule": "0x0CC7b38573273A7e8dd075b57429d63F5fA3195D", + "interchainSecurityModule": "0x3B4B8A030D1f668e3ecBB4b5F6d2B5c1265D8951", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", @@ -6074,7 +6007,7 @@ "interchainAccountIsm": "0x25EAC2007b0D40E3f0AF112FD346412321038719", "interchainAccountRouter": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", "interchainGasPaymaster": "0x9534122Aae7978dB8f5f10dF4432233c53e820A1", - "interchainSecurityModule": "0x8afefC5Ed40fE2c8D0714e6C63aEaEbf1353898b", + "interchainSecurityModule": "0x343F3617495669bb5f01B3fF8c0EEeC14694Fc82", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x9eaaC366BFD70430cFee6E70265fefFf1CfC9E47", "pausableHook": "0x9eb56085DdbDA60aDf7d2B533AFeD90e38fC9666", @@ -6147,7 +6080,7 @@ "interchainAccountIsm": "0xfF26696DcDb6BbFD27e959b847D4f1399D5BcF64", "interchainAccountRouter": "0x4D50044335dc1d4D26c343AdeDf6E47808475Deb", "interchainGasPaymaster": "0x70EbA87Cd15616f32C736B3f3BdCfaeD0713a82B", - "interchainSecurityModule": "0x597E327C711B88913Af0154965e101a9656CA2E0", + "interchainSecurityModule": "0xBa99F47802E9Bf7A1d91ca8E1Da5939ad9507Faf", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xbb0AE51BCa526cF313b6a95BfaB020794af6C394", "pausableHook": "0x83475ca5bEB2Eaa59A2FF48a0544ebaa4a32c2de", @@ -6211,7 +6144,7 @@ "interchainAccountIsm": "0x31Bb27f6007C33acD1be83ACEd3164C60f8F7b13", "interchainAccountRouter": "0xEeb5a99a75585fe137c83E7b62b74f87264A5481", "interchainGasPaymaster": "0xb7C9307fE90B9AB093c6D3EdeE3259f5378D5f03", - "interchainSecurityModule": "0xa16516880d2A1bA31f834d32395b60477D050C19", + "interchainSecurityModule": "0x6a8c4A6FE157fbeB0A216B023065951eaF3e797D", "mailbox": "0x0dF25A2d59F03F039b56E90EdC5B89679Ace28Bc", "merkleTreeHook": "0xC88636fFdFAc7cb87b7A76310B7a62AF0A000595", "pausableHook": "0x2AF32cF8e3Cf42d221eDa0c843818fA5ee129E27", @@ -6276,7 +6209,7 @@ "interchainAccountIsm": "0x28291a7062afA569104bEd52F7AcCA3dD2FafD11", "interchainAccountRouter": "0xe9E3444DDD80c50276c0Fcf316026f6d7fEc2c47", "interchainGasPaymaster": "0x25EAC2007b0D40E3f0AF112FD346412321038719", - "interchainSecurityModule": "0xD747187Ac977F658807844AA3bD5807e06AfED08", + "interchainSecurityModule": "0x6EcFFECAa23368CdcFcFb797a136333179c32e9e", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x5C02157068a52cEcfc98EDb6115DE6134EcB4764", "pausableHook": "0x99fEFc1119E86Ee0153eb887cF8E8ab2d92A16e8", @@ -6344,7 +6277,7 @@ "interchainAccountIsm": "0x027eFD1695941969435AA640542B690044dF7E06", "interchainAccountRouter": "0x65F1343AC23D4fF48bf6c7E0c55872d245397567", "interchainGasPaymaster": "0x28291a7062afA569104bEd52F7AcCA3dD2FafD11", - "interchainSecurityModule": "0x9C575B080373FBFA82954E32809fA2eC31e68402", + "interchainSecurityModule": "0xC2a8832Aa1a9cE1dbf3DF228Dc1919e6e42622EA", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xcd90D49b046772F710250b9119117169CB2e4D8b", "pausableHook": "0x7CE76f5f0C469bBB4cd7Ea6EbabB54437A093127", @@ -6529,7 +6462,7 @@ "interchainAccountIsm": "0xf9609bB22847e0DB5F6fB8f95b84D25A19b46ac5", "interchainAccountRouter": "0x2b6d3F7d28B5EC8C3C028fBCAdcf774D9709Dd29", "interchainGasPaymaster": "0xFb7D175d6F53800D68D32C3Fe1416807A394cC24", - "interchainSecurityModule": "0x90168597A6d225cc7F1A073712c5042868c6a048", + "interchainSecurityModule": "0xb3edE81D4A2d5dEb1b604F5DA664DC47964757d9", "mailbox": "0x473884010F0C1742DA8Ad01E7E295624B931076b", "merkleTreeHook": "0xdA629E1B79e3420ECd1e80571aDd6a4a3b13AE79", "pausableHook": "0xe93f2f409ad8B5000431D234472973fe848dcBEC", @@ -6596,7 +6529,7 @@ "interchainAccountIsm": "0xF457D831d9F55e87B2F0b35AD6D033fd6b4181Ed", "interchainAccountRouter": "0x021D2810a758c833080DEc2F1Fa8F571Aae97D45", "interchainGasPaymaster": "0xc0C2dB448fC2c84213394Fcb93a3C467e50ECa9E", - "interchainSecurityModule": "0x9A0DC6Af3B473be380D5aF928Df3d9C32E979cFB", + "interchainSecurityModule": "0xFD5B169AdBC9c5851ad1C69dB79C1A29e38d1f88", "mailbox": "0x7f50C5776722630a0024fAE05fDe8b47571D7B39", "merkleTreeHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", "pausableHook": "0x48C427782Bc1e9ecE406b3e277481b28ABcBdf03", @@ -6666,7 +6599,7 @@ "interchainAccountIsm": "0x93D41E41cA545a35A81d11b08D2eE8b852C768df", "interchainAccountRouter": "0xc2466492C451E1AE49d8C874bB9f89293Aaad59b", "interchainGasPaymaster": "0xDf178647caB5e0222F4B53C57274FD2A03BEaed6", - "interchainSecurityModule": "0xecbe3b13857248e7838d2f943489093be6322f28", + "interchainSecurityModule": "0x36b420b9091F010aB6571516B2DbF9E32BD8750e", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xDc1508844B99C606E16C2Ae87f33c373edD4B0F6", "pausableHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", @@ -6734,7 +6667,7 @@ "interchainAccountIsm": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "interchainAccountRouter": "0x93D41E41cA545a35A81d11b08D2eE8b852C768df", "interchainGasPaymaster": "0xDf178647caB5e0222F4B53C57274FD2A03BEaed6", - "interchainSecurityModule": "0x7916F47AE2F97e769F911a626ad33E67985db739", + "interchainSecurityModule": "0xFA75e09D3eDD644F9B0Fb42f384B3A91A9D2C3D5", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xDc1508844B99C606E16C2Ae87f33c373edD4B0F6", "pausableHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", @@ -6804,7 +6737,7 @@ "interchainAccountIsm": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "interchainAccountRouter": "0x93D41E41cA545a35A81d11b08D2eE8b852C768df", "interchainGasPaymaster": "0xDf178647caB5e0222F4B53C57274FD2A03BEaed6", - "interchainSecurityModule": "0x305EAe04133f504288fc52869Bb1403288f1D51D", + "interchainSecurityModule": "0x95F67EBdcf5b7a3DA25124ACfB8B45e89cA13871", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xDc1508844B99C606E16C2Ae87f33c373edD4B0F6", "pausableHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", @@ -6874,7 +6807,7 @@ "interchainAccountIsm": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "interchainAccountRouter": "0x93D41E41cA545a35A81d11b08D2eE8b852C768df", "interchainGasPaymaster": "0xDf178647caB5e0222F4B53C57274FD2A03BEaed6", - "interchainSecurityModule": "0xD064d98b8ff331cd3F809C3F88b985198fB4Ad61", + "interchainSecurityModule": "0xaC4b20f1C73bc1d4dA7f453C434F34d559a8eE55", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xDc1508844B99C606E16C2Ae87f33c373edD4B0F6", "pausableHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", @@ -6938,7 +6871,7 @@ "interchainAccountIsm": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "interchainAccountRouter": "0x93D41E41cA545a35A81d11b08D2eE8b852C768df", "interchainGasPaymaster": "0xDf178647caB5e0222F4B53C57274FD2A03BEaed6", - "interchainSecurityModule": "0x987149f27F49F7b6AE82906fe846ABE1bD7b8889", + "interchainSecurityModule": "0x01cD09F5c40286c1894EBFb7b3eC98f2aE4026FA", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xDc1508844B99C606E16C2Ae87f33c373edD4B0F6", "pausableHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", @@ -7003,7 +6936,7 @@ "interchainAccountIsm": "0x60515f328B2c55Df63f456D9D839a0082892dEf8", "interchainAccountRouter": "0xF457D831d9F55e87B2F0b35AD6D033fd6b4181Ed", "interchainGasPaymaster": "0xc0C2dB448fC2c84213394Fcb93a3C467e50ECa9E", - "interchainSecurityModule": "0x28cE203C1037a59bFC4628a31a12F2b32A159581", + "interchainSecurityModule": "0x123D1ba7ec942f13eE1F05FEe6cd6FFAE3CD3Dcf", "mailbox": "0x7f50C5776722630a0024fAE05fDe8b47571D7B39", "merkleTreeHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", "pausableHook": "0x48C427782Bc1e9ecE406b3e277481b28ABcBdf03", @@ -7071,7 +7004,7 @@ "interchainAccountIsm": "0xd64d126941EaC2Cf53e0E4E8146cC70449b60D73", "interchainAccountRouter": "0x1A4F09A615aA4a35E5a146DC2fa19975bebF21A5", "interchainGasPaymaster": "0x3cECBa60A580dE20CC57D87528953a00f4ED99EA", - "interchainSecurityModule": "0x13BB7A27bc7a14de29555EbE82924e81cC4fF3E8", + "interchainSecurityModule": "0x5e74434e07B64F6025Cbac2f423e29Fd92c12C6B", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x1c6f404800bA49Ed581af734eA0d25c0c7d017B2", "pausableHook": "0x9e8b689e83d929cb8c2d9166E55319a4e6aA83B7", @@ -7132,7 +7065,7 @@ "interchainAccountIsm": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "interchainAccountRouter": "0x93D41E41cA545a35A81d11b08D2eE8b852C768df", "interchainGasPaymaster": "0xDf178647caB5e0222F4B53C57274FD2A03BEaed6", - "interchainSecurityModule": "0x0EA9c2373Ab2c4704e02Ce2BCf81d219B9d8A118", + "interchainSecurityModule": "0xD329D039e51AFd7A6166E51243A56B482B150Afd", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xDc1508844B99C606E16C2Ae87f33c373edD4B0F6", "pausableHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", @@ -7197,7 +7130,7 @@ "interchainAccountIsm": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "interchainAccountRouter": "0x93D41E41cA545a35A81d11b08D2eE8b852C768df", "interchainGasPaymaster": "0xDf178647caB5e0222F4B53C57274FD2A03BEaed6", - "interchainSecurityModule": "0xFd94990aEdc5bF16CD83b6D5B617aD3CE9cC7faD", + "interchainSecurityModule": "0x73c4926837bD5Ce7D07Bd9Ad011469c05bE290F6", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xDc1508844B99C606E16C2Ae87f33c373edD4B0F6", "pausableHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", @@ -7264,7 +7197,7 @@ "interchainAccountIsm": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "interchainAccountRouter": "0xF457D831d9F55e87B2F0b35AD6D033fd6b4181Ed", "interchainGasPaymaster": "0xDf178647caB5e0222F4B53C57274FD2A03BEaed6", - "interchainSecurityModule": "0x5dca5E261d0c592AAcD07a276DA668bfD8D8F304", + "interchainSecurityModule": "0xF25c3035eB5fb5207F64Ca4704eE40aab9720bCA", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xDc1508844B99C606E16C2Ae87f33c373edD4B0F6", "pausableHook": "0xA8A311B69f688c1D9928259D872C31ca0d473642", @@ -7377,7 +7310,7 @@ "interchainAccountIsm": "0xd64d126941EaC2Cf53e0E4E8146cC70449b60D73", "interchainAccountRouter": "0x1A4F09A615aA4a35E5a146DC2fa19975bebF21A5", "interchainGasPaymaster": "0x3cECBa60A580dE20CC57D87528953a00f4ED99EA", - "interchainSecurityModule": "0x5180E538F1AF978436213cf1c410eD7057Dd3EbA", + "interchainSecurityModule": "0x9b4193BB8B62dD285DBabE6Da17856b29c4FF446", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x1c6f404800bA49Ed581af734eA0d25c0c7d017B2", "pausableHook": "0x9e8b689e83d929cb8c2d9166E55319a4e6aA83B7", @@ -7444,7 +7377,7 @@ "interchainAccountIsm": "0x2351FBe24C1212F253b7a300ff0cBCFd97952a19", "interchainAccountRouter": "0xC5f2c60073DCAA9D157C45d5B017D639dF9C5CeB", "interchainGasPaymaster": "0xc2466492C451E1AE49d8C874bB9f89293Aaad59b", - "interchainSecurityModule": "0xAea2203e00B20c6beebCc7A8f07ceD58bB0fE6C2", + "interchainSecurityModule": "0x18d58C1CAD950c7914E2E4395e1f1f9167B11DE1", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x7B032cBB00AD7438E802A66D8b64761A06E5df22", "pausableHook": "0x3881c3e945CBB89ae67c43E82f570baDF1c6EA94", @@ -7508,7 +7441,7 @@ "interchainAccountIsm": "0x8F23872dAb3B166cef411EeB6C391Ff6Ce419532", "interchainAccountRouter": "0xb201817dFdd822B75Fa9b595457E6Ee466a7C187", "interchainGasPaymaster": "0xA9D06082F4AA449D95b49D85F27fdC0cFb491d4b", - "interchainSecurityModule": "0x412B92216eE77fc3F16Fb84ad63C5E93c16F5577", + "interchainSecurityModule": "0xba29059bdfE0d7cd12017Fcb6E8eD133C472990b", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0x11b76D93a9D39Eb51F54eBf5566308640cDe882b", "pausableHook": "0x60515f328B2c55Df63f456D9D839a0082892dEf8", @@ -7572,7 +7505,7 @@ "interchainAccountIsm": "0x7937CB2886f01F38210506491A69B0D107Ea0ad9", "interchainAccountRouter": "0xc31B1E6c8E706cF40842C3d728985Cd2f85413eD", "interchainGasPaymaster": "0xc2466492C451E1AE49d8C874bB9f89293Aaad59b", - "interchainSecurityModule": "0xda30CE1D0bd69262C23a4066dB5204504e68C208", + "interchainSecurityModule": "0xB1877578A1Ae5A109e0b596f9c395739ea954561", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x7B032cBB00AD7438E802A66D8b64761A06E5df22", "pausableHook": "0x3881c3e945CBB89ae67c43E82f570baDF1c6EA94", @@ -7637,7 +7570,7 @@ "interchainAccountIsm": "0x9629c28990F11c31735765A6FD59E1E1bC197DbD", "interchainAccountRouter": "0x2351FBe24C1212F253b7a300ff0cBCFd97952a19", "interchainGasPaymaster": "0xc2466492C451E1AE49d8C874bB9f89293Aaad59b", - "interchainSecurityModule": "0xcD46a465562580AfDC21360dd7692BFbf39223CF", + "interchainSecurityModule": "0x9201A273e99bdCAB57c44f58131268f4980F97f8", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x7B032cBB00AD7438E802A66D8b64761A06E5df22", "pausableHook": "0x3881c3e945CBB89ae67c43E82f570baDF1c6EA94", @@ -7705,7 +7638,7 @@ "interchainAccountIsm": "0xE350143242a2F7962F23D71ee9Dd98f6e86D1772", "interchainAccountRouter": "0x5B24EE24049582fF74c1d311d72c70bA5B76a554", "interchainGasPaymaster": "0x8F23872dAb3B166cef411EeB6C391Ff6Ce419532", - "interchainSecurityModule": "0x4F8D3037a5d63f34a2EC24950019a225fa0D7120", + "interchainSecurityModule": "0x725f03eB3B2ee426d9e7e298485e6Ef9095ea22a", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x7B032cBB00AD7438E802A66D8b64761A06E5df22", "pausableHook": "0x3881c3e945CBB89ae67c43E82f570baDF1c6EA94", @@ -7770,7 +7703,7 @@ "domainRoutingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", "fallbackRoutingHook": "0xb2674E213019972f937CCFc5e23BF963D915809e", "interchainGasPaymaster": "0x93D41E41cA545a35A81d11b08D2eE8b852C768df", - "interchainSecurityModule": "0x0A5A1985d2b35381A9368c300516B22AB4050b87", + "interchainSecurityModule": "0x876C9c47379A0184e00EB7FdECBC27a9042EbD6C", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x67F36550b73B731e5b2FC44E4F8f250d89c87bD6", "pausableHook": "0xD8aF449f8fEFbA2064863DCE5aC248F8B232635F", @@ -7891,7 +7824,7 @@ "interchainAccountIsm": "0xE350143242a2F7962F23D71ee9Dd98f6e86D1772", "interchainAccountRouter": "0x5B24EE24049582fF74c1d311d72c70bA5B76a554", "interchainGasPaymaster": "0x8F23872dAb3B166cef411EeB6C391Ff6Ce419532", - "interchainSecurityModule": "0x7F632efCa151c1062d997488dABb70FDDa82E051", + "interchainSecurityModule": "0xE62144D2A6c85f52263595Ad11eF4265E7038be2", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xf0F937943Cd6D2a5D02b7f96C9Dd9e04AB633813", "pausableHook": "0xff72A726Ce261846f2dF6F32113e514b5Ddb0E37", @@ -7955,7 +7888,7 @@ "interchainAccountIsm": "0xD84981Ecd4c411A86E1Ccda77F944c8f3D9737ab", "interchainAccountRouter": "0xF16E63B42Df7f2676B373979120BBf7e6298F473", "interchainGasPaymaster": "0x9629c28990F11c31735765A6FD59E1E1bC197DbD", - "interchainSecurityModule": "0x007001DB696c6EB1075C60475aea97b3d0eEA7BD", + "interchainSecurityModule": "0xc9258a12d5dfbE6CC74B8da9396961f338AbEbEE", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x021D2810a758c833080DEc2F1Fa8F571Aae97D45", "pausableHook": "0x46008F5971eFb16e6c354Ef993EA021B489bc055", @@ -8020,7 +7953,7 @@ "interchainAccountIsm": "0xD84981Ecd4c411A86E1Ccda77F944c8f3D9737ab", "interchainAccountRouter": "0xF16E63B42Df7f2676B373979120BBf7e6298F473", "interchainGasPaymaster": "0x9629c28990F11c31735765A6FD59E1E1bC197DbD", - "interchainSecurityModule": "0x18cDdCb58f1AEBd4eE676bA9098b4e9d584E6b38", + "interchainSecurityModule": "0x8737665978597195590cE01979309BbAF5243a1E", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x021D2810a758c833080DEc2F1Fa8F571Aae97D45", "pausableHook": "0x46008F5971eFb16e6c354Ef993EA021B489bc055", @@ -8125,7 +8058,7 @@ "interchainAccountIsm": "0x30a539E2E2d09FB4e68661B1EDD70D266211602a", "interchainAccountRouter": "0xB2b0A80b2fa3fC9aB1564A4FaF013d4D6084B325", "interchainGasPaymaster": "0x7Ce3a48cd9FD80004d95b088760bD05bA86C1f7b", - "interchainSecurityModule": "0xc85D1d505bFCf97d7D17ea53dbf0032c315F972F", + "interchainSecurityModule": "0x297550Ec70C734ce9BC9d0194e349BCcf250E1F9", "mailbox": "0x7f50C5776722630a0024fAE05fDe8b47571D7B39", "merkleTreeHook": "0x8F23872dAb3B166cef411EeB6C391Ff6Ce419532", "pausableHook": "0x2351FBe24C1212F253b7a300ff0cBCFd97952a19", @@ -8190,7 +8123,7 @@ "interchainAccountIsm": "0xc261Bd2BD995d3D0026e918cBFD44b0Cc5416a57", "interchainAccountRouter": "0xf4035357EB3e3B48E498FA6e1207892f615A2c2f", "interchainGasPaymaster": "0x30a539E2E2d09FB4e68661B1EDD70D266211602a", - "interchainSecurityModule": "0x09fE300051612f0414D5920004665842d8578bD1", + "interchainSecurityModule": "0x9fb2C92266B20233CCC0f92EB358186696D5601f", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x6e1B9f776bd415d7cC3C7458A5f0d801016918f8", "pausableHook": "0x63012EE26bda8E5D1b96218778Eaf2492E553469", @@ -8254,7 +8187,7 @@ "interchainAccountIsm": "0xcDD89f19b2d00DCB9510BB3fBd5eCeCa761fe5Ab", "interchainAccountRouter": "0x7947b7Fe737B4bd1D3387153f32148974066E591", "interchainGasPaymaster": "0x5cD695ADCB156589cde501822C314bFD74398cA1", - "interchainSecurityModule": "0x341A1DE1d7B0396878b2a60d7ac3B94a71494b61", + "interchainSecurityModule": "0xF243582e9a0F3b2D45f033CC8Bb9FBA209922309", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xa377b8269e0A47cdd2fD5AAeAe860b45623c6d82", "pausableHook": "0x3E57E4908FB4Ac05e9928feE2Ffd78f59692317C", @@ -8322,7 +8255,7 @@ "interchainAccountIsm": "0xcDD89f19b2d00DCB9510BB3fBd5eCeCa761fe5Ab", "interchainAccountRouter": "0x7947b7Fe737B4bd1D3387153f32148974066E591", "interchainGasPaymaster": "0x5cD695ADCB156589cde501822C314bFD74398cA1", - "interchainSecurityModule": "0x1577868e114BE07c9d8E43f673a9eCB1DAc40891", + "interchainSecurityModule": "0x169DD2263dB5Cd3B9cE71c3B67c613edCd403967", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xa377b8269e0A47cdd2fD5AAeAe860b45623c6d82", "pausableHook": "0x3E57E4908FB4Ac05e9928feE2Ffd78f59692317C", @@ -8447,7 +8380,7 @@ "interchainAccountIsm": "0xcDD89f19b2d00DCB9510BB3fBd5eCeCa761fe5Ab", "interchainAccountRouter": "0x7947b7Fe737B4bd1D3387153f32148974066E591", "interchainGasPaymaster": "0x5cD695ADCB156589cde501822C314bFD74398cA1", - "interchainSecurityModule": "0x341A1DE1d7B0396878b2a60d7ac3B94a71494b61", + "interchainSecurityModule": "0x94ae2312C21369c1F7a2f566f41c571A8794B561", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xa377b8269e0A47cdd2fD5AAeAe860b45623c6d82", "pausableHook": "0x3E57E4908FB4Ac05e9928feE2Ffd78f59692317C", @@ -8511,7 +8444,7 @@ "interchainAccountIsm": "0x72246331d057741008751AB3976a8297Ce7267Bc", "interchainAccountRouter": "0xf303B04d9ad21dAe2658Cf302478A424e0B45368", "interchainGasPaymaster": "0x8d7E604460E1133ebB91513a6D1024f3A3ca17F9", - "interchainSecurityModule": "0xc3D86B34A064471e7e92BB172Dd58E265dfc560C", + "interchainSecurityModule": "0x5eBf236C500972ee837eFfCBb491d58ecd6aF502", "mailbox": "0xF767D698c510FE5E53b46BA6Fd1174F5271e390A", "merkleTreeHook": "0xB2b0A80b2fa3fC9aB1564A4FaF013d4D6084B325", "pausableHook": "0x1e4dE25C3b07c8DF66D4c193693d8B5f3b431d51", @@ -8575,7 +8508,7 @@ "interchainAccountIsm": "0xbE58F200ffca4e1cE4D2F4541E94Ae18370fC405", "interchainAccountRouter": "0xdf4aA3905e0391C7763e33CB6A08fFa97221D49B", "interchainGasPaymaster": "0xEa2Bcee14eA30bbBe3018E5E7829F963230F71C3", - "interchainSecurityModule": "0x5ddd9a4284FD2bC913f780016b8F2feA8f903319", + "interchainSecurityModule": "0x36BdC1EE4c4ac1E8bDB6B4Da41bC21efF033A2F4", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x3862A9B1aCd89245a59002C2a08658EC1d5690E3", "pausableHook": "0x8da1aE5A1fA3883c1c12b46270989EAC0EE7BA78", @@ -8636,7 +8569,7 @@ "interchainAccountIsm": "0x4A91738390a3D55CB27c2863e8950c9cD1b89d0e", "interchainAccountRouter": "0x284226F651eb5cbd696365BC27d333028FCc5D54", "interchainGasPaymaster": "0x4eB0d97B48711950ecB01871125c4523939c6Fce", - "interchainSecurityModule": "0x2dc7D1187c30E83B276cC398D84f0f81D727Ef2F", + "interchainSecurityModule": "0x9b5B8ba44bA66199b7029e7F75794af76F4B8f0b", "mailbox": "0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7", "merkleTreeHook": "0xB2b0A80b2fa3fC9aB1564A4FaF013d4D6084B325", "pausableHook": "0x1e4dE25C3b07c8DF66D4c193693d8B5f3b431d51", @@ -8700,7 +8633,7 @@ "interchainAccountIsm": "0x72246331d057741008751AB3976a8297Ce7267Bc", "interchainAccountRouter": "0xf303B04d9ad21dAe2658Cf302478A424e0B45368", "interchainGasPaymaster": "0xc261Bd2BD995d3D0026e918cBFD44b0Cc5416a57", - "interchainSecurityModule": "0x26150693b56A443f1E78F4828EE4a01710B48563", + "interchainSecurityModule": "0x2cB87354746a55F29bCAaE80736aED3f5087bf5e", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x25C87e735021F72d8728438C2130b02E3141f2cb", "pausableHook": "0x51545389E04c2Ac07d98A40b85d29B480a2AF6ce", @@ -8814,7 +8747,7 @@ "interchainAccountIsm": "0xA9Adb480F10547f10202173a49b7F52116304476", "interchainAccountRouter": "0xA697222b77cDe62A8C47E627d5A1c49A87018236", "interchainGasPaymaster": "0x8452363d5c78bf95538614441Dc8B465e03A89ca", - "interchainSecurityModule": "0xeAdfBCDE8BE901e11151B8c84E6f8BE67BA7765a", + "interchainSecurityModule": "0x44979aBe91F80a33109f2904f5A767B8c99fd0B4", "mailbox": "0x398633D19f4371e1DB5a8EFE90468eB70B1176AA", "merkleTreeHook": "0x6D48135b7584E8Bf828B6e23110Bc0Da4252704f", "pausableHook": "0x2FF6cf2651fec512D0618E33c9d1374aaCd8b310", @@ -8878,7 +8811,7 @@ "interchainAccountIsm": "0x5B7a808CaA2C3F1378B07cDd46eB8ccA52F67e3B", "interchainAccountRouter": "0xBCD18636e5876DFd7AAb5F2B2a5Eb5ca168BA1d8", "interchainGasPaymaster": "0xE911e75CECe0eF01df3ceD96BCd362941AE474D4", - "interchainSecurityModule": "0x0Cb57b789C6CF954F422F043E351cc6Ba8447aE1", + "interchainSecurityModule": "0x99a3482265C7f7309E7375846a18D226Ce83C045", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xcDD89f19b2d00DCB9510BB3fBd5eCeCa761fe5Ab", "pausableHook": "0x72246331d057741008751AB3976a8297Ce7267Bc", @@ -8942,7 +8875,7 @@ "interchainAccountIsm": "0x284226F651eb5cbd696365BC27d333028FCc5D54", "interchainAccountRouter": "0x64F077915B41479901a23Aa798B30701589C5063", "interchainGasPaymaster": "0x5887BDA66EC9e854b0da6BFFe423511e69d327DC", - "interchainSecurityModule": "0xf21392F755d016C6C26e095aB916d82f5b4153cB", + "interchainSecurityModule": "0xadbEea48a2DE87834ffe1b0AC22CA800912C53F5", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0x72246331d057741008751AB3976a8297Ce7267Bc", "pausableHook": "0x1A41a365A693b6A7aED1a46316097d290f569F22", @@ -9015,7 +8948,7 @@ "interchainAccountIsm": "0x5B7a808CaA2C3F1378B07cDd46eB8ccA52F67e3B", "interchainAccountRouter": "0xBCD18636e5876DFd7AAb5F2B2a5Eb5ca168BA1d8", "interchainGasPaymaster": "0xE911e75CECe0eF01df3ceD96BCd362941AE474D4", - "interchainSecurityModule": "0x0Cb57b789C6CF954F422F043E351cc6Ba8447aE1", + "interchainSecurityModule": "0x99a3482265C7f7309E7375846a18D226Ce83C045", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xcDD89f19b2d00DCB9510BB3fBd5eCeCa761fe5Ab", "pausableHook": "0x72246331d057741008751AB3976a8297Ce7267Bc", @@ -9079,7 +9012,7 @@ "interchainAccountIsm": "0x5244d3359065C883BDfeEEff5329DE38c0Bd227e", "interchainAccountRouter": "0x89Ebf977E83087959aD78e5372F4AF15DcdC8143", "interchainGasPaymaster": "0xE911e75CECe0eF01df3ceD96BCd362941AE474D4", - "interchainSecurityModule": "0x0Cb57b789C6CF954F422F043E351cc6Ba8447aE1", + "interchainSecurityModule": "0x0A4247f2837733d62bd1B2369eb2E25f5fECA681", "mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E", "merkleTreeHook": "0xcDD89f19b2d00DCB9510BB3fBd5eCeCa761fe5Ab", "pausableHook": "0x72246331d057741008751AB3976a8297Ce7267Bc", diff --git a/rust/main/config/test_sealevel_config.json b/rust/main/config/test_sealevel_config.json index 7b7cc1c8a7e..1a7745b9660 100644 --- a/rust/main/config/test_sealevel_config.json +++ b/rust/main/config/test_sealevel_config.json @@ -11,6 +11,7 @@ "protocol": "sealevel", "blocks": { "reorgPeriod": 0, + "estimateBlockTime": 0.4, "confirmations": 0 }, "rpcUrls": [ @@ -34,6 +35,7 @@ "protocol": "sealevel", "blocks": { "reorgPeriod": 0, + "estimateBlockTime": 0.4, "confirmations": 0 }, "rpcUrls": [ diff --git a/rust/main/config/testnet_config.json b/rust/main/config/testnet_config.json index e7667d2f276..ce7fa096344 100644 --- a/rust/main/config/testnet_config.json +++ b/rust/main/config/testnet_config.json @@ -31,7 +31,7 @@ "interchainAccountIsm": "0x6895d3916B94b386fAA6ec9276756e16dAe7480E", "interchainAccountRouter": "0xEbA64c8a9b4a61a9210d5fe7E4375380999C821b", "interchainGasPaymaster": "0x44769b0f4a6f01339e131a691cc2eebbb519d297", - "interchainSecurityModule": "0x7E82A6d3713D04218BD701C38d7AA054C8294dE0", + "interchainSecurityModule": "0xCF55470D7616D3257B1516703584B5d4280da4ED", "isTestnet": true, "mailbox": "0xEf9F292fcEBC3848bF4bB92a96a04F9ECBb78E59", "merkleTreeHook": "0x221FA9CBaFcd6c1C3d206571Cf4427703e023FFa", @@ -94,7 +94,7 @@ "from": 49690504 }, "interchainGasPaymaster": "0xc756cFc1b7d0d4646589EDf10eD54b201237F5e8", - "interchainSecurityModule": "0xa1bc890b46b5999fadB0E828B3Ff4D98e58e4286", + "interchainSecurityModule": "0x43C997061a5C222EFdCbf16426DdA0B6B32e7158", "isTestnet": true, "mailbox": "0x598facE78a4302f11E3de0bee1894Da0b2Cb71F8", "merkleTreeHook": "0xAD34A66Bf6dB18E858F6B686557075568c6E031C", @@ -131,7 +131,10 @@ "gasCurrencyCoinGeckoId": "ethereum", "interchainAccountIsm": "0xaec6382e1e16Ee12DBEf0e7EA5ADa51217813Fc3", "interchainAccountRouter": "0x20cC3a33C49fa13627303669edf2DcA7F1E76a50", - "timelockController": "0x0000000000000000000000000000000000000000" + "timelockController": "0x0000000000000000000000000000000000000000", + "transactionOverrides": { + "gasPriceCap": 100000000000 + } }, "basesepolia": { "aggregationHook": "0xccA408a6A9A6dc405C3278647421eb4317466943", @@ -162,7 +165,7 @@ "from": 13851043 }, "interchainGasPaymaster": "0x28B02B97a850872C4D33C3E024fab6499ad96564", - "interchainSecurityModule": "0x29F8734339C8230C3f149818555c9c5Fd0c4950C", + "interchainSecurityModule": "0x2820C18D0C5F90d3eC6850b9808BCaAE102C6F93", "isTestnet": true, "mailbox": "0x6966b0E55883d49BFB24539356a2f8A673E02039", "merkleTreeHook": "0x86fb9F1c124fB20ff130C41a79a432F770f67AFD", @@ -198,7 +201,10 @@ "gasCurrencyCoinGeckoId": "ethereum", "interchainAccountIsm": "0xDa5177080f7fC5d9255eB32cC64B9b4e5136A716", "interchainAccountRouter": "0xd876C01aB40e8cE42Db417fBC79c726d45504dE4", - "timelockController": "0x0000000000000000000000000000000000000000" + "timelockController": "0x0000000000000000000000000000000000000000", + "transactionOverrides": { + "gasPriceCap": 100000000000 + } }, "bsctestnet": { "aggregationHook": "0x3d675bB93250Ab7603F40cbb9194bae210784627", @@ -232,7 +238,7 @@ "interchainAccountIsm": "0xa9D8Ec959F34272B1a56D09AF00eeee58970d3AE", "interchainAccountRouter": "0x6d2B3e304E58c2a19f1492E7cf15CaF63Ce6e0d2", "interchainGasPaymaster": "0x0dD20e410bdB95404f71c5a4e7Fa67B892A5f949", - "interchainSecurityModule": "0xb01485e8B30ae7aCbd0EEB59655327daf6a53362", + "interchainSecurityModule": "0x3778EeeAf9492451d9031AfDBB4004626cbccE1B", "isTestnet": true, "mailbox": "0xF9F6F5646F478d5ab4e20B0F910C92F1CCC9Cc6D", "merkleTreeHook": "0xc6cbF39A747f5E28d1bDc8D9dfDAb2960Abd5A8f", @@ -269,7 +275,8 @@ "staticMessageIdWeightedMultisigIsmFactory": "0xaa80d23299861b7D7ab1bE665579029Ed9137BD1", "gasCurrencyCoinGeckoId": "binancecoin", "transactionOverrides": { - "gasPrice": 8000000000 + "gasPrice": 1000000000, + "gasPriceCap": 100000000000 } }, "connextsepolia": { @@ -301,7 +308,7 @@ "from": 4950 }, "interchainGasPaymaster": "0xeC7eb4196Bd601DEa7585A744FbFB4CF11278450", - "interchainSecurityModule": "0x144951Ca837cf460CF54d1205288b4F92a3C836A", + "interchainSecurityModule": "0xeE511CEae36cd8A8Adc531E72D9803ba37a85E18", "isTestnet": true, "mailbox": "0x6966b0E55883d49BFB24539356a2f8A673E02039", "merkleTreeHook": "0x4926a10788306D84202A2aDbd290b7743146Cc17", @@ -402,7 +409,7 @@ "from": 1606754 }, "interchainGasPaymaster": "0x28B02B97a850872C4D33C3E024fab6499ad96564", - "interchainSecurityModule": "0x0D8937C0d74F11320C9f10c20CbF75A8C0b64439", + "interchainSecurityModule": "0x7d2e30B43aA1582Bda31C58985c6743230e8ac62", "isTestnet": true, "mailbox": "0x6966b0E55883d49BFB24539356a2f8A673E02039", "merkleTreeHook": "0x86fb9F1c124fB20ff130C41a79a432F770f67AFD", @@ -468,7 +475,7 @@ "interchainAccountIsm": "0xfaB4815BDC5c60c6bD625459C8577aFdD79D9311", "interchainAccountRouter": "0xeEF6933122894fF217a7dd07510b3D64b747e29b", "interchainGasPaymaster": "0x6895d3916B94b386fAA6ec9276756e16dAe7480E", - "interchainSecurityModule": "0xCa6830f22d4bC97B05f828992E466B8d60c6DFeA", + "interchainSecurityModule": "0x3aeBc0206666CFcE29d6E678ff9c1e0D053e8077", "isTestnet": true, "mailbox": "0x5b6CFf85442B851A8e6eaBd2A4E4507B5135B3B0", "merkleTreeHook": "0x9ff6ac3dAf63103620BBf76136eA1AFf43c2F612", @@ -534,7 +541,7 @@ "from": 1543015 }, "interchainGasPaymaster": "0x5CBf4e70448Ed46c2616b04e9ebc72D29FF0cfA9", - "interchainSecurityModule": "0x74E4CC9ef963a63775013039b43615b3A696398b", + "interchainSecurityModule": "0x86B0AD09B05840Bcdb00ff4323f8Eb39cfc7e732", "isTestnet": true, "mailbox": "0x46f7C5D896bbeC89bE1B19e4485e59b4Be49e9Cc", "merkleTreeHook": "0x98AAE089CaD930C64a76dD2247a2aC5773a4B8cE", @@ -599,7 +606,7 @@ "from": 15833917 }, "interchainGasPaymaster": "0x28B02B97a850872C4D33C3E024fab6499ad96564", - "interchainSecurityModule": "0x380bFe84E44f790dB69A4021466414819dE71FB4", + "interchainSecurityModule": "0x62C1f1A1Fcc0C24eC1425c55317843259387EAb0", "isTestnet": true, "mailbox": "0x6966b0E55883d49BFB24539356a2f8A673E02039", "merkleTreeHook": "0x86fb9F1c124fB20ff130C41a79a432F770f67AFD", @@ -631,7 +638,10 @@ "staticMessageIdWeightedMultisigIsmFactory": "0x7c5B5bdA7F1d1F70A6678ABb4d894612Fc76498F", "interchainAccountIsm": "0xA7FA26ef3Ea88CD696779735AC9591E01146DA38", "interchainAccountRouter": "0x3F100cBBE5FD5466BdB4B3a15Ac226957e7965Ad", - "timelockController": "0x0000000000000000000000000000000000000000" + "timelockController": "0x0000000000000000000000000000000000000000", + "transactionOverrides": { + "gasPriceCap": 100000000000 + } }, "plumetestnet": { "aggregationHook": "0x31dF0EEE7Dc7565665468698a0da221225619a1B", @@ -727,7 +737,7 @@ "from": 10634605 }, "interchainGasPaymaster": "0x6c13643B3927C57DB92c790E4E3E7Ee81e13f78C", - "interchainSecurityModule": "0x4634cC2Ca2fA09EE893e202068c7d1153172E194", + "interchainSecurityModule": "0xbb1977DB0fA1a0a9F3BeA022C18039708412B863", "isTestnet": true, "mailbox": "0x54148470292C24345fb828B003461a9444414517", "merkleTreeHook": "0xddf4C3e791caCaFd26D7fb275549739B38ae6e75", @@ -802,7 +812,7 @@ "interchainAccountIsm": "0xE023239c8dfc172FF008D8087E7442d3eBEd9350", "interchainAccountRouter": "0xe17c37212d785760E8331D4A4395B17b34Ba8cDF", "interchainGasPaymaster": "0x86fb9F1c124fB20ff130C41a79a432F770f67AFD", - "interchainSecurityModule": "0xb7E6dc17b510a17B504bb7170fa0aA9e91209b6c", + "interchainSecurityModule": "0xd67E6C921A4285D9e57D3AC780a8aD7D928771f8", "isTestnet": true, "mailbox": "0x3C5154a193D6e2955650f9305c8d80c18C814A68", "merkleTreeHook": "0x863E8c26621c52ACa1849C53500606e73BA272F0", @@ -880,7 +890,7 @@ "interchainAccountIsm": "0x83a3068B719F764d413625dA77468ED74789ae02", "interchainAccountRouter": "0x8e131c8aE5BF1Ed38D05a00892b6001a7d37739d", "interchainGasPaymaster": "0x6f2756380FD49228ae25Aa7F2817993cB74Ecc56", - "interchainSecurityModule": "0xa572fe884de7957D4F1fc5d776a23B70621A0366", + "interchainSecurityModule": "0x85f13A4D3BE151e036A807D76893F70A03B96d58", "isTestnet": true, "mailbox": "0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766", "merkleTreeHook": "0x4917a9746A7B6E0A57159cCb7F5a6744247f2d0d", @@ -918,7 +928,10 @@ "validatorAnnounce": "0xE6105C59480a1B7DD3E4f28153aFdbE12F4CfCD9", "staticMerkleRootWeightedMultisigIsmFactory": "0x4afB48e864d308409d0D80E98fB7d5d6aA5b245f", "staticMessageIdWeightedMultisigIsmFactory": "0x196Ce28ED1Afdf015849ddEE82F03a903Bee9E94", - "gasCurrencyCoinGeckoId": "ethereum" + "gasCurrencyCoinGeckoId": "ethereum", + "transactionOverrides": { + "gasPriceCap": 100000000000 + } }, "solanatestnet": { "blockExplorers": [ @@ -991,7 +1004,7 @@ "from": 3111622 }, "interchainGasPaymaster": "0xeC7eb4196Bd601DEa7585A744FbFB4CF11278450", - "interchainSecurityModule": "0x0eD4Db276A60415F10Aad22AcF3302a67D3A618d", + "interchainSecurityModule": "0x8707f99c8B9C35Bd7795818394Fd6adC62B3E236", "isTestnet": true, "mailbox": "0x6966b0E55883d49BFB24539356a2f8A673E02039", "merkleTreeHook": "0x4926a10788306D84202A2aDbd290b7743146Cc17", @@ -1069,7 +1082,7 @@ "interchainAccountIsm": "0xD356C996277eFb7f75Ee8bd61b31cC781A12F54f", "interchainAccountRouter": "0x867f2089D09903f208AeCac84E599B90E5a4A821", "interchainGasPaymaster": "0xA2cf52064c921C11adCd83588CbEa08cc3bfF5d8", - "interchainSecurityModule": "0x83897fF105605d3592b8Be1e27FE99185A98f41b", + "interchainSecurityModule": "0x97b74242477D581F13944732811e42F4d46a287D", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0xD5eB5fa3f470eBBB93a4A58C644c87031268a04A", "pausableHook": "0x51A0a100e7BC63Ea7821A3a023B6F17fb94FF011", @@ -1137,7 +1150,7 @@ "interchainAccountIsm": "0xFfa913705484C9BAea32Ffe9945BeA099A1DFF72", "interchainAccountRouter": "0xB5fB1F5410a2c2b7deD462d018541383968cB01c", "interchainGasPaymaster": "0xD5eB5fa3f470eBBB93a4A58C644c87031268a04A", - "interchainSecurityModule": "0xfab06566A262E12A8e4b845db1521c6D7D6d2554", + "interchainSecurityModule": "0x5ef15024646fd93f454426931E5339b0cBd7Eb8f", "mailbox": "0xB08d78F439e55D02C398519eef61606A5926245F", "merkleTreeHook": "0x783c4a0bB6663359281aD4a637D5af68F83ae213", "pausableHook": "0x66b71A4e18FbE09a6977A6520B47fEDdffA82a1c", @@ -1198,7 +1211,7 @@ "interchainAccountIsm": "0xD356C996277eFb7f75Ee8bd61b31cC781A12F54f", "interchainAccountRouter": "0x867f2089D09903f208AeCac84E599B90E5a4A821", "interchainGasPaymaster": "0xA2cf52064c921C11adCd83588CbEa08cc3bfF5d8", - "interchainSecurityModule": "0x3344a46afF57EF1Dc38F13a8F82BFd506f676986", + "interchainSecurityModule": "0x8af28EB954c5EcBEEB1f7D38d6C9E13d14c74A15", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0xD5eB5fa3f470eBBB93a4A58C644c87031268a04A", "pausableHook": "0x51A0a100e7BC63Ea7821A3a023B6F17fb94FF011", @@ -1327,7 +1340,7 @@ "interchainAccountIsm": "0xD356C996277eFb7f75Ee8bd61b31cC781A12F54f", "interchainAccountRouter": "0x867f2089D09903f208AeCac84E599B90E5a4A821", "interchainGasPaymaster": "0xA2cf52064c921C11adCd83588CbEa08cc3bfF5d8", - "interchainSecurityModule": "0xBc460Beee56F193E29Aa42d9eB99c4329f87AcfC", + "interchainSecurityModule": "0x96C669123224074F31efa9119a86E9c08c1939Cc", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0xD5eB5fa3f470eBBB93a4A58C644c87031268a04A", "pausableHook": "0x51A0a100e7BC63Ea7821A3a023B6F17fb94FF011", @@ -1391,7 +1404,7 @@ "interchainAccountIsm": "0xD356C996277eFb7f75Ee8bd61b31cC781A12F54f", "interchainAccountRouter": "0x867f2089D09903f208AeCac84E599B90E5a4A821", "interchainGasPaymaster": "0xA2cf52064c921C11adCd83588CbEa08cc3bfF5d8", - "interchainSecurityModule": "0x7EA1EED9CDb09d21319F69E596163C842a83dd23", + "interchainSecurityModule": "0xF6DB5f2A6E521985d658bdF405b9727964424eA8", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0xD5eB5fa3f470eBBB93a4A58C644c87031268a04A", "pausableHook": "0x51A0a100e7BC63Ea7821A3a023B6F17fb94FF011", @@ -1642,7 +1655,7 @@ "interchainAccountIsm": "0x39c85C84876479694A2470c0E8075e9d68049aFc", "interchainAccountRouter": "0x80fE4Cb8c70fc60B745d4ffD4403c27a8cBC9e02", "interchainGasPaymaster": "0xfBeaF07855181f8476B235Cf746A7DF3F9e386Fb", - "interchainSecurityModule": "0x50168831F0b2eA69DbBE98977A7Dd21a53E59050", + "interchainSecurityModule": "0x4421F6B0F4775e7E1cFe9C4F6d29bFc40D5126bf", "mailbox": "0x33dB966328Ea213b0f76eF96CA368AB37779F065", "merkleTreeHook": "0xEa7e618Bee8927fBb2fA20Bc41eE8DEA51838aAD", "pausableHook": "0x4fE19d49F45854Da50b6009258929613EC92C147", @@ -1705,7 +1718,7 @@ "interchainAccountIsm": "0x3ca332A585FDB9d4FF51f2FA8999eA32184D3606", "interchainAccountRouter": "0x4eC139a771eBdD3b0a0b67bb7E08960210882d44", "interchainGasPaymaster": "0xa3AB7E6cE24E6293bD5320A53329Ef2f4DE73fCA", - "interchainSecurityModule": "0x9ac6C0fE6F70AE702764FC076A444f1875035749", + "interchainSecurityModule": "0xBc460Beee56F193E29Aa42d9eB99c4329f87AcfC", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0x086E902d2f99BcCEAa28B31747eC6Dc5fd43B1bE", "pausableHook": "0xe0B988062A0C6492177d64823Ab95a9c256c2a5F", @@ -1768,7 +1781,7 @@ "interchainAccountIsm": "0xBF2C366530C1269d531707154948494D3fF4AcA7", "interchainAccountRouter": "0xBdf49bE2201A1c4B13023F0a407196C6Adb32680", "interchainGasPaymaster": "0xD356C996277eFb7f75Ee8bd61b31cC781A12F54f", - "interchainSecurityModule": "0xC7755b358e857D179619c8e72Cdf7b04DE79aA15", + "interchainSecurityModule": "0xc9d10b90057268F7b37cDB94f5e8eD6f10A77a81", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0xFfa913705484C9BAea32Ffe9945BeA099A1DFF72", "pausableHook": "0xc76E477437065093D353b7d56c81ff54D167B0Ab", @@ -1839,7 +1852,7 @@ "interchainAccountIsm": "0x342B5630Ba1C1e4d3048E51Dad208201aF52692c", "interchainAccountRouter": "0xe036768e48Cb0D42811d2bF0748806FCcBfCd670", "interchainGasPaymaster": "0x867f2089D09903f208AeCac84E599B90E5a4A821", - "interchainSecurityModule": "0xe0412F94c9676968103C794573b2Eeae23f63106", + "interchainSecurityModule": "0x932AA65dB1555619BaB4Cc2219efdd415dD22a1b", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0xB5fB1F5410a2c2b7deD462d018541383968cB01c", "pausableHook": "0x7483faD0Bc297667664A43A064bA7c9911659f57", @@ -1898,7 +1911,7 @@ "domainRoutingIsmFactory": "0x44b764045BfDC68517e10e783E69B376cef196B2", "fallbackRoutingHook": "0xD356C996277eFb7f75Ee8bd61b31cC781A12F54f", "interchainGasPaymaster": "0x54Bd02f0f20677e9846F8E9FdB1Abc7315C49C38", - "interchainSecurityModule": "0xC9Bd23d896e027fD465e82E3F611DC28e6b9b386", + "interchainSecurityModule": "0x31dc078391518a8eC56aadCB54158714b596975a", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0x4fE19d49F45854Da50b6009258929613EC92C147", "pausableHook": "0x01812D60958798695391dacF092BAc4a715B1718", @@ -2163,7 +2176,7 @@ "domainRoutingIsmFactory": "0x16B710b86CAd07E6F1C531861a16F5feC29dba37", "fallbackRoutingHook": "0xCB3c489a2FB67a7Cd555D47B3a9A0E654784eD16", "interchainGasPaymaster": "0x39c85C84876479694A2470c0E8075e9d68049aFc", - "interchainSecurityModule": "0xc3dDd05026645eb44233Ee78ec7331918cEe7635", + "interchainSecurityModule": "0xF1F44a3F92F7dd8D03c4D461a9b2a6d83ddc6A5d", "mailbox": "0x589C201a07c26b4725A4A829d772f24423da480B", "merkleTreeHook": "0x843908541D24d9F6Fa30C8Bb1c39038C947D08fC", "pausableHook": "0xBdf49bE2201A1c4B13023F0a407196C6Adb32680", @@ -2232,7 +2245,7 @@ "interchainAccountIsm": "0x507C18fa4e3b0ce6beBD494488D62d1ed0fB0555", "interchainAccountRouter": "0x8584590ad637C61C7cDF72eFF3381Ee1c3D1bC8E", "interchainGasPaymaster": "0x39c85C84876479694A2470c0E8075e9d68049aFc", - "interchainSecurityModule": "0xd847d1EAf388659e92475ad07e01aAe24C20507C", + "interchainSecurityModule": "0x00C69bE4bff7775b3DF6Da748e087dbD93F1e07C", "mailbox": "0x589C201a07c26b4725A4A829d772f24423da480B", "merkleTreeHook": "0x843908541D24d9F6Fa30C8Bb1c39038C947D08fC", "pausableHook": "0xBdf49bE2201A1c4B13023F0a407196C6Adb32680", @@ -2297,7 +2310,7 @@ "domainRoutingIsmFactory": "0x16B710b86CAd07E6F1C531861a16F5feC29dba37", "fallbackRoutingHook": "0x39c85C84876479694A2470c0E8075e9d68049aFc", "interchainGasPaymaster": "0xB589407cf6bEA5CD81AD0946b9F1467933ede74c", - "interchainSecurityModule": "0x2b9c079E6C95A539F32CaFf38c6bE4b5451F3aca", + "interchainSecurityModule": "0xd847d1EAf388659e92475ad07e01aAe24C20507C", "mailbox": "0x589C201a07c26b4725A4A829d772f24423da480B", "merkleTreeHook": "0x342B5630Ba1C1e4d3048E51Dad208201aF52692c", "pausableHook": "0x4eC139a771eBdD3b0a0b67bb7E08960210882d44", @@ -2351,7 +2364,7 @@ "domainRoutingIsmFactory": "0x16B710b86CAd07E6F1C531861a16F5feC29dba37", "fallbackRoutingHook": "0x39c85C84876479694A2470c0E8075e9d68049aFc", "interchainGasPaymaster": "0xB589407cf6bEA5CD81AD0946b9F1467933ede74c", - "interchainSecurityModule": "0xD154b01d74A0a8646F793f5661464215d0e8Df8e", + "interchainSecurityModule": "0x13A179e08D9538Eb51DA4659E157f587F535Ff07", "mailbox": "0x589C201a07c26b4725A4A829d772f24423da480B", "merkleTreeHook": "0x342B5630Ba1C1e4d3048E51Dad208201aF52692c", "pausableHook": "0x4eC139a771eBdD3b0a0b67bb7E08960210882d44", @@ -2417,7 +2430,7 @@ "interchainAccountIsm": "0x4da6f7E710137657008D5BCeF26151aac5c9884f", "interchainAccountRouter": "0x919Af376D02751bFCaD9CBAD6bad0c3089dAE33f", "interchainGasPaymaster": "0x8584590ad637C61C7cDF72eFF3381Ee1c3D1bC8E", - "interchainSecurityModule": "0x46e149e41Dbe8d7695974976E1Cd73ae92E40d0f", + "interchainSecurityModule": "0x63a0e3a65293Ce106AAbD5A2124e4E318DE667aB", "mailbox": "0x589C201a07c26b4725A4A829d772f24423da480B", "merkleTreeHook": "0xE1CCB130389f687bf745Dd6dc05E50da17d9ea96", "pausableHook": "0x8d4f112cffa338D3c3Ef2Cf443179C5a48E678e4", @@ -2482,7 +2495,7 @@ "interchainAccountIsm": "0x4da6f7E710137657008D5BCeF26151aac5c9884f", "interchainAccountRouter": "0x919Af376D02751bFCaD9CBAD6bad0c3089dAE33f", "interchainGasPaymaster": "0x8584590ad637C61C7cDF72eFF3381Ee1c3D1bC8E", - "interchainSecurityModule": "0x2286070f23DBc441a758fF0BaB3784C8795f4992", + "interchainSecurityModule": "0x2380ecc0e745b2a06E890DbfD78E7c0f49c4037b", "mailbox": "0x589C201a07c26b4725A4A829d772f24423da480B", "merkleTreeHook": "0xE1CCB130389f687bf745Dd6dc05E50da17d9ea96", "pausableHook": "0x8d4f112cffa338D3c3Ef2Cf443179C5a48E678e4", @@ -2549,7 +2562,7 @@ "interchainAccountIsm": "0xD9dc83Ea22C6F1A224e51562B32b580695905A1A", "interchainAccountRouter": "0x17866ebE0e503784a9461d3e753dEeD0d3F61153", "interchainGasPaymaster": "0xce0e13f67399375eF0a7acb741E815145A6AAf67", - "interchainSecurityModule": "0x4a11bEA3Bd0CB52c1F6a9a5dB95463078AdE4Ab1", + "interchainSecurityModule": "0x899F40E204be29060aaFec03c2cDcfd219fc8734", "mailbox": "0x589C201a07c26b4725A4A829d772f24423da480B", "merkleTreeHook": "0xb3D796584fDeBE2321894eeF31e0C3ec52169C61", "pausableHook": "0x2bD9aF503B9F608beAD63D4ACC328Abf9796b576", @@ -2610,7 +2623,7 @@ "interchainAccountIsm": "0x740bEd6E4eEc7c57a2818177Fba3f9E896D5DE1c", "interchainAccountRouter": "0xD5B70f7Da85F98A5197E55114A38f3eDcDCf020e", "interchainGasPaymaster": "0x919Af376D02751bFCaD9CBAD6bad0c3089dAE33f", - "interchainSecurityModule": "0xC21401c403cc8a2bB4cdA43DB50e795b6b6C4Cdb", + "interchainSecurityModule": "0xd2e0F1931b77d9973feE1182294B80104bbf2CCB", "mailbox": "0x7d498740A4572f2B5c6b0A1Ba9d1d9DbE207e89E", "merkleTreeHook": "0x7d811da36c48cfDc7C445F14252F102bF06E3Fd7", "pausableHook": "0xCCC126d96efcc342BF2781A7d224D3AB1F25B19C", @@ -2670,7 +2683,7 @@ "interchainAccountIsm": "0x6cB503d97D1c900316583C8D55997A1f17b1ABd1", "interchainAccountRouter": "0x740bEd6E4eEc7c57a2818177Fba3f9E896D5DE1c", "interchainGasPaymaster": "0x48a53E3B176383BC98fcF4a24c9D470c19475164", - "interchainSecurityModule": "0xB70Aa3071c46D66314a5BEBe8e7b96352918A1F7", + "interchainSecurityModule": "0xAd8A3F13043Ea6D60158Fe708a4c0Aa66654784b", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0x8584590ad637C61C7cDF72eFF3381Ee1c3D1bC8E", "pausableHook": "0xdb3338da7947dc9beDAB5f8685da721C293E0cbF", @@ -2732,7 +2745,7 @@ "interchainAccountIsm": "0x890eB21B76DCB165A1807cBE279f883716eA47D4", "interchainAccountRouter": "0xB7697612fbfb4ad02a11dCa16e9711eCB6Da4ceA", "interchainGasPaymaster": "0xA9425D5cBcD2c83EB2a5BF453EAA18968db3ef77", - "interchainSecurityModule": "0xbDfD02590D4C93f1Af799C8134871C66e6b65963", + "interchainSecurityModule": "0xbF88AC2A75E0f666A1fF5e2B9c0Ae3378E940e41", "mailbox": "0x7FE7EA170cf08A25C2ff315814D96D93C311E692", "merkleTreeHook": "0x413c74F3D034dB54A1ecfFbd0Ad74Cb25E59f579", "pausableHook": "0x86abe4c3493A1eE0Aa42f0231b5594D42aBdA36e", @@ -2770,8 +2783,8 @@ }, "chainId": 919, "deployer": { - "name": "guy93354", - "url": "https://github.com/guy93354" + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" }, "displayName": "Mode Testnet", "domainId": 919, @@ -2796,7 +2809,7 @@ "interchainAccountIsm": "0x6C3132f78260EdD1cE88Ea4FeEB8C2D6309ecc75", "interchainAccountRouter": "0x1681cc382e08a72d4b64A123080896e30f96B740", "interchainGasPaymaster": "0xB261C52241E133f957630AeeFEd48a82963AC33e", - "interchainSecurityModule": "0x82a0d6047B624e542A09e7c1e0925AeCC92062f4", + "interchainSecurityModule": "0xe320c99D1c15be130C634ecB97c54dC02F2ab8A1", "mailbox": "0x589C201a07c26b4725A4A829d772f24423da480B", "merkleTreeHook": "0xf83416bA0491C8BC80Dad259Fc7C007bC57Bd766", "pausableHook": "0x6cB503d97D1c900316583C8D55997A1f17b1ABd1", @@ -2864,7 +2877,7 @@ "interchainAccountIsm": "0x9667EfF1556A9D092fdbeC09244CB99b677E9D1E", "interchainAccountRouter": "0x2A9E9188C7e76f3345e91fD4650aC654A9FE355C", "interchainGasPaymaster": "0xD5B70f7Da85F98A5197E55114A38f3eDcDCf020e", - "interchainSecurityModule": "0x322C3eC90341F6DbD0339b418dc3A09072C5D50b", + "interchainSecurityModule": "0xf7E4168C13aAAB55e6d483091368606ed5D82EB9", "mailbox": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD", "merkleTreeHook": "0x0b9A4A46f50f91f353B8Aa0F3Ca80E35E253bDd8", "pausableHook": "0x9450181a7719dAb93483d43a45473Ac2373E25B0", @@ -2882,6 +2895,73 @@ "testRecipient": "0x86abe4c3493A1eE0Aa42f0231b5594D42aBdA36e", "timelockController": "0x0000000000000000000000000000000000000000", "validatorAnnounce": "0xF8E6c1222049AAb68E410E43242449994Cb64996" + }, + "kyvetestnet": { + "bech32Prefix": "kyve", + "blockExplorers": [ + { + "apiUrl": "https://explorer.kyve.network/kaon", + "family": "other", + "name": "Ping.Pub", + "url": "https://explorer.kyve.network/kaon" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 6, + "reorgPeriod": 1 + }, + "canonicalAsset": "tkyve", + "chainId": "kaon-1", + "contractAddressBytes": 32, + "deployer": { + "name": "KYVE", + "url": "https://kyve.network" + }, + "displayName": "KYVE Testnet", + "domainId": 1262571342, + "gasPrice": { + "denom": "tkyve", + "amount": "2.0" + }, + "index": { + "chunk": 10, + "from": 11470395 + }, + "isTestnet": true, + "name": "kyvetestnet", + "nativeToken": { + "decimals": 6, + "denom": "tkyve", + "name": "KYVE", + "symbol": "KYVE" + }, + "protocol": "cosmosnative", + "restUrls": [ + { + "http": "https://api.kaon.kyve.network" + } + ], + "rpcUrls": [ + { + "http": "https://rpc.kaon.kyve.network" + } + ], + "slip44": 118, + "technicalStack": "other", + "interchainGasPaymaster": "0x726f757465725f706f73745f6469737061746368000000040000000000000000", + "interchainSecurityModule": "0x726f757465725f69736d00000000000000000000000000040000000000000000", + "mailbox": "0x68797065726c616e650000000000000000000000000000000000000000000000", + "merkleTreeHook": "0x726f757465725f706f73745f6469737061746368000000030000000000000001", + "validatorAnnounce": "0x68797065726c616e650000000000000000000000000000000000000000000000", + "grpcUrls": [ + { + "http": "https://grpc-raw.kaon.kyve.network" + } + ], + "transactionOverrides": { + "gasPrice": "2.0" + } } }, "defaultRpcConsensusType": "fallback" diff --git a/rust/main/helm/hyperlane-agent/templates/relayer-external-secret.yaml b/rust/main/helm/hyperlane-agent/templates/relayer-external-secret.yaml index c9bcd9a27d6..a8103a0d5b1 100644 --- a/rust/main/helm/hyperlane-agent/templates/relayer-external-secret.yaml +++ b/rust/main/helm/hyperlane-agent/templates/relayer-external-secret.yaml @@ -36,6 +36,9 @@ spec: AWS_ACCESS_KEY_ID: {{ print "'{{ .aws_access_key_id | toString }}'" }} AWS_SECRET_ACCESS_KEY: {{ print "'{{ .aws_secret_access_key | toString }}'" }} {{- end }} + {{- if .Values.hyperlane.relayer.dbBootstrap.enabled }} + DB_BOOTSTRAP_SERVICE_ACCOUNT_KEY: {{ print "'{{ .db_bootstrap_gcp_sa_json | toString }}'" }} + {{- end }} data: {{- range .Values.hyperlane.relayerChains }} {{- if or (eq .signer.type "hexKey") (eq .signer.type "cosmosKey") }} @@ -57,4 +60,9 @@ spec: remoteRef: key: {{ printf "%s-%s-relayer-aws-secret-access-key" .Values.hyperlane.context .Values.hyperlane.runEnv }} {{- end }} + {{- if .Values.hyperlane.relayer.dbBootstrap.enabled }} + - secretKey: db_bootstrap_gcp_sa_json + remoteRef: + key: {{ printf "%s-relayer-db-bootstrap-viewer-key" $.Values.hyperlane.runEnv }} + {{- end }} {{- end }} diff --git a/rust/main/helm/hyperlane-agent/templates/relayer-statefulset.yaml b/rust/main/helm/hyperlane-agent/templates/relayer-statefulset.yaml index 60a51e7847c..4dcf1a258c8 100644 --- a/rust/main/helm/hyperlane-agent/templates/relayer-statefulset.yaml +++ b/rust/main/helm/hyperlane-agent/templates/relayer-statefulset.yaml @@ -43,6 +43,37 @@ spec: terminationGracePeriodSeconds: 10 securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + {{- if .Values.hyperlane.relayer.dbBootstrap.enabled }} + initContainers: + - name: db-bootstrap + image: google/cloud-sdk:alpine + command: + - sh + - -c + - | + if find {{ .Values.hyperlane.dbPath }} -type f | grep -q .; then + echo "Files already exist in {{ .Values.hyperlane.dbPath }} — skipping bootstrap" + else + echo "No data found in {{ .Values.hyperlane.dbPath }} — bootstrapping from GCS" + gsutil cp gs://{{ .Values.hyperlane.relayer.dbBootstrap.bucket }}/{{ .Values.hyperlane.relayer.dbBootstrap.object_targz }} /tmp/seed.tar.gz + # GNU tar has more options than busybox tar + echo "Installing GNU tar..." + apk add tar + echo "Extracting data to {{ .Values.hyperlane.dbPath }}" + tar --no-overwrite-dir --no-same-owner -xzf /tmp/seed.tar.gz -C {{ .Values.hyperlane.dbPath }} + chown -R 1000:2000 {{ .Values.hyperlane.dbPath }} + fi + env: + - name: GOOGLE_APPLICATION_CREDENTIALS + valueFrom: + secretKeyRef: + name: {{ include "agent-common.fullname" . }}-relayer-secret + key: DB_BOOTSTRAP_SERVICE_ACCOUNT_KEY + volumeMounts: + - name: {{ .Values.hyperlane.relayer.storage.name | default "state" }} + mountPath: {{ .Values.hyperlane.dbPath }} + {{- end }} containers: - name: agent securityContext: @@ -61,6 +92,26 @@ spec: {{- include "agent-common.config-env-vars" (dict "config" .Values.hyperlane.relayer.envConfig) | nindent 10 }} - name: CONFIG_FILES value: "/relayer-configmap/relayer-config.json" + {{- with .Values.hyperlane.relayer.cacheDefaultExpirationSeconds }} + - name: HYP_CACHEDEFAULTEXPIRATIONSECONDS + value: {{ . | quote }} + {{- end }} + {{- with .Values.hyperlane.relayer.mixing }} + {{- if .enabled }} + - name: HYPERLANE_RELAYER_MIXING_ENABLED + value: "true" + - name: HYPERLANE_RELAYER_MIXING_SALT + value: {{ .salt | quote }} + {{- end }} + {{- end }} + {{- with .Values.hyperlane.relayer.environmentVariableEndpointEnabled }} + - name: HYPERLANE_RELAYER_ENVIRONMENT_VARIABLE_ENDPOINT_ENABLED + value: {{ . | quote }} + {{- end }} + {{- with .Values.hyperlane.relayer.maxSubmitQueueLength }} + - name: HYP_MAXSUBMITQUEUELENGTH + value: {{ . | quote }} + {{- end }} resources: {{- toYaml .Values.hyperlane.relayer.resources | nindent 10 }} volumeMounts: diff --git a/rust/main/helm/hyperlane-agent/values.yaml b/rust/main/helm/hyperlane-agent/values.yaml index 8e12fe7448d..3462b145059 100644 --- a/rust/main/helm/hyperlane-agent/values.yaml +++ b/rust/main/helm/hyperlane-agent/values.yaml @@ -122,6 +122,10 @@ hyperlane: # -- Specify whether a default signer key is used for all chains in Values.hyperlane.relayerChains list. # It affects chains whose signer type is hexKey. usingDefaultSignerKey: true + dbBootstrap: + enabled: false + bucket: '' + object_targz: '' relayerChains: - name: 'alfajores' diff --git a/rust/main/hyperlane-base/Cargo.toml b/rust/main/hyperlane-base/Cargo.toml index 95aeba0b255..0e3372d7039 100644 --- a/rust/main/hyperlane-base/Cargo.toml +++ b/rust/main/hyperlane-base/Cargo.toml @@ -10,12 +10,16 @@ version.workspace = true [dependencies] async-trait.workspace = true axum.workspace = true +aws-config.workspace = true +aws-sdk-s3.workspace = true bs58.workspace = true color-eyre = { workspace = true, optional = true } config.workspace = true console-subscriber.workspace = true convert_case.workspace = true +dashmap.workspace = true derive-new.workspace = true +derive_builder.workspace = true ed25519-dalek.workspace = true ethers.workspace = true eyre.workspace = true @@ -43,6 +47,8 @@ tracing.workspace = true url.workspace = true warp.workspace = true ya-gcp.workspace = true +moka = { workspace = true, features = ["future"] } +chrono = { workspace = true, features = ["serde"] } backtrace = { workspace = true, optional = true } backtrace-oneline = { path = "../utils/backtrace-oneline", optional = true } @@ -62,7 +68,6 @@ hyperlane-sealevel = { path = "../chains/hyperlane-sealevel" } # dependency version is determined by etheres rusoto_core = "*" rusoto_kms = "*" -rusoto_s3 = "*" rusoto_sts = "*" [dev-dependencies] diff --git a/rust/main/hyperlane-base/src/agent.rs b/rust/main/hyperlane-base/src/agent.rs index a8975269e86..84893ddc74e 100644 --- a/rust/main/hyperlane-base/src/agent.rs +++ b/rust/main/hyperlane-base/src/agent.rs @@ -1,10 +1,11 @@ -pub use crate::metadata::AgentMetadata; +pub use crate::metadata::{git_sha, AgentMetadata}; use std::{env, fmt::Debug, sync::Arc}; use async_trait::async_trait; use eyre::Result; use hyperlane_core::config::*; +use serde::Serialize; use tracing::info; use crate::{ @@ -29,6 +30,12 @@ pub trait LoadableFromSettings: AsRef + Sized { fn load() -> ConfigResult; } +/// Metadata of an agent defined from configuration +pub trait MetadataFromSettings: Serialize + Sized { + /// Create a new instance of the agent metadata from the settings + fn build_metadata(settings: &T) -> Self; +} + /// A fundamental agent which does not make any assumptions about the tools /// which are used. #[async_trait] @@ -39,9 +46,12 @@ pub trait BaseAgent: Send + Sync + Debug { /// The settings object for this agent type Settings: LoadableFromSettings; + /// The agents metadata type + type Metadata: MetadataFromSettings; + /// Instantiate the agent from the standard settings object async fn from_settings( - agent_metadata: AgentMetadata, + agent_metadata: Self::Metadata, settings: Self::Settings, metrics: Arc, agent_metrics: AgentMetrics, @@ -76,17 +86,15 @@ pub async fn agent_main() -> Result<()> { color_eyre::install()?; } - // Latest git commit hash at the time when agent was built. - // If .git was not present at the time of build, - // the variable defaults to "VERGEN_IDEMPOTENT_OUTPUT". - let git_sha = env!("VERGEN_GIT_SHA").to_owned(); - // Logging is not initialised at this point, so, using `println!` - println!("Agent {} starting up with version {git_sha}", A::AGENT_NAME); - - let agent_metadata = AgentMetadata::new(git_sha); + println!( + "Agent {} starting up with version {}", + A::AGENT_NAME, + git_sha() + ); let settings = A::Settings::load()?; + let agent_metadata = A::Metadata::build_metadata(&settings); let core_settings: &Settings = settings.as_ref(); let metrics = settings.as_ref().metrics(A::AGENT_NAME)?; diff --git a/rust/main/hyperlane-base/src/cache/error.rs b/rust/main/hyperlane-base/src/cache/error.rs new file mode 100644 index 00000000000..9917dbe519d --- /dev/null +++ b/rust/main/hyperlane-base/src/cache/error.rs @@ -0,0 +1,13 @@ +/// Error type for the cache module. +#[derive(thiserror::Error, Debug)] +pub enum CacheError { + /// Error when key or value serialization fails. + #[error("Failed to serialize input: {0}")] + FailedToSerializeInput(#[source] serde_json::Error), + + /// Error when entity fetched from cache is deserialized incorrectly. + /// Most of the time this can be caused due the mismatch of the type + /// expected vs actual type of the entity. + #[error("Failed to deserialize output: {0}")] + FailedToDeserializeOutput(#[source] serde_json::Error), +} diff --git a/rust/main/hyperlane-base/src/cache/metered_cache.rs b/rust/main/hyperlane-base/src/cache/metered_cache.rs new file mode 100644 index 00000000000..a3b94ba059a --- /dev/null +++ b/rust/main/hyperlane-base/src/cache/metered_cache.rs @@ -0,0 +1,107 @@ +use std::fmt::Debug; + +use async_trait::async_trait; +use derive_builder::Builder; +use derive_new::new; +use maplit::hashmap; +use prometheus::IntCounterVec; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::cache::FunctionCallCache; + +use super::CacheResult; + +/// Basic cache information. +#[derive(Debug, Clone)] +pub struct MeteredCacheConfig { + /// The name of the cache set on creation. + pub cache_name: String, +} + +/// Container for all the relevant cache metrics. +#[derive(Clone, Builder, Debug)] +pub struct MeteredCacheMetrics { + /// The amount of calls which returned a cached result. + /// - `cache_name`: the name of the cache. + /// - `chain`: the name of the chain. + /// - `method`: the call stored in the cache. + /// - `status`: the status of the call. + #[builder(setter(into, strip_option), default)] + pub hit_count: Option, + /// The amount of calls which did not return a cached result. + /// - `cache_name`: the name of the cache. + /// - `chain`: the name of the chain. + /// - `method`: the call stored in the cache. + /// - `status`: the status of the call. + #[builder(setter(into, strip_option), default)] + pub miss_count: Option, +} + +/// Expected label names for the metric. +pub const HIT_COUNT_HELP: &str = "Number of cache hits"; +/// Help string for the metric. +pub const HIT_COUNT_LABELS: &[&str] = &["cache_name", "chain", "method", "status"]; + +/// Expected label names for the metric. +pub const MISS_COUNT_HELP: &str = "Number of cache misses"; +/// Help string for the metric. +pub const MISS_COUNT_LABELS: &[&str] = &["cache_name", "chain", "method", "status"]; + +/// A Cache wrapper that instruments the cache calls with metrics. +#[derive(new, Debug, Clone)] +pub struct MeteredCache { + inner: C, + metrics: MeteredCacheMetrics, + config: MeteredCacheConfig, +} + +#[async_trait] +impl FunctionCallCache for MeteredCache +where + C: FunctionCallCache, +{ + async fn cache_call_result( + &self, + domain_name: &str, + fn_key: &str, + fn_params: &(impl Serialize + Send + Sync), + result: &(impl Serialize + Send + Sync), + ) -> CacheResult<()> { + self.inner + .cache_call_result(domain_name, fn_key, fn_params, result) + .await + } + + async fn get_cached_call_result( + &self, + domain_name: &str, + method: &str, + fn_params: &(impl Serialize + Send + Sync), + ) -> CacheResult> + where + T: DeserializeOwned, + { + let result = self + .inner + .get_cached_call_result::(domain_name, method, fn_params) + .await; + + let labels = hashmap! { + "cache_name" => self.config.cache_name.as_str(), + "chain" => domain_name, + "method" => method, + "status" => if result.is_ok() { "success" } else { "failure" } + }; + + let is_hit = result.is_ok() && result.as_ref().unwrap().is_some(); + if is_hit { + if let Some(hit_count) = &self.metrics.hit_count { + hit_count.with(&labels).inc(); + } + } else if let Some(miss_count) = &self.metrics.miss_count { + miss_count.with(&labels).inc(); + } + + result + } +} diff --git a/rust/main/hyperlane-base/src/cache/mod.rs b/rust/main/hyperlane-base/src/cache/mod.rs new file mode 100644 index 00000000000..470936d0d12 --- /dev/null +++ b/rust/main/hyperlane-base/src/cache/mod.rs @@ -0,0 +1,42 @@ +mod error; +mod metered_cache; +mod moka; +mod optional_cache; + +use async_trait::async_trait; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +pub use error::CacheError; +pub use metered_cache::{ + MeteredCache, MeteredCacheConfig, MeteredCacheMetrics, MeteredCacheMetricsBuilder, + HIT_COUNT_HELP, HIT_COUNT_LABELS, MISS_COUNT_HELP, MISS_COUNT_LABELS, +}; +pub use moka::{CacheResult, Expiration, LocalCache}; +pub use optional_cache::OptionalCache; + +/// Should be used as the `fn_params` when the function has no parameters +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NoParams; + +/// Cache for storing function calls with serializable results +#[async_trait] +pub trait FunctionCallCache: Send + Sync { + /// Cache a call result with the given parameters + async fn cache_call_result( + &self, + domain_name: &str, + fn_key: &str, + fn_params: &(impl Serialize + Send + Sync), + result: &(impl Serialize + Send + Sync), + ) -> CacheResult<()>; + + /// Get a cached call result with the given parameters + async fn get_cached_call_result( + &self, + domain_name: &str, + method: &str, + fn_params: &(impl Serialize + Send + Sync), + ) -> CacheResult> + where + T: DeserializeOwned; +} diff --git a/rust/main/hyperlane-base/src/cache/moka/dynamic_expiry.rs b/rust/main/hyperlane-base/src/cache/moka/dynamic_expiry.rs new file mode 100644 index 00000000000..0cd73178155 --- /dev/null +++ b/rust/main/hyperlane-base/src/cache/moka/dynamic_expiry.rs @@ -0,0 +1,103 @@ +use std::{ + sync::OnceLock, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use chrono::{offset::LocalResult, TimeZone, Utc}; +use moka::Expiry; +use serde::{Deserialize, Serialize}; + +/// Default expiration time for cache entries. +static DEFAULT_EXPIRATION: OnceLock = OnceLock::new(); + +pub fn default_expiration() -> Duration { + *DEFAULT_EXPIRATION.get_or_init(|| { + let secs = std::env::var("HYP_CACHEDEFAULTEXPIRATIONSECONDS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(120); // default: 2 minutes + Duration::from_secs(secs) + }) +} + +/// The type of expiration for a cache entry. +/// +/// ## Variants +/// +/// - `Never`: Never expire. +/// - `AfterDuration`: Expire after a specified duration. +/// - `AfterTimestamp`: Expire after a specified timestamp. +/// - `Default`: Use the default expiration. (2 minutes) +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum ExpirationType { + Never, + AfterDuration(Duration), + AfterTimestamp(u64), + Default, +} + +impl From for Expiration { + fn from(expiration: ExpirationType) -> Self { + Expiration { + variant: expiration, + created_at: Utc::now().timestamp() as u64, + } + } +} + +/// Expiration to store alongside a cache entry. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Expiration { + /// The type of expiration used when the entry was created. + pub variant: ExpirationType, + /// Unix timestamp when the entry was created. + pub created_at: u64, +} + +impl Expiration { + /// Get the duration until the entry expires + pub fn as_duration(&self) -> Option { + match self.variant { + ExpirationType::AfterDuration(duration) => Some(duration), + ExpirationType::AfterTimestamp(timestamp) => { + let target_time = UNIX_EPOCH + Duration::from_secs(timestamp); + target_time + .duration_since(SystemTime::now()) + .ok() + .or(Some(Duration::ZERO)) + } + ExpirationType::Never => None, + ExpirationType::Default => Some(default_expiration()), + } + } + + /// Calculate the time to live for the entry + /// Returns None if the entry should never expire or if the expiration time is in the past + pub fn time_to_live(&self) -> Option { + let expiration = self.as_duration()?; + let created_at = match Utc.timestamp_opt(self.created_at as i64, 0) { + LocalResult::Single(time) => time, + LocalResult::Ambiguous(earliest, _) => earliest, + LocalResult::None => return None, + }; + let now = Utc::now(); + let elapsed = now.signed_duration_since(created_at).to_std().ok()?; + expiration.checked_sub(elapsed) + } +} + +/// A dynamic expiry policy that uses the expiration stored alongside the value. +/// Used for setting up a new cache with an expiry policy. +pub struct DynamicExpiry {} + +impl Expiry for DynamicExpiry { + fn expire_after_create( + &self, + _key: &String, + value: &(String, Expiration), + _created_at: std::time::Instant, + ) -> Option { + value.1.as_duration() + } +} diff --git a/rust/main/hyperlane-base/src/cache/moka/local_cache.rs b/rust/main/hyperlane-base/src/cache/moka/local_cache.rs new file mode 100644 index 00000000000..bd4dd0fd310 --- /dev/null +++ b/rust/main/hyperlane-base/src/cache/moka/local_cache.rs @@ -0,0 +1,56 @@ +use std::fmt::Debug; + +use async_trait::async_trait; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::cache::FunctionCallCache; + +use super::{BaseCache, CacheResult, ExpirationType}; + +/// Local cache for storing function calls with serializable results in memory +#[derive(Debug, Clone)] +pub struct LocalCache(BaseCache); + +impl LocalCache { + /// Create a new local cache with the given name + pub fn new(name: &str) -> Self { + Self(BaseCache::new(name)) + } +} + +#[async_trait] +impl FunctionCallCache for LocalCache { + /// Cache a call result with the given parameters + async fn cache_call_result( + &self, + domain_name: &str, + method: &str, + fn_params: &(impl Serialize + Send + Sync), + result: &(impl Serialize + Send + Sync), + ) -> CacheResult<()> { + let key = (domain_name, method, fn_params); + self.0 + .set(&key, result, ExpirationType::Default) + .await + .map(|_| ()) + } + + /// Get a cached call result with the given parameters + async fn get_cached_call_result( + &self, + domain_name: &str, + method: &str, + fn_params: &(impl Serialize + Send + Sync), + ) -> CacheResult> + where + T: DeserializeOwned, + { + let key = (domain_name, method, fn_params); + let value = self.0.get::(&key).await?; + + match value { + Some((value, _)) => Ok(Some(value)), + None => Ok(None), + } + } +} diff --git a/rust/main/hyperlane-base/src/cache/moka/mod.rs b/rust/main/hyperlane-base/src/cache/moka/mod.rs new file mode 100644 index 00000000000..e86d88697db --- /dev/null +++ b/rust/main/hyperlane-base/src/cache/moka/mod.rs @@ -0,0 +1,345 @@ +/// Moka expiry trait implementation for dynamic lifetimes +mod dynamic_expiry; +mod local_cache; + +use std::hash::RandomState; + +use moka::{future::Cache, policy::EvictionPolicy}; +use serde::{de::DeserializeOwned, Serialize}; + +pub use dynamic_expiry::{DynamicExpiry, Expiration, ExpirationType}; +pub use local_cache::LocalCache; + +use crate::cache::CacheError; + +/// A simple generic cache that stores serializable values. +/// Supports dynamic expiration times +/// +/// ## Type Parameters +/// +/// - `String`: The type of the keys stored in the cache. +/// - `(String, Expiration)`: The type of the values stored in the cache. The tuple includes +/// a `String` value and an `Expiration` to track the TTL (Time to Live) for each entry. +/// - `RandomState`: The type of the hasher used for hashing keys in the cache. +#[derive(Debug, Clone)] +pub struct BaseCache { + cache: Cache, +} + +const MAX_CACHE_CAPACITY: u64 = 50 * 1024 * 1024; // 50MB + +/// The result type for cache operations, which can return a `CacheError` +pub type CacheResult = std::result::Result; + +impl BaseCache { + /// Create a new cache with the given name + pub fn new(name: &str) -> Self { + let cache = Cache::builder() + .name(name) + .expire_after(DynamicExpiry {}) + .eviction_policy(EvictionPolicy::lru()) + .max_capacity(MAX_CACHE_CAPACITY) + .build(); + Self { cache } + } + + /// Get the value for the given key + pub async fn get( + &self, + key: &impl Serialize, + ) -> CacheResult> { + let key = self.serialize(key)?; + + match self.cache.get(&key).await { + Some((json_value, expiry)) => { + let value = self.deserialize(json_value)?; + Ok(Some((value, expiry))) + } + None => Ok(None), + } + } + + /// Set the value for the given key and return the expiration time + pub async fn set( + &self, + key: &impl Serialize, + value: &impl Serialize, + ttl: ExpirationType, + ) -> CacheResult { + let key = self.serialize(key)?; + let value = self.serialize(value)?; + + let ttl = Expiration::from(ttl); + self.cache.insert(key, (value, ttl.clone())).await; + Ok(ttl) + } + + fn serialize(&self, value: &impl Serialize) -> CacheResult { + serde_json::to_string(value).map_err(CacheError::FailedToSerializeInput) + } + + fn deserialize(&self, json_value: String) -> CacheResult { + serde_json::from_str(&json_value).map_err(CacheError::FailedToDeserializeOutput) + } +} + +#[cfg(test)] +impl BaseCache { + /// Get the number of entries in the cache + /// This will run any pending tasks before returning the entry count + /// which ensures that the count is accurate. + pub async fn entries(&self) -> u64 { + self.cache.run_pending_tasks().await; + self.cache.entry_count() + } + + /// Check if the cache contains a value for the given key + pub fn contains_key(&self, key: &impl Serialize) -> CacheResult { + let key = self.serialize(key)?; + Ok(self.cache.contains_key(&key)) + } + + /// Remove the value for the given key + pub async fn remove(&self, key: &impl Serialize) -> CacheResult<()> { + let key = self.serialize(key)?; + self.cache.invalidate(&key).await; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::time::Duration; + + use chrono::Utc; + use serde::Deserialize; + + use hyperlane_core::{H256, U256}; + + use crate::cache::moka::dynamic_expiry::default_expiration; + + use super::*; + + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] + struct TestStruct { + a: String, + b: i32, + c: H256, + } + + async fn sleep(secs: u64) { + tokio::time::sleep(std::time::Duration::from_secs(secs)).await; + } + + #[tokio::test] + async fn basic_set_and_get() { + let cache = BaseCache::new("test-cache"); + + let key = "key".to_owned(); + let value = 123; + let ttl = ExpirationType::Default; + + cache.set(&key.clone(), &value, ttl).await.unwrap(); + + let entries = cache.entries().await; + assert_eq!(entries, 1); + + let cached_value = cache.get::(&key).await.unwrap(); + assert!(cached_value.is_some_and(|(v, _)| v == value)); + } + + #[tokio::test] + async fn str_set_and_get() { + let cache = BaseCache::new("test-cache"); + + let key = "key"; + let value = "value"; + let ttl = ExpirationType::Default; + + cache.set(&key, &value, ttl).await.unwrap(); + + let entries = cache.entries().await; + assert_eq!(entries, 1); + + let cached_value = cache.get::(&key).await.unwrap(); + assert!(cached_value.is_some_and(|(v, _)| v == value)); + } + + #[tokio::test] + async fn tuple_set_and_get() { + let cache = BaseCache::new("test-cache"); + + let key = ("key".to_owned(), 1, H256::zero()); + let value = ("value".to_owned(), 2, U256::zero()); + let ttl = ExpirationType::Default; + + cache.set(&key.clone(), &value, ttl).await.unwrap(); + + let entries = cache.entries().await; + assert_eq!(entries, 1); + + let cached_value = cache.get::<(String, i32, U256)>(&key).await.unwrap(); + assert!(cached_value.is_some_and(|(v, _)| v == value)); + } + + #[tokio::test] + async fn struct_set_and_get() { + let cache = BaseCache::new("test-cache"); + + let key = TestStruct { + a: "key".to_owned(), + b: 1, + c: H256::zero(), + }; + let value = TestStruct { + a: "value".to_owned(), + b: 2, + c: H256::zero(), + }; + let ttl = ExpirationType::Default; + + cache.set(&key.clone(), &value, ttl).await.unwrap(); + + let entries = cache.entries().await; + assert_eq!(entries, 1); + + let cached_value = cache.get::(&key).await.unwrap(); + assert!(cached_value.is_some_and(|(v, _)| v == value)); + } + + #[tokio::test] + async fn get_non_existent() { + let cache = BaseCache::new("test-cache"); + + let key = "key".to_owned(); + + let cached_value = cache.get::(&key).await.unwrap(); + assert!(cached_value.is_none()); + } + + #[tokio::test] + async fn get_with_wrong_type() { + let cache = BaseCache::new("test-cache"); + + let key = "key".to_owned(); + let value = 123; + let ttl = ExpirationType::Default; + + cache.set(&key.clone(), &value, ttl).await.unwrap(); + + let cached_value = cache.get::(&key).await; + assert!(cached_value.is_err()); + } + + #[tokio::test] + async fn insert_with_invalid_expiry_timestamp() { + let cache = BaseCache::new("test-cache"); + + let key = "key".to_owned(); + let value = 123; + let invalid_timestamp = 946684800; // 2000-01-01 00:00:00 + let ttl = ExpirationType::AfterTimestamp(invalid_timestamp as u64); + + let result = cache.set(&key, &value, ttl).await; + assert!(result.is_ok()); + + // If timestamp is in the past, the entry should be immediately expired + let entry = cache.get::(&key).await.unwrap(); + assert!(entry.is_none()); + } + + #[tokio::test] + async fn different_ttls() { + let cache = BaseCache::new("test-cache"); + + let value = 123; + let timestamp_in_10_seconds = Utc::now().timestamp() + 10; + + // Use a different ExpirationType for each key + let keys_with_ttl = vec![ + ( + "5sec".to_owned(), + ExpirationType::AfterDuration(Duration::from_secs(5)), + ), + ( + "10sec".to_owned(), + ExpirationType::AfterTimestamp(timestamp_in_10_seconds as u64), + ), + ("default".to_owned(), ExpirationType::Default), + ("never".to_owned(), ExpirationType::Never), + ]; + + // Set each key with its respective TTL + for (key, ttl) in keys_with_ttl.clone() { + cache.set(&key, &value, ttl).await.unwrap(); + } + + let entries = cache.entries().await; + assert_eq!(entries, keys_with_ttl.len() as u64); + + // Pull each key from the cache and check the value, expiration and TTL + for (i, (key, expiry_type)) in keys_with_ttl.iter().enumerate() { + let cached_value = cache.get::(&key).await.unwrap(); + + assert!(cached_value.is_some_and(|(v, e)| { + assert!(v == value); + assert!(&e.variant == expiry_type); + assert!(cache.contains_key(&key).unwrap()); + + let ttl = e.time_to_live(); + + println!("{}: {:?}", i, ttl); + + match i { + 0 => ttl + // The first entry should have a TTL between 1 and 5 seconds + .is_some_and(|duration| { + duration.as_millis() > 1 && duration.as_secs() <= 5 + }), + 1 => ttl + // The second entry should have a TTL between 5 and 10 seconds + .is_some_and(|duration| duration.as_secs() > 5 && duration.as_secs() <= 10), + 2 => ttl.is_some_and(|duration| { + let default_secs = default_expiration().as_secs(); + // The third entry should have a TTL of > 90% of the default + duration.as_secs() > ((default_secs * 9) / 10) + && duration.as_secs() <= default_secs + }), + // The fourth entry should never expire + 3 => ttl.is_none(), + _ => panic!("Unexpected index"), + } + })); + } + + // Ensure the first entry expires + sleep(5).await; + let entries = cache.entries().await; + assert_eq!(entries, keys_with_ttl.len() as u64 - 1); + assert!(!cache.contains_key(&keys_with_ttl[0].0).unwrap()); + assert!(cache.contains_key(&keys_with_ttl[1].0).unwrap()); + assert!(cache.contains_key(&keys_with_ttl[2].0).unwrap()); + assert!(cache.contains_key(&keys_with_ttl[3].0).unwrap()); + + // Ensure the second entry expires + sleep(5).await; + let entries = cache.entries().await; + assert_eq!(entries, keys_with_ttl.len() as u64 - 2); + assert!(!cache.contains_key(&keys_with_ttl[0].0).unwrap()); + assert!(!cache.contains_key(&keys_with_ttl[1].0).unwrap()); + assert!(cache.contains_key(&keys_with_ttl[2].0).unwrap()); + assert!(cache.contains_key(&keys_with_ttl[3].0).unwrap()); + + // Expire the last two entries + cache.remove(&keys_with_ttl[2].0).await.unwrap(); + cache.remove(&keys_with_ttl[3].0).await.unwrap(); + + // Ensure the last two entries are removed + let entries = cache.entries().await; + assert_eq!(entries, 0); + assert!(!cache.contains_key(&keys_with_ttl[0].0).unwrap()); + assert!(!cache.contains_key(&keys_with_ttl[1].0).unwrap()); + assert!(!cache.contains_key(&keys_with_ttl[2].0).unwrap()); + assert!(!cache.contains_key(&keys_with_ttl[3].0).unwrap()); + } +} diff --git a/rust/main/hyperlane-base/src/cache/optional_cache.rs b/rust/main/hyperlane-base/src/cache/optional_cache.rs new file mode 100644 index 00000000000..59a0d1339d4 --- /dev/null +++ b/rust/main/hyperlane-base/src/cache/optional_cache.rs @@ -0,0 +1,55 @@ +use std::fmt::Debug; + +use async_trait::async_trait; +use derive_new::new; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::cache::FunctionCallCache; + +use super::CacheResult; + +/// A Cache wrapper that instruments the cache calls with metrics. +#[derive(new, Debug, Clone)] +pub struct OptionalCache { + inner: Option, +} + +#[async_trait] +impl FunctionCallCache for OptionalCache +where + C: FunctionCallCache, +{ + /// Calls the inner cache if it exists, otherwise returns Ok(()) + async fn cache_call_result( + &self, + domain_name: &str, + fn_key: &str, + fn_params: &(impl Serialize + Send + Sync), + result: &(impl Serialize + Send + Sync), + ) -> CacheResult<()> { + if let Some(inner) = &self.inner { + return inner + .cache_call_result(domain_name, fn_key, fn_params, result) + .await; + } + Ok(()) + } + + /// Calls the inner cache if it exists, otherwise returns Ok(None) + async fn get_cached_call_result( + &self, + domain_name: &str, + method: &str, + fn_params: &(impl Serialize + Send + Sync), + ) -> CacheResult> + where + T: DeserializeOwned, + { + if let Some(inner) = &self.inner { + return inner + .get_cached_call_result::(domain_name, method, fn_params) + .await; + } + Ok(None) + } +} diff --git a/rust/main/hyperlane-base/src/contract_sync/mod.rs b/rust/main/hyperlane-base/src/contract_sync/mod.rs index d79b9a26160..fb95433bdf5 100644 --- a/rust/main/hyperlane-base/src/contract_sync/mod.rs +++ b/rust/main/hyperlane-base/src/contract_sync/mod.rs @@ -60,13 +60,19 @@ impl, I: Indexer> ContractSync store: S, indexer: I, metrics: ContractSyncMetrics, + broadcast_sender_enabled: bool, ) -> Self { + let broadcast_sender = if broadcast_sender_enabled { + T::broadcast_channel_size().map(BroadcastMpscSender::new) + } else { + None + }; Self { domain, store, indexer, metrics, - broadcast_sender: T::broadcast_channel_size().map(BroadcastMpscSender::new), + broadcast_sender, _phantom: PhantomData, } } diff --git a/rust/main/hyperlane-base/src/db/mod.rs b/rust/main/hyperlane-base/src/db/mod.rs index 04a3e59cc66..83a39386a27 100644 --- a/rust/main/hyperlane-base/src/db/mod.rs +++ b/rust/main/hyperlane-base/src/db/mod.rs @@ -1,11 +1,12 @@ +pub use self::storage_types::{InterchainGasExpenditureData, InterchainGasPaymentData}; pub use error::*; -use hyperlane_core::{ - GasPaymentKey, HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, - InterchainGasPaymentMeta, MerkleTreeInsertion, PendingOperationStatus, H256, -}; pub use rocks::*; -pub use self::storage_types::{InterchainGasExpenditureData, InterchainGasPaymentData}; +use hyperlane_core::{ + identifiers::UniqueIdentifier, GasPaymentKey, HyperlaneDomain, HyperlaneMessage, + InterchainGasPayment, InterchainGasPaymentMeta, MerkleTreeInsertion, PendingOperationStatus, + H256, +}; mod error; mod rocks; @@ -159,4 +160,17 @@ pub trait HyperlaneDb: Send + Sync { /// Retrieve the nonce of the highest processed message we're aware of fn retrieve_highest_seen_message_nonce_number(&self) -> DbResult>; + + /// Store payload id by message id + fn store_payload_id_by_message_id( + &self, + message_id: &H256, + payload_id: &UniqueIdentifier, + ) -> DbResult<()>; + + /// Retrieve payload id by message id + fn retrieve_payload_id_by_message_id( + &self, + message_id: &H256, + ) -> DbResult>; } diff --git a/rust/main/hyperlane-base/src/db/rocks/hyperlane_db.rs b/rust/main/hyperlane-base/src/db/rocks/hyperlane_db.rs index 711356b8a10..ef9cd44275d 100644 --- a/rust/main/hyperlane-base/src/db/rocks/hyperlane_db.rs +++ b/rust/main/hyperlane-base/src/db/rocks/hyperlane_db.rs @@ -3,18 +3,19 @@ use eyre::{bail, Result}; use tracing::{debug, instrument, trace}; use hyperlane_core::{ - Decode, Encode, GasPaymentKey, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, - HyperlaneSequenceAwareIndexerStoreReader, HyperlaneWatermarkedLogStore, Indexed, - InterchainGasExpenditure, InterchainGasPayment, InterchainGasPaymentMeta, LogMeta, - MerkleTreeInsertion, PendingOperationStatus, H256, + identifiers::UniqueIdentifier, Decode, Encode, GasPaymentKey, HyperlaneDomain, + HyperlaneLogStore, HyperlaneMessage, HyperlaneSequenceAwareIndexerStoreReader, + HyperlaneWatermarkedLogStore, Indexed, InterchainGasExpenditure, InterchainGasPayment, + InterchainGasPaymentMeta, LogMeta, MerkleTreeInsertion, PendingOperationStatus, H256, }; -use super::{DbError, TypedDB, DB}; use crate::db::{ storage_types::{InterchainGasExpenditureData, InterchainGasPaymentData}, HyperlaneDb, }; +use super::{DbError, TypedDB, DB}; + // these keys MUST not be given multiple uses in case multiple agents are // started with the same database and domain. @@ -36,6 +37,7 @@ const MERKLE_LEAF_INDEX_BY_MESSAGE_ID: &str = "merkle_leaf_index_by_message_id_" const MERKLE_TREE_INSERTION_BLOCK_NUMBER_BY_LEAF_INDEX: &str = "merkle_tree_insertion_block_number_by_leaf_index_"; const LATEST_INDEXED_GAS_PAYMENT_BLOCK: &str = "latest_indexed_gas_payment_block"; +const PAYLOAD_ID_BY_MESSAGE_ID: &str = "payload_id_by_message_id_"; /// Rocks DB result type pub type DbResult = std::result::Result; @@ -656,6 +658,21 @@ impl HyperlaneDb for HyperlaneRocksDB { // There's no unit struct Encode/Decode impl, so just use `bool` and always use the `Default::default()` key self.retrieve_value_by_key(HIGHEST_SEEN_MESSAGE_NONCE, &bool::default()) } + + fn store_payload_id_by_message_id( + &self, + message_id: &H256, + payload_id: &UniqueIdentifier, + ) -> DbResult<()> { + self.store_value_by_key(PAYLOAD_ID_BY_MESSAGE_ID, message_id, payload_id) + } + + fn retrieve_payload_id_by_message_id( + &self, + message_id: &H256, + ) -> DbResult> { + self.retrieve_value_by_key(PAYLOAD_ID_BY_MESSAGE_ID, message_id) + } } impl HyperlaneRocksDB { diff --git a/rust/main/hyperlane-base/src/lib.rs b/rust/main/hyperlane-base/src/lib.rs index 7f52c2e5bf4..4d1f7c30b52 100644 --- a/rust/main/hyperlane-base/src/lib.rs +++ b/rust/main/hyperlane-base/src/lib.rs @@ -12,6 +12,8 @@ pub mod settings; mod agent; pub use agent::*; +/// The local cache used by agents +pub mod cache; /// The local database used by agents pub mod db; diff --git a/rust/main/hyperlane-base/src/metadata.rs b/rust/main/hyperlane-base/src/metadata.rs index 3bdfce9af81..92d36c95e33 100644 --- a/rust/main/hyperlane-base/src/metadata.rs +++ b/rust/main/hyperlane-base/src/metadata.rs @@ -1,9 +1,26 @@ use derive_new::new; use serde::{Deserialize, Serialize}; +use crate::MetadataFromSettings; + /// Metadata about agent #[derive(Debug, Deserialize, Serialize, new)] pub struct AgentMetadata { /// Contains git commit hash of the agent binary pub git_sha: String, } + +/// Default is always the latest git commit hash at the time of build +impl MetadataFromSettings for AgentMetadata { + fn build_metadata(_settings: &T) -> Self { + Self { git_sha: git_sha() } + } +} + +/// Returns latest git commit hash at the time when agent was built. +/// +/// If .git was not present at the time of build, +/// the variable defaults to "VERGEN_IDEMPOTENT_OUTPUT". +pub fn git_sha() -> String { + env!("VERGEN_GIT_SHA").to_string() +} diff --git a/rust/main/hyperlane-base/src/metrics/agent_metrics.rs b/rust/main/hyperlane-base/src/metrics/agent_metrics.rs index 140116c1a55..b5fe6ce8288 100644 --- a/rust/main/hyperlane-base/src/metrics/agent_metrics.rs +++ b/rust/main/hyperlane-base/src/metrics/agent_metrics.rs @@ -206,7 +206,16 @@ impl ChainSpecificMetricsUpdater { } async fn update_block_details(&self) { - if let HyperlaneDomain::Unknown { .. } = self.conf.domain { + if let HyperlaneDomain::Unknown { + domain_id, + domain_name, + .. + } = &self.conf.domain + { + debug!( + domain_id, + domain_name, "Unknown domain, skipping chain metrics" + ); return; }; let chain = self.conf.domain.name(); @@ -218,7 +227,7 @@ impl ChainSpecificMetricsUpdater { return; } _ => { - trace!(chain, "No chain metrics available"); + debug!(chain, "No chain metrics available"); return; } }; @@ -226,6 +235,7 @@ impl ChainSpecificMetricsUpdater { let height = chain_metrics.latest_block.number as i64; trace!(chain, height, "Fetched block height for metrics"); self.chain_metrics.set_block_height(chain, height); + if self.chain_metrics.gas_price.is_some() { let protocol = self.conf.domain.domain_protocol(); let decimals_scale = 10f64.powf(decimals_by_protocol(protocol).into()); diff --git a/rust/main/hyperlane-base/src/metrics/cache.rs b/rust/main/hyperlane-base/src/metrics/cache.rs new file mode 100644 index 00000000000..ab7c6d2391a --- /dev/null +++ b/rust/main/hyperlane-base/src/metrics/cache.rs @@ -0,0 +1,12 @@ +use eyre::Result; + +use crate::cache::*; + +use super::CoreMetrics; + +pub(crate) fn create_cache_metrics(metrics: &CoreMetrics) -> Result { + Ok(MeteredCacheMetricsBuilder::default() + .hit_count(metrics.new_int_counter("hit_count", HIT_COUNT_HELP, HIT_COUNT_LABELS)?) + .miss_count(metrics.new_int_counter("miss_count", MISS_COUNT_HELP, MISS_COUNT_LABELS)?) + .build()?) +} diff --git a/rust/main/hyperlane-base/src/metrics/core.rs b/rust/main/hyperlane-base/src/metrics/core.rs index 324c0362138..ea95c37b086 100644 --- a/rust/main/hyperlane-base/src/metrics/core.rs +++ b/rust/main/hyperlane-base/src/metrics/core.rs @@ -16,8 +16,10 @@ use ethers_prometheus::middleware::MiddlewareMetrics; use hyperlane_core::{HyperlaneDomain, H160}; use hyperlane_metric::prometheus_metric::PrometheusClientMetrics; +use crate::cache::MeteredCacheMetrics; use crate::metrics::{ - json_rpc_client::create_json_rpc_client_metrics, provider::create_provider_metrics, + cache::create_cache_metrics, json_rpc_client::create_json_rpc_client_metrics, + provider::create_provider_metrics, }; /// Macro to prefix a string with the namespace. @@ -53,9 +55,18 @@ pub struct CoreMetrics { latest_checkpoint: IntGaugeVec, + announced: IntGaugeVec, + backfill_complete: IntGaugeVec, + reached_initial_consistency: IntGaugeVec, + + // metadata building metrics + metadata_build_count: IntCounterVec, + metadata_build_duration: CounterVec, + /// Set of metrics that tightly wrap the JsonRpcClient for use with the /// quorum provider. client_metrics: OnceLock, + cache_metrics: OnceLock, /// Set of provider-specific metrics. These only need to get created once. provider_metrics: OnceLock, @@ -205,6 +216,36 @@ impl CoreMetrics { registry )?; + let announced = register_int_gauge_vec_with_registry!( + opts!( + namespaced!("announced"), + "Whether the validator has been announced", + const_labels_ref + ), + &["chain"], + registry + )?; + + let backfill_complete = register_int_gauge_vec_with_registry!( + opts!( + namespaced!("backfill_complete"), + "Whether backfilling checkpoints is complete", + const_labels_ref + ), + &["chain"], + registry + )?; + + let reached_initial_consistency = register_int_gauge_vec_with_registry!( + opts!( + namespaced!("reached_initial_consistency"), + "Whether the tree has reached an initial point of consistency", + const_labels_ref + ), + &["chain"], + registry + )?; + let operations_processed_count = register_int_counter_vec_with_registry!( opts!( namespaced!("operations_processed_count"), @@ -225,6 +266,26 @@ impl CoreMetrics { registry )?; + let metadata_build_count = register_int_counter_vec_with_registry!( + opts!( + namespaced!("metadata_build_count"), + "Total number of times metadata was build", + const_labels_ref + ), + &["app_context", "origin", "remote", "status"], + registry + )?; + + let metadata_build_duration = register_counter_vec_with_registry!( + opts!( + namespaced!("metadata_build_duration"), + "Duration of metadata build times", + const_labels_ref + ), + &["app_context", "origin", "remote", "status"], + registry + )?; + Ok(Self { agent_name: for_agent.into(), registry, @@ -249,8 +310,16 @@ impl CoreMetrics { latest_checkpoint, + announced, + backfill_complete, + reached_initial_consistency, + + metadata_build_count, + metadata_build_duration, + client_metrics: OnceLock::new(), provider_metrics: OnceLock::new(), + cache_metrics: OnceLock::new(), validator_metrics: ValidatorObservabilityMetricManager::new( observed_validator_latest_index.clone(), @@ -277,6 +346,13 @@ impl CoreMetrics { .clone() } + /// Create the cache metrics attached to this core metrics instance. + pub fn cache_metrics(&self) -> MeteredCacheMetrics { + self.cache_metrics + .get_or_init(|| create_cache_metrics(self).expect("Failed to create cache metrics!")) + .clone() + } + /// Create and register a new int gauge. pub fn new_int_gauge( &self, @@ -427,6 +503,41 @@ impl CoreMetrics { self.latest_checkpoint.clone() } + /// Set the validator to be announced + /// + /// Labels: + /// - `chain`: Chain the validator was announced on. + pub fn set_announced(&self, origin_chain: HyperlaneDomain) { + self.announced + .clone() + .with_label_values(&[origin_chain.name()]) + .set(1); + } + + /// Whether the validator has been announced. + /// + /// Labels: + /// - `chain`: Chain the operation was submitted to. + pub fn announced(&self) -> IntGaugeVec { + self.announced.clone() + } + + /// Whether the validator has completed backfilling. + /// + /// Labels: + /// - `chain`: Chain the operation was submitted to. + pub fn backfill_complete(&self) -> IntGaugeVec { + self.backfill_complete.clone() + } + + /// Whether the validator has ever synced to the tip of the chain. + /// + /// Labels: + /// - `chain`: Chain the operation was submitted to. + pub fn reached_initial_consistency(&self) -> IntGaugeVec { + self.reached_initial_consistency.clone() + } + /// Measure of the queue lengths in Submitter instances /// /// Labels: @@ -500,6 +611,30 @@ impl CoreMetrics { self.span_counts.clone() } + /// The number of metadata built by this process during its + /// lifetime. + /// + /// Labels: + /// - `app_context`: Context + /// - `origin`: Chain the message came from. + /// - `remote`: Chain we delivered the message to. + /// - `status`: success or failure + pub fn metadata_build_count(&self) -> IntCounterVec { + self.metadata_build_count.clone() + } + + /// The durations of metadata build by this process during its + /// lifetime. + /// + /// Labels: + /// - `app_context`: Context + /// - `origin`: Chain the message came from. + /// - `remote`: Chain we delivered the message to. + /// - `status`: success or failure + pub fn metadata_build_duration(&self) -> CounterVec { + self.metadata_build_duration.clone() + } + /// Counts of tracing (logging framework) span events. /// /// Tracking the number of events emitted helps us verify logs are not being diff --git a/rust/main/hyperlane-base/src/metrics/mod.rs b/rust/main/hyperlane-base/src/metrics/mod.rs index 6220f75453c..90cbab1b110 100644 --- a/rust/main/hyperlane-base/src/metrics/mod.rs +++ b/rust/main/hyperlane-base/src/metrics/mod.rs @@ -8,6 +8,7 @@ pub const NAMESPACE: &str = "hyperlane"; mod core; mod agent_metrics; +mod cache; mod json_rpc_client; mod provider; mod runtime_metrics; diff --git a/rust/main/hyperlane-base/src/settings/base.rs b/rust/main/hyperlane-base/src/settings/base.rs index 0fad293caed..54ddf810872 100644 --- a/rust/main/hyperlane-base/src/settings/base.rs +++ b/rust/main/hyperlane-base/src/settings/base.rs @@ -162,6 +162,7 @@ impl Settings { sync_metrics: &ContractSyncMetrics, store: Arc, advanced_log_meta: bool, + broadcast_sender_enabled: bool, ) -> eyre::Result>> where T: Indexable + Debug, @@ -177,6 +178,7 @@ impl Settings { store.clone() as SequenceAwareLogStore<_>, indexer, sync_metrics.clone(), + broadcast_sender_enabled, ))) } @@ -188,6 +190,7 @@ impl Settings { sync_metrics: &ContractSyncMetrics, store: Arc, advanced_log_meta: bool, + broadcast_sender_enabled: bool, ) -> eyre::Result>> where T: Indexable + Debug, @@ -203,6 +206,7 @@ impl Settings { store.clone() as WatermarkLogStore<_>, indexer, sync_metrics.clone(), + broadcast_sender_enabled, ))) } @@ -216,6 +220,7 @@ impl Settings { sync_metrics: &ContractSyncMetrics, stores: HashMap>, advanced_log_meta: bool, + broadcast_sender_enabled: bool, ) -> Result>>> where T: Indexable + Debug + Send + Sync + Clone + Eq + Hash + 'static, @@ -230,7 +235,14 @@ impl Settings { for domain in domains { let store = stores.get(domain).unwrap().clone(); let sync = self - .contract_sync(domain, metrics, sync_metrics, store, advanced_log_meta) + .contract_sync( + domain, + metrics, + sync_metrics, + store, + advanced_log_meta, + broadcast_sender_enabled, + ) .await?; syncs.push(sync); } @@ -251,6 +263,7 @@ impl Settings { sync_metrics: &ContractSyncMetrics, store: Arc, advanced_log_meta: bool, + broadcast_sender_enabled: bool, ) -> Result>> where T: Indexable + Debug + Send + Sync + Clone + Eq + Hash + 'static, @@ -262,11 +275,25 @@ impl Settings { { let sync = match T::indexing_cursor(domain.domain_protocol()) { CursorType::SequenceAware => self - .sequenced_contract_sync(domain, metrics, sync_metrics, store, advanced_log_meta) + .sequenced_contract_sync( + domain, + metrics, + sync_metrics, + store, + advanced_log_meta, + broadcast_sender_enabled, + ) .await .map(|r| r as Arc>)?, CursorType::RateLimited => self - .watermark_contract_sync(domain, metrics, sync_metrics, store, advanced_log_meta) + .watermark_contract_sync( + domain, + metrics, + sync_metrics, + store, + advanced_log_meta, + broadcast_sender_enabled, + ) .await .map(|r| r as Arc>)?, }; diff --git a/rust/main/hyperlane-base/src/settings/chains.rs b/rust/main/hyperlane-base/src/settings/chains.rs index c929a038ae4..e4c93d458b0 100644 --- a/rust/main/hyperlane-base/src/settings/chains.rs +++ b/rust/main/hyperlane-base/src/settings/chains.rs @@ -1,4 +1,6 @@ -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; use axum::async_trait; use ethers::prelude::Selector; @@ -7,11 +9,11 @@ use serde_json::Value; use ethers_prometheus::middleware::{ContractInfo, PrometheusMiddlewareConf}; use hyperlane_core::{ - config::OperationBatchConfig, AggregationIsm, CcipReadIsm, ChainResult, ContractLocator, + config::OpSubmissionConfig, AggregationIsm, CcipReadIsm, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneDomain, HyperlaneDomainProtocol, HyperlaneMessage, HyperlaneProvider, IndexMode, InterchainGasPaymaster, InterchainGasPayment, InterchainSecurityModule, Mailbox, MerkleTreeHook, MerkleTreeInsertion, MultisigIsm, ReorgPeriod, RoutingIsm, - SequenceAwareIndexer, ValidatorAnnounce, H256, + SequenceAwareIndexer, SubmitterType, ValidatorAnnounce, H256, }; use hyperlane_metric::prometheus_metric::ChainInfo; use hyperlane_operation_verifier::ApplicationOperationVerifier; @@ -69,6 +71,10 @@ pub struct ChainConf { pub domain: HyperlaneDomain, /// Signer configuration for this chain pub signer: Option, + /// Submitter type for this chain + pub submitter: SubmitterType, + /// The estimated block time, i.e. the average time the next block is added to the chain + pub estimated_block_time: Duration, /// The reorg period of the chain, i.e. the number of blocks until finality pub reorg_period: ReorgPeriod, /// Addresses of contracts on the chain @@ -149,6 +155,9 @@ impl TryFromWithMetrics for MerkleTreeHookIndexer { /// A connection to _some_ blockchain. #[derive(Clone, Debug)] +// TODO: re-enable this clippy check once the new submitter is shipped, +// since it might take in configs in a different way +#[allow(clippy::large_enum_variant)] pub enum ChainConnectionConf { /// Ethereum configuration Ethereum(h_eth::ConnectionConf), @@ -175,11 +184,11 @@ impl ChainConnectionConf { } /// Get the message batch configuration for this chain. - pub fn operation_batch_config(&self) -> Option<&OperationBatchConfig> { + pub fn operation_submission_config(&self) -> Option<&OpSubmissionConfig> { match self { - Self::Ethereum(conf) => Some(&conf.operation_batch), - Self::Cosmos(conf) => Some(&conf.operation_batch), - Self::Sealevel(conf) => Some(&conf.operation_batch), + Self::Ethereum(conf) => Some(&conf.op_submission_config), + Self::Cosmos(conf) => Some(&conf.op_submission_config), + Self::Sealevel(conf) => Some(&conf.op_submission_config), _ => None, } } diff --git a/rust/main/hyperlane-base/src/settings/checkpoint_syncer.rs b/rust/main/hyperlane-base/src/settings/checkpoint_syncer.rs index b1335f12e9d..bd1af317ef9 100644 --- a/rust/main/hyperlane-base/src/settings/checkpoint_syncer.rs +++ b/rust/main/hyperlane-base/src/settings/checkpoint_syncer.rs @@ -2,11 +2,11 @@ use crate::{ CheckpointSyncer, GcsStorageClientBuilder, LocalStorage, S3Storage, GCS_SERVICE_ACCOUNT_KEY, GCS_USER_SECRET, }; +use aws_config::Region; use core::str::FromStr; use eyre::{eyre, Context, Report, Result}; use hyperlane_core::{ChainCommunicationError, ReorgEvent}; use prometheus::IntGauge; -use rusoto_core::Region; use std::{env, path::PathBuf}; use tracing::error; use ya_gcp::{AuthFlow, ServiceAccountAuth}; @@ -76,9 +76,15 @@ impl FromStr for CheckpointSyncerConf { Ok(CheckpointSyncerConf::S3 { bucket: bucket.into(), folder, - region: region - .parse() - .context("Invalid region when parsing storage location")?, + // Wildly, aws_config doesn't provide any client-side way to validate a region string, so while + // we still have Rusoto around we just use that to validate the region string :) + region: aws_config::Region::new( + region + .parse::() + .context("Invalid region when parsing storage location")? + .name() + .to_owned(), + ), }) } "file" => Ok(CheckpointSyncerConf::LocalStorage { diff --git a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs index fa73094f2a2..dbb5839fceb 100644 --- a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs @@ -6,7 +6,7 @@ use url::Url; use h_eth::TransactionOverrides; -use hyperlane_core::config::{ConfigErrResultExt, OperationBatchConfig}; +use hyperlane_core::config::{ConfigErrResultExt, OpSubmissionConfig}; use hyperlane_core::{config::ConfigParsingError, HyperlaneDomainProtocol, NativeToken}; use crate::settings::envs::*; @@ -20,7 +20,7 @@ pub fn build_ethereum_connection_conf( chain: &ValueParser, err: &mut ConfigParsingError, default_rpc_consensus_type: &str, - operation_batch: OperationBatchConfig, + operation_batch: OpSubmissionConfig, ) -> Option { let Some(first_url) = rpcs.to_owned().clone().into_iter().next() else { return None; @@ -68,13 +68,57 @@ pub fn build_ethereum_connection_conf( .get_opt_key("maxPriorityFeePerGas") .parse_u256() .end(), + + min_gas_price: value_parser + .chain(err) + .get_opt_key("minGasPrice") + .parse_u256() + .end(), + min_fee_per_gas: value_parser + .chain(err) + .get_opt_key("minFeePerGas") + .parse_u256() + .end(), + min_priority_fee_per_gas: value_parser + .chain(err) + .get_opt_key("minPriorityFeePerGas") + .parse_u256() + .end(), + + gas_limit_multiplier_denominator: value_parser + .chain(err) + .get_opt_key("gasLimitMultiplierDenominator") + .parse_u256() + .end(), + gas_limit_multiplier_numerator: value_parser + .chain(err) + .get_opt_key("gasLimitMultiplierNumerator") + .parse_u256() + .end(), + + gas_price_multiplier_denominator: value_parser + .chain(err) + .get_opt_key("gasPriceMultiplierDenominator") + .parse_u256() + .end(), + gas_price_multiplier_numerator: value_parser + .chain(err) + .get_opt_key("gasPriceMultiplierNumerator") + .parse_u256() + .end(), + + gas_price_cap: value_parser + .chain(err) + .get_opt_key("gasPriceCap") + .parse_u256() + .end(), }) .unwrap_or_default(); Some(ChainConnectionConf::Ethereum(h_eth::ConnectionConf { rpc_connection: rpc_connection_conf?, transaction_overrides, - operation_batch, + op_submission_config: operation_batch, })) } @@ -82,7 +126,7 @@ pub fn build_cosmos_connection_conf( rpcs: &[Url], chain: &ValueParser, err: &mut ConfigParsingError, - operation_batch: OperationBatchConfig, + operation_batch: OpSubmissionConfig, ) -> Option { let mut local_err = ConfigParsingError::default(); let grpcs = @@ -164,7 +208,7 @@ pub fn build_cosmos_native_connection_conf( rpcs: &[Url], chain: &ValueParser, err: &mut ConfigParsingError, - operation_batch: OperationBatchConfig, + operation_batch: OpSubmissionConfig, ) -> Option { let mut local_err = ConfigParsingError::default(); let grpcs = @@ -262,7 +306,7 @@ fn build_sealevel_connection_conf( urls: &[Url], chain: &ValueParser, err: &mut ConfigParsingError, - operation_batch: OperationBatchConfig, + operation_batch: OpSubmissionConfig, ) -> Option { let mut local_err = ConfigParsingError::default(); @@ -276,7 +320,7 @@ fn build_sealevel_connection_conf( } else { Some(ChainConnectionConf::Sealevel(h_sealevel::ConnectionConf { urls: urls.to_owned(), - operation_batch, + op_submission_config: operation_batch, native_token, priority_fee_oracle: priority_fee_oracle.unwrap(), transaction_submitter: transaction_submitter.unwrap(), @@ -458,7 +502,7 @@ pub fn build_connection_conf( chain: &ValueParser, err: &mut ConfigParsingError, default_rpc_consensus_type: &str, - operation_batch: OperationBatchConfig, + operation_batch: OpSubmissionConfig, ) -> Option { match domain_protocol { HyperlaneDomainProtocol::Ethereum => build_ethereum_connection_conf( diff --git a/rust/main/hyperlane-base/src/settings/parser/mod.rs b/rust/main/hyperlane-base/src/settings/parser/mod.rs index cf0c179fe1c..c678238fb1b 100644 --- a/rust/main/hyperlane-base/src/settings/parser/mod.rs +++ b/rust/main/hyperlane-base/src/settings/parser/mod.rs @@ -7,6 +7,7 @@ use std::{ collections::{HashMap, HashSet}, default::Default, + time::Duration, }; use convert_case::{Case, Casing}; @@ -19,7 +20,7 @@ use url::Url; use h_cosmos::RawCosmosAmount; use hyperlane_core::{ cfg_unwrap_all, config::*, HyperlaneDomain, HyperlaneDomainProtocol, - HyperlaneDomainTechnicalStack, IndexMode, ReorgPeriod, + HyperlaneDomainTechnicalStack, IndexMode, ReorgPeriod, SubmitterType, }; use crate::settings::{ @@ -134,6 +135,21 @@ fn parse_chain( .and_then(parse_signer) .end(); + let submitter = chain + .chain(&mut err) + .get_opt_key("submitter") + .parse_from_str::("Invalid Submitter type") + .unwrap_or_default(); + + // measured in seconds (with fractions) + let estimated_block_time = chain + .chain(&mut err) + .get_opt_key("blocks") + .get_key("estimateBlockTime") + .parse_value("Invalid estimateBlockTime") + .map(Duration::from_secs_f64) + .unwrap_or(Duration::from_secs(1)); + let reorg_period = chain .chain(&mut err) .get_opt_key("blocks") @@ -204,6 +220,18 @@ fn parse_chain( .parse_u32() .unwrap_or(1); + let bypass_batch_simulation = chain + .chain(&mut err) + .get_opt_key("bypassBatchSimulation") + .parse_bool() + .unwrap_or(false); + + let max_submit_queue_length = chain + .chain(&mut err) + .get_opt_key("maxSubmitQueueLength") + .parse_u32() + .end(); + cfg_unwrap_all!(&chain.cwp, err: [domain]); let connection = build_connection_conf( domain.domain_protocol(), @@ -211,9 +239,11 @@ fn parse_chain( &chain, &mut err, default_rpc_consensus_type, - OperationBatchConfig { + OpSubmissionConfig { batch_contract_address, max_batch_size, + bypass_batch_simulation, + max_submit_queue_length, }, ); @@ -221,6 +251,8 @@ fn parse_chain( err.into_result(ChainConf { domain, signer, + submitter, + estimated_block_time, reorg_period, addresses: CoreContractAddresses { mailbox, diff --git a/rust/main/hyperlane-base/src/settings/trace/mod.rs b/rust/main/hyperlane-base/src/settings/trace/mod.rs index 9f06b3ac0b8..bae93d9697b 100644 --- a/rust/main/hyperlane-base/src/settings/trace/mod.rs +++ b/rust/main/hyperlane-base/src/settings/trace/mod.rs @@ -76,6 +76,8 @@ impl TracingConfig { .with_target("tendermint", Level::Info) .with_target("tokio", Level::Debug) .with_target("tokio_util", Level::Debug) + .with_target("aws_sdk_s3", Level::Info) + .with_target("aws_smithy_runtime_api", Level::Info) // Enable Trace level for Tokio if you want to use tokio-console // .with_target("tokio", Level::Trace) // .with_target("tokio_util", Level::Trace) diff --git a/rust/main/hyperlane-base/src/traits/checkpoint_syncer.rs b/rust/main/hyperlane-base/src/traits/checkpoint_syncer.rs index 0dc6a1e6b7a..66e2999267b 100644 --- a/rust/main/hyperlane-base/src/traits/checkpoint_syncer.rs +++ b/rust/main/hyperlane-base/src/traits/checkpoint_syncer.rs @@ -3,7 +3,6 @@ use std::fmt::Debug; use async_trait::async_trait; use eyre::Result; -use crate::AgentMetadata; use hyperlane_core::{ReorgEvent, SignedAnnouncement, SignedCheckpointWithMessageId}; /// A generic trait to read/write Checkpoints offchain @@ -29,7 +28,7 @@ pub trait CheckpointSyncer: Debug + Send + Sync { signed_checkpoint: &SignedCheckpointWithMessageId, ) -> Result<()>; /// Write the agent metadata to this syncer - async fn write_metadata(&self, metadata: &AgentMetadata) -> Result<()>; + async fn write_metadata(&self, serialized_metadata: &str) -> Result<()>; /// Write the signed announcement to this syncer async fn write_announcement(&self, signed_announcement: &SignedAnnouncement) -> Result<()>; /// Return the announcement storage location for this syncer diff --git a/rust/main/hyperlane-base/src/types/gcs_storage.rs b/rust/main/hyperlane-base/src/types/gcs_storage.rs index 93219f85104..9b8a804fab1 100644 --- a/rust/main/hyperlane-base/src/types/gcs_storage.rs +++ b/rust/main/hyperlane-base/src/types/gcs_storage.rs @@ -1,4 +1,4 @@ -use crate::{AgentMetadata, CheckpointSyncer}; +use crate::CheckpointSyncer; use async_trait::async_trait; use derive_new::new; use eyre::{bail, Result}; @@ -247,10 +247,10 @@ impl CheckpointSyncer for GcsStorageClient { } /// Write the agent metadata to this syncer - #[instrument(skip(self, metadata))] - async fn write_metadata(&self, metadata: &AgentMetadata) -> Result<()> { + #[instrument(skip(self, serialized_metadata))] + async fn write_metadata(&self, serialized_metadata: &str) -> Result<()> { let object_name = self.object_path(METADATA_KEY); - let data = serde_json::to_string_pretty(metadata)?.into_bytes(); + let data = serialized_metadata.to_owned().into_bytes(); self.upload_and_log(&object_name, data).await } diff --git a/rust/main/hyperlane-base/src/types/local_storage.rs b/rust/main/hyperlane-base/src/types/local_storage.rs index bb1ebf1239a..62236b478d6 100644 --- a/rust/main/hyperlane-base/src/types/local_storage.rs +++ b/rust/main/hyperlane-base/src/types/local_storage.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use crate::traits::CheckpointSyncer; -use crate::AgentMetadata; use async_trait::async_trait; use eyre::{Context, Result}; use hyperlane_core::{ReorgEvent, SignedAnnouncement, SignedCheckpointWithMessageId}; @@ -99,10 +98,9 @@ impl CheckpointSyncer for LocalStorage { Ok(()) } - async fn write_metadata(&self, metadata: &AgentMetadata) -> Result<()> { - let serialized_metadata = serde_json::to_string_pretty(metadata)?; + async fn write_metadata(&self, serialized_metadata: &str) -> Result<()> { let path = self.metadata_file_path(); - tokio::fs::write(&path, &serialized_metadata) + tokio::fs::write(&path, serialized_metadata) .await .with_context(|| format!("Writing agent metadata to {path:?}"))?; Ok(()) diff --git a/rust/main/hyperlane-base/src/types/multisig.rs b/rust/main/hyperlane-base/src/types/multisig.rs index 35fe3715098..9cbbfb40117 100644 --- a/rust/main/hyperlane-base/src/types/multisig.rs +++ b/rust/main/hyperlane-base/src/types/multisig.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use derive_new::new; use eyre::Result; +use futures::StreamExt; use tracing::{debug, instrument, warn}; use hyperlane_core::{ @@ -17,8 +18,7 @@ use crate::{CheckpointSyncer, CoreMetrics}; pub struct MultisigCheckpointSyncer { /// The checkpoint syncer for each valid validator signer address checkpoint_syncers: HashMap>, - metrics: Arc, - app_context: Option, + metrics: Option<(Arc, String)>, // first arg is the metrics, second is the app context } impl MultisigCheckpointSyncer { @@ -31,42 +31,56 @@ impl MultisigCheckpointSyncer { validators: &[H256], origin: &HyperlaneDomain, destination: &HyperlaneDomain, - ) -> Vec { + ) -> Vec<(H160, u32)> { // Get the latest_index from each validator's checkpoint syncer. // If a validator does not return a latest index, None is recorded so // this can be surfaced in the metrics. let mut latest_indices: HashMap> = HashMap::with_capacity(validators.len()); - for validator in validators { - let address = H160::from(*validator); - debug!( - ?address, - "Getting latest checkpoint from validator via checkpoint syncer", - ); - if let Some(checkpoint_syncer) = self.checkpoint_syncers.get(&address) { - // Gracefully handle errors getting the latest_index - match checkpoint_syncer.latest_index().await { - Ok(Some(index)) => { - debug!(?address, ?index, "Validator returned latest index"); - latest_indices.insert(H160::from(*validator), Some(index)); - } - result => { - debug!( - ?address, - ?result, - "Failed to get latest index from validator" - ); - latest_indices.insert(H160::from(*validator), None); - } + let syncer = validators + .iter() + .map(|v| H160::from(*v)) + .filter_map(|v| { + if let Some(checkpoint_syncer) = self.checkpoint_syncers.get(&v) { + Some((v, checkpoint_syncer)) + } else { + warn!(validator=%v, "Checkpoint syncer is not provided for validator"); + None + } + }) + .collect::>(); + let futures = syncer + .iter() + .map( + |(v, checkpoint_syncer)| async move { (v, checkpoint_syncer.latest_index().await) }, + ) + .collect::>(); + + let validator_index_results = futures::stream::iter(futures) + .buffer_unordered(10) + .collect::>() + .await; + + for (validator, latest_index) in validator_index_results { + match latest_index { + Ok(Some(index)) => { + debug!(?validator, ?index, "Validator returned latest index"); + latest_indices.insert(*validator, Some(index)); + } + result => { + debug!( + ?validator, + ?result, + "Failed to get latest index from validator" + ); + latest_indices.insert(*validator, None); } - } else { - warn!(?address, "Checkpoint syncer is not provided for validator"); } } - if let Some(app_context) = &self.app_context { - self.metrics + if let Some((metrics, app_context)) = &self.metrics { + metrics .validator_metrics .set_validator_latest_checkpoints( origin, @@ -78,7 +92,10 @@ impl MultisigCheckpointSyncer { } // Filter out any validators that did not return a latest index - latest_indices.values().copied().flatten().collect() + latest_indices + .into_iter() + .filter_map(|(address, index)| index.map(|i| (address, i))) + .collect() } /// Attempts to get the latest checkpoint with a quorum of signatures among @@ -119,8 +136,15 @@ impl MultisigCheckpointSyncer { // Sort in descending order. The n'th index will represent // the highest index for which we (supposedly) have (n+1) signed checkpoints - latest_indices.sort_by(|a, b| b.cmp(a)); - if let Some(&highest_quorum_index) = latest_indices.get(threshold - 1) { + latest_indices.sort_by(|a, b| b.1.cmp(&a.1)); + + // create a slice with the sorted validators + let validators = latest_indices + .iter() + .map(|(address, _)| H256::from(*address)) + .collect::>(); + + if let Some(&(_, highest_quorum_index)) = latest_indices.get(threshold - 1) { // The highest viable checkpoint index is the minimum of the highest index // we (supposedly) have a quorum for, and the maximum index for which we can // generate a proof. @@ -129,9 +153,10 @@ impl MultisigCheckpointSyncer { debug!(%start_index, %highest_quorum_index, "Highest quorum index is below the minimum index"); return Ok(None); } + for index in (minimum_index..=start_index).rev() { if let Ok(Some(checkpoint)) = - self.fetch_checkpoint(validators, threshold, index).await + self.fetch_checkpoint(&validators, threshold, index).await { return Ok(Some(checkpoint)); } @@ -157,14 +182,39 @@ impl MultisigCheckpointSyncer { let mut signed_checkpoints_per_root: HashMap> = HashMap::new(); - for validator in validators.iter() { - let addr = H160::from(*validator); - if let Some(checkpoint_syncer) = self.checkpoint_syncers.get(&addr) { + // we iterate in batches of N=threshold*1.5 to avoid waiting for all validators. + // This reaches a quorum faster without having to fetch all the signatures. + + // Also limit this number in case we have a large threshold + let batch_size = (threshold as f64 * 1.5) as usize; + let batch_size = batch_size.clamp(1, 10); + + for validators in validators.chunks(batch_size) { + // Go through each validator and get the checkpoint syncer. + // Create a future for each validator that fetches its signed checkpoint + let futures = validators + .iter() + .filter_map(|address| { + if let Some(syncer) = self.checkpoint_syncers.get(&H160::from(*address)) { + Some((address, syncer)) + } else { + debug!(validator=%address, "Checkpoint syncer not found"); + None + } + }) + .map(|(address, syncer)| { + let checkpoint_syncer = syncer.clone(); + async move { (address, checkpoint_syncer.fetch_checkpoint(index).await) } + }) + .collect::>(); + + let checkpoints = futures::future::join_all(futures).await; + + for (validator, checkpoint) in checkpoints { // Gracefully ignore an error fetching the checkpoint from a validator's // checkpoint syncer, which can happen if the validator has not // signed the checkpoint at `index`. - if let Ok(Some(signed_checkpoint)) = checkpoint_syncer.fetch_checkpoint(index).await - { + if let Ok(Some(signed_checkpoint)) = checkpoint { // If the signed checkpoint is for a different index, ignore it if signed_checkpoint.value.index != index { debug!( @@ -216,12 +266,135 @@ impl MultisigCheckpointSyncer { "Unable to find signed checkpoint" ); } - } else { - debug!(%validator, "Unable to find checkpoint syncer"); - continue; } } debug!("No quorum checkpoint found for message"); Ok(None) } } + +#[cfg(test)] +mod test { + + use std::str::FromStr; + + use aws_config::Region; + use hyperlane_core::KnownHyperlaneDomain; + + use crate::S3Storage; + + use super::*; + + #[tokio::test] + #[ignore] + #[tracing_test::traced_test] + async fn test_s3_checkpoint_syncer() { + let validators = vec![ + ( + "0x4d966438fe9E2B1e7124c87bBB90cB4F0F6C59a1", + ( + "hyperlane-mainnet3-arbitrum-validator-0".to_string(), + Region::new("us-east-1"), + ), + ), + ( + "0x5450447aeE7B544c462C9352bEF7cAD049B0C2Dc", + ( + "zpl-hyperlane-v3-arbitrum".to_string(), + Region::new("eu-central-1"), + ), + ), + ( + "0xec68258A7c882AC2Fc46b81Ce80380054fFB4eF2", + ( + "dsrv-hyperlane-v3-validator-signatures-validator7-arbitrum".to_string(), + Region::new("eu-central-1"), + ), + ), + ( + "0x38C7A4ca1273ead2E867d096aDBCDD0e2AcB21D8", + ( + "hyperlane-v3-validator-signatures-everstake-one-arbitrum".to_string(), + Region::new("us-east-2"), + ), + ), + ( + "0xb3AC35d3988bCA8C2fFD195b1c6bee18536B317b", + ( + "can-outrun-imperial-starships-v3-arbitrum".to_string(), + Region::new("eu-west-1"), + ), + ), + ( + "0x14d0B24d3a8F3aAD17DB4b62cBcEC12821c98Cb3", + ( + "hyperlane-validator-signatures-bwarelabs-ethereum/arbitrum".to_string(), + Region::new("eu-north-1"), + ), + ), + ( + "0xc4b877Dd49ABe9B38EA9184683f9664c0F9FADe3", + ( + "arbitrum-validator-signatures/arbitrum".to_string(), + Region::new("us-east-1"), + ), + ), + ]; + + let syncers = validators + .iter() + .map(|(address, (bucket, region))| { + let syncer = S3Storage::new(bucket.clone(), None, region.clone(), None); + ( + H160::from_str(address).unwrap(), + Arc::new(syncer) as Arc, + ) + }) + .collect::>(); + + // Create a multisig checkpoint syncer + let multisig_syncer = MultisigCheckpointSyncer::new(syncers, None); + + let validators = validators + .iter() + .map(|(address, _)| { + let address: H256 = H160::from_str(address).unwrap().into(); + address + }) + .collect::>(); + + // get the latest checkpoint from each validator + let mut latest_indices = multisig_syncer + .get_validator_latest_checkpoints_and_update_metrics( + validators.as_slice(), + &HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum), + &HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum), + ) + .await; + latest_indices.sort_by(|a, b| b.cmp(a)); + + let lowest_index = *latest_indices.last().unwrap(); + + let start_time = std::time::Instant::now(); + + for threshold in 2..=6 { + println!("Starting to fetch checkpoints with threshold {}", threshold); + if let Some(&(_, highest_quorum_index)) = latest_indices.get(threshold - 1) { + let result = multisig_syncer + .fetch_checkpoint_in_range( + validators.as_slice(), + threshold, + lowest_index.1, + highest_quorum_index, + &HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum), + &HyperlaneDomain::Known(KnownHyperlaneDomain::Arbitrum), + ) + .await; + assert!(result.is_ok(), "Failed to fetch checkpoint"); + } + } + + let elapsed = start_time.elapsed(); + println!("Fetched checkpoints in {}ms", elapsed.as_millis()); + } +} diff --git a/rust/main/hyperlane-base/src/types/s3_storage.rs b/rust/main/hyperlane-base/src/types/s3_storage.rs index ea04a1c4f9a..8aa6ff225dc 100644 --- a/rust/main/hyperlane-base/src/types/s3_storage.rs +++ b/rust/main/hyperlane-base/src/types/s3_storage.rs @@ -1,27 +1,24 @@ use std::{fmt, sync::OnceLock, time::Duration}; use async_trait::async_trait; +use aws_config::{timeout::TimeoutConfig, BehaviorVersion, ConfigLoader, Region}; +use aws_sdk_s3::{ + error::SdkError, + operation::{get_object::GetObjectError as SdkGetObjectError, head_object::HeadObjectError}, + Client, +}; +use dashmap::DashMap; use derive_new::new; use eyre::{bail, Result}; -use futures_util::TryStreamExt; use hyperlane_core::{ReorgEvent, SignedAnnouncement, SignedCheckpointWithMessageId}; use prometheus::IntGauge; -use rusoto_core::{ - credential::{Anonymous, AwsCredentials, StaticProvider}, - Region, RusotoError, -}; -use rusoto_s3::{GetObjectError, GetObjectRequest, PutObjectRequest, S3Client, S3}; -use tokio::time::timeout; +use tokio::sync::OnceCell; -use crate::types::utils; -use crate::{ - settings::aws_credentials::AwsChainCredentialsProvider, AgentMetadata, CheckpointSyncer, -}; +use crate::CheckpointSyncer; -/// The timeout for S3 requests. Rusoto doesn't offer timeout configuration -/// out of the box, so S3 requests must be wrapped with a timeout. -/// See https://github.com/rusoto/rusoto/issues/1795. -const S3_REQUEST_TIMEOUT_SECONDS: u64 = 30; +/// The timeout for all S3 operations. +const S3_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); +const S3_MAX_OBJECT_SIZE: i64 = 50 * 1024; // 50KiB #[derive(Clone, new)] /// Type for reading/writing to S3 @@ -32,16 +29,25 @@ pub struct S3Storage { folder: Option, /// The region of the bucket. region: Region, - /// A client with AWS credentials. - #[new(default)] - authenticated_client: OnceLock, - /// A client without credentials for anonymous requests. + /// A client with AWS credentials. This client is not initialized globally and has a lifetime + /// tied to the S3Storage instance, so if heavy use of this client is expected, S3Storage + /// itself should be long-lived. #[new(default)] - anonymous_client: OnceLock, + authenticated_client: OnceCell, /// The latest seen signed checkpoint index. latest_index: Option, } +/// A global cache of anonymous S3 clients, per region. +/// We've seen freshly created S3 clients make expensive DNS / TCP +/// requests when creating them. This cache allows us to reuse +/// anonymous clients across the entire agent. +static ANONYMOUS_CLIENT_CACHE: OnceLock>> = OnceLock::new(); + +fn get_anonymous_client_cache() -> &'static DashMap> { + ANONYMOUS_CLIENT_CACHE.get_or_init(DashMap::new) +} + impl fmt::Debug for S3Storage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("S3Storage") @@ -54,73 +60,119 @@ impl fmt::Debug for S3Storage { impl S3Storage { async fn write_to_bucket(&self, key: String, body: &str) -> Result<()> { - let req = PutObjectRequest { - key: self.get_composite_key(key), - bucket: self.bucket.clone(), - body: Some(Vec::from(body).into()), - content_type: Some("application/json".to_owned()), - ..Default::default() - }; - timeout( - Duration::from_secs(S3_REQUEST_TIMEOUT_SECONDS), - self.authenticated_client().put_object(req), - ) - .await??; + self.authenticated_client() + .await + .put_object() + .bucket(self.bucket.clone()) + .key(self.get_composite_key(key)) + .body(Vec::from(body).into()) + .content_type("application/json") + .send() + .await?; + Ok(()) } - /// Uses an anonymous client. This should only be used for publicly accessible buckets. + /// Check if the metadata for the object satisfies our size constraints. + /// If the object is too big, we return an error. + async fn check_metadata(&self, key: String) -> Result { + let metadata_req = self + .anonymous_client() + .await + .head_object() + .bucket(self.bucket.clone()) + .key(self.get_composite_key(key.clone())) + .send() + .await; + match metadata_req { + Ok(value) => match value.content_length { + Some(length) if length >= S3_MAX_OBJECT_SIZE => { + bail!("Object size for key {key} is too big: {}KiB", length / 1024); + } + Some(_) => Ok(true), + None => Ok(false), + }, + Err(SdkError::ServiceError(err)) => match err.err() { + HeadObjectError::NotFound(_) => Ok(false), + _ => bail!(err.into_err()), + }, + Err(e) => bail!(e), + } + } + async fn anonymously_read_from_bucket(&self, key: String) -> Result>> { - let req = GetObjectRequest { - key: self.get_composite_key(key), - bucket: self.bucket.clone(), - ..Default::default() - }; - let get_object_result = timeout( - Duration::from_secs(S3_REQUEST_TIMEOUT_SECONDS), - self.anonymous_client().get_object(req), - ) - .await?; + // check for metadata first + if !self.check_metadata(key.clone()).await? { + return Ok(None); + } + let get_object_result = self + .anonymous_client() + .await + .get_object() + .bucket(self.bucket.clone()) + .key(self.get_composite_key(key)) + .send() + .await; match get_object_result { - Ok(res) => match res.body { - Some(body) => Ok(Some(body.map_ok(|b| b.to_vec()).try_concat().await?)), - None => Ok(None), + Ok(res) => Ok(Some(res.body.collect().await?.into_bytes().to_vec())), + Err(SdkError::ServiceError(err)) => match err.err() { + SdkGetObjectError::NoSuchKey(_) => Ok(None), + _ => bail!(err.into_err()), }, - Err(RusotoError::Service(GetObjectError::NoSuchKey(_))) => Ok(None), Err(e) => bail!(e), } } - /// Gets an authenticated S3Client, creating it if it doesn't already exist. - fn authenticated_client(&self) -> &S3Client { - self.authenticated_client.get_or_init(|| { - S3Client::new_with( - utils::http_client_with_timeout().unwrap(), - AwsChainCredentialsProvider::new(), - self.region.clone(), - ) - }) + /// Gets an authenticated S3 client, creating it if it doesn't already exist + /// within &self. + async fn authenticated_client(&self) -> &Client { + self.authenticated_client + .get_or_init(|| async { + let config = self.default_aws_sdk_config_loader().load().await; + Client::new(&config) + }) + .await } - /// Gets an anonymous S3Client, creating it if it doesn't already exist. + /// Gets an anonymous S3 client, creating it if it doesn't already exist globally. /// An anonymous client doesn't have AWS credentials and will not sign S3 - /// requests with any credentials. + /// requests with any credentials. We globally cache the clients per region to avoid + /// expensive DNS / TCP initialization. /// We've experienced an inability to make GetObjectRequests to public /// S3 buckets when signing with credentials from an AWS account not from the - /// S3 bucket's AWS account. - fn anonymous_client(&self) -> &S3Client { - self.anonymous_client.get_or_init(|| { - // By default, these credentials are anonymous, see https://docs.rs/rusoto_credential/latest/rusoto_credential/struct.AwsCredentials.html#anonymous-example - let credentials = AwsCredentials::default(); - assert!(credentials.is_anonymous(), "AWS credentials not anonymous"); - - S3Client::new_with( - utils::http_client_with_timeout().unwrap(), - StaticProvider::from(credentials), - self.region.clone(), - ) + /// S3 bucket's AWS account. Additionally, this allows relayer operators to not + /// require AWS credentials. + async fn anonymous_client(&self) -> Client { + let cell = get_anonymous_client_cache() + .entry(self.region.clone()) + .or_default(); + + cell.get_or_init(|| async { + let config = self + .default_aws_sdk_config_loader() + // Make anonymous, important to not require AWS credentials + // to operate the relayer + .no_credentials() + .load() + .await; + Client::new(&config) }) + .await + .clone() + } + + /// A default ConfigLoader with timeout, region, and behavior version. + /// Unless overridden, credentials will be loaded from the env. + fn default_aws_sdk_config_loader(&self) -> aws_config::ConfigLoader { + ConfigLoader::default() + .timeout_config( + TimeoutConfig::builder() + .operation_timeout(S3_REQUEST_TIMEOUT) + .build(), + ) + .behavior_version(BehaviorVersion::latest()) + .region(self.region.clone()) } fn get_composite_key(&self, key: String) -> String { @@ -198,9 +250,8 @@ impl CheckpointSyncer for S3Storage { Ok(()) } - async fn write_metadata(&self, metadata: &AgentMetadata) -> Result<()> { - let serialized_metadata = serde_json::to_string_pretty(metadata)?; - self.write_to_bucket(S3Storage::metadata_key(), &serialized_metadata) + async fn write_metadata(&self, serialized_metadata: &str) -> Result<()> { + self.write_to_bucket(S3Storage::metadata_key(), serialized_metadata) .await?; Ok(()) } @@ -214,9 +265,9 @@ impl CheckpointSyncer for S3Storage { fn announcement_location(&self) -> String { match self.folder.as_deref() { - None | Some("") => format!("s3://{}/{}", self.bucket, self.region.name()), + None | Some("") => format!("s3://{}/{}", self.bucket, self.region), Some(folder_str) => { - format!("s3://{}/{}/{}", self.bucket, self.region.name(), folder_str) + format!("s3://{}/{}/{}", self.bucket, self.region, folder_str) } } } @@ -236,3 +287,31 @@ impl CheckpointSyncer for S3Storage { .map_err(Into::into) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_announcement_location() { + // Test with a folder + let s3_storage = S3Storage::new( + "test-bucket".to_string(), + Some("test-folder".to_string()), + Region::new("us-east-1"), + None, + ); + let location = s3_storage.announcement_location(); + assert_eq!(location, "s3://test-bucket/us-east-1/test-folder"); + + // Test without a folder + let s3_storage = S3Storage::new( + "test-bucket".to_string(), + None, + Region::new("us-east-1"), + None, + ); + let location = s3_storage.announcement_location(); + assert_eq!(location, "s3://test-bucket/us-east-1"); + } +} diff --git a/rust/main/hyperlane-core/src/chain.rs b/rust/main/hyperlane-core/src/chain.rs index 688c4eac25b..937d457ca63 100644 --- a/rust/main/hyperlane-core/src/chain.rs +++ b/rust/main/hyperlane-core/src/chain.rs @@ -607,12 +607,26 @@ impl HyperlaneDomain { } } +/// Hyperlane domain protocol types. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr( + feature = "strum", + derive(strum::Display, EnumString, IntoStaticStr, EnumIter) +)] +pub enum SubmitterType { + /// Classic + #[default] + Classic, + /// Lander + Lander, +} + #[cfg(test)] #[cfg(feature = "strum")] mod tests { use std::{num::NonZeroU32, str::FromStr}; - use crate::{KnownHyperlaneDomain, ReorgPeriod}; + use crate::{KnownHyperlaneDomain, ReorgPeriod, SubmitterType}; #[test] fn domain_strings() { @@ -693,4 +707,17 @@ mod tests { ReorgPeriod::Tag("finalized".into()) ); } + + #[test] + fn parse_submitter_type() { + assert_eq!( + serde_json::from_value::("Classic".into()).unwrap(), + SubmitterType::Classic + ); + + assert_eq!( + serde_json::from_value::("Lander".into()).unwrap(), + SubmitterType::Lander + ); + } } diff --git a/rust/main/hyperlane-core/src/config/mod.rs b/rust/main/hyperlane-core/src/config/mod.rs index a0a29d36d52..ea7c66504ab 100644 --- a/rust/main/hyperlane-core/src/config/mod.rs +++ b/rust/main/hyperlane-core/src/config/mod.rs @@ -24,11 +24,18 @@ pub type NoFilter = (); /// Config for batching messages #[derive(Debug, Clone, Default)] -pub struct OperationBatchConfig { +pub struct OpSubmissionConfig { /// Optional batch contract address (e.g. Multicall3 on EVM chains) pub batch_contract_address: Option, + /// Batch size pub max_batch_size: u32, + + /// bypass batch simulation + pub bypass_batch_simulation: bool, + + /// max submit queue length + pub max_submit_queue_length: Option, } /// A trait that allows for constructing `Self` from a raw config type. diff --git a/rust/main/hyperlane-core/src/traits/interchain_security_module.rs b/rust/main/hyperlane-core/src/traits/interchain_security_module.rs index cf39b368c65..a7017a1e0ef 100644 --- a/rust/main/hyperlane-core/src/traits/interchain_security_module.rs +++ b/rust/main/hyperlane-core/src/traits/interchain_security_module.rs @@ -15,6 +15,7 @@ use crate::{ChainResult, HyperlaneContract, HyperlaneMessage, U256}; Debug, Default, Copy, + Hash, PartialEq, Eq, BorshDeserialize, diff --git a/rust/main/hyperlane-core/src/traits/mailbox.rs b/rust/main/hyperlane-core/src/traits/mailbox.rs index 659fd88e7b0..e7f97238f02 100644 --- a/rust/main/hyperlane-core/src/traits/mailbox.rs +++ b/rust/main/hyperlane-core/src/traits/mailbox.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use derive_new::new; use crate::{ - traits::TxOutcome, utils::domain_hash, BatchItem, ChainCommunicationError, ChainResult, - HyperlaneContract, HyperlaneMessage, QueueOperation, ReorgPeriod, TxCostEstimate, H256, U256, + traits::TxOutcome, utils::domain_hash, ChainCommunicationError, ChainResult, HyperlaneContract, + HyperlaneMessage, QueueOperation, ReorgPeriod, TxCostEstimate, H256, U256, }; /// Interface for the Mailbox chain contract. Allows abstraction over different @@ -40,21 +40,16 @@ pub trait Mailbox: HyperlaneContract + Send + Sync + Debug { tx_gas_limit: Option, ) -> ChainResult; - /// Process a message with a proof against the provided signed checkpoint - async fn process_batch( - &self, - _messages: &[BatchItem], - ) -> ChainResult { - // Batching is not supported by default - Err(ChainCommunicationError::BatchingFailed) + /// True if the destination chain supports batching + /// (i.e. if the mailbox contract will succeed on a `process_batch` call) + fn supports_batching(&self) -> bool { + // Default to false + false } /// Try process the given operations as a batch. Returns the outcome of the /// batch (if one was submitted) and the operations that were not submitted. - async fn try_process_batch<'a>( - &self, - _ops: Vec<&'a QueueOperation>, - ) -> ChainResult { + async fn process_batch<'a>(&self, _ops: Vec<&'a QueueOperation>) -> ChainResult { // Batching is not supported by default Err(ChainCommunicationError::BatchingFailed) } @@ -71,7 +66,11 @@ pub trait Mailbox: HyperlaneContract + Send + Sync + Debug { /// Get the calldata for a transaction to process a message with a proof /// against the provided signed checkpoint - fn process_calldata(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Vec; + async fn process_calldata( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + ) -> ChainResult>; } /// The result of processing a batch of messages diff --git a/rust/main/hyperlane-core/src/traits/pending_operation.rs b/rust/main/hyperlane-core/src/traits/pending_operation.rs index d17f5575280..b75d599ffe5 100644 --- a/rust/main/hyperlane-core/src/traits/pending_operation.rs +++ b/rust/main/hyperlane-core/src/traits/pending_operation.rs @@ -1,5 +1,6 @@ use std::{ cmp::Ordering, + env, fmt::{Debug, Display}, io::Write, sync::Arc, @@ -10,6 +11,7 @@ use async_trait::async_trait; use num::CheckedDiv; use prometheus::IntGauge; use serde::{Deserialize, Serialize}; +use sha3::{digest::Update, Digest, Keccak256}; use strum::Display; use tracing::warn; @@ -156,7 +158,6 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs { fn reset_attempts(&mut self); /// Set the number of times this operation has been retried. - #[cfg(any(test, feature = "test-utils"))] fn set_retries(&mut self, retries: u32); /// Get the number of times this operation has been retried. @@ -166,6 +167,16 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs { fn try_get_mailbox(&self) -> Option> { None } + + /// Creates payload for the operation + async fn payload(&self) -> ChainResult>; + + /// Public version of on_reprepare method + fn on_reprepare( + &mut self, + err_msg: Option, + reason: ReprepareReason, + ) -> PendingOperationResult; } #[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq)] @@ -262,6 +273,18 @@ pub enum ReprepareReason { #[strum(to_string = "ApplicationReport({0})")] /// Application report ApplicationReport(ApplicationReport), + #[strum(to_string = "Failed to create payload for message and metadata")] + /// Failed to create payload for message and metadata + ErrorCreatingPayload, + #[strum(to_string = "Failed to store payload id by message id")] + /// Failed to store payload id by message id + ErrorStoringPayloadIdByMessageId, + #[strum(to_string = "Failed to retrieve payload id by message id")] + /// Failed to retrieve payload id by message id + ErrorRetrievingPayloadId, + #[strum(to_string = "Failed to retrieve payload id by message id")] + /// Failed to retrieve payload id by message id + ErrorRetrievingPayloadStatus, } #[derive(Display, Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -341,18 +364,34 @@ impl Eq for QueueOperation {} impl Ord for QueueOperation { fn cmp(&self, other: &Self) -> Ordering { use Ordering::*; + + fn salted_hash(id: &H256, salt: &[u8]) -> H256 { + H256::from_slice(Keccak256::new().chain(id).chain(salt).finalize().as_slice()) + } + match (self.next_attempt_after(), other.next_attempt_after()) { (Some(a), Some(b)) => a.cmp(&b), // No time means it should come before (None, Some(_)) => Less, (Some(_), None) => Greater, (None, None) => { - if self.origin_domain_id() == other.origin_domain_id() { - // Should execute in order of nonce for the same origin - self.priority().cmp(&other.priority()) + let mixing = + env::var("HYPERLANE_RELAYER_MIXING_ENABLED").map_or(false, |v| v == "true"); + if !mixing { + if self.origin_domain_id() == other.origin_domain_id() { + // Should execute in order of nonce for the same origin + self.priority().cmp(&other.priority()) + } else { + // There is no priority between these messages, so arbitrarily use the id + self.id().cmp(&other.id()) + } } else { - // There is no priority between these messages, so arbitrarily use the id - self.id().cmp(&other.id()) + let salt = env::var("HYPERLANE_RELAYER_MIXING_SALT") + .map_or(0, |v| v.parse::().unwrap_or(0)) + .to_vec(); + let self_hash = salted_hash(&self.id(), &salt); + let other_hash = salted_hash(&other.id(), &salt); + self_hash.cmp(&other_hash) } } } @@ -375,14 +414,4 @@ pub enum PendingOperationResult { } #[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_encoding_pending_operation_status() { - let status = PendingOperationStatus::Retry(ReprepareReason::CouldNotFetchMetadata); - let encoded = status.to_vec(); - let decoded = PendingOperationStatus::read_from(&mut &encoded[..]).unwrap(); - assert_eq!(status, decoded); - } -} +mod tests; diff --git a/rust/main/hyperlane-core/src/traits/pending_operation/tests.rs b/rust/main/hyperlane-core/src/traits/pending_operation/tests.rs new file mode 100644 index 00000000000..1428ef7c47e --- /dev/null +++ b/rust/main/hyperlane-core/src/traits/pending_operation/tests.rs @@ -0,0 +1,154 @@ +use std::cmp::Ord; +use std::env; + +use super::*; + +#[derive(Debug, Serialize)] +struct MockQueueOperation { + id: H256, + origin_domain_id: u32, + priority: u32, +} + +#[async_trait] +#[typetag::serialize] +impl PendingOperation for MockQueueOperation { + fn id(&self) -> H256 { + self.id + } + fn priority(&self) -> u32 { + self.priority + } + fn origin_domain_id(&self) -> u32 { + self.origin_domain_id + } + fn next_attempt_after(&self) -> Option { + None + } + fn retrieve_status_from_db(&self) -> Option { + None + } + fn destination_domain(&self) -> &HyperlaneDomain { + unimplemented!() + } + fn sender_address(&self) -> &H256 { + unimplemented!() + } + fn recipient_address(&self) -> &H256 { + unimplemented!() + } + fn app_context(&self) -> Option { + None + } + fn get_metric(&self) -> Option> { + None + } + fn set_metric(&mut self, _metric: Arc) {} + fn status(&self) -> PendingOperationStatus { + unimplemented!() + } + fn set_status(&mut self, _status: PendingOperationStatus) {} + async fn prepare(&mut self) -> PendingOperationResult { + unimplemented!() + } + async fn submit(&mut self) -> PendingOperationResult { + unimplemented!() + } + fn set_submission_outcome(&mut self, _outcome: TxOutcome) {} + fn get_tx_cost_estimate(&self) -> Option { + None + } + async fn confirm(&mut self) -> PendingOperationResult { + unimplemented!() + } + fn set_operation_outcome( + &mut self, + _submission_outcome: TxOutcome, + _submission_estimated_cost: U256, + ) { + } + fn set_next_attempt_after(&mut self, _delay: Duration) {} + fn reset_attempts(&mut self) {} + #[cfg(any(test, feature = "test-utils"))] + fn set_retries(&mut self, _retries: u32) {} + fn get_retries(&self) -> u32 { + 0 + } + async fn payload(&self) -> ChainResult> { + unimplemented!() + } + fn on_reprepare( + &mut self, + _err_msg: Option, + _reason: ReprepareReason, + ) -> PendingOperationResult { + unimplemented!() + } +} + +impl TryBatchAs for MockQueueOperation {} + +#[test] +fn test_encoding_pending_operation_status() { + let status = PendingOperationStatus::Retry(ReprepareReason::CouldNotFetchMetadata); + let encoded = status.to_vec(); + let decoded = PendingOperationStatus::read_from(&mut &encoded[..]).unwrap(); + assert_eq!(status, decoded); +} + +#[test] +fn test_queue_operation_ord_without_mixing() { + env::set_var("HYPERLANE_RELAYER_MIXING_ENABLED", "false"); + + let op1 = Box::new(MockQueueOperation { + id: H256::from_low_u64_be(1), + origin_domain_id: 1, + priority: 10, + }) as QueueOperation; + let op2 = Box::new(MockQueueOperation { + id: H256::from_low_u64_be(2), + origin_domain_id: 1, + priority: 5, + }) as QueueOperation; + + assert!(op1 > op2); // Higher priority value means lower priority +} + +#[test] +fn test_queue_operation_ord_with_mixing() { + env::set_var("HYPERLANE_RELAYER_MIXING_ENABLED", "true"); + env::set_var("HYPERLANE_RELAYER_MIXING_SALT", "123"); + + let op1 = Box::new(MockQueueOperation { + id: H256::from_low_u64_be(1), + origin_domain_id: 1, + priority: 10, + }) as QueueOperation; + let op2 = Box::new(MockQueueOperation { + id: H256::from_low_u64_be(2), + origin_domain_id: 1, + priority: 5, + }) as QueueOperation; + + // Calculate salted hashes for both operations + let salt = env::var("HYPERLANE_RELAYER_MIXING_SALT") + .map_or(0, |v| v.parse::().unwrap_or(0)) + .to_vec(); + let salted_hash_op1 = H256::from_slice( + Keccak256::new() + .chain(op1.id()) + .chain(&salt) + .finalize() + .as_slice(), + ); + let salted_hash_op2 = H256::from_slice( + Keccak256::new() + .chain(op2.id()) + .chain(&salt) + .finalize() + .as_slice(), + ); + + // Assert that the ordering matches the salted hash comparison + assert_eq!(op1.cmp(&op2), salted_hash_op1.cmp(&salted_hash_op2)); +} diff --git a/rust/main/hyperlane-metric/Cargo.toml b/rust/main/hyperlane-metric/Cargo.toml index c6ac11f5889..48c8f964071 100644 --- a/rust/main/hyperlane-metric/Cargo.toml +++ b/rust/main/hyperlane-metric/Cargo.toml @@ -15,4 +15,4 @@ maplit.workspace = true prometheus.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -url.workspace = true +url.workspace = true \ No newline at end of file diff --git a/rust/main/hyperlane-test/src/mocks/mailbox.rs b/rust/main/hyperlane-test/src/mocks/mailbox.rs index 7333e2c8a35..dc6b9bb00e2 100644 --- a/rust/main/hyperlane-test/src/mocks/mailbox.rs +++ b/rust/main/hyperlane-test/src/mocks/mailbox.rs @@ -55,6 +55,14 @@ mock! { message: &HyperlaneMessage, metadata: &[u8], ) -> Vec {} + + pub fn process_batch<'a>( + &self, + ops: Vec<&'a QueueOperation>, + ) -> ChainResult {} + + pub fn supports_batching(&self) -> bool { + } } } @@ -91,13 +99,6 @@ impl Mailbox for MockMailboxContract { self.process(message, metadata, tx_gas_limit) } - async fn process_batch( - &self, - messages: &[BatchItem], - ) -> ChainResult { - self.process_batch(messages).await - } - async fn process_estimate_costs( &self, message: &HyperlaneMessage, @@ -106,8 +107,20 @@ impl Mailbox for MockMailboxContract { self.process_estimate_costs(message, metadata) } - fn process_calldata(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Vec { - self.process_calldata(message, metadata) + async fn process_calldata( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + ) -> ChainResult> { + Ok(self.process_calldata(message, metadata)) + } + + async fn process_batch<'a>(&self, ops: Vec<&'a QueueOperation>) -> ChainResult { + self.process_batch(ops) + } + + fn supports_batching(&self) -> bool { + self.supports_batching() } } @@ -126,3 +139,12 @@ impl HyperlaneContract for MockMailboxContract { self._address() } } + +impl MockMailboxContract { + pub fn new_with_default_ism(default_ism: H256) -> Self { + let mut mock = Self::new(); + mock.expect__default_ism() + .returning(move || Ok(default_ism)); + mock + } +} diff --git a/rust/main/submitter/Cargo.toml b/rust/main/submitter/Cargo.toml index bc02bfdded3..8019db4ae01 100644 --- a/rust/main/submitter/Cargo.toml +++ b/rust/main/submitter/Cargo.toml @@ -22,10 +22,12 @@ serde_json.workspace = true solana-client.workspace = true solana-sdk.workspace = true solana-transaction-status.workspace = true +thiserror.workspace = true tokio = { workspace = true, features = ["rt", "macros", "parking_lot"] } tracing.workspace = true uuid = { workspace = true, features = ["v4", "serde"] } [dev-dependencies] +tracing-subscriber.workspace = true mockall.workspace = true tempfile.workspace = true diff --git a/rust/main/submitter/src/chain_tx_adapter.rs b/rust/main/submitter/src/chain_tx_adapter.rs index 8dc664781be..af85d841b72 100644 --- a/rust/main/submitter/src/chain_tx_adapter.rs +++ b/rust/main/submitter/src/chain_tx_adapter.rs @@ -1,9 +1,8 @@ // TODO: re-enable clippy warnings #![allow(unused_imports)] -pub use adapter::{AdaptsChain, GasLimit}; +pub use adapter::{AdaptsChain, GasLimit, TxBuildingResult}; pub use chains::ChainTxAdapterFactory; -pub use chains::SealevelPayload; pub use chains::SealevelTxPrecursor; mod adapter; diff --git a/rust/main/submitter/src/chain_tx_adapter/adapter.rs b/rust/main/submitter/src/chain_tx_adapter/adapter.rs index 0b5beaaebe2..a2ed0e12bf3 100644 --- a/rust/main/submitter/src/chain_tx_adapter/adapter.rs +++ b/rust/main/submitter/src/chain_tx_adapter/adapter.rs @@ -4,45 +4,64 @@ use std::time::Duration; use async_trait::async_trait; -use eyre::Result; +use derive_new::new; +use eyre::Report; +use tokio::sync::mpsc::error::SendError; use uuid::Uuid; use hyperlane_core::U256; use crate::{ - payload::FullPayload, + error::SubmitterError, + payload::{FullPayload, PayloadDetails}, transaction::{Transaction, TransactionStatus}, }; pub type GasLimit = U256; +#[derive(new, Debug, Clone)] +pub struct TxBuildingResult { + /// payload details for the payloads in this transaction + /// this is a vector because multiple payloads can be included in a single transaction + pub payloads: Vec, + /// the transaction itself + /// this is an option because the transaction may have failed to be built + pub maybe_tx: Option, +} + /// The `AdaptsChain` trait is implemented by adapters for different VMs, stacks and chains, allowing the `PayloadDispatcher` to interact with them in a generic way. #[async_trait] pub trait AdaptsChain: Send + Sync { /// Simulates Payload and returns its gas limit. Called in the Building Stage (PayloadDispatcher) - async fn estimate_gas_limit(&self, payload: &FullPayload) -> Result; + async fn estimate_gas_limit( + &self, + payload: &FullPayload, + ) -> Result, SubmitterError>; /// Performs batching if available. Internally estimates gas limit for batch as well. Called in the Building Stage (PayloadDispatcher) - async fn build_transactions(&self, payloads: &[FullPayload]) -> Result>; + async fn build_transactions(&self, payloads: &[FullPayload]) -> Vec; /// Simulates a Transaction before submitting it for the first time. Called in the Inclusion Stage (PayloadDispatcher) - async fn simulate_tx(&self, tx: &Transaction) -> Result; + async fn simulate_tx(&self, tx: &Transaction) -> Result; /// Sets / escalates gas price, sets nonce / blockhash and broadcasts the Transaction. Even if broadcasting fails, the Transaction struct remains mutated with the new estimates. Called in the Inclusion Stage (PayloadDispatcher) - async fn submit(&self, tx: &mut Transaction) -> Result<()>; + async fn submit(&self, tx: &mut Transaction) -> Result<(), SubmitterError>; /// Queries the chain by txhash to get the tx status. Called in the Inclusion Stage and Finality Stage of the PayloadDispatcher - async fn tx_status(&self, tx: &Transaction) -> Result; + async fn tx_status(&self, tx: &Transaction) -> Result; /// uses BatchManager, returns any reverted Payload IDs sent in a Transaction. Called in the Finality Stage (PayloadDispatcher) - async fn reverted_payloads(&self, tx: &Transaction) -> Result>; + async fn reverted_payloads( + &self, + tx: &Transaction, + ) -> Result, SubmitterError>; /// Returns the estimated block time of the chain. Used for polling pending transactions. Called in the Inclusion and Finality Stages of the PayloadDispatcher - fn estimated_block_time(&self) -> Duration; + fn estimated_block_time(&self) -> &Duration; /// Returns the maximum batch size for this chain. Used to decide how many payloads to batch together, as well as /// how many network calls to perform in parallel - fn max_batch_size(&self) -> usize; + fn max_batch_size(&self) -> u32; // methods below are excluded from the MVP @@ -52,7 +71,7 @@ pub trait AdaptsChain: Send + Sync { } /// Replaces calldata in this tx with a transfer-to-self, to use its payload(s) for filling a nonce gap - async fn replace_tx(&self, _tx: &Transaction) -> Result<()> { + async fn replace_tx(&self, _tx: &Transaction) -> Result<(), SubmitterError> { todo!() } } diff --git a/rust/main/submitter/src/chain_tx_adapter/chains.rs b/rust/main/submitter/src/chain_tx_adapter/chains.rs index 62d5717999d..25a11921883 100644 --- a/rust/main/submitter/src/chain_tx_adapter/chains.rs +++ b/rust/main/submitter/src/chain_tx_adapter/chains.rs @@ -1,5 +1,4 @@ pub use factory::ChainTxAdapterFactory; -pub use sealevel::SealevelPayload; pub use sealevel::SealevelTxPrecursor; mod factory; diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/cosmos.rs b/rust/main/submitter/src/chain_tx_adapter/chains/cosmos.rs index f2c3bb722d3..a19d9a4f2c8 100644 --- a/rust/main/submitter/src/chain_tx_adapter/chains/cosmos.rs +++ b/rust/main/submitter/src/chain_tx_adapter/chains/cosmos.rs @@ -5,8 +5,9 @@ use uuid::Uuid; use hyperlane_base::settings::{ChainConf, RawChainConf}; use crate::{ - chain_tx_adapter::{AdaptsChain, GasLimit}, - payload::FullPayload, + chain_tx_adapter::{adapter::TxBuildingResult, AdaptsChain, GasLimit}, + error::SubmitterError, + payload::{FullPayload, PayloadDetails}, transaction::{Transaction, TransactionStatus}, }; @@ -26,35 +27,41 @@ impl CosmosTxAdapter { #[async_trait] impl AdaptsChain for CosmosTxAdapter { - async fn estimate_gas_limit(&self, _payload: &FullPayload) -> Result { + async fn estimate_gas_limit( + &self, + _payload: &FullPayload, + ) -> Result, SubmitterError> { todo!() } - async fn build_transactions(&self, _payloads: &[FullPayload]) -> Result> { + async fn build_transactions(&self, _payloads: &[FullPayload]) -> Vec { todo!() } - async fn simulate_tx(&self, _tx: &Transaction) -> Result { + async fn simulate_tx(&self, _tx: &Transaction) -> Result { todo!() } - async fn submit(&self, _tx: &mut Transaction) -> Result<()> { + async fn submit(&self, _tx: &mut Transaction) -> Result<(), SubmitterError> { todo!() } - async fn tx_status(&self, _tx: &Transaction) -> Result { + async fn tx_status(&self, _tx: &Transaction) -> Result { todo!() } - async fn reverted_payloads(&self, _tx: &Transaction) -> Result> { + async fn reverted_payloads( + &self, + _tx: &Transaction, + ) -> Result, SubmitterError> { todo!() } - fn estimated_block_time(&self) -> std::time::Duration { + fn estimated_block_time(&self) -> &std::time::Duration { todo!() } - fn max_batch_size(&self) -> usize { + fn max_batch_size(&self) -> u32 { todo!() } } diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/ethereum.rs b/rust/main/submitter/src/chain_tx_adapter/chains/ethereum.rs index 207e7dadf2b..4837c4c8b7e 100644 --- a/rust/main/submitter/src/chain_tx_adapter/chains/ethereum.rs +++ b/rust/main/submitter/src/chain_tx_adapter/chains/ethereum.rs @@ -5,8 +5,9 @@ use uuid::Uuid; use hyperlane_base::settings::{ChainConf, RawChainConf}; use crate::{ - chain_tx_adapter::{AdaptsChain, GasLimit}, - payload::FullPayload, + chain_tx_adapter::{adapter::TxBuildingResult, AdaptsChain, GasLimit}, + error::SubmitterError, + payload::{FullPayload, PayloadDetails}, transaction::{Transaction, TransactionStatus}, }; @@ -26,35 +27,41 @@ impl EthereumTxAdapter { #[async_trait] impl AdaptsChain for EthereumTxAdapter { - async fn estimate_gas_limit(&self, _payload: &FullPayload) -> Result { + async fn estimate_gas_limit( + &self, + _payload: &FullPayload, + ) -> Result, SubmitterError> { todo!() } - async fn build_transactions(&self, _payloads: &[FullPayload]) -> Result> { + async fn build_transactions(&self, _payloads: &[FullPayload]) -> Vec { todo!() } - async fn simulate_tx(&self, _tx: &Transaction) -> Result { + async fn simulate_tx(&self, _tx: &Transaction) -> Result { todo!() } - async fn submit(&self, _tx: &mut Transaction) -> Result<()> { + async fn submit(&self, _tx: &mut Transaction) -> Result<(), SubmitterError> { todo!() } - async fn tx_status(&self, _tx: &Transaction) -> Result { + async fn tx_status(&self, _tx: &Transaction) -> Result { todo!() } - async fn reverted_payloads(&self, _tx: &Transaction) -> Result> { + async fn reverted_payloads( + &self, + _tx: &Transaction, + ) -> Result, SubmitterError> { todo!() } - fn estimated_block_time(&self) -> std::time::Duration { + fn estimated_block_time(&self) -> &std::time::Duration { todo!() } - fn max_batch_size(&self) -> usize { + fn max_batch_size(&self) -> u32 { todo!() } } diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/factory.rs b/rust/main/submitter/src/chain_tx_adapter/chains/factory.rs index b3e99631114..54d9ea34be9 100644 --- a/rust/main/submitter/src/chain_tx_adapter/chains/factory.rs +++ b/rust/main/submitter/src/chain_tx_adapter/chains/factory.rs @@ -1,6 +1,8 @@ // TODO: re-enable clippy warnings #![allow(dead_code)] +use std::sync::Arc; + use eyre::Result; use hyperlane_base::{ settings::{ChainConf, RawChainConf}, @@ -20,18 +22,18 @@ impl ChainTxAdapterFactory { conf: &ChainConf, raw_conf: &RawChainConf, metrics: &CoreMetrics, - ) -> Result> { + ) -> Result> { use HyperlaneDomainProtocol::*; - let adapter: Box = match conf.domain.domain_protocol() { - Ethereum => Box::new(EthereumTxAdapter::new(conf.clone(), raw_conf.clone())), + let adapter: Arc = match conf.domain.domain_protocol() { + Ethereum => Arc::new(EthereumTxAdapter::new(conf.clone(), raw_conf.clone())), Fuel => todo!(), - Sealevel => Box::new(SealevelTxAdapter::new( + Sealevel => Arc::new(SealevelTxAdapter::new( conf.clone(), raw_conf.clone(), metrics, )?), - Cosmos => Box::new(CosmosTxAdapter::new(conf.clone(), raw_conf.clone())), + Cosmos => Arc::new(CosmosTxAdapter::new(conf.clone(), raw_conf.clone())), CosmosNative => todo!(), }; diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel.rs index a812d9b9883..a28fcd48ac9 100644 --- a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel.rs +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel.rs @@ -1,5 +1,4 @@ pub use adapter::SealevelTxAdapter; -pub use payload::SealevelPayload; pub use precursor::SealevelTxPrecursor; mod adapter; diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter.rs index 9f242978337..ae61e572ead 100644 --- a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter.rs +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter.rs @@ -1,7 +1,8 @@ use std::sync::Arc; +use std::time::Duration; use async_trait::async_trait; -use eyre::{bail, ContextCompat, Report, Result}; +use eyre::{bail, eyre, ContextCompat, Report, Result}; use serde_json::json; use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; use solana_sdk::{ @@ -32,18 +33,27 @@ use hyperlane_sealevel::{ PriorityFeeOracleConfig, SealevelProvider, SealevelProviderForSubmitter, SealevelTxCostEstimate, }; -use crate::chain_tx_adapter::chains::sealevel::conf::{create_keypair, get_connection_conf}; -use crate::chain_tx_adapter::chains::sealevel::transaction::{ - Precursor, TransactionFactory, Update, -}; -use crate::chain_tx_adapter::chains::sealevel::SealevelTxPrecursor; use crate::chain_tx_adapter::{AdaptsChain, GasLimit}; -use crate::payload::{FullPayload, VmSpecificPayloadData}; +use crate::payload::FullPayload; use crate::transaction::{ SignerAddress, Transaction, TransactionId, TransactionStatus, VmSpecificTxData, }; +use crate::{ + chain_tx_adapter::chains::sealevel::transaction::{Precursor, TransactionFactory, Update}, + error::SubmitterError, +}; +use crate::{chain_tx_adapter::chains::sealevel::SealevelTxPrecursor, payload::PayloadDetails}; +use crate::{ + chain_tx_adapter::{ + adapter::TxBuildingResult, + chains::sealevel::conf::{create_keypair, get_connection_conf}, + }, + error, +}; pub struct SealevelTxAdapter { + estimated_block_time: Duration, + max_batch_size: u32, reorg_period: ReorgPeriod, keypair: SealevelKeypair, client: Box, @@ -100,10 +110,14 @@ impl SealevelTxAdapter { oracle: Box, submitter: Box, ) -> Result { - let keypair = create_keypair(&conf)?; + let estimated_block_time = conf.estimated_block_time; let reorg_period = conf.reorg_period.clone(); + let max_batch_size = Self::batch_size(&conf)?; + let keypair = create_keypair(&conf)?; Ok(Self { + estimated_block_time, + max_batch_size, reorg_period, keypair, provider, @@ -122,6 +136,8 @@ impl SealevelTxAdapter { submitter: Box, ) -> Self { Self { + estimated_block_time: Duration::from_secs(1), + max_batch_size: 1, reorg_period: ReorgPeriod::default(), keypair: SealevelKeypair::default(), provider, @@ -131,7 +147,18 @@ impl SealevelTxAdapter { } } - async fn estimate(&self, precursor: SealevelTxPrecursor) -> ChainResult { + fn batch_size(conf: &ChainConf) -> Result { + Ok(conf + .connection + .operation_submission_config() + .ok_or_else(|| eyre!("no operation batch config"))? + .max_batch_size) + } + + async fn estimate( + &self, + precursor: SealevelTxPrecursor, + ) -> Result { let estimate = self .provider .get_estimated_costs_for_instruction( @@ -183,15 +210,18 @@ impl SealevelTxAdapter { #[async_trait] impl AdaptsChain for SealevelTxAdapter { - async fn estimate_gas_limit(&self, payload: &FullPayload) -> Result { + async fn estimate_gas_limit( + &self, + payload: &FullPayload, + ) -> Result, SubmitterError> { info!(?payload, "estimating payload"); let not_estimated = SealevelTxPrecursor::from_payload(payload); let estimated = self.estimate(not_estimated).await?; info!(?payload, ?estimated, "estimated payload"); - Ok(estimated.estimate.compute_units.into()) + Ok(Some(estimated.estimate.compute_units.into())) } - async fn build_transactions(&self, payloads: &[FullPayload]) -> Result> { + async fn build_transactions(&self, payloads: &[FullPayload]) -> Vec { info!(?payloads, "building transactions for payloads"); let payloads_and_precursors = payloads .iter() @@ -200,16 +230,22 @@ impl AdaptsChain for SealevelTxAdapter { let mut transactions = Vec::new(); for (not_estimated, payload) in payloads_and_precursors.into_iter() { - let estimated = self.estimate(not_estimated).await?; + let Ok(estimated) = self.estimate(not_estimated).await else { + transactions.push(TxBuildingResult::new(vec![payload.details.clone()], None)); + continue; + }; let transaction = TransactionFactory::build(payload, estimated); - transactions.push(transaction); + transactions.push(TxBuildingResult::new( + vec![payload.details.clone()], + Some(transaction), + )) } info!(?payloads, ?transactions, "built transactions for payloads"); - Ok(transactions) + transactions } - async fn simulate_tx(&self, tx: &Transaction) -> Result { + async fn simulate_tx(&self, tx: &Transaction) -> Result { info!(?tx, "simulating transaction"); let precursor = tx.precursor(); let svm_transaction = self.create_unsigned_transaction(precursor).await?; @@ -222,9 +258,14 @@ impl AdaptsChain for SealevelTxAdapter { Ok(success) } - async fn submit(&self, tx: &mut Transaction) -> Result<()> { + async fn submit(&self, tx: &mut Transaction) -> Result<(), SubmitterError> { + if tx.hash.is_some() { + return Ok(()); + } info!(?tx, "submitting transaction"); let not_estimated = tx.precursor(); + // TODO: the `estimate` call shouldn't happen here - the `Transaction` argument should already contain the precursor, + // set in the `build_transactions` method let estimated = self.estimate(not_estimated.clone()).await?; let svm_transaction = self.create_signed_transaction(&estimated).await?; let signature = self @@ -243,41 +284,26 @@ impl AdaptsChain for SealevelTxAdapter { info!(?tx, "confirmed transaction by signature status"); - let executed = self - .submitter - .confirm_transaction(signature, CommitmentConfig::processed()) - .await - .map_err(|err| { - warn!( - "Failed to confirm process transaction with commitment level processed: {}", - err - ) - }) - .unwrap_or(false); - - info!(?tx, "confirmed transaction with commitment level processed"); - - if !executed { - bail!("Process transaction is not confirmed with commitment level processed") - } - Ok(()) } - async fn tx_status(&self, tx: &Transaction) -> Result { + async fn tx_status(&self, tx: &Transaction) -> Result { info!(?tx, "checking status of transaction"); - let h512 = tx.hash.ok_or(eyre::eyre!( - "Hash should be set for transaction to check its status" - ))?; + let Some(h512) = tx.hash else { + return Ok(TransactionStatus::PendingInclusion); + }; let signature = Signature::new(h512.as_ref()); let transaction_search_result = self.client.get_transaction(signature).await; - let transaction = if let Ok(transaction) = transaction_search_result { - transaction - } else { - info!(?tx, "pending transaction"); - return Ok(TransactionStatus::PendingInclusion); + let transaction = match transaction_search_result { + Ok(transaction) => transaction, + Err(err) => { + warn!(?tx, ?err, "Failed to get transaction status by hash"); + return Err(SubmitterError::TxSubmissionError( + "Transaction hash not found".to_string(), + )); + } }; // slot at which transaction was included into blockchain @@ -310,17 +336,20 @@ impl AdaptsChain for SealevelTxAdapter { } } - async fn reverted_payloads(&self, _tx: &Transaction) -> Result> { + async fn reverted_payloads( + &self, + _tx: &Transaction, + ) -> Result, SubmitterError> { // Dummy implementation of reverted payloads for Sealevel since we don't have batching for Sealevel Ok(Vec::new()) } - fn estimated_block_time(&self) -> std::time::Duration { - todo!() + fn estimated_block_time(&self) -> &Duration { + &self.estimated_block_time } - fn max_batch_size(&self) -> usize { - todo!() + fn max_batch_size(&self) -> u32 { + self.max_batch_size } } diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests.rs index 0e17820058a..ef061b44387 100644 --- a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests.rs +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests.rs @@ -1,311 +1,7 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use eyre::Result; -use mockall::mock; -use solana_client::rpc_response::RpcSimulateTransactionResult; -use solana_sdk::{ - commitment_config::CommitmentConfig, - compute_budget::ComputeBudgetInstruction, - instruction::Instruction as SealevelInstruction, - message::Message, - pubkey::Pubkey, - signature::{Signature, Signer}, - transaction::Transaction as SealevelTransaction, -}; -use solana_transaction_status::{ - EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, - EncodedTransactionWithStatusMeta, UiConfirmedBlock, -}; - -use hyperlane_base::settings::parser::h_sealevel::{ - PriorityFeeOracle, SealevelKeypair, TransactionSubmitter, -}; -use hyperlane_base::settings::ChainConf; -use hyperlane_core::{ChainResult, H512, U256}; -use hyperlane_sealevel::fallback::SubmitSealevelRpc; -use hyperlane_sealevel::{SealevelProvider, SealevelProviderForSubmitter, SealevelTxCostEstimate}; - -use crate::chain_tx_adapter::chains::sealevel::transaction::{TransactionFactory, Update}; -use crate::chain_tx_adapter::chains::sealevel::{ - SealevelPayload, SealevelTxAdapter, SealevelTxPrecursor, -}; -use crate::chain_tx_adapter::AdaptsChain; -use crate::payload::{FullPayload, PayloadDetails, VmSpecificPayloadData}; -use crate::transaction::{SignerAddress, Transaction, TransactionStatus, VmSpecificTxData}; - -const GAS_LIMIT: u32 = 42; - -mock! { - pub Client {} - - #[async_trait] - impl SubmitSealevelRpc for Client { - async fn get_block(&self, slot: u64) -> ChainResult; - async fn get_transaction(&self, signature: Signature) -> ChainResult; - async fn simulate_transaction(&self, transaction: &SealevelTransaction) -> ChainResult; - } -} - -mock! { - pub Oracle {} - - #[async_trait] - impl PriorityFeeOracle for Oracle { - async fn get_priority_fee(&self, transaction: &SealevelTransaction) -> ChainResult; - } -} - -mock! { - pub Submitter {} - - #[async_trait] - impl TransactionSubmitter for Submitter { - fn get_priority_fee_instruction(&self, compute_unit_price_micro_lamports: u64, compute_units: u64, payer: &Pubkey) -> SealevelInstruction; - async fn send_transaction(&self, transaction: &SealevelTransaction, skip_preflight: bool) -> ChainResult; - async fn wait_for_transaction_confirmation(&self, transaction: &SealevelTransaction) -> ChainResult<()>; - async fn confirm_transaction(&self, signature: Signature, commitment: CommitmentConfig) -> ChainResult; - } -} - -struct MockProvider {} - -#[async_trait] -impl SealevelProviderForSubmitter for MockProvider { - async fn create_transaction_for_instruction( - &self, - _compute_unit_limit: u32, - _compute_unit_price_micro_lamports: u64, - _instruction: SealevelInstruction, - _payer: &SealevelKeypair, - _tx_submitter: &dyn TransactionSubmitter, - _sign: bool, - ) -> ChainResult { - let keypair = SealevelKeypair::default(); - Ok(SealevelTransaction::new_unsigned(Message::new( - &[instruction()], - Some(&keypair.pubkey()), - ))) - } - - async fn get_estimated_costs_for_instruction( - &self, - _instruction: SealevelInstruction, - _payer: &SealevelKeypair, - _tx_submitter: &dyn TransactionSubmitter, - _priority_fee_oracle: &dyn PriorityFeeOracle, - ) -> ChainResult { - Ok(SealevelTxCostEstimate { - compute_units: GAS_LIMIT, - compute_unit_price_micro_lamports: 0, - }) - } - - async fn wait_for_transaction_confirmation( - &self, - _transaction: &SealevelTransaction, - ) -> ChainResult<()> { - Ok(()) - } - - async fn confirm_transaction( - &self, - _signature: Signature, - _commitment: CommitmentConfig, - ) -> ChainResult { - Ok(true) - } -} - -#[tokio::test] -async fn test_estimate_gas_limit() { - // given - let adapter = adapter(); - let payload = payload(); - - let expected = U256::from(GAS_LIMIT); - - // when - let result = adapter.estimate_gas_limit(&payload).await; - - // then - assert!(result.is_ok()); - assert_eq!(expected, result.unwrap()); -} - -#[tokio::test] -async fn test_build_transactions() { - // given - let adapter = adapter(); - let payload = payload(); - let data = VmSpecificTxData::Svm(SealevelTxPrecursor::new(instruction(), estimate())); - let expected = (payload.details.clone(), data); - - // when - let result = adapter.build_transactions(&[payload.clone()]).await; - - // then - assert!(result.is_ok()); - let actual = payload_details_and_data_in_transaction(result); - assert_eq!(expected, actual); -} - -#[tokio::test] -async fn test_simulate_tx() { - // given - let adapter = adapter(); - let transaction = TransactionFactory::build(&payload(), precursor()); - - // when - let simulated = adapter.simulate_tx(&transaction).await.unwrap(); - - // then - assert!(simulated); -} - -#[tokio::test] -async fn test_submit() { - // given - let adapter = adapter(); - let mut transaction = TransactionFactory::build(&payload(), precursor()); - - // when - let result = adapter.submit(&mut transaction).await; - - // then - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_tx_status() { - // given - let adapter = adapter(); - let transaction = transaction(); - - // when - let result = adapter.tx_status(&transaction).await; - - // then - assert!(result.is_ok()); - let status = result.unwrap(); - assert!(matches!(status, TransactionStatus::Finalized)); -} - -fn payload_details_and_data_in_transaction( - result: Result>, -) -> (PayloadDetails, VmSpecificTxData) { - let transactions = result.unwrap(); - let transaction = transactions.first().unwrap(); - ( - transaction.payload_details.first().unwrap().clone(), - transaction.vm_specific_data.clone(), - ) -} - -fn estimate() -> SealevelTxCostEstimate { - SealevelTxCostEstimate { - compute_units: GAS_LIMIT, - compute_unit_price_micro_lamports: 0, - } -} - -fn adapter() -> SealevelTxAdapter { - let client = mock_client(); - let oracle = MockOracle::new(); - let provider = MockProvider {}; - let submitter = mock_submitter(); - - SealevelTxAdapter::new_internal_default( - Box::new(client), - Box::new(provider), - Box::new(oracle), - Box::new(submitter), - ) -} - -fn mock_submitter() -> MockSubmitter { - let signature = Signature::default(); - - let mut submitter = MockSubmitter::new(); - submitter - .expect_send_transaction() - .returning(move |_, _| Ok(signature)); - submitter - .expect_wait_for_transaction_confirmation() - .returning(move |_| Ok(())); - submitter - .expect_confirm_transaction() - .returning(move |_, _| Ok(true)); - submitter -} - -fn mock_client() -> MockClient { - let result = RpcSimulateTransactionResult { - err: None, - logs: None, - accounts: None, - units_consumed: None, - return_data: None, - }; - - let mut client = MockClient::new(); - client.expect_get_block().returning(move |_| Ok(block())); - client - .expect_get_transaction() - .returning(move |_| Ok(encoded_transaction())); - client - .expect_simulate_transaction() - .returning(move |_| Ok(result.clone())); - client -} - -fn block() -> UiConfirmedBlock { - UiConfirmedBlock { - previous_blockhash: "".to_string(), - blockhash: "".to_string(), - parent_slot: 0, - transactions: None, - signatures: None, - rewards: None, - block_time: None, - block_height: None, - } -} - -fn encoded_transaction() -> EncodedConfirmedTransactionWithStatusMeta { - EncodedConfirmedTransactionWithStatusMeta { - slot: 43, - transaction: EncodedTransactionWithStatusMeta { - transaction: EncodedTransaction::LegacyBinary("binary".to_string()), - meta: None, - version: None, - }, - block_time: None, - } -} - -fn instruction() -> SealevelInstruction { - ComputeBudgetInstruction::set_compute_unit_limit(GAS_LIMIT) -} - -fn payload() -> FullPayload { - let data = VmSpecificPayloadData::Svm(SealevelPayload { - instruction: instruction(), - }); - - FullPayload { - data, - ..Default::default() - } -} - -fn precursor() -> SealevelTxPrecursor { - SealevelTxPrecursor::new(instruction(), estimate()) -} - -fn transaction() -> Transaction { - let mut transaction = TransactionFactory::build(&payload(), precursor()); - transaction.update_after_submission(H512::zero(), precursor()); - - transaction -} +mod build; +mod common; +mod config; +mod estimate; +mod simulate; +mod status; +mod submit; diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/build.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/build.rs new file mode 100644 index 00000000000..385566488bc --- /dev/null +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/build.rs @@ -0,0 +1,40 @@ +use eyre::Result; + +use crate::chain_tx_adapter::chains::sealevel::adapter::tests::common::{ + adapter, estimate, instruction, payload, +}; +use crate::chain_tx_adapter::{AdaptsChain, SealevelTxPrecursor, TxBuildingResult}; +use crate::error::SubmitterError; +use crate::payload::PayloadDetails; +use crate::transaction::{Transaction, VmSpecificTxData}; + +#[tokio::test] +async fn test_build_transactions() { + // given + let adapter = adapter(); + let payload = payload(); + let data = VmSpecificTxData::Svm(SealevelTxPrecursor::new(instruction(), estimate())); + let expected = (payload.details.clone(), data); + + // when + let result = adapter.build_transactions(&[payload.clone()]).await; + + // then + let actual = payload_details_and_data_in_transaction(result); + assert_eq!(expected, actual); +} + +fn payload_details_and_data_in_transaction( + transactions: Vec, +) -> (PayloadDetails, VmSpecificTxData) { + let transaction = transactions.first().unwrap(); + ( + transaction.payloads.first().unwrap().clone(), + transaction + .maybe_tx + .clone() + .unwrap() + .vm_specific_data + .clone(), + ) +} diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/common.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/common.rs new file mode 100644 index 00000000000..249a25720f7 --- /dev/null +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/common.rs @@ -0,0 +1,237 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use mockall::mock; +use solana_client::rpc_response::RpcSimulateTransactionResult; +use solana_sdk::commitment_config::CommitmentConfig; +use solana_sdk::compute_budget::ComputeBudgetInstruction; +use solana_sdk::instruction::Instruction as SealevelInstruction; +use solana_sdk::message::Message; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::{Signature, Signer}; +use solana_sdk::transaction::Transaction as SealevelTransaction; +use solana_transaction_status::{ + EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, + EncodedTransactionWithStatusMeta, UiConfirmedBlock, +}; + +use hyperlane_base::settings::{ChainConf, RawChainConf}; +use hyperlane_core::{ChainResult, H512}; +use hyperlane_sealevel::fallback::SubmitSealevelRpc; +use hyperlane_sealevel::{ + PriorityFeeOracle, SealevelKeypair, SealevelProviderForSubmitter, SealevelTxCostEstimate, + TransactionSubmitter, +}; + +use crate::chain_tx_adapter::chains::sealevel::transaction::{TransactionFactory, Update}; +use crate::chain_tx_adapter::chains::sealevel::SealevelTxAdapter; +use crate::chain_tx_adapter::SealevelTxPrecursor; +use crate::payload::FullPayload; +use crate::transaction::{Transaction, VmSpecificTxData}; + +pub const GAS_LIMIT: u32 = 42; + +mock! { + pub Client {} + + #[async_trait] + impl SubmitSealevelRpc for Client { + async fn get_block(&self, slot: u64) -> ChainResult; + async fn get_transaction(&self, signature: Signature) -> ChainResult; + async fn simulate_transaction(&self, transaction: &SealevelTransaction) -> ChainResult; + } +} + +mock! { + pub Oracle {} + + #[async_trait] + impl PriorityFeeOracle for Oracle { + async fn get_priority_fee(&self, transaction: &SealevelTransaction) -> ChainResult; + } +} + +mock! { + pub Submitter {} + + #[async_trait] + impl TransactionSubmitter for Submitter { + fn get_priority_fee_instruction(&self, compute_unit_price_micro_lamports: u64, compute_units: u64, payer: &Pubkey) -> SealevelInstruction; + async fn send_transaction(&self, transaction: &SealevelTransaction, skip_preflight: bool) -> ChainResult; + async fn wait_for_transaction_confirmation(&self, transaction: &SealevelTransaction) -> ChainResult<()>; + async fn confirm_transaction(&self, signature: Signature, commitment: CommitmentConfig) -> ChainResult; + } +} + +struct MockProvider {} + +#[async_trait] +impl SealevelProviderForSubmitter for MockProvider { + async fn create_transaction_for_instruction( + &self, + _compute_unit_limit: u32, + _compute_unit_price_micro_lamports: u64, + _instruction: SealevelInstruction, + _payer: &SealevelKeypair, + _tx_submitter: &dyn TransactionSubmitter, + _sign: bool, + ) -> ChainResult { + let keypair = SealevelKeypair::default(); + Ok(SealevelTransaction::new_unsigned(Message::new( + &[instruction()], + Some(&keypair.pubkey()), + ))) + } + + async fn get_estimated_costs_for_instruction( + &self, + _instruction: SealevelInstruction, + _payer: &SealevelKeypair, + _tx_submitter: &dyn TransactionSubmitter, + _priority_fee_oracle: &dyn PriorityFeeOracle, + ) -> ChainResult { + Ok(SealevelTxCostEstimate { + compute_units: GAS_LIMIT, + compute_unit_price_micro_lamports: 0, + }) + } + + async fn wait_for_transaction_confirmation( + &self, + _transaction: &SealevelTransaction, + ) -> ChainResult<()> { + Ok(()) + } + + async fn confirm_transaction( + &self, + _signature: Signature, + _commitment: CommitmentConfig, + ) -> ChainResult { + Ok(true) + } +} + +pub fn estimate() -> SealevelTxCostEstimate { + SealevelTxCostEstimate { + compute_units: GAS_LIMIT, + compute_unit_price_micro_lamports: 0, + } +} + +pub fn adapter() -> SealevelTxAdapter { + let client = mock_client(); + let oracle = MockOracle::new(); + let provider = MockProvider {}; + let submitter = mock_submitter(); + + SealevelTxAdapter::new_internal_default( + Box::new(client), + Box::new(provider), + Box::new(oracle), + Box::new(submitter), + ) +} + +pub fn adapter_config(conf: ChainConf) -> SealevelTxAdapter { + let raw_conf = RawChainConf::default(); + let client = mock_client(); + let oracle = MockOracle::new(); + let provider = MockProvider {}; + let submitter = mock_submitter(); + + SealevelTxAdapter::new_internal( + conf, + raw_conf, + Box::new(client), + Box::new(provider), + Box::new(oracle), + Box::new(submitter), + ) + .unwrap() +} + +fn mock_submitter() -> MockSubmitter { + let signature = Signature::default(); + + let mut submitter = MockSubmitter::new(); + submitter + .expect_send_transaction() + .returning(move |_, _| Ok(signature.clone())); + submitter + .expect_wait_for_transaction_confirmation() + .returning(|_| Ok(())); + submitter + .expect_confirm_transaction() + .returning(move |_, _| Ok(true)); + submitter +} + +fn mock_client() -> MockClient { + let result = RpcSimulateTransactionResult { + err: None, + logs: None, + accounts: None, + units_consumed: None, + return_data: None, + }; + + let mut client = MockClient::new(); + client.expect_get_block().returning(move |_| Ok(block())); + client + .expect_get_transaction() + .returning(move |_| Ok(encoded_transaction())); + client + .expect_simulate_transaction() + .returning(move |_| Ok(result.clone())); + client +} + +fn block() -> UiConfirmedBlock { + UiConfirmedBlock { + previous_blockhash: "".to_string(), + blockhash: "".to_string(), + parent_slot: 0, + transactions: None, + signatures: None, + rewards: None, + block_time: None, + block_height: None, + } +} + +fn encoded_transaction() -> EncodedConfirmedTransactionWithStatusMeta { + EncodedConfirmedTransactionWithStatusMeta { + slot: 43, + transaction: EncodedTransactionWithStatusMeta { + transaction: EncodedTransaction::LegacyBinary("binary".to_string()), + meta: None, + version: None, + }, + block_time: None, + } +} + +pub fn instruction() -> SealevelInstruction { + ComputeBudgetInstruction::set_compute_unit_limit(GAS_LIMIT) +} + +pub fn payload() -> FullPayload { + let data = serde_json::to_vec(&instruction()).unwrap(); + let payload = FullPayload { + data, + ..Default::default() + }; + payload +} + +pub fn precursor() -> SealevelTxPrecursor { + SealevelTxPrecursor::new(instruction(), estimate()) +} + +pub fn transaction() -> Transaction { + let mut transaction = TransactionFactory::build(&payload(), precursor()); + transaction.update_after_submission(H512::zero(), precursor()); + + transaction +} diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/config.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/config.rs new file mode 100644 index 00000000000..56ba4305a58 --- /dev/null +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/config.rs @@ -0,0 +1,52 @@ +use std::num::NonZeroU32; +use std::time::Duration; + +use hyperlane_base::settings::{ChainConf, ChainConnectionConf, SignerConf}; +use hyperlane_core::config::OpSubmissionConfig; +use hyperlane_core::{HyperlaneDomain, KnownHyperlaneDomain, ReorgPeriod, SubmitterType}; + +use crate::chain_tx_adapter::chains::sealevel::adapter::tests::common::adapter_config; +use crate::chain_tx_adapter::AdaptsChain; + +#[test] +fn test_configuration_fields() { + // given + let expected_estimated_block_time = Duration::from_secs_f64(2.6); + let expected_max_batch_size = 43; + let expected_reorg_period = ReorgPeriod::Blocks(NonZeroU32::new(42).unwrap()); + + let conf = ChainConf { + domain: HyperlaneDomain::Known(KnownHyperlaneDomain::SolanaMainnet), + signer: Some(SignerConf::HexKey { + key: Default::default(), + }), + submitter: SubmitterType::Lander, + estimated_block_time: expected_estimated_block_time.clone(), + reorg_period: expected_reorg_period.clone(), + addresses: Default::default(), + connection: ChainConnectionConf::Sealevel(hyperlane_sealevel::ConnectionConf { + urls: vec![], + op_submission_config: OpSubmissionConfig { + batch_contract_address: None, + max_batch_size: expected_max_batch_size, + ..Default::default() + }, + native_token: Default::default(), + priority_fee_oracle: Default::default(), + transaction_submitter: Default::default(), + }), + metrics_conf: Default::default(), + index: Default::default(), + }; + let adapter = adapter_config(conf); + + // when + let estimated_block_time = adapter.estimated_block_time(); + let max_batch_size = adapter.max_batch_size(); + let reorg_period = adapter.reorg_period.clone(); + + // then + assert_eq!(estimated_block_time, &expected_estimated_block_time); + assert_eq!(max_batch_size, expected_max_batch_size); + assert_eq!(reorg_period, expected_reorg_period); +} diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/estimate.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/estimate.rs new file mode 100644 index 00000000000..ea8888cc730 --- /dev/null +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/estimate.rs @@ -0,0 +1,23 @@ +use hyperlane_core::U256; + +use crate::chain_tx_adapter::chains::sealevel::adapter::tests::common::{ + adapter, payload, GAS_LIMIT, +}; +use crate::chain_tx_adapter::AdaptsChain; +use crate::payload::FullPayload; + +#[tokio::test] +async fn test_estimate_gas_limit() { + // given + let adapter = adapter(); + let payload = payload(); + + let expected = U256::from(GAS_LIMIT); + + // when + let result = adapter.estimate_gas_limit(&payload).await; + + // then + assert!(result.is_ok()); + assert_eq!(expected, result.unwrap().unwrap()); +} diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/simulate.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/simulate.rs new file mode 100644 index 00000000000..e52a7ae1a51 --- /dev/null +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/simulate.rs @@ -0,0 +1,18 @@ +use crate::chain_tx_adapter::chains::sealevel::adapter::tests::common::{ + adapter, payload, precursor, +}; +use crate::chain_tx_adapter::chains::sealevel::transaction::TransactionFactory; +use crate::chain_tx_adapter::AdaptsChain; + +#[tokio::test] +async fn test_simulate_tx() { + // given + let adapter = adapter(); + let transaction = TransactionFactory::build(&payload(), precursor()); + + // when + let simulated = adapter.simulate_tx(&transaction).await.unwrap(); + + // then + assert!(simulated); +} diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/status.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/status.rs new file mode 100644 index 00000000000..279725d2497 --- /dev/null +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/status.rs @@ -0,0 +1,18 @@ +use crate::chain_tx_adapter::chains::sealevel::adapter::tests::common::{adapter, transaction}; +use crate::chain_tx_adapter::AdaptsChain; +use crate::transaction::TransactionStatus; + +#[tokio::test] +async fn test_tx_status() { + // given + let adapter = adapter(); + let transaction = transaction(); + + // when + let result = adapter.tx_status(&transaction).await; + + // then + assert!(result.is_ok()); + let status = result.unwrap(); + assert!(matches!(status, TransactionStatus::Finalized)); +} diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/submit.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/submit.rs new file mode 100644 index 00000000000..30c0784b013 --- /dev/null +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/adapter/tests/submit.rs @@ -0,0 +1,18 @@ +use crate::chain_tx_adapter::chains::sealevel::adapter::tests::common::{ + adapter, payload, precursor, +}; +use crate::chain_tx_adapter::chains::sealevel::transaction::TransactionFactory; +use crate::chain_tx_adapter::AdaptsChain; + +#[tokio::test] +async fn test_submit() { + // given + let adapter = adapter(); + let mut transaction = TransactionFactory::build(&payload(), precursor()); + + // when + let result = adapter.submit(&mut transaction).await; + + // then + assert!(result.is_ok()); +} diff --git a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/payload.rs b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/payload.rs index ddc0cae61a3..93bb44e54c7 100644 --- a/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/payload.rs +++ b/rust/main/submitter/src/chain_tx_adapter/chains/sealevel/payload.rs @@ -1,21 +1,14 @@ use solana_sdk::instruction::Instruction as SealevelInstruction; -use crate::payload::{FullPayload, VmSpecificPayloadData}; - -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct SealevelPayload { - pub instruction: SealevelInstruction, -} +use crate::payload::FullPayload; pub(crate) trait Instruction { - fn instruction(&self) -> &SealevelInstruction; + fn instruction(&self) -> SealevelInstruction; } impl Instruction for FullPayload { - fn instruction(&self) -> &SealevelInstruction { - match &self.data { - VmSpecificPayloadData::Svm(payload) => &payload.instruction, - _ => panic!(), - } + fn instruction(&self) -> SealevelInstruction { + serde_json::from_slice::(&self.data) + .expect("Payload should contain serialised Instruction for Sealevel") } } diff --git a/rust/main/submitter/src/error.rs b/rust/main/submitter/src/error.rs new file mode 100644 index 00000000000..376c04dcfc8 --- /dev/null +++ b/rust/main/submitter/src/error.rs @@ -0,0 +1,58 @@ +#![allow(dead_code)] + +use hyperlane_base::db::DbError; + +use crate::transaction::Transaction; + +#[derive(Debug, thiserror::Error)] +pub enum SubmitterError { + #[error("Network error: {0}")] + NetworkError(String), + #[error("Transaction error: {0}")] + TxSubmissionError(String), + #[error("This transaction has already been broadcast")] + TxAlreadyExists, + #[error("The transaction reverted")] + TxReverted, + #[error("Failed to send over a channel {0}")] + ChannelSendFailure(#[from] tokio::sync::mpsc::error::SendError), + #[error("Channel closed")] + ChannelClosed, + #[error("{0}")] + EyreError(#[from] eyre::Report), + #[error("Payload not found")] + PayloadNotFound, + #[error("Transaction simulation failed")] + SimulationFailed, + #[error("Non-retryable error: {0}")] + NonRetryableError(String), + + // TODO: fully decouple from these crates + #[error("DB error {0}")] + DbError(#[from] DbError), + #[error("Chain communication error {0}")] + ChainCommunicationError(#[from] hyperlane_core::ChainCommunicationError), +} + +pub trait IsRetryable { + fn is_retryable(&self) -> bool; +} + +impl IsRetryable for SubmitterError { + fn is_retryable(&self) -> bool { + match self { + SubmitterError::NetworkError(_) => true, + SubmitterError::TxSubmissionError(_) => true, + SubmitterError::ChannelSendFailure(_) => true, + SubmitterError::ChainCommunicationError(_) => true, + SubmitterError::EyreError(_) => true, + SubmitterError::NonRetryableError(_) => false, + SubmitterError::TxReverted => false, + SubmitterError::SimulationFailed => false, + SubmitterError::ChannelClosed => false, + SubmitterError::PayloadNotFound => false, + SubmitterError::TxAlreadyExists => false, + SubmitterError::DbError(_) => false, + } + } +} diff --git a/rust/main/submitter/src/lib.rs b/rust/main/submitter/src/lib.rs index fd8975a6d17..6935d118a26 100644 --- a/rust/main/submitter/src/lib.rs +++ b/rust/main/submitter/src/lib.rs @@ -1,4 +1,16 @@ +pub use error::SubmitterError; +pub use payload::{ + DropReason as PayloadDropReason, FullPayload, PayloadId, PayloadStatus, + RetryReason as PayloadRetryReason, +}; +pub use payload_dispatcher::{ + DatabaseOrPath, Entrypoint, PayloadDispatcher, PayloadDispatcherEntrypoint, + PayloadDispatcherSettings, +}; +pub use transaction::{DropReason as TransactionDropReason, TransactionStatus}; + mod chain_tx_adapter; +mod error; mod payload; mod payload_dispatcher; mod transaction; diff --git a/rust/main/submitter/src/payload.rs b/rust/main/submitter/src/payload.rs index 824da704e56..c8d2263611f 100644 --- a/rust/main/submitter/src/payload.rs +++ b/rust/main/submitter/src/payload.rs @@ -1,8 +1,6 @@ // TODO: re-enable clippy warnings #![allow(unused_imports)] -mod db; mod types; -pub use db::*; pub use types::*; diff --git a/rust/main/submitter/src/payload/types.rs b/rust/main/submitter/src/payload/types.rs index 76a1ce82d9c..7431fecb00c 100644 --- a/rust/main/submitter/src/payload/types.rs +++ b/rust/main/submitter/src/payload/types.rs @@ -8,7 +8,7 @@ use uuid::Uuid; use hyperlane_core::{identifiers::UniqueIdentifier, H256, U256}; -use crate::chain_tx_adapter::SealevelPayload; +use crate::transaction::TransactionStatus; pub type PayloadId = UniqueIdentifier; type Address = H256; @@ -27,13 +27,23 @@ pub struct PayloadDetails { pub success_criteria: Option<(Vec, Address)>, } +impl PayloadDetails { + pub fn new(id: PayloadId, metadata: impl Into) -> Self { + Self { + id, + metadata: metadata.into(), + success_criteria: None, + } + } +} + /// Full details about a payload. This is instantiated by the caller of PayloadDispatcher #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, PartialEq, Eq)] pub struct FullPayload { /// reference to payload used by other components pub details: PayloadDetails, /// calldata on EVM. On SVM, it is the serialized instructions and account list. On Cosmos, it is the serialized vec of msgs - pub data: VmSpecificPayloadData, + pub data: Vec, /// defaults to the hyperlane mailbox pub to: Address, /// defaults to `ReadyToSubmit` @@ -47,25 +57,61 @@ pub struct FullPayload { } impl FullPayload { + pub fn new(id: PayloadId, metadata: impl Into, data: Vec, to: Address) -> Self { + Self { + details: PayloadDetails::new(id, metadata), + data, + to, + status: Default::default(), + value: None, + inclusion_soft_deadline: None, + } + } + pub fn id(&self) -> &PayloadId { &self.details.id } + + #[cfg(test)] + pub fn random() -> Self { + let id = PayloadId::random(); + let details = PayloadDetails { + id: id.clone(), + metadata: format!("payload-{}", id.to_string()), + success_criteria: None, + }; + FullPayload { + details, + data: vec![], + to: Address::zero(), + status: PayloadStatus::default(), + value: None, + inclusion_soft_deadline: None, + } + } } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq, Default)] pub enum PayloadStatus { #[default] ReadyToSubmit, - PendingInclusion, - Included, - Finalized, - NotFound, + InTransaction(TransactionStatus), Dropped(DropReason), Retry(RetryReason), } +impl PayloadStatus { + pub fn is_finalized(&self) -> bool { + matches!( + self, + PayloadStatus::InTransaction(TransactionStatus::Finalized) + ) + } +} + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] pub enum DropReason { + FailedToBuildAsTransaction, FailedSimulation, Reverted, UnhandledError, @@ -75,12 +121,3 @@ pub enum DropReason { pub enum RetryReason { Reorged, } - -// add nested enum entries as we add VMs -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, PartialEq, Eq)] -pub enum VmSpecificPayloadData { - #[default] - Evm, - Svm(SealevelPayload), - CosmWasm, -} diff --git a/rust/main/submitter/src/payload_dispatcher.rs b/rust/main/submitter/src/payload_dispatcher.rs index 747376f100a..bfb0ed68b9a 100644 --- a/rust/main/submitter/src/payload_dispatcher.rs +++ b/rust/main/submitter/src/payload_dispatcher.rs @@ -1,12 +1,16 @@ // TODO: re-enable clippy warnings #![allow(unused_imports)] +mod db; mod dispatcher; mod entrypoint; - mod stages; -mod test_utils; +#[cfg(test)] +pub mod test_utils; +#[cfg(test)] +mod tests; +pub use db::*; pub use dispatcher::*; pub use entrypoint::*; pub use stages::*; diff --git a/rust/main/submitter/src/payload_dispatcher/db.rs b/rust/main/submitter/src/payload_dispatcher/db.rs new file mode 100644 index 00000000000..792516c7731 --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/db.rs @@ -0,0 +1,7 @@ +mod loader; +mod payload; +mod transaction; + +pub use loader::*; +pub use payload::*; +pub use transaction::*; diff --git a/rust/main/submitter/src/payload_dispatcher/db/loader.rs b/rust/main/submitter/src/payload_dispatcher/db/loader.rs new file mode 100644 index 00000000000..e74db0b9a77 --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/db/loader.rs @@ -0,0 +1,317 @@ +// TODO: re-enable clippy warnings +#![allow(dead_code)] + +use std::cmp::max; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; +use std::time::Duration; + +use derive_new::new; +use hyperlane_base::db::HyperlaneDb; + +use async_trait::async_trait; +use hyperlane_base::db::DbResult; +use tokio::time::sleep; +use tracing::{debug, instrument}; + +use crate::error::SubmitterError; + +#[async_trait] +pub trait LoadableFromDb { + type Item: Sized; + + async fn highest_index(&self) -> Result; + async fn retrieve_by_index(&self, index: u32) -> Result, SubmitterError>; + async fn load(&self, item: Self::Item) -> Result; +} + +pub enum LoadingOutcome { + Loaded, + Skipped, +} + +#[derive(Debug)] +pub struct DbIterator { + low_index_iter: DirectionalIndexIterator, + high_index_iter: Option>, + // here for debugging purposes + _metadata: String, +} + +impl DbIterator { + #[instrument(skip(loader), ret)] + pub async fn new(loader: Arc, metadata: String, only_load_backward: bool) -> Self { + // the db returns 0 if uninitialized + let high_index = max(loader.highest_index().await.unwrap_or_default(), 1); + let mut low_index_iter = DirectionalIndexIterator::new( + high_index, + IndexDirection::Low, + loader.clone(), + metadata.clone(), + ); + let high_index_iter = if only_load_backward { + None + } else { + let high_index_iter = DirectionalIndexIterator::new( + // If the high nonce is None, we start from the beginning + high_index, + IndexDirection::High, + loader, + metadata.clone(), + ); + // Decrement the low index to avoid processing the same index twice + low_index_iter.iterate(); + Some(high_index_iter) + }; + + debug!( + ?low_index_iter, + ?high_index_iter, + ?metadata, + "Initialized ForwardBackwardIterator" + ); + Self { + low_index_iter, + high_index_iter, + _metadata: metadata, + } + } + + async fn try_load_next_item(&mut self) -> Result { + // Always prioritize advancing the the high nonce iterator, as + // we have a preference for higher nonces + if let Some(high_index_iter) = &mut self.high_index_iter { + if let Some(LoadingOutcome::Loaded) = high_index_iter.try_load_item().await? { + // If we have a high nonce item, we can process it + high_index_iter.iterate(); + return Ok(LoadingOutcome::Loaded); + } + } + + // Low nonce messages are only processed if the high nonce iterator + // can't make any progress + if let Some(LoadingOutcome::Loaded) = self.low_index_iter.try_load_item().await? { + // If we have a low nonce item, we can process it + self.low_index_iter.iterate(); + return Ok(LoadingOutcome::Loaded); + } + Ok(LoadingOutcome::Skipped) + } + + pub async fn load_from_db(&mut self) -> Result<(), SubmitterError> { + loop { + if let LoadingOutcome::Skipped = self.try_load_next_item().await? { + if self.high_index_iter.is_none() { + // If we are only loading backward, we have processed all items + return Ok(()); + } + // sleep to wait for new items to be added + sleep(Duration::from_secs(1)).await; + } + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq)] +enum IndexDirection { + #[default] + High, + Low, +} + +#[derive(new)] +struct DirectionalIndexIterator { + index: u32, + direction: IndexDirection, + loader: Arc, + _metadata: String, +} + +impl Debug for DirectionalIndexIterator { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "DirectionalNonceIterator {{ index: {:?}, direction: {:?}, metadata: {:?} }}", + self.index, self.direction, self._metadata + ) + } +} + +impl DirectionalIndexIterator { + #[instrument] + fn iterate(&mut self) { + match self.direction { + IndexDirection::High => { + self.index = self.index.saturating_add(1); + debug!(?self, "Iterating high nonce"); + } + IndexDirection::Low => { + if self.index == 0 { + // If we are at the beginning, we can't go lower + return; + } + self.index = self.index.saturating_sub(1); + debug!(?self, "Iterating low nonce"); + } + } + } + + async fn try_load_item(&self) -> Result, SubmitterError> { + let Some(item) = self.loader.retrieve_by_index(self.index).await? else { + return Ok(None); + }; + Ok(Some(self.loader.load(item).await?)) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use solana_sdk::nonce::state; + use tokio::sync::Mutex; + + use super::*; + + #[derive(Debug, Clone)] + struct MockDbState { + data: HashMap, + highest_index: u32, + } + + #[derive(Debug, Clone)] + struct MockDb { + state: Arc>, + } + + impl MockDb { + fn new() -> Self { + Self { + state: Arc::new(Mutex::new(MockDbState { + data: HashMap::new(), + highest_index: 0, + })), + } + } + } + + #[async_trait] + impl LoadableFromDb for MockDb { + type Item = String; + + async fn highest_index(&self) -> Result { + let state = self.state.lock().await; + Ok(state.highest_index) + } + + async fn retrieve_by_index( + &self, + index: u32, + ) -> Result, SubmitterError> { + let state = self.state.lock().await; + Ok(state.data.get(&index).cloned()) + } + + async fn load(&self, item: Self::Item) -> Result { + debug!("Loading item: {:?}", item); + Ok(LoadingOutcome::Loaded) + } + } + + async fn set_up_state( + only_load_backward: bool, + num_items: usize, + ) -> (DbIterator, Arc) { + let db = Arc::new(MockDb::new()); + let metadata = "Test Metadata".to_string(); + + // Simulate adding items to the database + { + let mut state = db.state.lock().await; + + for i in 1..=num_items { + state.data.insert(i as u32, format!("Item {}", i)); + } + state.highest_index = num_items as u32; + } + ( + DbIterator::new(db.clone(), metadata.clone(), only_load_backward).await, + db, + ) + } + + #[tokio::test] + async fn test_db_iterator_forward_backward() { + let only_load_backward = false; + let num_db_insertions = 2; + let (mut iterator, _) = set_up_state(only_load_backward, num_db_insertions).await; + + assert_eq!(iterator.low_index_iter.index, 1); + assert_eq!(iterator.high_index_iter.as_ref().unwrap().index, 2); + iterator.try_load_next_item().await.unwrap(); + assert_eq!(iterator.low_index_iter.index, 1); + assert_eq!(iterator.high_index_iter.unwrap().index, 3); + } + + #[tokio::test] + async fn test_db_iterator_only_backward() { + let only_load_backward = true; + let num_db_insertions = 2; + let (mut iterator, _) = set_up_state(only_load_backward, num_db_insertions).await; + + assert_eq!(iterator.low_index_iter.index, 2); + assert!(iterator.high_index_iter.is_none()); + iterator.try_load_next_item().await.unwrap(); + assert_eq!(iterator.low_index_iter.index, 1); + assert!(iterator.high_index_iter.is_none()); + } + + #[tokio::test] + async fn test_load_from_db_finishes_if_only_loading_backward() { + let only_load_backward = true; + let num_db_insertions = 5; + let (mut iterator, _) = set_up_state(only_load_backward, num_db_insertions).await; + + iterator.load_from_db().await.unwrap(); + assert_eq!(iterator.low_index_iter.index, 0); + assert!(iterator.high_index_iter.is_none()); + } + + #[tokio::test] + async fn test_load_from_db_keeps_running_if_forward_backward() { + let only_load_backward = false; + let num_db_insertions: u32 = 5; + let (mut iterator, db) = set_up_state(only_load_backward, num_db_insertions as usize).await; + // this future is used to assert that the iterator keeps running + // and doesn't finish loading from the db + let first_assertion_and_state_change = async { + sleep(Duration::from_millis(100)).await; + { + let mut state = db.state.lock().await; + let new_num_db_insertions = num_db_insertions + 1; + state.data.insert( + (new_num_db_insertions) as u32, + format!("Item {}", new_num_db_insertions), + ); + state.highest_index = new_num_db_insertions as u32; + } + + // now sleep for a bit to let the iterator process the new item + sleep(Duration::from_millis(1100)).await; + }; + + tokio::select! { + _ = iterator.load_from_db() => { + panic!("Loading from db finished although the high iterator should've kept waiting for new items"); + } + _ = first_assertion_and_state_change => { + } + }; + + assert_eq!(iterator.low_index_iter.index, 0); + assert_eq!( + iterator.high_index_iter.as_ref().unwrap().index, + num_db_insertions + 2 + ); + } +} diff --git a/rust/main/submitter/src/payload_dispatcher/db/payload.rs b/rust/main/submitter/src/payload_dispatcher/db/payload.rs new file mode 100644 index 00000000000..d00bdc6e1c2 --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/db/payload.rs @@ -0,0 +1,5 @@ +mod loader; +mod payload_db; + +pub use loader::*; +pub use payload_db::*; diff --git a/rust/main/submitter/src/payload_dispatcher/db/payload/loader.rs b/rust/main/submitter/src/payload_dispatcher/db/payload/loader.rs new file mode 100644 index 00000000000..898d02ed92d --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/db/payload/loader.rs @@ -0,0 +1,60 @@ +use std::{ + fmt::{Debug, Formatter}, + sync::{mpsc::Sender, Arc}, +}; + +use async_trait::async_trait; +use derive_new::new; +use tracing::trace; + +use crate::{ + error::SubmitterError, + payload::{FullPayload, PayloadStatus}, + payload_dispatcher::{BuildingStageQueue, DbIterator, LoadableFromDb, LoadingOutcome}, +}; + +use super::PayloadDb; + +#[derive(new)] +pub struct PayloadDbLoader { + db: Arc, + building_stage_queue: BuildingStageQueue, +} + +impl PayloadDbLoader { + pub async fn into_iterator(self) -> DbIterator { + DbIterator::new(Arc::new(self), "payload_db_loader".to_string(), false).await + } +} + +impl Debug for PayloadDbLoader { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PayloadDbLoader").finish() + } +} + +#[async_trait] +impl LoadableFromDb for PayloadDbLoader { + type Item = FullPayload; + + async fn highest_index(&self) -> Result { + Ok(self.db.retrieve_highest_index().await?) + } + + async fn retrieve_by_index(&self, index: u32) -> Result, SubmitterError> { + Ok(self.db.retrieve_payload_by_index(index).await?) + } + + async fn load(&self, item: FullPayload) -> Result { + match item.status { + PayloadStatus::ReadyToSubmit | PayloadStatus::Retry(_) => { + self.building_stage_queue.lock().await.push_back(item); + Ok(LoadingOutcome::Loaded) + } + PayloadStatus::Dropped(_) | PayloadStatus::InTransaction(_) => { + trace!(?item, "Payload already processed"); + Ok(LoadingOutcome::Skipped) + } + } + } +} diff --git a/rust/main/submitter/src/payload_dispatcher/db/payload/payload_db.rs b/rust/main/submitter/src/payload_dispatcher/db/payload/payload_db.rs new file mode 100644 index 00000000000..3911fdfe195 --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/db/payload/payload_db.rs @@ -0,0 +1,246 @@ +// TODO: re-enable clippy warnings +#![allow(dead_code)] + +use std::io::Write; + +use async_trait::async_trait; +use eyre::eyre; +use hyperlane_base::db::{DbError, DbResult, HyperlaneRocksDB}; +use hyperlane_core::{identifiers::UniqueIdentifier, Decode, Encode, HyperlaneProtocolError}; + +use crate::{ + payload::{FullPayload, PayloadId, PayloadStatus}, + transaction::TransactionId, +}; + +const PAYLOAD_BY_ID_STORAGE_PREFIX: &str = "payload_by_id_"; +const TRANSACTION_ID_BY_PAYLOAD_ID_STORAGE_PREFIX: &str = "transaction_id_by_payload_id_"; +const PAYLOAD_INDEX_BY_ID_STORAGE_PREFIX: &str = "payload_index_by_id_"; +const PAYLOAD_ID_BY_INDEX_STORAGE_PREFIX: &str = "payload_id_by_index_"; +const HIGHEST_PAYLOAD_INDEX_STORAGE_PREFIX: &str = "highest_payload_index_"; + +#[async_trait] +pub trait PayloadDb: Send + Sync { + /// Retrieve a payload by its unique ID + async fn retrieve_payload_by_id(&self, id: &PayloadId) -> DbResult>; + + /// Store a payload by its unique ID + async fn store_payload_by_id(&self, payload: &FullPayload) -> DbResult<()>; + + /// Retrieve a payload index by its unique ID + async fn retrieve_payload_index_by_id(&self, payload_id: &PayloadId) -> DbResult>; + + /// Store a payload index by the payload's unique ID + async fn store_payload_index_by_id(&self, index: u32, payload_id: &PayloadId) -> DbResult<()>; + + /// Retrieve a payload's unique ID by its index + async fn retrieve_payload_id_by_index(&self, index: u32) -> DbResult>; + + /// Store a payload's unique ID by the payload's index + async fn store_payload_id_by_index(&self, index: u32, payload_id: &PayloadId) -> DbResult<()>; + + /// Retrieve a payload by its index + async fn retrieve_payload_by_index(&self, index: u32) -> DbResult> { + let id = self.retrieve_payload_id_by_index(index).await?; + if let Some(id) = id { + self.retrieve_payload_by_id(&id).await + } else { + Ok(None) + } + } + + /// Store the highest payload index + async fn store_highest_index(&self, index: u32) -> DbResult<()>; + + /// Retrieve the highest payload index + async fn retrieve_highest_index(&self) -> DbResult; + + /// Set the status of a payload by its unique ID. Performs one read (to first fetch the full payload) and one write. + async fn store_new_payload_status( + &self, + payload_id: &PayloadId, + new_status: PayloadStatus, + ) -> DbResult<()> { + if let Some(mut payload) = self.retrieve_payload_by_id(payload_id).await? { + payload.status = new_status; + self.store_payload_by_id(&payload).await?; + } else { + return Err(DbError::Other(format!( + "Payload with ID {:?} not found", + payload_id + ))); + } + Ok(()) + } + + async fn store_tx_id_by_payload_id( + &self, + payload_id: &PayloadId, + tx_id: &TransactionId, + ) -> DbResult<()>; + + async fn retrieve_tx_id_by_payload_id( + &self, + payload_id: &PayloadId, + ) -> DbResult>; +} + +#[async_trait] +impl PayloadDb for HyperlaneRocksDB { + async fn retrieve_payload_by_id(&self, id: &PayloadId) -> DbResult> { + self.retrieve_value_by_key(PAYLOAD_BY_ID_STORAGE_PREFIX, id) + } + + async fn store_payload_by_id(&self, payload: &FullPayload) -> DbResult<()> { + if self + .retrieve_payload_index_by_id(payload.id()) + .await? + .is_none() + { + let highest_index = self.retrieve_highest_index().await?; + let payload_index = highest_index + 1; + self.store_highest_index(payload_index).await?; + self.store_payload_index_by_id(payload_index, payload.id()) + .await?; + self.store_payload_id_by_index(payload_index, payload.id()) + .await?; + } + self.store_value_by_key(PAYLOAD_BY_ID_STORAGE_PREFIX, payload.id(), payload) + } + + async fn store_tx_id_by_payload_id( + &self, + payload_id: &PayloadId, + tx_id: &TransactionId, + ) -> DbResult<()> { + self.store_value_by_key( + TRANSACTION_ID_BY_PAYLOAD_ID_STORAGE_PREFIX, + payload_id, + tx_id, + ) + } + + async fn retrieve_tx_id_by_payload_id( + &self, + payload_id: &PayloadId, + ) -> DbResult> { + self.retrieve_value_by_key(TRANSACTION_ID_BY_PAYLOAD_ID_STORAGE_PREFIX, payload_id) + } + + async fn retrieve_payload_index_by_id(&self, id: &PayloadId) -> DbResult> { + self.retrieve_value_by_key(PAYLOAD_INDEX_BY_ID_STORAGE_PREFIX, id) + } + + async fn store_payload_index_by_id(&self, index: u32, payload_id: &PayloadId) -> DbResult<()> { + self.store_value_by_key(PAYLOAD_INDEX_BY_ID_STORAGE_PREFIX, payload_id, &index) + } + + async fn store_highest_index(&self, index: u32) -> DbResult<()> { + // There's no unit struct Encode/Decode impl, so just use `bool` and always use the `Default::default()` key + self.store_value_by_key( + HIGHEST_PAYLOAD_INDEX_STORAGE_PREFIX, + &bool::default(), + &index, + ) + } + + async fn retrieve_highest_index(&self) -> DbResult { + // return the default value (0) if no index has been stored yet + self.retrieve_value_by_key(HIGHEST_PAYLOAD_INDEX_STORAGE_PREFIX, &bool::default()) + .map(|index: Option| index.unwrap_or_default()) + } + + async fn store_payload_id_by_index(&self, index: u32, payload_id: &PayloadId) -> DbResult<()> { + self.store_value_by_key(PAYLOAD_ID_BY_INDEX_STORAGE_PREFIX, &index, payload_id) + } + + async fn retrieve_payload_id_by_index(&self, index: u32) -> DbResult> { + self.retrieve_value_by_key(PAYLOAD_ID_BY_INDEX_STORAGE_PREFIX, &index) + } +} + +impl Encode for FullPayload { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: Write, + { + // Serialize to JSON and write to the writer, to avoid having to implement the encoding manually + let serialized = serde_json::to_vec(self) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Failed to serialize"))?; + writer.write(&serialized) + } +} + +impl Decode for FullPayload { + fn read_from(reader: &mut R) -> Result + where + R: std::io::Read, + Self: Sized, + { + // Deserialize from JSON and read from the reader, to avoid having to implement the encoding / decoding manually + serde_json::from_reader(reader).map_err(|err| { + HyperlaneProtocolError::IoError(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to deserialize. Error: {}", err), + )) + }) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use hyperlane_base::db::{HyperlaneRocksDB, DB}; + use hyperlane_core::KnownHyperlaneDomain; + + use crate::{ + payload::{FullPayload, PayloadStatus}, + transaction::TransactionStatus, + }; + + use super::PayloadDb; + + fn tmp_db() -> Arc { + let temp_dir = tempfile::tempdir().unwrap(); + let db = DB::from_path(temp_dir.path()).unwrap(); + let domain = KnownHyperlaneDomain::Arbitrum.into(); + let rocksdb = Arc::new(HyperlaneRocksDB::new(&domain, db)); + rocksdb + } + + #[tokio::test] + async fn test_index_is_set_correctly() { + let num_payloads = 10; + let db = tmp_db(); + + for i in 0..num_payloads { + let mut payload = FullPayload::random(); + + // storing to this new payload ID for the first time should create a new + // highest index + db.store_payload_by_id(&payload).await.unwrap(); + let expected_payload_index = (i + 1) as u32; + let retrieved_payload = db + .retrieve_payload_by_index(expected_payload_index) + .await + .unwrap() + .unwrap(); + assert_eq!(retrieved_payload, payload); + let highest_index = db.retrieve_highest_index().await.unwrap(); + assert_eq!(highest_index, expected_payload_index); + + // storing to this payload ID again should not create a new highest index + payload.status = PayloadStatus::InTransaction(TransactionStatus::PendingInclusion); + db.store_payload_by_id(&payload).await.unwrap(); + let retrieved_payload = db + .retrieve_payload_by_index(expected_payload_index) + .await + .unwrap() + .unwrap(); + assert_eq!(retrieved_payload, payload); + let highest_index = db.retrieve_highest_index().await.unwrap(); + assert_eq!(highest_index, expected_payload_index); + } + } +} diff --git a/rust/main/submitter/src/payload_dispatcher/db/transaction.rs b/rust/main/submitter/src/payload_dispatcher/db/transaction.rs new file mode 100644 index 00000000000..1120156ae18 --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/db/transaction.rs @@ -0,0 +1,5 @@ +mod loader; +mod transaction_db; + +pub use loader::*; +pub use transaction_db::*; diff --git a/rust/main/submitter/src/payload_dispatcher/db/transaction/loader.rs b/rust/main/submitter/src/payload_dispatcher/db/transaction/loader.rs new file mode 100644 index 00000000000..fec29e084d4 --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/db/transaction/loader.rs @@ -0,0 +1,65 @@ +use std::{ + fmt::{Debug, Formatter}, + sync::Arc, +}; + +use async_trait::async_trait; +use derive_new::new; +use tokio::sync::mpsc::Sender; +use tracing::trace; + +use crate::{ + error::SubmitterError, + payload_dispatcher::{DbIterator, LoadableFromDb, LoadingOutcome}, + transaction::{Transaction, TransactionStatus}, +}; + +use super::TransactionDb; + +#[derive(new)] +pub struct TransactionDbLoader { + db: Arc, + inclusion_stage_sender: Sender, + finality_stage_sender: Sender, +} +impl TransactionDbLoader { + pub async fn into_iterator(self) -> DbIterator { + DbIterator::new(Arc::new(self), "transaction_db_loader".to_string(), true).await + } +} + +impl Debug for TransactionDbLoader { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TransactionDbLoader").finish() + } +} + +#[async_trait] +impl LoadableFromDb for TransactionDbLoader { + type Item = Transaction; + + async fn highest_index(&self) -> Result { + Ok(self.db.retrieve_highest_index().await?) + } + + async fn retrieve_by_index(&self, index: u32) -> Result, SubmitterError> { + Ok(self.db.retrieve_transaction_by_index(index).await?) + } + + async fn load(&self, item: Self::Item) -> Result { + match item.status { + TransactionStatus::PendingInclusion | TransactionStatus::Mempool => { + self.inclusion_stage_sender.send(item).await?; + Ok(LoadingOutcome::Loaded) + } + TransactionStatus::Included => { + self.finality_stage_sender.send(item).await?; + Ok(LoadingOutcome::Loaded) + } + TransactionStatus::Finalized | TransactionStatus::Dropped(_) => { + trace!(?item, "Transaction already processed"); + Ok(LoadingOutcome::Skipped) + } + } + } +} diff --git a/rust/main/submitter/src/payload_dispatcher/db/transaction/transaction_db.rs b/rust/main/submitter/src/payload_dispatcher/db/transaction/transaction_db.rs new file mode 100644 index 00000000000..00382ba474f --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/db/transaction/transaction_db.rs @@ -0,0 +1,221 @@ +// TODO: re-enable clippy warnings +#![allow(dead_code)] + +use std::io::Write; + +use async_trait::async_trait; +use hyperlane_base::db::{DbResult, HyperlaneRocksDB}; +use hyperlane_core::{Decode, Encode, HyperlaneProtocolError}; + +use crate::transaction::{Transaction, TransactionId}; + +const TRANSACTION_BY_ID_STORAGE_PREFIX: &str = "transaction_by_id_"; + +const NONCE_BY_TX_ID_STORAGE_PREFIX: &str = "nonce_by_tx_id_"; + +const TRANSACTION_INDEX_BY_ID_STORAGE_PREFIX: &str = "tx_index_by_id_"; +const TRANSACTION_ID_BY_INDEX_STORAGE_PREFIX: &str = "tx_id_by_index_"; +const HIGHEST_TRANSACTION_INDEX_STORAGE_PREFIX: &str = "highest_tx_index_"; + +#[async_trait] +pub trait TransactionDb: Send + Sync { + /// Retrieve a transaction by its unique ID + async fn retrieve_transaction_by_id(&self, id: &TransactionId) + -> DbResult>; + + /// Store a transaction by its unique ID + async fn store_transaction_by_id(&self, tx: &Transaction) -> DbResult<()>; + + /// Retrieve a transaction's index by its unique ID + async fn retrieve_transaction_index_by_id(&self, id: &TransactionId) -> DbResult>; + + /// Store a transaction's index by its unique ID + async fn store_transaction_index_by_id( + &self, + index: u32, + tx_id: &TransactionId, + ) -> DbResult<()>; + + /// Retrieve a transaction's unique ID by its index + async fn retrieve_transaction_id_by_index(&self, index: u32) + -> DbResult>; + + /// Store a transaction's unique ID by its index + async fn store_transaction_id_by_index( + &self, + index: u32, + tx_id: &TransactionId, + ) -> DbResult<()>; + + /// Retrieve a transaction by its index + async fn retrieve_transaction_by_index(&self, index: u32) -> DbResult> { + let id = self.retrieve_transaction_id_by_index(index).await?; + if let Some(id) = id { + self.retrieve_transaction_by_id(&id).await + } else { + Ok(None) + } + } + + /// Store the highest transaction index + async fn store_highest_index(&self, index: u32) -> DbResult<()>; + + /// Retrieve the highest transaction index + async fn retrieve_highest_index(&self) -> DbResult; +} + +#[async_trait] +impl TransactionDb for HyperlaneRocksDB { + async fn retrieve_transaction_by_id( + &self, + id: &TransactionId, + ) -> DbResult> { + self.retrieve_value_by_key(TRANSACTION_BY_ID_STORAGE_PREFIX, id) + } + + async fn store_transaction_by_id(&self, tx: &Transaction) -> DbResult<()> { + if self + .retrieve_transaction_index_by_id(&tx.id) + .await? + .is_none() + { + let highest_index = self.retrieve_highest_index().await?; + let tx_index = highest_index + 1; + self.store_highest_index(tx_index).await?; + self.store_transaction_index_by_id(tx_index, &tx.id).await?; + self.store_transaction_id_by_index(tx_index, &tx.id).await?; + } + self.store_value_by_key(TRANSACTION_BY_ID_STORAGE_PREFIX, &tx.id, tx) + } + + async fn retrieve_transaction_index_by_id( + &self, + tx_id: &TransactionId, + ) -> DbResult> { + self.retrieve_value_by_key(TRANSACTION_INDEX_BY_ID_STORAGE_PREFIX, tx_id) + } + + async fn store_transaction_index_by_id( + &self, + index: u32, + tx_id: &TransactionId, + ) -> DbResult<()> { + self.store_value_by_key(TRANSACTION_INDEX_BY_ID_STORAGE_PREFIX, tx_id, &index) + } + + async fn store_highest_index(&self, index: u32) -> DbResult<()> { + // There's no unit struct Encode/Decode impl, so just use `bool` and always use the `Default::default()` key + self.store_value_by_key( + HIGHEST_TRANSACTION_INDEX_STORAGE_PREFIX, + &bool::default(), + &index, + ) + } + + async fn retrieve_highest_index(&self) -> DbResult { + // return the default value (0) if no index has been stored yet + self.retrieve_value_by_key(HIGHEST_TRANSACTION_INDEX_STORAGE_PREFIX, &bool::default()) + .map(|index| index.unwrap_or_default()) + } + + async fn store_transaction_id_by_index( + &self, + index: u32, + tx_id: &TransactionId, + ) -> DbResult<()> { + self.store_value_by_key(TRANSACTION_ID_BY_INDEX_STORAGE_PREFIX, &index, tx_id) + } + + async fn retrieve_transaction_id_by_index( + &self, + index: u32, + ) -> DbResult> { + self.retrieve_value_by_key(TRANSACTION_ID_BY_INDEX_STORAGE_PREFIX, &index) + } +} + +impl Encode for Transaction { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: Write, + { + // Serialize to JSON and write to the writer, to avoid having to implement the encoding manually + let serialized = serde_json::to_vec(self) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Failed to serialize"))?; + writer.write(&serialized) + } +} + +impl Decode for Transaction { + fn read_from(reader: &mut R) -> Result + where + R: std::io::Read, + Self: Sized, + { + // Deserialize from JSON and read from the reader, to avoid having to implement the encoding / decoding manually + serde_json::from_reader(reader).map_err(|err| { + HyperlaneProtocolError::IoError(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to deserialize. Error: {}", err), + )) + }) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use hyperlane_base::db::{HyperlaneRocksDB, DB}; + use hyperlane_core::KnownHyperlaneDomain; + + use crate::payload::FullPayload; + use crate::payload_dispatcher::test_utils::dummy_tx; + use crate::transaction::TransactionStatus; + + use super::TransactionDb; + + fn tmp_db() -> Arc { + let temp_dir = tempfile::tempdir().unwrap(); + let db = DB::from_path(temp_dir.path()).unwrap(); + let domain = KnownHyperlaneDomain::Arbitrum.into(); + let rocksdb = Arc::new(HyperlaneRocksDB::new(&domain, db)); + rocksdb + } + + #[tokio::test] + async fn test_index_is_set_correctly() { + let num_txs = 10; + let db = tmp_db(); + + for i in 0..num_txs { + let payload = FullPayload::random(); + let mut tx = dummy_tx(vec![payload.clone()], TransactionStatus::PendingInclusion); + + // storing to this new tx ID for the first time should create a new + db.store_transaction_by_id(&tx).await.unwrap(); + let expected_index = i + 1; + let retrieved_tx = db + .retrieve_transaction_by_index(expected_index as u32) + .await + .unwrap() + .unwrap(); + assert_eq!(retrieved_tx, tx); + let highest_index = db.retrieve_highest_index().await.unwrap(); + assert_eq!(highest_index, expected_index as u32); + + // storing to this new tx ID again should not create a new + // highest index + tx.status = TransactionStatus::Included; + db.store_transaction_by_id(&tx).await.unwrap(); + let retrieved_tx = db + .retrieve_transaction_by_index(expected_index as u32) + .await + .unwrap() + .unwrap(); + assert_eq!(retrieved_tx, tx); + let highest_index = db.retrieve_highest_index().await.unwrap(); + assert_eq!(highest_index, expected_index as u32); + } + } +} diff --git a/rust/main/submitter/src/payload_dispatcher/dispatcher.rs b/rust/main/submitter/src/payload_dispatcher/dispatcher.rs index dce39f3bac8..8d5014e7709 100644 --- a/rust/main/submitter/src/payload_dispatcher/dispatcher.rs +++ b/rust/main/submitter/src/payload_dispatcher/dispatcher.rs @@ -1,12 +1,12 @@ // TODO: re-enable clippy warnings #![allow(dead_code)] -use std::{path::PathBuf, sync::Arc}; +use std::{collections::VecDeque, path::PathBuf, sync::Arc}; use derive_new::new; use eyre::Result; -use tokio::task::JoinHandle; -use tracing::instrument::Instrumented; +use futures_util::future::join_all; +use tokio::{sync::Mutex, task::JoinHandle}; use hyperlane_base::{ db::{HyperlaneRocksDB, DB}, @@ -14,12 +14,19 @@ use hyperlane_base::{ CoreMetrics, }; use hyperlane_core::HyperlaneDomain; +use tracing::{instrument, Instrument}; -use crate::chain_tx_adapter::{AdaptsChain, ChainTxAdapterFactory}; -use crate::payload::PayloadDb; -use crate::transaction::TransactionDb; +use crate::{ + chain_tx_adapter::{AdaptsChain, ChainTxAdapterFactory}, + payload_dispatcher::{ + BuildingStage, BuildingStageQueue, FinalityStage, InclusionStage, PayloadDbLoader, + }, + transaction::Transaction, +}; + +use super::{PayloadDispatcherState, TransactionDbLoader}; -use super::PayloadDispatcherState; +const SUBMITTER_CHANNEL_SIZE: usize = 1_000; /// Settings for `PayloadDispatcher` #[derive(Debug)] @@ -29,27 +36,135 @@ pub struct PayloadDispatcherSettings { /// settings needed for chain-specific adapter pub raw_chain_conf: RawChainConf, pub domain: HyperlaneDomain, - pub db_path: PathBuf, - pub metrics: CoreMetrics, + pub db: DatabaseOrPath, + pub metrics: Arc, +} + +#[derive(Debug)] +pub enum DatabaseOrPath { + Database(DB), + Path(PathBuf), } pub struct PayloadDispatcher { - inner: PayloadDispatcherState, + pub(crate) inner: PayloadDispatcherState, + /// the name of the destination chain + /// used for logging + pub(crate) domain: String, } impl PayloadDispatcher { - pub fn try_from_settings(settings: PayloadDispatcherSettings) -> Result { + pub fn try_from_settings(settings: PayloadDispatcherSettings, domain: String) -> Result { Ok(Self { inner: PayloadDispatcherState::try_from_settings(settings)?, + domain, }) } - pub fn spawn(self) -> Instrumented> { - // TODO: here - // create the submit queue and channels for the Dispatcher stages + // this is an async "constructor" that returns a JoinHandle, so the clippy warning doesn't apply + #[allow(clippy::async_yields_async)] + #[instrument(skip(self), fields(domain = %self.domain))] + pub async fn spawn(self) -> JoinHandle<()> { + let mut tasks = vec![]; + let building_stage_queue: BuildingStageQueue = Arc::new(Mutex::new(VecDeque::new())); + let (inclusion_stage_sender, inclusion_stage_receiver) = + tokio::sync::mpsc::channel::(SUBMITTER_CHANNEL_SIZE); + let (finality_stage_sender, finality_stage_receiver) = + tokio::sync::mpsc::channel::(SUBMITTER_CHANNEL_SIZE); + + let building_stage = BuildingStage::new( + building_stage_queue.clone(), + inclusion_stage_sender.clone(), + self.inner.clone(), + ); + let building_task = tokio::task::Builder::new() + .name("building_stage") + .spawn( + async move { + building_stage.run().await; + } + .instrument(tracing::info_span!("building_stage")), + ) + .expect("spawning tokio task from Builder is infallible"); + tasks.push(building_task); + + let inclusion_stage = InclusionStage::new( + inclusion_stage_receiver, + finality_stage_sender.clone(), + self.inner.clone(), + ); + let inclusion_task = tokio::task::Builder::new() + .name("inclusion_stage") + .spawn( + async move { + inclusion_stage.run().await; + } + .instrument(tracing::info_span!("inclusion_stage")), + ) + .expect("spawning tokio task from Builder is infallible"); + tasks.push(inclusion_task); + + let finality_state = FinalityStage::new( + finality_stage_receiver, + building_stage_queue.clone(), + self.inner.clone(), + ); + let finality_task = tokio::task::Builder::new() + .name("finality_stage") + .spawn( + async move { + finality_state.run().await; + } + .instrument(tracing::info_span!("finality_stage")), + ) + .expect("spawning tokio task from Builder is infallible"); + tasks.push(finality_task); + + let payload_db_loader = + PayloadDbLoader::new(self.inner.payload_db.clone(), building_stage_queue.clone()); + let mut payload_iterator = payload_db_loader.into_iterator().await; + let payload_loader_task = tokio::task::Builder::new() + .name("payload_loader") + .spawn( + async move { + payload_iterator + .load_from_db() + .await + .expect("Payload loader crashed"); + } + .instrument(tracing::info_span!("payload_db_loader")), + ) + .expect("spawning tokio task from Builder is infallible"); + tasks.push(payload_loader_task); + + let transaction_db_loader = TransactionDbLoader::new( + self.inner.tx_db.clone(), + inclusion_stage_sender.clone(), + finality_stage_sender.clone(), + ); + let mut transaction_iterator = transaction_db_loader.into_iterator().await; + let transaction_loader_task = tokio::task::Builder::new() + .name("transaction_loader") + .spawn( + async move { + transaction_iterator + .load_from_db() + .await + .expect("Transaction loader crashed"); + } + .instrument(tracing::info_span!("transaction_db_loader")), + ) + .expect("spawning tokio task from Builder is infallible"); + tasks.push(transaction_loader_task); - // spawn the DbLoader with references to the submit queue and channels - // spawn the 3 stages using the adapter, db, queue and channels - todo!() + tokio::task::Builder::new() + .name("payload_dispatcher") + .spawn( + async move { + join_all(tasks).await; + } + .instrument(tracing::info_span!("payload_dispatcher")), + ) + .expect("spawning tokio task from Builder is infallible") } } diff --git a/rust/main/submitter/src/payload_dispatcher/entrypoint.rs b/rust/main/submitter/src/payload_dispatcher/entrypoint.rs index 96272272367..07320afddc2 100644 --- a/rust/main/submitter/src/payload_dispatcher/entrypoint.rs +++ b/rust/main/submitter/src/payload_dispatcher/entrypoint.rs @@ -2,10 +2,12 @@ #![allow(dead_code)] use async_trait::async_trait; -use eyre::Result; +use eyre::{eyre, Result}; +use tracing::info; use crate::{ chain_tx_adapter::GasLimit, + error::SubmitterError, payload::{FullPayload, PayloadId, PayloadStatus}, }; @@ -13,13 +15,16 @@ use super::{PayloadDispatcherSettings, PayloadDispatcherState}; #[async_trait] pub trait Entrypoint { - async fn send_payload(&self, payloads: FullPayload) -> Result<()>; - async fn payload_status(&self, payload_id: PayloadId) -> Result; - async fn estimate_gas_limit(&self, payload: &FullPayload) -> Result; + async fn send_payload(&self, payloads: &FullPayload) -> Result<(), SubmitterError>; + async fn payload_status(&self, payload_id: PayloadId) -> Result; + async fn estimate_gas_limit( + &self, + payload: &FullPayload, + ) -> Result, SubmitterError>; } pub struct PayloadDispatcherEntrypoint { - inner: PayloadDispatcherState, + pub(crate) inner: PayloadDispatcherState, } impl PayloadDispatcherEntrypoint { @@ -36,27 +41,27 @@ impl PayloadDispatcherEntrypoint { #[async_trait] impl Entrypoint for PayloadDispatcherEntrypoint { - async fn send_payload(&self, payload: FullPayload) -> Result<()> { - self.inner - .payload_db - .store_payload_by_id(payload.clone()) - .await?; + async fn send_payload(&self, payload: &FullPayload) -> Result<(), SubmitterError> { + info!(payload_id=?payload.id(), "Sending payload to dispatcher"); + self.inner.payload_db.store_payload_by_id(payload).await?; Ok(()) } - async fn payload_status(&self, payload_id: PayloadId) -> Result { + async fn payload_status(&self, payload_id: PayloadId) -> Result { let payload = self .inner .payload_db .retrieve_payload_by_id(&payload_id) .await?; - let status = payload + payload .map(|payload| payload.status) - .unwrap_or(PayloadStatus::NotFound); - Ok(status) + .ok_or(SubmitterError::PayloadNotFound) } - async fn estimate_gas_limit(&self, payload: &FullPayload) -> Result { + async fn estimate_gas_limit( + &self, + payload: &FullPayload, + ) -> Result, SubmitterError> { self.inner.adapter.estimate_gas_limit(payload).await } } @@ -75,7 +80,9 @@ mod tests { use super::*; use crate::chain_tx_adapter::*; use crate::payload::*; - use crate::payload_dispatcher::test_utils::tests::MockAdapter; + use crate::payload_dispatcher::test_utils::MockAdapter; + use crate::payload_dispatcher::PayloadDb; + use crate::payload_dispatcher::TransactionDb; use crate::transaction::*; struct MockDb { @@ -97,11 +104,11 @@ mod tests { Ok(self.payloads.lock().unwrap().get(id).cloned()) } - async fn store_payload_by_id(&self, payload: FullPayload) -> DbResult<()> { + async fn store_payload_by_id(&self, payload: &FullPayload) -> DbResult<()> { self.payloads .lock() .unwrap() - .insert(payload.id().clone(), payload); + .insert(payload.id().clone(), payload.clone()); Ok(()) } @@ -119,6 +126,41 @@ mod tests { ) -> DbResult> { todo!() } + + async fn retrieve_payload_index_by_id( + &self, + _payload_id: &PayloadId, + ) -> DbResult> { + todo!() + } + + async fn store_payload_id_by_index( + &self, + _index: u32, + _payload_id: &PayloadId, + ) -> DbResult<()> { + todo!() + } + + async fn retrieve_payload_id_by_index(&self, _index: u32) -> DbResult> { + todo!() + } + + async fn store_highest_index(&self, _index: u32) -> DbResult<()> { + todo!() + } + + async fn retrieve_highest_index(&self) -> DbResult { + todo!() + } + + async fn store_payload_index_by_id( + &self, + _index: u32, + _payload_id: &PayloadId, + ) -> DbResult<()> { + todo!() + } } #[async_trait] @@ -133,13 +175,51 @@ mod tests { async fn store_transaction_by_id(&self, _tx: &Transaction) -> DbResult<()> { unimplemented!() } + + async fn retrieve_transaction_id_by_index( + &self, + _index: u32, + ) -> DbResult> { + todo!() + } + + async fn store_highest_index(&self, _index: u32) -> DbResult<()> { + todo!() + } + + async fn retrieve_highest_index(&self) -> DbResult { + todo!() + } + + async fn store_transaction_id_by_index( + &self, + _index: u32, + _tx_id: &TransactionId, + ) -> DbResult<()> { + todo!() + } + + async fn retrieve_transaction_index_by_id( + &self, + _id: &TransactionId, + ) -> DbResult> { + todo!() + } + + async fn store_transaction_index_by_id( + &self, + _index: u32, + _tx_id: &TransactionId, + ) -> DbResult<()> { + todo!() + } } fn set_up( payload_db: Arc, tx_db: Arc, ) -> Box { - let adapter = Box::new(MockAdapter::new()) as Box; + let adapter = Arc::new(MockAdapter::new()) as Arc; let entrypoint_state = PayloadDispatcherState::new(payload_db, tx_db, adapter); Box::new(PayloadDispatcherEntrypoint::from_inner(entrypoint_state)) } @@ -151,15 +231,15 @@ mod tests { let mut payload = FullPayload::default(); let payload_id = payload.id().clone(); - entrypoint.send_payload(payload.clone()).await?; + entrypoint.send_payload(&payload).await?; let status = entrypoint.payload_status(payload_id.clone()).await?; assert_eq!(status, PayloadStatus::ReadyToSubmit); // update the payload's status - let new_status = PayloadStatus::Finalized; + let new_status = PayloadStatus::InTransaction(TransactionStatus::Finalized); payload.status = new_status.clone(); - db.store_payload_by_id(payload).await.unwrap(); + db.store_payload_by_id(&payload).await.unwrap(); // ensure the db entry was updated let status = entrypoint.payload_status(payload_id.clone()).await?; @@ -205,13 +285,17 @@ mod tests { let mut mock_adapter = MockAdapter::new(); mock_adapter .expect_estimate_gas_limit() - .returning(move |_| Ok(mock_gas_limit)); - let adapter = Box::new(mock_adapter) as Box; + .returning(move |_| Ok(Some(mock_gas_limit))); + let adapter = Arc::new(mock_adapter) as Arc; let entrypoint_state = PayloadDispatcherState::new(payload_db, tx_db, adapter); let entrypoint = Box::new(PayloadDispatcherEntrypoint::from_inner(entrypoint_state)); let payload = FullPayload::default(); - let gas_limit = entrypoint.estimate_gas_limit(&payload).await.unwrap(); + let gas_limit = entrypoint + .estimate_gas_limit(&payload) + .await + .unwrap() + .unwrap(); assert_eq!(gas_limit, mock_gas_limit); } diff --git a/rust/main/submitter/src/payload_dispatcher/stages.rs b/rust/main/submitter/src/payload_dispatcher/stages.rs index 0073e6e7864..8a14919a52d 100644 --- a/rust/main/submitter/src/payload_dispatcher/stages.rs +++ b/rust/main/submitter/src/payload_dispatcher/stages.rs @@ -2,5 +2,9 @@ mod building_stage; mod finality_stage; mod inclusion_stage; mod state; +mod utils; +pub use building_stage::*; +pub use finality_stage::*; +pub use inclusion_stage::*; pub use state::*; diff --git a/rust/main/submitter/src/payload_dispatcher/stages/building_stage.rs b/rust/main/submitter/src/payload_dispatcher/stages/building_stage.rs index 6ecafa04961..c9f7efb3557 100644 --- a/rust/main/submitter/src/payload_dispatcher/stages/building_stage.rs +++ b/rust/main/submitter/src/payload_dispatcher/stages/building_stage.rs @@ -6,27 +6,30 @@ use std::{collections::VecDeque, sync::Arc}; use derive_new::new; use eyre::Result; use tokio::sync::{mpsc, Mutex}; -use tracing::{error, info, warn}; +use tracing::{error, info, instrument, warn}; use crate::{ + chain_tx_adapter::TxBuildingResult, + error::SubmitterError, payload::{DropReason, FullPayload, PayloadDetails, PayloadStatus}, transaction::Transaction, }; -use super::state::PayloadDispatcherState; +use super::{state::PayloadDispatcherState, utils::call_until_success_or_nonretryable_error}; pub type BuildingStageQueue = Arc>>; #[derive(new)] -struct BuildingStage { +pub(crate) struct BuildingStage { /// This queue is the entrypoint and event driver of the Building Stage queue: BuildingStageQueue, /// This channel is the exitpoint of the Building Stage inclusion_stage_sender: mpsc::Sender, - state: PayloadDispatcherState, + pub(crate) state: PayloadDispatcherState, } impl BuildingStage { + #[instrument(skip(self), name = "BuildingStage::run")] pub async fn run(&self) { loop { // event-driven by the Building queue @@ -40,84 +43,241 @@ impl BuildingStage { }; let payloads = vec![payload]; - let txs = match self.state.adapter.build_transactions(&payloads).await { - Err(err) => { - error!( - ?err, - ?payloads, - "Error building transactions. Dropping payloads" - ); - let details_for_payloads: Vec<_> = - payloads.into_iter().map(|p| p.details.clone()).collect(); - self.state - .drop_payloads(&details_for_payloads, DropReason::UnhandledError) - .await; - continue; - } - Ok(txs) => txs, - }; + info!(?payloads, "Building transactions from payloads"); + let tx_building_results = self.state.adapter.build_transactions(&payloads).await; - for tx in txs { - if let Err(err) = self.state.simulate_tx(&tx).await { - error!( - ?err, - payload_details = ?tx.payload_details, - "Error simulating transaction. Dropping transaction" - ); - // TODO: distinguish between network error and failed simulation - self.drop_tx(&tx, DropReason::FailedSimulation).await; - continue; - }; - if let Err(err) = self.send_tx_to_inclusion_stage(tx.clone()).await { - error!( - ?err, - payload_details = ?tx.payload_details, - "Error sending transaction to inclusion stage" - ); - self.drop_tx(&tx, DropReason::UnhandledError).await; - continue; - } else { - self.state.store_tx(&tx).await; + for tx_building_result in tx_building_results { + // push payloads that failed to be processed (but didn't fail simulation) + // to the back of the queue + if let Err(err) = self + .handle_tx_building_result(tx_building_result.clone()) + .await + { + error!(?err, payloads=?tx_building_result.payloads, "Error handling tx building result"); + let full_payloads = + get_full_payloads_from_details(&payloads, &tx_building_result.payloads); + { + let mut queue = self.queue.lock().await; + queue.extend(full_payloads); + } } } } } + #[instrument( + skip(self, tx_building_result), + name = "BuildingStage::handle_tx_building_result", + fields( + payloads = ?tx_building_result.payloads, + tx_ids = ?tx_building_result.maybe_tx.as_ref().map(|tx| tx.id.to_string()), + ) + )] + async fn handle_tx_building_result( + &self, + tx_building_result: TxBuildingResult, + ) -> Result<(), SubmitterError> { + let TxBuildingResult { payloads, maybe_tx } = tx_building_result; + let Some(tx) = maybe_tx else { + warn!( + ?payloads, + "Transaction building failed. Dropping transaction" + ); + self.state + .update_status_for_payloads( + &payloads, + PayloadStatus::Dropped(DropReason::FailedToBuildAsTransaction), + ) + .await; + return Ok(()); + }; + info!(?tx, "Transaction built successfully"); + let simulation_success = call_until_success_or_nonretryable_error( + || self.state.adapter.simulate_tx(&tx), + "Simulating transaction", + ) + .await + .unwrap_or(false); + if !simulation_success { + warn!( + ?tx, + payload_details = ?tx.payload_details, + "Transaction simulation failed. Dropping transaction" + ); + self.drop_tx(&tx, DropReason::FailedSimulation).await; + return Ok(()); + }; + call_until_success_or_nonretryable_error( + || self.send_tx_to_inclusion_stage(tx.clone()), + "Sending transaction to inclusion stage", + ) + .await?; + self.state.store_tx(&tx).await; + Ok(()) + } + async fn drop_tx(&self, tx: &Transaction, reason: DropReason) { + warn!( + ?tx, + payload_details = ?tx.payload_details, + "Transaction dropped from Building Stage" + ); // Transactions are only persisted if they are sent to the Inclusion Stage // so the only thing to update in this stage is the payload status - self.state.drop_payloads(&tx.payload_details, reason).await; + self.state + .update_status_for_payloads(&tx.payload_details, PayloadStatus::Dropped(reason)) + .await; } - async fn send_tx_to_inclusion_stage(&self, tx: Transaction) -> Result<()> { + async fn send_tx_to_inclusion_stage(&self, tx: Transaction) -> Result<(), SubmitterError> { if let Err(err) = self.inclusion_stage_sender.send(tx.clone()).await { - return Err(eyre::eyre!( - "Error sending transaction to Inclusion Stage: {:?}", - err - )); + return Err(SubmitterError::ChannelSendFailure(err)); } info!(?tx, "Transaction sent to Inclusion Stage"); Ok(()) } } +fn get_full_payloads_from_details( + full_payloads: &[FullPayload], + details: &[PayloadDetails], +) -> Vec { + full_payloads + .iter() + .filter(|payload| details.iter().any(|d| d.id == payload.details.id)) + .cloned() + .collect() +} + #[cfg(test)] mod tests { use eyre::Result; use std::{collections::VecDeque, sync::Arc}; use crate::{ - chain_tx_adapter::AdaptsChain, - payload::{self, FullPayload, PayloadDetails}, + chain_tx_adapter::{AdaptsChain, TxBuildingResult}, + payload::{self, DropReason, FullPayload, PayloadDetails, PayloadStatus}, payload_dispatcher::{ - test_utils::tests::{dummy_tx, tmp_dbs, MockAdapter}, - PayloadDispatcherState, + test_utils::{dummy_tx, initialize_payload_db, tmp_dbs, MockAdapter}, + PayloadDb, PayloadDispatcherState, TransactionDb, }, - transaction::Transaction, + transaction::{Transaction, TransactionStatus}, }; use super::{BuildingStage, BuildingStageQueue}; + #[tokio::test] + async fn test_send_payloads_one_by_one() { + const PAYLOADS_TO_SEND: usize = 3; + let succesful_build = true; + let successful_simulation = true; + let (building_stage, mut receiver, queue) = + test_setup(PAYLOADS_TO_SEND, succesful_build, successful_simulation); + + // send a single payload to the building stage and check that it is sent to the receiver + for _ in 0..PAYLOADS_TO_SEND { + let payload_to_send = FullPayload::random(); + initialize_payload_db(&building_stage.state.payload_db, &payload_to_send).await; + queue.lock().await.push_back(payload_to_send.clone()); + let payload_details_received = + run_building_stage(1, &building_stage, &mut receiver).await; + assert_eq!( + payload_details_received, + vec![payload_to_send.details.clone()] + ); + assert_db_status_for_payloads( + &building_stage.state, + &payload_details_received, + PayloadStatus::InTransaction(TransactionStatus::PendingInclusion), + ) + .await; + } + assert_eq!(queue.lock().await.len(), 0); + } + + #[tokio::test] + async fn test_send_multiple_payloads_at_once() { + const PAYLOADS_TO_SEND: usize = 3; + let succesful_build = true; + let successful_simulation = true; + let (building_stage, mut receiver, queue) = + test_setup(PAYLOADS_TO_SEND, succesful_build, successful_simulation); + + let mut sent_payloads = Vec::new(); + for _ in 0..PAYLOADS_TO_SEND { + let payload_to_send = FullPayload::random(); + initialize_payload_db(&building_stage.state.payload_db, &payload_to_send).await; + queue.lock().await.push_back(payload_to_send.clone()); + sent_payloads.push(payload_to_send); + } + + // send multiple payloads to the building stage and check that they are sent to the receiver in the same order + let payload_details_received = + run_building_stage(PAYLOADS_TO_SEND, &building_stage, &mut receiver).await; + let expected_payload_details = sent_payloads + .iter() + .map(|payload| payload.details.clone()) + .collect::>(); + assert_eq!(payload_details_received, expected_payload_details); + assert_db_status_for_payloads( + &building_stage.state, + &payload_details_received, + PayloadStatus::InTransaction(TransactionStatus::PendingInclusion), + ) + .await; + assert_eq!(queue.lock().await.len(), 0); + } + + #[tokio::test] + async fn test_txs_failed_to_build() { + const PAYLOADS_TO_SEND: usize = 3; + let succesful_build = false; + let successful_simulation = true; + let (building_stage, mut receiver, queue) = + test_setup(PAYLOADS_TO_SEND, succesful_build, successful_simulation); + + for _ in 0..PAYLOADS_TO_SEND { + let payload_to_send = FullPayload::random(); + initialize_payload_db(&building_stage.state.payload_db, &payload_to_send).await; + queue.lock().await.push_back(payload_to_send.clone()); + let payload_details_received = + run_building_stage(1, &building_stage, &mut receiver).await; + assert_eq!(payload_details_received, vec![]); + assert_db_status_for_payloads( + &building_stage.state, + &payload_details_received, + PayloadStatus::Dropped(DropReason::FailedToBuildAsTransaction), + ) + .await; + } + assert_eq!(queue.lock().await.len(), 0); + } + + #[tokio::test] + async fn test_txs_failed_simulation() { + const PAYLOADS_TO_SEND: usize = 3; + let succesful_build = true; + let successful_simulation = false; + let (building_stage, mut receiver, queue) = + test_setup(PAYLOADS_TO_SEND, succesful_build, successful_simulation); + + for _ in 0..PAYLOADS_TO_SEND { + let payload_to_send = FullPayload::random(); + initialize_payload_db(&building_stage.state.payload_db, &payload_to_send).await; + queue.lock().await.push_back(payload_to_send.clone()); + let payload_details_received = + run_building_stage(1, &building_stage, &mut receiver).await; + assert_eq!(payload_details_received, vec![]); + assert_db_status_for_payloads( + &building_stage.state, + &payload_details_received, + PayloadStatus::Dropped(DropReason::FailedSimulation), + ) + .await; + } + assert_eq!(queue.lock().await.len(), 0); + } + async fn run_building_stage( sent_payload_count: usize, building_stage: &BuildingStage, @@ -134,22 +294,23 @@ mod tests { received_payloads }; - // give the building stage 1 second to send the transaction(s) to the receiver + // give the building stage 100ms to send the transaction(s) to the receiver tokio::select! { - // this arm runs indefinitely res = building_stage.run() => res, // this arm runs until all sent payloads are sent as txs payloads = received_payloads => { return payloads; }, // this arm is the timeout - _ = tokio::time::sleep(tokio::time::Duration::from_secs(1)) => panic!("Timeout"), + _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {}, }; - panic!("No transaction was sent to the receiver") + vec![] } fn test_setup( payloads_to_send: usize, + succesful_build: bool, + successful_simulation: bool, ) -> ( BuildingStage, tokio::sync::mpsc::Receiver, @@ -160,12 +321,24 @@ mod tests { mock_adapter .expect_build_transactions() .times(payloads_to_send) - .returning(|payloads| Ok(dummy_tx(payloads.to_vec()))); + .returning(move |payloads| dummy_built_tx(payloads.to_vec(), succesful_build.clone())); mock_adapter .expect_simulate_tx() - .times(payloads_to_send) - .returning(|_| Ok(true)); - let adapter = Box::new(mock_adapter) as Box; + // .times(payloads_to_send) + .returning(move |_| Ok(successful_simulation.clone())); + dummy_stage_receiver_queue(mock_adapter, payload_db, tx_db) + } + + fn dummy_stage_receiver_queue( + mock_adapter: MockAdapter, + payload_db: Arc, + tx_db: Arc, + ) -> ( + BuildingStage, + tokio::sync::mpsc::Receiver, + BuildingStageQueue, + ) { + let adapter = Arc::new(mock_adapter) as Arc; let state = PayloadDispatcherState::new(payload_db, tx_db, adapter); let (sender, receiver) = tokio::sync::mpsc::channel(100); let queue = Arc::new(tokio::sync::Mutex::new(VecDeque::new())); @@ -173,43 +346,34 @@ mod tests { (building_stage, receiver, queue) } - #[tokio::test] - async fn test_send_payloads_one_by_one() { - const PAYLOADS_TO_SEND: usize = 3; - let (building_stage, mut receiver, queue) = test_setup(PAYLOADS_TO_SEND); - - // send a single payload to the building stage and check that it is sent to the receiver - for _ in 0..PAYLOADS_TO_SEND { - let payload_to_send = FullPayload::default(); - queue.lock().await.push_back(payload_to_send.clone()); - let payload_details_received = - run_building_stage(1, &building_stage, &mut receiver).await; - assert_eq!( - payload_details_received, - vec![payload_to_send.details.clone()] - ); - } + fn dummy_built_tx(payloads: Vec, success: bool) -> Vec { + let details: Vec = payloads + .clone() + .into_iter() + .map(|payload| payload.details) + .collect(); + let maybe_transaction = if success { + Some(dummy_tx(payloads, TransactionStatus::PendingInclusion)) + } else { + None + }; + let tx_building_result = TxBuildingResult::new(details, maybe_transaction); + vec![tx_building_result] } - #[tokio::test] - async fn test_send_multiple_payloads_at_once() { - const PAYLOADS_TO_SEND: usize = 3; - let (building_stage, mut receiver, queue) = test_setup(PAYLOADS_TO_SEND); - - let mut sent_payloads = Vec::new(); - for _ in 0..PAYLOADS_TO_SEND { - let payload_to_send = FullPayload::default(); - queue.lock().await.push_back(payload_to_send.clone()); - sent_payloads.push(payload_to_send); + async fn assert_db_status_for_payloads( + state: &PayloadDispatcherState, + payloads: &[PayloadDetails], + expected_status: PayloadStatus, + ) { + for payload in payloads { + let payload_from_db = state + .payload_db + .retrieve_payload_by_id(&payload.id) + .await + .unwrap() + .unwrap(); + assert_eq!(payload_from_db.status, expected_status); } - - // send multiple payloads to the building stage and check that they are sent to the receiver in the same order - let payload_details_received = - run_building_stage(PAYLOADS_TO_SEND, &building_stage, &mut receiver).await; - let expected_payload_details = sent_payloads - .iter() - .map(|payload| payload.details.clone()) - .collect::>(); - assert_eq!(payload_details_received, expected_payload_details); } } diff --git a/rust/main/submitter/src/payload_dispatcher/stages/finality_stage.rs b/rust/main/submitter/src/payload_dispatcher/stages/finality_stage.rs index 8b137891791..a575f74221f 100644 --- a/rust/main/submitter/src/payload_dispatcher/stages/finality_stage.rs +++ b/rust/main/submitter/src/payload_dispatcher/stages/finality_stage.rs @@ -1 +1,542 @@ +// TODO: re-enable clippy warnings +#![allow(dead_code)] +use std::{ + collections::{HashMap, VecDeque}, + future::Future, + sync::Arc, + time::Duration, +}; + +use derive_new::new; +use eyre::{eyre, Result}; +use futures_util::future::try_join_all; +use tokio::{ + sync::{mpsc, Mutex}, + time::sleep, +}; +use tracing::{error, info, info_span, instrument, warn, Instrument}; + +use crate::{ + error::SubmitterError, + payload::{DropReason, FullPayload, PayloadStatus}, + payload_dispatcher::stages::utils::update_tx_status, + transaction::{DropReason as TxDropReason, Transaction, TransactionId, TransactionStatus}, +}; + +use super::{ + building_stage::BuildingStageQueue, utils::call_until_success_or_nonretryable_error, + PayloadDispatcherState, +}; + +pub type FinalityStagePool = Arc>>; + +pub struct FinalityStage { + pub(crate) pool: FinalityStagePool, + tx_receiver: mpsc::Receiver, + building_stage_queue: BuildingStageQueue, + state: PayloadDispatcherState, +} + +impl FinalityStage { + pub fn new( + tx_receiver: mpsc::Receiver, + building_stage_queue: BuildingStageQueue, + state: PayloadDispatcherState, + ) -> Self { + Self { + pool: Arc::new(Mutex::new(HashMap::new())), + tx_receiver, + building_stage_queue, + state, + } + } + pub async fn run(self) { + let FinalityStage { + pool, + tx_receiver, + building_stage_queue, + state, + } = self; + let futures = vec![ + tokio::spawn( + Self::receive_txs(tx_receiver, pool.clone()).instrument(info_span!("receive_txs")), + ), + tokio::spawn( + Self::process_txs(pool, building_stage_queue, state) + .instrument(info_span!("process_txs")), + ), + ]; + if let Err(err) = try_join_all(futures).await { + error!( + error=?err, + "Finality stage future panicked" + ); + } + } + + async fn receive_txs( + mut tx_receiver: mpsc::Receiver, + pool: FinalityStagePool, + ) -> Result<(), SubmitterError> { + loop { + if let Some(tx) = tx_receiver.recv().await { + pool.lock().await.insert(tx.id.clone(), tx.clone()); + info!(?tx, "Received transaction"); + } else { + error!("Inclusion stage channel closed"); + return Err(SubmitterError::ChannelClosed); + } + } + } + + async fn process_txs( + pool: FinalityStagePool, + building_stage_queue: BuildingStageQueue, + state: PayloadDispatcherState, + ) -> Result<(), SubmitterError> { + let estimated_block_time = state.adapter.estimated_block_time(); + loop { + // evaluate the pool every block + sleep(*estimated_block_time).await; + + let pool_snapshot = pool.lock().await.clone(); + info!(pool_size=?pool_snapshot.len() , "Processing transactions in finality pool"); + for (_, tx) in pool_snapshot { + if let Err(err) = Self::try_process_tx( + tx.clone(), + pool.clone(), + building_stage_queue.clone(), + &state, + ) + .await + { + error!( + ?err, + ?tx, + "Error processing finality stage transaction. Skipping for now" + ); + } + } + } + } + + #[instrument( + skip(tx, pool, building_stage_queue, state), + name = "FinalityStage::try_process_tx" + fields( + tx_id = ?tx.id, + tx_status = ?tx.status, + payloads = ?tx.payload_details + ))] + async fn try_process_tx( + mut tx: Transaction, + pool: FinalityStagePool, + building_stage_queue: BuildingStageQueue, + state: &PayloadDispatcherState, + ) -> Result<(), SubmitterError> { + info!(?tx, "Processing finality stage transaction"); + let tx_status = call_until_success_or_nonretryable_error( + || state.adapter.tx_status(&tx), + "Querying transaction status", + ) + .await?; + + match tx_status { + TransactionStatus::Included => { + // tx is not finalized yet, keep it in the pool + info!(?tx, "Transaction is not yet finalized"); + let reverted_payloads = call_until_success_or_nonretryable_error( + || state.adapter.reverted_payloads(&tx), + "Checking reverted payloads", + ) + .await?; + state + .update_status_for_payloads( + &reverted_payloads, + PayloadStatus::Dropped(DropReason::Reverted), + ) + .await; + } + TransactionStatus::Finalized => { + // update tx status in db + update_tx_status(state, &mut tx, tx_status).await?; + let tx_id = tx.id.clone(); + info!(?tx_id, "Transaction is finalized"); + pool.lock().await.remove(&tx_id); + } + TransactionStatus::Dropped(drop_reason) => { + Self::handle_dropped_transaction( + tx.clone(), + drop_reason, + building_stage_queue.clone(), + state, + pool, + ) + .await?; + } + TransactionStatus::PendingInclusion | TransactionStatus::Mempool => { + error!(?tx, "Transaction should not be in the finality stage."); + } + } + Ok(()) + } + + async fn handle_dropped_transaction( + mut tx: Transaction, + drop_reason: TxDropReason, + building_stage_queue: BuildingStageQueue, + state: &PayloadDispatcherState, + pool: FinalityStagePool, + ) -> Result<(), SubmitterError> { + warn!(?tx, ?drop_reason, "Transaction was dropped"); + // push payloads in tx back to the building stage queue + update_tx_status( + state, + &mut tx, + TransactionStatus::Dropped(TxDropReason::DroppedByChain), + ) + .await?; + let payloads = tx.payload_details.clone(); + for payload in payloads.iter() { + if let Some(full_payload) = state + .payload_db + .retrieve_payload_by_id(&payload.id) + .await + .ok() + .flatten() + { + // update payload status in db + state + .update_status_for_payloads(&[payload.clone()], PayloadStatus::ReadyToSubmit) + .await; + // cannot remove a record from the db, so + // just link the payload to the null tx id + state + .payload_db + .store_tx_id_by_payload_id(&payload.id, &TransactionId::default()) + .await?; + info!( + ?payload, + "Pushing payload to the front of the building stage queue" + ); + building_stage_queue.lock().await.push_front(full_payload); + } + } + pool.lock().await.remove(&tx.id); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + payload::{PayloadDetails, PayloadId}, + payload_dispatcher::{ + stages::{building_stage, finality_stage}, + test_utils::{ + are_all_txs_in_pool, are_no_txs_in_pool, create_random_txs_and_store_them, + dummy_tx, initialize_payload_db, tmp_dbs, MockAdapter, + }, + PayloadDb, TransactionDb, + }, + transaction::{Transaction, TransactionId}, + }; + use eyre::Result; + use std::sync::Arc; + use tokio::sync::mpsc; + + #[tokio::test] + async fn test_processing_included_txs_no_reverted_payload() { + const TXS_TO_PROCESS: usize = 3; + + let mut mock_adapter = MockAdapter::new(); + mock_adapter + .expect_estimated_block_time() + .return_const(Duration::from_millis(10)); + + mock_adapter + .expect_tx_status() + .returning(|_| Ok(TransactionStatus::Included)); + + mock_adapter + .expect_reverted_payloads() + .returning(|_| Ok(vec![])); + + let (txs_created, txs_removed_from_pool, tx_db, payload_db, pool, _) = + set_up_test_and_run_stage(mock_adapter, TXS_TO_PROCESS, TransactionStatus::Included) + .await; + + assert_eq!(txs_removed_from_pool.len(), 0); + assert!(are_all_txs_in_pool(txs_created.clone(), &pool).await); + assert_tx_status( + txs_removed_from_pool.clone(), + &tx_db, + &payload_db, + TransactionStatus::Included, + ) + .await; + } + + #[tokio::test] + async fn test_processing_included_txs_some_reverted_payload() { + const TXS_TO_PROCESS: usize = 3; + + let mut mock_adapter = MockAdapter::new(); + mock_adapter + .expect_estimated_block_time() + .return_const(Duration::from_millis(10)); + + mock_adapter + .expect_tx_status() + .returning(|_| Ok(TransactionStatus::Included)); + + let (payload_db, tx_db) = tmp_dbs(); + + let generated_txs = create_random_txs_and_store_them( + TXS_TO_PROCESS, + &payload_db, + &tx_db, + TransactionStatus::Included, + ) + .await; + + // these payloads will be reported as reverted + let payloads_in_first_tx = generated_txs[0].payload_details.clone(); + let payloads_in_first_tx_clone = payloads_in_first_tx.clone(); + mock_adapter + .expect_reverted_payloads() + .returning(move |_| Ok(payloads_in_first_tx_clone.clone())); + + let (inclusion_stage_sender, inclusion_stage_receiver) = mpsc::channel(TXS_TO_PROCESS); + + let building_queue = Arc::new(tokio::sync::Mutex::new(VecDeque::new())); + + let state = + PayloadDispatcherState::new(payload_db.clone(), tx_db.clone(), Arc::new(mock_adapter)); + let finality_stage = + FinalityStage::new(inclusion_stage_receiver, building_queue.clone(), state); + let pool = finality_stage.pool.clone(); + + send_txs_to_channel(generated_txs.clone(), inclusion_stage_sender).await; + let txs_received = run_stage(finality_stage).await; + + assert_eq!(txs_received.len(), 0); + assert!(are_all_txs_in_pool(generated_txs.to_vec(), &pool).await); + // non-reverted payload txs are still pending finality + assert_tx_status( + generated_txs[1..].to_vec(), + &tx_db, + &payload_db, + TransactionStatus::Included, + ) + .await; + // reverted payloads are dropped + assert_payloads_status( + payloads_in_first_tx.clone(), + &payload_db, + PayloadStatus::Dropped(DropReason::Reverted), + ) + .await; + } + + #[tokio::test] + async fn test_processing_finalized_txs() { + const TXS_TO_PROCESS: usize = 3; + + let mut mock_adapter = MockAdapter::new(); + mock_adapter + .expect_estimated_block_time() + .return_const(Duration::from_millis(10)); + + mock_adapter + .expect_tx_status() + .returning(|_| Ok(TransactionStatus::Finalized)); + + let (txs_created, txs_received, tx_db, payload_db, pool, _) = + set_up_test_and_run_stage(mock_adapter, TXS_TO_PROCESS, TransactionStatus::Finalized) + .await; + + assert!(are_no_txs_in_pool(txs_created.clone(), &pool).await); + assert_tx_status( + txs_received.clone(), + &tx_db, + &payload_db, + TransactionStatus::Finalized, + ) + .await; + } + + #[tokio::test] + async fn test_processing_reorged_txs() { + const TXS_TO_PROCESS: usize = 3; + + let mut mock_adapter = MockAdapter::new(); + mock_adapter + .expect_estimated_block_time() + .return_const(Duration::from_millis(10)); + + // report all txs as reorged + mock_adapter + .expect_tx_status() + .returning(|_| Ok(TransactionStatus::Dropped(TxDropReason::DroppedByChain))); + + let (txs_created, txs_received, tx_db, payload_db, pool, queue) = + set_up_test_and_run_stage( + mock_adapter, + TXS_TO_PROCESS, + TransactionStatus::Dropped(TxDropReason::DroppedByChain), + ) + .await; + + // the finality pool becomes empty + assert!(are_no_txs_in_pool(txs_created.clone(), &pool).await); + assert_tx_status( + txs_received.clone(), + &tx_db, + &payload_db, + TransactionStatus::Dropped(TxDropReason::DroppedByChain), + ) + .await; + + // all payloads are in the building stage queue + assert_eq!(queue.lock().await.len(), TXS_TO_PROCESS); + for tx in txs_received { + assert_payloads_status( + tx.payload_details.clone(), + &payload_db, + PayloadStatus::ReadyToSubmit, + ) + .await; + } + } + + async fn set_up_test_and_run_stage( + mock_adapter: MockAdapter, + txs_to_process: usize, + tx_status: TransactionStatus, + ) -> ( + Vec, + Vec, + Arc, + Arc, + FinalityStagePool, + BuildingStageQueue, + ) { + let (txs_created, tx_db, payload_db, pool, finality_stage, building_queue) = + set_up_test(mock_adapter, txs_to_process, tx_status).await; + let txs_received = run_stage(finality_stage).await; + ( + txs_created, + txs_received, + tx_db, + payload_db, + pool, + building_queue, + ) + } + + async fn set_up_test( + mock_adapter: MockAdapter, + txs_to_process: usize, + tx_status: TransactionStatus, + ) -> ( + Vec, + Arc, + Arc, + FinalityStagePool, + FinalityStage, + BuildingStageQueue, + ) { + let (payload_db, tx_db) = tmp_dbs(); + let (inclusion_stage_sender, inclusion_stage_receiver) = mpsc::channel(txs_to_process); + + let building_queue = Arc::new(tokio::sync::Mutex::new(VecDeque::new())); + + let state = + PayloadDispatcherState::new(payload_db.clone(), tx_db.clone(), Arc::new(mock_adapter)); + let finality_stage = + FinalityStage::new(inclusion_stage_receiver, building_queue.clone(), state); + let pool = finality_stage.pool.clone(); + + let test_txs = + create_random_txs_and_store_them(txs_to_process, &payload_db, &tx_db, tx_status).await; + send_txs_to_channel(test_txs.clone(), inclusion_stage_sender).await; + ( + test_txs, + tx_db, + payload_db, + pool, + finality_stage, + building_queue, + ) + } + + async fn send_txs_to_channel( + txs: Vec, + inclusion_stage_sender: mpsc::Sender, + ) { + for tx in txs { + inclusion_stage_sender.send(tx).await.unwrap(); + } + } + + async fn run_stage(stage: FinalityStage) -> Vec { + let pool = stage.pool.clone(); + let pool_before = pool.lock().await.clone(); + let stage_task = tokio::spawn(async move { stage.run().await }); + // give the building stage 100ms to send the transaction(s) to the receiver + let _ = tokio::select! { + // this arm runs indefinitely + res = stage_task => res.unwrap(), + // this arm is the timeout + _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => { + }, + }; + let pool_after = pool.lock().await.clone(); + pool_before + .iter() + .filter(|(id, _)| !pool_after.contains_key(id)) + .map(|(_, tx)| tx.clone()) + .collect::>() + } + + async fn assert_tx_status( + txs: Vec, + tx_db: &Arc, + payload_db: &Arc, + expected_status: TransactionStatus, + ) { + // check that the payload and tx dbs were updated + for tx in txs { + let tx_from_db = tx_db + .retrieve_transaction_by_id(&tx.id) + .await + .unwrap() + .unwrap(); + assert_eq!(tx_from_db.status, expected_status.clone()); + assert_payloads_status( + tx_from_db.payload_details, + payload_db, + PayloadStatus::InTransaction(expected_status.clone()), + ) + .await; + } + } + + async fn assert_payloads_status( + payloads: Vec, + payload_db: &Arc, + expected_status: PayloadStatus, + ) { + for payload in payloads { + let payload = payload_db + .retrieve_payload_by_id(&payload.id) + .await + .unwrap() + .unwrap(); + assert_eq!(payload.status, expected_status.clone()); + } + } +} diff --git a/rust/main/submitter/src/payload_dispatcher/stages/inclusion_stage.rs b/rust/main/submitter/src/payload_dispatcher/stages/inclusion_stage.rs index 8b137891791..9e9c940ce63 100644 --- a/rust/main/submitter/src/payload_dispatcher/stages/inclusion_stage.rs +++ b/rust/main/submitter/src/payload_dispatcher/stages/inclusion_stage.rs @@ -1 +1,429 @@ +// TODO: re-enable clippy warnings +#![allow(dead_code)] +use std::{ + collections::{HashMap, VecDeque}, + future::Future, + sync::Arc, + time::Duration, +}; + +use derive_new::new; +use eyre::{eyre, Result}; +use futures_util::future::try_join_all; +use tokio::{ + sync::{mpsc, Mutex}, + time::sleep, +}; +use tracing::{error, info, info_span, instrument, warn, Instrument}; + +use crate::{ + error::SubmitterError, + payload::{FullPayload, PayloadStatus}, + payload_dispatcher::stages::utils::update_tx_status, + transaction::{DropReason as TxDropReason, Transaction, TransactionId, TransactionStatus}, +}; + +use super::{utils::call_until_success_or_nonretryable_error, PayloadDispatcherState}; + +pub type InclusionStagePool = Arc>>; + +pub struct InclusionStage { + pub(crate) pool: InclusionStagePool, + tx_receiver: mpsc::Receiver, + finality_stage_sender: mpsc::Sender, + state: PayloadDispatcherState, +} + +impl InclusionStage { + pub fn new( + tx_receiver: mpsc::Receiver, + finality_stage_sender: mpsc::Sender, + state: PayloadDispatcherState, + ) -> Self { + Self { + pool: Arc::new(Mutex::new(HashMap::new())), + tx_receiver, + finality_stage_sender, + state, + } + } + + pub async fn run(self) { + let InclusionStage { + pool, + tx_receiver, + finality_stage_sender, + state, + } = self; + let futures = vec![ + tokio::spawn( + Self::receive_txs(tx_receiver, pool.clone()).instrument(info_span!("receive_txs")), + ), + tokio::spawn( + Self::process_txs(pool, finality_stage_sender, state) + .instrument(info_span!("process_txs")), + ), + ]; + if let Err(err) = try_join_all(futures).await { + error!( + error=?err, + "Inclusion stage future panicked" + ); + } + } + + async fn receive_txs( + mut building_stage_receiver: mpsc::Receiver, + pool: InclusionStagePool, + ) -> Result<(), SubmitterError> { + loop { + if let Some(tx) = building_stage_receiver.recv().await { + pool.lock().await.insert(tx.id.clone(), tx.clone()); + info!(?tx, "Received transaction"); + } else { + error!("Building stage channel closed"); + return Err(SubmitterError::ChannelClosed); + } + } + } + + async fn process_txs( + pool: InclusionStagePool, + finality_stage_sender: mpsc::Sender, + state: PayloadDispatcherState, + ) -> Result<(), SubmitterError> { + let estimated_block_time = state.adapter.estimated_block_time(); + loop { + // evaluate the pool every block + sleep(*estimated_block_time).await; + + let pool_snapshot = pool.lock().await.clone(); + info!(pool_size=?pool_snapshot.len() , "Processing transactions in inclusion pool"); + for (_, tx) in pool_snapshot { + if let Err(err) = + Self::try_process_tx(tx.clone(), &finality_stage_sender, &state, &pool).await + { + error!(?err, ?tx, "Error processing transaction. Skipping for now"); + } + } + } + } + + #[instrument( + skip_all, + name = "InclusionStage::try_process_tx", + fields(tx_id = ?tx.id, tx_status = ?tx.status, payloads = ?tx.payload_details) + )] + async fn try_process_tx( + mut tx: Transaction, + finality_stage_sender: &mpsc::Sender, + state: &PayloadDispatcherState, + pool: &InclusionStagePool, + ) -> Result<()> { + info!(?tx, "Processing inclusion stage transaction"); + let tx_status = call_until_success_or_nonretryable_error( + || state.adapter.tx_status(&tx), + "Querying transaction status", + ) + .await?; + info!(?tx, ?tx_status, "Transaction status"); + + match tx_status { + TransactionStatus::PendingInclusion | TransactionStatus::Mempool => { + info!(tx_id = ?tx.id, ?tx_status, "Transaction is pending inclusion"); + return Self::process_pending_tx(tx, state, pool).await; + } + TransactionStatus::Included | TransactionStatus::Finalized => { + update_tx_status(state, &mut tx, tx_status.clone()).await?; + let tx_id = tx.id.clone(); + finality_stage_sender.send(tx).await?; + info!(?tx_id, ?tx_status, "Transaction included in block"); + pool.lock().await.remove(&tx_id); + return Ok(()); + } + TransactionStatus::Dropped(_) => { + error!( + ?tx, + ?tx_status, + "Transaction has invalid status for inclusion stage" + ); + } + } + + Ok(()) + } + + #[instrument(skip_all, name = "InclusionStage::process_pending_tx")] + async fn process_pending_tx( + mut tx: Transaction, + state: &PayloadDispatcherState, + pool: &InclusionStagePool, + ) -> Result<()> { + info!(?tx, "Processing pending transaction"); + let simulation_success = call_until_success_or_nonretryable_error( + || state.adapter.simulate_tx(&tx), + "Simulating transaction", + ) + .await?; + if !simulation_success { + warn!(?tx, "Transaction simulation failed"); + Self::drop_tx(state, &mut tx, TxDropReason::FailedSimulation, pool).await?; + return Err(eyre!("Transaction simulation failed")); + } + info!(?tx, "Transaction simulation succeeded"); + + // successively calling `submit` will result in escalating gas price until the tx is accepted + // by the node. + // at this point, not all VMs return information about whether the tx was reverted. + // so dropping reverted payloads has to happen in the finality step + tx = call_until_success_or_nonretryable_error( + || { + let tx_clone = tx.clone(); + async move { + let mut tx_clone_inner = tx_clone.clone(); + state.adapter.submit(&mut tx_clone_inner).await?; + Ok(tx_clone_inner) + } + }, + "Submitting transaction", + ) + .await?; + info!(?tx, "Transaction submitted to node"); + + // update tx submission attempts + tx.submission_attempts += 1; + // update tx status in db + update_tx_status(state, &mut tx, TransactionStatus::Mempool).await?; + + // update the pool entry of this tx, to reflect any changes such as the gas price, hash, etc + pool.lock().await.insert(tx.id.clone(), tx.clone()); + Ok(()) + } + + async fn drop_tx( + state: &PayloadDispatcherState, + tx: &mut Transaction, + reason: TxDropReason, + pool: &InclusionStagePool, + ) -> Result<()> { + warn!(?tx, "Dropping tx"); + let new_tx_status = TransactionStatus::Dropped(reason); + update_tx_status(state, tx, new_tx_status.clone()).await?; + // drop the payloads as well + state + .update_status_for_payloads( + &tx.payload_details, + PayloadStatus::InTransaction(new_tx_status), + ) + .await; + pool.lock().await.remove(&tx.id); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + payload_dispatcher::{ + test_utils::{ + are_all_txs_in_pool, are_no_txs_in_pool, create_random_txs_and_store_them, + dummy_tx, initialize_payload_db, tmp_dbs, MockAdapter, + }, + PayloadDb, TransactionDb, + }, + transaction::{Transaction, TransactionId}, + }; + use eyre::Result; + use std::sync::Arc; + use tokio::sync::mpsc; + + #[tokio::test] + async fn test_processing_included_txs() { + const TXS_TO_PROCESS: usize = 3; + + let mut mock_adapter = MockAdapter::new(); + mock_adapter + .expect_estimated_block_time() + .return_const(Duration::from_millis(10)); + + mock_adapter + .expect_tx_status() + .returning(|_| Ok(TransactionStatus::Included)); + + let (txs_created, txs_received, tx_db, payload_db, pool) = + set_up_test_and_run_stage(mock_adapter, TXS_TO_PROCESS).await; + + assert_eq!(txs_received.len(), TXS_TO_PROCESS); + assert!(are_no_txs_in_pool(txs_created.clone(), &pool).await); + assert_tx_status( + txs_received.clone(), + &tx_db, + &payload_db, + TransactionStatus::Included, + ) + .await; + } + + #[tokio::test] + async fn test_unincluded_txs_reach_mempool() { + const TXS_TO_PROCESS: usize = 3; + + let mut mock_adapter = MockAdapter::new(); + mock_adapter + .expect_estimated_block_time() + .return_const(Duration::from_millis(10)); + + mock_adapter + .expect_tx_status() + .returning(|_| Ok(TransactionStatus::PendingInclusion)); + + mock_adapter.expect_simulate_tx().returning(|_| Ok(true)); + + mock_adapter.expect_submit().returning(|_| Ok(())); + + let (txs_created, txs_received, tx_db, payload_db, pool) = + set_up_test_and_run_stage(mock_adapter, TXS_TO_PROCESS).await; + + assert_eq!(txs_received.len(), 0); + assert!(are_all_txs_in_pool(txs_created.clone(), &pool).await); + assert_tx_status( + txs_received.clone(), + &tx_db, + &payload_db, + TransactionStatus::Mempool, + ) + .await; + } + + #[tokio::test] + async fn test_failed_simulation() { + const TXS_TO_PROCESS: usize = 3; + + let mut mock_adapter = MockAdapter::new(); + mock_adapter + .expect_estimated_block_time() + .return_const(Duration::from_millis(10)); + + mock_adapter + .expect_tx_status() + .returning(|_| Ok(TransactionStatus::PendingInclusion)); + + mock_adapter.expect_simulate_tx().returning(|_| Ok(false)); + + let (txs_created, txs_received, tx_db, payload_db, pool) = + set_up_test_and_run_stage(mock_adapter, TXS_TO_PROCESS).await; + + assert_eq!(txs_received.len(), 0); + assert!(are_no_txs_in_pool(txs_created.clone(), &pool).await); + assert_tx_status( + txs_received.clone(), + &tx_db, + &payload_db, + TransactionStatus::Dropped(TxDropReason::FailedSimulation), + ) + .await; + } + + async fn set_up_test_and_run_stage( + mock_adapter: MockAdapter, + txs_to_process: usize, + ) -> ( + Vec, + Vec, + Arc, + Arc, + InclusionStagePool, + ) { + let (payload_db, tx_db) = tmp_dbs(); + let (building_stage_sender, building_stage_receiver) = mpsc::channel(txs_to_process); + let (finality_stage_sender, mut finality_stage_receiver) = mpsc::channel(txs_to_process); + + let state = + PayloadDispatcherState::new(payload_db.clone(), tx_db.clone(), Arc::new(mock_adapter)); + let inclusion_stage = + InclusionStage::new(building_stage_receiver, finality_stage_sender, state); + let pool = inclusion_stage.pool.clone(); + + let txs_created = create_random_txs_and_store_them( + txs_to_process, + &payload_db, + &tx_db, + TransactionStatus::PendingInclusion, + ) + .await; + for tx in txs_created.iter() { + building_stage_sender.send(tx.clone()).await.unwrap(); + } + let txs_received = run_stage( + txs_to_process, + inclusion_stage, + &mut finality_stage_receiver, + ) + .await; + (txs_created, txs_received, tx_db, payload_db, pool) + } + + async fn run_stage( + sent_txs_count: usize, + stage: InclusionStage, + receiver: &mut tokio::sync::mpsc::Receiver, + ) -> Vec { + // future that receives `sent_payload_count` payloads from the building stage + let receiving_closure = async { + let mut received = Vec::new(); + while received.len() < sent_txs_count { + let tx_received = receiver.recv().await.unwrap(); + received.push(tx_received); + } + received + }; + + let stage = tokio::spawn(async move { stage.run().await }); + + // give the building stage 100ms to send the transaction(s) to the receiver + let _ = tokio::select! { + // this arm runs indefinitely + res = stage => res, + // this arm runs until all sent payloads are sent as txs + received = receiving_closure => { + return received; + }, + // this arm is the timeout + _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => { + return vec![] + }, + }; + vec![] + } + + async fn assert_tx_status( + txs: Vec, + tx_db: &Arc, + payload_db: &Arc, + expected_status: TransactionStatus, + ) { + // check that the payload and tx dbs were updated + for tx in txs { + let tx_from_db = tx_db + .retrieve_transaction_by_id(&tx.id) + .await + .unwrap() + .unwrap(); + assert_eq!(tx_from_db.status, expected_status.clone()); + + for detail in tx.payload_details.iter() { + let payload = payload_db + .retrieve_payload_by_id(&detail.id) + .await + .unwrap() + .unwrap(); + assert_eq!( + payload.status, + PayloadStatus::InTransaction(expected_status.clone()) + ); + } + } + } +} diff --git a/rust/main/submitter/src/payload_dispatcher/stages/state.rs b/rust/main/submitter/src/payload_dispatcher/stages/state.rs index 1cdbf82d0b9..14c7a25ab40 100644 --- a/rust/main/submitter/src/payload_dispatcher/stages/state.rs +++ b/rust/main/submitter/src/payload_dispatcher/stages/state.rs @@ -15,23 +15,24 @@ use tracing::{error, info, instrument::Instrumented, warn}; use crate::{ chain_tx_adapter::{AdaptsChain, ChainTxAdapterFactory}, - payload::{DropReason, PayloadDb, PayloadDetails, PayloadStatus}, - payload_dispatcher::PayloadDispatcherSettings, - transaction::{Transaction, TransactionDb}, + payload::{DropReason, PayloadDetails, PayloadStatus}, + payload_dispatcher::{DatabaseOrPath, PayloadDb, PayloadDispatcherSettings, TransactionDb}, + transaction::Transaction, }; /// State that is common (but not shared) to all components of the `PayloadDispatcher` +#[derive(Clone)] pub struct PayloadDispatcherState { pub(crate) payload_db: Arc, pub(crate) tx_db: Arc, - pub(crate) adapter: Box, + pub(crate) adapter: Arc, } impl PayloadDispatcherState { pub fn new( payload_db: Arc, tx_db: Arc, - adapter: Box, + adapter: Arc, ) -> Self { Self { payload_db, @@ -46,27 +47,35 @@ impl PayloadDispatcherState { &settings.raw_chain_conf, &settings.metrics, )?; - let db = DB::from_path(&settings.db_path)?; + let db = match settings.db { + DatabaseOrPath::Database(db) => db, + DatabaseOrPath::Path(path) => DB::from_path(&path)?, + }; let rocksdb = Arc::new(HyperlaneRocksDB::new(&settings.domain, db)); let payload_db = rocksdb.clone() as Arc; let tx_db = rocksdb as Arc; Ok(Self::new(payload_db, tx_db, adapter)) } - pub(crate) async fn drop_payloads(&self, details: &[PayloadDetails], reason: DropReason) { + pub(crate) async fn update_status_for_payloads( + &self, + details: &[PayloadDetails], + status: PayloadStatus, + ) { for d in details { if let Err(err) = self .payload_db - .store_new_payload_status(&d.id, PayloadStatus::Dropped(reason.clone())) + .store_new_payload_status(&d.id, status.clone()) .await { error!( ?err, payload_details = ?details, - "Error updating payload status to `dropped`" + new_status = ?status, + "Error updating payload status in the database" ); } - warn!(?details, "Payload dropped from Building Stage"); + info!(?details, new_status=?status, "Updated payload status"); } } @@ -78,19 +87,12 @@ impl PayloadDispatcherState { "Error storing transaction in the database" ); } + self.update_status_for_payloads( + &tx.payload_details, + PayloadStatus::InTransaction(tx.status.clone()), + ) + .await; for payload_detail in &tx.payload_details { - if let Err(err) = self - .payload_db - .store_new_payload_status(&payload_detail.id, PayloadStatus::PendingInclusion) - .await - { - error!( - ?err, - payload_details = ?tx.payload_details, - "Error updating payload status to `sent`" - ); - } - if let Err(err) = self .payload_db .store_tx_id_by_payload_id(&payload_detail.id, &tx.id) @@ -99,20 +101,9 @@ impl PayloadDispatcherState { error!( ?err, payload_details = ?tx.payload_details, - "Error storing transaction id in the database" + "Error storing to the payload_id to tx_id mapping in the database" ); } } } - - pub(crate) async fn simulate_tx(&self, tx: &Transaction) -> Result<()> { - match self.adapter.simulate_tx(tx).await { - Ok(true) => { - info!(?tx, "Transaction simulation succeeded"); - Ok(()) - } - Ok(false) => Err(eyre::eyre!("Transaction simulation failed")), - Err(err) => Err(eyre::eyre!("Error simulating transaction: {:?}", err)), - } - } } diff --git a/rust/main/submitter/src/payload_dispatcher/stages/utils.rs b/rust/main/submitter/src/payload_dispatcher/stages/utils.rs new file mode 100644 index 00000000000..706b6a62076 --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/stages/utils.rs @@ -0,0 +1,45 @@ +use std::{future::Future, time::Duration}; + +use tokio::time::sleep; +use tracing::{error, info}; + +use crate::{ + error::{IsRetryable, SubmitterError}, + transaction::{Transaction, TransactionStatus}, +}; + +use super::PayloadDispatcherState; + +pub async fn call_until_success_or_nonretryable_error( + f: F, + action: &str, +) -> Result +where + F: Fn() -> Fut, + Fut: Future>, +{ + loop { + match f().await { + Ok(result) => return Ok(result), + Err(err) => { + if err.is_retryable() { + error!(?err, ?action, "Error making call. Retrying..."); + sleep(Duration::from_secs(1)).await; + } else { + return Err(SubmitterError::NonRetryableError(err.to_string())); + } + } + } + } +} + +pub async fn update_tx_status( + state: &PayloadDispatcherState, + tx: &mut Transaction, + new_status: TransactionStatus, +) -> Result<(), SubmitterError> { + info!(?tx, ?new_status, "Updating tx status"); + tx.status = new_status; + state.store_tx(tx).await; + Ok(()) +} diff --git a/rust/main/submitter/src/payload_dispatcher/test_utils.rs b/rust/main/submitter/src/payload_dispatcher/test_utils.rs index 8426315a7b9..4da718d0cdd 100644 --- a/rust/main/submitter/src/payload_dispatcher/test_utils.rs +++ b/rust/main/submitter/src/payload_dispatcher/test_utils.rs @@ -1,63 +1,99 @@ -#[cfg(test)] -pub(crate) mod tests { - use std::sync::Arc; - use std::sync::Mutex; - - use async_trait::async_trait; - use eyre::Result; - use hyperlane_base::db::{DbResult, HyperlaneRocksDB, DB}; - use hyperlane_core::identifiers::UniqueIdentifier; - use hyperlane_core::KnownHyperlaneDomain; - use uuid::Uuid; - - use super::*; - use crate::chain_tx_adapter::*; - use crate::payload::*; - use crate::transaction::*; - - mockall::mock! { - pub Adapter { - } - - #[async_trait] - impl AdaptsChain for Adapter { - async fn estimate_gas_limit(&self, payload: &FullPayload) -> Result; - async fn build_transactions(&self, payloads: &[FullPayload]) -> Result>; - async fn simulate_tx(&self, tx: &Transaction) -> Result; - async fn submit(&self, tx: &mut Transaction) -> Result<()>; - async fn tx_status(&self, tx: &Transaction) -> Result; - async fn reverted_payloads(&self, tx: &Transaction) -> Result>; - async fn nonce_gap_exists(&self) -> bool; - async fn replace_tx(&self, _tx: &Transaction) -> Result<()>; - fn estimated_block_time(&self) -> std::time::Duration; - fn max_batch_size(&self) -> usize; - } +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use eyre::Result; +use hyperlane_base::db::{DbResult, HyperlaneRocksDB, DB}; +use hyperlane_core::identifiers::UniqueIdentifier; +use hyperlane_core::KnownHyperlaneDomain; +use tokio::sync::Mutex; +use uuid::Uuid; + +use super::*; +use crate::chain_tx_adapter::*; +use crate::error::SubmitterError; +use crate::payload::*; +use crate::transaction::*; + +mockall::mock! { + pub Adapter { + } + + #[async_trait] + impl AdaptsChain for Adapter { + async fn estimate_gas_limit(&self, payload: &FullPayload) -> Result, SubmitterError>; + async fn build_transactions(&self, payloads: &[FullPayload]) -> Vec; + async fn simulate_tx(&self, tx: &Transaction) -> Result; + async fn submit(&self, tx: &mut Transaction) -> Result<(), SubmitterError>; + async fn tx_status(&self, tx: &Transaction) -> Result; + async fn reverted_payloads(&self, tx: &Transaction) -> Result, SubmitterError>; + async fn nonce_gap_exists(&self) -> bool; + async fn replace_tx(&self, _tx: &Transaction) -> Result<(), SubmitterError>; + fn estimated_block_time(&self) -> &std::time::Duration; + fn max_batch_size(&self) -> u32; } +} - pub(crate) fn tmp_dbs() -> (Arc, Arc) { - let temp_dir = tempfile::tempdir().unwrap(); - let db = DB::from_path(temp_dir.path()).unwrap(); - let domain = KnownHyperlaneDomain::Arbitrum.into(); - let rocksdb = Arc::new(HyperlaneRocksDB::new(&domain, db)); +pub(crate) fn tmp_dbs() -> (Arc, Arc) { + let temp_dir = tempfile::tempdir().unwrap(); + let db = DB::from_path(temp_dir.path()).unwrap(); + let domain = KnownHyperlaneDomain::Arbitrum.into(); + let rocksdb = Arc::new(HyperlaneRocksDB::new(&domain, db)); + + let payload_db = rocksdb.clone() as Arc; + let tx_db = rocksdb.clone() as Arc; + (payload_db, tx_db) +} - let payload_db = rocksdb.clone() as Arc; - let tx_db = rocksdb.clone() as Arc; - (payload_db, tx_db) +pub(crate) fn dummy_tx(payloads: Vec, status: TransactionStatus) -> Transaction { + let details: Vec = payloads + .into_iter() + .map(|payload| payload.details) + .collect(); + Transaction { + id: UniqueIdentifier::random(), + hash: None, + vm_specific_data: VmSpecificTxData::Evm, + payload_details: details.clone(), + status, + submission_attempts: 0, } +} - pub(crate) fn dummy_tx(payloads: Vec) -> Vec { - let details = payloads - .into_iter() - .map(|payload| payload.details) - .collect(); - let transaction = Transaction { - id: UniqueIdentifier::random(), - hash: None, - vm_specific_data: VmSpecificTxData::Evm, - payload_details: details, - status: Default::default(), - submission_attempts: 0, - }; - vec![transaction] +pub(crate) async fn create_random_txs_and_store_them( + num: usize, + payload_db: &Arc, + tx_db: &Arc, + status: TransactionStatus, +) -> Vec { + let mut txs = Vec::new(); + for _ in 0..num { + let mut payload = FullPayload::random(); + payload.status = PayloadStatus::InTransaction(status.clone()); + payload_db.store_payload_by_id(&payload).await.unwrap(); + let tx = dummy_tx(vec![payload], status.clone()); + tx_db.store_transaction_by_id(&tx).await.unwrap(); + txs.push(tx); } + txs +} + +pub(crate) async fn initialize_payload_db(payload_db: &Arc, payload: &FullPayload) { + payload_db.store_payload_by_id(payload).await.unwrap(); +} + +pub async fn are_all_txs_in_pool( + txs: Vec, + pool: &Arc>>, +) -> bool { + let pool = pool.lock().await; + txs.iter().all(|tx| pool.contains_key(&tx.id)) +} + +pub async fn are_no_txs_in_pool( + txs: Vec, + pool: &Arc>>, +) -> bool { + let pool = pool.lock().await; + txs.iter().all(|tx| !pool.contains_key(&tx.id)) } diff --git a/rust/main/submitter/src/payload_dispatcher/tests.rs b/rust/main/submitter/src/payload_dispatcher/tests.rs new file mode 100644 index 00000000000..25d926279da --- /dev/null +++ b/rust/main/submitter/src/payload_dispatcher/tests.rs @@ -0,0 +1,128 @@ +use std::{collections::VecDeque, sync::Arc, time::Duration}; + +use tokio::{sync::Mutex, time::sleep}; + +use crate::{ + chain_tx_adapter::TxBuildingResult, + payload_dispatcher::{ + test_utils::{dummy_tx, tmp_dbs, MockAdapter}, + BuildingStageQueue, PayloadDbLoader, PayloadDispatcherState, + }, + Entrypoint, FullPayload, PayloadDispatcher, PayloadDispatcherEntrypoint, PayloadStatus, + TransactionStatus, +}; + +#[tokio::test] +async fn test_entrypoint_send_is_detected_by_loader() { + let (payload_db, tx_db) = tmp_dbs(); + let building_stage_queue = Arc::new(Mutex::new(VecDeque::new())); + let payload_db_loader = PayloadDbLoader::new(payload_db.clone(), building_stage_queue.clone()); + let mut payload_iterator = payload_db_loader.into_iterator().await; + + let adapter = Arc::new(MockAdapter::new()); + let state = PayloadDispatcherState::new(payload_db, tx_db, adapter); + let dispatcher_entrypoint = PayloadDispatcherEntrypoint { + inner: state.clone(), + }; + + let _payload_db_loader = tokio::spawn(async move { + payload_iterator + .load_from_db() + .await + .expect("Payload loader crashed"); + }); + + // Simulate writing to the database + let payload = FullPayload::random(); + dispatcher_entrypoint.send_payload(&payload).await.unwrap(); + + // Check if the loader detects the new payload + sleep(Duration::from_millis(100)).await; // Wait for the loader to process the payload + let detected_payload_count = { + let queue = building_stage_queue.lock().await; + queue.len() + }; + assert_eq!( + detected_payload_count, 1, + "Loader did not detect the new payload" + ); +} + +#[tokio::test] +async fn test_entrypoint_send_is_finalized_by_dispatcher() { + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .try_init(); + + let (payload_db, tx_db) = tmp_dbs(); + let building_stage_queue = Arc::new(Mutex::new(VecDeque::new())); + let payload_db_loader = PayloadDbLoader::new(payload_db.clone(), building_stage_queue.clone()); + let mut payload_iterator = payload_db_loader.into_iterator().await; + let payload = FullPayload::random(); + + let mut adapter = MockAdapter::new(); + adapter + .expect_estimated_block_time() + .return_const(Duration::from_millis(100)); + + let tx = dummy_tx(vec![payload.clone()], TransactionStatus::PendingInclusion); + let tx_building_result = TxBuildingResult::new(vec![payload.details.clone()], Some(tx)); + let txs = vec![tx_building_result]; + adapter + .expect_build_transactions() + .returning(move |_| txs.clone()); + adapter.expect_simulate_tx().returning(move |_| Ok(true)); + let mut counter = 0; + adapter.expect_tx_status().returning(move |_| { + counter += 1; + match counter { + 1 => Ok(TransactionStatus::PendingInclusion), + 2 => Ok(TransactionStatus::PendingInclusion), + 3 => Ok(TransactionStatus::Included), + 4 => Ok(TransactionStatus::Included), + 5 => Ok(TransactionStatus::Included), + _ => Ok(TransactionStatus::Finalized), + } + }); + adapter.expect_reverted_payloads().returning(|_| Ok(vec![])); + + adapter.expect_simulate_tx().returning(|_| Ok(true)); + + adapter.expect_submit().returning(|_| Ok(())); + + let adapter = Arc::new(adapter); + + let state = PayloadDispatcherState::new(payload_db, tx_db, adapter); + let dispatcher_entrypoint = PayloadDispatcherEntrypoint { + inner: state.clone(), + }; + + let _payload_db_loader = tokio::spawn(async move { + payload_iterator + .load_from_db() + .await + .expect("Payload loader crashed"); + }); + + let payload_dispatcher = PayloadDispatcher { + inner: state.clone(), + domain: "dummy_destination".to_string(), + }; + let _payload_dispatcher = tokio::spawn(async move { payload_dispatcher.spawn().await }); + + dispatcher_entrypoint.send_payload(&payload).await.unwrap(); + + // wait until the payload status is InTransaction(Finalized) + loop { + let stored_payload = state + .payload_db + .retrieve_payload_by_id(payload.id()) + .await + .unwrap() + .unwrap(); + if stored_payload.status == PayloadStatus::InTransaction(TransactionStatus::Finalized) { + break; + } + sleep(Duration::from_millis(100)).await; + } +} diff --git a/rust/main/submitter/src/transaction.rs b/rust/main/submitter/src/transaction.rs index 824da704e56..c8d2263611f 100644 --- a/rust/main/submitter/src/transaction.rs +++ b/rust/main/submitter/src/transaction.rs @@ -1,8 +1,6 @@ // TODO: re-enable clippy warnings #![allow(unused_imports)] -mod db; mod types; -pub use db::*; pub use types::*; diff --git a/rust/main/submitter/src/transaction/types.rs b/rust/main/submitter/src/transaction/types.rs index bd1d6e34cf9..825f19ff4f9 100644 --- a/rust/main/submitter/src/transaction/types.rs +++ b/rust/main/submitter/src/transaction/types.rs @@ -40,8 +40,16 @@ pub enum TransactionStatus { Included, /// in a block older than the configured `reorgPeriod` Finalized, + /// the tx was drop either by the submitter or by the chain + Dropped(DropReason), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub enum DropReason { /// currently only assigned when a reorg is detected DroppedByChain, + /// dropped by the submitter + FailedSimulation, } // add nested enum entries as we add VMs diff --git a/rust/main/utils/abigen/src/lib.rs b/rust/main/utils/abigen/src/lib.rs index 3fe21a55bb4..18b1811165c 100644 --- a/rust/main/utils/abigen/src/lib.rs +++ b/rust/main/utils/abigen/src/lib.rs @@ -68,6 +68,8 @@ pub fn generate_bindings_for_dir( /// Generate the bindings for a given ABI and return the new module name. Will /// create a file within the designated path with the correct `{module_name}.rs` /// format. +// We allow unused variables due to some feature flagging. +#[allow(unused_variables)] pub fn generate_bindings( contract_path: impl AsRef, output_dir: impl AsRef, diff --git a/rust/main/utils/run-locally/src/cosmosnative/cli.rs b/rust/main/utils/run-locally/src/cosmosnative/cli.rs index 15593bd261f..eaa8b549198 100644 --- a/rust/main/utils/run-locally/src/cosmosnative/cli.rs +++ b/rust/main/utils/run-locally/src/cosmosnative/cli.rs @@ -82,7 +82,7 @@ impl SimApp { .arg("rpc.pprof_laddr", &self.pprof_addr) // default is localhost:6060 .arg("log_level", "panic") .spawn("SIMAPP", None); - sleep(Duration::from_secs(2)); + sleep(Duration::from_secs(5)); node } @@ -102,7 +102,7 @@ impl SimApp { .filter_logs(|_| false) .run() .join(); - sleep(Duration::from_secs(1)); // wait for the block to mined + sleep(Duration::from_secs(5)); // wait for the block to mined } pub fn remote_transfer( @@ -134,7 +134,7 @@ impl SimApp { .filter_logs(|_| false) .run() .join(); - sleep(Duration::from_secs(1)); // wait for the block to mined + sleep(Duration::from_secs(5)); // wait for the block to mined } pub fn deploy_and_configure_contracts( @@ -226,7 +226,7 @@ impl SimApp { .flag("yes") .run() .join(); - sleep(Duration::from_secs(1)); // wait for the block to mined + sleep(Duration::from_secs(5)); // wait for the block to mined // create warp route // expected address: 0x726f757465725f61707000000000000000000000000000010000000000000000 diff --git a/rust/main/utils/run-locally/src/ethereum/termination_invariants.rs b/rust/main/utils/run-locally/src/ethereum/termination_invariants.rs index 85145e85268..61bedc0aafe 100644 --- a/rust/main/utils/run-locally/src/ethereum/termination_invariants.rs +++ b/rust/main/utils/run-locally/src/ethereum/termination_invariants.rs @@ -4,10 +4,11 @@ use crate::config::Config; use crate::invariants::{ provider_metrics_invariant_met, relayer_termination_invariants_met, scraper_termination_invariants_met, RelayerTerminationInvariantParams, + ScraperTerminationInvariantParams, }; use crate::logging::log; use crate::server::{fetch_relayer_gas_payment_event_count, fetch_relayer_message_processed_count}; -use crate::{RELAYER_METRICS_PORT, ZERO_MERKLE_INSERTION_KATHY_MESSAGES}; +use crate::{FAILED_MESSAGE_COUNT, RELAYER_METRICS_PORT, ZERO_MERKLE_INSERTION_KATHY_MESSAGES}; /// Use the metrics to check if the relayer queues are empty and the expected /// number of messages have been sent. @@ -33,7 +34,9 @@ pub fn termination_invariants_met( gas_payment_events_count, total_messages_expected, total_messages_dispatched: total_messages_expected, - submitter_queue_length_expected: ZERO_MERKLE_INSERTION_KATHY_MESSAGES, + failed_message_count: FAILED_MESSAGE_COUNT, + submitter_queue_length_expected: ZERO_MERKLE_INSERTION_KATHY_MESSAGES + + FAILED_MESSAGE_COUNT, non_matching_igp_message_count: 0, double_insertion_message_count: (config.kathy_messages as u32 / 4) * 2, }; @@ -41,11 +44,15 @@ pub fn termination_invariants_met( return Ok(false); } - if !scraper_termination_invariants_met( + let params = ScraperTerminationInvariantParams { gas_payment_events_count, - total_messages_expected + ZERO_MERKLE_INSERTION_KATHY_MESSAGES, - total_messages_expected, - )? { + total_messages_dispatched: total_messages_expected + + ZERO_MERKLE_INSERTION_KATHY_MESSAGES + + FAILED_MESSAGE_COUNT, + delivered_messages_scraped_expected: total_messages_expected, + }; + + if !scraper_termination_invariants_met(params)? { return Ok(false); } diff --git a/rust/main/utils/run-locally/src/invariants/termination_invariants.rs b/rust/main/utils/run-locally/src/invariants/termination_invariants.rs index e976cd43d47..0144f7e430b 100644 --- a/rust/main/utils/run-locally/src/invariants/termination_invariants.rs +++ b/rust/main/utils/run-locally/src/invariants/termination_invariants.rs @@ -17,6 +17,7 @@ pub struct RelayerTerminationInvariantParams<'a> { pub gas_payment_events_count: u32, pub total_messages_expected: u32, pub total_messages_dispatched: u32, + pub failed_message_count: u32, pub submitter_queue_length_expected: u32, pub non_matching_igp_message_count: u32, pub double_insertion_message_count: u32, @@ -34,6 +35,7 @@ pub fn relayer_termination_invariants_met( gas_payment_events_count, total_messages_expected, total_messages_dispatched, + failed_message_count, submitter_queue_length_expected, non_matching_igp_message_count, double_insertion_message_count, @@ -49,8 +51,9 @@ pub fn relayer_termination_invariants_met( assert!(!lengths.is_empty(), "Could not find queue length metric"); if lengths.iter().sum::() != submitter_queue_length_expected { log!( - "Relayer queues contain more messages than the zero-merkle-insertion ones. Lengths: {:?}", - lengths + "Relayer queues contain more messages than expected. Lengths: {:?}, expected {}", + lengths, + submitter_queue_length_expected ); return Ok(false); }; @@ -94,16 +97,17 @@ pub fn relayer_termination_invariants_met( // EDIT: Having had a quick look, it seems like there are some legitimate reverts happening in the confirm step // (`Transaction attempting to process message either reverted or was reorged`) // in which case more gas expenditure logs than messages are expected. - let gas_expenditure_log_count = *log_counts - .get(&gas_expenditure_line_filter) - .expect("Failed to get gas expenditure log count"); - assert!( - gas_expenditure_log_count >= total_messages_expected, - "Didn't record gas payment for all delivered messages. Got {} gas payment logs, expected at least {}", - gas_expenditure_log_count, - total_messages_expected - ); - // These tests check that we fixed https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3915, where some logs would not show up + // TODO: re-enable once the MessageProcessor IGP is integrated with the dispatcher + // let gas_expenditure_log_count = *log_counts + // .get(&gas_expenditure_line_filter) + // .expect("Failed to get gas expenditure log count"); + // assert!( + // gas_expenditure_log_count >= total_messages_expected, + // "Didn't record gas payment for all delivered messages. Got {} gas payment logs, expected at least {}", + // gas_expenditure_log_count, + // total_messages_expected + // ); + // // These tests check that we fixed https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3915, where some logs would not show up let storing_new_msg_log_count = *log_counts .get(&storing_new_msg_line_filter) @@ -160,10 +164,21 @@ pub fn relayer_termination_invariants_met( // RHS: total_messages_expected + non_matching_igp_messages + double_insertion_message_count let non_zero_sequence_count = merkle_tree_max_sequence.iter().filter(|&x| *x > 0).count() as u32; - assert_eq!( - merkle_tree_max_sequence.iter().sum::() + non_zero_sequence_count, - total_messages_expected + non_matching_igp_message_count + double_insertion_message_count, - ); + + let lhs = merkle_tree_max_sequence.iter().sum::() + non_zero_sequence_count; + let rhs = total_messages_expected + + non_matching_igp_message_count + + double_insertion_message_count + + failed_message_count; + if lhs != rhs { + log!( + "highest tree index does not match messages sent. got {} expected {}", + lhs, + rhs + ); + return Ok(false); + } + assert_eq!(lhs, rhs); let dropped_tasks = fetch_metric( RELAYER_METRICS_PORT, @@ -180,13 +195,23 @@ pub fn relayer_termination_invariants_met( Ok(true) } +pub struct ScraperTerminationInvariantParams { + pub gas_payment_events_count: u32, + pub total_messages_dispatched: u32, + pub delivered_messages_scraped_expected: u32, +} + /// returns false if invariants are not met /// returns true if invariants are met pub fn scraper_termination_invariants_met( - gas_payment_events_count: u32, - total_messages_dispatched: u32, - delivered_messages_scraped_expected: u32, + params: ScraperTerminationInvariantParams, ) -> eyre::Result { + let ScraperTerminationInvariantParams { + gas_payment_events_count, + total_messages_dispatched, + delivered_messages_scraped_expected, + } = params; + log!("Checking scraper termination invariants"); let dispatched_messages_scraped = fetch_metric( @@ -265,7 +290,6 @@ pub fn provider_metrics_invariant_met( let request_count = fetch_metric(relayer_port, "hyperlane_request_count", filter_hashmap)? .iter() .sum::(); - if request_count < expected_request_count { log!( "hyperlane_request_count {} count, expected {}", @@ -282,9 +306,7 @@ pub fn provider_metrics_invariant_met( )? .iter() .sum::(); - log!("Provider created count: {}", provider_create_count); - if provider_create_count < expected_request_count { log!( "hyperlane_provider_create_count only has {} count, expected at least {}", @@ -294,5 +316,23 @@ pub fn provider_metrics_invariant_met( return Ok(false); } + let metadata_build_hashmap: HashMap<&str, &str> = HashMap::new(); + + let metadata_build_count = fetch_metric( + relayer_port, + "hyperlane_metadata_build_count", + &metadata_build_hashmap, + )? + .iter() + .sum::(); + if metadata_build_count < expected_request_count { + log!( + "hyperlane_metadata_build_count only has {} count, expected at least {}", + metadata_build_count, + expected_request_count + ); + return Ok(false); + } + Ok(true) } diff --git a/rust/main/utils/run-locally/src/main.rs b/rust/main/utils/run-locally/src/main.rs index c312530b64b..c8956c28a4b 100644 --- a/rust/main/utils/run-locally/src/main.rs +++ b/rust/main/utils/run-locally/src/main.rs @@ -34,7 +34,7 @@ use logging::log; pub use metrics::fetch_metric; use once_cell::sync::Lazy; use program::Program; -use relayer::msg::pending_message::RETRIEVED_MESSAGE_LOG; +use relayer::msg::pending_message::{INVALIDATE_CACHE_METADATA_LOG, RETRIEVED_MESSAGE_LOG}; use tempfile::{tempdir, TempDir}; use utils::get_matching_lines; use utils::get_ts_infra_path; @@ -92,6 +92,7 @@ const ETH_VALIDATOR_KEYS: &[&str] = &[ const AGENT_BIN_PATH: &str = "target/debug"; const ZERO_MERKLE_INSERTION_KATHY_MESSAGES: u32 = 10; +const FAILED_MESSAGE_COUNT: u32 = 1; const RELAYER_METRICS_PORT: &str = "9092"; const SCRAPER_METRICS_PORT: &str = "9093"; @@ -294,6 +295,17 @@ fn main() -> ExitCode { .join(); state.push_agent(scraper_env.spawn("SCR", None)); + // Send a message that's guaranteed to fail + // "failMessageBody" hex value is 0x6661696c4d657373616765426f6479 + let fail_message_body = format!("0x{}", hex::encode("failMessageBody")); + let kathy_failed_tx = Program::new("yarn") + .working_dir(&ts_infra_path) + .cmd("kathy") + .arg("messages", FAILED_MESSAGE_COUNT.to_string()) + .arg("timeout", "1000") + .arg("body", fail_message_body.as_str()); + kathy_failed_tx.clone().run().join(); + // Send half the kathy messages before starting the rest of the agents let kathy_env_single_insertion = Program::new("yarn") .working_dir(&ts_infra_path) @@ -404,7 +416,13 @@ fn main() -> ExitCode { test_passed = wait_for_condition( &config, loop_start, - || Ok(relayer_restart_invariants_met()? && relayer_reorg_handling_invariants_met()?), + || { + Ok( + relayer_restart_invariants_met()? && relayer_reorg_handling_invariants_met()?, + // TODO: fix and uncomment + // && relayer_cached_metadata_invariant_met()? + ) + }, || !SHUTDOWN.load(Ordering::Relaxed), || long_running_processes_exited_check(&mut state), ); @@ -575,6 +593,33 @@ fn relayer_restart_invariants_met() -> eyre::Result { Ok(true) } +/// Check relayer reused already built metadata +/// TODO: fix +#[allow(dead_code)] +fn relayer_cached_metadata_invariant_met() -> eyre::Result { + let log_file_path = AGENT_LOGGING_DIR.join("RLY-output.log"); + let relayer_logfile = File::open(log_file_path).unwrap(); + + let line_filters = vec![vec![INVALIDATE_CACHE_METADATA_LOG]]; + + log!("Checking invalidate metadata cache happened..."); + let matched_logs = get_matching_lines(&relayer_logfile, line_filters.clone()); + + log!("matched_logs: {:?}", matched_logs); + + let invalidate_metadata_cache_count = *matched_logs + .get(&line_filters[0]) + .ok_or_else(|| eyre::eyre!("No logs matched line filters"))?; + if invalidate_metadata_cache_count == 0 { + log!( + "Invalidate cache metadata reuse count is {}, expected non-zero value", + invalidate_metadata_cache_count, + ); + return Ok(false); + } + Ok(true) +} + pub fn wait_for_condition( config: &Config, start_time: Instant, @@ -589,13 +634,22 @@ where { let loop_check_interval = Duration::from_secs(5); while loop_invariant_fn() { + log!("Checking e2e invariants..."); sleep(loop_check_interval); if !config.ci_mode { continue; } - if condition_fn().unwrap_or(false) { - // end condition reached successfully - break; + match condition_fn() { + Ok(true) => { + // end condition reached successfully + break; + } + Ok(false) => { + log!("E2E invariants not met yet..."); + } + Err(e) => { + log!("Error checking e2e invariants: {}", e); + } } if check_ci_timed_out(config.ci_mode_timeout, start_time) { // we ran out of time diff --git a/rust/main/utils/run-locally/src/sealevel/mod.rs b/rust/main/utils/run-locally/src/sealevel/mod.rs index a851b103419..58253483568 100644 --- a/rust/main/utils/run-locally/src/sealevel/mod.rs +++ b/rust/main/utils/run-locally/src/sealevel/mod.rs @@ -98,6 +98,8 @@ fn run_locally() { .hyp_env("DB", relayer_db.to_str().unwrap()) .hyp_env("CHAINS_SEALEVELTEST1_SIGNER_KEY", RELAYER_KEYS[0]) .hyp_env("CHAINS_SEALEVELTEST2_SIGNER_KEY", RELAYER_KEYS[1]) + .hyp_env("CHAINS_SEALEVELTEST1_SUBMITTER", "Lander") + .hyp_env("CHAINS_SEALEVELTEST2_SUBMITTER", "Lander") .hyp_env("RELAYCHAINS", "invalidchain,otherinvalid") .hyp_env("ALLOWLOCALCHECKPOINTSYNCERS", "true") .hyp_env( diff --git a/rust/main/utils/run-locally/src/sealevel/termination_invariants.rs b/rust/main/utils/run-locally/src/sealevel/termination_invariants.rs index 2bd935c20ab..110054be230 100644 --- a/rust/main/utils/run-locally/src/sealevel/termination_invariants.rs +++ b/rust/main/utils/run-locally/src/sealevel/termination_invariants.rs @@ -7,6 +7,7 @@ use crate::{ invariants::{ provider_metrics_invariant_met, relayer_termination_invariants_met, scraper_termination_invariants_met, RelayerTerminationInvariantParams, + ScraperTerminationInvariantParams, }, logging::log, sealevel::{solana::*, SOL_MESSAGES_EXPECTED, SOL_MESSAGES_WITH_NON_MATCHING_IGP}, @@ -23,6 +24,7 @@ pub fn termination_invariants_met( solana_cli_tools_path: &Path, solana_config_path: &Path, ) -> eyre::Result { + log!("Checking sealevel termination invariants"); let sol_messages_expected = SOL_MESSAGES_EXPECTED; let sol_messages_with_non_matching_igp = SOL_MESSAGES_WITH_NON_MATCHING_IGP; @@ -42,11 +44,13 @@ pub fn termination_invariants_met( gas_payment_events_count, total_messages_expected, total_messages_dispatched, + failed_message_count: 0, submitter_queue_length_expected: sol_messages_with_non_matching_igp, non_matching_igp_message_count: 0, double_insertion_message_count: sol_messages_with_non_matching_igp, }; if !relayer_termination_invariants_met(params)? { + log!("Relayer termination invariants not met"); return Ok(false); } @@ -55,11 +59,14 @@ pub fn termination_invariants_met( return Ok(false); } - if !scraper_termination_invariants_met( + let params = ScraperTerminationInvariantParams { gas_payment_events_count, total_messages_dispatched, - total_messages_expected, - )? { + delivered_messages_scraped_expected: total_messages_expected, + }; + + if !scraper_termination_invariants_met(params)? { + log!("Scraper termination invariants not met"); return Ok(false); } @@ -69,6 +76,7 @@ pub fn termination_invariants_met( &hashmap! {"chain" => "sealeveltest2", "connection" => "rpc", "status" => "success"}, &hashmap! {"chain" => "sealeveltest2"}, )? { + log!("Provider metrics invariants not met"); return Ok(false); } diff --git a/rust/sealevel/.gitignore b/rust/sealevel/.gitignore index b8a7e200a5d..9e807879ebc 100644 --- a/rust/sealevel/.gitignore +++ b/rust/sealevel/.gitignore @@ -1,3 +1,4 @@ /target environments/**/deploy-logs.txt -**/**/keys \ No newline at end of file +environments/local-e2e/igp/* +**/**/keys diff --git a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json index c5e945eae3c..ba62748efea 100644 --- a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json +++ b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json @@ -1,10 +1,10 @@ { - "sealeveltest2": { - "hex": "0x2317f9615d4ebc2419ad4b88580e2a80a03b2c7a60bc960de7d6934dbc37a87e", - "base58": "3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH" - }, "sealeveltest1": { "hex": "0xa77b4e2ed231894cc8cb8eee21adcc705d8489bccc6b2fcf40a358de23e60b7b", "base58": "CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga" + }, + "sealeveltest2": { + "hex": "0x2317f9615d4ebc2419ad4b88580e2a80a03b2c7a60bc960de7d6934dbc37a87e", + "base58": "3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH" } } \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet3/warp-routes/GIGA-solanamainnet-soon/program-ids.json b/rust/sealevel/environments/mainnet3/warp-routes/GIGA-solanamainnet-soon/program-ids.json new file mode 100644 index 00000000000..0ba60890eb6 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/GIGA-solanamainnet-soon/program-ids.json @@ -0,0 +1,10 @@ +{ + "soon": { + "hex": "0xe320ef1ee7d61f2fb1a866b7d42ac3ba74c62baf78f71386d83049ddcc4094e0", + "base58": "GHcg7qY2cXQ4Ej9xQqjnLdYUV41bt2ncaPuo5AxN5eTy" + }, + "solanamainnet": { + "hex": "0xae4bf81d5e7a4759c4c2f28098f9facca1a14abf95121f3adfe612da7454afa6", + "base58": "CjP8N5L2KV4iWobXoVV1dhftRdfQ7S3rKVrT7Lja9Jku" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet3/warp-routes/GIGA-solanamainnet-soon/token-config.json b/rust/sealevel/environments/mainnet3/warp-routes/GIGA-solanamainnet-soon/token-config.json new file mode 100644 index 00000000000..fb132f2f2d2 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/GIGA-solanamainnet-soon/token-config.json @@ -0,0 +1,18 @@ +{ + "solanamainnet": { + "type": "collateral", + "decimals": 5, + "interchainGasPaymaster": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF", + "token": "63LfDmNb3MQ8mw9MtZ2To9bEA2M71kZUUGq5tiJxcqj9", + "owner": "E2vjMXigtkS9xxaTt87MgddnesCoiW87akkg3U5BXvTY" + }, + "soon": { + "type": "synthetic", + "decimals": 5, + "name": "GIGACHAD", + "symbol": "GIGA", + "uri": "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/2fc8f3fe42df31dbd39b6daf4a96dcaf5ee8de06/deployments/warp_routes/GIGA/metadata.json", + "interchainGasPaymaster": "GctmRsKQM5VLPrUPpsQ7Kc7RaVKNC2Fev31n1KtLkj8A", + "owner": "CvD8JycSHKTXV5ibc3xeq8CojMMJBtL2ZMNDqjk5fLF3" + } +} diff --git a/rust/sealevel/environments/mainnet3/warp-routes/GOAT-solanamainnet-soon/program-ids.json b/rust/sealevel/environments/mainnet3/warp-routes/GOAT-solanamainnet-soon/program-ids.json new file mode 100644 index 00000000000..dc48f27a615 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/GOAT-solanamainnet-soon/program-ids.json @@ -0,0 +1,10 @@ +{ + "soon": { + "hex": "0x7c6cb9656e4a59d4326f161b97d59e53ed6f915a16e2d6cc9ec22cc082e2e732", + "base58": "9NhiDziXERQUsb3NxRNdamR6ivfeF8GanUJvBffQbyBF" + }, + "solanamainnet": { + "hex": "0xf2def01dc8fac959dd9ddbc306daf3c364d7151c6c87936fd92ad45f7ec382ef", + "base58": "HM4qABV4C6KPYhZQmJnQXJ9Q9Syo5waPcxFWLQNiDq2e" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet3/warp-routes/GOAT-solanamainnet-soon/token-config.json b/rust/sealevel/environments/mainnet3/warp-routes/GOAT-solanamainnet-soon/token-config.json new file mode 100644 index 00000000000..fc29d67f9b6 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/GOAT-solanamainnet-soon/token-config.json @@ -0,0 +1,18 @@ +{ + "solanamainnet": { + "type": "collateral", + "decimals": 6, + "interchainGasPaymaster": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF", + "token": "CzLSujWBLFsSjncfkh59rUFqvafWcY5tzedWJSuypump", + "owner": "E2vjMXigtkS9xxaTt87MgddnesCoiW87akkg3U5BXvTY" + }, + "soon": { + "type": "synthetic", + "decimals": 6, + "name": "Goatseus Maximus", + "symbol": "GOAT", + "uri": "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/2fc8f3fe42df31dbd39b6daf4a96dcaf5ee8de06/deployments/warp_routes/GOAT/metadata.json", + "interchainGasPaymaster": "GctmRsKQM5VLPrUPpsQ7Kc7RaVKNC2Fev31n1KtLkj8A", + "owner": "CvD8JycSHKTXV5ibc3xeq8CojMMJBtL2ZMNDqjk5fLF3" + } +} diff --git a/rust/sealevel/environments/mainnet3/warp-routes/POPCAT-solanamainnet-soon/program-ids.json b/rust/sealevel/environments/mainnet3/warp-routes/POPCAT-solanamainnet-soon/program-ids.json new file mode 100644 index 00000000000..91963c7ba9c --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/POPCAT-solanamainnet-soon/program-ids.json @@ -0,0 +1,10 @@ +{ + "soon": { + "hex": "0x11c642370c857086ba6f09da7c2f083e1a1b1582d1fe5f2e07177e2030e7efaf", + "base58": "2CPGmQCNdGrh45n6BgTG98G7AkQfEKPZCcYW2reju2kn" + }, + "solanamainnet": { + "hex": "0x9b34da3f776559c4f477126e4a2bb9f2cbe4a3036fba27afba1d3d0ce2728a55", + "base58": "BSrwJNiHM88XRYX93CRXGSTR4GfnZfxn3kTa4rvanPKr" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet3/warp-routes/POPCAT-solanamainnet-soon/token-config.json b/rust/sealevel/environments/mainnet3/warp-routes/POPCAT-solanamainnet-soon/token-config.json new file mode 100644 index 00000000000..c0818462815 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/POPCAT-solanamainnet-soon/token-config.json @@ -0,0 +1,18 @@ +{ + "solanamainnet": { + "type": "collateral", + "decimals": 9, + "interchainGasPaymaster": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF", + "token": "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr", + "owner": "E2vjMXigtkS9xxaTt87MgddnesCoiW87akkg3U5BXvTY" + }, + "soon": { + "type": "synthetic", + "decimals": 9, + "name": "POPCAT", + "symbol": "POPCAT", + "uri": "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/2fc8f3fe42df31dbd39b6daf4a96dcaf5ee8de06/deployments/warp_routes/POPCAT/metadata.json", + "interchainGasPaymaster": "GctmRsKQM5VLPrUPpsQ7Kc7RaVKNC2Fev31n1KtLkj8A", + "owner": "CvD8JycSHKTXV5ibc3xeq8CojMMJBtL2ZMNDqjk5fLF3" + } +} diff --git a/rust/sealevel/environments/mainnet3/warp-routes/SPORE-solanamainnet-soon/program-ids.json b/rust/sealevel/environments/mainnet3/warp-routes/SPORE-solanamainnet-soon/program-ids.json new file mode 100644 index 00000000000..7cf5a5f4589 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/SPORE-solanamainnet-soon/program-ids.json @@ -0,0 +1,10 @@ +{ + "solanamainnet": { + "hex": "0x1e9bcc616075f0fb7026ac02fa7bc3efe7be24a3b2fc2916eaac9a0821fec279", + "base58": "34V29ar8oahnZVnnQqEfd9pUE3MBrAxpgMtQf1Xccs1A" + }, + "soon": { + "hex": "0xffdd5bb49a4fcaf33ae9a5823336c7d0599a523095e494da804d957cb1f86514", + "base58": "JDnjXhoucA9HwTk1s7piyoCLvwQSKdnaRitdkpXsRgtj" + } +} \ No newline at end of file diff --git a/rust/sealevel/environments/mainnet3/warp-routes/SPORE-solanamainnet-soon/token-config.json b/rust/sealevel/environments/mainnet3/warp-routes/SPORE-solanamainnet-soon/token-config.json new file mode 100644 index 00000000000..5a71a4aaf17 --- /dev/null +++ b/rust/sealevel/environments/mainnet3/warp-routes/SPORE-solanamainnet-soon/token-config.json @@ -0,0 +1,18 @@ +{ + "solanamainnet": { + "type": "collateral", + "decimals": 6, + "interchainGasPaymaster": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF", + "token": "8bdhP1UQMevciC9oJ7NrvgDfoW8XPXPfbkkm6vKtMS7N", + "owner": "E2vjMXigtkS9xxaTt87MgddnesCoiW87akkg3U5BXvTY" + }, + "soon": { + "type": "synthetic", + "decimals": 6, + "name": "SPORE", + "symbol": "SPORE", + "uri": "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/2fc8f3fe42df31dbd39b6daf4a96dcaf5ee8de06/deployments/warp_routes/SPORE/metadata.json", + "interchainGasPaymaster": "GctmRsKQM5VLPrUPpsQ7Kc7RaVKNC2Fev31n1KtLkj8A", + "owner": "CvD8JycSHKTXV5ibc3xeq8CojMMJBtL2ZMNDqjk5fLF3" + } +} From 25662f482320defdd3e6f91c8225982a1c4ebd1d Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 10:33:31 +0200 Subject: [PATCH 126/132] chore: merge sdk package from main --- typescript/sdk/CHANGELOG.md | 43 ++++ typescript/sdk/package.json | 9 +- typescript/sdk/src/consts/multisigIsm.ts | 103 +++++++--- .../sdk/src/core/CoreDeployer.hardhat-test.ts | 9 +- typescript/sdk/src/core/EvmCoreModule.ts | 3 +- typescript/sdk/src/core/HyperlaneCore.ts | 6 +- .../sdk/src/core/HyperlaneCoreChecker.ts | 3 +- typescript/sdk/src/core/HyperlaneRelayer.ts | 8 +- typescript/sdk/src/deploy/proxy.ts | 11 +- .../sdk/src/deploy/proxyFactoryUtils.ts | 18 ++ typescript/sdk/src/gas/token-prices.test.ts | 1 + typescript/sdk/src/hook/EvmHookReader.ts | 3 +- typescript/sdk/src/hook/types.ts | 22 +++ typescript/sdk/src/index.ts | 17 +- typescript/sdk/src/ism/EvmIsmReader.ts | 4 +- .../sdk/src/ism/metadata/aggregation.ts | 3 +- typescript/sdk/src/ism/metadata/routing.ts | 9 +- typescript/sdk/src/ism/metadata/types.ts | 4 +- typescript/sdk/src/ism/types.ts | 22 ++- typescript/sdk/src/ism/utils.ts | 35 ++++ typescript/sdk/src/metadata/agentConfig.ts | 62 ++++++ .../src/metadata/chainMetadataConversion.ts | 5 +- .../sdk/src/metadata/chainMetadataTypes.ts | 10 + typescript/sdk/src/providers/ProviderType.ts | 40 +++- .../sdk/src/providers/providerBuilders.ts | 22 ++- .../sdk/src/router/HyperlaneRouterChecker.ts | 24 ++- .../sdk/src/router/ProxiedRouterChecker.ts | 6 + typescript/sdk/src/router/types.ts | 24 ++- .../token/EvmERC20WarpModule.hardhat-test.ts | 18 +- .../sdk/src/token/EvmERC20WarpModule.ts | 43 ++-- .../EvmERC20WarpRouteReader.hardhat-test.ts | 20 +- .../sdk/src/token/EvmERC20WarpRouteReader.ts | 17 +- typescript/sdk/src/token/Token.test.ts | 1 - typescript/sdk/src/token/Token.ts | 18 +- typescript/sdk/src/token/TokenStandard.ts | 2 - .../token/adapters/CosmWasmTokenAdapter.ts | 21 ++ .../src/token/adapters/CosmosTokenAdapter.ts | 7 + .../sdk/src/token/adapters/EvmTokenAdapter.ts | 92 +++++++++ .../sdk/src/token/adapters/ITokenAdapter.ts | 1 + .../token/adapters/SealevelTokenAdapter.ts | 14 ++ typescript/sdk/src/token/checker.ts | 14 +- typescript/sdk/src/token/config.ts | 3 - typescript/sdk/src/token/contracts.ts | 6 - .../sdk/src/token/deploy.hardhat-test.ts | 89 +++++++-- typescript/sdk/src/token/types.test.ts | 7 +- typescript/sdk/src/token/types.ts | 33 +++- typescript/sdk/src/utils/zksync.ts | 32 +++ typescript/sdk/src/warp/WarpCore.test.ts | 4 + typescript/sdk/src/warp/WarpCore.ts | 37 +++- typescript/sdk/src/warp/types.ts | 1 + typescript/sdk/src/zksync/ZKSyncDeployer.ts | 185 ++++++++++++++++++ yarn.lock | 96 ++++++--- 52 files changed, 1068 insertions(+), 219 deletions(-) create mode 100644 typescript/sdk/src/deploy/proxyFactoryUtils.ts create mode 100644 typescript/sdk/src/utils/zksync.ts create mode 100644 typescript/sdk/src/zksync/ZKSyncDeployer.ts diff --git a/typescript/sdk/CHANGELOG.md b/typescript/sdk/CHANGELOG.md index 67489d0e653..d5415ce0e52 100644 --- a/typescript/sdk/CHANGELOG.md +++ b/typescript/sdk/CHANGELOG.md @@ -1,5 +1,48 @@ # @hyperlane-xyz/sdk +## 12.1.0 + +### Minor Changes + +- acbf5936a: New check: HyperlaneRouterChecker now compares the list of domains + the Router is enrolled with against the warp route expectations. + It will raise a violation for missing remote domains. + `check-deploy` and `check-warp-deploy` scripts use this new check. +- c757b6a18: Include entire RPC array for chainMetadataToViemChain +- a646f9ca1: Added ZKSync specific deployment logic and artifact related utils +- 3b615c892: Adds the proxyAdmin.owner to the Checker ownerOverrides such that it checks proxyAdmin.owner instead of always using the top-level owner + +### Patch Changes + +- Updated dependencies [e6f6d61a0] + - @hyperlane-xyz/core@7.1.0 + - @hyperlane-xyz/utils@12.1.0 + +## 12.0.0 + +### Major Changes + +- 59a087ded: Remove unused FastTokenRouter + +### Minor Changes + +- 4d3738d14: Update Checker to only check collateralToken and collateralProxyAdmin if provided in ownerOverrides +- 07321f6f0: ZKSync Provider types with builders +- 337193305: Add new `public` field to RpcUrlSchema + +### Patch Changes + +- f7ca32315: fix: correct exported TypeScript types for synthetic tokens +- 59a087ded: Deploy new scaled warp route bytecode +- Updated dependencies [07321f6f0] +- Updated dependencies [59a087ded] +- Updated dependencies [59a087ded] +- Updated dependencies [59a087ded] +- Updated dependencies [59a087ded] +- Updated dependencies [59a087ded] + - @hyperlane-xyz/core@7.0.0 + - @hyperlane-xyz/utils@12.0.0 + ## 11.0.0 ### Major Changes diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 36aa0c230c2..b8f70efbb0a 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -1,16 +1,16 @@ { "name": "@hyperlane-xyz/sdk", "description": "The official SDK for the Hyperlane Network", - "version": "11.0.0", + "version": "12.1.0", "dependencies": { "@arbitrum/sdk": "^4.0.0", "@aws-sdk/client-s3": "^3.577.0", "@chain-registry/types": "^0.50.14", "@cosmjs/cosmwasm-stargate": "^0.32.4", "@cosmjs/stargate": "^0.32.4", - "@hyperlane-xyz/core": "6.1.0", + "@hyperlane-xyz/core": "7.1.0", "@hyperlane-xyz/starknet-core": "0.3.1", - "@hyperlane-xyz/utils": "11.0.0", + "@hyperlane-xyz/utils": "12.1.0", "@safe-global/api-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0", "@safe-global/safe-deployments": "1.37.23", @@ -21,8 +21,9 @@ "cross-fetch": "^3.1.5", "ethers": "^5.7.2", "pino": "^8.19.0", - "starknet": "6.23.1", + "starknet": "^6.23.1", "viem": "^2.21.45", + "zksync-ethers": "^5.10.0", "zod": "^3.21.2" }, "devDependencies": { diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index 940258962b6..65e75d5ab9e 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -258,18 +258,6 @@ export const defaultMultisigConfigs: ChainMap = { ], }, - astarzkevm: { - threshold: 2, - validators: [ - { - address: '0x89ecdd6caf138934bf3a2fb7b323984d72fd66de', - alias: AW_VALIDATOR_ALIAS, - }, - DEFAULT_MERKLY_VALIDATOR, - DEFAULT_MITOSIS_VALIDATOR, - ], - }, - aurora: { threshold: 2, validators: [ @@ -352,7 +340,7 @@ export const defaultMultisigConfigs: ChainMap = { }, bitlayer: { - threshold: 2, + threshold: 4, validators: [ { address: '0x1d9b0f4ea80dbfc71cb7d64d8005eccf7c41e75f', @@ -360,6 +348,9 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + DEFAULT_BLOCKPI_VALIDATOR, + DEFAULT_ZKV_VALIDATOR, + DEFAULT_HASHKEY_CLOUD_VALIDATOR, ], }, @@ -416,7 +407,7 @@ export const defaultMultisigConfigs: ChainMap = { }, bsc: { - threshold: 3, + threshold: 4, validators: [ { address: '0x570af9b7b36568c8877eebba6c6727aa9dab7268', @@ -425,6 +416,8 @@ export const defaultMultisigConfigs: ChainMap = { { address: '0x8292b1a53907ece0f76af8a50724e9492bcdc8a3', alias: 'DSRV' }, DEFAULT_EVERSTAKE_VALIDATOR, DEFAULT_ZEE_PRIME_VALIDATOR, + DEFAULT_BLOCKPI_VALIDATOR, + DEFAULT_TESSELLATED_VALIDATOR, ], }, @@ -485,12 +478,20 @@ export const defaultMultisigConfigs: ChainMap = { address: '0x63478422679303c3e4fc611b771fa4a707ef7f4a', alias: AW_VALIDATOR_ALIAS, }, - { address: '0x622e43baf06ad808ca8399360d9a2d9a1a12688b', alias: 'DSRV' }, - DEFAULT_EVERSTAKE_VALIDATOR, - DEFAULT_STAKED_VALIDATOR, - DEFAULT_ZEE_PRIME_VALIDATOR, - DEFAULT_ZKV_VALIDATOR, - DEFAULT_BLOCKPI_VALIDATOR, + { + address: '0xeb0c31e2f2671d724a2589d4a8eca91b97559148', + alias: 'Imperator', + }, + { + address: '0x033e391e9fc57a7b5dd6c91b69be9a1ed11c4986', + alias: 'Enigma', + }, + { + address: '0x4a2423ef982b186729e779b6e54b0e84efea7285', + alias: 'Luganodes', + }, + DEFAULT_BWARE_LABS_VALIDATOR, + DEFAULT_TESSELLATED_VALIDATOR, ], }, @@ -618,12 +619,14 @@ export const defaultMultisigConfigs: ChainMap = { }, coti: { - threshold: 1, + threshold: 2, validators: [ { address: '0x3c89379537f8beafc54e7e8ab4f8a1cf7974b9f0', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -650,12 +653,14 @@ export const defaultMultisigConfigs: ChainMap = { }, deepbrainchain: { - threshold: 1, + threshold: 2, validators: [ { address: '0x3825ea1e0591b58461cc4aa34867668260c0e6a8', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -753,7 +758,7 @@ export const defaultMultisigConfigs: ChainMap = { }, ethereum: { - threshold: 4, + threshold: 6, validators: [ { address: '0x03c842db86a6a3e524d4a6615390c1ea8e2b9541', @@ -771,6 +776,11 @@ export const defaultMultisigConfigs: ChainMap = { address: '0xbf1023eff3dba21263bf2db2add67a0d6bcda2de', alias: 'AVS: Pier Two', }, + { + address: '0x5d7442439959af11172bf92d9a8d21cf88d136e3', + alias: 'P2P', + }, + DEFAULT_ZKV_VALIDATOR, ], }, @@ -1198,8 +1208,18 @@ export const defaultMultisigConfigs: ChainMap = { ], }, + kyvetestnet: { + threshold: 1, + validators: [ + { + address: '0x3c470ad2640bc0bcb6a790e8cf85e54d34ca92f5', + alias: AW_VALIDATOR_ALIAS, + }, + ], + }, + linea: { - threshold: 2, + threshold: 4, validators: [ { address: '0xf2d5409a59e0f5ae7635aff73685624904a77d94', @@ -1207,6 +1227,9 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + DEFAULT_BLOCKPI_VALIDATOR, + DEFAULT_ZKV_VALIDATOR, + DEFAULT_HASHKEY_CLOUD_VALIDATOR, ], }, @@ -1367,7 +1390,7 @@ export const defaultMultisigConfigs: ChainMap = { }, metis: { - threshold: 2, + threshold: 4, validators: [ { address: '0xc4a3d25107060e800a43842964546db508092260', @@ -1375,6 +1398,9 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + DEFAULT_BLOCKPI_VALIDATOR, + DEFAULT_ZKV_VALIDATOR, + DEFAULT_HASHKEY_CLOUD_VALIDATOR, ], }, @@ -1509,12 +1535,14 @@ export const defaultMultisigConfigs: ChainMap = { }, nibiru: { - threshold: 1, + threshold: 2, validators: [ { address: '0xba9779d84a8efba1c6bc66326d875c3611a24b24', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1541,12 +1569,14 @@ export const defaultMultisigConfigs: ChainMap = { }, opbnb: { - threshold: 1, + threshold: 2, validators: [ { address: '0x1bdf52749ef2411ab9c28742dea92f209e96c9c4', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1722,12 +1752,14 @@ export const defaultMultisigConfigs: ChainMap = { }, reactive: { - threshold: 1, + threshold: 2, validators: [ { address: '0x45768525f6c5ca2e4e7cc50d405370eadee2d624', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1772,7 +1804,7 @@ export const defaultMultisigConfigs: ChainMap = { }, ronin: { - threshold: 2, + threshold: 4, validators: [ { address: '0xa3e11929317e4a871c3d47445ea7bb8c4976fd8a', @@ -1780,6 +1812,9 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + DEFAULT_BLOCKPI_VALIDATOR, + DEFAULT_ZKV_VALIDATOR, + DEFAULT_HASHKEY_CLOUD_VALIDATOR, ], }, @@ -1989,7 +2024,7 @@ export const defaultMultisigConfigs: ChainMap = { }, sonic: { - threshold: 2, + threshold: 4, validators: [ { address: '0xa313d72dbbd3fa51a2ed1611ea50c37946fa42f7', @@ -1997,6 +2032,9 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + DEFAULT_BLOCKPI_VALIDATOR, + DEFAULT_ZKV_VALIDATOR, + DEFAULT_HASHKEY_CLOUD_VALIDATOR, ], }, @@ -2117,7 +2155,7 @@ export const defaultMultisigConfigs: ChainMap = { }, subtensor: { - threshold: 2, + threshold: 4, validators: [ { address: '0xd5f8196d7060b85bea491f0b52a671e05f3d10a2', @@ -2125,6 +2163,9 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + DEFAULT_BLOCKPI_VALIDATOR, + DEFAULT_ZKV_VALIDATOR, + DEFAULT_TESSELLATED_VALIDATOR, ], }, diff --git a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts index 581568f3346..dd6516f5e24 100644 --- a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts +++ b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts @@ -9,10 +9,13 @@ import { Address, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { TestChainName, testChains } from '../consts/testChains.js'; import { HyperlaneContractsMap } from '../contracts/types.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; -import { DerivedHookConfig } from '../hook/EvmHookReader.js'; -import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; +import { DerivedHookConfig } from '../hook/types.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; -import { AggregationIsmConfig, IsmType } from '../ism/types.js'; +import { + AggregationIsmConfig, + DerivedIsmConfig, + IsmType, +} from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { testCoreConfig } from '../test/testUtils.js'; import { ChainMap } from '../types.js'; diff --git a/typescript/sdk/src/core/EvmCoreModule.ts b/typescript/sdk/src/core/EvmCoreModule.ts index 50d6efd8321..11562e688ee 100644 --- a/typescript/sdk/src/core/EvmCoreModule.ts +++ b/typescript/sdk/src/core/EvmCoreModule.ts @@ -38,9 +38,8 @@ import { proxyAdminUpdateTxs } from '../deploy/proxy.js'; import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; import { HookFactories } from '../hook/contracts.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js'; -import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; -import { IsmConfig } from '../ism/types.js'; +import { DerivedIsmConfig, IsmConfig } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; import { ChainName, ChainNameOrId } from '../types.js'; diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index 10d4415ee72..bc4b2a882bc 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -31,8 +31,10 @@ import { HyperlaneAddressesMap, HyperlaneContracts, } from '../contracts/types.js'; -import { DerivedHookConfig, EvmHookReader } from '../hook/EvmHookReader.js'; -import { DerivedIsmConfig, EvmIsmReader } from '../ism/EvmIsmReader.js'; +import { EvmHookReader } from '../hook/EvmHookReader.js'; +import { DerivedHookConfig } from '../hook/types.js'; +import { EvmIsmReader } from '../ism/EvmIsmReader.js'; +import { DerivedIsmConfig } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { RouterConfig } from '../router/types.js'; import { ChainMap, ChainName, OwnableConfig } from '../types.js'; diff --git a/typescript/sdk/src/core/HyperlaneCoreChecker.ts b/typescript/sdk/src/core/HyperlaneCoreChecker.ts index 82a26c40593..29f11b36723 100644 --- a/typescript/sdk/src/core/HyperlaneCoreChecker.ts +++ b/typescript/sdk/src/core/HyperlaneCoreChecker.ts @@ -7,8 +7,9 @@ import { BytecodeHash } from '../consts/bytecode.js'; import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker.js'; import { proxyImplementation } from '../deploy/proxy.js'; import { OwnerViolation, ViolationType } from '../deploy/types.js'; -import { DerivedIsmConfig, EvmIsmReader } from '../ism/EvmIsmReader.js'; +import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; +import { DerivedIsmConfig } from '../ism/types.js'; import { collectValidators, moduleMatchesConfig } from '../ism/utils.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap, ChainName } from '../types.js'; diff --git a/typescript/sdk/src/core/HyperlaneRelayer.ts b/typescript/sdk/src/core/HyperlaneRelayer.ts index c36a37aef30..3e5b7e3cbd2 100644 --- a/typescript/sdk/src/core/HyperlaneRelayer.ts +++ b/typescript/sdk/src/core/HyperlaneRelayer.ts @@ -15,11 +15,11 @@ import { sleep, } from '@hyperlane-xyz/utils'; -import { DerivedHookConfig, EvmHookReader } from '../hook/EvmHookReader.js'; -import { HookConfigSchema } from '../hook/types.js'; -import { DerivedIsmConfig, EvmIsmReader } from '../ism/EvmIsmReader.js'; +import { EvmHookReader } from '../hook/EvmHookReader.js'; +import { DerivedHookConfig, HookConfigSchema } from '../hook/types.js'; +import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { BaseMetadataBuilder } from '../ism/metadata/builder.js'; -import { IsmConfigSchema } from '../ism/types.js'; +import { DerivedIsmConfig, IsmConfigSchema } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap, ChainName } from '../types.js'; diff --git a/typescript/sdk/src/deploy/proxy.ts b/typescript/sdk/src/deploy/proxy.ts index 674bd32d849..ecd00215977 100644 --- a/typescript/sdk/src/deploy/proxy.ts +++ b/typescript/sdk/src/deploy/proxy.ts @@ -1,4 +1,5 @@ import { ethers } from 'ethers'; +import { Provider as ZKSyncProvider } from 'zksync-ethers'; import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; import { Address, ChainId, eqAddress } from '@hyperlane-xyz/utils'; @@ -7,6 +8,8 @@ import { transferOwnershipTransactions } from '../contracts/contracts.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; import { DeployedOwnableConfig } from '../types.js'; +type EthersLikeProvider = ethers.providers.Provider | ZKSyncProvider; + export type UpgradeConfig = { timelock: { delay: number; @@ -19,7 +22,7 @@ export type UpgradeConfig = { }; export async function proxyImplementation( - provider: ethers.providers.Provider, + provider: EthersLikeProvider, proxy: Address, ): Promise
{ // Hardcoded storage slot for implementation per EIP-1967 @@ -31,7 +34,7 @@ export async function proxyImplementation( } export async function isInitialized( - provider: ethers.providers.Provider, + provider: EthersLikeProvider, contract: Address, ): Promise { // Using OZ's Initializable 4.9 which keeps it at the 0x0 slot @@ -43,7 +46,7 @@ export async function isInitialized( } export async function proxyAdmin( - provider: ethers.providers.Provider, + provider: EthersLikeProvider, proxy: Address, ): Promise
{ // Hardcoded storage slot for admin per EIP-1967 @@ -72,7 +75,7 @@ export function proxyConstructorArgs( } export async function isProxy( - provider: ethers.providers.Provider, + provider: EthersLikeProvider, proxy: Address, ): Promise { const admin = await proxyAdmin(provider, proxy); diff --git a/typescript/sdk/src/deploy/proxyFactoryUtils.ts b/typescript/sdk/src/deploy/proxyFactoryUtils.ts new file mode 100644 index 00000000000..b4a27c709eb --- /dev/null +++ b/typescript/sdk/src/deploy/proxyFactoryUtils.ts @@ -0,0 +1,18 @@ +import { ethers } from 'ethers'; + +import { objMap } from '@hyperlane-xyz/utils'; + +import { proxyFactoryFactories } from './contracts.js'; +import { ProxyFactoryFactoriesAddresses } from './types.js'; + +/** + * Creates a default ProxyFactoryFactoriesAddresses object with all values set to ethers.constants.AddressZero. + * @returns {ProxyFactoryFactoriesAddresses} An object with all factory addresses set to AddressZero. + */ +export function createDefaultProxyFactoryFactories(): ProxyFactoryFactoriesAddresses { + const defaultAddress = ethers.constants.AddressZero; + return objMap( + proxyFactoryFactories, + () => defaultAddress, + ) as ProxyFactoryFactoriesAddresses; +} diff --git a/typescript/sdk/src/gas/token-prices.test.ts b/typescript/sdk/src/gas/token-prices.test.ts index dbc1dc76b1a..90ad20edcfb 100644 --- a/typescript/sdk/src/gas/token-prices.test.ts +++ b/typescript/sdk/src/gas/token-prices.test.ts @@ -20,6 +20,7 @@ describe('TokenPriceGetter', () => { beforeEach(() => { tokenPriceGetter = new CoinGeckoTokenPriceGetter({ + // @ts-ignore TODO: remove once merged with main chainMetadata: { ethereum, solanamainnet, ...testChainMetadata }, apiKey: 'test', expirySeconds: 10, diff --git a/typescript/sdk/src/hook/EvmHookReader.ts b/typescript/sdk/src/hook/EvmHookReader.ts index 1452feb0271..91f983c8e57 100644 --- a/typescript/sdk/src/hook/EvmHookReader.ts +++ b/typescript/sdk/src/hook/EvmHookReader.ts @@ -39,6 +39,7 @@ import { AmountRoutingHookConfig, ArbL2ToL1HookConfig, CCIPHookConfig, + DerivedHookConfig, DomainRoutingHookConfig, FallbackRoutingHookConfig, HookConfig, @@ -53,8 +54,6 @@ import { RoutingHookConfig, } from './types.js'; -export type DerivedHookConfig = WithAddress>; - export interface HookReader { deriveHookConfig(address: Address): Promise>; deriveMerkleTreeConfig( diff --git a/typescript/sdk/src/hook/types.ts b/typescript/sdk/src/hook/types.ts index 1cea49b6be4..170071a4d3a 100644 --- a/typescript/sdk/src/hook/types.ts +++ b/typescript/sdk/src/hook/types.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import { Address, WithAddress } from '@hyperlane-xyz/utils'; + import { ProtocolAgnositicGasOracleConfigSchema } from '../gas/oracle/types.js'; import { ZHash } from '../metadata/customZodTypes.js'; import { @@ -44,6 +46,24 @@ export enum HookType { CCIP = 'ccipHook', } +export const HookTypeToContractNameMap: Record< + Exclude, + string +> = { + [HookType.MERKLE_TREE]: 'merkleTreeHook', + [HookType.INTERCHAIN_GAS_PAYMASTER]: 'interchainGasPaymaster', + [HookType.AGGREGATION]: 'staticAggregationHook', + [HookType.PROTOCOL_FEE]: 'protocolFee', + [HookType.OP_STACK]: 'opStackHook', + [HookType.ROUTING]: 'domainRoutingHook', + [HookType.FALLBACK_ROUTING]: 'fallbackDomainRoutingHook', + [HookType.AMOUNT_ROUTING]: 'amountRoutingHook', + [HookType.PAUSABLE]: 'pausableHook', + [HookType.ARB_L2_TO_L1]: 'arbL2ToL1Hook', + [HookType.MAILBOX_DEFAULT]: 'defaultHook', + [HookType.CCIP]: 'ccipHook', +}; + export type MerkleTreeHookConfig = z.infer; export type IgpHookConfig = z.infer; export type ProtocolFeeHookConfig = z.infer; @@ -77,6 +97,8 @@ export type AmountRoutingHookConfig = { export type HookConfig = z.infer; +export type DerivedHookConfig = WithAddress>; + // Hook types that can be updated in-place export const MUTABLE_HOOK_TYPE = [ HookType.INTERCHAIN_GAS_PAYMASTER, diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index b47ca077f8d..5f0170779a8 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -180,7 +180,7 @@ export { ProtocolFeeHookConfig, ProtocolFeeSchema, } from './hook/types.js'; -export { DerivedIsmConfig, EvmIsmReader } from './ism/EvmIsmReader.js'; +export { EvmIsmReader } from './ism/EvmIsmReader.js'; export { HyperlaneIsmFactory } from './ism/HyperlaneIsmFactory.js'; export { BaseMetadataBuilder } from './ism/metadata/builder.js'; export { decodeIsmMetadata } from './ism/metadata/decode.js'; @@ -196,6 +196,7 @@ export { ArbL2ToL1IsmConfigSchema, DeployedIsm, DeployedIsmType, + DerivedIsmConfig, DomainRoutingIsmConfig, IcaRoutingIsmConfig, IsmConfig, @@ -217,7 +218,12 @@ export { WeightedMultisigIsmConfig, WeightedMultisigIsmConfigSchema, } from './ism/types.js'; -export { collectValidators, moduleCanCertainlyVerify } from './ism/utils.js'; +export { + collectValidators, + moduleCanCertainlyVerify, + isStaticDeploymentSupported, + isIsmCompatible, +} from './ism/utils.js'; export { AgentChainMetadata, AgentChainMetadataSchema, @@ -240,6 +246,9 @@ export { buildAgentConfig, GasPaymentEnforcement, GasPaymentEnforcementPolicyType, + IsmCacheConfig, + IsmCachePolicy, + IsmCacheSelectorType, RelayerConfig, RpcConsensusType, ScraperConfig, @@ -693,10 +702,6 @@ export { CCIPContractCache, } from './ccip/utils.js'; export { HyperlaneCCIPDeployer } from './ccip/HyperlaneCCIPDeployer.js'; - -export { MailboxClientConfigSchema as mailboxClientConfigSchema } from './router/types.js'; - export { StarknetCoreModule } from './core/StarknetCoreModule.js'; - export { StarknetERC20WarpModule } from './token/StarknetERC20WarpModule.js'; export { StarknetCore } from './core/StarknetCore.js'; diff --git a/typescript/sdk/src/ism/EvmIsmReader.ts b/typescript/sdk/src/ism/EvmIsmReader.ts index 1a91c1d726b..0b182fe4ac2 100644 --- a/typescript/sdk/src/ism/EvmIsmReader.ts +++ b/typescript/sdk/src/ism/EvmIsmReader.ts @@ -34,8 +34,8 @@ import { HyperlaneReader } from '../utils/HyperlaneReader.js'; import { AggregationIsmConfig, ArbL2ToL1IsmConfig, + DerivedIsmConfig, DomainRoutingIsmConfig, - IsmConfig, IsmType, ModuleType, MultisigIsmConfig, @@ -43,8 +43,6 @@ import { RoutingIsmConfig, } from './types.js'; -export type DerivedIsmConfig = WithAddress>; - export interface IsmReader { deriveIsmConfig(address: Address): Promise; deriveRoutingConfig(address: Address): Promise>; diff --git a/typescript/sdk/src/ism/metadata/aggregation.ts b/typescript/sdk/src/ism/metadata/aggregation.ts index 0f35e7e2cf4..4150362b2f5 100644 --- a/typescript/sdk/src/ism/metadata/aggregation.ts +++ b/typescript/sdk/src/ism/metadata/aggregation.ts @@ -7,8 +7,7 @@ import { toHexString, } from '@hyperlane-xyz/utils'; -import { DerivedIsmConfig } from '../EvmIsmReader.js'; -import { AggregationIsmConfig, IsmType } from '../types.js'; +import { AggregationIsmConfig, DerivedIsmConfig, IsmType } from '../types.js'; import type { BaseMetadataBuilder } from './builder.js'; import { decodeIsmMetadata } from './decode.js'; diff --git a/typescript/sdk/src/ism/metadata/routing.ts b/typescript/sdk/src/ism/metadata/routing.ts index a34c3364313..7a19e2f8c3c 100644 --- a/typescript/sdk/src/ism/metadata/routing.ts +++ b/typescript/sdk/src/ism/metadata/routing.ts @@ -5,8 +5,13 @@ import { import { Address, WithAddress, assert } from '@hyperlane-xyz/utils'; import { ChainName } from '../../types.js'; -import { DerivedIsmConfig, EvmIsmReader } from '../EvmIsmReader.js'; -import { DomainRoutingIsmConfig, IsmType, RoutingIsmConfig } from '../types.js'; +import { EvmIsmReader } from '../EvmIsmReader.js'; +import { + DerivedIsmConfig, + DomainRoutingIsmConfig, + IsmType, + RoutingIsmConfig, +} from '../types.js'; import type { BaseMetadataBuilder } from './builder.js'; import { decodeIsmMetadata } from './decode.js'; diff --git a/typescript/sdk/src/ism/metadata/types.ts b/typescript/sdk/src/ism/metadata/types.ts index 104ef5757d3..3fd669a0338 100644 --- a/typescript/sdk/src/ism/metadata/types.ts +++ b/typescript/sdk/src/ism/metadata/types.ts @@ -1,8 +1,8 @@ import type { providers } from 'ethers'; import type { DispatchedMessage } from '../../core/types.js'; -import type { DerivedHookConfig } from '../../hook/EvmHookReader.js'; -import type { DerivedIsmConfig } from '../EvmIsmReader.js'; +import type { DerivedHookConfig } from '../../hook/types.js'; +import type { DerivedIsmConfig } from '../types.js'; import type { AggregationMetadata } from './aggregation.js'; import type { ArbL2ToL1Metadata } from './arbL2ToL1.js'; diff --git a/typescript/sdk/src/ism/types.ts b/typescript/sdk/src/ism/types.ts index b56edb1f640..18b893bde5d 100644 --- a/typescript/sdk/src/ism/types.ts +++ b/typescript/sdk/src/ism/types.ts @@ -13,7 +13,12 @@ import { TestIsm, TrustedRelayerIsm, } from '@hyperlane-xyz/core'; -import type { Address, Domain, ValueOf } from '@hyperlane-xyz/utils'; +import type { + Address, + Domain, + ValueOf, + WithAddress, +} from '@hyperlane-xyz/utils'; import { ZHash } from '../metadata/customZodTypes.js'; import { @@ -72,6 +77,19 @@ export const MUTABLE_ISM_TYPE = [ IsmType.PAUSABLE, ]; +/** + * @notice Statically deployed ISM types + * @dev ISM types with immutable config embedded in contract bytecode via MetaProxy + */ +export const STATIC_ISM_TYPES = [ + IsmType.AGGREGATION, + IsmType.MERKLE_ROOT_MULTISIG, + IsmType.MESSAGE_ID_MULTISIG, + IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG, + IsmType.WEIGHTED_MESSAGE_ID_MULTISIG, + IsmType.ICA_ROUTING, +]; + // mapping between the two enums export function ismTypeToModuleType(ismType: IsmType): ModuleType { switch (ismType) { @@ -172,6 +190,8 @@ export type AggregationIsmConfig = { export type IsmConfig = z.infer; +export type DerivedIsmConfig = WithAddress>; + export type DeployedIsmType = { [IsmType.CUSTOM]: IInterchainSecurityModule; [IsmType.ROUTING]: IRoutingIsm; diff --git a/typescript/sdk/src/ism/utils.ts b/typescript/sdk/src/ism/utils.ts index 30486035498..b93db8858a8 100644 --- a/typescript/sdk/src/ism/utils.ts +++ b/typescript/sdk/src/ism/utils.ts @@ -27,6 +27,7 @@ import { import { getChainNameFromCCIPSelector } from '../ccip/utils.js'; import { HyperlaneContracts } from '../contracts/types.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; @@ -37,6 +38,7 @@ import { ModuleType, RoutingIsmConfig, RoutingIsmDelta, + STATIC_ISM_TYPES, ismTypeToModuleType, } from './types.js'; @@ -586,3 +588,36 @@ export function collectValidators( return new Set(validators); } + +/** + * Determines if static ISM deployment is supported on a given chain's technical stack + * @dev Currently, only ZkSync does not support static deployments + * @param chainTechnicalStack - The technical stack of the target chain + * @returns boolean - true if static deployment is supported, false for ZkSync + */ +export function isStaticDeploymentSupported( + chainTechnicalStack: ChainTechnicalStack | undefined, +): boolean { + return chainTechnicalStack !== ChainTechnicalStack.ZkSync; +} + +/** + * Checks if the given ISM type is compatible with the chain's technical stack. + * + * @param {Object} params - The parameters object + * @param {ChainTechnicalStack | undefined} params.chainTechnicalStack - The technical stack of the chain + * @param {IsmType} params.ismType - The type of Interchain Security Module (ISM) + * @returns {boolean} True if the ISM type is compatible with the chain, false otherwise + */ +export function isIsmCompatible({ + chainTechnicalStack, + ismType, +}: { + chainTechnicalStack: ChainTechnicalStack | undefined; + ismType: IsmType; +}): boolean { + // Skip compatibility check for non-static ISMs as they're always supported + if (!STATIC_ISM_TYPES.includes(ismType)) return true; + + return isStaticDeploymentSupported(chainTechnicalStack); +} diff --git a/typescript/sdk/src/metadata/agentConfig.ts b/typescript/sdk/src/metadata/agentConfig.ts index 58a80dc709e..6ca87dda348 100644 --- a/typescript/sdk/src/metadata/agentConfig.ts +++ b/typescript/sdk/src/metadata/agentConfig.ts @@ -4,6 +4,7 @@ */ import { z } from 'zod'; +import { ModuleType } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; import { MultiProvider } from '../providers/MultiProvider.js'; @@ -349,6 +350,45 @@ const MetricAppContextSchema = z.object({ ), }); +export enum IsmCachePolicy { + MessageSpecific = 'messageSpecific', + IsmSpecific = 'ismSpecific', +} + +export enum IsmCacheSelectorType { + DefaultIsm = 'defaultIsm', + AppContext = 'appContext', +} + +const IsmCacheSelector = z.discriminatedUnion('type', [ + z.object({ + type: z.literal(IsmCacheSelectorType.DefaultIsm), + }), + z.object({ + type: z.literal(IsmCacheSelectorType.AppContext), + context: z.string(), + }), +]); + +const IsmCacheConfigSchema = z.object({ + selector: IsmCacheSelector.describe( + 'The selector to use for the ISM cache policy', + ), + moduleTypes: z + .array(z.nativeEnum(ModuleType)) + .describe('The ISM module types to use the cache policy for.'), + chains: z + .array(z.string()) + .optional() + .describe( + 'The chains to use the cache policy for. If not specified, all chains will be used.', + ), + cachePolicy: z + .nativeEnum(IsmCachePolicy) + .describe('The cache policy to use.'), +}); +export type IsmCacheConfig = z.infer; + export const RelayerAgentConfigSchema = AgentConfigSchema.extend({ db: z .string() @@ -398,6 +438,28 @@ export const RelayerAgentConfigSchema = AgentConfigSchema.extend({ .describe( 'A list of app contexts and their matching lists to use for metrics. A message will be classified as the first matching app context.', ), + ismCacheConfigs: z + .union([z.array(IsmCacheConfigSchema), z.string().min(1)]) + .optional() + .describe( + 'The ISM cache configs to be used. If not specified, default caching will be used.', + ), + allowContractCallCaching: z + .boolean() + .optional() + .describe( + 'If true, allows caching of certain contract calls that can be appropriately cached.', + ), + txIdIndexingEnabled: z + .boolean() + .optional() + .describe( + 'Whether to enable TX ID based indexing for hook events given indexed messages', + ), + igpIndexingEnabled: z + .boolean() + .optional() + .describe('Whether to enable IGP indexing'), }); export type RelayerConfig = z.infer; diff --git a/typescript/sdk/src/metadata/chainMetadataConversion.ts b/typescript/sdk/src/metadata/chainMetadataConversion.ts index cbe50446a10..15cd34ee466 100644 --- a/typescript/sdk/src/metadata/chainMetadataConversion.ts +++ b/typescript/sdk/src/metadata/chainMetadataConversion.ts @@ -12,14 +12,15 @@ import { import { PROTOCOL_TO_DEFAULT_NATIVE_TOKEN } from '../token/nativeTokenMetadata.js'; export function chainMetadataToViemChain(metadata: ChainMetadata): Chain { + const rpcUrls = metadata.rpcUrls.map((rpcUrl) => rpcUrl.http); return defineChain({ id: getChainIdNumber(metadata), name: metadata.displayName || metadata.name, network: metadata.name, nativeCurrency: metadata.nativeToken || test1.nativeToken!, rpcUrls: { - public: { http: [metadata.rpcUrls[0].http] }, - default: { http: [metadata.rpcUrls[0].http] }, + public: { http: rpcUrls }, + default: { http: rpcUrls }, }, blockExplorers: metadata.blockExplorers?.length ? { diff --git a/typescript/sdk/src/metadata/chainMetadataTypes.ts b/typescript/sdk/src/metadata/chainMetadataTypes.ts index 1b2db06195f..76d9415eff3 100644 --- a/typescript/sdk/src/metadata/chainMetadataTypes.ts +++ b/typescript/sdk/src/metadata/chainMetadataTypes.ts @@ -23,6 +23,7 @@ export enum ExplorerFamily { Blockscout = 'blockscout', Routescan = 'routescan', Voyager = 'voyager', + ZkSync = 'zksync', Other = 'other', } @@ -95,6 +96,10 @@ export const RpcUrlSchema = z.object({ .describe( 'Default retry settings to be used by a provider such as MultiProvider.', ), + public: z + .boolean() + .optional() + .describe('Flag if the RPC is publicly available.'), }); export type RpcUrl = z.infer; @@ -194,6 +199,11 @@ export const ChainMetadataSchemaObject = z.object({ .optional() .describe('Block settings for the chain/deployment.'), + bypassBatchSimulation: z + .boolean() + .optional() + .describe('Whether to bypass batch simulation for this chain.'), + chainId: z .union([ZNzUint, z.string()]) .describe(`The chainId of the chain. Uses EIP-155 for EVM chains`), diff --git a/typescript/sdk/src/providers/ProviderType.ts b/typescript/sdk/src/providers/ProviderType.ts index d19347dec15..83dc465fe53 100644 --- a/typescript/sdk/src/providers/ProviderType.ts +++ b/typescript/sdk/src/providers/ProviderType.ts @@ -28,6 +28,11 @@ import type { Transaction as VTransaction, TransactionReceipt as VTransactionReceipt, } from 'viem'; +import { + Contract as ZKSyncBaseContract, + Provider as ZKSyncBaseProvider, + types as zkSyncTypes, +} from 'zksync-ethers'; import { Annotated, ProtocolType } from '@hyperlane-xyz/utils'; @@ -39,6 +44,7 @@ export enum ProviderType { CosmJsWasm = 'cosmjs-wasm', GnosisTxBuilder = 'gnosis-txBuilder', Starknet = 'starknet', + ZkSync = 'zksync', } export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< @@ -152,6 +158,11 @@ export interface StarknetJsProvider provider: StarknetProvider; } +export interface ZKSyncProvider extends TypedProviderBase { + type: ProviderType.ZkSync; + provider: ZKSyncBaseProvider; +} + export type TypedProvider = | EthersV5Provider // | EthersV6Provider @@ -159,7 +170,8 @@ export type TypedProvider = | SolanaWeb3Provider | CosmJsProvider | CosmJsWasmProvider - | StarknetJsProvider; + | StarknetJsProvider + | ZKSyncProvider; /** * Contracts with discriminated union of provider type @@ -204,6 +216,11 @@ export interface StarknetJsContract contract: StarknetContract; } +export interface ZKSyncContract extends TypedContractBase { + type: ProviderType.ZkSync; + contract: ZKSyncBaseContract; +} + export type TypedContract = | EthersV5Contract // | EthersV6Contract @@ -211,7 +228,8 @@ export type TypedContract = | SolanaWeb3Contract | CosmJsContract | CosmJsWasmContract - | StarknetJsContract; + | StarknetJsContract + | ZKSyncBaseContract; /** * Transactions with discriminated union of provider type @@ -258,6 +276,12 @@ export interface StarknetJsTransaction transaction: StarknetInvocation; } +export interface ZKSyncTransaction + extends TypedTransactionBase { + type: ProviderType.ZkSync; + transaction: zkSyncTypes.TransactionRequest; +} + export type TypedTransaction = | EthersV5Transaction // | EthersV6Transaction @@ -265,7 +289,8 @@ export type TypedTransaction = | SolanaWeb3Transaction | CosmJsTransaction | CosmJsWasmTransaction - | StarknetJsTransaction; + | StarknetJsTransaction + | ZKSyncTransaction; /** * Transaction receipt/response with discriminated union of provider type @@ -312,10 +337,17 @@ export interface StarknetJsTransactionReceipt receipt: StarknetTxReceipt | StarknetReceiptTx; } +export interface ZKSyncTransactionReceipt + extends TypedTransactionReceiptBase { + type: ProviderType.ZkSync; + receipt: zkSyncTypes.TransactionReceipt; +} + export type TypedTransactionReceipt = | EthersV5TransactionReceipt | ViemTransactionReceipt | SolanaWeb3TransactionReceipt | CosmJsTransactionReceipt | CosmJsWasmTransactionReceipt - | StarknetJsTransactionReceipt; + | StarknetJsTransactionReceipt + | ZKSyncTransactionReceipt; diff --git a/typescript/sdk/src/providers/providerBuilders.ts b/typescript/sdk/src/providers/providerBuilders.ts index ea3395e49d5..74107faafd5 100644 --- a/typescript/sdk/src/providers/providerBuilders.ts +++ b/typescript/sdk/src/providers/providerBuilders.ts @@ -4,8 +4,9 @@ import { Connection } from '@solana/web3.js'; import { providers } from 'ethers'; import { RpcProvider as StarknetRpcProvider } from 'starknet'; import { createPublicClient, http } from 'viem'; +import { Provider as ZKProvider } from 'zksync-ethers'; -import { ProtocolType, isNumeric } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert, isNumeric } from '@hyperlane-xyz/utils'; import { ChainMetadata, RpcUrl } from '../metadata/chainMetadataTypes.js'; @@ -18,6 +19,7 @@ import { StarknetJsProvider, TypedProvider, ViemProvider, + ZKSyncProvider, } from './ProviderType.js'; import { HyperlaneSmartProvider } from './SmartProvider/SmartProvider.js'; import { ProviderRetryOptions } from './SmartProvider/types.js'; @@ -120,6 +122,16 @@ export function defaultStarknetJsProviderBuilder( return { provider, type: ProviderType.Starknet }; } +export function defaultZKSyncProviderBuilder( + rpcUrls: RpcUrl[], + network: providers.Networkish, +): ZKSyncProvider { + assert(rpcUrls.length, 'No RPC URLs provided'); + const url = rpcUrls[0].http; + const provider = new ZKProvider(url, network); + return { type: ProviderType.ZkSync, provider }; +} + // Kept for backwards compatibility export function defaultProviderBuilder( rpcUrls: RpcUrl[], @@ -128,6 +140,13 @@ export function defaultProviderBuilder( return defaultEthersV5ProviderBuilder(rpcUrls, _network).provider; } +export function defaultZKProviderBuilder( + rpcUrls: RpcUrl[], + _network: number | string, +): ZKProvider { + return defaultZKSyncProviderBuilder(rpcUrls, _network).provider; +} + export type ProviderBuilderMap = Record< ProviderType, ProviderBuilderFn @@ -140,6 +159,7 @@ export const defaultProviderBuilderMap: ProviderBuilderMap = { [ProviderType.CosmJs]: defaultCosmJsProviderBuilder, [ProviderType.CosmJsWasm]: defaultCosmJsWasmProviderBuilder, [ProviderType.Starknet]: defaultStarknetJsProviderBuilder, + [ProviderType.ZkSync]: defaultZKSyncProviderBuilder, }; export const protocolToDefaultProviderBuilder: Record< diff --git a/typescript/sdk/src/router/HyperlaneRouterChecker.ts b/typescript/sdk/src/router/HyperlaneRouterChecker.ts index a3b015fc138..1210c709ceb 100644 --- a/typescript/sdk/src/router/HyperlaneRouterChecker.ts +++ b/typescript/sdk/src/router/HyperlaneRouterChecker.ts @@ -10,8 +10,9 @@ import { import { HyperlaneFactories } from '../contracts/types.js'; import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker.js'; -import { DerivedIsmConfig, EvmIsmReader } from '../ism/EvmIsmReader.js'; +import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; +import { DerivedIsmConfig } from '../ism/types.js'; import { moduleMatchesConfig } from '../ism/utils.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap, ChainName } from '../types.js'; @@ -20,6 +21,7 @@ import { RouterApp } from './RouterApps.js'; import { ClientViolation, ClientViolationType, + MissingEnrolledRouterViolation, MissingRouterViolation, RouterConfig, RouterViolation, @@ -127,10 +129,17 @@ export class HyperlaneRouterChecker< async checkEnrolledRouters(chain: ChainName): Promise { const router = this.app.router(this.app.getContracts(chain)); const actualRemoteChains = await this.app.remoteChains(chain); + const expectedRemoteChains = this.app + .chains() + .filter((c) => c !== chain) + .sort(); const currentRouters: ChainMap = {}; const expectedRouters: ChainMap = {}; + const missingRemoteChains = expectedRemoteChains + .filter((chn) => !actualRemoteChains.includes(chn)) + .sort(); const misconfiguredRouterDiff: ChainMap<{ actual: AddressBytes32; expected: AddressBytes32; @@ -167,6 +176,19 @@ export class HyperlaneRouterChecker< (chain) => !missingRouterDomains.includes(chain), ); + if (missingRemoteChains.length > 0) { + const violation: MissingEnrolledRouterViolation = { + chain, + type: RouterViolationType.MissingEnrolledRouter, + contract: router, + actual: actualRemoteChains.join(', '), + expected: expectedRemoteChains.join(), + missingChains: missingRemoteChains, + description: `Routers for some domains are missing from the router`, + }; + this.addViolation(violation); + } + if (Object.keys(misconfiguredRouterDiff).length > 0) { const violation: RouterViolation = { chain, diff --git a/typescript/sdk/src/router/ProxiedRouterChecker.ts b/typescript/sdk/src/router/ProxiedRouterChecker.ts index a297329ea43..5ae5c16c4b4 100644 --- a/typescript/sdk/src/router/ProxiedRouterChecker.ts +++ b/typescript/sdk/src/router/ProxiedRouterChecker.ts @@ -13,11 +13,17 @@ export abstract class ProxiedRouterChecker< getOwnableOverrides(chain: ChainName): AddressesMap | undefined { const config = this.configMap[chain]; let ownableOverrides = config?.ownerOverrides; + // timelock and proxyAdmin are mutally exclusive if (config?.timelock) { ownableOverrides = { ...ownableOverrides, proxyAdmin: this.app.getAddresses(chain).timelockController, }; + } else if (config?.proxyAdmin) { + ownableOverrides = { + ...ownableOverrides, + proxyAdmin: config.proxyAdmin.owner, + }; } return ownableOverrides; } diff --git a/typescript/sdk/src/router/types.ts b/typescript/sdk/src/router/types.ts index 1219498c58e..1db558f877e 100644 --- a/typescript/sdk/src/router/types.ts +++ b/typescript/sdk/src/router/types.ts @@ -11,8 +11,8 @@ import { Address, AddressBytes32 } from '@hyperlane-xyz/utils'; import { HyperlaneFactories } from '../contracts/types.js'; import { UpgradeConfig } from '../deploy/proxy.js'; import { CheckerViolation } from '../deploy/types.js'; -import { HookConfigSchema } from '../hook/types.js'; -import { IsmConfigSchema } from '../ism/types.js'; +import { DerivedHookConfig, HookConfigSchema } from '../hook/types.js'; +import { DerivedIsmConfig, IsmConfigSchema } from '../ism/types.js'; import { ZHash } from '../metadata/customZodTypes.js'; import { ChainMap, DeployedOwnableSchema, OwnableSchema } from '../types.js'; @@ -21,6 +21,18 @@ export type RouterAddress = { }; export type MailboxClientConfig = z.infer; + +export type DerivedMailboxClientFields = { + hook: string | DerivedHookConfig; + interchainSecurityModule: string | DerivedIsmConfig; +}; + +export type DerivedMailboxClientConfig = Omit< + MailboxClientConfig, + keyof DerivedMailboxClientFields +> & + DerivedMailboxClientFields; + export type RouterConfig = z.infer; export type GasRouterConfig = z.infer; @@ -49,6 +61,7 @@ export interface ClientViolation extends CheckerViolation { export enum RouterViolationType { MisconfiguredEnrolledRouter = 'MisconfiguredEnrolledRouter', + MissingEnrolledRouter = 'MissingEnrolledRouter', MissingRouter = 'MissingRouter', } @@ -62,6 +75,13 @@ export interface RouterViolation extends CheckerViolation { description?: string; } +export interface MissingEnrolledRouterViolation extends CheckerViolation { + type: RouterViolationType.MissingEnrolledRouter; + contract: Router; + missingChains: string[]; + description?: string; +} + export interface MissingRouterViolation extends CheckerViolation { type: RouterViolationType.MissingRouter; contract: Router; diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts index e8be6762020..f351af43f0c 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -33,7 +33,6 @@ import { TestCoreApp } from '../core/TestCoreApp.js'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; -import { DerivedHookConfig } from '../hook/EvmHookReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; @@ -44,7 +43,7 @@ import { normalizeConfig } from '../utils/ism.js'; import { EvmERC20WarpModule } from './EvmERC20WarpModule.js'; import { TokenType } from './config.js'; -import { HypTokenRouterConfig } from './types.js'; +import { HypTokenRouterConfig, derivedHookAddress } from './types.js'; const randomRemoteRouters = (n: number) => { const routers: RemoteRouters = {}; @@ -387,9 +386,8 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); - const updatedConfig = (await evmERC20WarpModule.read()) - .hook as DerivedHookConfig; - expect(normalizeConfig(updatedConfig)).to.deep.equal(hook); + const updatedConfig = await evmERC20WarpModule.read(); + expect(normalizeConfig(updatedConfig.hook)).to.deep.equal(hook); } }); @@ -415,11 +413,10 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); - const updatedConfig = (await evmERC20WarpModule.read()) - .hook as DerivedHookConfig; + const updatedConfig = await evmERC20WarpModule.read(); const hook = MailboxClient__factory.connect( - updatedConfig.address, + derivedHookAddress(updatedConfig), multiProvider.getProvider(chain), ); expect(await hook.mailbox()).to.equal(expectedConfig.mailbox); @@ -447,13 +444,12 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); - const updatedConfig = (await evmERC20WarpModule.read()) - .hook as DerivedHookConfig; + const updatedConfig = await evmERC20WarpModule.read(); expect( await proxyAdmin( multiProvider.getProvider(chain), - updatedConfig.address, + derivedHookAddress(updatedConfig), ), ).to.equal(expectedConfig.proxyAdmin?.address); }); diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.ts b/typescript/sdk/src/token/EvmERC20WarpModule.ts index 6ec5c2b9b90..487edebd18c 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.ts @@ -34,9 +34,7 @@ import { proxyAdminUpdateTxs } from '../deploy/proxy.js'; import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; import { ExplorerLicenseType } from '../deploy/verify/types.js'; import { EvmHookModule } from '../hook/EvmHookModule.js'; -import { DerivedHookConfig } from '../hook/EvmHookReader.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js'; -import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; import { ChainName, ChainNameOrId } from '../types.js'; @@ -44,7 +42,13 @@ import { extractIsmAndHookFactoryAddresses } from '../utils/ism.js'; import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js'; import { HypERC20Deployer } from './deploy.js'; -import { HypTokenRouterConfig, HypTokenRouterConfigSchema } from './types.js'; +import { + DerivedTokenRouterConfig, + HypTokenRouterConfig, + HypTokenRouterConfigSchema, + derivedHookAddress, + derivedIsmAddress, +} from './types.js'; type WarpRouteAddresses = HyperlaneAddresses & { deployedTokenRoute: Address; @@ -88,7 +92,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @param address - The address to derive the token router configuration from. * @returns A promise that resolves to the token router configuration. */ - async read(): Promise { + async read(): Promise { return this.reader.deriveWarpRouteConfig( this.args.addresses.deployedTokenRoute, ); @@ -143,7 +147,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @returns A array with a single Ethereum transaction that need to be executed to enroll the routers */ createEnrollRemoteRoutersUpdateTxs( - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, expectedConfig: HypTokenRouterConfig, ): AnnotatedEV5Transaction[] { const updateTransactions: AnnotatedEV5Transaction[] = []; @@ -196,7 +200,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< } createUnenrollRemoteRoutersUpdateTxs( - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, expectedConfig: HypTokenRouterConfig, ): AnnotatedEV5Transaction[] { const updateTransactions: AnnotatedEV5Transaction[] = []; @@ -247,7 +251,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @returns A array with a single Ethereum transaction that need to be executed to enroll the routers */ createSetDestinationGasUpdateTxs( - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, expectedConfig: HypTokenRouterConfig, ): AnnotatedEV5Transaction[] { const updateTransactions: AnnotatedEV5Transaction[] = []; @@ -298,7 +302,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @returns Ethereum transaction that need to be executed to update the ISM configuration. */ async createIsmUpdateTxs( - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, expectedConfig: HypTokenRouterConfig, ): Promise { const updateTransactions: AnnotatedEV5Transaction[] = []; @@ -309,9 +313,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< return []; } - const actualDeployedIsm = ( - actualConfig.interchainSecurityModule as DerivedIsmConfig - ).address; + const actualDeployedIsm = derivedIsmAddress(actualConfig); // Try to update (may also deploy) Ism with the expected config const { @@ -343,7 +345,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< } async createHookUpdateTxs( - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, expectedConfig: HypTokenRouterConfig, ): Promise { const updateTransactions: AnnotatedEV5Transaction[] = []; @@ -352,8 +354,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< return []; } - const actualDeployedHook = (actualConfig.hook as DerivedHookConfig) - ?.address; + const actualDeployedHook = derivedHookAddress(actualConfig); // Try to deploy or update Hook with the expected config const { @@ -391,7 +392,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @returns Ethereum transaction that need to be executed to update the owner. */ createOwnershipUpdateTxs( - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, expectedConfig: HypTokenRouterConfig, ): AnnotatedEV5Transaction[] { return transferOwnershipTransactions( @@ -409,7 +410,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @returns Object with deployedIsm address, and update Transactions */ async deployOrUpdateIsm( - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, expectedConfig: HypTokenRouterConfig, ): Promise<{ deployedIsm: Address; @@ -425,9 +426,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< addresses: { ...this.args.addresses, mailbox: expectedConfig.mailbox, - deployedIsm: ( - actualConfig.interchainSecurityModule as DerivedIsmConfig - ).address, + deployedIsm: derivedIsmAddress(actualConfig), }, }, this.ccipContractCache, @@ -450,7 +449,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @returns Object with deployedHook address, and update Transactions */ async deployOrUpdateHook( - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, expectedConfig: HypTokenRouterConfig, ): Promise<{ deployedHook: Address; @@ -497,7 +496,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< async updateExistingHook( expectedConfig: HypTokenRouterConfig, - actualConfig: HypTokenRouterConfig, + actualConfig: DerivedTokenRouterConfig, ): Promise<{ deployedHook: Address; updateTransactions: AnnotatedEV5Transaction[]; @@ -514,7 +513,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< ...extractIsmAndHookFactoryAddresses(this.args.addresses), mailbox: actualConfig.mailbox, proxyAdmin: actualConfig.proxyAdmin?.address, - deployedHook: (actualConfig.hook as DerivedHookConfig).address, + deployedHook: derivedHookAddress(actualConfig), }, }, this.ccipContractCache, diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts index 999cf9ad6e1..98d5e802c4f 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts @@ -26,7 +26,6 @@ import { TestCoreApp } from '../core/TestCoreApp.js'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; -import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap } from '../types.js'; @@ -34,6 +33,7 @@ import { ChainMap } from '../types.js'; import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js'; import { TokenType } from './config.js'; import { HypERC20Deployer } from './deploy.js'; +import { derivedIsmAddress } from './types.js'; describe('ERC20WarpRouterReader', async () => { const TOKEN_NAME = 'fake'; @@ -155,9 +155,9 @@ describe('ERC20WarpRouterReader', async () => { ), ); // Check ism - expect( - (derivedConfig.interchainSecurityModule as DerivedIsmConfig).address, - ).to.be.equal(await mailbox.defaultIsm()); + expect(derivedIsmAddress(derivedConfig)).to.be.equal( + await mailbox.defaultIsm(), + ); // Check if token values matches if (derivedConfig.type === TokenType.collateral) { @@ -205,9 +205,9 @@ describe('ERC20WarpRouterReader', async () => { ), ); // Check ism - expect( - (derivedConfig.interchainSecurityModule as DerivedIsmConfig).address, - ).to.be.equal(await mailbox.defaultIsm()); + expect(derivedIsmAddress(derivedConfig)).to.be.equal( + await mailbox.defaultIsm(), + ); // Check if token values matches if (derivedConfig.type === TokenType.XERC20) { @@ -255,9 +255,9 @@ describe('ERC20WarpRouterReader', async () => { ), ); // Check ism - expect( - (derivedConfig.interchainSecurityModule as DerivedIsmConfig).address, - ).to.be.equal(await mailbox.defaultIsm()); + expect(derivedIsmAddress(derivedConfig)).to.be.equal( + await mailbox.defaultIsm(), + ); // Check if token values matches if (derivedConfig.type === TokenType.XERC20) { diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index 1e232098c70..7a3b136f451 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -24,8 +24,8 @@ import { EvmHookReader } from '../hook/EvmHookReader.js'; import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { + DerivedMailboxClientConfig, DestinationGas, - MailboxClientConfig, RemoteRouters, RemoteRoutersSchema, } from '../router/types.js'; @@ -35,8 +35,8 @@ import { HyperlaneReader } from '../utils/HyperlaneReader.js'; import { proxyAdmin } from './../deploy/proxy.js'; import { NON_ZERO_SENDER_ADDRESS, TokenType } from './config.js'; import { + DerivedTokenRouterConfig, HypTokenConfig, - HypTokenRouterConfig, TokenMetadata, XERC20TokenMetadata, } from './types.js'; @@ -68,23 +68,24 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { */ async deriveWarpRouteConfig( warpRouteAddress: Address, - ): Promise { + ): Promise { // Derive the config type const type = await this.deriveTokenType(warpRouteAddress); - const baseMetadata = await this.fetchMailboxClientConfig(warpRouteAddress); + const mailboxClientConfig = await this.fetchMailboxClientConfig( + warpRouteAddress, + ); const tokenConfig = await this.fetchTokenConfig(type, warpRouteAddress); const remoteRouters = await this.fetchRemoteRouters(warpRouteAddress); const proxyAdmin = await this.fetchProxyAdminConfig(warpRouteAddress); const destinationGas = await this.fetchDestinationGas(warpRouteAddress); return { - ...baseMetadata, + ...mailboxClientConfig, ...tokenConfig, remoteRouters, proxyAdmin, destinationGas, - type, - } as HypTokenRouterConfig; + }; } /** @@ -178,7 +179,7 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { */ async fetchMailboxClientConfig( routerAddress: Address, - ): Promise { + ): Promise { const warpRoute = HypERC20Collateral__factory.connect( routerAddress, this.provider, diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index b9b24cc8fc8..e5f26d4abf5 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -215,7 +215,6 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'TIA.n', name: 'TIA.n', }, - [TokenStandard.CwHypSynthetic]: null, //TODO: check this and manage it. diff --git a/typescript/sdk/src/token/Token.ts b/typescript/sdk/src/token/Token.ts index b4119201d61..3c5f3a55363 100644 --- a/typescript/sdk/src/token/Token.ts +++ b/typescript/sdk/src/token/Token.ts @@ -42,7 +42,9 @@ import { EvmHypCollateralAdapter, EvmHypCollateralFiatAdapter, EvmHypNativeAdapter, + EvmHypRebaseCollateralAdapter, EvmHypSyntheticAdapter, + EvmHypSyntheticRebaseAdapter, EvmHypXERC20Adapter, EvmHypXERC20LockboxAdapter, EvmNativeTokenAdapter, @@ -190,23 +192,27 @@ export class Token implements IToken { }); } else if ( standard === TokenStandard.EvmHypCollateral || - standard === TokenStandard.EvmHypOwnerCollateral || - standard === TokenStandard.EvmHypRebaseCollateral + standard === TokenStandard.EvmHypOwnerCollateral ) { return new EvmHypCollateralAdapter(chainName, multiProvider, { token: addressOrDenom, }); + } else if (standard === TokenStandard.EvmHypRebaseCollateral) { + return new EvmHypRebaseCollateralAdapter(chainName, multiProvider, { + token: addressOrDenom, + }); } else if (standard === TokenStandard.EvmHypCollateralFiat) { return new EvmHypCollateralFiatAdapter(chainName, multiProvider, { token: addressOrDenom, }); - } else if ( - standard === TokenStandard.EvmHypSynthetic || - standard === TokenStandard.EvmHypSyntheticRebase - ) { + } else if (standard === TokenStandard.EvmHypSynthetic) { return new EvmHypSyntheticAdapter(chainName, multiProvider, { token: addressOrDenom, }); + } else if (standard === TokenStandard.EvmHypSyntheticRebase) { + return new EvmHypSyntheticRebaseAdapter(chainName, multiProvider, { + token: addressOrDenom, + }); } else if ( standard === TokenStandard.EvmHypXERC20 || standard === TokenStandard.EvmHypVSXERC20 diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 6872098d508..6f1efec3d9b 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -186,11 +186,9 @@ export const TOKEN_TYPE_TO_STANDARD: Record = { [TokenType.collateralVault]: TokenStandard.EvmHypOwnerCollateral, [TokenType.collateralVaultRebase]: TokenStandard.EvmHypRebaseCollateral, [TokenType.collateralUri]: TokenStandard.EvmHypCollateral, - [TokenType.fastCollateral]: TokenStandard.EvmHypCollateral, [TokenType.synthetic]: TokenStandard.EvmHypSynthetic, [TokenType.syntheticRebase]: TokenStandard.EvmHypSyntheticRebase, [TokenType.syntheticUri]: TokenStandard.EvmHypSynthetic, - [TokenType.fastSynthetic]: TokenStandard.EvmHypSynthetic, [TokenType.nativeScaled]: TokenStandard.EvmHypNative, }; diff --git a/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts b/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts index c1a58487ec1..bbd6a78c386 100644 --- a/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts @@ -72,6 +72,13 @@ export class CwNativeTokenAdapter return false; } + async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } + async populateApproveTx( _params: TransferParams, ): Promise { @@ -154,6 +161,13 @@ export class CwTokenAdapter return false; } + async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } + async populateApproveTx({ weiAmountOrId, recipient, @@ -460,4 +474,11 @@ export class CwHypCollateralAdapter ) { super(chainName, multiProvider, addresses); } + + async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } } diff --git a/typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts b/typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts index 0983e2b45db..4cd8def5c06 100644 --- a/typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts @@ -54,6 +54,13 @@ export class CosmNativeTokenAdapter return false; } + async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } + populateApproveTx( _transferParams: TransferParams, ): Promise { diff --git a/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts b/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts index e971d35dbfe..34bf24801b7 100644 --- a/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts @@ -3,10 +3,15 @@ import { BigNumber, PopulatedTransaction } from 'ethers'; import { ERC20, ERC20__factory, + ERC4626__factory, HypERC20, HypERC20Collateral, HypERC20Collateral__factory, HypERC20__factory, + HypERC4626, + HypERC4626Collateral, + HypERC4626Collateral__factory, + HypERC4626__factory, HypXERC20, HypXERC20Lockbox, HypXERC20Lockbox__factory, @@ -74,6 +79,13 @@ export class EvmNativeTokenAdapter return false; } + async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } + async populateApproveTx( _params: TransferParams, ): Promise { @@ -137,6 +149,15 @@ export class EvmTokenAdapter return allowance.lt(weiAmountOrId); } + async isRevokeApprovalRequired( + owner: Address, + spender: Address, + ): Promise { + const allowance = await this.contract.allowance(owner, spender); + + return !allowance.isZero(); + } + override populateApproveTx({ weiAmountOrId, recipient, @@ -185,6 +206,13 @@ export class EvmHypSyntheticAdapter return false; } + async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } + getDomains(): Promise { return this.contract.domains(); } @@ -293,6 +321,15 @@ export class EvmHypCollateralAdapter ); } + override async isRevokeApprovalRequired( + owner: Address, + spender: Address, + ): Promise { + const collateral = await this.getWrappedTokenAdapter(); + + return collateral.isRevokeApprovalRequired(owner, spender); + } + override populateApproveTx( params: TransferParams, ): Promise { @@ -325,6 +362,54 @@ export class EvmHypCollateralFiatAdapter } } +export class EvmHypRebaseCollateralAdapter + extends EvmHypCollateralAdapter + implements IHypTokenAdapter +{ + public override collateralContract: HypERC4626Collateral; + + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { token: Address }, + ) { + super(chainName, multiProvider, addresses); + this.collateralContract = HypERC4626Collateral__factory.connect( + addresses.token, + this.getProvider(), + ); + } + + override async getBridgedSupply(): Promise { + const vault = ERC4626__factory.connect( + await this.collateralContract.vault(), + this.getProvider(), + ); + const balance = await vault.balanceOf(this.addresses.token); + return balance.toBigInt(); + } +} + +export class EvmHypSyntheticRebaseAdapter + extends EvmHypSyntheticAdapter + implements IHypTokenAdapter +{ + public declare contract: HypERC4626; + + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { token: Address }, + ) { + super(chainName, multiProvider, addresses, HypERC4626__factory); + } + + override async getBridgedSupply(): Promise { + const totalShares = await this.contract.totalShares(); + return totalShares.toBigInt(); + } +} + abstract class BaseEvmHypXERC20Adapter extends EvmHypCollateralAdapter implements IHypXERC20Adapter @@ -574,6 +659,13 @@ export class EvmHypNativeAdapter return false; } + override async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } + override async populateTransferRemoteTx({ weiAmountOrId, destination, diff --git a/typescript/sdk/src/token/adapters/ITokenAdapter.ts b/typescript/sdk/src/token/adapters/ITokenAdapter.ts index c892ab9f04a..95986b6922e 100644 --- a/typescript/sdk/src/token/adapters/ITokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/ITokenAdapter.ts @@ -39,6 +39,7 @@ export interface ITokenAdapter { spender: Address, weiAmountOrId: Numberish, ): Promise; + isRevokeApprovalRequired(owner: Address, spender: Address): Promise; populateApproveTx(params: TransferParams): Promise; populateTransferTx(params: TransferParams): Promise; } diff --git a/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts b/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts index eb7757054d1..d0f11bef727 100644 --- a/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts @@ -118,6 +118,13 @@ export class SealevelNativeTokenAdapter return false; } + async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } + async populateApproveTx(): Promise { throw new Error('Approve not required for native tokens'); } @@ -188,6 +195,13 @@ export class SealevelTokenAdapter return false; } + async isRevokeApprovalRequired( + _owner: Address, + _spender: Address, + ): Promise { + return false; + } + populateApproveTx(_params: TransferParams): Promise { throw new Error('Approve not required for sealevel tokens'); } diff --git a/typescript/sdk/src/token/checker.ts b/typescript/sdk/src/token/checker.ts index 88407cdc669..a5716b5cab9 100644 --- a/typescript/sdk/src/token/checker.ts +++ b/typescript/sdk/src/token/checker.ts @@ -43,17 +43,24 @@ export class HypERC20Checker extends ProxiedRouterChecker< async ownables(chain: ChainName): Promise<{ [key: string]: Ownable }> { const contracts = this.app.getContracts(chain); + const expectedConfig = this.configMap[chain]; + + // This is used to trigger checks for collateralProxyAdmin or collateralToken + const hasCollateralProxyOverrides = + expectedConfig.ownerOverrides?.collateralProxyAdmin || + expectedConfig.ownerOverrides?.collateralToken; if ( - isCollateralTokenConfig(this.configMap[chain]) || - isXERC20TokenConfig(this.configMap[chain]) + (isCollateralTokenConfig(this.configMap[chain]) || + isXERC20TokenConfig(this.configMap[chain])) && + hasCollateralProxyOverrides ) { let collateralToken = await this.getCollateralToken(chain); const provider = this.multiProvider.getProvider(chain); // XERC20s are Ownable - const expectedConfig = this.configMap[chain]; + if (expectedConfig.type === TokenType.XERC20Lockbox) { const lockbox = IXERC20Lockbox__factory.connect( expectedConfig.token, @@ -75,7 +82,6 @@ export class HypERC20Checker extends ProxiedRouterChecker< provider, ); } - if (await isProxy(provider, collateralToken.address)) { const admin = await proxyAdmin(provider, collateralToken.address); contracts['collateralProxyAdmin'] = ProxyAdmin__factory.connect( diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index 3fe7f963b2d..9ba18df6174 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -1,7 +1,6 @@ export enum TokenType { synthetic = 'synthetic', syntheticRebase = 'syntheticRebase', - fastSynthetic = 'fastSynthetic', syntheticUri = 'syntheticUri', collateral = 'collateral', collateralVault = 'collateralVault', @@ -9,7 +8,6 @@ export enum TokenType { XERC20 = 'xERC20', XERC20Lockbox = 'xERC20Lockbox', collateralFiat = 'collateralFiat', - fastCollateral = 'fastCollateral', collateralUri = 'collateralUri', native = 'native', // backwards compatible alias to native @@ -18,7 +16,6 @@ export enum TokenType { export const gasOverhead = (tokenType: TokenType): number => { switch (tokenType) { - case TokenType.fastSynthetic: case TokenType.synthetic: return 64_000; case TokenType.native: diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index 0fbba10ae7e..a50740e66d2 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -1,6 +1,4 @@ import { - FastHypERC20Collateral__factory, - FastHypERC20__factory, HypERC20Collateral__factory, HypERC20__factory, HypERC721Collateral__factory, @@ -19,8 +17,6 @@ import { import { TokenType } from './config.js'; export const hypERC20contracts = { - [TokenType.fastCollateral]: 'FastHypERC20Collateral', - [TokenType.fastSynthetic]: 'FastHypERC20', [TokenType.synthetic]: 'HypERC20', [TokenType.syntheticRebase]: 'HypERC4626', [TokenType.collateral]: 'HypERC20Collateral', @@ -36,8 +32,6 @@ export const hypERC20contracts = { export type HypERC20contracts = typeof hypERC20contracts; export const hypERC20factories = { - [TokenType.fastCollateral]: new FastHypERC20Collateral__factory(), - [TokenType.fastSynthetic]: new FastHypERC20__factory(), [TokenType.synthetic]: new HypERC20__factory(), [TokenType.collateral]: new HypERC20Collateral__factory(), [TokenType.collateralVault]: new HypERC4626OwnerCollateral__factory(), diff --git a/typescript/sdk/src/token/deploy.hardhat-test.ts b/typescript/sdk/src/token/deploy.hardhat-test.ts index b6e589cdf21..910161f6d1c 100644 --- a/typescript/sdk/src/token/deploy.hardhat-test.ts +++ b/typescript/sdk/src/token/deploy.hardhat-test.ts @@ -28,12 +28,28 @@ import { HypERC20Checker } from './checker.js'; import { TokenType } from './config.js'; import { HypERC20Deployer } from './deploy.js'; import { - HypTokenRouterConfig, + SyntheticTokenConfig, WarpRouteDeployConfigMailboxRequired, } from './types.js'; const chain = TestChainName.test1; +function addOverridesToConfig( + config: WarpRouteDeployConfigMailboxRequired, + ownerOverrides: Record, +): WarpRouteDeployConfigMailboxRequired { + return Object.fromEntries( + Object.entries(config).map(([chain, config]) => { + return [ + chain, + { + ...config, + ownerOverrides, + }, + ]; + }), + ); +} describe('TokenDeployer', async () => { let signer: SignerWithAddress; let deployer: HypERC20Deployer; @@ -56,16 +72,16 @@ describe('TokenDeployer', async () => { const ismFactory = new HyperlaneIsmFactory(factories, multiProvider); coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp(); const routerConfigMap = coreApp.getRouterConfig(signer.address); - config = objMap( - routerConfigMap, - (chain, c): HypTokenRouterConfig => ({ - type: TokenType.synthetic, - name: chain, - symbol: `u${chain}`, - decimals: 18, - ...c, - }), - ); + const token: SyntheticTokenConfig = { + type: TokenType.synthetic, + name: chain, + symbol: `u${chain}`, + decimals: 18, + }; + config = objMap(routerConfigMap, (chain, c) => ({ + ...token, + ...c, + })); }); beforeEach(async () => { @@ -116,7 +132,7 @@ describe('TokenDeployer', async () => { describe('HypERC20Checker', async () => { let checker: HypERC20Checker; - + let app: HypERC20App; beforeEach(async () => { config[chain] = { ...config[chain], @@ -126,7 +142,7 @@ describe('TokenDeployer', async () => { }; const contractsMap = await deployer.deploy(config); - const app = new HypERC20App(contractsMap, multiProvider); + app = new HypERC20App(contractsMap, multiProvider); checker = new HypERC20Checker(multiProvider, app, config); }); @@ -135,7 +151,7 @@ describe('TokenDeployer', async () => { checker.expectEmpty(); }); - it(`should check owner of collateral`, async () => { + it(`should not output "collateralToken" violation when ownerOverrides is unset`, async () => { if (type !== TokenType.XERC20) { return; } @@ -143,11 +159,33 @@ describe('TokenDeployer', async () => { await xerc20.transferOwnership(ethers.Wallet.createRandom().address); await checker.check(); checker.expectViolations({ + [ViolationType.Owner]: 0, // No violation because ownerOverrides is not set + }); + }); + + it('should output "collateralToken" violation when ownerOverrides.collateralToken is set', async () => { + if (type !== TokenType.XERC20) { + return; + } + const previousOwner = await xerc20.owner(); + const configWithOverrides = addOverridesToConfig(config, { + collateralToken: previousOwner, + }); + + const checkerWithOwnerOverrides = new HypERC20Checker( + multiProvider, + app, + configWithOverrides, + ); + + await xerc20.transferOwnership(ethers.Wallet.createRandom().address); + await checkerWithOwnerOverrides.check(); + checkerWithOwnerOverrides.expectViolations({ [ViolationType.Owner]: 1, }); }); - it(`should check owner of collateral proxyAdmin`, async () => { + it(`should not output "collateralProxyAdmin" violation when ownerOverrides is unset`, async () => { if (type !== TokenType.XERC20) { return; } @@ -155,6 +193,27 @@ describe('TokenDeployer', async () => { await admin.transferOwnership(ethers.Wallet.createRandom().address); await checker.check(); checker.expectViolations({ + [ViolationType.Owner]: 0, // No violation because ownerOverrides is not set + }); + }); + + it('should output "collateralProxyAdmin" violation when ownerOverrides.collateralProxyAdmin is set', async () => { + if (type !== TokenType.XERC20) { + return; + } + const previousOwner = await admin.owner(); + const configWithOverrides = addOverridesToConfig(config, { + collateralProxyAdmin: previousOwner, + }); + const checkerWithOwnerOverrides = new HypERC20Checker( + multiProvider, + app, + configWithOverrides, + ); + + await admin.transferOwnership(ethers.Wallet.createRandom().address); + await checkerWithOwnerOverrides.check(); + checkerWithOwnerOverrides.expectViolations({ [ViolationType.Owner]: 1, }); }); diff --git a/typescript/sdk/src/token/types.test.ts b/typescript/sdk/src/token/types.test.ts index c25858f4b3f..cec6aedb2b1 100644 --- a/typescript/sdk/src/token/types.test.ts +++ b/typescript/sdk/src/token/types.test.ts @@ -15,15 +15,10 @@ const SOME_ADDRESS = ethers.Wallet.createRandom().address; const COLLATERAL_TYPES = [ TokenType.collateral, TokenType.collateralUri, - TokenType.fastCollateral, TokenType.collateralVault, ]; -const NON_COLLATERAL_TYPES = [ - TokenType.synthetic, - TokenType.syntheticUri, - TokenType.fastSynthetic, -]; +const NON_COLLATERAL_TYPES = [TokenType.synthetic, TokenType.syntheticUri]; describe('WarpRouteDeployConfigSchema refine', () => { let config: WarpRouteDeployConfig; diff --git a/typescript/sdk/src/token/types.ts b/typescript/sdk/src/token/types.ts index 3e0551592b9..e661cdf0e25 100644 --- a/typescript/sdk/src/token/types.ts +++ b/typescript/sdk/src/token/types.ts @@ -4,7 +4,10 @@ import { objMap } from '@hyperlane-xyz/utils'; import { HookConfig, HookType } from '../hook/types.js'; import { IsmConfig, IsmType } from '../ism/types.js'; -import { GasRouterConfigSchema } from '../router/types.js'; +import { + DerivedMailboxClientFields, + GasRouterConfigSchema, +} from '../router/types.js'; import { ChainMap, ChainName } from '../types.js'; import { isCompliant } from '../utils/schemas.js'; @@ -37,7 +40,6 @@ export const CollateralTokenConfigSchema = TokenMetadataSchema.partial().extend( TokenType.collateralVault, TokenType.collateralVaultRebase, TokenType.collateralFiat, - TokenType.fastCollateral, TokenType.collateralUri, ]), token: z @@ -93,14 +95,10 @@ export const isCollateralRebaseTokenConfig = isCompliant( ); export const SyntheticTokenConfigSchema = TokenMetadataSchema.partial().extend({ + type: z.enum([TokenType.synthetic, TokenType.syntheticUri]), initialSupply: z.string().or(z.number()).optional(), - type: z.enum([ - TokenType.synthetic, - TokenType.syntheticUri, - TokenType.fastSynthetic, - ]), }); -export type SyntheticTokenConfig = z.infer; +export type SyntheticTokenConfig = z.infer; export const isSyntheticTokenConfig = isCompliant(SyntheticTokenConfigSchema); export const SyntheticRebaseTokenConfigSchema = @@ -109,7 +107,7 @@ export const SyntheticRebaseTokenConfigSchema = collateralChainName: z.string(), }); export type SyntheticRebaseTokenConfig = z.infer< - typeof CollateralTokenConfigSchema + typeof SyntheticRebaseTokenConfigSchema >; export const isSyntheticRebaseTokenConfig = isCompliant( SyntheticRebaseTokenConfigSchema, @@ -134,6 +132,23 @@ export const HypTokenRouterConfigSchema = HypTokenConfigSchema.and( ); export type HypTokenRouterConfig = z.infer; +export type DerivedTokenRouterConfig = z.infer & + Omit< + z.infer, + keyof DerivedMailboxClientFields + > & + DerivedMailboxClientFields; + +export function derivedHookAddress(config: DerivedTokenRouterConfig) { + return typeof config.hook === 'string' ? config.hook : config.hook.address; +} + +export function derivedIsmAddress(config: DerivedTokenRouterConfig) { + return typeof config.interchainSecurityModule === 'string' + ? config.interchainSecurityModule + : config.interchainSecurityModule.address; +} + const HypTokenRouterConfigMailboxOptionalSchema = HypTokenConfigSchema.and( GasRouterConfigSchema.extend({ mailbox: z.string().optional(), diff --git a/typescript/sdk/src/utils/zksync.ts b/typescript/sdk/src/utils/zksync.ts new file mode 100644 index 00000000000..0b97e4330c2 --- /dev/null +++ b/typescript/sdk/src/utils/zksync.ts @@ -0,0 +1,32 @@ +import { ZKSyncArtifact, loadAllZKSyncArtifacts } from '@hyperlane-xyz/core'; + +/** + * @dev Retrieves a ZkSync artifact by its contract name or qualified name. + * @param name The name of the contract or qualified name in the format "sourceName:contractName". + * @return The corresponding ZKSyncArtifact if found, or undefined if not found. + */ +export const getZKSyncArtifactByContractName = async ( + name: string, +): Promise => { + // Load all ZkSync artifacts + const allArtifacts = loadAllZKSyncArtifacts(); + + // Find the artifact that matches the contract name or qualified name + const artifact = Object.values(allArtifacts).find( + ({ contractName, sourceName }: ZKSyncArtifact) => { + const lowerCaseContractName = contractName.toLowerCase(); + const lowerCaseName = name.toLowerCase(); + + // Check if the contract name matches + if (lowerCaseContractName === lowerCaseName) { + return true; + } + + // Check if the qualified name matches + const qualifiedName = `${sourceName}:${contractName}`; + return qualifiedName === name; // Return true if qualified name matches + }, + ); + + return artifact; +}; diff --git a/typescript/sdk/src/warp/WarpCore.test.ts b/typescript/sdk/src/warp/WarpCore.test.ts index a6f3cf06895..33b947f9623 100644 --- a/typescript/sdk/src/warp/WarpCore.test.ts +++ b/typescript/sdk/src/warp/WarpCore.test.ts @@ -98,6 +98,7 @@ describe('WarpCore', () => { quoteTransferRemoteGas: () => Promise.resolve(MOCK_INTERCHAIN_QUOTE), isApproveRequired: () => Promise.resolve(false), populateTransferRemoteTx: () => Promise.resolve({}), + isRevokeApprovalRequired: () => Promise.resolve(false), } as any), ); @@ -162,6 +163,7 @@ describe('WarpCore', () => { sinon.stub(t, 'getHypAdapter').returns({ getBalance: () => Promise.resolve(MOCK_BALANCE), getBridgedSupply: () => Promise.resolve(MOCK_BALANCE), + isRevokeApprovalRequired: () => Promise.resolve(false), } as any), ); @@ -215,6 +217,7 @@ describe('WarpCore', () => { getBridgedSupply: () => Promise.resolve(MOCK_BALANCE), getMintLimit: () => Promise.resolve(MEDIUM_MOCK_BALANCE), getMintMaxLimit: () => Promise.resolve(MEDIUM_MOCK_BALANCE), + isRevokeApprovalRequired: () => Promise.resolve(false), } as any), ); @@ -319,6 +322,7 @@ describe('WarpCore', () => { sinon.stub(t, 'getHypAdapter').returns({ quoteTransferRemoteGas: () => Promise.resolve(MOCK_INTERCHAIN_QUOTE), populateTransferRemoteTx: () => Promise.resolve({}), + isRevokeApprovalRequired: () => Promise.resolve(false), } as any), ); diff --git a/typescript/sdk/src/warp/WarpCore.ts b/typescript/sdk/src/warp/WarpCore.ts index ddccadeb43d..a3d06f84e3c 100644 --- a/typescript/sdk/src/warp/WarpCore.ts +++ b/typescript/sdk/src/warp/WarpCore.ts @@ -3,6 +3,7 @@ import { Logger } from 'pino'; import { Address, HexString, + Numberish, ProtocolType, assert, convertDecimalsToIntegerString, @@ -332,16 +333,42 @@ export class WarpCore { const providerType = TOKEN_STANDARD_TO_PROVIDER_TYPE[token.standard]; const hypAdapter = token.getHypAdapter(this.multiProvider, destinationName); - if (await this.isApproveRequired({ originTokenAmount, owner: sender })) { - this.logger.info(`Approval required for transfer of ${token.symbol}`); + const [isApproveRequired, isRevokeApprovalRequired] = await Promise.all([ + this.isApproveRequired({ + originTokenAmount, + owner: sender, + }), + hypAdapter.isRevokeApprovalRequired( + sender, + originTokenAmount.token.addressOrDenom, + ), + ]); + + const preTransferRemoteTxs: [Numberish, WarpTxCategory][] = []; + // if the approval is required and the current allowance is not 0 we reset + // the allowance before setting the right approval as some tokens don't allow + // to override an already existing allowance. USDT is one of these tokens + // see: https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L205 + if (isApproveRequired && isRevokeApprovalRequired) { + preTransferRemoteTxs.push([0, WarpTxCategory.Revoke]); + } + + if (isApproveRequired) { + preTransferRemoteTxs.push([amount.toString(), WarpTxCategory.Approval]); + } + + for (const [approveAmount, txCategory] of preTransferRemoteTxs) { + this.logger.info( + `${txCategory} required for transfer of ${token.symbol}`, + ); const approveTxReq = await hypAdapter.populateApproveTx({ - weiAmountOrId: amount.toString(), + weiAmountOrId: approveAmount, recipient: token.addressOrDenom, }); - this.logger.debug(`Approval tx for ${token.symbol} populated`); + this.logger.debug(`${txCategory} tx for ${token.symbol} populated`); const approveTx = { - category: WarpTxCategory.Approval, + category: txCategory, type: providerType, transaction: approveTxReq, } as WarpTypedTransaction; diff --git a/typescript/sdk/src/warp/types.ts b/typescript/sdk/src/warp/types.ts index e5db231af78..3da7999873c 100644 --- a/typescript/sdk/src/warp/types.ts +++ b/typescript/sdk/src/warp/types.ts @@ -49,6 +49,7 @@ export type RouteBlacklist = Array<{ // Transaction types for warp core remote transfers export enum WarpTxCategory { Approval = 'approval', + Revoke = 'revoke', Transfer = 'transfer', } diff --git a/typescript/sdk/src/zksync/ZKSyncDeployer.ts b/typescript/sdk/src/zksync/ZKSyncDeployer.ts new file mode 100644 index 00000000000..92069263115 --- /dev/null +++ b/typescript/sdk/src/zksync/ZKSyncDeployer.ts @@ -0,0 +1,185 @@ +import { BigNumber, BytesLike, Overrides, utils } from 'ethers'; +import { + Contract, + ContractFactory, + Wallet, + types as zksyncTypes, +} from 'zksync-ethers'; + +import { ZKSyncArtifact } from '@hyperlane-xyz/core'; +import { assert } from '@hyperlane-xyz/utils'; + +import { defaultZKProviderBuilder } from '../providers/providerBuilders.js'; +import { getZKSyncArtifactByContractName } from '../utils/zksync.js'; + +/** + * Class for deploying contracts to the ZKSync network. + */ +export class ZKSyncDeployer { + public zkWallet: Wallet; + public deploymentType?: zksyncTypes.DeploymentType; + + constructor(zkWallet: Wallet, deploymentType?: zksyncTypes.DeploymentType) { + this.deploymentType = deploymentType; + + this.zkWallet = zkWallet.connect( + zkWallet.provider ?? + defaultZKProviderBuilder([{ http: 'http://127.0.0.1:8011' }], 260), + ); + } + + /** + * Loads and validates a ZKSync contract artifact by name + * @param contractTitle - Contract name or qualified name (sourceName:contractName) + * + * @returns The ZKSync artifact + */ + private async loadArtifact(contractTitle: string): Promise { + const artifact = await getZKSyncArtifactByContractName(contractTitle); + assert(artifact, `No ZKSync artifact for contract ${contractTitle} found!`); + return artifact; + } + + /** + * Estimates the price of calling a deploy transaction in ETH. + * + * @param artifact The previously loaded artifact object. + * @param constructorArguments List of arguments to be passed to the contract constructor. + * + * @returns Calculated fee in ETH wei + */ + public async estimateDeployFee( + artifact: ZKSyncArtifact, + constructorArguments: any[], + ): Promise { + const gas = await this.estimateDeployGas(artifact, constructorArguments); + const gasPrice = await this.zkWallet.provider.getGasPrice(); + return gas.mul(gasPrice); + } + + /** + * Estimates the amount of gas needed to execute a deploy transaction. + * + * @param artifact The previously loaded artifact object. + * @param constructorArguments List of arguments to be passed to the contract constructor. + * + * @returns Calculated amount of gas. + */ + public async estimateDeployGas( + artifact: ZKSyncArtifact, + constructorArguments: any[], + ): Promise { + const factoryDeps = await this.extractFactoryDeps(artifact); + + const factory = new ContractFactory( + artifact.abi, + artifact.bytecode, + this.zkWallet, + this.deploymentType, + ); + + // Encode deploy transaction so it can be estimated. + const deployTx = factory.getDeployTransaction(...constructorArguments, { + customData: { + factoryDeps, + }, + }); + deployTx.from = this.zkWallet.address; + + return this.zkWallet.provider.estimateGas(deployTx); + } + + /** + * Sends a deploy transaction to the zkSync network. + * For now, it will use defaults for the transaction parameters: + * - fee amount is requested automatically from the zkSync server. + * + * @param artifact The previously loaded artifact object. + * @param constructorArguments List of arguments to be passed to the contract constructor. + * @param overrides Optional object with additional deploy transaction parameters. + * @param additionalFactoryDeps Additional contract bytecodes to be added to the factory dependencies list. + * + * @returns A contract object. + */ + public async deploy( + artifact: ZKSyncArtifact, + constructorArguments: any[] = [], + overrides?: Overrides, + additionalFactoryDeps?: BytesLike[], + ): Promise { + const baseDeps = await this.extractFactoryDeps(artifact); + const additionalDeps = additionalFactoryDeps + ? additionalFactoryDeps.map((val) => utils.hexlify(val)) + : []; + const factoryDeps = [...baseDeps, ...additionalDeps]; + + const factory = new ContractFactory( + artifact.abi, + artifact.bytecode, + this.zkWallet, + this.deploymentType, + ); + + const { customData, ..._overrides } = overrides ?? {}; + + // Encode and send the deploy transaction providing factory dependencies. + const contract = await factory.deploy(...constructorArguments, { + ..._overrides, + customData: { + ...customData, + factoryDeps, + }, + }); + + await contract.deployed(); + + return contract; + } + + /** + * Extracts factory dependencies from the artifact. + * + * @param artifact Artifact to extract dependencies from + * + * @returns Factory dependencies in the format expected by SDK. + */ + async extractFactoryDeps(artifact: ZKSyncArtifact): Promise { + const visited = new Set(); + + visited.add(`${artifact.sourceName}:${artifact.contractName}`); + return this.extractFactoryDepsRecursive(artifact, visited); + } + + private async extractFactoryDepsRecursive( + artifact: ZKSyncArtifact, + visited: Set, + ): Promise { + // Load all the dependency bytecodes. + // We transform it into an array of bytecodes. + const factoryDeps: string[] = []; + for (const dependencyHash in artifact.factoryDeps) { + if ( + Object.prototype.hasOwnProperty.call( + artifact.factoryDeps, + dependencyHash, + ) + ) { + const dependencyContract = artifact.factoryDeps[dependencyHash]; + if (!visited.has(dependencyContract)) { + const dependencyArtifact = await this.loadArtifact( + dependencyContract, + ); + factoryDeps.push(dependencyArtifact.bytecode); + visited.add(dependencyContract); + const transitiveDeps = await this.extractFactoryDepsRecursive( + dependencyArtifact, + visited, + ); + factoryDeps.push(...transitiveDeps); + } + } + } + + return factoryDeps; + } +} diff --git a/yarn.lock b/yarn.lock index 295317a7c16..2f4c4cc15cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7969,61 +7969,36 @@ __metadata: languageName: node linkType: hard -"@hyperlane-xyz/sdk@npm:11.0.0, @hyperlane-xyz/sdk@workspace:typescript/sdk": - version: 0.0.0-use.local - resolution: "@hyperlane-xyz/sdk@workspace:typescript/sdk" +"@hyperlane-xyz/sdk@npm:11.0.0": + version: 11.0.0 + resolution: "@hyperlane-xyz/sdk@npm:11.0.0" dependencies: "@arbitrum/sdk": "npm:^4.0.0" "@aws-sdk/client-s3": "npm:^3.577.0" "@chain-registry/types": "npm:^0.50.14" "@cosmjs/cosmwasm-stargate": "npm:^0.32.4" "@cosmjs/stargate": "npm:^0.32.4" - "@eslint/js": "npm:^9.15.0" "@hyperlane-xyz/core": "npm:6.1.0" - "@hyperlane-xyz/starknet-core": "npm:0.3.1" "@hyperlane-xyz/utils": "npm:11.0.0" - "@nomiclabs/hardhat-ethers": "npm:^2.2.3" - "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@safe-global/api-kit": "npm:1.3.0" "@safe-global/protocol-kit": "npm:1.3.0" "@safe-global/safe-deployments": "npm:1.37.23" "@solana/spl-token": "npm:^0.4.9" "@solana/web3.js": "npm:^1.95.4" - "@types/mocha": "npm:^10.0.1" - "@types/node": "npm:^18.14.5" - "@types/sinon": "npm:^17.0.1" - "@types/sinon-chai": "npm:^3.2.12" - "@types/ws": "npm:^8.5.5" - "@typescript-eslint/eslint-plugin": "npm:^8.1.6" - "@typescript-eslint/parser": "npm:^8.1.6" bignumber.js: "npm:^9.1.1" - chai: "npm:^4.5.0" cosmjs-types: "npm:^0.9.0" cross-fetch: "npm:^3.1.5" - dotenv: "npm:^10.0.0" - eslint: "npm:^9.15.0" - eslint-config-prettier: "npm:^9.1.0" - eslint-import-resolver-typescript: "npm:^3.6.3" - eslint-plugin-import: "npm:^2.31.0" - ethereum-waffle: "npm:^4.0.10" ethers: "npm:^5.7.2" - hardhat: "npm:^2.22.2" - mocha: "npm:^10.2.0" pino: "npm:^8.19.0" - prettier: "npm:^2.8.8" - sinon: "npm:^13.0.2" - starknet: "npm:6.23.1" - ts-node: "npm:^10.8.0" - tsx: "npm:^4.19.1" - typescript: "npm:5.3.3" + starknet: "npm:^6.23.1" viem: "npm:^2.21.45" - yaml: "npm:2.4.5" zod: "npm:^3.21.2" peerDependencies: "@ethersproject/abi": "*" "@ethersproject/providers": "*" - languageName: unknown - linkType: soft + checksum: 10/35c26c37be707af86a847b335399d8dbeb15b18c486ddf9e79a5f7b96b38a4282e89e551ef5018d0a05a11c7d120989e2e918d08b16dd7d54e46c7c9b6df2267 + languageName: node + linkType: hard "@hyperlane-xyz/sdk@npm:12.1.0": version: 12.1.0 @@ -8057,6 +8032,63 @@ __metadata: languageName: node linkType: hard +"@hyperlane-xyz/sdk@workspace:typescript/sdk": + version: 0.0.0-use.local + resolution: "@hyperlane-xyz/sdk@workspace:typescript/sdk" + dependencies: + "@arbitrum/sdk": "npm:^4.0.0" + "@aws-sdk/client-s3": "npm:^3.577.0" + "@chain-registry/types": "npm:^0.50.14" + "@cosmjs/cosmwasm-stargate": "npm:^0.32.4" + "@cosmjs/stargate": "npm:^0.32.4" + "@eslint/js": "npm:^9.15.0" + "@hyperlane-xyz/core": "npm:7.1.0" + "@hyperlane-xyz/starknet-core": "npm:0.3.1" + "@hyperlane-xyz/utils": "npm:12.1.0" + "@nomiclabs/hardhat-ethers": "npm:^2.2.3" + "@nomiclabs/hardhat-waffle": "npm:^2.0.6" + "@safe-global/api-kit": "npm:1.3.0" + "@safe-global/protocol-kit": "npm:1.3.0" + "@safe-global/safe-deployments": "npm:1.37.23" + "@solana/spl-token": "npm:^0.4.9" + "@solana/web3.js": "npm:^1.95.4" + "@types/mocha": "npm:^10.0.1" + "@types/node": "npm:^18.14.5" + "@types/sinon": "npm:^17.0.1" + "@types/sinon-chai": "npm:^3.2.12" + "@types/ws": "npm:^8.5.5" + "@typescript-eslint/eslint-plugin": "npm:^8.1.6" + "@typescript-eslint/parser": "npm:^8.1.6" + bignumber.js: "npm:^9.1.1" + chai: "npm:^4.5.0" + cosmjs-types: "npm:^0.9.0" + cross-fetch: "npm:^3.1.5" + dotenv: "npm:^10.0.0" + eslint: "npm:^9.15.0" + eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import: "npm:^2.31.0" + ethereum-waffle: "npm:^4.0.10" + ethers: "npm:^5.7.2" + hardhat: "npm:^2.22.2" + mocha: "npm:^10.2.0" + pino: "npm:^8.19.0" + prettier: "npm:^2.8.8" + sinon: "npm:^13.0.2" + starknet: "npm:^6.23.1" + ts-node: "npm:^10.8.0" + tsx: "npm:^4.19.1" + typescript: "npm:5.3.3" + viem: "npm:^2.21.45" + yaml: "npm:2.4.5" + zksync-ethers: "npm:^5.10.0" + zod: "npm:^3.21.2" + peerDependencies: + "@ethersproject/abi": "*" + "@ethersproject/providers": "*" + languageName: unknown + linkType: soft + "@hyperlane-xyz/starknet-core@npm:0.3.1, @hyperlane-xyz/starknet-core@workspace:starknet": version: 0.0.0-use.local resolution: "@hyperlane-xyz/starknet-core@workspace:starknet" From 488e9900e4a1aaa9ae03199591ba008699fd364f Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 10:36:37 +0200 Subject: [PATCH 127/132] chore: merge widgets from main --- typescript/widgets/CHANGELOG.md | 29 +++++++++++++++++++ typescript/widgets/package.json | 6 ++-- typescript/widgets/src/index.ts | 1 + typescript/widgets/src/logos/Binance.tsx | 21 ++++++++++++++ typescript/widgets/src/starknetkit.d.ts | 21 ++++++++++++++ .../src/walletIntegrations/WalletLogo.tsx | 6 +++- .../src/walletIntegrations/multiProtocol.tsx | 25 +++++----------- typescript/widgets/tsconfig.json | 4 +-- 8 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 typescript/widgets/src/logos/Binance.tsx create mode 100644 typescript/widgets/src/starknetkit.d.ts diff --git a/typescript/widgets/CHANGELOG.md b/typescript/widgets/CHANGELOG.md index 47fcf123fef..eb882f68d7e 100644 --- a/typescript/widgets/CHANGELOG.md +++ b/typescript/widgets/CHANGELOG.md @@ -1,5 +1,34 @@ # @hyperlane-xyz/widgets +## 12.1.0 + +### Patch Changes + +- Updated dependencies [acbf5936a] +- Updated dependencies [c757b6a18] +- Updated dependencies [a646f9ca1] +- Updated dependencies [3b615c892] + - @hyperlane-xyz/sdk@12.1.0 + - @hyperlane-xyz/utils@12.1.0 + +## 12.0.0 + +### Minor Changes + +- 7b0e2fed6: Add BinanceLogo to WalletLogo component + +### Patch Changes + +- 86685505f: Revert bundler module resolution and update starknetkit imports +- Updated dependencies [f7ca32315] +- Updated dependencies [4d3738d14] +- Updated dependencies [07321f6f0] +- Updated dependencies [59a087ded] +- Updated dependencies [59a087ded] +- Updated dependencies [337193305] + - @hyperlane-xyz/sdk@12.0.0 + - @hyperlane-xyz/utils@12.0.0 + ## 11.0.0 ### Major Changes diff --git a/typescript/widgets/package.json b/typescript/widgets/package.json index b81fd7946c2..57d067f40e5 100644 --- a/typescript/widgets/package.json +++ b/typescript/widgets/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/widgets", "description": "Common react components for Hyperlane projects", - "version": "11.0.0", + "version": "12.1.0", "peerDependencies": { "react": "^18", "react-dom": "^18" @@ -9,8 +9,8 @@ "dependencies": { "@cosmos-kit/react": "^2.18.0", "@headlessui/react": "^2.1.8", - "@hyperlane-xyz/sdk": "11.0.0", - "@hyperlane-xyz/utils": "11.0.0", + "@hyperlane-xyz/sdk": "12.1.0", + "@hyperlane-xyz/utils": "12.1.0", "@interchain-ui/react": "^1.23.28", "@rainbow-me/rainbowkit": "^2.2.0", "@solana/wallet-adapter-react": "^0.15.32", diff --git a/typescript/widgets/src/index.ts b/typescript/widgets/src/index.ts index bfb7d29e4af..e61b4957ca8 100644 --- a/typescript/widgets/src/index.ts +++ b/typescript/widgets/src/index.ts @@ -62,6 +62,7 @@ export { XCircleIcon } from './icons/XCircle.js'; export { DropdownMenu, type DropdownMenuProps } from './layout/DropdownMenu.js'; export { Modal, useModal, type ModalProps } from './layout/Modal.js'; export { Popover, type PopoverProps } from './layout/Popover.js'; +export { BinanceLogo } from './logos/Binance.js'; export { CosmosLogo } from './logos/Cosmos.js'; export { EthereumLogo } from './logos/Ethereum.js'; export { HyperlaneLogo } from './logos/Hyperlane.js'; diff --git a/typescript/widgets/src/logos/Binance.tsx b/typescript/widgets/src/logos/Binance.tsx new file mode 100644 index 00000000000..722ba74a488 --- /dev/null +++ b/typescript/widgets/src/logos/Binance.tsx @@ -0,0 +1,21 @@ +import React, { SVGProps, memo } from 'react'; + +function _BinanceLogo(props: SVGProps) { + return ( + + + + + + + + + + ); +} + +export const BinanceLogo = memo(_BinanceLogo); diff --git a/typescript/widgets/src/starknetkit.d.ts b/typescript/widgets/src/starknetkit.d.ts new file mode 100644 index 00000000000..f42350f6f89 --- /dev/null +++ b/typescript/widgets/src/starknetkit.d.ts @@ -0,0 +1,21 @@ +declare module 'starknetkit' { + export interface StarknetkitConnector { + id: string; + name: string; + icon?: string | { light: string; dark: string }; + } + + export interface StarknetkitConnectModalOptions { + connectors: StarknetkitConnector[]; + } + + export interface StarknetkitConnectModalResult { + connector?: any; + } + + export function useStarknetkitConnectModal( + options: StarknetkitConnectModalOptions, + ): { + starknetkitConnectModal: () => Promise; + }; +} diff --git a/typescript/widgets/src/walletIntegrations/WalletLogo.tsx b/typescript/widgets/src/walletIntegrations/WalletLogo.tsx index a41658295c4..91542575cba 100644 --- a/typescript/widgets/src/walletIntegrations/WalletLogo.tsx +++ b/typescript/widgets/src/walletIntegrations/WalletLogo.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { WalletIcon } from '../icons/Wallet.js'; +import { BinanceLogo } from '../logos/Binance.js'; import { WalletConnectLogo } from '../logos/WalletConnect.js'; import { WalletDetails } from './types.js'; @@ -13,11 +14,14 @@ export function WalletLogo({ size?: number; }) { const src = walletDetails.logoUrl?.trim(); + const name = walletDetails.name?.toLowerCase(); if (src) { return ; - } else if (walletDetails.name?.toLowerCase() === 'walletconnect') { + } else if (name === 'walletconnect') { return ; + } else if (name === 'binance wallet') { + return ; } else { return ; } diff --git a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx index 477ed2002c6..80ac26dc30d 100644 --- a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx +++ b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx @@ -52,7 +52,7 @@ export function useAccounts( multiProvider: MultiProtocolProvider, blacklistedAddresses: Address[] = [], ): { - accounts: Record, AccountInfo>; + accounts: Record; readyAccounts: Array; } { const evmAccountInfo = useEthereumAccount(multiProvider); @@ -122,7 +122,7 @@ export function useAccountAddressForChain( export function getAccountAddressForChain( multiProvider: MultiProtocolProvider, chainName?: ChainName, - accounts?: Record, AccountInfo>, + accounts?: Record, ): Address | undefined { if (!chainName || !accounts) return undefined; const protocol = multiProvider.getProtocol(chainName); @@ -138,7 +138,7 @@ export function getAccountAddressForChain( export function getAccountAddressAndPubKey( multiProvider: MultiProtocolProvider, chainName?: ChainName, - accounts?: Record, AccountInfo>, + accounts?: Record, ): { address?: Address; publicKey?: Promise } { const address = getAccountAddressForChain(multiProvider, chainName, accounts); if (!accounts || !chainName || !address) return {}; @@ -147,10 +147,7 @@ export function getAccountAddressAndPubKey( return { address, publicKey }; } -export function useWalletDetails(): Record< - Exclude, - WalletDetails -> { +export function useWalletDetails(): Record { const evmWallet = useEthereumWalletDetails(); const solWallet = useSolanaWalletDetails(); const cosmosWallet = useCosmosWalletDetails(); @@ -168,10 +165,7 @@ export function useWalletDetails(): Record< ); } -export function useConnectFns(): Record< - Exclude, - () => void -> { +export function useConnectFns(): Record void> { const onConnectEthereum = useEthereumConnectFn(); const onConnectSolana = useSolanaConnectFn(); const onConnectCosmos = useCosmosConnectFn(); @@ -189,10 +183,7 @@ export function useConnectFns(): Record< ); } -export function useDisconnectFns(): Record< - Exclude, - () => Promise -> { +export function useDisconnectFns(): Record Promise> { const disconnectEvm = useEthereumDisconnectFn(); const disconnectSol = useSolanaDisconnectFn(); const disconnectCosmos = useCosmosDisconnectFn(); @@ -237,7 +228,7 @@ export function useDisconnectFns(): Record< } export function useActiveChains(multiProvider: MultiProtocolProvider): { - chains: Record, ActiveChainInfo>; + chains: Record; readyChains: Array; } { const evmChain = useEthereumActiveChain(multiProvider); @@ -270,7 +261,7 @@ export function useActiveChains(multiProvider: MultiProtocolProvider): { export function useTransactionFns( multiProvider: MultiProtocolProvider, -): Record, ChainTransactionFns> { +): Record { const { switchNetwork: onSwitchEvmNetwork, sendTransaction: onSendEvmTx } = useEthereumTransactionFns(multiProvider); const { switchNetwork: onSwitchSolNetwork, sendTransaction: onSendSolTx } = diff --git a/typescript/widgets/tsconfig.json b/typescript/widgets/tsconfig.json index 3e4dd7363f1..99d9a0d4d04 100644 --- a/typescript/widgets/tsconfig.json +++ b/typescript/widgets/tsconfig.json @@ -6,9 +6,7 @@ "jsx": "react", "noImplicitAny": false, "rootDir": "./src", - "outDir": "./dist", - "moduleResolution": "bundler", - "module": "ES2015" + "outDir": "./dist" }, "include": ["./src/**/*"], "exclude": ["node_modules", "dist", "**/*.stories.tsx"], From c49c0827222a1f545410fa11fe83e0c84f8dc501 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 12:23:10 +0200 Subject: [PATCH 128/132] chore: merge cli from main --- typescript/cli/.gitignore | 1 + typescript/cli/CHANGELOG.md | 28 ++++++ typescript/cli/cli.ts | 2 + typescript/cli/package.json | 60 ++++++------ typescript/cli/scripts/ncc.post-bundle.mjs | 37 ++++++++ typescript/cli/src/commands/options.ts | 7 ++ typescript/cli/src/config/warp.ts | 3 - typescript/cli/src/context/context.ts | 5 + typescript/cli/src/context/types.ts | 1 + typescript/cli/src/deploy/utils.ts | 2 +- typescript/cli/src/deploy/warp.ts | 6 +- typescript/cli/src/tests/commands/core.ts | 71 +++------------ typescript/cli/src/tests/commands/helpers.ts | 12 ++- typescript/cli/src/tests/commands/warp.ts | 34 +++---- .../cli/src/tests/warp/warp-init.e2e-test.ts | 2 +- typescript/cli/src/utils/env.ts | 1 + typescript/cli/src/version.ts | 2 +- .../fork/warp-route-deployment.yaml | 2 - yarn.lock | 91 ++++--------------- 19 files changed, 171 insertions(+), 196 deletions(-) create mode 100644 typescript/cli/scripts/ncc.post-bundle.mjs diff --git a/typescript/cli/.gitignore b/typescript/cli/.gitignore index 4a0b6f51bf2..37175be29a5 100644 --- a/typescript/cli/.gitignore +++ b/typescript/cli/.gitignore @@ -1,5 +1,6 @@ .env* /dist +/cli-bundle /cache # Deployment artifacts and local registry configs diff --git a/typescript/cli/CHANGELOG.md b/typescript/cli/CHANGELOG.md index 379dab3c456..38a58bb2fd5 100644 --- a/typescript/cli/CHANGELOG.md +++ b/typescript/cli/CHANGELOG.md @@ -1,5 +1,33 @@ # @hyperlane-xyz/cli +## 12.1.0 + +### Patch Changes + +- Updated dependencies [acbf5936a] +- Updated dependencies [c757b6a18] +- Updated dependencies [a646f9ca1] +- Updated dependencies [3b615c892] + - @hyperlane-xyz/sdk@12.1.0 + - @hyperlane-xyz/utils@12.1.0 + +## 12.0.0 + +### Minor Changes + +- 82166916a: feat: support github auth token for authenticated registries + +### Patch Changes + +- Updated dependencies [f7ca32315] +- Updated dependencies [4d3738d14] +- Updated dependencies [07321f6f0] +- Updated dependencies [59a087ded] +- Updated dependencies [59a087ded] +- Updated dependencies [337193305] + - @hyperlane-xyz/sdk@12.0.0 + - @hyperlane-xyz/utils@12.0.0 + ## 11.0.0 ### Patch Changes diff --git a/typescript/cli/cli.ts b/typescript/cli/cli.ts index 03330b09ce2..181ac4ab994 100644 --- a/typescript/cli/cli.ts +++ b/typescript/cli/cli.ts @@ -13,6 +13,7 @@ import { hookCommand } from './src/commands/hook.js'; import { ismCommand } from './src/commands/ism.js'; import { disableProxyCommandOption, + githubAuthTokenOption, keyCommandOption, logFormatCommandOption, logLevelCommandOption, @@ -44,6 +45,7 @@ try { .option('log', logFormatCommandOption) .option('verbosity', logLevelCommandOption) .option('registry', registryUrisCommandOption) + .option('authToken', githubAuthTokenOption) .option('overrides', overrideRegistryUriCommandOption) .option('key', keyCommandOption) .option('disableProxy', disableProxyCommandOption) diff --git a/typescript/cli/package.json b/typescript/cli/package.json index f04bd35cccd..bb3fbeec1ff 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -1,81 +1,77 @@ { "name": "@hyperlane-xyz/cli", - "version": "11.0.0", + "version": "12.1.0", "description": "A command-line utility for common Hyperlane operations", - "dependencies": { + "devDependencies": { "@aws-sdk/client-kms": "^3.577.0", "@aws-sdk/client-s3": "^3.577.0", + "@eslint/js": "^9.15.0", + "@ethersproject/abi": "*", + "@ethersproject/providers": "*", "@hyperlane-xyz/registry": "11.1.0", - "@hyperlane-xyz/sdk": "11.0.0", - "@hyperlane-xyz/utils": "11.0.0", + "@hyperlane-xyz/sdk": "12.1.0", + "@hyperlane-xyz/utils": "12.1.0", "@inquirer/core": "9.0.10", "@inquirer/figures": "1.0.5", "@inquirer/prompts": "3.3.2", "@inquirer/search": "^3.0.1", - "ansi-escapes": "^7.0.0", - "asn1.js": "^5.4.1", - "bignumber.js": "^9.1.1", - "chalk": "^5.3.0", - "ethers": "^5.7.2", - "latest-version": "^8.0.0", - "starknet": "6.23.1", - "terminal-link": "^3.0.0", - "tsx": "^4.19.1", - "yaml": "2.4.5", - "yargs": "^17.7.2", - "zksync-ethers": "^5.10.0", - "zod": "^3.21.2", - "zod-validation-error": "^3.3.0", - "zx": "^8.1.4" - }, - "devDependencies": { - "@eslint/js": "^9.15.0", - "@ethersproject/abi": "*", - "@ethersproject/providers": "*", "@types/chai-as-promised": "^8", "@types/mocha": "^10.0.1", "@types/node": "^18.14.5", "@types/yargs": "^17.0.24", "@typescript-eslint/eslint-plugin": "^8.1.6", "@typescript-eslint/parser": "^8.1.6", + "@vercel/ncc": "^0.38.3", + "ansi-escapes": "^7.0.0", + "asn1.js": "^5.4.1", + "bignumber.js": "^9.1.1", "chai": "^4.5.0", "chai-as-promised": "^8.0.0", + "chalk": "^5.3.0", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-import": "^2.31.0", + "ethers": "^5.7.2", + "latest-version": "^8.0.0", "mocha": "^10.2.0", "prettier": "^2.8.8", + "terminal-link": "^3.0.0", + "tsx": "^4.19.1", "typescript": "5.3.3", - "zksync-ethers": "^5.10.0" + "yaml": "2.4.5", + "yargs": "^17.7.2", + "zksync-ethers": "^5.10.0", + "zod": "^3.21.2", + "zod-validation-error": "^3.3.0", + "zx": "^8.1.4" }, "scripts": { "hyperlane": "node ./dist/cli.js", "build": "yarn version:update && tsc", + "bundle": "rm -rf ./cli-bundle && ncc build ./dist/cli.js -o cli-bundle && node ./scripts/ncc.post-bundle.mjs", "dev": "yarn version:update && tsc --watch", - "clean": "rm -rf ./dist", + "clean": "rm -rf ./dist && rm -rf ./cli-bundle", "lint": "eslint -c ./eslint.config.mjs", "prettier": "prettier --write ./src ./examples", "test:ci": "yarn mocha --config .mocharc.json", "test:e2e": "./scripts/run-e2e-test.sh", - "version:update": "echo \"export const VERSION = '$npm_package_version';\" > src/version.ts" + "version:update": "echo \"export const VERSION = '$npm_package_version';\" > src/version.ts", + "prepack": "yarn bundle" }, "files": [ - "./dist", + "./cli-bundle", "./examples", "package.json" ], "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "type": "module", - "exports": { - ".": "./dist/src/index.js" - }, "engines": { "node": ">=16" }, "bin": { - "hyperlane": "./dist/cli.js" + "hyperlane": "./cli-bundle/index.js" }, "repository": { "type": "git", diff --git a/typescript/cli/scripts/ncc.post-bundle.mjs b/typescript/cli/scripts/ncc.post-bundle.mjs new file mode 100644 index 00000000000..337fe95835f --- /dev/null +++ b/typescript/cli/scripts/ncc.post-bundle.mjs @@ -0,0 +1,37 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { readFile, writeFile } from 'fs/promises'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const outputFile = path.join(__dirname, '..', 'cli-bundle', 'index.js'); + +const shebang = '#! /usr/bin/env node'; +const dirnameDef = `import { fileURLToPath } from 'url'; +import path from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename);`; + +async function prepend() { + try { + const content = await readFile(outputFile, 'utf8'); + + // Assume the 'cli.ts' file entry point already has the shebang + if (!content.startsWith(shebang)) { + throw new Error('Missing shebang from cli entry point'); + } + + if (!content.includes(dirnameDef)) { + const [, executable] = content.split(shebang); + const newContent = `${shebang}\n${dirnameDef}\n${executable}`; + await writeFile(outputFile, newContent, 'utf8'); + console.log('Adding missing __dirname definition to cli executable'); + } + } catch (err) { + console.error('Error processing output file:', err); + } +} + +await prepend(); diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 6e55f5eb7e3..130006265d9 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -36,6 +36,13 @@ export const registryUrisCommandOption: Options = { default: [DEFAULT_GITHUB_REGISTRY, DEFAULT_LOCAL_REGISTRY], }; +export const githubAuthTokenOption: Options = { + type: 'string', + description: 'Github auth token for accessing registry repository', + default: ENV.GH_AUTH_TOKEN, + defaultDescription: 'process.env.GH_AUTH_TOKEN', +}; + export const overrideRegistryUriCommandOption: Options = { type: 'string', description: 'Path to a local registry to override the default registry', diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 1f7f4ed55a8..b29db91e678 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -51,9 +51,7 @@ const TYPE_DESCRIPTIONS: Record = { [TokenType.XERC20Lockbox]: 'Extends an existing xERC20 Lockbox with Warp Route functionality', // TODO: describe - [TokenType.fastSynthetic]: '', [TokenType.syntheticUri]: '', - [TokenType.fastCollateral]: '', [TokenType.collateralUri]: '', [TokenType.nativeScaled]: '', }; @@ -185,7 +183,6 @@ export async function createWarpRouteDeployConfig({ case TokenType.XERC20Lockbox: case TokenType.collateralFiat: case TokenType.collateralUri: - case TokenType.fastCollateral: result[chain] = { type, owner, diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index f0c03f78f50..014c73f84d1 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -42,6 +42,7 @@ export async function contextMiddleware(argv: Record) { disableProxy: argv.disableProxy, skipConfirmation: argv.yes, strategyPath: argv.strategy, + authToken: argv.authToken, }; if (!isDryRun && settings.fromAddress) throw new Error( @@ -107,11 +108,13 @@ export async function getContext({ skipConfirmation, disableProxy = false, strategyPath, + authToken, }: ContextSettings): Promise { const registry = getRegistry({ registryUris, enableProxy: !disableProxy, logger: rootLogger, + authToken, }); //Just for backward compatibility @@ -147,6 +150,7 @@ export async function getDryRunContext( fromAddress, skipConfirmation, disableProxy = false, + authToken, }: ContextSettings, chain?: ChainName, ): Promise { @@ -154,6 +158,7 @@ export async function getDryRunContext( registryUris, enableProxy: !disableProxy, logger: rootLogger, + authToken, }); const chainMetadata = await registry.getMetadata(); diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 9744e533ab5..c6fc29f2d25 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -20,6 +20,7 @@ export interface ContextSettings { disableProxy?: boolean; skipConfirmation?: boolean; strategyPath?: string; + authToken?: string; } export interface CommandContext { diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index 55acb393594..280319ddd16 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -57,7 +57,7 @@ export async function runPreflightChecksForChains({ if (metadata.protocol === ProtocolType.Ethereum) { const signer = multiProvider.getSigner(chain); assertSigner(signer); - logGreen(`✅ ${chain} signer is valid`); + logGreen(`✅ ${metadata.displayName ?? chain} signer is valid`); } } logGreen('✅ Chains are valid'); diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 34cb8f5b44c..664c74e9fa6 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -86,7 +86,11 @@ import { writeYamlOrJson, } from '../utils/files.js'; -import { prepareDeploy, runPreflightChecksForChains } from './utils.js'; +import { + completeDeploy, + prepareDeploy, + runPreflightChecksForChains, +} from './utils.js'; interface DeployParams { context: WriteCommandContext; diff --git a/typescript/cli/src/tests/commands/core.ts b/typescript/cli/src/tests/commands/core.ts index e1cbe0cb59f..ee68e25c6a9 100644 --- a/typescript/cli/src/tests/commands/core.ts +++ b/typescript/cli/src/tests/commands/core.ts @@ -5,7 +5,7 @@ import { Address } from '@hyperlane-xyz/utils'; import { readYamlOrJson } from '../../utils/files.js'; -import { ANVIL_KEY, REGISTRY_PATH } from './helpers.js'; +import { ANVIL_KEY, REGISTRY_PATH, localTestRunCmdPrefix } from './helpers.js'; /** * Deploys the Hyperlane core contracts to the specified chain using the provided config. @@ -16,28 +16,14 @@ export function hyperlaneCoreDeployRaw( skipConfirmationPrompts?: boolean, hypKey?: string, ): ProcessPromise { - if (hypKey) { - return $`HYP_KEY=${hypKey} yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \ + return $`${ + hypKey ? ['HYP_KEY=' + hypKey] : '' + } ${localTestRunCmdPrefix()} hyperlane core deploy \ --registry ${REGISTRY_PATH} \ --config ${coreInputPath} \ + ${privateKey ? ['--key', privateKey] : ''} \ --verbosity debug \ - ${skipConfirmationPrompts ? '--yes' : ''}`; - } - - if (privateKey) { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \ - --registry ${REGISTRY_PATH} \ - --config ${coreInputPath} \ - --key ${privateKey} \ - --verbosity debug \ - ${skipConfirmationPrompts ? '--yes' : ''}`; - } - - return $`yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \ - --registry ${REGISTRY_PATH} \ - --config ${coreInputPath} \ - --verbosity debug \ - ${skipConfirmationPrompts ? '--yes' : ''}`; + ${skipConfirmationPrompts ? ['--yes'] : ''}`; } /** @@ -47,7 +33,7 @@ export async function hyperlaneCoreDeploy( chain: string, coreInputPath: string, ) { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \ + return $`${localTestRunCmdPrefix()} hyperlane core deploy \ --registry ${REGISTRY_PATH} \ --config ${coreInputPath} \ --chain ${chain} \ @@ -60,7 +46,7 @@ export async function hyperlaneCoreDeploy( * Reads a Hyperlane core deployment on the specified chain using the provided config. */ export async function hyperlaneCoreRead(chain: string, coreOutputPath: string) { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane core read \ + return $`${localTestRunCmdPrefix()} hyperlane core read \ --registry ${REGISTRY_PATH} \ --config ${coreOutputPath} \ --chain ${chain} \ @@ -76,20 +62,11 @@ export function hyperlaneCoreCheck( coreOutputPath: string, mailbox?: Address, ): ProcessPromise { - if (mailbox) { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane core check \ - --registry ${REGISTRY_PATH} \ - --config ${coreOutputPath} \ - --chain ${chain} \ - --mailbox ${mailbox} \ - --verbosity debug \ - --yes`; - } - - return $`yarn workspace @hyperlane-xyz/cli run hyperlane core check \ + return $`${localTestRunCmdPrefix()} hyperlane core check \ --registry ${REGISTRY_PATH} \ --config ${coreOutputPath} \ --chain ${chain} \ + ${mailbox ? ['--mailbox', mailbox] : ''} \ --verbosity debug \ --yes`; } @@ -102,30 +79,12 @@ export function hyperlaneCoreInit( privateKey?: string, hyp_key?: string, ): ProcessPromise { - if (hyp_key) { - return $`${ - hyp_key ? `HYP_KEY=${hyp_key}` : '' - } yarn workspace @hyperlane-xyz/cli run hyperlane core init \ - --registry ${REGISTRY_PATH} \ - --config ${coreOutputPath} \ - --verbosity debug \ - --yes`; - } - - if (privateKey) { - return $`${ - hyp_key ? 'HYP_KEY=${hyp_key}' : '' - } yarn workspace @hyperlane-xyz/cli run hyperlane core init \ - --registry ${REGISTRY_PATH} \ - --config ${coreOutputPath} \ - --verbosity debug \ - --key ${privateKey} \ - --yes`; - } - - return $`yarn workspace @hyperlane-xyz/cli run hyperlane core init \ + return $`${ + hyp_key ? ['HYP_KEY=' + hyp_key] : '' + } ${localTestRunCmdPrefix()} hyperlane core init \ --registry ${REGISTRY_PATH} \ --config ${coreOutputPath} \ + ${privateKey ? ['--key', privateKey] : ''} \ --verbosity debug \ --yes`; } @@ -137,7 +96,7 @@ export async function hyperlaneCoreApply( chain: string, coreOutputPath: string, ) { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane core apply \ + return $`${localTestRunCmdPrefix()} hyperlane core apply \ --registry ${REGISTRY_PATH} \ --config ${coreOutputPath} \ --chain ${chain} \ diff --git a/typescript/cli/src/tests/commands/helpers.ts b/typescript/cli/src/tests/commands/helpers.ts index 3467fe1f651..ba4c2475b05 100644 --- a/typescript/cli/src/tests/commands/helpers.ts +++ b/typescript/cli/src/tests/commands/helpers.ts @@ -20,7 +20,7 @@ import { WarpCoreConfig, WarpCoreConfigSchema, } from '@hyperlane-xyz/sdk'; -import { Address, sleep } from '@hyperlane-xyz/utils'; +import { Address, inCIMode, sleep } from '@hyperlane-xyz/utils'; import { getContext } from '../../context/context.js'; import { CommandContext } from '../../context/types.js'; @@ -493,11 +493,17 @@ export async function sendWarpRouteMessageRoundTrip( return hyperlaneWarpSendRelay(chain2, chain1, warpCoreConfigPath); } +// Verifies if the IS_CI var is set and generates the correct prefix for running the command +// in the current env +export function localTestRunCmdPrefix() { + return inCIMode() ? [] : ['yarn', 'workspace', '@hyperlane-xyz/cli', 'run']; +} + export async function hyperlaneSendMessage( origin: string, destination: string, ) { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane send message \ + return $`${localTestRunCmdPrefix()} hyperlane send message \ --registry ${REGISTRY_PATH} \ --origin ${origin} \ --destination ${destination} \ @@ -507,7 +513,7 @@ export async function hyperlaneSendMessage( } export function hyperlaneRelayer(chains: string[], warp?: string) { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane relayer \ + return $`${localTestRunCmdPrefix()} hyperlane relayer \ --registry ${REGISTRY_PATH} \ --chains ${chains.join(',')} \ --warp ${warp ?? ''} \ diff --git a/typescript/cli/src/tests/commands/warp.ts b/typescript/cli/src/tests/commands/warp.ts index 9ee792e1494..ca59297128e 100644 --- a/typescript/cli/src/tests/commands/warp.ts +++ b/typescript/cli/src/tests/commands/warp.ts @@ -12,7 +12,12 @@ import { Address } from '@hyperlane-xyz/utils'; import { readYamlOrJson } from '../../utils/files.js'; -import { ANVIL_KEY, REGISTRY_PATH, getDeployedWarpAddress } from './helpers.js'; +import { + ANVIL_KEY, + REGISTRY_PATH, + getDeployedWarpAddress, + localTestRunCmdPrefix, +} from './helpers.js'; $.verbose = true; @@ -20,7 +25,7 @@ $.verbose = true; * Deploys the Warp route to the specified chain using the provided config. */ export function hyperlaneWarpInit(warpCorePath: string): ProcessPromise { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp init \ + return $`${localTestRunCmdPrefix()} hyperlane warp init \ --registry ${REGISTRY_PATH} \ --out ${warpCorePath} \ --key ${ANVIL_KEY} \ @@ -44,7 +49,7 @@ export function hyperlaneWarpDeployRaw({ }): ProcessPromise { return $`${ hypKey ? ['HYP_KEY=' + hypKey] : '' - } yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \ + } ${localTestRunCmdPrefix()} hyperlane warp deploy \ --registry ${REGISTRY_PATH} \ ${warpCorePath ? ['--config', warpCorePath] : ''} \ ${privateKey ? ['--key', privateKey] : ''} \ @@ -71,7 +76,7 @@ export async function hyperlaneWarpApply( warpCorePath: string, strategyUrl = '', ) { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp apply \ + return $`${localTestRunCmdPrefix()} hyperlane warp apply \ --registry ${REGISTRY_PATH} \ --config ${warpDeployPath} \ --warp ${warpCorePath} \ @@ -92,7 +97,7 @@ export function hyperlaneWarpReadRaw({ warpAddress?: string; outputPath?: string; }): ProcessPromise { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp read \ + return $`${localTestRunCmdPrefix()} hyperlane warp read \ --registry ${REGISTRY_PATH} \ ${warpAddress ? ['--address', warpAddress] : ''} \ ${chain ? ['--chain', chain] : ''} \ @@ -120,7 +125,7 @@ export function hyperlaneWarpCheckRaw({ symbol?: string; warpDeployPath?: string; }): ProcessPromise { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp check \ + return $`${localTestRunCmdPrefix()} hyperlane warp check \ --registry ${REGISTRY_PATH} \ ${symbol ? ['--symbol', symbol] : ''} \ --verbosity debug \ @@ -144,7 +149,7 @@ export function hyperlaneWarpSendRelay( relay = true, value = 1, ): ProcessPromise { - return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp send \ + return $`${localTestRunCmdPrefix()} hyperlane warp send \ ${relay ? '--relay' : ''} \ --registry ${REGISTRY_PATH} \ --origin ${origin} \ @@ -215,21 +220,6 @@ function getWarpTokenConfigForType({ token: vault, }; break; - case TokenType.fastCollateral: - tokenConfig = { - type: TokenType.fastCollateral, - mailbox, - owner, - token, - }; - break; - case TokenType.fastSynthetic: - tokenConfig = { - type: TokenType.fastSynthetic, - mailbox, - owner, - }; - break; case TokenType.native: tokenConfig = { type: TokenType.native, diff --git a/typescript/cli/src/tests/warp/warp-init.e2e-test.ts b/typescript/cli/src/tests/warp/warp-init.e2e-test.ts index 05b424f0d8a..d1ee57a4f91 100644 --- a/typescript/cli/src/tests/warp/warp-init.e2e-test.ts +++ b/typescript/cli/src/tests/warp/warp-init.e2e-test.ts @@ -119,7 +119,7 @@ describe('hyperlane warp init e2e tests', async function () { check: (currentOutput: string) => !!currentOutput.match(/Select .+?'s token type/), // Scroll down through the token type list and select collateral - input: `${KeyBoardKeys.ARROW_DOWN.repeat(4)}${KeyBoardKeys.ENTER}`, + input: `${KeyBoardKeys.ARROW_DOWN.repeat(3)}${KeyBoardKeys.ENTER}`, }, { check: (currentOutput: string) => diff --git a/typescript/cli/src/utils/env.ts b/typescript/cli/src/utils/env.ts index a0a5b232f12..5f299cfeae6 100644 --- a/typescript/cli/src/utils/env.ts +++ b/typescript/cli/src/utils/env.ts @@ -7,6 +7,7 @@ const envScheme = z.object({ AWS_ACCESS_KEY_ID: z.string().optional(), AWS_SECRET_ACCESS_KEY: z.string().optional(), AWS_REGION: z.string().optional(), + GH_AUTH_TOKEN: z.string().optional(), }); const parsedEnv = envScheme.safeParse(process.env); diff --git a/typescript/cli/src/version.ts b/typescript/cli/src/version.ts index 0c68bbacf96..e04a4acc82b 100644 --- a/typescript/cli/src/version.ts +++ b/typescript/cli/src/version.ts @@ -1 +1 @@ -export const VERSION = '11.0.0'; +export const VERSION = '12.1.0'; diff --git a/typescript/cli/test-configs/fork/warp-route-deployment.yaml b/typescript/cli/test-configs/fork/warp-route-deployment.yaml index 845ce5247d2..2965e4aa0c2 100644 --- a/typescript/cli/test-configs/fork/warp-route-deployment.yaml +++ b/typescript/cli/test-configs/fork/warp-route-deployment.yaml @@ -7,8 +7,6 @@ # synthetic # collateralUri # syntheticUri -# fastCollateral -# fastSynthetic --- anvil1: type: native diff --git a/yarn.lock b/yarn.lock index 2f4c4cc15cd..f7414c41c8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7651,8 +7651,8 @@ __metadata: "@ethersproject/abi": "npm:*" "@ethersproject/providers": "npm:*" "@hyperlane-xyz/registry": "npm:11.1.0" - "@hyperlane-xyz/sdk": "npm:11.0.0" - "@hyperlane-xyz/utils": "npm:11.0.0" + "@hyperlane-xyz/sdk": "npm:12.1.0" + "@hyperlane-xyz/utils": "npm:12.1.0" "@inquirer/core": "npm:9.0.10" "@inquirer/figures": "npm:1.0.5" "@inquirer/prompts": "npm:3.3.2" @@ -7663,6 +7663,7 @@ __metadata: "@types/yargs": "npm:^17.0.24" "@typescript-eslint/eslint-plugin": "npm:^8.1.6" "@typescript-eslint/parser": "npm:^8.1.6" + "@vercel/ncc": "npm:^0.38.3" ansi-escapes: "npm:^7.0.0" asn1.js: "npm:^5.4.1" bignumber.js: "npm:^9.1.1" @@ -7677,7 +7678,6 @@ __metadata: latest-version: "npm:^8.0.0" mocha: "npm:^10.2.0" prettier: "npm:^2.8.8" - starknet: "npm:6.23.1" terminal-link: "npm:^3.0.0" tsx: "npm:^4.19.1" typescript: "npm:5.3.3" @@ -7688,32 +7688,10 @@ __metadata: zod-validation-error: "npm:^3.3.0" zx: "npm:^8.1.4" bin: - hyperlane: ./dist/cli.js + hyperlane: ./cli-bundle/index.js languageName: unknown linkType: soft -"@hyperlane-xyz/core@npm:6.1.0": - version: 6.1.0 - resolution: "@hyperlane-xyz/core@npm:6.1.0" - dependencies: - "@arbitrum/nitro-contracts": "npm:^1.2.1" - "@chainlink/contracts-ccip": "npm:^1.5.0" - "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:11.0.0" - "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" - "@matterlabs/hardhat-zksync-solc": "npm:1.2.5" - "@matterlabs/hardhat-zksync-verify": "npm:1.7.1" - "@openzeppelin/contracts": "npm:^4.9.3" - "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" - fx-portal: "npm:^1.0.3" - peerDependencies: - "@ethersproject/abi": "*" - "@ethersproject/providers": "*" - "@types/sinon-chai": "*" - checksum: 10/a45eb1ae30b240f345b8873d6d8f2ee98cca6701fcbe1ef2bcd62f9d47ce7399137b2d4c5ab6abee41ffde6ae5892af54158b9e39394509781a9a9e6acd8ece1 - languageName: node - linkType: hard - "@hyperlane-xyz/core@npm:7.1.0": version: 7.1.0 resolution: "@hyperlane-xyz/core@npm:7.1.0" @@ -7969,37 +7947,6 @@ __metadata: languageName: node linkType: hard -"@hyperlane-xyz/sdk@npm:11.0.0": - version: 11.0.0 - resolution: "@hyperlane-xyz/sdk@npm:11.0.0" - dependencies: - "@arbitrum/sdk": "npm:^4.0.0" - "@aws-sdk/client-s3": "npm:^3.577.0" - "@chain-registry/types": "npm:^0.50.14" - "@cosmjs/cosmwasm-stargate": "npm:^0.32.4" - "@cosmjs/stargate": "npm:^0.32.4" - "@hyperlane-xyz/core": "npm:6.1.0" - "@hyperlane-xyz/utils": "npm:11.0.0" - "@safe-global/api-kit": "npm:1.3.0" - "@safe-global/protocol-kit": "npm:1.3.0" - "@safe-global/safe-deployments": "npm:1.37.23" - "@solana/spl-token": "npm:^0.4.9" - "@solana/web3.js": "npm:^1.95.4" - bignumber.js: "npm:^9.1.1" - cosmjs-types: "npm:^0.9.0" - cross-fetch: "npm:^3.1.5" - ethers: "npm:^5.7.2" - pino: "npm:^8.19.0" - starknet: "npm:^6.23.1" - viem: "npm:^2.21.45" - zod: "npm:^3.21.2" - peerDependencies: - "@ethersproject/abi": "*" - "@ethersproject/providers": "*" - checksum: 10/35c26c37be707af86a847b335399d8dbeb15b18c486ddf9e79a5f7b96b38a4282e89e551ef5018d0a05a11c7d120989e2e918d08b16dd7d54e46c7c9b6df2267 - languageName: node - linkType: hard - "@hyperlane-xyz/sdk@npm:12.1.0": version: 12.1.0 resolution: "@hyperlane-xyz/sdk@npm:12.1.0" @@ -8108,21 +8055,6 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/utils@npm:11.0.0": - version: 11.0.0 - resolution: "@hyperlane-xyz/utils@npm:11.0.0" - dependencies: - "@cosmjs/encoding": "npm:^0.32.4" - "@solana/web3.js": "npm:^1.95.4" - bignumber.js: "npm:^9.1.1" - ethers: "npm:^5.7.2" - lodash-es: "npm:^4.17.21" - pino: "npm:^8.19.0" - yaml: "npm:2.4.5" - checksum: 10/1f605c1280b23877ff696a31d325c0869af4671f7b3658ae0a31c2359b971c970e180cb91257732cf23f239fc5e70f6b1bc9dd05d959b698a2599e466c04fca0 - languageName: node - linkType: hard - "@hyperlane-xyz/utils@npm:12.1.0": version: 12.1.0 resolution: "@hyperlane-xyz/utils@npm:12.1.0" @@ -8180,8 +8112,8 @@ __metadata: "@eslint/js": "npm:^9.15.0" "@headlessui/react": "npm:^2.1.8" "@hyperlane-xyz/registry": "npm:11.1.0" - "@hyperlane-xyz/sdk": "npm:11.0.0" - "@hyperlane-xyz/utils": "npm:11.0.0" + "@hyperlane-xyz/sdk": "npm:12.1.0" + "@hyperlane-xyz/utils": "npm:12.1.0" "@interchain-ui/react": "npm:^1.23.28" "@rainbow-me/rainbowkit": "npm:^2.2.0" "@solana/wallet-adapter-react": "npm:^0.15.32" @@ -17488,6 +17420,17 @@ __metadata: languageName: node linkType: hard +"@vercel/ncc@npm:^0.38.3": + version: 0.38.3 + resolution: "@vercel/ncc@npm:0.38.3" + dependencies: + node-gyp: "npm:latest" + bin: + ncc: dist/ncc/cli.js + checksum: 10/f1a05a58e9c90d6940027b628590715a62bf1611c47bca546ad51bd6a0e8d25ce64c1c39eb27ba0b6747017182cb59ec42088da8d6530a6d561e9e1a4e8c9941 + languageName: node + linkType: hard + "@vitejs/plugin-react@npm:^3.0.1": version: 3.1.0 resolution: "@vitejs/plugin-react@npm:3.1.0" From a074a69bf0c182448a5cdf45c2caefc2668f1cb0 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 12:27:03 +0200 Subject: [PATCH 129/132] chore: merge starknet address utils --- typescript/utils/package.json | 1 + typescript/utils/src/addresses.ts | 35 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/typescript/utils/package.json b/typescript/utils/package.json index 07adc835b50..c5eee539cab 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -9,6 +9,7 @@ "ethers": "^5.7.2", "lodash-es": "^4.17.21", "pino": "^8.19.0", + "starknet": "6.23.1", "yaml": "2.4.5" }, "devDependencies": { diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index 63c28cf5e08..ba89a68f7f5 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -1,6 +1,12 @@ import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding'; import { PublicKey } from '@solana/web3.js'; import { Wallet, utils as ethersUtils } from 'ethers'; +import { + addAddressPadding, + encode, + num, + validateAndParseAddress, +} from 'starknet'; import { isNullish } from './typeof.js'; import { Address, HexString, ProtocolType } from './types.js'; @@ -48,6 +54,10 @@ export function isAddressCosmos(address: Address) { ); } +export function isAddressStarknet(address: Address) { + return !!validateAndParseAddress(address); +} + export function getAddressProtocolType(address: Address) { if (!address) return undefined; if (isAddressEvm(address)) { @@ -56,6 +66,8 @@ export function getAddressProtocolType(address: Address) { return ProtocolType.Cosmos; } else if (isAddressSealevel(address)) { return ProtocolType.Sealevel; + } else if (isAddressStarknet(address)) { + return ProtocolType.Starknet; } else { return undefined; } @@ -112,12 +124,22 @@ export function isValidAddressCosmos(address: Address) { } } +export function isValidAddressStarknet(address: Address) { + try { + const isValid = address && validateAndParseAddress(address); + return !!isValid; + } catch { + return false; + } +} + export function isValidAddress(address: Address, protocol?: ProtocolType) { return routeAddressUtil( { [ProtocolType.Ethereum]: isValidAddressEvm, [ProtocolType.Sealevel]: isValidAddressSealevel, [ProtocolType.Cosmos]: isValidAddressCosmos, + [ProtocolType.Starknet]: isValidAddressStarknet, }, address, false, @@ -266,6 +288,11 @@ export function addressToBytesCosmos(address: Address): Uint8Array { return fromBech32(address).data; } +export function addressToBytesStarknet(address: Address): Uint8Array { + const normalizedAddress = validateAndParseAddress(address); + return num.hexToBytes(normalizedAddress); +} + export function addressToBytes( address: Address, protocol?: ProtocolType, @@ -275,6 +302,7 @@ export function addressToBytes( [ProtocolType.Ethereum]: addressToBytesEvm, [ProtocolType.Sealevel]: addressToBytesSol, [ProtocolType.Cosmos]: addressToBytesCosmos, + [ProtocolType.Starknet]: addressToBytesStarknet, }, address, new Uint8Array(), @@ -343,6 +371,11 @@ export function bytesToAddressCosmos( return toBech32(prefix, bytes); } +export function bytesToAddressStarknet(bytes: Uint8Array): Address { + const hexString = encode.buf2hex(bytes); + return addAddressPadding(hexString); +} + export function bytesToProtocolAddress( bytes: Uint8Array, toProtocol: ProtocolType, @@ -358,6 +391,8 @@ export function bytesToProtocolAddress( return bytesToAddressSol(bytes); } else if (toProtocol === ProtocolType.Cosmos) { return bytesToAddressCosmos(bytes, prefix!); + } else if (toProtocol === ProtocolType.Starknet) { + return bytesToAddressStarknet(bytes); } else { throw new Error(`Unsupported protocol for address ${toProtocol}`); } From 517a1c399c4f283ba5e7166458daac7c831b969a Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 12:27:40 +0200 Subject: [PATCH 130/132] chore: yarn install --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index f7414c41c8d..984e9fe9c2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8095,6 +8095,7 @@ __metadata: pino: "npm:^8.19.0" prettier: "npm:^2.8.8" sinon: "npm:^13.0.2" + starknet: "npm:6.23.1" typescript: "npm:5.3.3" yaml: "npm:2.4.5" languageName: unknown From ded94629258fac69dae5022534336efd417e91f5 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 12:55:44 +0200 Subject: [PATCH 131/132] chore: merge cli from feat/starknet-send-relayer --- typescript/cli/src/commands/relayer.ts | 124 ++++++-- typescript/cli/src/commands/warp.ts | 1 + typescript/cli/src/context/context.ts | 4 +- .../strategies/chain/MultiChainResolver.ts | 11 +- .../signer/MultiProtocolSignerFactory.ts | 25 +- typescript/cli/src/deploy/core.ts | 11 +- typescript/cli/src/deploy/utils.ts | 108 +++++-- typescript/cli/src/deploy/warp.ts | 264 +++++++++++++++--- typescript/cli/src/send/message.ts | 188 ++++--------- typescript/cli/src/send/transfer.ts | 199 ++++++++++--- typescript/cli/src/utils/env.ts | 1 + 11 files changed, 676 insertions(+), 260 deletions(-) diff --git a/typescript/cli/src/commands/relayer.ts b/typescript/cli/src/commands/relayer.ts index 72beb35bf7b..9f4a3a77762 100644 --- a/typescript/cli/src/commands/relayer.ts +++ b/typescript/cli/src/commands/relayer.ts @@ -1,14 +1,15 @@ import { ChainMap, + EvmAdapter, HyperlaneCore, - HyperlaneRelayer, - RelayerCacheSchema, + MessageBus, + StarknetAdapter, + StarknetCore, } from '@hyperlane-xyz/sdk'; -import { Address } from '@hyperlane-xyz/utils'; +import { Address, ProtocolType } from '@hyperlane-xyz/utils'; import { CommandModuleWithContext } from '../context/types.js'; import { log } from '../logger.js'; -import { tryReadJson, writeJson } from '../utils/files.js'; import { getWarpCoreConfigOrExit } from '../utils/warp.js'; import { @@ -43,11 +44,6 @@ export const relayerCommand: CommandModuleWithContext< }, handler: async ({ context, cache, chains, symbol, warp }) => { const chainAddresses = await context.registry.getAddresses(); - const core = HyperlaneCore.fromAddressesMap( - chainAddresses, - context.multiProvider, - ); - const chainsArray = chains?.split(',').map((_) => _.trim()) ?? Object.keys(chainAddresses); @@ -55,7 +51,6 @@ export const relayerCommand: CommandModuleWithContext< chainsArray.map((chain) => [chain, []]), ); - // add warp route addresses to whitelist if (symbol || warp) { const warpRoute = await getWarpCoreConfigOrExit({ context, @@ -68,27 +63,106 @@ export const relayerCommand: CommandModuleWithContext< ); } - const relayer = new HyperlaneRelayer({ core, whitelist }); - // TODO: fix merkle hook stubbing + const protocols = new Set(); + const chainsByProtocol: Record = { + [ProtocolType.Ethereum]: [], + [ProtocolType.Starknet]: [], + [ProtocolType.Cosmos]: [], + [ProtocolType.Sealevel]: [], + [ProtocolType.CosmosNative]: [], + }; + + const cores: { + [ProtocolType.Ethereum]?: HyperlaneCore; + [ProtocolType.Starknet]?: StarknetCore; + } = {}; + + chainsArray.forEach((chain) => { + const protocol = context.multiProvider.getProtocol(chain); + protocols.add(protocol); + chainsByProtocol[protocol]?.push(chain); + }); + + // Initialize cores based on protocols + const initializeCore = ( + protocol: ProtocolType.Ethereum | ProtocolType.Starknet, + ) => { + if (!protocols.has(protocol)) return; - const jsonCache = tryReadJson(cache); - if (jsonCache) { - try { - const parsedCache = RelayerCacheSchema.parse(jsonCache); - relayer.hydrate(parsedCache); - } catch (error) { - log(`Error hydrating cache: ${error}`); + const protocolAddresses: ChainMap = {}; + chainsByProtocol[protocol].forEach( + (chain) => + chainAddresses[chain] && + (protocolAddresses[chain] = chainAddresses[chain]), + ); + + // Skip if no addresses found + if (!Object.keys(protocolAddresses).length) return; + + if (protocol === ProtocolType.Ethereum) { + cores[protocol] = HyperlaneCore.fromAddressesMap( + protocolAddresses, + context.multiProvider, + ); + } else if (protocol === ProtocolType.Starknet) { + cores[protocol] = new StarknetCore( + protocolAddresses, + context.multiProvider!, + context.multiProtocolSigner!, + context.multiProtocolProvider!, + ); } - } + }; + + initializeCore(ProtocolType.Ethereum); + initializeCore(ProtocolType.Starknet); - log('Starting relayer ...'); - relayer.start(); + const messageBus = new MessageBus(context.multiProvider); + + log('Initialized Message Bus for cross-protocol communication'); + + const initializeAdapter = ( + protocolType: ProtocolType.Ethereum | ProtocolType.Starknet, + adapterName: string, + ) => { + const core = cores[protocolType]; + if (!core) return false; + + const protocolWhitelist: ChainMap = {}; + chainsByProtocol[protocolType].forEach((chain) => { + if (whitelist[chain]) { + protocolWhitelist[chain] = whitelist[chain]; + } + }); + + const adapter = + protocolType === ProtocolType.Starknet + ? new StarknetAdapter(core as StarknetCore, protocolWhitelist) + : new EvmAdapter(core as HyperlaneCore, protocolWhitelist); + + messageBus.registerHandler(adapter); + adapter.listenForMessages(messageBus); + + log(`${adapterName} adapter registered with Message Bus`); + return true; + }; + + initializeAdapter(ProtocolType.Starknet, 'Starknet'); + initializeAdapter(ProtocolType.Ethereum, 'Hyperlane'); + + log('Starknet relayer initialized successfully.'); + + // Start the message bus + messageBus.start(); + log('Message Bus started for cross-protocol communication'); process.once('SIGINT', () => { - log('Stopping relayer ...'); - relayer.stop(); + log('Stopping relayers and message bus...'); + + // Stop the message bus + messageBus.stop(); + log('Message Bus stopped'); - writeJson(cache, relayer.cache); process.exit(0); }); }, diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index ae84ea8606f..1d829549f4c 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -149,6 +149,7 @@ export const deploy: CommandModuleWithWriteContext<{ await runWarpRouteDeploy({ context, warpRouteDeploymentConfigPath: config, + multiProtocolSigner, }); } catch (error: any) { evaluateIfDryRunFailure(error, dryRun); diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 014c73f84d1..39fe1682b2d 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -87,11 +87,13 @@ export async function signerMiddleware(argv: Record) { { key }, ); + await multiProtocolSigner.initAllSigners(); + /** * @notice Attaches signers to MultiProvider and assigns it to argv.multiProvider */ argv.multiProvider = await multiProtocolSigner.getMultiProvider(); - argv.multiProtocolSigner = multiProtocolSigner; // TODO: remove this line after making sure `argv.context.multiProtocolSigner` is working properly + argv.multiProtocolSigner = multiProtocolSigner; argv.context.multiProtocolSigner = multiProtocolSigner; return argv; diff --git a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts index b7caa5455da..f07b2986176 100644 --- a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -131,12 +131,13 @@ export class MultiChainResolver implements ChainResolver { const { multiProvider } = argv.context; const chains = new Set(); - if (argv.origin) { - chains.add(argv.origin); - } - if (argv.chain) { chains.add(argv.chain); + return Array.from(chains); + } + + if (argv.origin) { + chains.add(argv.origin); } if (argv.chains) { @@ -147,7 +148,7 @@ export class MultiChainResolver implements ChainResolver { } // If no destination is specified, return all EVM chains - if (!argv.destination) { + if (argv.origin && !argv.destination) { return Array.from(this.getEvmChains(multiProvider)); } diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index a38ad422a05..d7df18fe54d 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -1,6 +1,6 @@ import { password } from '@inquirer/prompts'; import { Signer, Wallet } from 'ethers'; -import { Account as StarknetAccount } from 'starknet'; +import { Account as StarknetAccount, constants } from 'starknet'; import { Wallet as ZKSyncWallet } from 'zksync-ethers'; import { @@ -12,6 +12,8 @@ import { } from '@hyperlane-xyz/sdk'; import { ProtocolType, assert } from '@hyperlane-xyz/utils'; +import { ENV } from '../../../utils/env.js'; + import { BaseMultiProtocolSigner, IMultiProtocolSigner, @@ -103,6 +105,14 @@ class StarknetSignerStrategy extends BaseMultiProtocolSigner { return { privateKey, userAddress: address }; } + private getTransactionVersion( + versionFromEnv?: string, + ): ConstructorParameters[4] { + if (versionFromEnv === 'V2') return constants.TRANSACTION_VERSION.V2; + if (versionFromEnv === 'V3') return constants.TRANSACTION_VERSION.V3; + return undefined; + } + getSigner({ privateKey, userAddress, @@ -112,6 +122,17 @@ class StarknetSignerStrategy extends BaseMultiProtocolSigner { userAddress && extraParams?.provider, 'Missing StarknetAccount arguments', ); - return new StarknetAccount(extraParams.provider, userAddress, privateKey); + + const transactionVersion = this.getTransactionVersion( + ENV.STARKNET_TRANSACTION_VERSION, + ); + + return new StarknetAccount( + extraParams.provider, + userAddress, + privateKey, + undefined, + transactionVersion, + ); } } diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index f20ebfdb47c..7919225bf0a 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -53,6 +53,7 @@ export async function runCoreDeploy(params: DeployParams) { registry, skipConfirmation, multiProvider, + multiProtocolProvider, } = context; // Select a dry-run chain if it's not supplied @@ -118,10 +119,14 @@ export async function runCoreDeploy(params: DeployParams) { case ProtocolType.Starknet: { - const domainId = multiProvider.getDomainId(chain); - const account = multiProtocolSigner?.getStarknetSigner(chain); + const account = multiProtocolSigner!.getStarknetSigner(chain); assert(account, 'Starknet account failed!'); - const starknetCoreModule = new StarknetCoreModule(account, domainId); + const starknetCoreModule = new StarknetCoreModule( + account, + multiProvider, + multiProtocolProvider!, + chain, + ); deployedAddresses = await starknetCoreModule.deploy({ chain, config, diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index 280319ddd16..1aaa989c0f1 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -8,8 +8,10 @@ import { IsmConfig, MultisigConfig, getLocalProvider, + getStarknetEtherContract, + getStarknetHypERC20Contract, } from '@hyperlane-xyz/sdk'; -import { Address, ProtocolType } from '@hyperlane-xyz/utils'; +import { Address, ProtocolType, assert } from '@hyperlane-xyz/utils'; import { parseIsmConfig } from '../config/ism.js'; import { CommandContext, WriteCommandContext } from '../context/types.js'; @@ -57,7 +59,7 @@ export async function runPreflightChecksForChains({ if (metadata.protocol === ProtocolType.Ethereum) { const signer = multiProvider.getSigner(chain); assertSigner(signer); - logGreen(`✅ ${metadata.displayName ?? chain} signer is valid`); + logGreen(`✅ ${chain} signer is valid`); } } logGreen('✅ Chains are valid'); @@ -155,31 +157,99 @@ export async function prepareDeploy( return initialBalances; } +export async function prepareStarknetDeploy( + context: WriteCommandContext, + userAddress: Address | null, + chains: ChainName[], +): Promise> { + const { multiProtocolProvider, multiProtocolSigner } = context; + const initialBalances: Record = {}; + await Promise.all( + chains.map(async (chain: ChainName) => { + const provider = multiProtocolProvider?.getStarknetProvider(chain); + assert(provider, `No provider found for ${chain}`); + const address = + userAddress ?? multiProtocolSigner?.getStarknetSigner(chain).address; + assert(address, `No address found for ${chain}`); + + const nativeTokenAddress = + multiProtocolProvider?.getChainMetadata(chain).nativeToken?.denom; // TODO: fetch default token + assert(nativeTokenAddress, `No native token found for ${chain}`); + const etherContract = getStarknetEtherContract( + nativeTokenAddress, + provider, + ); + const currentBalance = await etherContract.balanceOf(address); + initialBalances[chain] = currentBalance; + }), + ); + return initialBalances; +} + export async function completeDeploy( context: WriteCommandContext, command: string, - initialBalances: Record, + initialBalances: Record, userAddress: Address | null, chains: ChainName[], ) { - const { multiProvider, isDryRun } = context; + const { + multiProvider, + multiProtocolProvider, + multiProtocolSigner, + isDryRun, + } = context; if (chains.length > 0) logPink(`⛽️ Gas Usage Statistics`); for (const chain of chains) { - const provider = isDryRun - ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) - : multiProvider.getProvider(chain); - const address = - userAddress ?? (await multiProvider.getSigner(chain).getAddress()); - const currentBalance = await provider.getBalance(address); - const balanceDelta = initialBalances[chain].sub(currentBalance); - if (isDryRun && balanceDelta.lt(0)) break; - logPink( - `\t- Gas required for ${command} ${ - isDryRun ? 'dry-run' : 'deploy' - } on ${chain}: ${ethers.utils.formatEther(balanceDelta)} ${ - multiProvider.getChainMetadata(chain).nativeToken?.symbol ?? 'ETH' - }`, - ); + const metadata = multiProvider.tryGetChainMetadata(chain); + if (metadata?.protocol === ProtocolType.Starknet) { + const provider = multiProtocolProvider?.getStarknetProvider(chain); + assert(provider, `No provider found for ${chain}`); + const address = + userAddress ?? multiProtocolSigner?.getStarknetSigner(chain).address; + assert(address, `No address found for ${chain}`); + + const nativeTokenAddress = + multiProtocolProvider?.getChainMetadata(chain).nativeToken?.denom; + assert(nativeTokenAddress, `No native token found for ${chain}`); + + const etherContract = getStarknetHypERC20Contract( + nativeTokenAddress, + provider, + ); + const currentBalance = (await etherContract.balanceOf(address)) as bigint; + const balanceDelta = (initialBalances[chain] as bigint) - currentBalance; + + if (isDryRun && balanceDelta > 0) continue; + + logPink( + `\t- Gas required for ${command} ${ + isDryRun ? 'dry-run' : 'deploy' + } on ${chain} (Starknet): ${ethers.utils.formatEther(balanceDelta)} ${ + multiProtocolProvider?.getChainMetadata(chain).nativeToken?.symbol ?? + 'ETH' + }`, + ); + } else { + // Original Ethereum chain handling + const provider = isDryRun + ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) + : multiProvider.getProvider(chain); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); + const balanceDelta = (initialBalances[chain] as BigNumber).sub( + currentBalance, + ); + if (isDryRun && balanceDelta.lt(0)) continue; + logPink( + `\t- Gas required for ${command} ${ + isDryRun ? 'dry-run' : 'deploy' + } on ${chain}: ${ethers.utils.formatEther(balanceDelta)} ${ + multiProvider.getChainMetadata(chain).nativeToken?.symbol ?? 'ETH' + }`, + ); + } } if (isDryRun) await completeDryRun(command); diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 664c74e9fa6..34239cf5b7f 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -1,4 +1,5 @@ import { confirm } from '@inquirer/prompts'; +import { BigNumber } from 'ethers'; import { groupBy } from 'lodash-es'; import { Account as StarknetAccount } from 'starknet'; import { stringify as yamlStringify } from 'yaml'; @@ -16,6 +17,7 @@ import { ChainSubmissionStrategySchema, ContractVerifier, EvmERC20WarpModule, + EvmERC20WarpRouteReader, EvmHookModule, EvmIsmModule, ExplorerLicenseType, @@ -34,6 +36,7 @@ import { MultisigIsmConfig, OpStackIsmConfig, PausableIsmConfig, + RemoteRouters, RoutingIsmConfig, STARKNET_SUPPORTED_TOKEN_TYPES, STARKNET_TOKEN_TYPE_TO_STANDARD, @@ -75,6 +78,7 @@ import { import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { requestAndSaveApiKeys } from '../context/context.js'; +import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js'; import { WriteCommandContext } from '../context/types.js'; import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; import { getSubmitterBuilder } from '../submit/submit.js'; @@ -89,6 +93,7 @@ import { import { completeDeploy, prepareDeploy, + prepareStarknetDeploy, runPreflightChecksForChains, } from './utils.js'; @@ -106,17 +111,15 @@ interface WarpApplyParams extends DeployParams { export async function runWarpRouteDeploy({ context, warpRouteDeploymentConfigPath, + multiProtocolSigner, }: { context: WriteCommandContext; warpRouteDeploymentConfigPath?: string; + multiProtocolSigner?: MultiProtocolSignerManager; }) { - const { - skipConfirmation, - chainMetadata, - registry, - multiProvider, - multiProtocolSigner, - } = context; + const { skipConfirmation, chainMetadata, registry } = context; + const multiProvider = await multiProtocolSigner?.getMultiProvider(); + assert(multiProvider, 'No MultiProvider found!'); if ( !warpRouteDeploymentConfigPath || @@ -156,7 +159,7 @@ export async function runWarpRouteDeploy({ const deployments: WarpCoreConfig = { tokens: [] }; let deploymentAddWarpRouteOptions: AddWarpRouteOptions | undefined; - const routerAddresses: { + const deployedContracts: { evm: ChainMap
; starknet: ChainMap
; } = { @@ -166,6 +169,9 @@ export async function runWarpRouteDeploy({ let starknetSigners: ChainMap = {}; + // Collect all initial balances across protocols + let allInitialBalances: Record = {}; + // Execute deployments for each protocol for (const protocol of Object.keys(chainsByProtocol) as ProtocolType[]) { const protocolChains = chainsByProtocol[protocol]; @@ -178,25 +184,30 @@ export async function runWarpRouteDeploy({ chains: protocolChains, minGas: MINIMUM_WARP_DEPLOY_GAS, }); - // const initialBalances = - await prepareDeploy(context, null, protocolChains); - const deployedContracts = await executeDeploy( + const initialBalances = await prepareDeploy( + context, + null, + protocolChains, + ); + allInitialBalances = { ...allInitialBalances, ...initialBalances }; + + const deployedEvmContracts = (await executeDeploy( { context, warpDeployConfig: warpRouteConfig }, apiKeys, - ); + )) as any; // Store EVM router addresses - routerAddresses.evm = objMap( - deployedContracts as HyperlaneContractsMap, + // used in enrollCrossChainRouters + deployedContracts.evm = objMap( + deployedEvmContracts as HyperlaneContractsMap, (_, contracts) => getRouter(contracts).address, ); const { warpCoreConfig, addWarpRouteOptions } = await getWarpCoreConfig( { context, warpDeployConfig: warpRouteConfig }, - deployedContracts, + deployedEvmContracts, ); - deploymentAddWarpRouteOptions = addWarpRouteOptions; deployments.tokens = [ ...deployments.tokens, @@ -221,16 +232,24 @@ export async function runWarpRouteDeploy({ }), {}, ); - routerAddresses.starknet = await executeStarknetDeployments({ + + const initialBalances = await prepareStarknetDeploy( + context, + null, + protocolChains, + ); + allInitialBalances = { ...allInitialBalances, ...initialBalances }; + + deployedContracts.starknet = await executeStarknetDeployments({ starknetSigners, - warpRouteConfig, + warpRouteConfig, // Only pass protocol-specific config multiProvider, }); const { warpCoreConfig, addWarpRouteOptions } = await getWarpCoreConfigForStarknet( - warpRouteConfig, + warpRouteConfig, // Only pass protocol-specific config multiProvider, - routerAddresses.starknet, + deployedContracts.starknet, ); deploymentAddWarpRouteOptions = addWarpRouteOptions; deployments.tokens = [...deployments.tokens, ...warpCoreConfig.tokens]; @@ -239,36 +258,40 @@ export async function runWarpRouteDeploy({ default: throw new Error(`Unsupported protocol type: ${protocol}`); } - // TODO: handle - assert( - deploymentAddWarpRouteOptions, - 'Deployment add warp route options is required', - ); } - // Some of the below functions throw if passed non-EVM chains - // const ethereumChains = chains.filter( - // (chain) => chainMetadata[chain].protocol === ProtocolType.Ethereum, - // ); + logGreen('✅ Warp contract deployments complete'); - // await runPreflightChecksForChains({ - // context, - // chains: ethereumChains, - // minGas: MINIMUM_WARP_DEPLOY_GAS, - // }); + await enrollCrossChainRouters({ + evmAddresses: deployedContracts.evm, + starknetAddresses: deployedContracts.starknet, + context, + warpRouteConfig, + deployments, + multiProvider, + starknetSigners, + }); - // const initialBalances = await prepareDeploy(context, null, ethereumChains); + fullyConnectTokens(deployments); - // const deployedContracts = await executeDeploy(deploymentParams, apiKeys); + await writeDeploymentArtifacts( + deployments, + context, + deploymentAddWarpRouteOptions, + ); - // const { warpCoreConfig, addWarpRouteOptions } = await getWarpCoreConfig( - // deploymentParams, - // deployedContracts, - // ); + // can't be handled in getWarpCoreConfig + // because its not compatible with starknet + fullyConnectTokens(deployments); - // await writeDeploymentArtifacts(warpCoreConfig, context, addWarpRouteOptions); + await writeDeploymentArtifacts( + deployments, + context, + deploymentAddWarpRouteOptions, + ); - // await completeDeploy(context, 'warp', initialBalances, null, ethereumChains!); + // Compatible only with EVM and Starknet chains + await completeDeploy(context, 'warp', allInitialBalances, null, chains); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { @@ -351,6 +374,24 @@ async function resolveWarpIsmAndHook( ): Promise { return promiseObjAll( objMap(warpConfig, async (chain, config) => { + if ( + // for non-evm chains just return config as it is + context.multiProvider.getChainMetadata(chain).protocol !== + ProtocolType.Ethereum || + !config.interchainSecurityModule || + typeof config.interchainSecurityModule === 'string' + ) { + logGray( + `Config Ism is ${ + !config.interchainSecurityModule + ? 'empty' + : config.interchainSecurityModule + }, skipping deployment.`, + ); + return config; + } + + logBlue(`Loading registry factory addresses for ${chain}...`); const registryAddresses = await context.registry.getAddresses(); const ccipContractCache = new CCIPContractCache(registryAddresses); const chainAddresses = registryAddresses[chain]; @@ -1214,7 +1255,144 @@ async function getWarpCoreConfigCore( return { warpCoreConfig, addWarpRouteOptions: { symbol } }; } -function getRouter(contracts: HyperlaneContracts) { +async function enrollStarknetRoutersOnEvmChains( + multiProvider: MultiProvider, + evmChains: ChainName[], + evmRouterAddresses: ChainMap
, + starknetDeployedAddresses: ChainMap
, + registryAddresses: ChainMap, +): Promise { + const transactions: AnnotatedEV5Transaction[] = []; + + await promiseObjAll( + objMap(evmRouterAddresses, async (evmChain, evmRouterAddress) => { + if (!evmChains.includes(evmChain)) return; + + // Create warp route reader for the EVM chain + const warpRouteReader = new EvmERC20WarpRouteReader( + multiProvider, + evmChain, + ); + + // Get current config from the deployed router + const mutatedWarpRouteConfig = + await warpRouteReader.deriveWarpRouteConfig(evmRouterAddress); + + // Filter for only Starknet chains + const starknetChains = Object.keys(starknetDeployedAddresses).filter( + (chain) => + multiProvider.getChainMetadata(chain).protocol === + ProtocolType.Starknet, + ); + + // Add Starknet routers to the config + mutatedWarpRouteConfig.remoteRouters = + starknetChains.reduce((remoteRouters, starknetChain) => { + remoteRouters[multiProvider.getDomainId(starknetChain)] = { + address: starknetDeployedAddresses[starknetChain], + }; + return remoteRouters; + }, {}); + + const { + domainRoutingIsmFactory, + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + } = registryAddresses[evmChain]; + + // Create warp module to update the router + const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { + config: mutatedWarpRouteConfig, + chain: evmChain, + addresses: { + deployedTokenRoute: evmRouterAddress, + domainRoutingIsmFactory, + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + }, + }); + + // Generate transactions to update the router + const chainTxs = await evmERC20WarpModule.update(mutatedWarpRouteConfig); + if (chainTxs.length > 0) { + transactions.push(...chainTxs); + } + }), + ); + + return transactions; +} + +interface EnrollRoutersParams { + evmAddresses: ChainMap
; + starknetAddresses: ChainMap
; + context: WriteCommandContext; + warpRouteConfig: WarpRouteDeployConfigMailboxRequired; + deployments: WarpCoreConfig; + multiProvider: MultiProvider; + starknetSigners: ChainMap; +} + +async function enrollCrossChainRouters({ + evmAddresses, + starknetAddresses, + context, + warpRouteConfig, + deployments, + multiProvider, + starknetSigners, +}: EnrollRoutersParams): Promise { + const hasEvmChains = Object.keys(evmAddresses).length > 0; + const hasStarknetChains = Object.keys(starknetAddresses).length > 0; + + if (!hasEvmChains || !hasStarknetChains) return; + + logBlue('Enrolling Starknet routers with EVM chains...'); + + const registryAddresses = await context.registry.getAddresses(); + const evmChains = Object.keys(evmAddresses); + + const starknetWarpModule = new StarknetERC20WarpModule( + starknetSigners, + warpRouteConfig, + multiProvider, + ); + + await starknetWarpModule.enrollRemoteRouters({ + ...evmAddresses, + ...starknetAddresses, + }); + const evmEnrollmentTxs = await enrollStarknetRoutersOnEvmChains( + multiProvider, + evmChains, + evmAddresses, + starknetAddresses, + registryAddresses, + ); + + if (evmEnrollmentTxs.length === 0) return; + + const chainTransactions = groupBy(evmEnrollmentTxs, 'chainId'); + await submitWarpApplyTransactions( + { + context, + warpDeployConfig: warpRouteConfig, + warpCoreConfig: deployments, + receiptsDir: './generated/transactions', + }, + chainTransactions, + ); +} + +export function getRouter(contracts: HyperlaneContracts) { for (const key of objKeys(hypERC20factories)) { if (contracts[key]) return contracts[key]; } diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index 42eafe2ce15..853ce5c6d68 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -1,25 +1,12 @@ -import { stringify as yamlStringify } from 'yaml'; +import { HyperlaneCore, StarknetCore } from '@hyperlane-xyz/sdk'; +import { ChainName, MessageService } from '@hyperlane-xyz/sdk'; +import { ProtocolType, timeout } from '@hyperlane-xyz/utils'; -import { - ChainName, - HyperlaneCore, - HyperlaneRelayer, - StarknetCore, -} from '@hyperlane-xyz/sdk'; -import { - ProtocolType, - addressToBytes32, - assert, - timeout, -} from '@hyperlane-xyz/utils'; - -import { EXPLORER_URL, MINIMUM_TEST_SEND_GAS } from '../consts.js'; -import { CommandContext, WriteCommandContext } from '../context/types.js'; +import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; +import { WriteCommandContext } from '../context/types.js'; import { runPreflightChecksForChains } from '../deploy/utils.js'; -import { errorRed, log, logBlue, logGreen } from '../logger.js'; +import { log, logBlue, logGreen } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; -import { indentYamlOrJson } from '../utils/files.js'; -import { stubMerkleTreeConfig } from '../utils/relay.js'; export async function sendTestMessage({ context, @@ -38,8 +25,9 @@ export async function sendTestMessage({ skipWaitForDelivery: boolean; selfRelay?: boolean; }) { - const { chainMetadata } = context; + const { chainMetadata, multiProvider } = context; + // Chain selection if not provided if (!origin) { origin = await runSingleChainSelectionStep( chainMetadata, @@ -54,6 +42,7 @@ export async function sendTestMessage({ ); } + // Preflight checks await runPreflightChecksForChains({ context, chains: [origin, destination], @@ -61,117 +50,60 @@ export async function sendTestMessage({ minGas: MINIMUM_TEST_SEND_GAS, }); - await timeout( - executeDelivery({ - context, - origin, - destination, - messageBody, - skipWaitForDelivery, - selfRelay, - }), - timeoutSec * 1000, - 'Timed out waiting for messages to be delivered', - ); -} - -async function executeDelivery({ - context, - origin, - destination, - messageBody, - skipWaitForDelivery, - selfRelay, -}: { - context: CommandContext; - origin: ChainName; - destination: ChainName; - messageBody: string; - skipWaitForDelivery: boolean; - selfRelay?: boolean; -}) { - const { registry, multiProvider, multiProtocolSigner } = context; - const chainAddresses = await registry.getAddresses(); - const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); - const destinationDomain = multiProvider.getDomainId(destination); - const originProtocol = multiProvider.getProtocol(origin); - - if (originProtocol === ProtocolType.Starknet) { - assert(multiProtocolSigner, 'MultiProtocolSignerManager is required'); - - const starknetSigner = await multiProtocolSigner.getStarknetSigner(origin); - assert(starknetSigner, `Signer for ${origin} chain is required`); - - const starknet = new StarknetCore( - starknetSigner, - chainAddresses, - multiProvider, - ); - const { messages } = await starknet.sendMessage({ - origin, - destinationDomain, - messageBody, - recipientAddress: chainAddresses[destination].testRecipient, - }); - - console.log({ messages }); - if (messages.length > 0) { - const message = messages[0]; - logBlue( - `Sent message from ${origin} to ${chainAddresses[destination].testRecipient} on ${destination}.`, - ); - logBlue(`Message ID: ${message.id}`); - log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`); + const addressMap = await context.registry.getAddresses(); + + // Create protocol-specific cores map + const protocolCores: Partial< + Record + > = {}; + + // Initialize cores for the chains we're working with + for (const chain of [origin, destination]) { + const protocol = chainMetadata[chain].protocol; + + // Only initialize each protocol type once + if (!protocolCores[protocol]) { + if (protocol === ProtocolType.Starknet) { + protocolCores[protocol] = new StarknetCore( + addressMap, + multiProvider, + context.multiProtocolSigner!, + context.multiProtocolProvider!, + ); + } else { + // For all other protocols, use HyperlaneCore + protocolCores[protocol] = HyperlaneCore.fromAddressesMap( + addressMap, + multiProvider, + ); + } } - - return; } - try { - const recipient = chainAddresses[destination].testRecipient; - if (!recipient) { - throw new Error(`Unable to find TestRecipient for ${destination}`); - } - const formattedRecipient = addressToBytes32(recipient); - - log('Dispatching message'); - const { dispatchTx, message } = await core.sendMessage( - origin, - destination, - formattedRecipient, - messageBody, - // override the default hook (with IGP) for self-relay to avoid race condition with the production relayer - selfRelay ? chainAddresses[origin].merkleTreeHook : undefined, - ); - logBlue(`Sent message from ${origin} to ${recipient} on ${destination}.`); - logBlue(`Message ID: ${message.id}`); - logBlue(`Explorer Link: ${EXPLORER_URL}/message/${message.id}`); - log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`); - - if (selfRelay) { - const relayer = new HyperlaneRelayer({ core }); + const messageService = new MessageService(multiProvider, protocolCores); - const hookAddress = await core.getSenderHookAddress(message); - const merkleAddress = chainAddresses[origin].merkleTreeHook; - stubMerkleTreeConfig(relayer, origin, hookAddress, merkleAddress); - - log('Attempting self-relay of message'); - await relayer.relayMessage(dispatchTx); - logGreen('Message was self-relayed!'); - } else { - if (skipWaitForDelivery) { - return; + await timeout( + Promise.resolve().then(async () => { + logBlue(`Sending message from ${origin} to ${destination}`); + + const { message } = await messageService.sendMessage({ + origin: origin!, + destination: destination!, + recipient: addressMap[destination!].testRecipient, + body: messageBody, + }); + + log(`Message dispatched with ID: ${message.id}`); + + if (selfRelay) { + log('Attempting self-relay of message'); + await messageService.relayMessage(message); + logGreen('Message was self-relayed!'); + } else if (!skipWaitForDelivery) { + log('Waiting for message delivery...'); } - - log('Waiting for message delivery on destination chain...'); - // Max wait 10 minutes - await core.waitForMessageProcessed(dispatchTx, 10000, 60); - logGreen('Message was delivered!'); - } - } catch (e) { - errorRed( - `Encountered error sending message from ${origin} to ${destination}`, - ); - throw e; - } + }), + timeoutSec * 1000, + 'Timed out waiting for message to be delivered', + ); } diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index 1fabc3c853a..4ebc2087094 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -1,25 +1,26 @@ +// import { stubMerkleTreeConfig } from '../utils/relay.js'; import { stringify as yamlStringify } from 'yaml'; import { ChainName, DispatchedMessage, HyperlaneCore, - HyperlaneRelayer, + MessageService, MultiProtocolProvider, ProviderType, + StarknetCore, Token, TokenAmount, WarpCore, WarpCoreConfig, } from '@hyperlane-xyz/sdk'; -import { parseWarpRouteMessage, timeout } from '@hyperlane-xyz/utils'; +import { ProtocolType, timeout } from '@hyperlane-xyz/utils'; import { EXPLORER_URL, MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { WriteCommandContext } from '../context/types.js'; import { runPreflightChecksForChains } from '../deploy/utils.js'; import { log, logBlue, logGreen, logRed } from '../logger.js'; import { indentYamlOrJson } from '../utils/files.js'; -import { stubMerkleTreeConfig } from '../utils/relay.js'; import { runTokenSelectionStep } from '../utils/tokens.js'; export const WarpSendLogs = { @@ -95,22 +96,46 @@ async function executeDelivery({ selfRelay?: boolean; }) { const { multiProvider, registry } = context; + const { chainMetadata } = context; - const signer = multiProvider.getSigner(origin); - const recipientSigner = multiProvider.getSigner(destination); + const chainAddresses = await registry.getAddresses(); - const recipientAddress = await recipientSigner.getAddress(); - const signerAddress = await signer.getAddress(); + // Create protocol-specific cores map + const protocolCores: Partial< + Record + > = {}; - recipient ||= recipientAddress; + // Initialize cores for the chains + for (const chain of [origin, destination]) { + const protocol = chainMetadata[chain].protocol; + if (!protocolCores[protocol]) { + if (protocol === ProtocolType.Starknet) { + protocolCores[protocol] = new StarknetCore( + chainAddresses, + multiProvider, + context.multiProtocolSigner!, + context.multiProtocolProvider!, + ); + } else { + protocolCores[protocol] = HyperlaneCore.fromAddressesMap( + chainAddresses, + multiProvider, + ); + } + } + } - const chainAddresses = await registry.getAddresses(); + const messageService = new MessageService(multiProvider, protocolCores); - const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider); - - const provider = multiProvider.getProvider(origin); - const connectedSigner = signer.connect(provider); + const { signerAddress, recipientAddress } = + await getSignerAndRecipientAddresses({ + context, + origin, + destination, + recipient, + }); + recipient ||= recipientAddress; const warpCore = WarpCore.FromConfig( MultiProtocolProvider.fromMultiProvider(multiProvider), warpCoreConfig, @@ -150,18 +175,27 @@ async function executeDelivery({ const txReceipts = []; for (const tx of transferTxs) { - if (tx.type === ProviderType.EthersV5) { - const txResponse = await connectedSigner.sendTransaction(tx.transaction); - const txReceipt = await multiProvider.handleTx(origin, txResponse); - txReceipts.push(txReceipt); - } + const txReceipt = await executeTxByType(tx, origin, context, multiProvider); + txReceipts.push(txReceipt); } + const transferTxReceipt = txReceipts[txReceipts.length - 1]; const messageIndex: number = 0; - const message: DispatchedMessage = - HyperlaneCore.getDispatchedMessages(transferTxReceipt)[messageIndex]; - const parsed = parseWarpRouteMessage(message.parsed.body); + const message = parseMessageFromReceipt( + transferTxReceipt, + origin, + messageIndex, + protocolCores, + ); + + // const parsedBody = message?.parsed?.body + // ? parseWarpRouteMessage(message.parsed.body) + // : null; + + // if (parsedBody) { + // log(`Body:\n${indentYamlOrJson(yamlStringify(parsedBody, null, 2), 4)}`); + // } logBlue( `Sent transfer from sender (${signerAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`, @@ -169,24 +203,121 @@ async function executeDelivery({ logBlue(`Message ID: ${message.id}`); logBlue(`Explorer Link: ${EXPLORER_URL}/message/${message.id}`); log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`); - log(`Body:\n${indentYamlOrJson(yamlStringify(parsed, null, 2), 4)}`); + + logBlue(`Message dispatched with ID: ${message.id}`); if (selfRelay) { - const relayer = new HyperlaneRelayer({ core }); + log('Attempting self-relay of message'); + await messageService.relayMessage(message); + logGreen(WarpSendLogs.SUCCESS); + } else if (!skipWaitForDelivery) { + log('Waiting for message delivery...'); + await messageService.awaitMessagesDelivery(message, 10000, 60); + logGreen('Transfer sent to destination chain!'); + } +} - const hookAddress = await core.getSenderHookAddress(message); - const merkleAddress = chainAddresses[origin].merkleTreeHook; - stubMerkleTreeConfig(relayer, origin, hookAddress, merkleAddress); +async function executeTxByType( + tx: any, + chain: ChainName, + context: WriteCommandContext, + multiProvider: any, +) { + switch (tx.type) { + case ProviderType.EthersV5: { + return executeEthersTransaction(tx, chain, multiProvider); + } + case ProviderType.Starknet: { + return executeStarknetTransaction(tx, chain, context); + } + default: + throw new Error(`Unsupported provider type: ${tx.type}`); + } +} - log('Attempting self-relay of transfer...'); - await relayer.relayMessage(transferTxReceipt, messageIndex, message); - logGreen(WarpSendLogs.SUCCESS); - return; +async function executeEthersTransaction( + tx: any, + chain: ChainName, + multiProvider: any, +) { + const signer = multiProvider.getSigner(chain); + const provider = multiProvider.getProvider(chain); + const connectedSigner = signer.connect(provider); + const txResponse = await connectedSigner.sendTransaction(tx.transaction); + return multiProvider.handleTx(chain, txResponse); +} + +async function executeStarknetTransaction( + tx: any, + chain: ChainName, + context: WriteCommandContext, +) { + const starknetSigner = context.multiProtocolSigner!.getStarknetSigner(chain)!; + const txResponse = await starknetSigner.execute([tx.transaction as any]); + return starknetSigner.waitForTransaction(txResponse.transaction_hash); +} + +function parseMessageFromReceipt( + receipt: any, + origin: ChainName, + messageIndex: number, + protocolCores: Partial>, +): DispatchedMessage { + if ('transaction_hash' in receipt) { + return ( + protocolCores.starknet! as StarknetCore + ).parseDispatchedMessagesFromReceipt(receipt, origin); + } + + return HyperlaneCore.getDispatchedMessages(receipt)[messageIndex]; +} + +async function getSignerAndRecipientAddresses({ + context, + origin, + destination, + recipient, +}: { + context: WriteCommandContext; + origin: ChainName; + destination: ChainName; + recipient?: string; +}): Promise<{ + signerAddress: string; + recipientAddress: string; +}> { + const { multiProvider } = context; + const originMetadata = multiProvider.getChainMetadata(origin); + const destinationMetadata = multiProvider.getChainMetadata(destination); + + // Get signer address based on origin protocol + let signerAddress: string; + if (originMetadata.protocol === ProtocolType.Starknet) { + const starknetSigner = + context.multiProtocolSigner!.getStarknetSigner(origin); + signerAddress = starknetSigner.address; + } else { + // EVM-based chains + const evmSigner = multiProvider.getSigner(origin); + signerAddress = await evmSigner.getAddress(); } - if (skipWaitForDelivery) return; + // Get recipient address based on destination protocol + let recipientAddress: string; + if (recipient) { + recipientAddress = recipient; + } else if (destinationMetadata.protocol === ProtocolType.Starknet) { + const starknetSigner = + context.multiProtocolSigner!.getStarknetSigner(destination); + recipientAddress = starknetSigner.address; + } else { + // EVM-based chains + const evmSigner = multiProvider.getSigner(destination); + recipientAddress = await evmSigner.getAddress(); + } - // Max wait 10 minutes - await core.waitForMessageProcessed(transferTxReceipt, 10000, 60); - logGreen(`Transfer sent to ${destination} chain!`); + return { + signerAddress, + recipientAddress, + }; } diff --git a/typescript/cli/src/utils/env.ts b/typescript/cli/src/utils/env.ts index 5f299cfeae6..46726c1d892 100644 --- a/typescript/cli/src/utils/env.ts +++ b/typescript/cli/src/utils/env.ts @@ -8,6 +8,7 @@ const envScheme = z.object({ AWS_SECRET_ACCESS_KEY: z.string().optional(), AWS_REGION: z.string().optional(), GH_AUTH_TOKEN: z.string().optional(), + STARKNET_TRANSACTION_VERSION: z.string().optional(), }); const parsedEnv = envScheme.safeParse(process.env); From 24e253173ddfd4eeb352663280eb276dd15d89ee Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 24 Apr 2025 14:17:19 +0200 Subject: [PATCH 132/132] chore: merge sdk from feat/starknet-send-relay --- typescript/sdk/src/app/MultiProtocolApp.ts | 9 + typescript/sdk/src/bus/MessageBus.ts | 328 ++++++++++++++++ typescript/sdk/src/bus/adapters/EvmAdapter.ts | 160 ++++++++ .../sdk/src/bus/adapters/StarknetAdapter.ts | 105 ++++++ typescript/sdk/src/bus/types.ts | 51 +++ typescript/sdk/src/consts/testChains.ts | 19 + typescript/sdk/src/core/MultiProtocolCore.ts | 2 + typescript/sdk/src/core/StarknetCore.ts | 354 ++++++++++++------ typescript/sdk/src/core/StarknetCoreModule.ts | 92 +++-- typescript/sdk/src/core/StarknetCoreReader.ts | 21 +- .../src/core/adapters/StarknetCoreAdapter.ts | 135 +++++++ typescript/sdk/src/deploy/StarknetDeployer.ts | 51 ++- typescript/sdk/src/hook/StarknetHookReader.ts | 188 ++++++++-- typescript/sdk/src/index.ts | 31 ++ typescript/sdk/src/ism/HyperlaneIsmFactory.ts | 18 +- typescript/sdk/src/ism/StarknetIsmReader.ts | 222 +++++------ .../sdk/src/messaging/MessageService.ts | 80 ++++ typescript/sdk/src/messaging/messageUtils.ts | 199 ++++++++++ typescript/sdk/src/providers/MultiProvider.ts | 2 +- typescript/sdk/src/providers/ProviderType.ts | 7 +- typescript/sdk/src/providers/rpcHealthTest.ts | 13 + .../src/providers/transactionFeeEstimators.ts | 33 ++ .../sdk/src/token/StarknetERC20WarpModule.ts | 134 +++---- .../src/token/StarknetERC20WarpRouteReader.ts | 306 +++++++++++++++ typescript/sdk/src/token/Token.ts | 17 + .../token/adapters/StarknetTokenAdapter.ts | 284 ++++++++++++++ typescript/sdk/src/token/deploy.ts | 12 +- typescript/sdk/src/utils/starknet.ts | 224 +++++++++++ typescript/sdk/src/warp/WarpCore.test.ts | 9 +- typescript/sdk/src/warp/WarpCore.ts | 58 ++- .../sdk/src/warp/test-warp-core-config.yaml | 8 + 31 files changed, 2733 insertions(+), 439 deletions(-) create mode 100644 typescript/sdk/src/bus/MessageBus.ts create mode 100644 typescript/sdk/src/bus/adapters/EvmAdapter.ts create mode 100644 typescript/sdk/src/bus/adapters/StarknetAdapter.ts create mode 100644 typescript/sdk/src/bus/types.ts create mode 100644 typescript/sdk/src/core/adapters/StarknetCoreAdapter.ts create mode 100644 typescript/sdk/src/messaging/MessageService.ts create mode 100644 typescript/sdk/src/messaging/messageUtils.ts create mode 100644 typescript/sdk/src/token/StarknetERC20WarpRouteReader.ts create mode 100644 typescript/sdk/src/token/adapters/StarknetTokenAdapter.ts create mode 100644 typescript/sdk/src/utils/starknet.ts diff --git a/typescript/sdk/src/app/MultiProtocolApp.ts b/typescript/sdk/src/app/MultiProtocolApp.ts index 6444d68a164..215ef0e201f 100644 --- a/typescript/sdk/src/app/MultiProtocolApp.ts +++ b/typescript/sdk/src/app/MultiProtocolApp.ts @@ -16,6 +16,7 @@ import { CosmJsWasmProvider, EthersV5Provider, SolanaWeb3Provider, + StarknetJsProvider, TypedProvider, } from '../providers/ProviderType.js'; import { ChainMap, ChainName } from '../types.js'; @@ -95,6 +96,14 @@ export class BaseSealevelAdapter extends BaseAppAdapter { } } +export class BaseStarknetAdapter extends BaseAppAdapter { + public readonly protocol: ProtocolType = ProtocolType.Starknet; + + public getProvider(): StarknetJsProvider['provider'] { + return this.multiProvider.getStarknetProvider(this.chainName); + } +} + /** * A version of HyperlaneApp that can support different * provider types across different protocol types. diff --git a/typescript/sdk/src/bus/MessageBus.ts b/typescript/sdk/src/bus/MessageBus.ts new file mode 100644 index 00000000000..77b88b62e0a --- /dev/null +++ b/typescript/sdk/src/bus/MessageBus.ts @@ -0,0 +1,328 @@ +import { Logger } from 'pino'; + +import { ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; + +import { + formatParsedStarknetMessageForEthereum, + translateMessage as translateMessageUtil, +} from '../messaging/messageUtils.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; + +import { + MessageBusConfig, + MessageHandler, + MessageWithStatus, +} from './types.js'; + +/** + * Default configuration for the message bus + */ +const DEFAULT_CONFIG: MessageBusConfig = { + maxAttempts: 10, + retryTimeout: 1000, // 1 second retry timeout +}; + +/** + * A message bus that manages the routing and delivery of cross-protocol messages + */ +export class MessageBus { + private readonly config: MessageBusConfig; + protected readonly multiProvider: MultiProvider; + private backlog: MessageWithStatus[] = []; + private handlers: Map = new Map(); + private readonly logger: Logger; + private isProcessing = false; + private processingInterval: ReturnType | null = null; + + /** + * Creates a new MessageBus + * @param config Configuration options + * @param multiProvider The multi-provider instance + * @param logger Optional logger instance + */ + constructor( + multiProvider: MultiProvider, + config?: Partial, + logger?: Logger, + ) { + this.multiProvider = multiProvider; + this.config = { ...DEFAULT_CONFIG, ...config }; + this.logger = (logger || rootLogger).child({ module: 'MessageBus' }); + } + + /** + * Registers a message handler for a specific protocol + * @param handler The handler to register + */ + registerHandler(handler: MessageHandler): void { + if (!this.handlers.has(handler.protocol)) { + this.handlers.set(handler.protocol, []); + } + + this.handlers.get(handler.protocol)!.push(handler); + this.logger.info(`Registered handler for protocol ${handler.protocol}`); + } + + /** + * Publishes a message to the message bus + * @param message The base dispatched message to publish + */ + publish(message: MessageWithStatus): void { + // Don't add duplicates based on message ID + if (this.backlog.some((m) => m.id === message.id)) { + this.logger.debug(`Message ${message.id} already in backlog, skipping`); + return; + } + + this.backlog.push(message); + this.logger.info( + `Added message ${message.id} to backlog (${this.backlog.length} total)`, + ); + } + + /** + * Starts the message bus processing loop + * @param intervalMs How often to check for new messages, in milliseconds + */ + start(intervalMs = 1000): void { + if (this.processingInterval) { + this.logger.warn('Message bus already started'); + return; + } + + this.logger.info('Starting message bus'); + this.processingInterval = setInterval(() => { + this.processNextBatch().catch((err) => { + this.logger.error({ err }, 'Error processing message batch'); + }); + }, intervalMs); + } + + /** + * Stops the message bus processing loop + */ + stop(): void { + if (!this.processingInterval) { + this.logger.warn('Message bus not started'); + return; + } + + this.logger.info('Stopping message bus'); + clearInterval(this.processingInterval); + this.processingInterval = null; + } + + /** + * Returns the current backlog of messages + */ + getBacklog(): MessageWithStatus[] { + return [...this.backlog]; + } + + /** + * Process a batch of pending messages + */ + private async processNextBatch(): Promise { + if (this.isProcessing) { + return; + } + + this.isProcessing = true; + + try { + // Get pending messages that are ready to be processed + const now = Date.now(); + const messagesToProcess = this.backlog.filter((message) => { + if (message.status !== 'pending') { + return false; + } + + // Apply backoff if this isn't the first attempt + if (message.attempts > 0) { + const backoffTime = message.attempts * this.config.retryTimeout; + return now - message.lastAttempt > backoffTime; + } + + return true; + }); + + if (messagesToProcess.length === 0) { + return; + } + + this.logger.debug(`Processing ${messagesToProcess.length} messages`); + + // Process each message + for (const message of messagesToProcess) { + await this.processMessage(message); + } + + // Clean up delivered/failed messages + this.cleanupBacklog(); + } finally { + this.isProcessing = false; + } + } + + /** + * Process a single message + */ + private async processMessage(message: MessageWithStatus): Promise { + const { id, parsed } = message; + const destinationDomain = parsed.destination; + const originDomain = parsed.origin; + + // Find the destination chain name and protocol + const destinationChain = + this.multiProvider.tryGetChainName(destinationDomain); + const destinationProtocol = this.multiProvider.tryGetProtocol( + destinationChain!, + ); + + // Find the origin chain name and protocol + const originChain = this.multiProvider.tryGetChainName(originDomain); + const originProtocol = this.multiProvider.tryGetProtocol(originChain!); + + // Update status and attempt count + message.status = 'processing'; + message.attempts += 1; + message.lastAttempt = Date.now(); + + const handlers = this.handlers.get(destinationProtocol!) || []; + + if (handlers.length === 0) { + this.logger.warn( + `No handlers registered for protocol ${destinationProtocol}, message ${id} cannot be processed`, + ); + message.status = 'failed'; + return; + } + + // Try each handler until one succeeds + let processed = false; + for (const handler of handlers) { + try { + this.logger.debug( + `Attempting to process message ${id} with handler for ${handler.protocol}`, + ); + + // Translate the message if the origin protocol is different from the destination protocol + let translatedMessage = message; + if ( + originProtocol && + destinationProtocol && + originProtocol !== destinationProtocol + ) { + translatedMessage = this.translateMessage( + message, + originProtocol, + destinationProtocol, + ); + // translate message.parsed + translatedMessage.parsed = { + ...translatedMessage.parsed, + recipient: translatedMessage.parsed.recipient, + sender: translatedMessage.parsed.sender, + }; + + this.logger.debug( + `Translated message ${id} from ${originProtocol} to ${destinationProtocol}`, + ); + } + + processed = await handler.processMessage(translatedMessage); + + if (processed) { + this.logger.info(`Successfully processed message ${id}`); + message.status = 'delivered'; + break; + } + } catch (error) { + this.logger.error( + { error }, + `Error processing message ${id} with handler for ${handler.protocol}`, + ); + } + } + + // If no handler succeeded and we've reached the max attempts, mark as failed + if (message.status === 'processing') { + if (message.attempts >= this.config.maxAttempts) { + this.logger.error( + `Failed to process message ${id} after ${message.attempts} attempts, marking as failed`, + ); + message.status = 'failed'; + } else { + this.logger.info( + `Message ${id} processing attempt ${message.attempts} failed, will retry later`, + ); + message.status = 'pending'; + } + } + } + + /** + * Translates a message between protocols + * @param message The message to translate + * @param originProtocol The origin protocol + * @param destinationProtocol The destination protocol + * @returns The translated message + */ + private translateMessage( + message: MessageWithStatus, + originProtocol: ProtocolType, + destinationProtocol: ProtocolType, + ): MessageWithStatus { + // Create a shallow copy of the message + const translatedMessage = { ...message }; + + // Special case handling for Starknet to Ethereum translation + if ( + originProtocol === ProtocolType.Starknet && + destinationProtocol === ProtocolType.Ethereum && + message.parsed + ) { + this.logger.debug( + `Translating Starknet message ${message.id} to Ethereum format`, + ); + + // TODO: fix types + const formattedParsed = formatParsedStarknetMessageForEthereum( + message.parsed as any, + ); + + translatedMessage.parsed = formattedParsed; + } + + // Standard translation flow for other protocol combinations + const translatedPayload = translateMessageUtil( + message, + originProtocol, + destinationProtocol, + ); + + // Update the message with the translated payload + translatedMessage.message = translatedPayload; + + return translatedMessage; + } + + /** + * Remove delivered/failed messages from the backlog + */ + private cleanupBacklog(): void { + // Keep pending/processing messages, remove delivered/failed ones + const oldLength = this.backlog.length; + this.backlog = this.backlog.filter( + (message) => + message.status === 'pending' || message.status === 'processing', + ); + + const removedCount = oldLength - this.backlog.length; + if (removedCount > 0) { + this.logger.debug( + `Removed ${removedCount} processed messages from backlog`, + ); + } + } +} diff --git a/typescript/sdk/src/bus/adapters/EvmAdapter.ts b/typescript/sdk/src/bus/adapters/EvmAdapter.ts new file mode 100644 index 00000000000..520aea9e8e4 --- /dev/null +++ b/typescript/sdk/src/bus/adapters/EvmAdapter.ts @@ -0,0 +1,160 @@ +import { Logger } from 'pino'; + +import { + Address, + ProtocolType, + bytes32ToAddress, + rootLogger, +} from '@hyperlane-xyz/utils'; + +import { HyperlaneCore } from '../../core/HyperlaneCore.js'; +import { DispatchedMessage } from '../../core/types.js'; +import { getMessageMetadata } from '../../messaging/messageUtils.js'; +import { ChainMap } from '../../types.js'; +import { MessageHandler, MessageWithStatus } from '../types.js'; + +/** + * Adapter for handling messages between EVM chains + */ +export class EvmAdapter implements MessageHandler { + protocol = ProtocolType.Ethereum; + protected logger: Logger; + protected core: HyperlaneCore; + protected whitelist: ChainMap> | undefined; + + /** + * Creates a new EvmAdapter + * @param core The Hyperlane core instance + * @param whitelist Optional whitelist of addresses to filter messages + * @param logger Optional logger instance + */ + constructor( + core: HyperlaneCore, + whitelist?: ChainMap, + logger?: Logger, + ) { + this.core = core; + this.logger = (logger || rootLogger).child({ module: 'EvmAdapter' }); + + if (whitelist) { + this.whitelist = Object.fromEntries( + Object.entries(whitelist).map(([chain, addresses]) => [ + chain, + new Set(addresses as Address[]), + ]), + ); + } + } + + /** + * Register this adapter to listen for messages from Hyperlane core + * and publish them to the message bus + * @param messageBus The message bus to publish to + * @returns A function to stop listening + */ + listenForMessages(messageBus: { + publish: (message: MessageWithStatus) => void; + }): () => void { + const chainNames = this.whitelist ? Object.keys(this.whitelist) : undefined; + + const { removeHandler } = this.core.onDispatch( + async (message: DispatchedMessage, event: any) => { + // if (this.whitelist && !this.messageMatchesWhitelist(message.parsed)) { + // this.logger.debug( + // `Skipping message ${message.id} - doesn't match whitelist`, + // ); + // return; + // } + + const enrichedMessage = this.enrichMessage(message, event); + messageBus.publish(enrichedMessage); + }, + chainNames, + ); + + return () => removeHandler(chainNames); + } + + /** + * Process a message destined for an EVM chain + * @param message The message to process + * @returns true if successful, false otherwise + */ + async processMessage(message: MessageWithStatus): Promise { + try { + // TODO: Implement EVM message processing + // // Get ISM metadata + // const ism = await this.core.getRecipientIsmConfig(message); + // const hook = await this.core.getHookConfig(message); + + // // Use the metadataBuilder from the core to build the metadata + // const metadata = await (this.core as any).metadataBuilder.build({ + // message, + // ism, + // hook, + // dispatchTx: { transactionHash: message.dispatchTx }, + // }); + + await this.core.deliver( + message, + getMessageMetadata(ProtocolType.Ethereum), // Evals to '0x001' + ); + return true; + } catch (error) { + this.logger.error({ error }, `Error processing message ${message.id}`); + return false; + } + } + + /** + * Add status tracking fields to a dispatched message + */ + protected enrichMessage( + message: DispatchedMessage, + event: any, + ): MessageWithStatus { + return { + ...message, + attempts: 0, + lastAttempt: Date.now(), + dispatchTx: event.transactionHash, + status: 'pending', + }; + } + + /** + * Check if a message matches the whitelist + */ + protected messageMatchesWhitelist(message: any): boolean { + if (!this.whitelist) { + return true; + } + + const originAddresses = + this.whitelist[message.originChain ?? message.origin]; + if (!originAddresses) { + return false; + } + + const sender = bytes32ToAddress(message.sender); + if (originAddresses.size !== 0 && !originAddresses.has(sender)) { + return false; + } + + const destinationAddresses = + this.whitelist[message.destinationChain ?? message.destination]; + if (!destinationAddresses) { + return false; + } + + const recipient = bytes32ToAddress(message.recipient); + if ( + destinationAddresses.size !== 0 && + !destinationAddresses.has(recipient) + ) { + return false; + } + + return true; + } +} diff --git a/typescript/sdk/src/bus/adapters/StarknetAdapter.ts b/typescript/sdk/src/bus/adapters/StarknetAdapter.ts new file mode 100644 index 00000000000..2e43292513a --- /dev/null +++ b/typescript/sdk/src/bus/adapters/StarknetAdapter.ts @@ -0,0 +1,105 @@ +import { Logger } from 'pino'; +import { ParsedEvent } from 'starknet'; + +import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; + +import { StarknetCore } from '../../core/StarknetCore.js'; +import { DispatchedMessage } from '../../core/types.js'; +import { getMessageMetadata } from '../../messaging/messageUtils.js'; +import { ChainMap } from '../../types.js'; +import { MessageHandler, MessageWithStatus } from '../types.js'; + +/** + * Adapter for handling messages between Starknet chains + */ +export class StarknetAdapter implements MessageHandler { + protocol = ProtocolType.Starknet; + protected logger: Logger; + protected core: StarknetCore; + protected whitelist: ChainMap> | undefined; + + /** + * Creates a new StarknetAdapter + * @param core The Starknet core instance + * @param whitelist Optional whitelist of addresses to filter messages + * @param logger Optional logger instance + */ + constructor( + core: StarknetCore, + whitelist?: ChainMap, + logger?: Logger, + ) { + this.core = core; + this.logger = (logger || rootLogger).child({ module: 'StarknetAdapter' }); + + if (whitelist) { + this.whitelist = Object.fromEntries( + Object.entries(whitelist).map(([chain, addresses]) => [ + chain, + new Set(addresses as Address[]), + ]), + ); + } + } + + /** + * Register this adapter to listen for messages from Starknet core + * and publish them to the message bus + * @param messageBus The message bus to publish to + * @returns A function to stop listening + */ + listenForMessages(messageBus: { + publish: (message: MessageWithStatus) => void; + }): () => void { + const chainNames = this.whitelist ? Object.keys(this.whitelist) : undefined; + + const { removeHandler } = this.core.onDispatch( + async (message: DispatchedMessage, event: any) => { + // Note: Currently not implementing whitelist for Starknet messages + // We could implement this logic similar to the EvmAdapter once + // whitelist filtering for Starknet is fully implemented in StarknetCore + + const enrichedMessage = this.enrichMessage(message, event); + messageBus.publish(enrichedMessage); + }, + chainNames, + ); + + return () => removeHandler(chainNames); + } + + /** + * Process a message destined for a Starknet chain + * @param message The message to process + * @returns true if successful, false otherwise + */ + async processMessage(message: MessageWithStatus): Promise { + try { + // Get metadata for message delivery + const metadata = getMessageMetadata(ProtocolType.Starknet); + + // Deliver the message + await this.core.deliver(message, metadata); + return true; + } catch (error) { + this.logger.error({ error }, `Error processing message ${message.id}`); + return false; + } + } + + /** + * Add status tracking fields to a dispatched message + */ + protected enrichMessage( + message: DispatchedMessage, + event: ParsedEvent, + ): MessageWithStatus { + return { + ...message, + attempts: 0, + lastAttempt: Date.now(), + dispatchTx: event.transaction_hash ?? '', + status: 'pending', + }; + } +} diff --git a/typescript/sdk/src/bus/types.ts b/typescript/sdk/src/bus/types.ts new file mode 100644 index 00000000000..288c0c3643a --- /dev/null +++ b/typescript/sdk/src/bus/types.ts @@ -0,0 +1,51 @@ +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { DispatchedMessage } from '../core/types.js'; + +/** + * Represents the processing state of a message in the message bus + */ +export type MessageStatus = 'pending' | 'processing' | 'delivered' | 'failed'; + +/** + * Configuration for the message bus + */ +export interface MessageBusConfig { + /** Maximum number of delivery attempts before marking a message as failed */ + maxAttempts: number; + + /** Base timeout between retry attempts in milliseconds */ + retryTimeout: number; +} + +/** + * Extended dispatch message type with processing metadata + */ +export interface MessageWithStatus extends DispatchedMessage { + /** Number of delivery attempts made */ + attempts: number; + + /** Timestamp of the last delivery attempt */ + lastAttempt: number; + + /** Transaction hash of the dispatch transaction */ + dispatchTx: string; + + /** Current status of message processing */ + status: MessageStatus; +} + +/** + * Interface for message handlers that process messages for specific protocols + */ +export interface MessageHandler { + /** The protocol type this handler can process */ + protocol: ProtocolType; + + /** + * Process a message destined for this handler's protocol + * @param message The message to process + * @returns true if successfully processed, false otherwise + */ + processMessage(message: MessageWithStatus): Promise; +} diff --git a/typescript/sdk/src/consts/testChains.ts b/typescript/sdk/src/consts/testChains.ts index a983e145fd2..b63e48ce490 100644 --- a/typescript/sdk/src/consts/testChains.ts +++ b/typescript/sdk/src/consts/testChains.ts @@ -143,6 +143,24 @@ export const testSealevelChain: ChainMetadata = { rpcUrls: [{ http: 'http://127.0.0.1:8899' }], }; +export const testStarknetChain: ChainMetadata = { + chainId: '0x534e5f5345504f4c4941', + domainId: 5854809, + name: 'starknetdevnet', + nativeToken: { + decimals: 18, + denom: '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', + name: 'Ether', + symbol: 'ETH', + }, + protocol: ProtocolType.Starknet, + rpcUrls: [ + { + http: 'http://127.0.0.1:5050', + }, + ], +}; + export const multiProtocolTestChainMetadata: ChainMap = { ...testChainMetadata, testcosmos: testCosmosChain, @@ -150,6 +168,7 @@ export const multiProtocolTestChainMetadata: ChainMap = { testxerc20: testXERC20, testvsxerc20: testVSXERC20, testxerc20lockbox: testXERC20Lockbox, + starknetdevnet: testStarknetChain, }; export const multiProtocolTestChains: Array = Object.keys( diff --git a/typescript/sdk/src/core/MultiProtocolCore.ts b/typescript/sdk/src/core/MultiProtocolCore.ts index 560567b727b..56a830dcb82 100644 --- a/typescript/sdk/src/core/MultiProtocolCore.ts +++ b/typescript/sdk/src/core/MultiProtocolCore.ts @@ -8,6 +8,7 @@ import { ChainMap, ChainName } from '../types.js'; import { CosmWasmCoreAdapter } from './adapters/CosmWasmCoreAdapter.js'; import { EvmCoreAdapter } from './adapters/EvmCoreAdapter.js'; import { SealevelCoreAdapter } from './adapters/SealevelCoreAdapter.js'; +import { StarknetCoreAdapter } from './adapters/StarknetCoreAdapter.js'; import { ICoreAdapter } from './adapters/types.js'; import { CoreAddresses } from './contracts.js'; @@ -39,6 +40,7 @@ export class MultiProtocolCore extends MultiProtocolApp< if (protocol === ProtocolType.Ethereum) return EvmCoreAdapter; if (protocol === ProtocolType.Sealevel) return SealevelCoreAdapter; if (protocol === ProtocolType.Cosmos) return CosmWasmCoreAdapter; + if (protocol === ProtocolType.Starknet) return StarknetCoreAdapter; throw new Error(`No adapter for protocol ${protocol}`); } diff --git a/typescript/sdk/src/core/StarknetCore.ts b/typescript/sdk/src/core/StarknetCore.ts index 6e0bd7b3597..f97e250e84d 100644 --- a/typescript/sdk/src/core/StarknetCore.ts +++ b/typescript/sdk/src/core/StarknetCore.ts @@ -1,167 +1,293 @@ -import { Account, CairoOption, CairoOptionVariant, Contract } from 'starknet'; +import { + Account, + CairoOption, + CairoOptionVariant, + GetTransactionReceiptResponse, + InvokeFunctionResponse, +} from 'starknet'; import { ChainName, + DispatchedMessage, HyperlaneAddressesMap, + MultiProtocolProvider, MultiProvider, } from '@hyperlane-xyz/sdk'; -import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; -import { rootLogger } from '@hyperlane-xyz/utils'; +import { Address, pollAsync, rootLogger } from '@hyperlane-xyz/utils'; + +import { toStarknetMessageBytes } from '../messaging/messageUtils.js'; +import { + getStarknetMailboxContract, + parseStarknetDispatchEvents, + quoteStarknetDispatch, +} from '../utils/starknet.js'; + +export interface IMultiProtocolSignerManager { + getStarknetSigner(chain: ChainName): Account; +} export class StarknetCore { protected logger = rootLogger.child({ module: 'StarknetCore' }); - protected signer: Account; protected addressesMap: HyperlaneAddressesMap; - protected multiProvider: MultiProvider; + public multiProvider: MultiProvider; + private multiProtocolSigner: IMultiProtocolSignerManager; + private multiProtocolProvider: MultiProtocolProvider; constructor( - signer: Account, // Use MultiProtocolSignerManager instead addressesMap: HyperlaneAddressesMap, multiProvider: MultiProvider, + multiProtocolSigner: IMultiProtocolSignerManager, + multiProtocolProvider: MultiProtocolProvider, ) { - this.signer = signer; this.addressesMap = addressesMap; this.multiProvider = multiProvider; + this.multiProtocolSigner = multiProtocolSigner; + this.multiProtocolProvider = multiProtocolProvider; } - /** - * Convert a byte array to a starknet message - * Pads the bytes to 16 bytes chunks - * @param bytes Input byte array - * @returns Object containing size and padded data array - */ - static toStarknetMessageBytes(bytes: Uint8Array): { - size: number; - data: bigint[]; - } { - // Calculate the required padding - const padding = (16 - (bytes.length % 16)) % 16; - const totalLen = bytes.length + padding; - - // Create a new byte array with the necessary padding - const paddedBytes = new Uint8Array(totalLen); - paddedBytes.set(bytes); - // Padding remains as zeros by default in Uint8Array - - // Convert to chunks of 16 bytes - const result: bigint[] = []; - for (let i = 0; i < totalLen; i += 16) { - const chunk = paddedBytes.slice(i, i + 16); - // Convert chunk to bigint (equivalent to u128 in Rust) - const value = BigInt('0x' + Buffer.from(chunk).toString('hex')); - result.push(value); - } - - return { - size: bytes.length, - data: result, - }; + getAddresses(chain: ChainName) { + return this.addressesMap[chain]; } - static getMailboxContract(address: string, signer: Account): Contract { - const { abi } = getCompiledContract('mailbox'); - return new Contract(abi, address, signer); + parseDispatchedMessagesFromReceipt( + receipt: GetTransactionReceiptResponse, + origin: ChainName, + ): DispatchedMessage { + const mailboxAddress = this.addressesMap[origin].mailbox; + const signer = this.multiProtocolSigner.getStarknetSigner(origin); + const mailboxContract = getStarknetMailboxContract(mailboxAddress, signer); + + const parsedEvents = mailboxContract.parseEvents(receipt); + return parseStarknetDispatchEvents( + parsedEvents, + (domain) => this.multiProvider.tryGetChainName(domain) ?? undefined, + )[0]; } - async sendMessage(params: { - origin: ChainName; - destinationDomain: number; - recipientAddress: string; - messageBody: string; - }): Promise<{ txHash: string; messages: any[] }> { - const mailboxAddress = this.addressesMap[params.origin].mailbox; - const mailboxContract = StarknetCore.getMailboxContract( + async sendMessage( + origin: ChainName, + destination: ChainName, + recipient: Address, + body: string, + _hook?: Address, + _metadata?: string, + ): Promise<{ + dispatchTx: InvokeFunctionResponse; + message: DispatchedMessage; + }> { + const destinationDomain = this.multiProvider.getDomainId(destination); + const mailboxAddress = this.addressesMap[origin].mailbox; + const mailboxContract = getStarknetMailboxContract( mailboxAddress, - this.signer, + this.multiProtocolSigner.getStarknetSigner(origin), ); - // Convert messageBody to Bytes struct format - const messageBodyBytes = StarknetCore.toStarknetMessageBytes( - new TextEncoder().encode(params.messageBody), + const messageBodyBytes = toStarknetMessageBytes( + new TextEncoder().encode(body), ); - console.log({ + + this.logger.debug({ messageBodyBytes, - encoded: new TextEncoder().encode(params.messageBody), + encoded: new TextEncoder().encode(body), }); const nonOption = new CairoOption(CairoOptionVariant.None); // Quote the dispatch first to ensure enough fees are provided - const quote = await mailboxContract.call('quote_dispatch', [ - params.destinationDomain, - params.recipientAddress, - messageBodyBytes, - nonOption, - nonOption, - ]); + const quote = await quoteStarknetDispatch({ + mailboxContract, + destinationDomain, + recipientAddress: recipient, + messageBody: messageBodyBytes, + }); - // Dispatch the message - const { transaction_hash } = await mailboxContract.invoke('dispatch', [ - params.destinationDomain, - params.recipientAddress, + const dispatchTx = await mailboxContract.invoke('dispatch', [ + destinationDomain, + recipient, messageBodyBytes, BigInt(quote.toString()), //fee amount nonOption, nonOption, ]); - this.logger.info(`Message sent with transaction hash: ${transaction_hash}`); + this.logger.info( + `Message sent with transaction hash: ${dispatchTx.transaction_hash}`, + ); + const account = this.multiProtocolSigner.getStarknetSigner(origin); + const receipt = await account.waitForTransaction( + dispatchTx.transaction_hash, + ); - const receipt = await this.signer.waitForTransaction(transaction_hash); const parsedEvents = mailboxContract.parseEvents(receipt); return { - txHash: transaction_hash, - messages: this.getDispatchedMessages(parsedEvents), + dispatchTx, + message: parseStarknetDispatchEvents( + parsedEvents, + (domain) => this.multiProvider.tryGetChainName(domain) ?? undefined, + )[0], }; } - async quoteDispatch(params: { - destinationDomain: number; - recipientAddress: string; - messageBody: string; - customHookMetadata?: string; - customHook?: string; - }): Promise { - const { abi } = getCompiledContract('mailbox'); - const mailboxContract = new Contract(abi, 'mailbox_address', this.signer); - - const quote = await mailboxContract.call('quote_dispatch', [ - params.destinationDomain, - params.recipientAddress, - params.messageBody, - params.customHookMetadata || '', - params.customHook || '', + onDispatch( + handler: (message: DispatchedMessage, event: any) => Promise, + chains = Object.keys(this.addressesMap), + ): { + removeHandler: (chains?: ChainName[]) => void; + } { + const eventSubscriptions: (() => void)[] = []; + + chains.forEach((originChain) => { + const account = this.multiProtocolSigner.getStarknetSigner(originChain); + const mailboxAddress = this.addressesMap[originChain].mailbox; + const mailboxContract = getStarknetMailboxContract( + mailboxAddress, + account, + ); + + this.logger.debug(`Listening for dispatch on ${originChain}`); + + let lastBlockChecked: number | undefined; + + const pollForEvents = async () => { + try { + // Get the latest block + const provider = + this.multiProtocolProvider.getStarknetProvider(originChain); + const latestBlock = await provider.getBlock('latest'); + + // If this is the first check, just record the current block and wait for next poll + if (lastBlockChecked === undefined) { + lastBlockChecked = latestBlock.block_number; + return; + } + + // Only check for new blocks + if (latestBlock.block_number <= lastBlockChecked) { + return; + } + + // Get events from the blocks we haven't checked yet + const { events } = await provider.getEvents({ + address: mailboxAddress, + from_block: { block_number: lastBlockChecked + 1 }, + to_block: { block_number: latestBlock.block_number }, + chunk_size: 400, // not sure what this is + }); + + lastBlockChecked = latestBlock.block_number; + + if (events.length > 0) { + for (const event of events) { + // Get transaction receipt -> this is the receipt of the dispatch transaction + const receipt = await provider.getTransactionReceipt( + event.transaction_hash, + ); + + const parsedEvents = mailboxContract.parseEvents(receipt); + const messages = parseStarknetDispatchEvents( + parsedEvents, + (domain) => + this.multiProvider.tryGetChainName(domain) ?? undefined, + ); + + for (const dispatched of messages) { + this.logger.info( + `Observed message ${dispatched.id} on ${originChain} to ${dispatched.parsed.destinationChain}`, + ); + + await handler(dispatched, event); + } + } + } + } catch (error) { + this.logger.error( + `Error polling for events on ${originChain}: ${error}`, + ); + } + }; + + const intervalId = setInterval(pollForEvents, 10000); // Poll every 15 seconds + + pollForEvents().catch((error) => { + this.logger.error( + `Error in initial poll for events on ${originChain}: ${error}`, + ); + }); + + eventSubscriptions.push(() => { + clearInterval(intervalId); + }); + }); + + return { + removeHandler: (removeChains?: ChainName[]) => { + (removeChains ?? chains).forEach((chain, index) => { + if (eventSubscriptions[index]) { + eventSubscriptions[index](); + this.logger.debug(`Stopped listening for dispatch on ${chain}`); + } + }); + }, + }; + } + + async deliver( + message: DispatchedMessage, + metadata: any, + ): Promise<{ transaction_hash: string }> { + const destinationChain = this.multiProvider.getChainName( + message.parsed.destination, + ); + const mailboxAddress = this.addressesMap[destinationChain].mailbox; + const mailboxContract = getStarknetMailboxContract( + mailboxAddress, + this.multiProtocolSigner.getStarknetSigner(destinationChain), + ); + + const data = message.message; + + const { transaction_hash } = await mailboxContract.invoke('process', [ + metadata, + data, // formatted message ]); - return quote.toString(); + this.logger.info( + `Message processed with transaction hash: ${transaction_hash}`, + ); + + // Wait for transaction to be mined + await this.multiProtocolSigner + .getStarknetSigner(destinationChain) + .waitForTransaction(transaction_hash); + + return { transaction_hash }; } - getDispatchedMessages(parsedEvents: any): any { - return parsedEvents - .filter((event: any) => 'contracts::mailbox::mailbox::Dispatch' in event) - .map((event: any) => { - const dispatchEvent = event['contracts::mailbox::mailbox::Dispatch']; - const message = dispatchEvent.message; - - const originChain = - this.multiProvider.tryGetChainName(message.origin) ?? undefined; - const destinationChain = - this.multiProvider.tryGetChainName(message.destination) ?? undefined; - - // Convert the message to the expected format - // TODO: stringify the message body - const messageString = { - version: Number(message.version), - nonce: Number(message.nonce), - origin: originChain, - sender: message.sender.toString(), - destination: destinationChain, - recipient: message.recipient.toString(), - body: Array.from(message.body.data).map((n: any) => n.toString()), // TODO: causes stringify error - }; - - return messageString; - }); + async waitForMessageIdProcessed( + messageId: string, + destinationChain: ChainName, + delay?: number, + maxAttempts?: number, + ): Promise { + await pollAsync( + async () => { + const mailboxAddress = this.addressesMap[destinationChain].mailbox; + const mailboxContract = getStarknetMailboxContract( + mailboxAddress, + this.multiProtocolSigner.getStarknetSigner(destinationChain), + ); + const delivered = await mailboxContract.delivered(messageId); + if (delivered) { + this.logger.info(`Message ${messageId} was processed`); + return true; + } else { + throw new Error(`Message ${messageId} not yet processed`); + } + }, + delay, + maxAttempts, + ); + return true; } } diff --git a/typescript/sdk/src/core/StarknetCoreModule.ts b/typescript/sdk/src/core/StarknetCoreModule.ts index 666a3b5805e..b87d72e95cc 100644 --- a/typescript/sdk/src/core/StarknetCoreModule.ts +++ b/typescript/sdk/src/core/StarknetCoreModule.ts @@ -1,12 +1,18 @@ import { BigNumber } from 'ethers'; -import { Account, Contract } from 'starknet'; +import { Account, Contract, MultiType } from 'starknet'; -import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; -import { assert, rootLogger } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert, rootLogger } from '@hyperlane-xyz/utils'; import { StarknetDeployer } from '../deploy/StarknetDeployer.js'; import { HookType } from '../hook/types.js'; +import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { PROTOCOL_TO_DEFAULT_NATIVE_TOKEN } from '../token/nativeTokenMetadata.js'; import { ChainNameOrId } from '../types.js'; +import { + StarknetContractName, + getStarknetMailboxContract, +} from '../utils/starknet.js'; import { StarknetCoreReader } from './StarknetCoreReader.js'; import { CoreConfig } from './types.js'; @@ -18,10 +24,12 @@ export class StarknetCoreModule { constructor( protected readonly signer: Account, - protected readonly domainId: number, + protected readonly multiProvider: MultiProvider, + protected readonly multiProtocolProvider: MultiProtocolProvider, + protected readonly chain: ChainNameOrId, ) { - this.deployer = new StarknetDeployer(signer); - this.coreReader = new StarknetCoreReader(signer); + this.deployer = new StarknetDeployer(signer, multiProvider); + this.coreReader = new StarknetCoreReader(multiProtocolProvider, chain); } /** @@ -48,22 +56,33 @@ export class StarknetCoreModule { // Deploy core components in sequence: // 1. NoopISM - A basic interchain security module that performs no validation - const noopIsm = await this.deployer.deployContract('noop_ism', []); + const noopIsm = await this.deployer.deployContract( + StarknetContractName.NOOP_ISM, + [], + ); // 2. Default Hook - A basic hook implementation for message processing - const defaultHook = await this.deployer.deployContract('hook', []); + const defaultHook = await this.deployer.deployContract( + StarknetContractName.HOOK, + [], + ); // 3. Protocol Fee Hook - Handles fee collection for cross-chain messages - const protocolFee = await this.deployer.deployContract('protocol_fee', [ - BigNumber.from(config.requiredHook.maxProtocolFee), - BigNumber.from(config.requiredHook.protocolFee), - config.requiredHook.beneficiary, - config.owner, - '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', // ETH address on Starknet chains - ]); + const protocolFee = await this.deployer.deployContract( + StarknetContractName.PROTOCOL_FEE, + [ + BigNumber.from(config.requiredHook.maxProtocolFee), + BigNumber.from(config.requiredHook.protocolFee), + config.requiredHook.beneficiary, + config.owner, + PROTOCOL_TO_DEFAULT_NATIVE_TOKEN[ProtocolType.Starknet]! + .denom as MultiType, + ], + ); // 4. Deploy Mailbox with initial configuration const mailboxContract = await this.deployMailbox( + chain, config.owner, noopIsm, defaultHook, @@ -78,12 +97,12 @@ export class StarknetCoreModule { }); const validatorAnnounce = await this.deployer.deployContract( - 'validator_announce', + StarknetContractName.VALIDATOR_ANNOUNCE, [mailboxContract.address, config.owner], ); const testRecipient = await this.deployer.deployContract( - 'message_recipient', + StarknetContractName.MESSAGE_RECIPIENT, [defaultIsm || noopIsm], ); @@ -100,21 +119,19 @@ export class StarknetCoreModule { } async deployMailbox( + chain: ChainNameOrId, owner: string, defaultIsm: string, defaultHook: string, requiredHook: string, ) { - const mailboxAddress = await this.deployer.deployContract('mailbox', [ - this.domainId, - owner, - defaultIsm, - defaultHook, - requiredHook, - ]); + const domainId = this.multiProvider.getDomainId(chain); + const mailboxAddress = await this.deployer.deployContract( + StarknetContractName.MAILBOX, + [BigInt(domainId), owner, defaultIsm, defaultHook, requiredHook], + ); - const { abi } = getCompiledContract('mailbox'); - return new Contract(abi, mailboxAddress, this.signer); + return getStarknetMailboxContract(mailboxAddress, this.signer); } async update( @@ -137,12 +154,15 @@ export class StarknetCoreModule { mailbox: args.mailboxContract.address, }); - this.logger.trace(`Updating default ism ${defaultIsm}..`); + this.logger.info(`Updating default ism ${defaultIsm}..`); + const nonce = await this.signer.getNonce(); const { transaction_hash: defaultIsmUpdateTxHash } = - await args.mailboxContract.invoke('set_default_ism', [defaultIsm]); + await args.mailboxContract.invoke('set_default_ism', [defaultIsm], { + nonce, + }); await this.signer.waitForTransaction(defaultIsmUpdateTxHash); - this.logger.trace( + this.logger.info( `Transaction hash for updated default ism: ${defaultIsmUpdateTxHash}`, ); result.defaultIsm = defaultIsm; @@ -151,23 +171,23 @@ export class StarknetCoreModule { // Update required hook to MerkleTreeHook if specified if (expectedConfig.requiredHook) { this.logger.info( - `Deploying MerkleTreeHook with explicit owner (${args.owner}). Note: Unlike EVM where deployer becomes owner, ` + - `in Starknet the owner is specified during construction.`, + `Deploying MerkleTreeHook with explicit owner (${args.owner}). Note: Unlike EVM where deployer automatically becomes owner, ` + + `in Starknet the owner must be explicitly passed as a constructor parameter.`, ); const merkleTreeHook = await this.deployer.deployContract( - 'merkle_tree_hook', + StarknetContractName.MERKLE_TREE_HOOK, [args.mailboxContract.address, args.owner], ); - this.logger.trace(`Updating required hook ${merkleTreeHook}..`); + this.logger.info(`Updating required hook ${merkleTreeHook}..`); const { transaction_hash: requiredHookUpdateTxHash } = await args.mailboxContract.invoke('set_required_hook', [ merkleTreeHook, ]); await this.signer.waitForTransaction(requiredHookUpdateTxHash); - this.logger.trace( + this.logger.info( `Transaction hash for updated required hook: ${requiredHookUpdateTxHash}`, ); @@ -176,14 +196,14 @@ export class StarknetCoreModule { // Update owner if different from current if (expectedConfig.owner && actualConfig.owner !== expectedConfig.owner) { - this.logger.trace(`Updating mailbox owner ${expectedConfig.owner}..`); + this.logger.info(`Updating mailbox owner ${expectedConfig.owner}..`); const { transaction_hash: transferOwnershipTxHash } = await args.mailboxContract.invoke('transfer_ownership', [ expectedConfig.owner, ]); await this.signer.waitForTransaction(transferOwnershipTxHash); - this.logger.trace( + this.logger.info( `Transaction hash for updated owner: ${transferOwnershipTxHash}`, ); diff --git a/typescript/sdk/src/core/StarknetCoreReader.ts b/typescript/sdk/src/core/StarknetCoreReader.ts index 1106d152adb..2a09342a666 100644 --- a/typescript/sdk/src/core/StarknetCoreReader.ts +++ b/typescript/sdk/src/core/StarknetCoreReader.ts @@ -1,10 +1,13 @@ -import { Account, Contract, num } from 'starknet'; +import { num } from 'starknet'; -import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; import { Address, rootLogger } from '@hyperlane-xyz/utils'; import { StarknetHookReader } from '../hook/StarknetHookReader.js'; import { StarknetIsmReader } from '../ism/StarknetIsmReader.js'; +import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js'; +import { StarknetJsProvider } from '../providers/ProviderType.js'; +import { ChainNameOrId } from '../types.js'; +import { getStarknetMailboxContract } from '../utils/starknet.js'; import { CoreConfig } from './types.js'; @@ -12,17 +15,21 @@ export class StarknetCoreReader { protected readonly logger = rootLogger.child({ module: 'StarknetCoreReader', }); + protected provider: StarknetJsProvider['provider']; protected ismReader: StarknetIsmReader; protected hookReader: StarknetHookReader; - constructor(protected readonly signer: Account) { - this.ismReader = new StarknetIsmReader(this.signer); - this.hookReader = new StarknetHookReader(this.signer); + constructor( + protected readonly multiProvider: MultiProtocolProvider, + protected readonly chain: ChainNameOrId, + ) { + this.provider = this.multiProvider.getStarknetProvider(chain); + this.ismReader = new StarknetIsmReader(this.multiProvider, this.chain); + this.hookReader = new StarknetHookReader(this.multiProvider, this.chain); } async deriveCoreConfig(mailboxAddress: Address): Promise { - const { abi } = getCompiledContract('mailbox'); - const mailbox = new Contract(abi, mailboxAddress, this.signer); + const mailbox = getStarknetMailboxContract(mailboxAddress, this.provider); const [defaultIsm, defaultHook, requiredHook, owner] = ( await Promise.all([ diff --git a/typescript/sdk/src/core/adapters/StarknetCoreAdapter.ts b/typescript/sdk/src/core/adapters/StarknetCoreAdapter.ts new file mode 100644 index 00000000000..5e12d71feb6 --- /dev/null +++ b/typescript/sdk/src/core/adapters/StarknetCoreAdapter.ts @@ -0,0 +1,135 @@ +import { + CallData, + InvokeTransactionReceiptResponse, + ParsedEvents, + events as eventsUtils, +} from 'starknet'; + +import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; +import { Address, HexString } from '@hyperlane-xyz/utils'; + +import { BaseStarknetAdapter } from '../../app/MultiProtocolApp.js'; +import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider.js'; +import { + ProviderType, + StarknetJsTransactionReceipt, +} from '../../providers/ProviderType.js'; +import { ChainName } from '../../types.js'; +import { + getStarknetMailboxContract, + parseStarknetDispatchEvents, +} from '../../utils/starknet.js'; + +import { ICoreAdapter } from './types.js'; + +export class StarknetCoreAdapter + extends BaseStarknetAdapter + implements ICoreAdapter +{ + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { mailbox: Address }, + ) { + super(chainName, multiProvider, addresses); + } + + extractMessageIds( + sourceTx: StarknetJsTransactionReceipt, + ): Array<{ messageId: string; destination: ChainName }> { + if (sourceTx.type !== ProviderType.Starknet) { + throw new Error( + `Unsupported provider type for StarknetCoreAdapter ${sourceTx.type}`, + ); + } + + let parsedEvents: ParsedEvents = []; + sourceTx.receipt.match({ + success: (txR) => { + const emittedEvents = + (txR as InvokeTransactionReceiptResponse).events?.map((event) => { + return { + block_hash: (txR as any).block_hash, + block_number: (txR as any).block_number, + transaction_hash: (txR as any).transaction_hash, + ...event, + }; + }) || []; + + if (emittedEvents.length === 0) return; + const mailboxAbi = getCompiledContract('mailbox').abi; + parsedEvents = eventsUtils.parseEvents( + emittedEvents, + eventsUtils.getAbiEvents(mailboxAbi), + CallData.getAbiStruct(mailboxAbi), + CallData.getAbiEnum(mailboxAbi), + ); + }, + _: () => { + throw Error('This transaction was not successful.'); + }, + }); + + if (!parsedEvents || parsedEvents.length === 0) return []; + + const messages = parseStarknetDispatchEvents( + parsedEvents, + (domain) => this.multiProvider.tryGetChainName(domain) ?? undefined, + ); + + return messages.map(({ id, parsed }) => ({ + messageId: id, + destination: this.multiProvider.getChainName(parsed.destination), + })); + } + + async waitForMessageProcessed( + messageId: HexString, + destination: ChainName, + delayMs = 5000, + maxAttempts = 60, + ): Promise { + const destAdapter = new StarknetCoreAdapter( + destination, + this.multiProvider, + { mailbox: this.addresses.mailbox }, + ); + + const mailboxContract = getStarknetMailboxContract( + destAdapter.addresses.mailbox, + destAdapter.getProvider(), + ); + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + // Check if the message has been delivered + const isDelivered = await mailboxContract.call('delivered', [ + messageId, + ]); + + if (isDelivered) { + this.logger.debug( + `Message ${messageId} confirmed delivered on ${destination}`, + ); + return true; + } + } catch (error) { + this.logger.error( + `Error checking if message ${messageId} is delivered: ${error}`, + ); + } + + this.logger.debug( + `Message ${messageId} not yet delivered on ${destination}, waiting ${delayMs}ms`, + ); + + // Wait before checking again + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + + this.logger.warn( + `Timed out waiting for message ${messageId} to be delivered on ${destination}`, + ); + return false; + } +} diff --git a/typescript/sdk/src/deploy/StarknetDeployer.ts b/typescript/sdk/src/deploy/StarknetDeployer.ts index e4f5abd09a2..e59774f1aca 100644 --- a/typescript/sdk/src/deploy/StarknetDeployer.ts +++ b/typescript/sdk/src/deploy/StarknetDeployer.ts @@ -1,6 +1,7 @@ import { Logger } from 'pino'; import { Account, + BigNumberish, CallData, ContractFactory, ContractFactoryParams, @@ -23,13 +24,18 @@ import { IsmType, SupportedIsmTypesOnStarknetType, } from '../ism/types.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; +import { getStarknetIsmContract } from '../utils/starknet.js'; export class StarknetDeployer { private readonly logger: Logger; private readonly deployedContracts: Record = {}; - constructor(private readonly account: Account) { + constructor( + private readonly account: Account, + private readonly multiProvider: MultiProvider, + ) { this.logger = rootLogger.child({ module: 'starknet-deployer' }); } @@ -52,6 +58,11 @@ export class StarknetDeployer { const contractFactory = new ContractFactory(params); const contract = await contractFactory.deploy(constructorCalldata); + const receipt = await this.account.waitForTransaction( + contract.deployTransactionHash as BigNumberish, + ); + + assert(receipt.isSuccess(), `Contract ${contractName} deployment failed`); let address = contract.address; // Ensure the address is 66 characters long (including the '0x' prefix) @@ -73,12 +84,11 @@ export class StarknetDeployer { mailbox: Address; }): Promise
{ const { chain, ismConfig, mailbox } = params; - assert( - typeof ismConfig !== 'string', - 'String ism config is not supported on starknet', - ); + if (typeof ismConfig === 'string') { + return ismConfig; + } const ismType = ismConfig.type; - this.logger.debug(`Deploying ${ismType} to ${chain}`); + this.logger.info(`Deploying ${ismType} to ${chain}`); assert( SupportedIsmTypesOnStarknet.includes( @@ -117,13 +127,37 @@ export class StarknetDeployer { break; case IsmType.ROUTING: constructorArgs = [ismConfig.owner]; + const ismAddress = await this.deployContract( + contractName, + constructorArgs, + ); + const routingContract = getStarknetIsmContract( + IsmType.ROUTING, + ismAddress, + this.account, + ); + const domains = ismConfig.domains; + for (const domain of Object.keys(domains)) { + const route = await this.deployIsm({ + chain, + ismConfig: domains[domain], + mailbox, + }); + const domainId = this.multiProvider.getDomainId(domain); + const tx = await routingContract.invoke('set', [ + BigInt(domainId), + route, + ]); + await this.account.waitForTransaction(tx.transaction_hash); + this.logger.info(`ISM ${route} set for domain ${domain}`); + } - break; + return ismAddress; case IsmType.PAUSABLE: constructorArgs = [ismConfig.owner]; break; - case IsmType.AGGREGATION: + case IsmType.AGGREGATION: { const addresses: Address[] = []; for (const module of ismConfig.modules) { const submodule = await this.deployIsm({ @@ -140,6 +174,7 @@ export class StarknetDeployer { ]; break; + } case IsmType.TRUSTED_RELAYER: constructorArgs = [mailbox, ismConfig.relayer]; break; diff --git a/typescript/sdk/src/hook/StarknetHookReader.ts b/typescript/sdk/src/hook/StarknetHookReader.ts index 6881e5c455d..241d59f3f09 100644 --- a/typescript/sdk/src/hook/StarknetHookReader.ts +++ b/typescript/sdk/src/hook/StarknetHookReader.ts @@ -1,32 +1,67 @@ -import { Account, CairoCustomEnum, Contract, num } from 'starknet'; +import { CairoCustomEnum, num } from 'starknet'; -import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; -import { Address, rootLogger } from '@hyperlane-xyz/utils'; +import { Address, WithAddress, rootLogger } from '@hyperlane-xyz/utils'; -import { HookType } from './types.js'; +import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js'; +import { StarknetJsProvider } from '../providers/ProviderType.js'; +import { ChainMap } from '../types.js'; +import { ChainNameOrId } from '../types.js'; +import { + StarknetContractName, + StarknetHookType, + getStarknetContract, +} from '../utils/starknet.js'; + +import { + AggregationHookConfig, + DomainRoutingHookConfig, + FallbackRoutingHookConfig, + HookConfig, + HookType, + MailboxDefaultHookConfig, + MerkleTreeHookConfig, + ProtocolFeeHookConfig, +} from './types.js'; export class StarknetHookReader { protected readonly logger = rootLogger.child({ module: 'StarknetHookReader', }); + protected provider: StarknetJsProvider['provider']; - constructor(protected readonly signer: Account) {} + constructor( + protected readonly multiProvider: MultiProtocolProvider, + protected readonly chain: ChainNameOrId, + ) { + this.provider = this.multiProvider.getStarknetProvider(chain); + } - async deriveHookConfig(address: Address): Promise { + async deriveHookConfig(address: Address): Promise { try { - const { abi } = getCompiledContract('hook'); - const hook = new Contract(abi, address, this.signer); - + const hook = getStarknetContract( + StarknetContractName.HOOK, + address, + this.provider, + ); const hookType: CairoCustomEnum = await hook.hook_type(); - switch (hookType.activeVariant()) { - case 'UNUSED': - return this.deriveUnusedConfig(address); - case 'MERKLE_TREE': + const variant = hookType.activeVariant(); + switch (variant) { + case StarknetHookType.AGGREGATION: + return this.deriveAggregationHookConfig(address); + case StarknetHookType.FALLBACK_ROUTING: + return this.deriveFallbackRoutingHookConfig(address); + case StarknetHookType.MAILBOX_DEFAULT_HOOK: + return this.deriveMailboxDefaultHookConfig(address); + case StarknetHookType.MERKLE_TREE: return this.deriveMerkleTreeConfig(address); - case 'PROTOCOL_FEE': + case StarknetHookType.PROTOCOL_FEE: return this.deriveProtocolFeeConfig(address); + case StarknetHookType.ROUTING: + return this.deriveRoutingHookConfig(address); + case StarknetHookType.UNUSED: + return this.deriveMerkleTreeConfig(address); default: - throw Error; + throw new Error(`Unsupported hook type: ${variant}`); } } catch (error) { this.logger.error(`Failed to derive Hook config for ${address}`, error); @@ -34,34 +69,145 @@ export class StarknetHookReader { } } - private async deriveMerkleTreeConfig(address: Address) { + private async deriveMerkleTreeConfig( + address: Address, + ): Promise> { return { type: HookType.MERKLE_TREE, address, }; } - private async deriveProtocolFeeConfig(address: Address) { - const { abi } = getCompiledContract('protocol_fee'); - const hook = new Contract(abi, address, this.signer); + private async deriveProtocolFeeConfig( + address: Address, + ): Promise> { + const hook = getStarknetContract( + StarknetContractName.PROTOCOL_FEE, + address, + this.provider, + ); const [owner, protocolFee, beneficiary] = await Promise.all([ hook.owner(), hook.get_protocol_fee(), hook.get_beneficiary(), ]); + + // no getter for max protocol fee + // pub const MAX_PROTOCOL_FEE: u256 = 1000000000; return { type: HookType.PROTOCOL_FEE, address, owner: num.toHex64(owner.toString()), protocolFee: protocolFee.toString(), beneficiary: num.toHex64(beneficiary.toString()), + maxProtocolFee: '1000000000', }; } - private async deriveUnusedConfig(address: Address) { + private async deriveRoutingHookConfig( + address: Address, + ): Promise> { + const hook = getStarknetContract( + StarknetContractName.DOMAIN_ROUTING_HOOK, + address, + this.provider, + ); + const [domains, owner] = await Promise.all([hook.domains(), hook.owner()]); + const domainConfigs: Record = {}; + for (const domain of domains) { + try { + const moduleAddress = await hook.hook(domain); + const moduleConfig = await this.deriveHookConfig( + num.toHex64(moduleAddress.toString()), + ); + domainConfigs[domain.toString()] = moduleConfig; + } catch (error) { + this.logger.error( + `Failed to derive config for domain ${domain}`, + error, + ); + } + } + return { - type: HookType.MERKLE_TREE, + type: HookType.ROUTING, + address, + domains: domainConfigs as ChainMap, + owner: num.toHex64(owner.toString()), + }; + } + + private async deriveFallbackRoutingHookConfig( + address: Address, + ): Promise> { + const hook = getStarknetContract( + StarknetContractName.FALLBACK_DOMAIN_ROUTING_HOOK, + address, + this.provider, + ); + const [domains, owner, fallbackHookAddress] = await Promise.all([ + hook.domains(), + hook.owner(), + hook.fallback_hook(), + ]); + + const domainConfigs: Record = {}; + for (const domain of domains) { + try { + const moduleAddress = await hook.hook(domain); + const moduleConfig = await this.deriveHookConfig( + num.toHex64(moduleAddress.toString()), + ); + domainConfigs[domain.toString()] = moduleConfig; + } catch (error) { + this.logger.error( + `Failed to derive config for domain ${domain}`, + error, + ); + } + } + + const fallbackHook = await this.deriveHookConfig( + num.toHex64(fallbackHookAddress.toString()), + ); + + return { + type: HookType.FALLBACK_ROUTING, + address, + domains: domainConfigs as ChainMap, + owner: num.toHex64(owner.toString()), + fallback: fallbackHook, + }; + } + + private async deriveAggregationHookConfig( + address: Address, + ): Promise> { + const hook = getStarknetContract( + StarknetContractName.STATIC_AGGREGATION_HOOK, + address, + this.provider, + ); + const hooks = await hook.get_hooks(); + const hookConfigs = await Promise.all( + hooks.map(async (hookAddress: any) => { + return await this.deriveHookConfig(num.toHex64(hookAddress.toString())); + }), + ); + + return { + type: HookType.AGGREGATION, + address, + hooks: hookConfigs.filter(Boolean), + }; + } + + private async deriveMailboxDefaultHookConfig( + address: Address, + ): Promise> { + return { + type: HookType.MAILBOX_DEFAULT, address, }; } diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 5f0170779a8..04f883af63a 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -60,6 +60,7 @@ export { export { CosmWasmCoreAdapter } from './core/adapters/CosmWasmCoreAdapter.js'; export { EvmCoreAdapter } from './core/adapters/EvmCoreAdapter.js'; export { SealevelCoreAdapter } from './core/adapters/SealevelCoreAdapter.js'; +export { StarknetCoreAdapter } from './core/adapters/StarknetCoreAdapter.js'; export { ICoreAdapter } from './core/adapters/types.js'; export { CoreAddresses, @@ -365,6 +366,10 @@ export { SolanaWeb3Provider, SolanaWeb3Transaction, SolanaWeb3TransactionReceipt, + StarknetJsContract, + StarknetJsProvider, + StarknetJsTransaction, + StarknetJsTransactionReceipt, TypedContract, TypedProvider, TypedTransaction, @@ -705,3 +710,29 @@ export { HyperlaneCCIPDeployer } from './ccip/HyperlaneCCIPDeployer.js'; export { StarknetCoreModule } from './core/StarknetCoreModule.js'; export { StarknetERC20WarpModule } from './token/StarknetERC20WarpModule.js'; export { StarknetCore } from './core/StarknetCore.js'; +export { MessageService } from './messaging/MessageService.js'; +export { + formatEthereumMessageForStarknet, + formatStarknetMessageForEthereum, + convertU128ArrayToBytes, +} from './messaging/messageUtils.js'; +export { + StarknetContractName, + StarknetIsmType, + StarknetHookType, + getStarknetContract, + getStarknetHypERC20Contract, + getStarknetHypERC20CollateralContract, + getStarknetMailboxContract, + getStarknetHypNativeContract, + getStarknetEtherContract, + getStarknetIsmContract, +} from './utils/starknet.js'; +export { MessageBus } from './bus/MessageBus.js'; +export { EvmAdapter } from './bus/adapters/EvmAdapter.js'; +export { StarknetAdapter } from './bus/adapters/StarknetAdapter.js'; +export { + MessageHandler, + MessageWithStatus, + MessageBusConfig, +} from './bus/types.js'; diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts index f120bac6d05..3d56882044a 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts @@ -32,6 +32,7 @@ import { import { Address, Domain, + ProtocolType, addBufferToGasLimit, assert, eqAddress, @@ -133,11 +134,18 @@ export class HyperlaneIsmFactory extends HyperlaneApp { }): Promise { const { destination, config, origin, mailbox, existingIsmAddress } = params; if (typeof config === 'string') { - // @ts-ignore - return IInterchainSecurityModule__factory.connect( - config, - this.multiProvider.getSignerOrProvider(destination), - ); + const chainMetadata = this.multiProvider.getChainMetadata(destination); + if (chainMetadata.protocol === ProtocolType.Ethereum) { + // @ts-ignore + return IInterchainSecurityModule__factory.connect( + config, + this.multiProvider.getSignerOrProvider(destination), + ); + } else { + throw new Error( + `ISM address ${config} is not supported for ${destination}`, + ); + } } const ismType = config.type; diff --git a/typescript/sdk/src/ism/StarknetIsmReader.ts b/typescript/sdk/src/ism/StarknetIsmReader.ts index 2151b262b57..333aa5f51fb 100644 --- a/typescript/sdk/src/ism/StarknetIsmReader.ts +++ b/typescript/sdk/src/ism/StarknetIsmReader.ts @@ -1,46 +1,64 @@ -import { Account, CairoCustomEnum, Contract, num } from 'starknet'; +import { CairoCustomEnum, num } from 'starknet'; -import { getCompiledContract } from '@hyperlane-xyz/starknet-core'; -import { Address, rootLogger } from '@hyperlane-xyz/utils'; +import { + ChainNameOrId, + StarknetIsmType, + StarknetJsProvider, + getStarknetContract, +} from '@hyperlane-xyz/sdk'; +import { Address, WithAddress, rootLogger } from '@hyperlane-xyz/utils'; +import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js'; + +import { DerivedIsmConfig } from './EvmIsmReader.js'; import { StarknetIsmContractName } from './starknet-utils.js'; -import { IsmType } from './types.js'; +import { + AggregationIsmConfig, + IsmType, + MultisigIsmConfig, + RoutingIsmConfig, +} from './types.js'; export class StarknetIsmReader { protected readonly logger = rootLogger.child({ module: 'StarknetIsmReader' }); + protected readonly provider: StarknetJsProvider['provider']; - constructor(protected readonly signer: Account) {} - - private getContractAbi(ismType: keyof typeof StarknetIsmContractName) { - return getCompiledContract(StarknetIsmContractName[ismType]).abi; + constructor( + protected readonly multiProvider: MultiProtocolProvider, + protected readonly chain: ChainNameOrId, + ) { + this.provider = multiProvider.getStarknetProvider(this.chain); } - async deriveIsmConfig(address: Address): Promise { + async deriveIsmConfig(address: Address): Promise { try { - const ism = new Contract( - this.getContractAbi(IsmType.ROUTING), + const ism = getStarknetContract( + StarknetIsmContractName[IsmType.MERKLE_ROOT_MULTISIG], // fn module_type same across all isms address, - this.signer, + this.provider, ); + const moduleType: CairoCustomEnum = await ism.module_type(); - switch (moduleType.activeVariant()) { - case 'NULL': - return this.deriveNullConfig(address); - case 'MESSAGE_ID_MULTISIG': - return this.deriveMessageIdMultisigConfig(address); - case 'MERKLE_ROOT_MULTISIG': + const variant = moduleType.activeVariant(); + switch (variant) { + case StarknetIsmType.AGGREGATION: + return this.deriveAggregationConfig(address); + case StarknetIsmType.CCIP_READ: + throw new Error('CCIP_READ does not have a corresponding IsmType'); + case StarknetIsmType.LEGACY_MULTISIG: + throw new Error('LEGACY_MULTISIG is deprecated and not supported'); + case StarknetIsmType.MERKLE_ROOT_MULTISIG: return this.deriveMerkleRootMultisigConfig(address); - case 'ROUTING': + case StarknetIsmType.MESSAGE_ID_MULTISIG: + return this.deriveMessageIdMultisigConfig(address); + case StarknetIsmType.NULL: + return this.deriveNullConfig(address); + case StarknetIsmType.ROUTING: return this.deriveRoutingConfig(address); - case 'FALLBACK_ROUTING': - return this.deriveFallbackRoutingConfig(address); - case 'AGGREGATION': - return this.deriveAggregationConfig(address); + case StarknetIsmType.UNUSED: + throw new Error('Error deriving NULL ISM type'); default: - return { - type: IsmType.TEST_ISM, - address, - }; + throw new Error(`Unknown ISM ModuleType: ${variant}`); } } catch (error) { this.logger.error(`Failed to derive ISM config for ${address}`, error); @@ -48,51 +66,43 @@ export class StarknetIsmReader { } } - private async deriveNullConfig(address: Address) { - try { - const ism = new Contract( - this.getContractAbi(IsmType.PAUSABLE), - address, - this.signer, - ); - await ism.paused(); // Will succeed for pausable ISM - return { - type: IsmType.PAUSABLE, - address, - }; - } catch { - return { - type: IsmType.TRUSTED_RELAYER, - address, - }; - } - } - - private async deriveMessageIdMultisigConfig(address: Address) { - const ism = new Contract( - this.getContractAbi(IsmType.MESSAGE_ID_MULTISIG), + private async deriveAggregationConfig( + address: Address, + ): Promise> { + const ism = getStarknetContract( + StarknetIsmContractName[IsmType.AGGREGATION], address, - this.signer, + this.provider, ); - const [validators, threshold] = await Promise.all([ - ism.get_validators(), + const [modules, threshold] = await Promise.all([ + ism.get_modules(), ism.get_threshold(), ]); + const moduleConfigs = await Promise.all( + modules.map(async (moduleAddress: any) => { + return await this.deriveIsmConfig( + num.toHex64(moduleAddress.toString()), + ); + }), + ); + return { - type: IsmType.MESSAGE_ID_MULTISIG, + type: IsmType.AGGREGATION, address, - validators: validators.map((v: any) => num.toHex64(v.toString())), + modules: moduleConfigs.filter(Boolean), threshold: threshold.toString(), }; } - private async deriveMerkleRootMultisigConfig(address: Address) { - const ism = new Contract( - this.getContractAbi(IsmType.MERKLE_ROOT_MULTISIG), + private async deriveMerkleRootMultisigConfig( + address: Address, + ): Promise> { + const ism = getStarknetContract( + StarknetIsmContractName[IsmType.MERKLE_ROOT_MULTISIG], address, - this.signer, + this.provider, ); const [validators, threshold] = await Promise.all([ @@ -108,48 +118,46 @@ export class StarknetIsmReader { }; } - private async deriveRoutingConfig(address: Address) { - const ism = new Contract( - this.getContractAbi(IsmType.ROUTING), + private async deriveMessageIdMultisigConfig( + address: Address, + ): Promise { + const ism = getStarknetContract( + StarknetIsmContractName[IsmType.MESSAGE_ID_MULTISIG], address, - this.signer, + this.provider, ); - const domains = await ism.domains(); - const domainConfigs: Record = {}; + const [validators, threshold] = await Promise.all([ + ism.get_validators(), + ism.get_threshold(), + ]); - for (const domain of domains) { - try { - const module = await ism.module(domain); - const moduleConfig = await this.deriveIsmConfig( - num.toHex64(module.toString()), - ); - domainConfigs[domain.toString()] = moduleConfig; - } catch (error) { - this.logger.error( - `Failed to derive config for domain ${domain}`, - error, - ); - } - } + return { + type: IsmType.MESSAGE_ID_MULTISIG, + address, + validators: validators.map((v: any) => num.toHex64(v.toString())), + threshold: threshold.toString(), + }; + } + private async deriveNullConfig(address: Address): Promise { return { - type: IsmType.ROUTING, + type: IsmType.TEST_ISM, address, - domains: domainConfigs, }; } - private async deriveFallbackRoutingConfig(address: Address) { - const ism = new Contract( - this.getContractAbi(IsmType.FALLBACK_ROUTING), + private async deriveRoutingConfig( + address: Address, + ): Promise> { + const ism = getStarknetContract( + StarknetIsmContractName[IsmType.ROUTING], address, - this.signer, + this.provider, ); - const domains = await ism.domains(); + const [domains, owner] = await Promise.all([ism.domains(), ism.owner()]); const domainConfigs: Record = {}; - const mailbox = await ism.mailbox(); for (const domain of domains) { try { @@ -167,46 +175,10 @@ export class StarknetIsmReader { } return { - type: IsmType.FALLBACK_ROUTING, + type: IsmType.ROUTING, address, domains: domainConfigs, - mailbox: num.toHex64(mailbox.toString()), - }; - } - - private async deriveAggregationConfig(address: Address) { - const ism = new Contract( - this.getContractAbi(IsmType.AGGREGATION), - address, - this.signer, - ); - - const [modules, threshold] = await Promise.all([ - ism.get_modules(), - ism.get_threshold(), - ]); - - const moduleConfigs = await Promise.all( - modules.map(async (moduleAddress: any) => { - try { - return await this.deriveIsmConfig( - num.toHex64(moduleAddress.toString()), - ); - } catch (error) { - this.logger.error( - `Failed to derive config for module ${moduleAddress}`, - error, - ); - return null; - } - }), - ); - - return { - type: IsmType.AGGREGATION, - address, - modules: moduleConfigs.filter(Boolean), - threshold: threshold.toString(), + owner: num.toHex64(owner.toString()), }; } } diff --git a/typescript/sdk/src/messaging/MessageService.ts b/typescript/sdk/src/messaging/MessageService.ts new file mode 100644 index 00000000000..9811ceba09a --- /dev/null +++ b/typescript/sdk/src/messaging/MessageService.ts @@ -0,0 +1,80 @@ +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { HyperlaneCore } from '../core/HyperlaneCore.js'; +import { StarknetCore } from '../core/StarknetCore.js'; +import { DispatchedMessage } from '../core/types.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { ChainName } from '../types.js'; + +import { getMessageMetadata, translateMessage } from './messageUtils.js'; + +export class MessageService { + constructor( + protected readonly multiProvider: MultiProvider, + private readonly cores: Partial< + Record + >, + ) {} + + async sendMessage({ + origin, + destination, + recipient, + body, + }: { + origin: ChainName; + destination: ChainName; + recipient: string; + body: string; + }) { + const originProtocol = this.multiProvider.getProtocol(origin); + const core = this.getProtocolCoreOrFail(originProtocol); + + return core.sendMessage(origin, destination, recipient, body); + } + + async relayMessage(message: DispatchedMessage) { + const originProtocol = this.multiProvider.getProtocol( + message.parsed.origin!, + ); + const destinationProtocol = this.multiProvider.getProtocol( + message.parsed.destination!, + ); + const core = this.getProtocolCoreOrFail(destinationProtocol); + + const messageData = translateMessage( + message, + originProtocol, + destinationProtocol, + ); + + return core.deliver( + messageData ? { ...message, message: messageData } : message, + getMessageMetadata(destinationProtocol), + ); + } + + async awaitMessagesDelivery( + message: DispatchedMessage, + delay?: number, + maxAttempts?: number, + ) { + const destinationProtocol = this.multiProvider.getProtocol( + message.parsed.destination!, + ); + const core = this.getProtocolCoreOrFail(destinationProtocol); + + await core.waitForMessageIdProcessed( + message.id, + this.multiProvider.getChainName(message.parsed.destination!), + delay, + maxAttempts, + ); + } + + private getProtocolCoreOrFail(protocol: ProtocolType) { + const core = this.cores[protocol]; + if (!core) throw new Error(`No core for ${protocol}`); + return core; + } +} diff --git a/typescript/sdk/src/messaging/messageUtils.ts b/typescript/sdk/src/messaging/messageUtils.ts new file mode 100644 index 00000000000..4664a1903dd --- /dev/null +++ b/typescript/sdk/src/messaging/messageUtils.ts @@ -0,0 +1,199 @@ +import { ethers } from 'ethers'; +import { Uint256, num, uint256 } from 'starknet'; + +import { ParsedMessage, ProtocolType } from '@hyperlane-xyz/utils'; + +import { DispatchedMessage } from '../core/types.js'; + +export function formatEthereumMessageForStarknet(message: DispatchedMessage): { + version: number; + nonce: number; + origin: number; + sender: Uint256; + destination: number; + recipient: Uint256; + body: { size: number; data: bigint[] }; +} { + const sender = uint256.bnToUint256(message.parsed.sender); + const recipient = uint256.bnToUint256(message.parsed.recipient); + + // Rest of the code remains the same + const messageArray = ethers.utils.arrayify(message.message); + const version = messageArray[0]; + const nonce = message.parsed.nonce; + const origin = message.parsed.origin; + const destination = message.parsed.destination; + const body = messageArray.slice(77); + + return { + version, + nonce, + origin, + sender, + destination, + recipient, + body: toStarknetMessageBytes(body), + }; +} + +export function formatParsedStarknetMessageForEthereum(message: { + version: number; + nonce: number; + origin: number; + sender: Uint256; + destination: number; + recipient: Uint256; + body: { size: number; data: bigint[] }; +}): DispatchedMessage['parsed'] { + const sender = uint256.uint256ToBN(message.sender).toString(); + const recipient = uint256.uint256ToBN(message.recipient).toString(); + + const nonce = message.nonce; + const origin = message.origin; + const destination = message.destination; + + return { + version: message.version, + nonce, + origin, + sender: '0x' + sender, + destination, + recipient: '0x' + recipient, + body: '0x', + }; +} + +export function formatStarknetMessageForEthereum( + starknetMessage: ParsedMessage & { + body: { size: bigint; data: bigint[] }; + }, +): Uint8Array { + const VERSION_OFFSET = 0; + const NONCE_OFFSET = 1; + const ORIGIN_OFFSET = 5; + const SENDER_OFFSET = 9; + const DESTINATION_OFFSET = 41; + const RECIPIENT_OFFSET = 45; + const BODY_OFFSET = 77; + + const bodyBytes = convertU128ArrayToBytes(starknetMessage.body.data); + const buffer = new Uint8Array(BODY_OFFSET + bodyBytes.length); + const view = new DataView(buffer.buffer); + buffer[VERSION_OFFSET] = Number(starknetMessage.version); + view.setUint32(NONCE_OFFSET, Number(starknetMessage.nonce), false); + view.setUint32(ORIGIN_OFFSET, Number(starknetMessage.origin), false); + const senderValue = + typeof starknetMessage.sender === 'string' + ? BigInt(starknetMessage.sender) + : starknetMessage.sender; + const senderBytes = num.hexToBytes(num.toHex64(senderValue)); + buffer.set(senderBytes, SENDER_OFFSET); + view.setUint32( + DESTINATION_OFFSET, + Number(starknetMessage.destination), + false, + ); + const recipientValue = + typeof starknetMessage.recipient === 'string' + ? BigInt(starknetMessage.recipient) + : starknetMessage.recipient; + const recipientBytes = num.hexToBytes(num.toHex64(recipientValue)); + buffer.set(recipientBytes, RECIPIENT_OFFSET); + buffer.set(bodyBytes, BODY_OFFSET); + return buffer; +} + +/** + * Convert a byte array to a starknet message + * Pads the bytes to 16 bytes chunks + * @param bytes Input byte array + * @returns Object containing size and padded data array + */ +export function toStarknetMessageBytes(bytes: Uint8Array): { + size: number; + data: bigint[]; +} { + // Calculate the required padding + const padding = (16 - (bytes.length % 16)) % 16; + const totalLen = bytes.length + padding; + + // Create a new byte array with the necessary padding + const paddedBytes = new Uint8Array(totalLen); + paddedBytes.set(bytes); + // Padding remains as zeros by default in Uint8Array + + // Convert to chunks of 16 bytes + const result: bigint[] = []; + for (let i = 0; i < totalLen; i += 16) { + const chunk = paddedBytes.slice(i, i + 16); + // Convert chunk to bigint (equivalent to u128 in Rust) + const value = BigInt('0x' + Buffer.from(chunk).toString('hex')); + result.push(value); + } + + return { + size: bytes.length, + data: result, + }; +} + +/** + * Convert vector of u128 to bytes + */ +export function convertU128ArrayToBytes(input: bigint[]): Uint8Array { + const output = new Uint8Array(input.length * 16); // Each u128 takes 16 bytes + input.forEach((value, index) => { + const hex = num.toHex(value); + // Remove '0x' prefix, pad to 32 chars, then add '0x' back + const paddedHex = '0x' + hex.replace('0x', '').padStart(32, '0'); + const bytes = num.hexToBytes(paddedHex); + output.set(bytes, index * 16); + }); + return output; +} + +type TranslatorFn = (message: DispatchedMessage) => any; + +const translators: Partial< + Record>> +> = { + [ProtocolType.Ethereum]: { + [ProtocolType.Ethereum]: (message) => message.message, + [ProtocolType.Starknet]: (message) => + formatEthereumMessageForStarknet(message), + }, + [ProtocolType.Starknet]: { + [ProtocolType.Ethereum]: (message) => + formatStarknetMessageForEthereum(message.parsed as any), + [ProtocolType.Starknet]: (message) => message.message, + }, +}; + +export function translateMessage( + message: DispatchedMessage, + originProtocol: ProtocolType, + destinationProtocol: ProtocolType, +) { + const translator = translators[originProtocol]?.[destinationProtocol]; + if (!translator) { + throw new Error( + `No translator found for ${originProtocol} -> ${destinationProtocol}`, + ); + } + return translator(message); +} + +type MetadataFn = () => any; + +const metadataHandlers: Partial> = { + [ProtocolType.Ethereum]: () => '0x0001', + [ProtocolType.Starknet]: () => ({ size: 0, data: [] }), +}; + +export function getMessageMetadata(destinationProtocol: ProtocolType): any { + const handler = metadataHandlers[destinationProtocol]; + if (!handler) { + throw new Error(`No metadata handler for ${destinationProtocol}`); + } + return handler(); +} diff --git a/typescript/sdk/src/providers/MultiProvider.ts b/typescript/sdk/src/providers/MultiProvider.ts index 7b5f923061f..9f9f8b0fe26 100644 --- a/typescript/sdk/src/providers/MultiProvider.ts +++ b/typescript/sdk/src/providers/MultiProvider.ts @@ -316,7 +316,7 @@ export class MultiProvider extends ChainMetadataManager { // setup contract factory const overrides = this.getTransactionOverrides(chainNameOrId); const signer = this.getSigner(chainNameOrId); - const contractFactory = await factory.connect(signer); + const contractFactory = factory.connect(signer); // estimate gas const deployTx = contractFactory.getDeployTransaction(...params); diff --git a/typescript/sdk/src/providers/ProviderType.ts b/typescript/sdk/src/providers/ProviderType.ts index 83dc465fe53..772e378979c 100644 --- a/typescript/sdk/src/providers/ProviderType.ts +++ b/typescript/sdk/src/providers/ProviderType.ts @@ -19,8 +19,7 @@ import { Contract as StarknetContract, Invocation as StarknetInvocation, Provider as StarknetProvider, - ReceiptTx as StarknetReceiptTx, - TransactionReceipt as StarknetTxReceipt, + GetTransactionReceiptResponse as StarknetTxReceipt, } from 'starknet'; import type { GetContractReturnType, @@ -332,9 +331,9 @@ export interface CosmJsWasmTransactionReceipt } export interface StarknetJsTransactionReceipt - extends TypedTransactionReceiptBase { + extends TypedTransactionReceiptBase { type: ProviderType.Starknet; - receipt: StarknetTxReceipt | StarknetReceiptTx; + receipt: StarknetTxReceipt; } export interface ZKSyncTransactionReceipt diff --git a/typescript/sdk/src/providers/rpcHealthTest.ts b/typescript/sdk/src/providers/rpcHealthTest.ts index 47b9ece24ad..b3e8e4616aa 100644 --- a/typescript/sdk/src/providers/rpcHealthTest.ts +++ b/typescript/sdk/src/providers/rpcHealthTest.ts @@ -9,6 +9,7 @@ import { EthersV5Provider, ProviderType, SolanaWeb3Provider, + StarknetJsProvider, } from './ProviderType.js'; import { protocolToDefaultProviderBuilder } from './providerBuilders.js'; @@ -28,6 +29,8 @@ export async function isRpcHealthy( provider.type === ProviderType.CosmJs ) return isCosmJsProviderHealthy(provider.provider, metadata); + else if (provider.type === ProviderType.Starknet) + return isStarknetJsProviderHealthy(provider.provider, metadata); else throw new Error( `Unsupported provider type ${provider.type}, new health check required`, @@ -83,3 +86,13 @@ export async function isCosmJsProviderHealthy( rootLogger.debug(`Block number is okay for ${metadata.name}`); return true; } + +export async function isStarknetJsProviderHealthy( + provider: StarknetJsProvider['provider'], + metadata: ChainMetadata, +): Promise { + const blockNumber = await provider.getBlockNumber(); + if (!blockNumber || blockNumber < 0) return false; + rootLogger.debug(`Block number is okay for ${metadata.name}`); + return true; +} diff --git a/typescript/sdk/src/providers/transactionFeeEstimators.ts b/typescript/sdk/src/providers/transactionFeeEstimators.ts index f689fe78af0..9d28cc39eb7 100644 --- a/typescript/sdk/src/providers/transactionFeeEstimators.ts +++ b/typescript/sdk/src/providers/transactionFeeEstimators.ts @@ -20,6 +20,8 @@ import { ProviderType, SolanaWeb3Provider, SolanaWeb3Transaction, + StarknetJsProvider, + StarknetJsTransaction, TypedProvider, TypedTransaction, ViemProvider, @@ -280,9 +282,40 @@ export function estimateTransactionFee({ sender, senderPubKey, }); + } else if ( + transaction.type === ProviderType.Starknet && + provider.type === ProviderType.Starknet + ) { + return estimateTransactionFeeStarknet({ transaction, provider, sender }); } else { throw new Error( `Unsupported transaction type ${transaction.type} or provider type ${provider.type} for gas estimation`, ); } } + +export async function estimateTransactionFeeStarknet({ + transaction, + provider, + sender, +}: { + transaction: StarknetJsTransaction; + provider: StarknetJsProvider; + sender: Address; +}): Promise { + // const { provider: starknetProvider } = provider; + // const nonce = await starknetProvider.getNonceForAddress(sender); + // const estimation = await starknetProvider.getInvokeEstimateFee( + // { + // calldata: transaction.transaction.calldata, + // contractAddress: transaction.transaction.contractAddress, + // entrypoint: transaction.transaction.entrypoint, + // signature: [], + // }, + // { nonce }, + // 'latest', + // true, + // ); + // console.log({ estimation }); + return { gasUnits: 0, gasPrice: 0, fee: 0 }; +} diff --git a/typescript/sdk/src/token/StarknetERC20WarpModule.ts b/typescript/sdk/src/token/StarknetERC20WarpModule.ts index d5257404997..da068d87fe7 100644 --- a/typescript/sdk/src/token/StarknetERC20WarpModule.ts +++ b/typescript/sdk/src/token/StarknetERC20WarpModule.ts @@ -1,6 +1,6 @@ import { Account, - Contract, + MultiType, Uint256, byteArray, eth, @@ -8,7 +8,11 @@ import { uint256, } from 'starknet'; -import { TokenType } from '@hyperlane-xyz/sdk'; +import { + StarknetContractName, + TokenType, + getStarknetHypERC20Contract, +} from '@hyperlane-xyz/sdk'; import { ContractType } from '@hyperlane-xyz/starknet-core'; import { ProtocolType, assert, rootLogger } from '@hyperlane-xyz/utils'; @@ -18,19 +22,19 @@ import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap } from '../types.js'; import { HypERC20Deployer } from './deploy.js'; -import { WarpRouteDeployConfig } from './types.js'; +import { PROTOCOL_TO_DEFAULT_NATIVE_TOKEN } from './nativeTokenMetadata.js'; +import { WarpRouteDeployConfigMailboxRequired } from './types.js'; export class StarknetERC20WarpModule { protected logger = rootLogger.child({ module: 'StarknetERC20WarpModule' }); constructor( protected readonly account: ChainMap, - protected readonly config: WarpRouteDeployConfig, + protected readonly config: WarpRouteDeployConfigMailboxRequired, protected readonly multiProvider: MultiProvider, ) {} public async deployToken(): Promise> { - // TODO: manage this in a multi-protocol way, for now works as we just support native-synthetic pair const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( this.multiProvider, this.config, @@ -51,7 +55,10 @@ export class StarknetERC20WarpModule { ) continue; - const deployer = new StarknetDeployer(this.account[chain]); + const deployer = new StarknetDeployer( + this.account[chain], + this.multiProvider, + ); const deployerAccountAddress = this.account[chain].address; const ismAddress = await this.getStarknetDeploymentISMAddress({ ismConfig: interchainSecurityModule, @@ -62,7 +69,7 @@ export class StarknetERC20WarpModule { switch (type) { case TokenType.synthetic: { const tokenAddress = await deployer.deployContract( - 'HypErc20', + StarknetContractName.HYP_ERC20, { decimals: tokenMetadata.decimals, mailbox: mailbox!, @@ -80,11 +87,12 @@ export class StarknetERC20WarpModule { } case TokenType.native: { const tokenAddress = await deployer.deployContract( - 'HypNative', + StarknetContractName.HYP_NATIVE, { - mailbox: mailbox!, - native_token: - '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7', // ETH address on Starknet chains + mailbox: mailbox, + native_token: PROTOCOL_TO_DEFAULT_NATIVE_TOKEN[ + ProtocolType.Starknet + ]!.denom as MultiType, hook: getChecksumAddress(0), interchain_security_module: ismAddress, owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner @@ -96,16 +104,8 @@ export class StarknetERC20WarpModule { } case TokenType.collateral: { - console.log({ - mailbox: mailbox, - // @ts-ignore - erc20: rest.token, - owner: deployerAccountAddress, //TODO: use config.owner, and in warp init ask for starknet owner - hook: getChecksumAddress(0), - interchain_security_module: ismAddress, - }); const tokenAddress = await deployer.deployContract( - 'HypErc20Collateral', + StarknetContractName.HYP_ERC20_COLLATERAL, { mailbox: mailbox!, // @ts-ignore @@ -166,73 +166,53 @@ export class StarknetERC20WarpModule { const account = this.account[chain]; - // Updated Router ABI to include batch enrollment - const ROUTER_ABI = [ - { - type: 'function', - name: 'enroll_remote_routers', - inputs: [ - { - name: 'domains', - type: 'core::array::Array::', - }, - { - name: 'routers', - type: 'core::array::Array::', - }, - ], - outputs: [], - state_mutability: 'external', + // HypERC20 inherits RouterComponent + const routerContract = getStarknetHypERC20Contract(tokenAddress, account); + + // Prepare arrays for batch enrollment + const domains: number[] = []; + const routers: Uint256[] = []; + + // Collect all remote chains' data + Object.entries(routerAddresses).forEach( + ([remoteChain, remoteAddress]) => { + if (remoteChain === chain) return; // Skip self-enrollment + + const remoteDomain = this.multiProvider.getDomainId(remoteChain); + const remoteProtocol = + this.multiProvider.getChainMetadata(remoteChain).protocol; + + // Only validate and parse ETH address for Ethereum chains + const remoteRouter = uint256.bnToUint256( + remoteProtocol === ProtocolType.Ethereum + ? eth.validateAndParseEthAddress(remoteAddress) + : remoteAddress, + ); + + domains.push(remoteDomain); + routers.push(remoteRouter); }, - ]; - - const contract = new Contract(ROUTER_ABI, tokenAddress, account); - - try { - // Prepare arrays for batch enrollment - const domains: number[] = []; - const routers: Uint256[] = []; - - // Collect all remote chains' data - Object.entries(routerAddresses).forEach( - ([remoteChain, remoteAddress]) => { - if (remoteChain === chain) return; // Skip self-enrollment - - const remoteDomain = this.multiProvider.getDomainId(remoteChain); - const remoteProtocol = - this.multiProvider.getChainMetadata(remoteChain).protocol; - - // Only validate and parse ETH address for Ethereum chains - const remoteRouter = uint256.bnToUint256( - remoteProtocol === ProtocolType.Ethereum - ? eth.validateAndParseEthAddress(remoteAddress) - : remoteAddress, - ); - - domains.push(remoteDomain); - routers.push(remoteRouter); - }, - ); + ); - this.logger.info( - `Batch enrolling ${domains.length} remote routers on ${chain}`, - ); + this.logger.info( + `Batch enrolling ${domains.length} remote routers on ${chain}`, + ); - const tx = await contract.invoke('enroll_remote_routers', [ - domains, - routers, - ]); + const tx = await routerContract.invoke('enroll_remote_routers', [ + domains, + routers, + ]); - await account.waitForTransaction(tx.transaction_hash); + const receipt = await account.waitForTransaction(tx.transaction_hash); + if (receipt.isSuccess()) { this.logger.info( `Successfully enrolled all remote routers on ${chain}. Transaction: ${tx.transaction_hash}`, ); - } catch (error) { + } else { this.logger.error( - `Failed to enroll remote routers on ${chain}: ${error}`, + `Failed to enroll all remote routers on ${chain}. Transaction: ${tx.transaction_hash}`, ); - throw error; } } } diff --git a/typescript/sdk/src/token/StarknetERC20WarpRouteReader.ts b/typescript/sdk/src/token/StarknetERC20WarpRouteReader.ts new file mode 100644 index 00000000000..2bf8a0cd388 --- /dev/null +++ b/typescript/sdk/src/token/StarknetERC20WarpRouteReader.ts @@ -0,0 +1,306 @@ +import { getChecksumAddress, num, uint256 } from 'starknet'; + +import { Address, Domain, rootLogger } from '@hyperlane-xyz/utils'; + +import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; +import { StarknetHookReader } from '../hook/StarknetHookReader.js'; +import { StarknetIsmReader } from '../ism/StarknetIsmReader.js'; +import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js'; +import { StarknetJsProvider } from '../providers/ProviderType.js'; +import { + DestinationGas, + MailboxClientConfig, + RemoteRouters, +} from '../router/types.js'; +import { ChainName } from '../types.js'; +import { + getStarknetHypERC20CollateralContract, + getStarknetHypERC20Contract, + getStarknetHypNativeContract, +} from '../utils/starknet.js'; + +import { TokenType } from './config.js'; +import { + HypTokenConfig, + HypTokenRouterConfig, + TokenMetadata, +} from './types.js'; + +export class StarknetERC20WarpRouteReader { + protected readonly logger = rootLogger.child({ + module: 'StarknetERC20WarpRouteReader', + }); + starknetHookReader: StarknetHookReader; + starknetIsmReader: StarknetIsmReader; + protected readonly domainId: Domain; + protected readonly provider: StarknetJsProvider['provider']; + + private static tokenTypeCache: Map = new Map(); + + constructor( + protected readonly multiProvider: MultiProtocolProvider, + protected readonly chain: ChainName, + protected readonly concurrency: number = DEFAULT_CONTRACT_READ_CONCURRENCY, + ) { + this.provider = multiProvider.getStarknetProvider(chain); + this.starknetHookReader = new StarknetHookReader(multiProvider, chain); + this.starknetIsmReader = new StarknetIsmReader(multiProvider, chain); + this.domainId = multiProvider.getDomainId(chain); + } + + /** + * Derives the configuration for a Hyperlane Starknet token router contract at the given address. + * + * @param warpRouteAddress - The address of the Hyperlane token router contract. + * @returns The configuration for the Hyperlane token router. + */ + async deriveWarpRouteConfig( + warpRouteAddress: Address, + ): Promise { + // Derive the token type + const type = await this.deriveTokenType(warpRouteAddress); + const baseMetadata = await this.fetchMailboxClientConfig(warpRouteAddress); + const tokenConfig = await this.fetchTokenConfig(type, warpRouteAddress); + const remoteRouters = await this.fetchRemoteRouters(warpRouteAddress); + const destinationGas = await this.fetchDestinationGas(warpRouteAddress); + + return { + ...baseMetadata, + ...tokenConfig, + remoteRouters, + destinationGas, + type, + } as HypTokenRouterConfig; + } + + /** + * Determines the type of token contract at the given address. + * + * @param warpRouteAddress - The address of the token contract. + * @returns The token type. + */ + async deriveTokenType(warpRouteAddress: Address): Promise { + const cacheKey = `${this.domainId}-${warpRouteAddress}`; + const cached = StarknetERC20WarpRouteReader.tokenTypeCache.get(cacheKey); + if (cached) return cached; + + this.logger.debug( + `Deriving token type for ${warpRouteAddress} on ${this.chain}`, + ); + + try { + // Try to detect if this is a collateral token + try { + const collateralContract = getStarknetHypERC20CollateralContract( + warpRouteAddress, + this.provider, + ); + // Check if contract has wrapped_token function + await collateralContract.wrapped_token(); + const type = TokenType.collateral; + StarknetERC20WarpRouteReader.tokenTypeCache.set(cacheKey, type); + return type; + } catch (e) { + // Not a collateral token + } + + // Try to detect if this is a native token + try { + const nativeContract = getStarknetHypNativeContract( + warpRouteAddress, + this.provider, + ); + // Check if contract has native_token function + await nativeContract.native_token(); + const type = TokenType.native; + StarknetERC20WarpRouteReader.tokenTypeCache.set(cacheKey, type); + return type; + } catch (e) { + // Not a native token + } + + // Default to synthetic token if previous checks fail + const type = TokenType.synthetic; + StarknetERC20WarpRouteReader.tokenTypeCache.set(cacheKey, type); + return type; + } catch (e) { + this.logger.error( + `Failed to derive token type for ${warpRouteAddress}`, + e, + ); + throw new Error(`Unable to determine token type: ${e}`); + } + } + + /** + * Fetches the base metadata for a token contract. + * + * @param routerAddress - The address of the token contract. + * @returns The base metadata for the token contract. + */ + async fetchMailboxClientConfig( + routerAddress: Address, + ): Promise { + const contract = getStarknetHypERC20Contract(routerAddress, this.provider); + + const [mailbox, owner, hook, ism] = await Promise.all([ + contract.mailbox().then((res: any) => num.toHex64(res.toString())), + contract.owner().then((res: any) => num.toHex64(res.toString())), + contract.hook().then((res: any) => num.toHex64(res.toString())), + contract + .interchain_security_module() + .then((res: any) => num.toHex64(res.toString())), + ]); + + const derivedIsm = + ism === getChecksumAddress(0) + ? getChecksumAddress(0) + : await this.starknetIsmReader.deriveIsmConfig(ism); + + const derivedHook = + hook === getChecksumAddress(0) + ? getChecksumAddress(0) + : await this.starknetHookReader.deriveHookConfig(hook); + + return { + mailbox, + owner, + hook: derivedHook, + interchainSecurityModule: derivedIsm, + }; + } + + /** + * Fetches token-specific configuration based on token type. + * + * @param type - The token type. + * @param warpRouteAddress - The address of the token contract. + * @returns Token-specific configuration. + */ + async fetchTokenConfig( + type: TokenType, + warpRouteAddress: Address, + ): Promise { + if (type === TokenType.collateral) { + const contract = getStarknetHypERC20CollateralContract( + warpRouteAddress, + this.provider, + ); + + const token = await contract + .wrapped_token() + .then((res: any) => num.toHex64(res.toString())); + + const { name, symbol, decimals } = await this.fetchERC20Metadata(token); + + return { + type, + name, + symbol, + decimals, + token, + }; + } else if (type === TokenType.synthetic) { + const metadata = await this.fetchERC20Metadata(warpRouteAddress); + return { + type, + ...metadata, + }; + } else if (type === TokenType.native) { + const chainMetadata = this.multiProvider.getChainMetadata(this.chain); + if (chainMetadata.nativeToken) { + const { name, symbol, decimals } = chainMetadata.nativeToken; + return { + type, + name, + symbol, + decimals, + }; + } else { + throw new Error( + `Chain metadata for ${this.chain} does not provide native token details`, + ); + } + } else { + throw new Error( + `Unsupported token type ${type} when fetching token metadata`, + ); + } + } + + /** + * Fetches ERC20 metadata from a token contract. + * + * @param tokenAddress - The address of the token contract. + * @returns Token metadata including name, symbol, and decimals. + */ + async fetchERC20Metadata(tokenAddress: Address): Promise { + const contract = getStarknetHypERC20Contract(tokenAddress, this.provider); + + const [nameBytes, symbolBytes, decimals] = await Promise.all([ + contract.name(), + contract.symbol(), + contract.decimals(), + ]); + + return { + name: nameBytes, + symbol: symbolBytes, + decimals: Number(decimals), + }; + } + + /** + * Fetches the remote routers configuration. + * + * @param warpRouteAddress - The address of the token contract. + * @returns Map of remote domain IDs to router addresses. + */ + async fetchRemoteRouters(warpRouteAddress: Address): Promise { + const contract = getStarknetHypERC20Contract( + warpRouteAddress, + this.provider, + ); + + const domains: number[] = await contract.domains(); + + const routers: RemoteRouters = {}; + + for (const domain of domains) { + const routerUint256 = await contract.routers(domain); + // Convert Uint256 to Address format + const routerAddress = num.toHex64( + uint256.uint256ToBN(routerUint256).toString(), + ); + routers[domain.toString()] = { address: routerAddress }; + } + + return routers; + } + + /** + * Fetches destination gas configuration. + * + * @param warpRouteAddress - The address of the token contract. + * @returns Map of domain IDs to gas amounts. + */ + async fetchDestinationGas( + warpRouteAddress: Address, + ): Promise { + const contract = getStarknetHypERC20Contract( + warpRouteAddress, + this.provider, + ); + + const domains: number[] = await contract.domains(); + + const destinationGas: DestinationGas = {}; + + for (const domain of domains) { + const gasAmount = await contract.destination_gas(domain); + destinationGas[domain.toString()] = gasAmount.toString(); + } + + return destinationGas; + } +} diff --git a/typescript/sdk/src/token/Token.ts b/typescript/sdk/src/token/Token.ts index 3c5f3a55363..7cf9d3be083 100644 --- a/typescript/sdk/src/token/Token.ts +++ b/typescript/sdk/src/token/Token.ts @@ -61,6 +61,11 @@ import { SealevelNativeTokenAdapter, SealevelTokenAdapter, } from './adapters/SealevelTokenAdapter.js'; +import { + StarknetHypCollateralAdapter, + StarknetHypNativeAdapter, + StarknetHypSyntheticAdapter, +} from './adapters/StarknetTokenAdapter.js'; import { PROTOCOL_TO_DEFAULT_NATIVE_TOKEN } from './nativeTokenMetadata.js'; // Declaring the interface in addition to class allows @@ -284,6 +289,18 @@ export class Token implements IToken { const connection = this.getConnectionForChain(destination); assert(connection, `No connection found for chain ${destination}`); return this.getIbcAdapter(multiProvider, connection); + } else if (standard === TokenStandard.StarknetHypNative) { + return new StarknetHypNativeAdapter(chainName, multiProvider, { + warpRouter: addressOrDenom, + }); + } else if (standard === TokenStandard.StarknetHypSynthetic) { + return new StarknetHypSyntheticAdapter(chainName, multiProvider, { + warpRouter: addressOrDenom, + }); + } else if (standard === TokenStandard.StarknetHypCollateral) { + return new StarknetHypCollateralAdapter(chainName, multiProvider, { + warpRouter: addressOrDenom, + }); } else { throw new Error(`No hyp adapter found for token standard: ${standard}`); } diff --git a/typescript/sdk/src/token/adapters/StarknetTokenAdapter.ts b/typescript/sdk/src/token/adapters/StarknetTokenAdapter.ts new file mode 100644 index 00000000000..45966ef36c0 --- /dev/null +++ b/typescript/sdk/src/token/adapters/StarknetTokenAdapter.ts @@ -0,0 +1,284 @@ +import { BigNumber } from 'ethers'; +import { CairoOption, CairoOptionVariant, Call, Contract, num } from 'starknet'; + +import { + Address, + Domain, + Numberish, + ProtocolType, + assert, +} from '@hyperlane-xyz/utils'; + +import { BaseStarknetAdapter } from '../../app/MultiProtocolApp.js'; +import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider.js'; +import { ChainName } from '../../types.js'; +import { + getStarknetEtherContract, + getStarknetHypERC20CollateralContract, + getStarknetHypERC20Contract, +} from '../../utils/starknet.js'; +import { PROTOCOL_TO_DEFAULT_NATIVE_TOKEN } from '../nativeTokenMetadata.js'; +import { TokenMetadata } from '../types.js'; + +import { + IHypTokenAdapter, + InterchainGasQuote, + TransferParams, + TransferRemoteParams, +} from './ITokenAdapter.js'; + +export class StarknetHypSyntheticAdapter + extends BaseStarknetAdapter + implements IHypTokenAdapter +{ + public readonly contract: Contract; + + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { warpRouter: Address }, + ) { + super(chainName, multiProvider, addresses); + this.contract = getStarknetHypERC20Contract( + addresses.warpRouter, + multiProvider.getStarknetProvider(chainName), + ); + } + + async getBalance(address: Address): Promise { + return this.contract.balanceOf(address); + } + + async getMetadata(_isNft?: boolean): Promise { + const [decimals, symbol, name] = await Promise.all([ + this.contract.decimals(), + this.contract.symbol(), + this.contract.name(), + ]); + return { decimals, symbol, name }; + } + + async isApproveRequired( + owner: Address, + spender: Address, + weiAmountOrId: Numberish, + ): Promise { + const allowance = await this.contract.allowance(owner, spender); + return BigNumber.from(allowance.toString()).lt( + BigNumber.from(weiAmountOrId), + ); + } + + async populateApproveTx({ + weiAmountOrId, + recipient, + }: TransferParams): Promise { + return this.contract.populateTransaction.approve(recipient, weiAmountOrId); + } + + async populateTransferTx({ + weiAmountOrId, + recipient, + }: TransferParams): Promise { + return this.contract.populateTransaction.transfer(recipient, weiAmountOrId); + } + + async getTotalSupply(): Promise { + return undefined; + } + + async quoteTransferRemoteGas( + _destination: Domain, + ): Promise { + return { amount: BigInt(0) }; + } + + async populateTransferRemoteTx({ + weiAmountOrId, + destination, + recipient, + interchainGas, + }: TransferRemoteParams): Promise { + const nonOption = new CairoOption(CairoOptionVariant.None); + const transferTx = this.contract.populateTransaction.transfer_remote( + destination, + recipient, + BigInt(weiAmountOrId.toString()), + BigInt(0), + nonOption, + nonOption, + ); + + return { + ...transferTx, + value: interchainGas?.amount + ? BigNumber.from(interchainGas.amount) + : BigNumber.from(0), + }; + } + + async getMinimumTransferAmount(_recipient: Address): Promise { + return 0n; + } + + async getDomains(): Promise { + return this.contract.domains(); + } + + async getRouterAddress(domain: Domain): Promise { + const routerAddresses = await this.contract.routers(domain); + return Buffer.from(routerAddresses); + } + + async getAllRouters(): Promise> { + const domains = await this.getDomains(); + const routers: Buffer[] = await Promise.all( + domains.map((d) => this.getRouterAddress(d)), + ); + return domains.map((d, i) => ({ domain: d, address: routers[i] })); + } + + async getBridgedSupply(): Promise { + return undefined; + } +} + +export class StarknetHypCollateralAdapter extends StarknetHypSyntheticAdapter { + public readonly collateralContract: Contract; + protected wrappedTokenAddress?: Address; + + constructor( + chainName: ChainName, + multiProvider: MultiProtocolProvider, + addresses: { warpRouter: Address }, + ) { + super(chainName, multiProvider, addresses); + this.collateralContract = getStarknetHypERC20CollateralContract( + addresses.warpRouter, + multiProvider.getStarknetProvider(chainName), + ); + } + + protected async getWrappedTokenAddress(): Promise
{ + if (!this.wrappedTokenAddress) { + this.wrappedTokenAddress = num.toHex64( + await this.collateralContract.get_wrapped_token(), + ); + } + return this.wrappedTokenAddress!; + } + + protected async getWrappedTokenAdapter(): Promise { + return new StarknetHypSyntheticAdapter(this.chainName, this.multiProvider, { + warpRouter: await this.getWrappedTokenAddress(), + }); + } + + async getBalance(address: Address): Promise { + const adapter = await this.getWrappedTokenAdapter(); + return adapter.getBalance(address); + } + + override getBridgedSupply(): Promise { + return this.getBalance(this.addresses.warpRouter); + } + + override async getMetadata(isNft?: boolean): Promise { + const adapter = await this.getWrappedTokenAdapter(); + return adapter.getMetadata(isNft); + } + + override async isApproveRequired( + owner: Address, + spender: Address, + weiAmountOrId: Numberish, + ): Promise { + const adapter = await this.getWrappedTokenAdapter(); + return adapter.isApproveRequired(owner, spender, weiAmountOrId); + } + + override async populateApproveTx(params: TransferParams): Promise { + const adapter = await this.getWrappedTokenAdapter(); + return adapter.populateApproveTx(params); + } + + override async populateTransferTx(params: TransferParams): Promise { + const adapter = await this.getWrappedTokenAdapter(); + return adapter.populateTransferTx(params); + } +} + +export class StarknetHypNativeAdapter extends StarknetHypSyntheticAdapter { + public readonly collateralContract: Contract; + public readonly nativeContract: Contract; + + constructor( + chainName: ChainName, + multiProvider: MultiProtocolProvider, + addresses: { warpRouter: Address }, + ) { + super(chainName, multiProvider, addresses); + this.collateralContract = getStarknetHypERC20CollateralContract( + addresses.warpRouter, + multiProvider.getStarknetProvider(chainName), + ); + const nativeAddress = + multiProvider.getChainMetadata(chainName)?.nativeToken?.denom; + assert(nativeAddress, `Native address not found for chain ${chainName}`); + this.nativeContract = getStarknetEtherContract( + nativeAddress ?? + PROTOCOL_TO_DEFAULT_NATIVE_TOKEN[ProtocolType.Starknet]!.denom, + multiProvider.getStarknetProvider(chainName), + ); + } + + async getBalance(address: Address): Promise { + return this.nativeContract.balanceOf(address); + } + + async isApproveRequired( + owner: Address, + spender: Address, + weiAmountOrId: Numberish, + ): Promise { + const allowance = await this.nativeContract.allowance(owner, spender); + return BigNumber.from(allowance.toString()).lt( + BigNumber.from(weiAmountOrId), + ); + } + + async populateApproveTx({ + weiAmountOrId, + recipient, + }: TransferParams): Promise { + return this.nativeContract.populateTransaction.approve( + recipient, + weiAmountOrId, + ); + } + + async populateTransferRemoteTx({ + weiAmountOrId, + destination, + recipient, + interchainGas, + }: TransferRemoteParams): Promise { + const nonOption = new CairoOption(CairoOptionVariant.None); + const transferTx = + this.collateralContract.populateTransaction.transfer_remote( + destination, + recipient, + BigInt(weiAmountOrId.toString()), + BigInt(weiAmountOrId.toString()), + nonOption, + nonOption, + ); + + return { + ...transferTx, + value: interchainGas?.amount + ? BigNumber.from(interchainGas.amount) + : BigNumber.from(0), + }; + } +} diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index de02c3e2d98..359756db5f0 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -1,5 +1,5 @@ import { constants } from 'ethers'; -import { Contract, RpcProvider, shortString } from 'starknet'; +import { Contract, shortString } from 'starknet'; import { ERC20__factory, @@ -20,6 +20,7 @@ import { HyperlaneContracts } from '../contracts/types.js'; import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; +import { defaultStarknetJsProviderBuilder } from '../providers/providerBuilders.js'; import { GasRouterDeployer } from '../router/GasRouterDeployer.js'; import { ChainName } from '../types.js'; @@ -146,14 +147,14 @@ abstract class TokenDeployer< if (isCollateralTokenConfig(config) || isXERC20TokenConfig(config)) { // Add chain type checking const chainMetadata = multiProvider.getChainMetadata(chain); - const isStarknet = chainMetadata.protocol === 'starknet'; + const isStarknet = chainMetadata.protocol === ProtocolType.Starknet; let provider; // Handle different chain types if (isStarknet) { - provider = new RpcProvider({ - nodeUrl: chainMetadata.rpcUrls[0].http as any, // Use the actual RPC URL from chain metadata - }); + provider = defaultStarknetJsProviderBuilder( + chainMetadata.rpcUrls, + ).provider; } else { provider = multiProvider.getProvider(chain) as any; } @@ -192,6 +193,7 @@ abstract class TokenDeployer< break; } if (isStarknet) { + // TODO: check if taking ABI from starknet-core might break this const erc20Abi = [ { name: 'name', diff --git a/typescript/sdk/src/utils/starknet.ts b/typescript/sdk/src/utils/starknet.ts new file mode 100644 index 00000000000..e4cbbdddc62 --- /dev/null +++ b/typescript/sdk/src/utils/starknet.ts @@ -0,0 +1,224 @@ +import { utils } from 'ethers'; +import { + AccountInterface, + CairoOption, + CairoOptionVariant, + Contract, + ParsedEvent, + ParsedEvents, + ParsedStruct, + ProviderInterface, +} from 'starknet'; + +import { + ContractType, + getCompiledContract, +} from '@hyperlane-xyz/starknet-core'; + +import { DispatchedMessage } from '../core/types.js'; +import { StarknetIsmContractName } from '../ism/starknet-utils.js'; +import { SupportedIsmTypesOnStarknetType } from '../ism/types.js'; + +export enum StarknetContractName { + MAILBOX = 'mailbox', + HYP_ERC20 = 'HypErc20', + HYP_ERC20_COLLATERAL = 'HypErc20Collateral', + HYP_NATIVE = 'HypNative', + ETHER = 'Ether', + MERKLE_TREE_HOOK = 'merkle_tree_hook', + NOOP_ISM = 'noop_ism', + HOOK = 'hook', + PROTOCOL_FEE = 'protocol_fee', + VALIDATOR_ANNOUNCE = 'validator_announce', + MESSAGE_RECIPIENT = 'message_recipient', + DOMAIN_ROUTING_HOOK = 'domain_routing_hook', + FALLBACK_DOMAIN_ROUTING_HOOK = 'fallback_domain_routing_hook', + STATIC_AGGREGATION_HOOK = 'static_aggregation_hook', +} + +export enum StarknetHookType { + AGGREGATION = 'AGGREGATION', + FALLBACK_ROUTING = 'FALLBACK_ROUTING', + MAILBOX_DEFAULT_HOOK = 'MAILBOX_DEFAULT_HOOK', + MERKLE_TREE = 'MERKLE_TREE', + PROTOCOL_FEE = 'PROTOCOL_FEE', + ROUTING = 'ROUTING', + UNUSED = 'UNUSED', +} + +export enum StarknetIsmType { + AGGREGATION = 'AGGREGATION', + CCIP_READ = 'CCIP_READ', // Not supported + LEGACY_MULTISIG = 'LEGACY_MULTISIG', // Deprecated + MERKLE_ROOT_MULTISIG = 'MERKLE_ROOT_MULTISIG', + MESSAGE_ID_MULTISIG = 'MESSAGE_ID_MULTISIG', + NULL = 'NULL', + ROUTING = 'ROUTING', + UNUSED = 'UNUSED', +} + +export interface Message { + version: number; + nonce: number; + origin: number; + sender: bigint; + destination: number; + recipient: bigint; + body: { size: bigint; data: bigint[] }; +} + +export interface ByteData { + value: bigint; + size: number; +} + +/** + * Creates a Starknet contract instance with the given parameters + */ +export function getStarknetContract( + contractName: string, + address: string, + providerOrAccount?: ProviderInterface | AccountInterface, + contractType: ContractType = ContractType.CONTRACT, +): Contract { + const { abi } = getCompiledContract(contractName, contractType); + return new Contract(abi, address, providerOrAccount); +} + +export function getStarknetMailboxContract( + address: string, + providerOrAccount?: ProviderInterface | AccountInterface, +): Contract { + return getStarknetContract( + StarknetContractName.MAILBOX, + address, + providerOrAccount, + ); +} + +export function getStarknetHypERC20Contract( + address: string, + providerOrAccount?: ProviderInterface | AccountInterface, +): Contract { + return getStarknetContract( + StarknetContractName.HYP_ERC20, + address, + providerOrAccount, + ContractType.TOKEN, + ); +} + +export function getStarknetHypERC20CollateralContract( + address: string, + providerOrAccount?: ProviderInterface | AccountInterface, +): Contract { + return getStarknetContract( + StarknetContractName.HYP_ERC20_COLLATERAL, + address, + providerOrAccount, + ContractType.TOKEN, + ); +} + +export function getStarknetHypNativeContract( + address: string, + providerOrAccount?: ProviderInterface | AccountInterface, +): Contract { + return getStarknetContract( + StarknetContractName.HYP_NATIVE, + address, + providerOrAccount, + ContractType.TOKEN, + ); +} + +export function getStarknetEtherContract( + address: string, + providerOrAccount?: ProviderInterface | AccountInterface, +): Contract { + return getStarknetContract( + StarknetContractName.ETHER, + address, + providerOrAccount, + ContractType.TOKEN, + ); +} + +export function getStarknetIsmContract( + starkIsmType: SupportedIsmTypesOnStarknetType, + address: string, + providerOrAccount?: ProviderInterface | AccountInterface, +): Contract { + return getStarknetContract( + StarknetIsmContractName[starkIsmType], + address, + providerOrAccount, + ); +} + +export async function quoteStarknetDispatch({ + mailboxContract, + destinationDomain, + recipientAddress, + messageBody, + customHookMetadata, + customHook, +}: { + mailboxContract: Contract; + destinationDomain: number; + recipientAddress: string; + messageBody: { + size: number; + data: bigint[]; + }; + customHookMetadata?: string; + customHook?: string; +}): Promise { + const nonOption = new CairoOption(CairoOptionVariant.None); + + const quote = await mailboxContract.call('quote_dispatch', [ + destinationDomain, + recipientAddress, + messageBody, + customHookMetadata || nonOption, + customHook || nonOption, + ]); + + return quote.toString(); +} + +const DISPATCH_EVENT = 'contracts::mailbox::mailbox::Dispatch'; +const DISPATCH_ID_EVENT = 'contracts::mailbox::mailbox::DispatchId'; + +export function parseStarknetDispatchEvents( + parsedEvents: ParsedEvents, + chainNameResolver: (domain: number) => string | undefined, +): DispatchedMessage[] { + return parsedEvents + .filter((event: ParsedEvent) => DISPATCH_EVENT in event) + .map((dispatchEvent: ParsedEvent) => { + const message = dispatchEvent[DISPATCH_EVENT].message as ParsedStruct; + const originChain = chainNameResolver(Number(message.origin)); + const destinationChain = chainNameResolver(Number(message.destination)); + + return { + parsed: { + ...message, + originChain, + destinationChain, + }, + id: parseStarknetDispatchIdEvents(parsedEvents)[0], + message: message.raw, + } as DispatchedMessage; + }); +} + +export function parseStarknetDispatchIdEvents( + parsedEvents: ParsedEvents, +): string[] { + return parsedEvents + .filter((event: ParsedEvent) => DISPATCH_ID_EVENT in event) + .map((dispatchEvent: ParsedEvent) => + utils.hexlify(dispatchEvent[DISPATCH_ID_EVENT].id as bigint), + ); +} diff --git a/typescript/sdk/src/warp/WarpCore.test.ts b/typescript/sdk/src/warp/WarpCore.test.ts index 33b947f9623..b172eb52c27 100644 --- a/typescript/sdk/src/warp/WarpCore.test.ts +++ b/typescript/sdk/src/warp/WarpCore.test.ts @@ -43,6 +43,7 @@ describe('WarpCore', () => { let cwHypCollateral: Token; let cw20: Token; let cosmosIbc: Token; + let starknetHypSynthetic: Token; // Stub MultiProvider fee estimation to avoid real network calls sinon @@ -72,6 +73,8 @@ describe('WarpCore', () => { cwHypCollateral, cw20, cosmosIbc, + , + starknetHypSynthetic, ] = warpCore.tokens; }); @@ -154,7 +157,11 @@ describe('WarpCore', () => { amount: 1n, addressOrDenom: 'atom', }); - + await testQuote( + starknetHypSynthetic, + test1.name, + TokenStandard.StarknetHypNative, + ); stubs.forEach((s) => s.restore()); }); diff --git a/typescript/sdk/src/warp/WarpCore.ts b/typescript/sdk/src/warp/WarpCore.ts index a3d06f84e3c..a05acb00a00 100644 --- a/typescript/sdk/src/warp/WarpCore.ts +++ b/typescript/sdk/src/warp/WarpCore.ts @@ -3,7 +3,6 @@ import { Logger } from 'pino'; import { Address, HexString, - Numberish, ProtocolType, assert, convertDecimalsToIntegerString, @@ -187,12 +186,14 @@ export class WarpCore { originToken, destination, sender, + recipient, senderPubKey, interchainFee, }: { originToken: IToken; destination: ChainNameOrId; sender: Address; + recipient?: Address; senderPubKey?: HexString; interchainFee?: TokenAmount; }): Promise { @@ -213,9 +214,10 @@ export class WarpCore { return { gasUnits: 0, gasPrice: 0, fee: Number(defaultQuote.amount) }; } + // TODO: DOES NOT WORK FOR STARKNET // Form transactions to estimate local gas with - const recipient = convertToProtocolAddress( - sender, + const recipientAddress = convertToProtocolAddress( + recipient ?? sender, // TODO: get recipient instead of sender destinationMetadata.protocol, destinationMetadata.bech32Prefix, ); @@ -223,7 +225,7 @@ export class WarpCore { originTokenAmount: originToken.amount(1), destination, sender, - recipient, + recipient: recipientAddress, interchainFee, }); @@ -260,6 +262,12 @@ export class WarpCore { provider, gasUnits: EVM_TRANSFER_REMOTE_GAS_ESTIMATE, }); + } else if ( + txs.length === 2 && + originToken.protocol === ProtocolType.Starknet + ) { + this.logger.info(`Skipping gas estimation for Starknet`); + return { gasUnits: 0, gasPrice: 0, fee: 0 }; } else { throw new Error('Cannot estimate local gas for multiple transactions'); } @@ -274,12 +282,14 @@ export class WarpCore { originToken, destination, sender, + recipient, senderPubKey, interchainFee, }: { originToken: IToken; destination: ChainNameOrId; sender: Address; + recipient?: Address; senderPubKey?: HexString; interchainFee?: TokenAmount; }): Promise { @@ -298,6 +308,7 @@ export class WarpCore { originToken, destination, sender, + recipient, senderPubKey, interchainFee, }); @@ -333,42 +344,16 @@ export class WarpCore { const providerType = TOKEN_STANDARD_TO_PROVIDER_TYPE[token.standard]; const hypAdapter = token.getHypAdapter(this.multiProvider, destinationName); - const [isApproveRequired, isRevokeApprovalRequired] = await Promise.all([ - this.isApproveRequired({ - originTokenAmount, - owner: sender, - }), - hypAdapter.isRevokeApprovalRequired( - sender, - originTokenAmount.token.addressOrDenom, - ), - ]); - - const preTransferRemoteTxs: [Numberish, WarpTxCategory][] = []; - // if the approval is required and the current allowance is not 0 we reset - // the allowance before setting the right approval as some tokens don't allow - // to override an already existing allowance. USDT is one of these tokens - // see: https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L205 - if (isApproveRequired && isRevokeApprovalRequired) { - preTransferRemoteTxs.push([0, WarpTxCategory.Revoke]); - } - - if (isApproveRequired) { - preTransferRemoteTxs.push([amount.toString(), WarpTxCategory.Approval]); - } - - for (const [approveAmount, txCategory] of preTransferRemoteTxs) { - this.logger.info( - `${txCategory} required for transfer of ${token.symbol}`, - ); + if (await this.isApproveRequired({ originTokenAmount, owner: sender })) { + this.logger.info(`Approval required for transfer of ${token.symbol}`); const approveTxReq = await hypAdapter.populateApproveTx({ - weiAmountOrId: approveAmount, + weiAmountOrId: amount.toString(), recipient: token.addressOrDenom, }); - this.logger.debug(`${txCategory} tx for ${token.symbol} populated`); + this.logger.debug(`Approval tx for ${token.symbol} populated`); const approveTx = { - category: txCategory, + category: WarpTxCategory.Approval, type: providerType, transaction: approveTxReq, } as WarpTypedTransaction; @@ -623,6 +608,7 @@ export class WarpCore { originTokenAmount, destination, sender, + recipient, senderPubKey, ); if (balancesError) return balancesError; @@ -738,6 +724,7 @@ export class WarpCore { originTokenAmount: TokenAmount, destination: ChainNameOrId, sender: Address, + recipient: Address, senderPubKey?: HexString, ): Promise | null> { const { token: originToken, amount } = originTokenAmount; @@ -775,6 +762,7 @@ export class WarpCore { originToken, destination, sender, + recipient, senderPubKey, interchainFee: interchainQuote, }); diff --git a/typescript/sdk/src/warp/test-warp-core-config.yaml b/typescript/sdk/src/warp/test-warp-core-config.yaml index 3b1944f5e6d..4c551ebbd79 100644 --- a/typescript/sdk/src/warp/test-warp-core-config.yaml +++ b/typescript/sdk/src/warp/test-warp-core-config.yaml @@ -124,6 +124,14 @@ tokens: symbol: atom name: atom addressOrDenom: atom + - chainName: starknetdevnet + standard: StarknetHypSynthetic + decimals: 18 + symbol: ETH + name: Ether on starknet + addressOrDenom: '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + connections: + - { token: ethereum|test1|0x1234567890123456789012345678901234567890 } options: interchainFeeConstants: