From 5f41b113467ff458d3103bc4c5885441ede744ff Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 7 Nov 2024 16:04:25 -0500 Subject: [PATCH 1/6] fix: Remove coingecko-api-v3 library and de-dupe fetching utils (#4837) ### Description - Remove coingecko-api-v3 lib which is unnecessary and breaks bundling - Remove `getCoingeckoTokenPrices` function which is redundant with `CoinGeckoTokenPriceGetter` - Simplify `CoinGeckoTokenPriceGetter` and remove need for Mock class Related: https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/4787 See discussion here: https://discord.com/channels/935678348330434570/1304125818817220653 ### Drive-by changes - Fix bundling issue with Storybook in widgets lib ### Backward compatibility No ### Testing Rewrote unit tests and tested Storybook --- .changeset/unlucky-pillows-clap.md | 5 + typescript/cli/src/config/hooks.ts | 16 +++- .../monitor-warp-routes-balances.ts | 8 +- typescript/sdk/package.json | 1 - typescript/sdk/src/gas/token-prices.test.ts | 94 +++++++++++++------ typescript/sdk/src/gas/token-prices.ts | 85 +++++++++-------- typescript/sdk/src/gas/utils.ts | 35 ------- typescript/sdk/src/index.ts | 1 - typescript/sdk/src/test/MockCoinGecko.ts | 47 ---------- typescript/widgets/.storybook/main.ts | 10 ++ yarn.lock | 17 ---- 11 files changed, 141 insertions(+), 178 deletions(-) create mode 100644 .changeset/unlucky-pillows-clap.md delete mode 100644 typescript/sdk/src/test/MockCoinGecko.ts diff --git a/.changeset/unlucky-pillows-clap.md b/.changeset/unlucky-pillows-clap.md new file mode 100644 index 000000000..ddefb4f2e --- /dev/null +++ b/.changeset/unlucky-pillows-clap.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': major +--- + +Remove getCoingeckoTokenPrices (use CoinGeckoTokenPriceGetter instead) diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index 0bfd8cb1f..e8df64dc0 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -8,12 +8,12 @@ import { ChainMap, ChainMetadata, ChainName, + CoinGeckoTokenPriceGetter, HookConfig, HookConfigSchema, HookType, IgpHookConfig, MultiProtocolProvider, - getCoingeckoTokenPrices, getGasPrice, getLocalStorageGasOracleConfig, } from '@hyperlane-xyz/sdk'; @@ -305,9 +305,17 @@ async function getIgpTokenPrices( ) { const isTestnet = context.chainMetadata[Object.keys(filteredMetadata)[0]].isTestnet; - const fetchedPrices = isTestnet - ? objMap(filteredMetadata, () => '10') - : await getCoingeckoTokenPrices(filteredMetadata); + + let fetchedPrices: ChainMap; + if (isTestnet) { + fetchedPrices = objMap(filteredMetadata, () => '10'); + } else { + const tokenPriceGetter = new CoinGeckoTokenPriceGetter({ + chainMetadata: filteredMetadata, + }); + const results = await tokenPriceGetter.getAllTokenPrices(); + fetchedPrices = objMap(results, (v) => v.toString()); + } logBlue( isTestnet diff --git a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts index fd4da97f5..06b4fb78f 100644 --- a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts @@ -3,12 +3,12 @@ import { ethers } from 'ethers'; import { Gauge, Registry } from 'prom-client'; import { + ERC20__factory, HypXERC20Lockbox__factory, HypXERC20__factory, IXERC20, IXERC20__factory, } from '@hyperlane-xyz/core'; -import { ERC20__factory } from '@hyperlane-xyz/core'; import { createWarpRouteConfigId } from '@hyperlane-xyz/registry'; import { ChainMap, @@ -638,10 +638,10 @@ async function checkWarpRouteMetrics( tokenConfig: WarpRouteConfig, chainMetadata: ChainMap, ) { - const tokenPriceGetter = CoinGeckoTokenPriceGetter.withDefaultCoinGecko( + const tokenPriceGetter = new CoinGeckoTokenPriceGetter({ chainMetadata, - await getCoinGeckoApiKey(), - ); + apiKey: await getCoinGeckoApiKey(), + }); setInterval(async () => { try { diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index dbb340026..2eec9a68f 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -15,7 +15,6 @@ "@solana/spl-token": "^0.4.9", "@solana/web3.js": "^1.95.4", "bignumber.js": "^9.1.1", - "coingecko-api-v3": "^0.0.29", "cosmjs-types": "^0.9.0", "cross-fetch": "^3.1.5", "ethers": "^5.7.2", diff --git a/typescript/sdk/src/gas/token-prices.test.ts b/typescript/sdk/src/gas/token-prices.test.ts index 48deda8bc..dbc1dc76b 100644 --- a/typescript/sdk/src/gas/token-prices.test.ts +++ b/typescript/sdk/src/gas/token-prices.test.ts @@ -1,51 +1,83 @@ import { expect } from 'chai'; +import sinon from 'sinon'; + +import { ethereum, solanamainnet } from '@hyperlane-xyz/registry'; import { TestChainName, testChainMetadata } from '../consts/testChains.js'; -import { MockCoinGecko } from '../test/MockCoinGecko.js'; import { CoinGeckoTokenPriceGetter } from './token-prices.js'; +const MOCK_FETCH_CALLS = true; + describe('TokenPriceGetter', () => { let tokenPriceGetter: CoinGeckoTokenPriceGetter; - let mockCoinGecko: MockCoinGecko; - const chainA = TestChainName.test1, - chainB = TestChainName.test2, - priceA = 1, - priceB = 5.5; - before(async () => { - mockCoinGecko = new MockCoinGecko(); - // Origin token - mockCoinGecko.setTokenPrice(chainA, priceA); - // Destination token - mockCoinGecko.setTokenPrice(chainB, priceB); - tokenPriceGetter = new CoinGeckoTokenPriceGetter( - mockCoinGecko, - testChainMetadata, - undefined, - 0, - ); + + const chainA = TestChainName.test1; + const chainB = TestChainName.test2; + const priceA = 2; + const priceB = 5; + let stub: sinon.SinonStub; + + beforeEach(() => { + tokenPriceGetter = new CoinGeckoTokenPriceGetter({ + chainMetadata: { ethereum, solanamainnet, ...testChainMetadata }, + apiKey: 'test', + expirySeconds: 10, + sleepMsBetweenRequests: 10, + }); + + if (MOCK_FETCH_CALLS) { + stub = sinon + .stub(tokenPriceGetter, 'fetchPriceData') + .returns(Promise.resolve([priceA, priceB])); + } }); - describe('getTokenPrice', () => { - it('returns a token price', async () => { - expect(await tokenPriceGetter.getTokenPrice(chainA)).to.equal(priceA); + afterEach(() => { + if (MOCK_FETCH_CALLS && stub) { + stub.restore(); + } + }); + + describe('getTokenPriceByIds', () => { + it('returns token prices', async () => { + // stubbed results + expect( + await tokenPriceGetter.getTokenPriceByIds([ + ethereum.name, + solanamainnet.name, + ]), + ).to.eql([priceA, priceB]); }); + }); - it('caches a token price', async () => { - mockCoinGecko.setFail(chainA, true); - expect(await tokenPriceGetter.getTokenPrice(chainA)).to.equal(priceA); - mockCoinGecko.setFail(chainA, false); + describe('getTokenPrice', () => { + it('returns a token price', async () => { + // hardcoded result of 1 for testnets + expect( + await tokenPriceGetter.getTokenPrice(TestChainName.test1), + ).to.equal(1); + // stubbed result for non-testnet + expect(await tokenPriceGetter.getTokenPrice(ethereum.name)).to.equal( + priceA, + ); }); }); describe('getTokenExchangeRate', () => { it('returns a value consistent with getTokenPrice()', async () => { - const exchangeRate = await tokenPriceGetter.getTokenExchangeRate( - chainA, - chainB, - ); - // Should equal 1 because testnet prices are always forced to 1 - expect(exchangeRate).to.equal(1); + // hardcoded result of 1 for testnets + expect( + await tokenPriceGetter.getTokenExchangeRate(chainA, chainB), + ).to.equal(1); + + // stubbed result for non-testnet + expect( + await tokenPriceGetter.getTokenExchangeRate( + ethereum.name, + solanamainnet.name, + ), + ).to.equal(priceA / priceB); }); }); }); diff --git a/typescript/sdk/src/gas/token-prices.ts b/typescript/sdk/src/gas/token-prices.ts index 80b1ae288..62a477422 100644 --- a/typescript/sdk/src/gas/token-prices.ts +++ b/typescript/sdk/src/gas/token-prices.ts @@ -1,21 +1,15 @@ -import { CoinGeckoClient, SimplePriceResponse } from 'coingecko-api-v3'; - -import { rootLogger, sleep } from '@hyperlane-xyz/utils'; +import { objKeys, rootLogger, sleep } from '@hyperlane-xyz/utils'; import { ChainMetadata } from '../metadata/chainMetadataTypes.js'; import { ChainMap, ChainName } from '../types.js'; +const COINGECKO_PRICE_API = 'https://api.coingecko.com/api/v3/simple/price'; + export interface TokenPriceGetter { getTokenPrice(chain: ChainName): Promise; getTokenExchangeRate(base: ChainName, quote: ChainName): Promise; } -export type CoinGeckoInterface = Pick; -export type CoinGeckoSimplePriceInterface = CoinGeckoClient['simplePrice']; -export type CoinGeckoSimplePriceParams = - Parameters[0]; -export type CoinGeckoResponse = ReturnType; - type TokenPriceCacheEntry = { price: number; timestamp: Date; @@ -65,38 +59,28 @@ class TokenPriceCache { } export class CoinGeckoTokenPriceGetter implements TokenPriceGetter { - protected coinGecko: CoinGeckoInterface; protected cache: TokenPriceCache; + protected apiKey?: string; protected sleepMsBetweenRequests: number; protected metadata: ChainMap; - constructor( - coinGecko: CoinGeckoInterface, - chainMetadata: ChainMap, - expirySeconds?: number, + constructor({ + chainMetadata, + apiKey, + expirySeconds, sleepMsBetweenRequests = 5000, - ) { - this.coinGecko = coinGecko; + }: { + chainMetadata: ChainMap; + apiKey?: string; + expirySeconds?: number; + sleepMsBetweenRequests?: number; + }) { + this.apiKey = apiKey; this.cache = new TokenPriceCache(expirySeconds); this.metadata = chainMetadata; this.sleepMsBetweenRequests = sleepMsBetweenRequests; } - static withDefaultCoinGecko( - chainMetadata: ChainMap, - apiKey?: string, - expirySeconds?: number, - sleepMsBetweenRequests = 5000, - ): CoinGeckoTokenPriceGetter { - const coinGecko = new CoinGeckoClient(undefined, apiKey); - return new CoinGeckoTokenPriceGetter( - coinGecko, - chainMetadata, - expirySeconds, - sleepMsBetweenRequests, - ); - } - async getTokenPrice( chain: ChainName, currency: string = 'usd', @@ -105,6 +89,15 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter { return price; } + async getAllTokenPrices(currency: string = 'usd'): Promise> { + const chains = objKeys(this.metadata); + const prices = await this.getTokenPrices(chains, currency); + return chains.reduce( + (agg, chain, i) => ({ ...agg, [chain]: prices[i] }), + {}, + ); + } + async getTokenExchangeRate( base: ChainName, quote: ChainName, @@ -153,14 +146,9 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter { await sleep(this.sleepMsBetweenRequests); if (toQuery.length > 0) { - let response: SimplePriceResponse; try { - response = await this.coinGecko.simplePrice({ - ids: toQuery.join(','), - vs_currencies: currency, - }); - const prices = toQuery.map((id) => response[id][currency]); - toQuery.map((id, i) => this.cache.put(id, prices[i])); + const prices = await this.fetchPriceData(toQuery, currency); + prices.forEach((price, i) => this.cache.put(toQuery[i], price)); } catch (e) { rootLogger.warn('Error when querying token prices', e); return undefined; @@ -168,4 +156,25 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter { } return ids.map((id) => this.cache.fetch(id)); } + + public async fetchPriceData( + ids: string[], + currency: string, + ): Promise { + let url = `${COINGECKO_PRICE_API}?ids=${Object.entries(ids).join( + ',', + )}&vs_currencies=${currency}`; + if (this.apiKey) { + url += `&x-cg-pro-api-key=${this.apiKey}`; + } + + const resp = await fetch(url); + const idPrices = await resp.json(); + + return ids.map((id) => { + const price = idPrices[id]?.[currency]; + if (!price) throw new Error(`No price found for ${id}`); + return Number(price); + }); + } } diff --git a/typescript/sdk/src/gas/utils.ts b/typescript/sdk/src/gas/utils.ts index 3a658d9b6..6f007398c 100644 --- a/typescript/sdk/src/gas/utils.ts +++ b/typescript/sdk/src/gas/utils.ts @@ -9,7 +9,6 @@ import { } from '../consts/igp.js'; import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js'; import { AgentCosmosGasPrice } from '../metadata/agentConfig.js'; -import { ChainMetadata } from '../metadata/chainMetadataTypes.js'; import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js'; import { ChainMap, ChainName } from '../types.js'; import { getCosmosRegistryChain } from '../utils/cosmos.js'; @@ -215,37 +214,3 @@ export function getLocalStorageGasOracleConfig({ }; }, {} as ChainMap); } - -const COINGECKO_PRICE_API = 'https://api.coingecko.com/api/v3/simple/price'; - -export async function getCoingeckoTokenPrices( - chainMetadata: ChainMap, - currency = 'usd', -): Promise> { - const ids = objMap( - chainMetadata, - (_, metadata) => metadata.gasCurrencyCoinGeckoId ?? metadata.name, - ); - - const resp = await fetch( - `${COINGECKO_PRICE_API}?ids=${Object.entries(ids).join( - ',', - )}&vs_currencies=${currency}`, - ); - - const idPrices = await resp.json(); - - const prices = objMap(ids, (chain, id) => { - const idData = idPrices[id]; - if (!idData) { - return undefined; - } - const price = idData[currency]; - if (!price) { - return undefined; - } - return price.toString(); - }); - - return prices; -} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index d01a9c68c..6fb5d57a8 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -554,7 +554,6 @@ export { ChainGasOracleParams, GasPriceConfig, NativeTokenPriceConfig, - getCoingeckoTokenPrices, getCosmosChainGasPrice, getGasPrice, getLocalStorageGasOracleConfig, diff --git a/typescript/sdk/src/test/MockCoinGecko.ts b/typescript/sdk/src/test/MockCoinGecko.ts deleted file mode 100644 index 8b410125a..000000000 --- a/typescript/sdk/src/test/MockCoinGecko.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { SimplePriceResponse } from 'coingecko-api-v3'; - -import type { - CoinGeckoInterface, - CoinGeckoResponse, - CoinGeckoSimplePriceInterface, - CoinGeckoSimplePriceParams, -} from '../gas/token-prices.js'; -import type { ChainName } from '../types.js'; - -// A mock CoinGecko intended to be used by tests -export class MockCoinGecko implements CoinGeckoInterface { - // Prices keyed by coingecko id - private tokenPrices: Record; - // Whether or not to fail to return a response, keyed by coingecko id - private fail: Record; - - constructor() { - this.tokenPrices = {}; - this.fail = {}; - } - - price(input: CoinGeckoSimplePriceParams): CoinGeckoResponse { - const data: SimplePriceResponse = {}; - for (const id of input.ids) { - if (this.fail[id]) { - return Promise.reject(`Failed to fetch price for ${id}`); - } - data[id] = { - usd: this.tokenPrices[id], - }; - } - return Promise.resolve(data); - } - - get simplePrice(): CoinGeckoSimplePriceInterface { - return this.price; - } - - setTokenPrice(chain: ChainName, price: number): void { - this.tokenPrices[chain] = price; - } - - setFail(chain: ChainName, fail: boolean): void { - this.fail[chain] = fail; - } -} diff --git a/typescript/widgets/.storybook/main.ts b/typescript/widgets/.storybook/main.ts index 2afe9d398..d4aa7c994 100644 --- a/typescript/widgets/.storybook/main.ts +++ b/typescript/widgets/.storybook/main.ts @@ -1,3 +1,4 @@ +import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'; import type { StorybookConfig } from '@storybook/react-vite'; import { mergeConfig } from 'vite'; @@ -19,6 +20,15 @@ const config: StorybookConfig = { async viteFinal(config, { configType }) { return mergeConfig(config, { define: { 'process.env': {} }, + optimizeDeps: { + esbuildOptions: { + plugins: [ + NodeGlobalsPolyfillPlugin({ + buffer: true, + }), + ], + }, + }, }); }, }; diff --git a/yarn.lock b/yarn.lock index 44046d57d..84393f395 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8073,7 +8073,6 @@ __metadata: "@types/ws": "npm:^8.5.5" bignumber.js: "npm:^9.1.1" chai: "npm:4.5.0" - coingecko-api-v3: "npm:^0.0.29" cosmjs-types: "npm:^0.9.0" cross-fetch: "npm:^3.1.5" dotenv: "npm:^10.0.0" @@ -16860,15 +16859,6 @@ __metadata: languageName: node linkType: hard -"coingecko-api-v3@npm:^0.0.29": - version: 0.0.29 - resolution: "coingecko-api-v3@npm:0.0.29" - dependencies: - https: "npm:^1.0.0" - checksum: 10/e60a0996472419232a144ec77028c060bd9c289f799dd40d46dbb7229cff3d868a3e35bf88724059dc25767b8136d794789e4dd31711592fa73a7be1ca2fcbc7 - languageName: node - linkType: hard - "collect-v8-coverage@npm:^1.0.0": version: 1.0.2 resolution: "collect-v8-coverage@npm:1.0.2" @@ -21773,13 +21763,6 @@ __metadata: languageName: node linkType: hard -"https@npm:^1.0.0": - version: 1.0.0 - resolution: "https@npm:1.0.0" - checksum: 10/ccea8a8363a018d4b241db7748cff3a85c9f5b71bf80639e9c37dc6823f590f35dda098b80b726930e9f945387c8bfd6b1461df25cab5bf65a31903d81875b5d - languageName: node - linkType: hard - "human-id@npm:^1.0.2": version: 1.0.2 resolution: "human-id@npm:1.0.2" From bf4c77996eabcd82873b075b70c2e7b9c5bdfc49 Mon Sep 17 00:00:00 2001 From: Jason Guo <33064781+Xaroz@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:05:26 -0400 Subject: [PATCH 2/6] chore(widgets): add react and react hooks eslint config (#4838) ### Description Add react and react hooks eslint rules for better linting in the widgets project ### Drive-by changes No ### Backward compatibility Yes ### Testing Manual testing --- typescript/widgets/.eslintrc | 14 +- typescript/widgets/package.json | 2 + yarn.lock | 709 +++++++++++++++++++++++++++++++- 3 files changed, 714 insertions(+), 11 deletions(-) diff --git a/typescript/widgets/.eslintrc b/typescript/widgets/.eslintrc index 42933a0cc..8bc48f0d9 100644 --- a/typescript/widgets/.eslintrc +++ b/typescript/widgets/.eslintrc @@ -1,6 +1,18 @@ { + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "prettier" + ], + "plugins": ["react", "react-hooks", "@typescript-eslint"], "rules": { // TODO use utils rootLogger in widgets lib - "no-console": ["off"] + "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/package.json b/typescript/widgets/package.json index 9a08861a6..b22494fd2 100644 --- a/typescript/widgets/package.json +++ b/typescript/widgets/package.json @@ -32,6 +32,8 @@ "babel-loader": "^8.3.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-storybook": "^0.6.15", "postcss": "^8.4.21", "prettier": "^2.8.8", diff --git a/yarn.lock b/yarn.lock index 84393f395..0a8c3912f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8145,6 +8145,8 @@ __metadata: clsx: "npm:^2.1.1" eslint: "npm:^8.57.0" eslint-config-prettier: "npm:^9.1.0" + eslint-plugin-react: "npm:^7.37.2" + eslint-plugin-react-hooks: "npm:^5.0.0" eslint-plugin-storybook: "npm:^0.6.15" postcss: "npm:^8.4.21" prettier: "npm:^2.8.8" @@ -15081,6 +15083,16 @@ __metadata: languageName: node linkType: hard +"array-buffer-byte-length@npm:^1.0.1": + version: 1.0.1 + resolution: "array-buffer-byte-length@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.5" + is-array-buffer: "npm:^3.0.4" + checksum: 10/53524e08f40867f6a9f35318fafe467c32e45e9c682ba67b11943e167344d2febc0f6977a17e699b05699e805c3e8f073d876f8bbf1b559ed494ad2cd0fae09e + languageName: node + linkType: hard + "array-flatten@npm:1.1.1": version: 1.1.1 resolution: "array-flatten@npm:1.1.1" @@ -15088,6 +15100,20 @@ __metadata: languageName: node linkType: hard +"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": + version: 3.1.8 + resolution: "array-includes@npm:3.1.8" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + es-object-atoms: "npm:^1.0.0" + get-intrinsic: "npm:^1.2.4" + is-string: "npm:^1.0.7" + checksum: 10/290b206c9451f181fb2b1f79a3bf1c0b66bb259791290ffbada760c79b284eef6f5ae2aeb4bcff450ebc9690edd25732c4c73a3c2b340fcc0f4563aed83bf488 + languageName: node + linkType: hard + "array-union@npm:^2.1.0": version: 2.1.0 resolution: "array-union@npm:2.1.0" @@ -15115,7 +15141,21 @@ __metadata: languageName: node linkType: hard -"array.prototype.flat@npm:^1.2.3": +"array.prototype.findlast@npm:^1.2.5": + version: 1.2.5 + resolution: "array.prototype.findlast@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/7dffcc665aa965718ad6de7e17ac50df0c5e38798c0a5bf9340cf24feb8594df6ec6f3fcbe714c1577728a1b18b5704b15669474b27bceeca91ef06ce2a23c31 + languageName: node + linkType: hard + +"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.3.1": version: 1.3.2 resolution: "array.prototype.flat@npm:1.3.2" dependencies: @@ -15127,6 +15167,18 @@ __metadata: languageName: node linkType: hard +"array.prototype.flatmap@npm:^1.3.2": + version: 1.3.2 + resolution: "array.prototype.flatmap@npm:1.3.2" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + es-shim-unscopables: "npm:^1.0.0" + checksum: 10/33f20006686e0cbe844fde7fd290971e8366c6c5e3380681c2df15738b1df766dd02c7784034aeeb3b037f65c496ee54de665388288edb323a2008bb550f77ea + languageName: node + linkType: hard + "array.prototype.reduce@npm:^1.0.4": version: 1.0.4 resolution: "array.prototype.reduce@npm:1.0.4" @@ -15140,6 +15192,19 @@ __metadata: languageName: node linkType: hard +"array.prototype.tosorted@npm:^1.1.4": + version: 1.1.4 + resolution: "array.prototype.tosorted@npm:1.1.4" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.3" + es-errors: "npm:^1.3.0" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10/874694e5d50e138894ff5b853e639c29b0aa42bbd355acda8e8e9cd337f1c80565f21edc15e8c727fa4c0877fd9d8783c575809e440cc4d2d19acaa048bf967d + languageName: node + linkType: hard + "arraybuffer.prototype.slice@npm:^1.0.2": version: 1.0.2 resolution: "arraybuffer.prototype.slice@npm:1.0.2" @@ -15155,6 +15220,22 @@ __metadata: languageName: node linkType: hard +"arraybuffer.prototype.slice@npm:^1.0.3": + version: 1.0.3 + resolution: "arraybuffer.prototype.slice@npm:1.0.3" + dependencies: + array-buffer-byte-length: "npm:^1.0.1" + call-bind: "npm:^1.0.5" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.22.3" + es-errors: "npm:^1.2.1" + get-intrinsic: "npm:^1.2.3" + is-array-buffer: "npm:^3.0.4" + is-shared-array-buffer: "npm:^1.0.2" + checksum: 10/0221f16c1e3ec7b67da870ee0e1f12b825b5f9189835392b59a22990f715827561a4f4cd5330dc7507de272d8df821be6cd4b0cb569babf5ea4be70e365a2f3d + languageName: node + linkType: hard + "arrify@npm:^1.0.1": version: 1.0.1 resolution: "arrify@npm:1.0.1" @@ -15331,6 +15412,15 @@ __metadata: languageName: node linkType: hard +"available-typed-arrays@npm:^1.0.7": + version: 1.0.7 + resolution: "available-typed-arrays@npm:1.0.7" + dependencies: + possible-typed-array-names: "npm:^1.0.0" + checksum: 10/6c9da3a66caddd83c875010a1ca8ef11eac02ba15fb592dc9418b2b5e7b77b645fa7729380a92d9835c2f05f2ca1b6251f39b993e0feb3f1517c74fa1af02cab + languageName: node + linkType: hard + "aws-kms-ethers-signer@npm:^0.1.3": version: 0.1.3 resolution: "aws-kms-ethers-signer@npm:0.1.3" @@ -16243,7 +16333,7 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.7": +"call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" dependencies: @@ -17445,6 +17535,39 @@ __metadata: languageName: node linkType: hard +"data-view-buffer@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-buffer@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.6" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 10/5919a39a18ee919573336158fd162fdf8ada1bc23a139f28543fd45fac48e0ea4a3ad3bfde91de124d4106e65c4a7525f6a84c20ba0797ec890a77a96d13a82a + languageName: node + linkType: hard + +"data-view-byte-length@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-byte-length@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 10/f33c65e58d8d0432ad79761f2e8a579818d724b5dc6dc4e700489b762d963ab30873c0f1c37d8f2ed12ef51c706d1195f64422856d25f067457aeec50cc40aac + languageName: node + linkType: hard + +"data-view-byte-offset@npm:^1.0.0": + version: 1.0.0 + resolution: "data-view-byte-offset@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.6" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 10/96f34f151bf02affb7b9f98762fb7aca1dd5f4553cb57b80bce750ca609c15d33ca659568ef1d422f7e35680736cbccb893a3d4b012760c758c1446bbdc4c6db + languageName: node + linkType: hard + "date-fns@npm:^3.6.0": version: 3.6.0 resolution: "date-fns@npm:3.6.0" @@ -17924,6 +18047,15 @@ __metadata: languageName: node linkType: hard +"doctrine@npm:^2.1.0": + version: 2.1.0 + resolution: "doctrine@npm:2.1.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10/555684f77e791b17173ea86e2eea45ef26c22219cb64670669c4f4bebd26dbc95cd90ec1f4159e9349a6bb9eb892ce4dde8cd0139e77bedd8bf4518238618474 + languageName: node + linkType: hard + "doctrine@npm:^3.0.0": version: 3.0.0 resolution: "doctrine@npm:3.0.0" @@ -18264,6 +18396,60 @@ __metadata: languageName: node linkType: hard +"es-abstract@npm:^1.17.5, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.1, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3": + version: 1.23.3 + resolution: "es-abstract@npm:1.23.3" + dependencies: + array-buffer-byte-length: "npm:^1.0.1" + arraybuffer.prototype.slice: "npm:^1.0.3" + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.7" + data-view-buffer: "npm:^1.0.1" + data-view-byte-length: "npm:^1.0.1" + data-view-byte-offset: "npm:^1.0.0" + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + es-set-tostringtag: "npm:^2.0.3" + es-to-primitive: "npm:^1.2.1" + function.prototype.name: "npm:^1.1.6" + get-intrinsic: "npm:^1.2.4" + get-symbol-description: "npm:^1.0.2" + globalthis: "npm:^1.0.3" + gopd: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.2" + has-proto: "npm:^1.0.3" + has-symbols: "npm:^1.0.3" + hasown: "npm:^2.0.2" + internal-slot: "npm:^1.0.7" + is-array-buffer: "npm:^3.0.4" + is-callable: "npm:^1.2.7" + is-data-view: "npm:^1.0.1" + is-negative-zero: "npm:^2.0.3" + is-regex: "npm:^1.1.4" + is-shared-array-buffer: "npm:^1.0.3" + is-string: "npm:^1.0.7" + is-typed-array: "npm:^1.1.13" + is-weakref: "npm:^1.0.2" + object-inspect: "npm:^1.13.1" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.5" + regexp.prototype.flags: "npm:^1.5.2" + safe-array-concat: "npm:^1.1.2" + safe-regex-test: "npm:^1.0.3" + string.prototype.trim: "npm:^1.2.9" + string.prototype.trimend: "npm:^1.0.8" + string.prototype.trimstart: "npm:^1.0.8" + typed-array-buffer: "npm:^1.0.2" + typed-array-byte-length: "npm:^1.0.1" + typed-array-byte-offset: "npm:^1.0.2" + typed-array-length: "npm:^1.0.6" + unbox-primitive: "npm:^1.0.2" + which-typed-array: "npm:^1.1.15" + checksum: 10/2da795a6a1ac5fc2c452799a409acc2e3692e06dc6440440b076908617188899caa562154d77263e3053bcd9389a07baa978ab10ac3b46acc399bd0c77be04cb + languageName: node + linkType: hard + "es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.0, es-abstract@npm:^1.20.1": version: 1.20.1 resolution: "es-abstract@npm:1.20.1" @@ -18358,7 +18544,7 @@ __metadata: languageName: node linkType: hard -"es-errors@npm:^1.3.0": +"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" checksum: 10/96e65d640156f91b707517e8cdc454dd7d47c32833aa3e85d79f24f9eb7ea85f39b63e36216ef0114996581969b59fe609a94e30316b08f5f4df1d44134cf8d5 @@ -18382,6 +18568,29 @@ __metadata: languageName: node linkType: hard +"es-iterator-helpers@npm:^1.1.0": + version: 1.2.0 + resolution: "es-iterator-helpers@npm:1.2.0" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.3" + es-errors: "npm:^1.3.0" + es-set-tostringtag: "npm:^2.0.3" + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.2.4" + globalthis: "npm:^1.0.4" + gopd: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.2" + has-proto: "npm:^1.0.3" + has-symbols: "npm:^1.0.3" + internal-slot: "npm:^1.0.7" + iterator.prototype: "npm:^1.1.3" + safe-array-concat: "npm:^1.1.2" + checksum: 10/a4159e36c6bae03d4b636894fff2ff1acfcedc16c622939298b00adf4d2da6356ad92f682cc75c037a012a4b06adb903f67dfdfd05bac61847e9b763de2acbcb + languageName: node + linkType: hard + "es-module-lexer@npm:^0.9.3": version: 0.9.3 resolution: "es-module-lexer@npm:0.9.3" @@ -18389,6 +18598,15 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.0.0": + version: 1.0.0 + resolution: "es-object-atoms@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10/f8910cf477e53c0615f685c5c96210591841850871b81924fcf256bfbaa68c254457d994a4308c60d15b20805e7f61ce6abc669375e01a5349391a8c1767584f + languageName: node + linkType: hard + "es-set-tostringtag@npm:^2.0.1": version: 2.0.2 resolution: "es-set-tostringtag@npm:2.0.2" @@ -18400,7 +18618,18 @@ __metadata: languageName: node linkType: hard -"es-shim-unscopables@npm:^1.0.0": +"es-set-tostringtag@npm:^2.0.3": + version: 2.0.3 + resolution: "es-set-tostringtag@npm:2.0.3" + dependencies: + get-intrinsic: "npm:^1.2.4" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.1" + checksum: 10/7227fa48a41c0ce83e0377b11130d324ac797390688135b8da5c28994c0165be8b252e15cd1de41e1325e5a5412511586960213e88f9ab4a5e7d028895db5129 + languageName: node + linkType: hard + +"es-shim-unscopables@npm:^1.0.0, es-shim-unscopables@npm:^1.0.2": version: 1.0.2 resolution: "es-shim-unscopables@npm:1.0.2" dependencies: @@ -18991,6 +19220,43 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-react-hooks@npm:^5.0.0": + version: 5.0.0 + resolution: "eslint-plugin-react-hooks@npm:5.0.0" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + checksum: 10/b762789832806b6981e2d910994e72aa7a85136fe0880572334b26cf1274ba37bd3b1365e77d2c2f92465337c4a65c84ef647bc499d33b86fc1110f2df7ef1bb + languageName: node + linkType: hard + +"eslint-plugin-react@npm:^7.37.2": + version: 7.37.2 + resolution: "eslint-plugin-react@npm:7.37.2" + dependencies: + array-includes: "npm:^3.1.8" + array.prototype.findlast: "npm:^1.2.5" + array.prototype.flatmap: "npm:^1.3.2" + array.prototype.tosorted: "npm:^1.1.4" + doctrine: "npm:^2.1.0" + es-iterator-helpers: "npm:^1.1.0" + estraverse: "npm:^5.3.0" + hasown: "npm:^2.0.2" + jsx-ast-utils: "npm:^2.4.1 || ^3.0.0" + minimatch: "npm:^3.1.2" + object.entries: "npm:^1.1.8" + object.fromentries: "npm:^2.0.8" + object.values: "npm:^1.2.0" + prop-types: "npm:^15.8.1" + resolve: "npm:^2.0.0-next.5" + semver: "npm:^6.3.1" + string.prototype.matchall: "npm:^4.0.11" + string.prototype.repeat: "npm:^1.0.0" + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + checksum: 10/df2f7ab198018d3378f305a8a5ceceebc9bd31f019fc7567a2ef9c77789dc8a6a2c3c3957f8b0805f26c11c02f9f86c972e02cd0eda12f4d0370526c11f8a9a3 + languageName: node + linkType: hard + "eslint-plugin-storybook@npm:^0.6.15": version: 0.6.15 resolution: "eslint-plugin-storybook@npm:0.6.15" @@ -19157,7 +19423,7 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": +"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" checksum: 10/37cbe6e9a68014d34dbdc039f90d0baf72436809d02edffcc06ba3c2a12eb298048f877511353b130153e532aac8d68ba78430c0dd2f44806ebc7c014b01585e @@ -20604,7 +20870,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4": +"get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4": version: 1.2.4 resolution: "get-intrinsic@npm:1.2.4" dependencies: @@ -20702,6 +20968,17 @@ __metadata: languageName: node linkType: hard +"get-symbol-description@npm:^1.0.2": + version: 1.0.2 + resolution: "get-symbol-description@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.5" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.4" + checksum: 10/e1cb53bc211f9dbe9691a4f97a46837a553c4e7caadd0488dc24ac694db8a390b93edd412b48dcdd0b4bbb4c595de1709effc75fc87c0839deedc6968f5bd973 + languageName: node + linkType: hard + "get-tsconfig@npm:^4.7.2": version: 4.7.3 resolution: "get-tsconfig@npm:4.7.3" @@ -20975,6 +21252,16 @@ __metadata: languageName: node linkType: hard +"globalthis@npm:^1.0.4": + version: 1.0.4 + resolution: "globalthis@npm:1.0.4" + dependencies: + define-properties: "npm:^1.2.1" + gopd: "npm:^1.0.1" + checksum: 10/1f1fd078fb2f7296306ef9dd51019491044ccf17a59ed49d375b576ca108ff37e47f3d29aead7add40763574a992f16a5367dd1e2173b8634ef18556ab719ac4 + languageName: node + linkType: hard + "globby@npm:^10.0.1": version: 10.0.2 resolution: "globby@npm:10.0.2" @@ -21510,6 +21797,13 @@ __metadata: languageName: node linkType: hard +"has-proto@npm:^1.0.3": + version: 1.0.3 + resolution: "has-proto@npm:1.0.3" + checksum: 10/0b67c2c94e3bea37db3e412e3c41f79d59259875e636ba471e94c009cdfb1fa82bf045deeffafc7dbb9c148e36cae6b467055aaa5d9fad4316e11b41e3ba551a + languageName: node + linkType: hard + "has-symbol-support-x@npm:^1.4.1": version: 1.4.2 resolution: "has-symbol-support-x@npm:1.4.2" @@ -21542,6 +21836,15 @@ __metadata: languageName: node linkType: hard +"has-tostringtag@npm:^1.0.2": + version: 1.0.2 + resolution: "has-tostringtag@npm:1.0.2" + dependencies: + has-symbols: "npm:^1.0.3" + checksum: 10/c74c5f5ceee3c8a5b8bc37719840dc3749f5b0306d818974141dda2471a1a2ca6c8e46b9d6ac222c5345df7a901c9b6f350b1e6d62763fec877e26609a401bfe + languageName: node + linkType: hard + "has-unicode@npm:^2.0.0, has-unicode@npm:^2.0.1": version: 2.0.1 resolution: "has-unicode@npm:2.0.1" @@ -21598,6 +21901,15 @@ __metadata: languageName: node linkType: hard +"hasown@npm:^2.0.1, hasown@npm:^2.0.2": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10/7898a9c1788b2862cf0f9c345a6bec77ba4a0c0983c7f19d610c382343d4f98fa260686b225dfb1f88393a66679d2ec58ee310c1d6868c081eda7918f32cc70a + languageName: node + linkType: hard + "he@npm:1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" @@ -21963,7 +22275,7 @@ __metadata: languageName: node linkType: hard -"internal-slot@npm:^1.0.4": +"internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.7": version: 1.0.7 resolution: "internal-slot@npm:1.0.7" dependencies: @@ -22052,6 +22364,16 @@ __metadata: languageName: node linkType: hard +"is-array-buffer@npm:^3.0.4": + version: 3.0.4 + resolution: "is-array-buffer@npm:3.0.4" + dependencies: + call-bind: "npm:^1.0.2" + get-intrinsic: "npm:^1.2.1" + checksum: 10/34a26213d981d58b30724ef37a1e0682f4040d580fa9ff58fdfdd3cefcb2287921718c63971c1c404951e7b747c50fdc7caf6e867e951353fa71b369c04c969b + languageName: node + linkType: hard + "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -22059,6 +22381,15 @@ __metadata: languageName: node linkType: hard +"is-async-function@npm:^2.0.0": + version: 2.0.0 + resolution: "is-async-function@npm:2.0.0" + dependencies: + has-tostringtag: "npm:^1.0.0" + checksum: 10/2cf336fbf8cba3badcf526aa3d10384c30bab32615ac4831b74492eb4e843ccb7d8439a119c27f84bcf217d72024e611b1373f870f433b48f3fa57d3d1b863f1 + languageName: node + linkType: hard + "is-bigint@npm:^1.0.1": version: 1.0.4 resolution: "is-bigint@npm:1.0.4" @@ -22148,6 +22479,15 @@ __metadata: languageName: node linkType: hard +"is-data-view@npm:^1.0.1": + version: 1.0.1 + resolution: "is-data-view@npm:1.0.1" + dependencies: + is-typed-array: "npm:^1.1.13" + checksum: 10/4ba4562ac2b2ec005fefe48269d6bd0152785458cd253c746154ffb8a8ab506a29d0cfb3b74af87513843776a88e4981ae25c89457bf640a33748eab1a7216b5 + languageName: node + linkType: hard + "is-date-object@npm:^1.0.1, is-date-object@npm:^1.0.5": version: 1.0.5 resolution: "is-date-object@npm:1.0.5" @@ -22180,6 +22520,15 @@ __metadata: languageName: node linkType: hard +"is-finalizationregistry@npm:^1.0.2": + version: 1.0.2 + resolution: "is-finalizationregistry@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.2" + checksum: 10/1b8e9e1bf2075e862315ef9d38ce6d39c43ca9d81d46f73b34473506992f4b0fbaadb47ec9b420a5e76afe3f564d9f1f0d9b552ef272cc2395e0f21d743c9c29 + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^1.0.0": version: 1.0.0 resolution: "is-fullwidth-code-point@npm:1.0.0" @@ -22224,7 +22573,7 @@ __metadata: languageName: node linkType: hard -"is-generator-function@npm:^1.0.7": +"is-generator-function@npm:^1.0.10, is-generator-function@npm:^1.0.7": version: 1.0.10 resolution: "is-generator-function@npm:1.0.10" dependencies: @@ -22294,6 +22643,13 @@ __metadata: languageName: node linkType: hard +"is-negative-zero@npm:^2.0.3": + version: 2.0.3 + resolution: "is-negative-zero@npm:2.0.3" + checksum: 10/8fe5cffd8d4fb2ec7b49d657e1691889778d037494c6f40f4d1a524cadd658b4b53ad7b6b73a59bcb4b143ae9a3d15829af864b2c0f9d65ac1e678c4c80f17e5 + languageName: node + linkType: hard + "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -22394,6 +22750,15 @@ __metadata: languageName: node linkType: hard +"is-shared-array-buffer@npm:^1.0.3": + version: 1.0.3 + resolution: "is-shared-array-buffer@npm:1.0.3" + dependencies: + call-bind: "npm:^1.0.7" + checksum: 10/bc5402900dc62b96ebb2548bf5b0a0bcfacc2db122236fe3ab3b3e3c884293a0d5eb777e73f059bcbf8dc8563bb65eae972fee0fb97e38a9ae27c8678f62bcfe + languageName: node + linkType: hard + "is-stream@npm:^1.0.0": version: 1.1.0 resolution: "is-stream@npm:1.1.0" @@ -22451,6 +22816,15 @@ __metadata: languageName: node linkType: hard +"is-typed-array@npm:^1.1.13": + version: 1.1.13 + resolution: "is-typed-array@npm:1.1.13" + dependencies: + which-typed-array: "npm:^1.1.14" + checksum: 10/f850ba08286358b9a11aee6d93d371a45e3c59b5953549ee1c1a9a55ba5c1dd1bd9952488ae194ad8f32a9cf5e79c8fa5f0cc4d78c00720aa0bbcf238b38062d + languageName: node + linkType: hard + "is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": version: 1.1.9 resolution: "is-typed-array@npm:1.1.9" @@ -22672,6 +23046,19 @@ __metadata: languageName: node linkType: hard +"iterator.prototype@npm:^1.1.3": + version: 1.1.3 + resolution: "iterator.prototype@npm:1.1.3" + dependencies: + define-properties: "npm:^1.2.1" + get-intrinsic: "npm:^1.2.1" + has-symbols: "npm:^1.0.3" + reflect.getprototypeof: "npm:^1.0.4" + set-function-name: "npm:^2.0.1" + checksum: 10/1a2a508d3baac121b76c834404ff552d1bb96a173b1d74ff947b2c5763840c0b1e5be01be7e2183a19b08e99e38729812668ff1f23b35f6655a366017bc32519 + languageName: node + linkType: hard + "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -23494,6 +23881,18 @@ __metadata: languageName: node linkType: hard +"jsx-ast-utils@npm:^2.4.1 || ^3.0.0": + version: 3.3.5 + resolution: "jsx-ast-utils@npm:3.3.5" + dependencies: + array-includes: "npm:^3.1.6" + array.prototype.flat: "npm:^1.3.1" + object.assign: "npm:^4.1.4" + object.values: "npm:^1.1.6" + checksum: 10/b61d44613687dfe4cc8ad4b4fbf3711bf26c60b8d5ed1f494d723e0808415c59b24a7c0ed8ab10736a40ff84eef38cbbfb68b395e05d31117b44ffc59d31edfc + languageName: node + linkType: hard + "just-extend@npm:^4.0.2": version: 4.2.1 resolution: "just-extend@npm:4.2.1" @@ -25713,6 +26112,41 @@ __metadata: languageName: node linkType: hard +"object.assign@npm:^4.1.5": + version: 4.1.5 + resolution: "object.assign@npm:4.1.5" + dependencies: + call-bind: "npm:^1.0.5" + define-properties: "npm:^1.2.1" + has-symbols: "npm:^1.0.3" + object-keys: "npm:^1.1.1" + checksum: 10/dbb22da4cda82e1658349ea62b80815f587b47131b3dd7a4ab7f84190ab31d206bbd8fe7e26ae3220c55b65725ac4529825f6142154211220302aa6b1518045d + languageName: node + linkType: hard + +"object.entries@npm:^1.1.8": + version: 1.1.8 + resolution: "object.entries@npm:1.1.8" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/2301918fbd1ee697cf6ff7cd94f060c738c0a7d92b22fd24c7c250e9b593642c9707ad2c44d339303c1439c5967d8964251cdfc855f7f6ec55db2dd79e8dc2a7 + languageName: node + linkType: hard + +"object.fromentries@npm:^2.0.8": + version: 2.0.8 + resolution: "object.fromentries@npm:2.0.8" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + es-object-atoms: "npm:^1.0.0" + checksum: 10/5b2e80f7af1778b885e3d06aeb335dcc86965e39464671adb7167ab06ac3b0f5dd2e637a90d8ebd7426d69c6f135a4753ba3dd7d0fe2a7030cf718dcb910fd92 + languageName: node + linkType: hard + "object.getownpropertydescriptors@npm:^2.0.3": version: 2.1.4 resolution: "object.getownpropertydescriptors@npm:2.1.4" @@ -25725,6 +26159,17 @@ __metadata: 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" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/db2e498019c354428c5dd30d02980d920ac365b155fce4dcf63eb9433f98ccf0f72624309e182ce7cc227c95e45d474e1d483418e60de2293dd23fa3ebe34903 + languageName: node + linkType: hard + "obliterator@npm:^2.0.0": version: 2.0.4 resolution: "obliterator@npm:2.0.4" @@ -26432,6 +26877,13 @@ __metadata: 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" + checksum: 10/8ed3e96dfeea1c5880c1f4c9cb707e5fb26e8be22f14f82ef92df20fd2004e635c62ba47fbe8f2bb63bfd80dac1474be2fb39798da8c2feba2815435d1f749af + languageName: node + linkType: hard + "postcss-import@npm:^15.1.0": version: 15.1.0 resolution: "postcss-import@npm:15.1.0" @@ -26754,7 +27206,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.7.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: @@ -27525,6 +27977,21 @@ __metadata: languageName: node linkType: hard +"reflect.getprototypeof@npm:^1.0.4": + version: 1.0.6 + resolution: "reflect.getprototypeof@npm:1.0.6" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.1" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.4" + globalthis: "npm:^1.0.3" + which-builtin-type: "npm:^1.1.3" + checksum: 10/518f6457e4bb470c9b317d239c62d4b4a05678b7eae4f1c3f4332fad379b3ea6d2d8999bfad448547fdba8fb77e4725cfe8c6440d0168ff387f16b4f19f759ad + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.1.0": version: 10.1.1 resolution: "regenerate-unicode-properties@npm:10.1.1" @@ -27586,6 +28053,18 @@ __metadata: languageName: node linkType: hard +"regexp.prototype.flags@npm:^1.5.2": + version: 1.5.3 + resolution: "regexp.prototype.flags@npm:1.5.3" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-errors: "npm:^1.3.0" + set-function-name: "npm:^2.0.2" + checksum: 10/fe17bc4eebbc72945aaf9dd059eb7784a5ca453a67cc4b5b3e399ab08452c9a05befd92063e2c52e7b24d9238c60031656af32dd57c555d1ba6330dbf8c23b43 + languageName: node + linkType: hard + "regexpu-core@npm:^5.3.1": version: 5.3.2 resolution: "regexpu-core@npm:5.3.2" @@ -27844,6 +28323,19 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^2.0.0-next.5": + version: 2.0.0-next.5 + resolution: "resolve@npm:2.0.0-next.5" + dependencies: + is-core-module: "npm:^2.13.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10/2d6fd28699f901744368e6f2032b4268b4c7b9185fd8beb64f68c93ac6b22e52ae13560ceefc96241a665b985edf9ffd393ae26d2946a7d3a07b7007b7d51e79 + languageName: node + linkType: hard + "resolve@patch:resolve@npm%3A1.1.x#optional!builtin": version: 1.1.7 resolution: "resolve@patch:resolve@npm%3A1.1.7#optional!builtin::version=1.1.7&hash=3bafbf" @@ -27886,6 +28378,19 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@npm%3A^2.0.0-next.5#optional!builtin": + version: 2.0.0-next.5 + resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#optional!builtin::version=2.0.0-next.5&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.13.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10/05fa778de9d0347c8b889eb7a18f1f06bf0f801b0eb4610b4871a4b2f22e220900cf0ad525e94f990bb8d8921c07754ab2122c0c225ab4cdcea98f36e64fa4c2 + languageName: node + linkType: hard + "responselike@npm:^2.0.0": version: 2.0.1 resolution: "responselike@npm:2.0.1" @@ -28290,6 +28795,18 @@ __metadata: languageName: node linkType: hard +"safe-array-concat@npm:^1.1.2": + version: 1.1.2 + resolution: "safe-array-concat@npm:1.1.2" + dependencies: + call-bind: "npm:^1.0.7" + get-intrinsic: "npm:^1.2.4" + has-symbols: "npm:^1.0.3" + isarray: "npm:^2.0.5" + checksum: 10/a54f8040d7cb696a1ee38d19cc71ab3cfb654b9b81bae00c6459618cfad8214ece7e6666592f9c925aafef43d0a20c5e6fbb3413a2b618e1ce9d516a2e6dcfc5 + languageName: node + linkType: hard + "safe-buffer@npm:5.1.2, safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" @@ -28315,6 +28832,17 @@ __metadata: languageName: node linkType: hard +"safe-regex-test@npm:^1.0.3": + version: 1.0.3 + resolution: "safe-regex-test@npm:1.0.3" + dependencies: + call-bind: "npm:^1.0.6" + es-errors: "npm:^1.3.0" + is-regex: "npm:^1.1.4" + checksum: 10/b04de61114b10274d92e25b6de7ccb5de07f11ea15637ff636de4b5190c0f5cd8823fe586dde718504cf78055437d70fd8804976894df502fcf5a210c970afb3 + languageName: node + linkType: hard + "safe-stable-stringify@npm:^2.3.1": version: 2.4.3 resolution: "safe-stable-stringify@npm:2.4.3" @@ -28598,6 +29126,18 @@ __metadata: languageName: node linkType: hard +"set-function-name@npm:^2.0.1, set-function-name@npm:^2.0.2": + version: 2.0.2 + resolution: "set-function-name@npm:2.0.2" + dependencies: + define-data-property: "npm:^1.1.4" + es-errors: "npm:^1.3.0" + functions-have-names: "npm:^1.2.3" + has-property-descriptors: "npm:^1.0.2" + checksum: 10/c7614154a53ebf8c0428a6c40a3b0b47dac30587c1a19703d1b75f003803f73cdfa6a93474a9ba678fa565ef5fbddc2fae79bca03b7d22ab5fd5163dbe571a74 + languageName: node + linkType: hard + "setimmediate@npm:1.0.4": version: 1.0.4 resolution: "setimmediate@npm:1.0.4" @@ -29511,6 +30051,36 @@ __metadata: languageName: node linkType: hard +"string.prototype.matchall@npm:^4.0.11": + version: 4.0.11 + resolution: "string.prototype.matchall@npm:4.0.11" + 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" + get-intrinsic: "npm:^1.2.4" + gopd: "npm:^1.0.1" + has-symbols: "npm:^1.0.3" + internal-slot: "npm:^1.0.7" + regexp.prototype.flags: "npm:^1.5.2" + set-function-name: "npm:^2.0.2" + side-channel: "npm:^1.0.6" + checksum: 10/a902ff4500f909f2a08e55cc5ab1ffbbc905f603b36837674370ee3921058edd0392147e15891910db62a2f31ace2adaf065eaa3bc6e9810bdbc8ca48e05a7b5 + languageName: node + linkType: hard + +"string.prototype.repeat@npm:^1.0.0": + version: 1.0.0 + resolution: "string.prototype.repeat@npm:1.0.0" + dependencies: + define-properties: "npm:^1.1.3" + es-abstract: "npm:^1.17.5" + checksum: 10/4b1bd91b75fa8fdf0541625184ebe80e445a465ce4253c19c3bccd633898005dadae0f74b85ae72662a53aafb8035bf48f8f5c0755aec09bc106a7f13959d05e + languageName: node + linkType: hard + "string.prototype.trim@npm:^1.2.8": version: 1.2.8 resolution: "string.prototype.trim@npm:1.2.8" @@ -29522,6 +30092,18 @@ __metadata: languageName: node linkType: hard +"string.prototype.trim@npm:^1.2.9": + version: 1.2.9 + resolution: "string.prototype.trim@npm:1.2.9" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.0" + es-object-atoms: "npm:^1.0.0" + checksum: 10/b2170903de6a2fb5a49bb8850052144e04b67329d49f1343cdc6a87cb24fb4e4b8ad00d3e273a399b8a3d8c32c89775d93a8f43cb42fbff303f25382079fb58a + languageName: node + linkType: hard + "string.prototype.trimend@npm:^1.0.5": version: 1.0.5 resolution: "string.prototype.trimend@npm:1.0.5" @@ -29544,6 +30126,17 @@ __metadata: languageName: node linkType: hard +"string.prototype.trimend@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimend@npm:1.0.8" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/c2e862ae724f95771da9ea17c27559d4eeced9208b9c20f69bbfcd1b9bc92375adf8af63a103194dba17c4cc4a5cb08842d929f415ff9d89c062d44689c8761b + languageName: node + linkType: hard + "string.prototype.trimstart@npm:^1.0.5": version: 1.0.5 resolution: "string.prototype.trimstart@npm:1.0.5" @@ -29566,6 +30159,17 @@ __metadata: languageName: node linkType: hard +"string.prototype.trimstart@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimstart@npm:1.0.8" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/160167dfbd68e6f7cb9f51a16074eebfce1571656fc31d40c3738ca9e30e35496f2c046fe57b6ad49f65f238a152be8c86fd9a2dd58682b5eba39dad995b3674 + languageName: node + linkType: hard + "string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" @@ -30765,6 +31369,17 @@ __metadata: languageName: node linkType: hard +"typed-array-buffer@npm:^1.0.2": + version: 1.0.2 + resolution: "typed-array-buffer@npm:1.0.2" + dependencies: + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + is-typed-array: "npm:^1.1.13" + checksum: 10/02ffc185d29c6df07968272b15d5319a1610817916ec8d4cd670ded5d1efe72901541ff2202fcc622730d8a549c76e198a2f74e312eabbfb712ed907d45cbb0b + languageName: node + linkType: hard + "typed-array-byte-length@npm:^1.0.0": version: 1.0.0 resolution: "typed-array-byte-length@npm:1.0.0" @@ -30777,6 +31392,19 @@ __metadata: languageName: node linkType: hard +"typed-array-byte-length@npm:^1.0.1": + version: 1.0.1 + resolution: "typed-array-byte-length@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.7" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-proto: "npm:^1.0.3" + is-typed-array: "npm:^1.1.13" + checksum: 10/e4a38329736fe6a73b52a09222d4a9e8de14caaa4ff6ad8e55217f6705b017d9815b7284c85065b3b8a7704e226ccff1372a72b78c2a5b6b71b7bf662308c903 + languageName: node + linkType: hard + "typed-array-byte-offset@npm:^1.0.0": version: 1.0.0 resolution: "typed-array-byte-offset@npm:1.0.0" @@ -30790,6 +31418,20 @@ __metadata: languageName: node linkType: hard +"typed-array-byte-offset@npm:^1.0.2": + version: 1.0.2 + resolution: "typed-array-byte-offset@npm:1.0.2" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.7" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-proto: "npm:^1.0.3" + is-typed-array: "npm:^1.1.13" + checksum: 10/ac26d720ebb2aacbc45e231347c359e6649f52e0cfe0e76e62005912f8030d68e4cb7b725b1754e8fdd48e433cb68df5a8620a3e420ad1457d666e8b29bf9150 + languageName: node + linkType: hard + "typed-array-length@npm:^1.0.4": version: 1.0.4 resolution: "typed-array-length@npm:1.0.4" @@ -30801,6 +31443,20 @@ __metadata: languageName: node linkType: hard +"typed-array-length@npm:^1.0.6": + version: 1.0.6 + resolution: "typed-array-length@npm:1.0.6" + dependencies: + call-bind: "npm:^1.0.7" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-proto: "npm:^1.0.3" + is-typed-array: "npm:^1.1.13" + possible-typed-array-names: "npm:^1.0.0" + checksum: 10/05e96cf4ff836743ebfc593d86133b8c30e83172cb5d16c56814d7bacfed57ce97e87ada9c4b2156d9aaa59f75cdef01c25bd9081c7826e0b869afbefc3e8c39 + languageName: node + linkType: hard + "typedarray-to-buffer@npm:^3.1.5": version: 3.1.5 resolution: "typedarray-to-buffer@npm:3.1.5" @@ -32021,7 +32677,27 @@ __metadata: languageName: node linkType: hard -"which-collection@npm:^1.0.1": +"which-builtin-type@npm:^1.1.3": + version: 1.1.4 + resolution: "which-builtin-type@npm:1.1.4" + dependencies: + function.prototype.name: "npm:^1.1.6" + has-tostringtag: "npm:^1.0.2" + is-async-function: "npm:^2.0.0" + is-date-object: "npm:^1.0.5" + is-finalizationregistry: "npm:^1.0.2" + is-generator-function: "npm:^1.0.10" + is-regex: "npm:^1.1.4" + is-weakref: "npm:^1.0.2" + isarray: "npm:^2.0.5" + which-boxed-primitive: "npm:^1.0.2" + which-collection: "npm:^1.0.2" + which-typed-array: "npm:^1.1.15" + checksum: 10/c0cdb9b004e7a326f4ce54c75b19658a3bec73601a71dd7e2d9538accb3e781b546b589c3f306caf5e7429ac1c8019028d5e662e2860f03603354105b8247c83 + languageName: node + linkType: hard + +"which-collection@npm:^1.0.1, which-collection@npm:^1.0.2": version: 1.0.2 resolution: "which-collection@npm:1.0.2" dependencies: @@ -32070,6 +32746,19 @@ __metadata: languageName: node linkType: hard +"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15": + version: 1.1.15 + resolution: "which-typed-array@npm:1.1.15" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.7" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.2" + checksum: 10/c3b6a99beadc971baa53c3ee5b749f2b9bdfa3b3b9a70650dd8511a48b61d877288b498d424712e9991d16019633086bd8b5923369460d93463c5825fa36c448 + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.2": version: 1.1.8 resolution: "which-typed-array@npm:1.1.8" From fa424826c0132c135511e5316a6e559b574b3a8f Mon Sep 17 00:00:00 2001 From: xeno097 Date: Thu, 7 Nov 2024 20:11:21 -0400 Subject: [PATCH 3/6] feat: core deploy apply admin proxy ownership fixes (#4767) ### Description This PR updates the `hyperlane core init`, `hyperlane core deploy` and `hyperlane core apply` commands to allow a user to change ownership of the mailbox ProxyAdmin contract by setting a value in the config. ### Drive-by changes - deduped `randomAddress` test util implementations across the `sdk`, 'infra' and `cli` package - added `anvil1` to the `run-e2e-test.sh` script to test `hyperlane core` commands in isolation - implemented the `proxyAdminOwnershipUpdateTxs` to deduplicate proxy admin ownership tx data generation ### Related issues - Fixes #4728 ### Backward compatibility - Yes ### Testing - Manual - e2e ### NOTE: - Merge https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/4726 first --------- Co-authored-by: Paul Balaji <10051819+paulbalaji@users.noreply.github.com> --- .changeset/dry-ties-approve.md | 7 + typescript/cli/src/config/core.ts | 27 ++- typescript/cli/src/deploy/core.ts | 1 + typescript/cli/src/tests/commands/core.ts | 43 ++++ typescript/cli/src/tests/core.e2e-test.ts | 199 ++++++++++++++++++ .../cli/src/tests/warp-deploy.e2e-test.ts | 11 +- typescript/cli/src/utils/input.ts | 10 +- typescript/infra/test/govern.hardhat-test.ts | 6 +- typescript/sdk/src/core/EvmCoreModule.ts | 42 +++- typescript/sdk/src/core/EvmCoreReader.ts | 32 ++- typescript/sdk/src/core/schemas.ts | 5 +- typescript/sdk/src/deploy/proxy.ts | 55 ++++- typescript/sdk/src/hook/EvmHookReader.test.ts | 27 ++- typescript/sdk/src/index.ts | 1 + typescript/sdk/src/ism/EvmIsmReader.test.ts | 14 +- .../sdk/src/token/EvmERC20WarpModule.ts | 42 +--- 16 files changed, 438 insertions(+), 84 deletions(-) create mode 100644 .changeset/dry-ties-approve.md create mode 100644 typescript/cli/src/tests/core.e2e-test.ts diff --git a/.changeset/dry-ties-approve.md b/.changeset/dry-ties-approve.md new file mode 100644 index 000000000..88334d522 --- /dev/null +++ b/.changeset/dry-ties-approve.md @@ -0,0 +1,7 @@ +--- +'@hyperlane-xyz/infra': minor +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Add support for updating the mailbox proxy admin owner diff --git a/typescript/cli/src/config/core.ts b/typescript/cli/src/config/core.ts index 8be42113d..92e6fc688 100644 --- a/typescript/cli/src/config/core.ts +++ b/typescript/cli/src/config/core.ts @@ -1,6 +1,11 @@ import { stringify as yamlStringify } from 'yaml'; -import { CoreConfigSchema, HookConfig, IsmConfig } from '@hyperlane-xyz/sdk'; +import { + CoreConfigSchema, + HookConfig, + IsmConfig, + OwnableConfig, +} from '@hyperlane-xyz/sdk'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; @@ -18,6 +23,9 @@ import { } from './hooks.js'; import { createAdvancedIsmConfig, createTrustedRelayerConfig } from './ism.js'; +const ENTER_DESIRED_VALUE_MSG = 'Enter the desired'; +const SIGNER_PROMPT_LABEL = 'signer'; + export async function createCoreDeployConfig({ context, configFilePath, @@ -31,9 +39,9 @@ export async function createCoreDeployConfig({ const owner = await detectAndConfirmOrPrompt( async () => context.signer?.getAddress(), - 'Enter the desired', + ENTER_DESIRED_VALUE_MSG, 'owner address', - 'signer', + SIGNER_PROMPT_LABEL, ); const defaultIsm: IsmConfig = advanced @@ -41,6 +49,7 @@ export async function createCoreDeployConfig({ : await createTrustedRelayerConfig(context, advanced); let defaultHook: HookConfig, requiredHook: HookConfig; + let proxyAdmin: OwnableConfig; if (advanced) { defaultHook = await createHookConfig({ context, @@ -52,9 +61,20 @@ export async function createCoreDeployConfig({ selectMessage: 'Select required hook type', advanced, }); + proxyAdmin = { + owner: await detectAndConfirmOrPrompt( + async () => context.signer?.getAddress(), + ENTER_DESIRED_VALUE_MSG, + 'ProxyAdmin owner address', + SIGNER_PROMPT_LABEL, + ), + }; } else { defaultHook = await createMerkleTreeConfig(); requiredHook = await createProtocolFeeConfig(context, advanced); + proxyAdmin = { + owner, + }; } try { @@ -63,6 +83,7 @@ export async function createCoreDeployConfig({ defaultIsm, defaultHook, requiredHook, + proxyAdmin, }); logBlue(`Core config is valid, writing to file ${configFilePath}:\n`); log(indentYamlOrJson(yamlStringify(coreConfig, null, 2), 4)); diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index ec1d6b271..28ff4d50e 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -34,6 +34,7 @@ interface DeployParams { interface ApplyParams extends DeployParams { deployedCoreAddresses: DeployedCoreAddresses; } + /** * Executes the core deploy command. */ diff --git a/typescript/cli/src/tests/commands/core.ts b/typescript/cli/src/tests/commands/core.ts index b28dbafd2..24c910974 100644 --- a/typescript/cli/src/tests/commands/core.ts +++ b/typescript/cli/src/tests/commands/core.ts @@ -1,5 +1,9 @@ import { $ } from 'zx'; +import { CoreConfig } from '@hyperlane-xyz/sdk'; + +import { readYamlOrJson } from '../../utils/files.js'; + import { ANVIL_KEY, REGISTRY_PATH } from './helpers.js'; /** @@ -17,3 +21,42 @@ export async function hyperlaneCoreDeploy( --verbosity debug \ --yes`; } + +/** + * 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 \ + --registry ${REGISTRY_PATH} \ + --config ${coreOutputPath} \ + --chain ${chain} \ + --verbosity debug \ + --yes`; +} + +/** + * Updates a Hyperlane core deployment on the specified chain using the provided config. + */ +export async function hyperlaneCoreApply( + chain: string, + coreOutputPath: string, +) { + return $`yarn workspace @hyperlane-xyz/cli run hyperlane core apply \ + --registry ${REGISTRY_PATH} \ + --config ${coreOutputPath} \ + --chain ${chain} \ + --key ${ANVIL_KEY} \ + --verbosity debug \ + --yes`; +} + +/** + * Reads the Core deployment config and outputs it to specified output path. + */ +export async function readCoreConfig( + chain: string, + coreConfigPath: string, +): Promise { + await hyperlaneCoreRead(chain, coreConfigPath); + return readYamlOrJson(coreConfigPath); +} diff --git a/typescript/cli/src/tests/core.e2e-test.ts b/typescript/cli/src/tests/core.e2e-test.ts new file mode 100644 index 000000000..9d4324479 --- /dev/null +++ b/typescript/cli/src/tests/core.e2e-test.ts @@ -0,0 +1,199 @@ +import { expect } from 'chai'; +import { Signer, Wallet, ethers } from 'ethers'; + +import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; +import { + CoreConfig, + ProtocolFeeHookConfig, + randomAddress, +} from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; + +import { + hyperlaneCoreApply, + hyperlaneCoreDeploy, + readCoreConfig, +} from './commands/core.js'; +import { ANVIL_KEY, REGISTRY_PATH } from './commands/helpers.js'; + +const CHAIN_NAME = 'anvil2'; + +const EXAMPLES_PATH = './examples'; +const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config.yaml`; + +const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh +const CORE_READ_CONFIG_PATH = `${TEMP_PATH}/anvil2/core-config-read.yaml`; + +const TEST_TIMEOUT = 100_000; // Long timeout since these tests can take a while +describe('hyperlane core e2e tests', async function () { + this.timeout(TEST_TIMEOUT); + + let signer: Signer; + let initialOwnerAddress: Address; + + before(async () => { + const chainMetadata: any = readYamlOrJson( + `${REGISTRY_PATH}/chains/${CHAIN_NAME}/metadata.yaml`, + ); + + const provider = new ethers.providers.JsonRpcProvider( + chainMetadata.rpcUrls[0].http, + ); + + const wallet = new Wallet(ANVIL_KEY); + signer = wallet.connect(provider); + + initialOwnerAddress = await signer.getAddress(); + }); + + describe('core.deploy', () => { + it('should create a core deployment with the signer as the mailbox owner', async () => { + await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH); + + const coreConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + + expect(coreConfig.owner).to.equal(initialOwnerAddress); + expect(coreConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress); + // Assuming that the ProtocolFeeHook is used for deployment + expect((coreConfig.requiredHook as ProtocolFeeHookConfig).owner).to.equal( + initialOwnerAddress, + ); + }); + + it('should create a core deployment with the mailbox owner set to the address in the config', async () => { + const coreConfig: CoreConfig = await readYamlOrJson(CORE_CONFIG_PATH); + + const newOwner = randomAddress().toLowerCase(); + + coreConfig.owner = newOwner; + writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); + + // Deploy the core contracts with the updated mailbox owner + await hyperlaneCoreDeploy(CHAIN_NAME, CORE_READ_CONFIG_PATH); + + // Verify that the owner has been set correctly without modifying any other owner values + const updatedConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + + expect(updatedConfig.owner.toLowerCase()).to.equal(newOwner); + expect(updatedConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress); + // Assuming that the ProtocolFeeHook is used for deployment + expect( + (updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, + ).to.equal(initialOwnerAddress); + }); + + it('should create a core deployment with ProxyAdmin owner of the mailbox set to the address in the config', async () => { + const coreConfig: CoreConfig = await readYamlOrJson(CORE_CONFIG_PATH); + + const newOwner = randomAddress().toLowerCase(); + + coreConfig.proxyAdmin = { owner: newOwner }; + writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); + + // Deploy the core contracts with the updated mailbox owner + await hyperlaneCoreDeploy(CHAIN_NAME, CORE_READ_CONFIG_PATH); + + // Verify that the owner has been set correctly without modifying any other owner values + const updatedConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + + expect(updatedConfig.owner).to.equal(initialOwnerAddress); + expect(updatedConfig.proxyAdmin?.owner.toLowerCase()).to.equal(newOwner); + // Assuming that the ProtocolFeeHook is used for deployment + expect( + (updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, + ).to.equal(initialOwnerAddress); + }); + }); + + describe('core.apply', () => { + it('should update the mailbox owner', async () => { + await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH); + const coreConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + expect(coreConfig.owner).to.equal(initialOwnerAddress); + const newOwner = randomAddress().toLowerCase(); + coreConfig.owner = newOwner; + writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); + await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH); + // Verify that the owner has been set correctly without modifying any other owner values + const updatedConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + expect(updatedConfig.owner.toLowerCase()).to.equal(newOwner); + expect(updatedConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress); + // Assuming that the ProtocolFeeHook is used for deployment + expect( + (updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, + ).to.equal(initialOwnerAddress); + }); + + it('should update the ProxyAdmin to a new one for the mailbox', async () => { + await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH); + const coreConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + expect(coreConfig.owner).to.equal(initialOwnerAddress); + + const proxyFactory = new ProxyAdmin__factory().connect(signer); + const deployTx = await proxyFactory.deploy(); + const newProxyAdmin = await deployTx.deployed(); + coreConfig.proxyAdmin!.address = newProxyAdmin.address; + + writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); + await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH); + + // Verify that the owner has been set correctly without modifying any other owner values + const updatedConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + expect(updatedConfig.owner).to.equal(initialOwnerAddress); + expect(updatedConfig.proxyAdmin?.address).to.equal(newProxyAdmin.address); + // Assuming that the ProtocolFeeHook is used for deployment + expect( + (updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, + ).to.equal(initialOwnerAddress); + }); + + it('should update the ProxyAdmin owner for the mailbox', async () => { + await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH); + const coreConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + expect(coreConfig.owner).to.equal(initialOwnerAddress); + + const newOwner = randomAddress().toLowerCase(); + coreConfig.proxyAdmin!.owner = newOwner; + writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); + await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH); + + // Verify that the owner has been set correctly without modifying any other owner values + const updatedConfig: CoreConfig = await readCoreConfig( + CHAIN_NAME, + CORE_READ_CONFIG_PATH, + ); + expect(updatedConfig.owner).to.equal(initialOwnerAddress); + expect(updatedConfig.proxyAdmin?.owner.toLowerCase()).to.equal(newOwner); + // Assuming that the ProtocolFeeHook is used for deployment + expect( + (updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, + ).to.equal(initialOwnerAddress); + }); + }); +}); diff --git a/typescript/cli/src/tests/warp-deploy.e2e-test.ts b/typescript/cli/src/tests/warp-deploy.e2e-test.ts index 6263f70cb..9971878eb 100644 --- a/typescript/cli/src/tests/warp-deploy.e2e-test.ts +++ b/typescript/cli/src/tests/warp-deploy.e2e-test.ts @@ -34,6 +34,7 @@ const WARP_CORE_CONFIG_PATH_2_3 = `${REGISTRY_PATH}/deployments/warp_routes/VAUL const TEST_TIMEOUT = 60_000; // Long timeout since these tests can take a while describe('WarpDeploy e2e tests', async function () { let chain2Addresses: ChainAddresses = {}; + let chain3Addresses: ChainAddresses = {}; let token: any; let vault: any; @@ -46,7 +47,11 @@ describe('WarpDeploy e2e tests', async function () { ANVIL_KEY, ); - await deployOrUseExistingCore(CHAIN_NAME_3, CORE_CONFIG_PATH, ANVIL_KEY); + chain3Addresses = await deployOrUseExistingCore( + CHAIN_NAME_3, + CORE_CONFIG_PATH, + ANVIL_KEY, + ); token = await deployToken(ANVIL_KEY, CHAIN_NAME_2); vault = await deploy4626Vault(ANVIL_KEY, CHAIN_NAME_2, token.address); @@ -81,8 +86,8 @@ describe('WarpDeploy e2e tests', async function () { }, [CHAIN_NAME_3]: { type: TokenType.syntheticRebase, - mailbox: chain2Addresses.mailbox, - owner: chain2Addresses.mailbox, + mailbox: chain3Addresses.mailbox, + owner: chain3Addresses.mailbox, collateralChainName: CHAIN_NAME_2, }, }; diff --git a/typescript/cli/src/utils/input.ts b/typescript/cli/src/utils/input.ts index 19b6954f5..2ccef32db 100644 --- a/typescript/cli/src/utils/input.ts +++ b/typescript/cli/src/utils/input.ts @@ -19,14 +19,16 @@ import ansiEscapes from 'ansi-escapes'; import chalk from 'chalk'; import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; -import { ChainName, DeployedOwnableConfig } from '@hyperlane-xyz/sdk'; -import { WarpCoreConfig } from '@hyperlane-xyz/sdk'; +import { + ChainName, + DeployedOwnableConfig, + WarpCoreConfig, +} 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 } from '../logger.js'; -import { logRed } from '../logger.js'; +import { logGray, logRed } from '../logger.js'; import { indentYamlOrJson } from './files.js'; import { selectRegistryWarpRoute } from './tokens.js'; diff --git a/typescript/infra/test/govern.hardhat-test.ts b/typescript/infra/test/govern.hardhat-test.ts index 6de17b11f..90bf2d11b 100644 --- a/typescript/infra/test/govern.hardhat-test.ts +++ b/typescript/infra/test/govern.hardhat-test.ts @@ -1,6 +1,6 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; import { expect } from 'chai'; -import { BigNumber, utils } from 'ethers'; +import { BigNumber } from 'ethers'; import hre from 'hardhat'; import { @@ -27,6 +27,7 @@ import { TestChainName, TestCoreApp, TestCoreDeployer, + randomAddress, } from '@hyperlane-xyz/sdk'; import { Address, CallData, eqAddress } from '@hyperlane-xyz/utils'; @@ -35,9 +36,6 @@ import { HyperlaneAppGovernor, } from '../src/govern/HyperlaneAppGovernor.js'; -// TODO de-dupe with test-utils after migrating this file to the SDK -const randomAddress = () => utils.hexlify(utils.randomBytes(20)); - export class TestApp extends HyperlaneApp<{}> {} export class TestChecker extends HyperlaneAppChecker { diff --git a/typescript/sdk/src/core/EvmCoreModule.ts b/typescript/sdk/src/core/EvmCoreModule.ts index 84a931dca..a14cdc5fe 100644 --- a/typescript/sdk/src/core/EvmCoreModule.ts +++ b/typescript/sdk/src/core/EvmCoreModule.ts @@ -1,4 +1,8 @@ -import { Mailbox, Mailbox__factory } from '@hyperlane-xyz/core'; +import { + Mailbox, + Mailbox__factory, + Ownable__factory, +} from '@hyperlane-xyz/core'; import { Address, Domain, @@ -24,6 +28,7 @@ import { ProxyFactoryFactories, proxyFactoryFactories, } from '../deploy/contracts.js'; +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'; @@ -65,6 +70,7 @@ export class EvmCoreModule extends HyperlaneModule< this.chainName = multiProvider.getChainName(args.chain); this.chainId = multiProvider.getEvmChainId(args.chain); this.domainId = multiProvider.getDomainId(args.chain); + this.chainId = multiProvider.getEvmChainId(args.chain); } /** @@ -92,6 +98,12 @@ export class EvmCoreModule extends HyperlaneModule< transactions.push( ...(await this.createDefaultIsmUpdateTxs(actualConfig, expectedConfig)), ...this.createMailboxOwnerUpdateTxs(actualConfig, expectedConfig), + ...proxyAdminUpdateTxs( + this.chainId, + this.args.addresses.mailbox, + actualConfig, + expectedConfig, + ), ); return transactions; @@ -276,15 +288,17 @@ export class EvmCoreModule extends HyperlaneModule< ); // Deploy proxyAdmin - const proxyAdmin = ( - await coreDeployer.deployContract(chainName, 'proxyAdmin', []) - ).address; + const proxyAdmin = await coreDeployer.deployContract( + chainName, + 'proxyAdmin', + [], + ); // Deploy Mailbox const mailbox = await this.deployMailbox({ config, coreDeployer, - proxyAdmin, + proxyAdmin: proxyAdmin.address, multiProvider, chain, }); @@ -333,11 +347,27 @@ export class EvmCoreModule extends HyperlaneModule< const { merkleTreeHook, interchainGasPaymaster } = serializedContracts[chainName]; + // Update the ProxyAdmin owner of the Mailbox if the config defines a different owner from the current signer + const currentProxyOwner = await proxyAdmin.owner(); + if ( + config?.proxyAdmin?.owner && + !eqAddress(config.proxyAdmin.owner, currentProxyOwner) + ) { + await multiProvider.sendTransaction(chainName, { + annotation: `Transferring ownership of ProxyAdmin to the configured address ${config.proxyAdmin.owner}`, + to: proxyAdmin.address, + data: Ownable__factory.createInterface().encodeFunctionData( + 'transferOwnership(address)', + [config.proxyAdmin.owner], + ), + }); + } + // Set Core & extra addresses return { ...ismFactoryFactories, - proxyAdmin, + proxyAdmin: proxyAdmin.address, mailbox: mailbox.address, interchainAccountRouter, interchainAccountIsm, diff --git a/typescript/sdk/src/core/EvmCoreReader.ts b/typescript/sdk/src/core/EvmCoreReader.ts index 6c945bcf5..0c0a7887a 100644 --- a/typescript/sdk/src/core/EvmCoreReader.ts +++ b/typescript/sdk/src/core/EvmCoreReader.ts @@ -1,6 +1,6 @@ import { providers } from 'ethers'; -import { Mailbox__factory } from '@hyperlane-xyz/core'; +import { Mailbox__factory, ProxyAdmin__factory } from '@hyperlane-xyz/core'; import { Address, objMap, @@ -9,6 +9,8 @@ import { } from '@hyperlane-xyz/utils'; import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; +import { proxyAdmin } from '../deploy/proxy.js'; +import { DeployedOwnableConfig } from '../deploy/types.js'; import { EvmHookReader } from '../hook/EvmHookReader.js'; import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; @@ -46,11 +48,13 @@ export class EvmCoreReader implements CoreReader { */ async deriveCoreConfig(address: Address): Promise { const mailbox = Mailbox__factory.connect(address, this.provider); - const [defaultIsm, defaultHook, requiredHook] = await Promise.all([ - mailbox.defaultIsm(), - mailbox.defaultHook(), - mailbox.requiredHook(), - ]); + const [defaultIsm, defaultHook, requiredHook, mailboxProxyAdmin] = + await Promise.all([ + mailbox.defaultIsm(), + mailbox.defaultHook(), + mailbox.requiredHook(), + proxyAdmin(this.provider, mailbox.address), + ]); // Parallelize each configuration request const results = await promiseObjAll( @@ -60,6 +64,7 @@ export class EvmCoreReader implements CoreReader { defaultIsm: this.evmIsmReader.deriveIsmConfig(defaultIsm), defaultHook: this.evmHookReader.deriveHookConfig(defaultHook), requiredHook: this.evmHookReader.deriveHookConfig(requiredHook), + proxyAdmin: this.getProxyAdminConfig(mailboxProxyAdmin), }, async (_, readerCall) => { try { @@ -77,4 +82,19 @@ export class EvmCoreReader implements CoreReader { return results as CoreConfig; } + + private async getProxyAdminConfig( + proxyAdminAddress: Address, + ): Promise { + const instance = ProxyAdmin__factory.connect( + proxyAdminAddress, + this.provider, + ); + + const owner = await instance.owner(); + return { + owner, + address: proxyAdminAddress, + }; + } } diff --git a/typescript/sdk/src/core/schemas.ts b/typescript/sdk/src/core/schemas.ts index 569bb2ee0..470df95ab 100644 --- a/typescript/sdk/src/core/schemas.ts +++ b/typescript/sdk/src/core/schemas.ts @@ -3,12 +3,15 @@ import { z } from 'zod'; import { ProxyFactoryFactoriesSchema } from '../deploy/schemas.js'; import { HookConfigSchema } from '../hook/schemas.js'; import { IsmConfigSchema } from '../ism/schemas.js'; -import { OwnableSchema } from '../schemas.js'; +import { DeployedOwnableSchema, OwnableSchema } from '../schemas.js'; export const CoreConfigSchema = OwnableSchema.extend({ defaultIsm: IsmConfigSchema, defaultHook: HookConfigSchema, requiredHook: HookConfigSchema, + // This field is set as optional because the old core config + // did not have it and we want to maintain backward compatibility + proxyAdmin: DeployedOwnableSchema.optional(), }); export const DeployedCoreAddressesSchema = ProxyFactoryFactoriesSchema.extend({ diff --git a/typescript/sdk/src/deploy/proxy.ts b/typescript/sdk/src/deploy/proxy.ts index 041d27cac..ed9767225 100644 --- a/typescript/sdk/src/deploy/proxy.ts +++ b/typescript/sdk/src/deploy/proxy.ts @@ -1,6 +1,12 @@ import { ethers } from 'ethers'; -import { Address, eqAddress } from '@hyperlane-xyz/utils'; +import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; +import { Address, ChainId, eqAddress } from '@hyperlane-xyz/utils'; + +import { transferOwnershipTransactions } from '../contracts/contracts.js'; +import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; + +import { DeployedOwnableConfig } from './types.js'; export type UpgradeConfig = { timelock: { @@ -67,3 +73,50 @@ export async function isProxy( const admin = await proxyAdmin(provider, proxy); return !eqAddress(admin, ethers.constants.AddressZero); } + +export function proxyAdminUpdateTxs( + chainId: ChainId, + proxyAddress: Address, + actualConfig: Readonly<{ proxyAdmin?: DeployedOwnableConfig }>, + expectedConfig: Readonly<{ proxyAdmin?: DeployedOwnableConfig }>, +): AnnotatedEV5Transaction[] { + const transactions: AnnotatedEV5Transaction[] = []; + + // Return early because old config files did not have the + // proxyAdmin property + if (!expectedConfig.proxyAdmin?.address) { + return transactions; + } + + const actualProxyAdmin = actualConfig.proxyAdmin!; + const parsedChainId = + typeof chainId === 'string' ? parseInt(chainId) : chainId; + + if ( + actualProxyAdmin.address && + actualProxyAdmin.address !== expectedConfig.proxyAdmin.address + ) { + transactions.push({ + chainId: parsedChainId, + annotation: `Updating ProxyAdmin for proxy at "${proxyAddress}" from "${actualProxyAdmin.address}" to "${expectedConfig.proxyAdmin.address}"`, + to: actualProxyAdmin.address, + data: ProxyAdmin__factory.createInterface().encodeFunctionData( + 'changeProxyAdmin(address,address)', + [proxyAddress, expectedConfig.proxyAdmin.address], + ), + }); + } else { + transactions.push( + // Internally the createTransferOwnershipTx method already checks if the + // two owner values are the same and produces an empty tx batch if they are + ...transferOwnershipTransactions( + parsedChainId, + actualProxyAdmin.address!, + actualProxyAdmin, + expectedConfig.proxyAdmin, + ), + ); + } + + return transactions; +} diff --git a/typescript/sdk/src/hook/EvmHookReader.test.ts b/typescript/sdk/src/hook/EvmHookReader.test.ts index 6bab47a92..befd73a43 100644 --- a/typescript/sdk/src/hook/EvmHookReader.test.ts +++ b/typescript/sdk/src/hook/EvmHookReader.test.ts @@ -19,6 +19,7 @@ import { WithAddress } from '@hyperlane-xyz/utils'; import { TestChainName, test1 } from '../consts/testChains.js'; import { MultiProvider } from '../providers/MultiProvider.js'; +import { randomAddress } from '../test/testUtils.js'; import { EvmHookReader } from './EvmHookReader.js'; import { @@ -35,8 +36,6 @@ describe('EvmHookReader', () => { let multiProvider: MultiProvider; let sandbox: sinon.SinonSandbox; - const generateRandomAddress = () => ethers.Wallet.createRandom().address; - beforeEach(() => { sandbox = sinon.createSandbox(); multiProvider = MultiProvider.createTestMultiProvider(); @@ -48,8 +47,8 @@ describe('EvmHookReader', () => { }); it('should derive merkle tree config correctly', async () => { - const mockAddress = generateRandomAddress(); - const mockOwner = generateRandomAddress(); + const mockAddress = randomAddress(); + const mockOwner = randomAddress(); // Mocking the connect method + returned what we need from contract object const mockContract = { @@ -78,9 +77,9 @@ describe('EvmHookReader', () => { }); it('should derive protocol fee hook correctly', async () => { - const mockAddress = generateRandomAddress(); - const mockOwner = generateRandomAddress(); - const mockBeneficiary = generateRandomAddress(); + const mockAddress = randomAddress(); + const mockOwner = randomAddress(); + const mockBeneficiary = randomAddress(); // Mocking the connect method + returned what we need from contract object const mockContract = { @@ -116,8 +115,8 @@ describe('EvmHookReader', () => { }); it('should derive pausable config correctly', async () => { - const mockAddress = generateRandomAddress(); - const mockOwner = generateRandomAddress(); + const mockAddress = randomAddress(); + const mockOwner = randomAddress(); const mockPaused = randomBytes(1)[0] % 2 === 0; // Mocking the connect method + returned what we need from contract object @@ -151,9 +150,9 @@ describe('EvmHookReader', () => { // eslint-disable-next-line @typescript-eslint/no-empty-function it('should derive op stack config correctly', async () => { - const mockAddress = generateRandomAddress(); - const mockOwner = generateRandomAddress(); - const l1Messenger = generateRandomAddress(); + const mockAddress = randomAddress(); + const mockOwner = randomAddress(); + const l1Messenger = randomAddress(); // Mocking the connect method + returned what we need from contract object const mockContract = { @@ -187,8 +186,8 @@ describe('EvmHookReader', () => { }); it('should throw if derivation fails', async () => { - const mockAddress = generateRandomAddress(); - const mockOwner = generateRandomAddress(); + const mockAddress = randomAddress(); + const mockOwner = randomAddress(); // Mocking the connect method + returned what we need from contract object const mockContract = { diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 6fb5d57a8..ccc0a324f 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -26,6 +26,7 @@ export { testCosmosChain, testSealevelChain, } from './consts/testChains.js'; +export { randomAddress } from './test/testUtils.js'; export { attachAndConnectContracts, attachContracts, diff --git a/typescript/sdk/src/ism/EvmIsmReader.test.ts b/typescript/sdk/src/ism/EvmIsmReader.test.ts index 95368949b..4da13a80a 100644 --- a/typescript/sdk/src/ism/EvmIsmReader.test.ts +++ b/typescript/sdk/src/ism/EvmIsmReader.test.ts @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import { ethers } from 'ethers'; import sinon from 'sinon'; import { @@ -20,6 +19,7 @@ import { WithAddress } from '@hyperlane-xyz/utils'; import { TestChainName } from '../consts/testChains.js'; import { MultiProvider } from '../providers/MultiProvider.js'; +import { randomAddress } from '../test/testUtils.js'; import { EvmIsmReader } from './EvmIsmReader.js'; import { @@ -35,8 +35,6 @@ describe('EvmIsmReader', () => { let multiProvider: MultiProvider; let sandbox: sinon.SinonSandbox; - const generateRandomAddress = () => ethers.Wallet.createRandom().address; - beforeEach(() => { sandbox = sinon.createSandbox(); multiProvider = MultiProvider.createTestMultiProvider(); @@ -48,8 +46,8 @@ describe('EvmIsmReader', () => { }); it('should derive multisig config correctly', async () => { - const mockAddress = generateRandomAddress(); - const mockValidators = [generateRandomAddress(), generateRandomAddress()]; + const mockAddress = randomAddress(); + const mockValidators = [randomAddress(), randomAddress()]; const mockThreshold = 2; // Mocking the connect method + returned what we need from contract object @@ -83,8 +81,8 @@ describe('EvmIsmReader', () => { }); it('should derive pausable config correctly', async () => { - const mockAddress = generateRandomAddress(); - const mockOwner = generateRandomAddress(); + const mockAddress = randomAddress(); + const mockOwner = randomAddress(); const mockPaused = true; // Mocking the connect method + returned what we need from contract object @@ -120,7 +118,7 @@ describe('EvmIsmReader', () => { }); it('should derive test ISM config correctly', async () => { - const mockAddress = generateRandomAddress(); + const mockAddress = randomAddress(); // Mocking the connect method + returned what we need from contract object const mockContract = { diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.ts b/typescript/sdk/src/token/EvmERC20WarpModule.ts index 85ef112f0..2652c7826 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.ts @@ -15,7 +15,6 @@ import { addressToBytes32, assert, deepEquals, - eqAddress, isObjEmpty, objMap, rootLogger, @@ -26,6 +25,7 @@ import { HyperlaneModule, HyperlaneModuleParams, } from '../core/AbstractHyperlaneModule.js'; +import { proxyAdminUpdateTxs } from '../deploy/proxy.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js'; import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; @@ -67,6 +67,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< this.chainName = this.multiProvider.getChainName(args.chain); this.chainId = multiProvider.getEvmChainId(args.chain); this.domainId = multiProvider.getDomainId(args.chain); + this.chainId = multiProvider.getEvmChainId(args.chain); this.contractVerifier ??= new ContractVerifier( multiProvider, {}, @@ -112,7 +113,12 @@ export class EvmERC20WarpModule extends HyperlaneModule< ...this.createRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig), ...this.createOwnershipUpdateTxs(actualConfig, expectedConfig), - ...this.updateProxyAdminOwnershipTxs(actualConfig, expectedConfig), + ...proxyAdminUpdateTxs( + this.chainId, + this.args.addresses.deployedTokenRoute, + actualConfig, + expectedConfig, + ), ); return transactions; @@ -291,38 +297,6 @@ export class EvmERC20WarpModule extends HyperlaneModule< ); } - updateProxyAdminOwnershipTxs( - actualConfig: Readonly, - expectedConfig: Readonly, - ): AnnotatedEV5Transaction[] { - const transactions: AnnotatedEV5Transaction[] = []; - - // Return early because old warp config files did not have the - // proxyAdmin property - if (!expectedConfig.proxyAdmin) { - return transactions; - } - - const actualProxyAdmin = actualConfig.proxyAdmin!; - assert( - eqAddress(actualProxyAdmin.address!, expectedConfig.proxyAdmin.address!), - `ProxyAdmin contract addresses do not match. Expected ${expectedConfig.proxyAdmin.address}, got ${actualProxyAdmin.address}`, - ); - - transactions.push( - // Internally the createTransferOwnershipTx method already checks if the - // two owner values are the same and produces an empty tx batch if they are - ...transferOwnershipTransactions( - this.chainId, - actualProxyAdmin.address!, - actualProxyAdmin, - expectedConfig.proxyAdmin, - ), - ); - - return transactions; - } - /** * Updates or deploys the ISM using the provided configuration. * From 8e3085ad0c9131557b9ade42379783b29fab0f94 Mon Sep 17 00:00:00 2001 From: Danil Nemirovsky Date: Fri, 8 Nov 2024 11:55:35 +0000 Subject: [PATCH 4/6] feat: Scraper report mailbox as recipient of transactions for Sealevel (#4813) ### Description Scraper reports mailbox as recipient of transactions for Sealevel ### Related issues - Contributes into https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4272 ### Backward compatibility Yes ### Testing Manual test of Scraper E2E tests for Ethereum and Sealevel --------- Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com> --- rust/main/Cargo.lock | 1 + rust/main/Cargo.toml | 1 + .../main/chains/hyperlane-sealevel/Cargo.toml | 1 + .../chains/hyperlane-sealevel/src/error.rs | 6 ++ .../chains/hyperlane-sealevel/src/provider.rs | 72 ++++++++++++++++--- 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index b6f02ecfe..6dfcb67ce 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -4629,6 +4629,7 @@ dependencies = [ "hyperlane-sealevel-multisig-ism-message-id", "hyperlane-sealevel-validator-announce", "jsonrpc-core", + "lazy_static", "multisig-ism", "num-traits", "reqwest", diff --git a/rust/main/Cargo.toml b/rust/main/Cargo.toml index 0f1ec5f74..e9192b7f0 100644 --- a/rust/main/Cargo.toml +++ b/rust/main/Cargo.toml @@ -83,6 +83,7 @@ itertools = "*" jobserver = "=0.1.26" jsonrpc-core = "18.0" k256 = { version = "0.13.4", features = ["arithmetic", "std", "ecdsa"] } +lazy_static = "1.5.0" log = "0.4" macro_rules_attribute = "0.2" maplit = "1.0" diff --git a/rust/main/chains/hyperlane-sealevel/Cargo.toml b/rust/main/chains/hyperlane-sealevel/Cargo.toml index 3a1bda12f..666ebee87 100644 --- a/rust/main/chains/hyperlane-sealevel/Cargo.toml +++ b/rust/main/chains/hyperlane-sealevel/Cargo.toml @@ -11,6 +11,7 @@ bincode.workspace = true borsh.workspace = true derive-new.workspace = true jsonrpc-core.workspace = true +lazy_static.workspace = true num-traits.workspace = true reqwest.workspace = true serde.workspace = true diff --git a/rust/main/chains/hyperlane-sealevel/src/error.rs b/rust/main/chains/hyperlane-sealevel/src/error.rs index 569c5cff5..cb9306158 100644 --- a/rust/main/chains/hyperlane-sealevel/src/error.rs +++ b/rust/main/chains/hyperlane-sealevel/src/error.rs @@ -42,6 +42,12 @@ pub enum HyperlaneSealevelError { /// Empty compute units consumed #[error("received empty compute units consumed in transaction")] EmptyComputeUnitsConsumed, + /// Too many non-native programs + #[error("transaction contains too many non-native programs, hash: {0:?}")] + TooManyNonNativePrograms(H512), + /// No non-native programs + #[error("transaction contains no non-native programs, hash: {0:?}")] + NoNonNativePrograms(H512), } impl From for ChainCommunicationError { diff --git a/rust/main/chains/hyperlane-sealevel/src/provider.rs b/rust/main/chains/hyperlane-sealevel/src/provider.rs index 144cc9c96..7c431558a 100644 --- a/rust/main/chains/hyperlane-sealevel/src/provider.rs +++ b/rust/main/chains/hyperlane-sealevel/src/provider.rs @@ -1,10 +1,13 @@ +use std::collections::HashSet; use std::sync::Arc; use async_trait::async_trait; +use lazy_static::lazy_static; use solana_sdk::signature::Signature; use solana_transaction_status::{ option_serializer::OptionSerializer, EncodedTransaction, EncodedTransactionWithStatusMeta, - UiMessage, UiTransaction, UiTransactionStatusMeta, + UiInstruction, UiMessage, UiParsedInstruction, UiParsedMessage, UiTransaction, + UiTransactionStatusMeta, }; use tracing::warn; @@ -18,6 +21,19 @@ use crate::error::HyperlaneSealevelError; use crate::utils::{decode_h256, decode_h512, decode_pubkey}; use crate::{ConnectionConf, SealevelRpcClient}; +lazy_static! { + static ref NATIVE_PROGRAMS: HashSet = HashSet::from([ + solana_sdk::bpf_loader_upgradeable::ID.to_string(), + solana_sdk::compute_budget::ID.to_string(), + solana_sdk::config::program::ID.to_string(), + solana_sdk::ed25519_program::ID.to_string(), + solana_sdk::secp256k1_program::ID.to_string(), + solana_sdk::stake::program::ID.to_string(), + solana_sdk::system_program::ID.to_string(), + solana_sdk::vote::program::ID.to_string(), + ]); +} + /// A wrapper around a Sealevel provider to get generic blockchain information. #[derive(Debug)] pub struct SealevelProvider { @@ -33,7 +49,7 @@ impl SealevelProvider { let rpc_client = Arc::new(SealevelRpcClient::new(conf.url.to_string())); let native_token = conf.native_token.clone(); - SealevelProvider { + Self { domain, rpc_client, native_token, @@ -64,12 +80,7 @@ impl SealevelProvider { } fn sender(hash: &H512, txn: &UiTransaction) -> ChainResult { - let message = match &txn.message { - UiMessage::Parsed(m) => m, - m => Err(Into::::into( - HyperlaneSealevelError::UnsupportedMessageEncoding(m.clone()), - ))?, - }; + let message = Self::parsed_message(txn)?; let signer = message .account_keys @@ -80,6 +91,39 @@ impl SealevelProvider { Ok(sender) } + fn recipient(hash: &H512, txn: &UiTransaction) -> ChainResult { + let message = Self::parsed_message(txn)?; + + let programs = message + .instructions + .iter() + .filter_map(|ii| { + if let UiInstruction::Parsed(iii) = ii { + Some(iii) + } else { + None + } + }) + .map(|ii| match ii { + UiParsedInstruction::Parsed(iii) => &iii.program_id, + UiParsedInstruction::PartiallyDecoded(iii) => &iii.program_id, + }) + .filter(|program_id| !NATIVE_PROGRAMS.contains(*program_id)) + .collect::>(); + + if programs.len() > 1 { + Err(HyperlaneSealevelError::TooManyNonNativePrograms(*hash))?; + } + + let program_id = programs + .first() + .ok_or(HyperlaneSealevelError::NoNonNativePrograms(*hash))?; + + let pubkey = decode_pubkey(program_id)?; + let recipient = H256::from_slice(&pubkey.to_bytes()); + Ok(recipient) + } + fn gas(meta: &UiTransactionStatusMeta) -> ChainResult { let OptionSerializer::Some(gas) = meta.compute_units_consumed else { Err(HyperlaneSealevelError::EmptyComputeUnitsConsumed)? @@ -108,6 +152,15 @@ impl SealevelProvider { .ok_or(HyperlaneSealevelError::EmptyMetadata)?; Ok(meta) } + + fn parsed_message(txn: &UiTransaction) -> ChainResult<&UiParsedMessage> { + Ok(match &txn.message { + UiMessage::Parsed(m) => m, + m => Err(Into::::into( + HyperlaneSealevelError::UnsupportedMessageEncoding(m.clone()), + ))?, + }) + } } impl HyperlaneChain for SealevelProvider { @@ -165,6 +218,7 @@ impl HyperlaneProvider for SealevelProvider { Self::validate_transaction(hash, txn)?; let sender = Self::sender(hash, txn)?; + let recipient = Self::recipient(hash, txn)?; let meta = Self::meta(txn_with_meta)?; let gas_used = Self::gas(meta)?; let fee = self.fee(meta)?; @@ -189,7 +243,7 @@ impl HyperlaneProvider for SealevelProvider { gas_price, nonce: 0, sender, - recipient: None, + recipient: Some(recipient), receipt: Some(receipt), raw_input_data: None, }) From f24835438e3cca5b403638d1af7a9b079811c155 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Fri, 8 Nov 2024 12:30:50 +0000 Subject: [PATCH 5/6] feat: Added coinGeckoId as an optional property of the TokenConfigSchema (#4842) ### Description - Ripped out of https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/4835, which is necessary so I can merge and release https://github.com/hyperlane-xyz/hyperlane-registry/pull/370 before 4835 ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- .changeset/thirty-actors-wonder.md | 5 +++++ typescript/sdk/src/token/IToken.ts | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/thirty-actors-wonder.md diff --git a/.changeset/thirty-actors-wonder.md b/.changeset/thirty-actors-wonder.md new file mode 100644 index 000000000..6b098fe78 --- /dev/null +++ b/.changeset/thirty-actors-wonder.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Added coinGeckoId as an optional property of the TokenConfigSchema diff --git a/typescript/sdk/src/token/IToken.ts b/typescript/sdk/src/token/IToken.ts index 28321597c..d903e21e9 100644 --- a/typescript/sdk/src/token/IToken.ts +++ b/typescript/sdk/src/token/IToken.ts @@ -47,6 +47,10 @@ export const TokenConfigSchema = z.object({ .array(TokenConnectionConfigSchema) .optional() .describe('The list of token connections (e.g. warp or IBC)'), + coinGeckoId: z + .string() + .optional() + .describe('The CoinGecko id of the token, used for price lookups'), }); export type TokenArgs = Omit< From 5bebd6d0a3736b59ce8c18f6980b4bb69730f4a5 Mon Sep 17 00:00:00 2001 From: Danil Nemirovsky Date: Fri, 8 Nov 2024 13:01:27 +0000 Subject: [PATCH 6/6] feat: Scraper populates delivered messages for Sealevel (#4817) ### Description Scraper populates delivered messages for Sealevel ### Related issues - Contributes into https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4301 ### Backward compatibility Yes ### Testing Manual run of Scraper Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com> --- .../chains/hyperlane-sealevel/src/mailbox.rs | 174 +++++++++---- .../hyperlane-sealevel/src/transaction.rs | 65 +++-- .../src/transaction/tests.rs | 230 +++++++++++++++++- 3 files changed, 391 insertions(+), 78 deletions(-) diff --git a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs index 45e446127..6e5c21c70 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs @@ -33,6 +33,7 @@ use solana_client::{ use solana_sdk::{ account::Account, bs58, + clock::Slot, commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, hash::Hash, @@ -61,7 +62,9 @@ use hyperlane_core::{ use crate::account::{search_accounts_by_discriminator, search_and_validate_account}; use crate::error::HyperlaneSealevelError; -use crate::transaction::search_dispatched_message_transactions; +use crate::transaction::{ + is_message_delivery_instruction, is_message_dispatch_instruction, search_message_transactions, +}; use crate::utils::{decode_h256, decode_h512, from_base58}; use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; @@ -694,44 +697,15 @@ impl SealevelMailboxIndexer { let hyperlane_message = HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?; - let block = self - .mailbox - .provider - .rpc() - .get_block(dispatched_message_account.slot) + let log_meta = self + .dispatch_message_log_meta( + U256::from(nonce), + &valid_message_storage_pda_pubkey, + &dispatched_message_account.slot, + ) .await?; - let block_hash = decode_h256(&block.blockhash)?; - let transactions = - block.transactions.ok_or(HyperlaneSealevelError::NoTransactions("block which should contain message dispatch transaction does not contain any transaction".to_owned()))?; - - let transaction_hashes = search_dispatched_message_transactions( - &self.mailbox.program_id, - &valid_message_storage_pda_pubkey, - transactions, - ); - - // We expect to see that there is only one message dispatch transaction - if transaction_hashes.len() > 1 { - Err(HyperlaneSealevelError::TooManyTransactions("Block contains more than one dispatch message transaction operating on the same dispatch message store PDA".to_owned()))? - } - - let (transaction_index, transaction_hash) = transaction_hashes - .into_iter() - .next() - .ok_or(HyperlaneSealevelError::NoTransactions("block which should contain message dispatch transaction does not contain any after filtering".to_owned()))?; - - Ok(( - hyperlane_message.into(), - LogMeta { - address: self.mailbox.program_id.to_bytes().into(), - block_number: dispatched_message_account.slot, - block_hash, - transaction_id: transaction_hash, - transaction_index: transaction_index as u64, - log_index: U256::from(nonce), - }, - )) + Ok((hyperlane_message.into(), log_meta)) } fn dispatched_message_account(&self, account: &Account) -> ChainResult { @@ -748,6 +722,28 @@ impl SealevelMailboxIndexer { Ok(expected_pubkey) } + async fn dispatch_message_log_meta( + &self, + log_index: U256, + message_storage_pda_pubkey: &Pubkey, + message_account_slot: &Slot, + ) -> ChainResult { + let error_msg_no_txn = "block which should contain message dispatch transaction does not contain any transaction".to_owned(); + let error_msg_too_many_txns = "block contains more than one dispatch message transaction operating on the same dispatch message store PDA".to_owned(); + let error_msg_no_txn_after_filtering = "block which should contain message dispatch transaction does not contain any after filtering".to_owned(); + + self.log_meta( + log_index, + message_storage_pda_pubkey, + message_account_slot, + &is_message_dispatch_instruction, + error_msg_no_txn, + error_msg_too_many_txns, + error_msg_no_txn_after_filtering, + ) + .await + } + async fn get_delivered_message_with_nonce( &self, nonce: u32, @@ -782,19 +778,15 @@ impl SealevelMailboxIndexer { .into_inner(); let message_id = delivered_message_account.message_id; - Ok(( - message_id.into(), - LogMeta { - address: self.mailbox.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 = self + .delivered_message_log_meta( + U256::from(nonce), + &valid_message_storage_pda_pubkey, + &delivered_message_account.slot, + ) + .await?; + + Ok((message_id.into(), log_meta)) } fn delivered_message_account(&self, account: &Account) -> ChainResult { @@ -808,6 +800,88 @@ impl SealevelMailboxIndexer { })?; Ok(expected_pubkey) } + + async fn delivered_message_log_meta( + &self, + log_index: U256, + message_storage_pda_pubkey: &Pubkey, + message_account_slot: &Slot, + ) -> ChainResult { + let error_msg_no_txn = "block which should contain message delivery transaction does not contain any transaction".to_owned(); + let error_msg_too_many_txns = "block contains more than one deliver message transaction operating on the same delivery message store PDA".to_owned(); + let error_msg_no_txn_after_filtering = "block which should contain message delivery transaction does not contain any after filtering".to_owned(); + + self.log_meta( + log_index, + message_storage_pda_pubkey, + message_account_slot, + &is_message_delivery_instruction, + error_msg_no_txn, + error_msg_too_many_txns, + error_msg_no_txn_after_filtering, + ) + .await + } + + async fn log_meta( + &self, + log_index: U256, + message_storage_pda_pubkey: &Pubkey, + message_account_slot: &Slot, + is_message_instruction: &F, + error_msg_no_txn: String, + error_msg_too_many_txns: String, + error_msg_no_txn_after_filtering: String, + ) -> ChainResult + where + F: Fn(instruction::Instruction) -> bool, + { + let block = self + .mailbox + .provider + .rpc() + .get_block(*message_account_slot) + .await?; + + let block_hash = decode_h256(&block.blockhash)?; + + let transactions = block + .transactions + .ok_or(HyperlaneSealevelError::NoTransactions(error_msg_no_txn))?; + + let transaction_hashes = search_message_transactions( + &self.mailbox.program_id, + &message_storage_pda_pubkey, + transactions, + &is_message_instruction, + ); + + // We expect to see that there is only one message dispatch transaction + if transaction_hashes.len() > 1 { + Err(HyperlaneSealevelError::TooManyTransactions( + error_msg_too_many_txns, + ))? + } + + let (transaction_index, transaction_hash) = + transaction_hashes + .into_iter() + .next() + .ok_or(HyperlaneSealevelError::NoTransactions( + error_msg_no_txn_after_filtering, + ))?; + + let log_meta = LogMeta { + address: self.mailbox.program_id.to_bytes().into(), + block_number: *message_account_slot, + block_hash, + transaction_id: transaction_hash, + transaction_index: transaction_index as u64, + log_index, + }; + + Ok(log_meta) + } } #[async_trait] diff --git a/rust/main/chains/hyperlane-sealevel/src/transaction.rs b/rust/main/chains/hyperlane-sealevel/src/transaction.rs index 26a0722cf..bf6dc29f8 100644 --- a/rust/main/chains/hyperlane-sealevel/src/transaction.rs +++ b/rust/main/chains/hyperlane-sealevel/src/transaction.rs @@ -13,27 +13,35 @@ use hyperlane_core::H512; use crate::utils::{decode_h512, from_base58}; -/// This function searches for a transaction which dispatches Hyperlane message and returns -/// list of hashes of such transactions. +/// This function searches for a transaction which specified instruction on Hyperlane message and +/// returns list of hashes of such transactions. /// /// This function takes the mailbox program identifier and the identifier for PDA for storing -/// a dispatched message and searches a message dispatch transaction in a list of transaction. +/// a dispatched or delivered message and searches a message dispatch or delivery transaction +/// in a list of transactions. +/// /// The list of transaction is usually comes from a block. The function returns list of hashes -/// of such transactions. +/// of such transactions and their relative index in the block. /// /// The transaction will be searched with the following criteria: /// 1. Transaction contains Mailbox program id in the list of accounts. -/// 2. Transaction contains dispatched message PDA in the list of accounts. -/// 3. Transaction is performing message dispatch (OutboxDispatch). +/// 2. Transaction contains dispatched/delivered message PDA in the list of accounts. +/// 3. Transaction is performing the specified message instruction. /// /// * `mailbox_program_id` - Identifier of Mailbox program -/// * `message_storage_pda_pubkey` - Identifier for dispatch message store PDA +/// * `message_storage_pda_pubkey` - Identifier for message store PDA /// * `transactions` - List of transactions -pub fn search_dispatched_message_transactions( +/// * `is_specified_message_instruction` - Function which returns `true` for specified message +/// instruction +pub fn search_message_transactions( mailbox_program_id: &Pubkey, message_storage_pda_pubkey: &Pubkey, transactions: Vec, -) -> Vec<(usize, H512)> { + is_specified_message_instruction: &F, +) -> Vec<(usize, H512)> +where + F: Fn(Instruction) -> bool, +{ transactions .into_iter() .enumerate() @@ -43,38 +51,43 @@ pub fn search_dispatched_message_transactions( .map(|(hash, account_keys, instructions)| (index, hash, account_keys, instructions)) }) .filter_map(|(index, hash, account_keys, instructions)| { - filter_not_relevant( + filter_by_relevancy( mailbox_program_id, message_storage_pda_pubkey, hash, account_keys, instructions, + is_specified_message_instruction, ) .map(|hash| (index, hash)) }) .collect::>() } -fn filter_not_relevant( +fn filter_by_relevancy( mailbox_program_id: &Pubkey, message_storage_pda_pubkey: &Pubkey, hash: H512, account_keys: Vec, instructions: Vec, -) -> Option { + is_specified_message_instruction: &F, +) -> Option +where + F: Fn(Instruction) -> bool, +{ let account_index_map = account_index_map(account_keys); let mailbox_program_id_str = mailbox_program_id.to_string(); let mailbox_program_index = match account_index_map.get(&mailbox_program_id_str) { Some(i) => *i as u8, - None => return None, // If account keys do not contain Mailbox program, transaction is not message dispatch. + None => return None, // If account keys do not contain Mailbox program, transaction is not message dispatch/delivery. }; let message_storage_pda_pubkey_str = message_storage_pda_pubkey.to_string(); - let dispatch_message_pda_account_index = + let message_storage_pda_account_index = match account_index_map.get(&message_storage_pda_pubkey_str) { Some(i) => *i as u8, - None => return None, // If account keys do not contain dispatch message store PDA account, transaction is not message dispatch. + None => return None, // If account keys do not contain dispatch/delivery message store PDA account, transaction is not message dispatch/delivery. }; let mailbox_program_maybe = instructions @@ -83,35 +96,43 @@ fn filter_not_relevant( let mailbox_program = match mailbox_program_maybe { Some(p) => p, - None => return None, // If transaction does not contain call into Mailbox, transaction is not message dispatch. + None => return None, // If transaction does not contain call into Mailbox, transaction is not message dispatch/delivery. }; - // If Mailbox program does not operate on dispatch message store PDA account, transaction is not message dispatch. + // If Mailbox program does not operate on dispatch/delivery message store PDA account, transaction is not message dispatch/delivery. if !mailbox_program .accounts - .contains(&dispatch_message_pda_account_index) + .contains(&message_storage_pda_account_index) { return None; } let instruction_data = match from_base58(&mailbox_program.data) { Ok(d) => d, - Err(_) => return None, // If we cannot decode instruction data, transaction is not message dispatch. + Err(_) => return None, // If we cannot decode instruction data, transaction is not message dispatch/delivery. }; let instruction = match Instruction::from_instruction_data(&instruction_data) { Ok(ii) => ii, - Err(_) => return None, // If we cannot parse instruction data, transaction is not message dispatch. + Err(_) => return None, // If we cannot parse instruction data, transaction is not message dispatch/delivery. }; - // If the call into Mailbox program is not OutboxDispatch, transaction is not message dispatch. - if !matches!(instruction, Instruction::OutboxDispatch(_)) { + // If the call into Mailbox program is not OutboxDispatch/InboxProcess, transaction is not message dispatch/delivery. + if is_specified_message_instruction(instruction) { return None; } Some(hash) } +pub fn is_message_dispatch_instruction(instruction: Instruction) -> bool { + !matches!(instruction, Instruction::OutboxDispatch(_)) +} + +pub fn is_message_delivery_instruction(instruction: Instruction) -> bool { + !matches!(instruction, Instruction::InboxProcess(_)) +} + fn filter_by_validity( tx: UiTransaction, meta: UiTransactionStatusMeta, diff --git a/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs b/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs index 759f15de9..634418c21 100644 --- a/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs +++ b/rust/main/chains/hyperlane-sealevel/src/transaction/tests.rs @@ -1,29 +1,57 @@ +use solana_sdk::pubkey::Pubkey; use solana_transaction_status::EncodedTransactionWithStatusMeta; -use crate::transaction::search_dispatched_message_transactions; +use crate::transaction::{ + is_message_delivery_instruction, is_message_dispatch_instruction, search_message_transactions, +}; use crate::utils::decode_pubkey; #[test] pub fn test_search_dispatched_message_transaction() { // given - let mailbox_program_id = decode_pubkey("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi").unwrap(); let dispatched_message_pda_account = decode_pubkey("6eG8PheL41qLFFUtPjSYMtsp4aoAQsMgcsYwkGCB8kwT").unwrap(); - let transaction = serde_json::from_str::(JSON).unwrap(); - let transactions = vec![transaction]; + let (mailbox_program_id, transactions) = transactions(DISPATCH_TXN_JSON); // when - let transaction_hashes = search_dispatched_message_transactions( + let transaction_hashes = search_message_transactions( &mailbox_program_id, &dispatched_message_pda_account, transactions, + &is_message_dispatch_instruction, ); // then assert!(!transaction_hashes.is_empty()); } -const JSON: &str = r#" +#[test] +pub fn test_search_delivered_message_transaction() { + // given + let delivered_message_pda_account = + decode_pubkey("Dj7jk47KKXvw4nseNGdyHtNHtjPes2XSfByhF8xymrtS").unwrap(); + let (mailbox_program_id, transactions) = transactions(DELIVERY_TXN_JSON); + + // when + let transaction_hashes = search_message_transactions( + &mailbox_program_id, + &delivered_message_pda_account, + transactions, + &is_message_delivery_instruction, + ); + + // then + assert!(!transaction_hashes.is_empty()); +} + +fn transactions(json: &str) -> (Pubkey, Vec) { + let mailbox_program_id = decode_pubkey("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi").unwrap(); + let transaction = serde_json::from_str::(json).unwrap(); + let transactions = vec![transaction]; + (mailbox_program_id, transactions) +} + +const DISPATCH_TXN_JSON: &str = r#" { "blockTime": 1729865514, "meta": { @@ -327,3 +355,193 @@ const JSON: &str = r#" } } "#; + +const DELIVERY_TXN_JSON: &str = r#" +{ + "blockTime": 1726514134, + "meta": { + "computeUnitsConsumed": 200654, + "err": null, + "fee": 5000, + "innerInstructions": [ + { + "index": 1, + "instructions": [ + { + "accounts": [ + 10 + ], + "data": "8YGwT5LUTP4", + "programIdIndex": 9, + "stackHeight": 2 + }, + { + "accounts": [ + 12 + ], + "data": "2848tnNKZjKzgKikguTY4s5nESn7KLYUbLsrp6Z1FYq4BmM31xRwBXnJU5RW9rEvRUjJfJa58kXdgQYEQpg4sDrRfx5HnGsgXfitkxJw5NKVcFAYLSqKvpkYxer2tAn3a8ZzPvuDD9iqyLkvJnRZ3TbcoAHNisFfvBeWK95YL8zxsyzDS9ZBMaoYrLKQx9b915xj9oijw2UNk7FF5qxThZDKwF8rwckb6t2o6ypzFEqYeQCsRW5quayYsLBjHi8RdY18NDkcnPVkQbdR7FmfrncV4H5ZYZaayMtgAs6kHxRgeuuBEtrYG1UbGjWTQAss9zmeXcKipqS3S2bee96U5w9Cd981e8dkakCtKR7KusjE9nhsFTfXoxcwkRhi3TzqDicrqt7Erf78K", + "programIdIndex": 8, + "stackHeight": 2 + }, + { + "accounts": [ + 0, + 3 + ], + "data": "11117UpxCJ2YqmddN2ykgdMGRXkyPgnqEtj5XYrnk1iC4P1xrvXq2zvZQkj3uNaitHEw2k", + "programIdIndex": 5, + "stackHeight": 2 + }, + { + "accounts": [ + 11, + 5, + 10, + 1, + 5, + 2 + ], + "data": "7MHiQP8ahsZcB5cM9ZXGa2foMYQENm7GnrFaV4AmfgKNzSndaXhrcqbVNRgN2kGmrrsfTi8bNEGkAJn6MWjY95PnakaF2HAchXrUUBzQrWKQdRp8VbKjDsnH1tEUiAWm439Y12TpWTW3uSphh1oycpTJP", + "programIdIndex": 9, + "stackHeight": 2 + }, + { + "accounts": [ + 2, + 1 + ], + "data": "3Bxs4ThwQbE4vyj5", + "programIdIndex": 5, + "stackHeight": 3 + } + ] + } + ], + "loadedAddresses": { + "readonly": [], + "writable": [] + }, + "logMessages": [ + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi invoke [1]", + "Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg invoke [2]", + "Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg consumed 4402 of 1363482 compute units", + "Program return: 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg AA==", + "Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg success", + "Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC invoke [2]", + "Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC consumed 106563 of 1353660 compute units", + "Program 372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC success", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg invoke [2]", + "Program 11111111111111111111111111111111 invoke [3]", + "Program 11111111111111111111111111111111 success", + "Program log: Warp route transfer completed from origin: 1408864445, recipient: 528MctBmY7rXqufM3r8k7t9DTfVNuB4K1rr8xVU4naJM, remote_amount: 100000", + "Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg consumed 28117 of 1240216 compute units", + "Program 4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg success", + "Program log: Hyperlane inbox processed message 0x34ed0705362554568a1a2d24aef6bfde71894dd1bb2f0457fb4bd66016074fcc", + "Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi consumed 200504 of 1399850 compute units", + "Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi success" + ], + "postBalances": [ + 338367600, + 199691000, + 891880, + 1287600, + 1211040, + 1, + 1, + 1141440, + 1141440, + 1141440, + 2686560, + 0, + 8017920, + 1141440 + ], + "postTokenBalances": [], + "preBalances": [ + 339660200, + 199591000, + 991880, + 0, + 1211040, + 1, + 1, + 1141440, + 1141440, + 1141440, + 2686560, + 0, + 8017920, + 1141440 + ], + "preTokenBalances": [], + "rewards": [], + "status": { + "Ok": null + } + }, + "slot": 290198208, + "transaction": { + "message": { + "accountKeys": [ + "G5FM3UKwcBJ47PwLWLLY1RQpqNtTMgnqnd6nZGcJqaBp", + "528MctBmY7rXqufM3r8k7t9DTfVNuB4K1rr8xVU4naJM", + "5H4cmX5ybSqK6Ro6nvr9eiR8G8ATTYRwVsZ42VRRW3wa", + "Dj7jk47KKXvw4nseNGdyHtNHtjPes2XSfByhF8xymrtS", + "H3EgdESu59M4hn5wrbeyi9VjmFiLYM7iUAbGtrA5uHNE", + "11111111111111111111111111111111", + "ComputeBudget111111111111111111111111111111", + "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV", + "372D5YP7jMYUgYBXTVJ7BZtzKv1mq1J6wvjSFLNTRreC", + "4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg", + "A2nmLy86tmraneRMEZ5yWbDGq6YsPKNcESGaTZKkRWZU", + "DmU32nL975xAshVYgLLdyMoaUzHa2aCzHJyfLyKRdz3M", + "E2jimXLCtTiuZ6jbXP8B7SyZ5vVc1PKYYnMeho9yJ1en", + "E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi" + ], + "header": { + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 9, + "numRequiredSignatures": 1 + }, + "instructions": [ + { + "accounts": [], + "data": "K1FDJ7", + "programIdIndex": 6, + "stackHeight": null + }, + { + "accounts": [ + 0, + 5, + 4, + 11, + 3, + 10, + 7, + 8, + 12, + 9, + 5, + 10, + 1, + 5, + 2 + ], + "data": "3RwSrioTudpACxczi2EejzKoZCPVuzq6qWLCQYAWoZoTcRPBobUn7tB5SFvMPNHGJ551rmjXDyKdaQLuzX3d5bjHSrSsquwHqWgM6L2kMEEJZjtygNyx3RhJD9GyZqekDuK19cfYfn1dyLuo7SSqswV3t6yptLhnCv8DhxBLRuXhV2GdNy9PLU3VNc9PvPWxg1Grtr9UZ5GnmdKDeqRvonM9AqmuN6mnv3UaqjjAEX8yDKPhWHm6w1HRzfgbjkXQVL5aSqdgJeF3EVBKJCzvMKbUVjTRgD6iHQyUVrSYvrHpKZxc6EctBHN6tyeZrW5RD1M6giasnm4WqrjDwUyz9xwvk31srJrZp7W7D6i2tTajmBbiKjpNo75iaHj4dycf1H", + "programIdIndex": 13, + "stackHeight": null + } + ], + "recentBlockhash": "AzQN8x5uKk7ExXW4eUu2FiqRG1BX73uvfHcQeBDHcu8a" + }, + "signatures": [ + "5pBEVfDD3siir1CBf9taeWuee44GspA7EixYkKnzN1hkeYXLxtKYrbe3aE6hxswbY3hhDRVPDor1ZsSXUorC7bcR" + ] + } +} +"#;