diff --git a/.changeset/calm-eels-care.md b/.changeset/calm-eels-care.md new file mode 100644 index 0000000000..a7911cd16e --- /dev/null +++ b/.changeset/calm-eels-care.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Gracefully handle RPC failures during warp send & fix deriving hook error that prevents warp and core test messages on the cli. diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index fb40d93b56..7e585d4b97 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -83,8 +83,6 @@ export async function runCoreDeploy({ chainName: chain, addresses: deployedAddresses, }); - - // @TODO implement writeAgentConfig } logGreen('✅ Core contract deployments complete:\n'); diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index fe4fd7d68a..fbe3ea4987 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -8,6 +8,7 @@ import { AddressBytes32, ProtocolType, addressToBytes32, + assert, bytes32ToAddress, messageId, objFilter, @@ -116,7 +117,9 @@ export class HyperlaneCore extends HyperlaneApp { const originChain = this.getOrigin(message); const hookReader = new EvmHookReader(this.multiProvider, originChain); const address = await this.getHookAddress(message); - return hookReader.deriveHookConfig(address); + const hookConfig = await hookReader.deriveHookConfig(address); + assert(hookConfig, `No hook config found for ${address}.`); + return hookConfig; } async buildMetadata( diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts index 29ea955349..612043dd43 100644 --- a/typescript/sdk/src/hook/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -20,6 +20,7 @@ import { Address, ProtocolType, addressToBytes32, + assert, configDeepEquals, rootLogger, } from '@hyperlane-xyz/utils'; @@ -108,9 +109,18 @@ export class EvmHookModule extends HyperlaneModule< } public async read(): Promise { - return typeof this.args.config === 'string' - ? this.args.addresses.deployedHook - : this.reader.deriveHookConfig(this.args.addresses.deployedHook); + if (typeof this.args.config === 'string') { + return this.args.addresses.deployedHook; + } else { + const hookConfig = await this.reader.deriveHookConfig( + this.args.addresses.deployedHook, + ); + assert( + hookConfig, + `No hook config found for ${this.args.addresses.deployedHook}`, + ); + return hookConfig; + } } public async update(_config: HookConfig): Promise { diff --git a/typescript/sdk/src/hook/EvmHookReader.test.ts b/typescript/sdk/src/hook/EvmHookReader.test.ts index e9d45e4284..5650b12acc 100644 --- a/typescript/sdk/src/hook/EvmHookReader.test.ts +++ b/typescript/sdk/src/hook/EvmHookReader.test.ts @@ -186,6 +186,27 @@ describe('EvmHookReader', () => { expect(config).to.deep.equal(hookConfig); }); + it('should return an empty config if deriving fails', async () => { + const mockAddress = generateRandomAddress(); + const mockOwner = generateRandomAddress(); + + // Mocking the connect method + returned what we need from contract object + const mockContract = { + // No type + owner: sandbox.stub().resolves(mockOwner), + }; + sandbox + .stub(MerkleTreeHook__factory, 'connect') + .returns(mockContract as unknown as MerkleTreeHook); + sandbox + .stub(IPostDispatchHook__factory, 'connect') + .returns(mockContract as unknown as IPostDispatchHook); + + // top-level method infers hook type + const hookConfig = await evmHookReader.deriveHookConfig(mockAddress); + expect(hookConfig).to.be.undefined; + }); + /* Testing for more nested hook types can be done manually by reading from existing contracts onchain. Examples of nested hook types include: diff --git a/typescript/sdk/src/hook/EvmHookReader.ts b/typescript/sdk/src/hook/EvmHookReader.ts index e4fa971983..b57fa8ede1 100644 --- a/typescript/sdk/src/hook/EvmHookReader.ts +++ b/typescript/sdk/src/hook/EvmHookReader.ts @@ -20,6 +20,7 @@ import { assert, concurrentMap, eqAddress, + getLogLevel, rootLogger, } from '@hyperlane-xyz/utils'; @@ -45,7 +46,9 @@ import { export type DerivedHookConfig = WithAddress>; export interface HookReader { - deriveHookConfig(address: Address): Promise>; + deriveHookConfig( + address: Address, + ): Promise | undefined>; deriveMerkleTreeConfig( address: Address, ): Promise>; @@ -84,35 +87,51 @@ export class EvmHookReader implements HookReader { this.provider = multiProvider.getProvider(chain); } - async deriveHookConfig(address: Address): Promise { - const hook = IPostDispatchHook__factory.connect(address, this.provider); - const onchainHookType: OnchainHookType = await hook.hookType(); - this.logger.debug('Deriving HookConfig', { address, onchainHookType }); - - switch (onchainHookType) { - case OnchainHookType.ROUTING: - return this.deriveDomainRoutingConfig(address); - case OnchainHookType.AGGREGATION: - return this.deriveAggregationConfig(address); - case OnchainHookType.MERKLE_TREE: - return this.deriveMerkleTreeConfig(address); - case OnchainHookType.INTERCHAIN_GAS_PAYMASTER: - return this.deriveIgpConfig(address); - case OnchainHookType.FALLBACK_ROUTING: - return this.deriveFallbackRoutingConfig(address); - case OnchainHookType.PAUSABLE: - return this.derivePausableConfig(address); - case OnchainHookType.PROTOCOL_FEE: - return this.deriveProtocolFeeConfig(address); - // ID_AUTH_ISM could be OPStackHook, ERC5164Hook or LayerZeroV2Hook - // For now assume it's OP_STACK - case OnchainHookType.ID_AUTH_ISM: - return this.deriveOpStackConfig(address); - default: - throw new Error( - `Unsupported HookType: ${OnchainHookType[onchainHookType]}`, - ); + async deriveHookConfig( + address: Address, + ): Promise { + let onchainHookType = undefined; + try { + const hook = IPostDispatchHook__factory.connect(address, this.provider); + this.logger.debug('Deriving HookConfig', { address }); + + // Temporarily turn off SmartProvider logging + // Provider errors are expected because deriving will call methods that may not exist in the Bytecode + this.setSmartProviderLogLevel('silent'); + onchainHookType = await hook.hookType(); + + switch (onchainHookType) { + case OnchainHookType.ROUTING: + return this.deriveDomainRoutingConfig(address); + case OnchainHookType.AGGREGATION: + return this.deriveAggregationConfig(address); + case OnchainHookType.MERKLE_TREE: + return this.deriveMerkleTreeConfig(address); + case OnchainHookType.INTERCHAIN_GAS_PAYMASTER: + return this.deriveIgpConfig(address); + case OnchainHookType.FALLBACK_ROUTING: + return this.deriveFallbackRoutingConfig(address); + case OnchainHookType.PAUSABLE: + return this.derivePausableConfig(address); + case OnchainHookType.PROTOCOL_FEE: + return this.deriveProtocolFeeConfig(address); + // ID_AUTH_ISM could be OPStackHook, ERC5164Hook or LayerZeroV2Hook + // For now assume it's OP_STACK + case OnchainHookType.ID_AUTH_ISM: + return this.deriveOpStackConfig(address); + default: + throw new Error( + `Unsupported HookType: ${OnchainHookType[onchainHookType]}`, + ); + } + } catch (e) { + this.logger.debug( + `Failed to derive ${onchainHookType} hook (${address}): ${e}`, + ); + } finally { + this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger } + return undefined; } async deriveMerkleTreeConfig( @@ -134,10 +153,14 @@ export class EvmHookReader implements HookReader { assert((await hook.hookType()) === OnchainHookType.AGGREGATION); const hooks = await hook.hooks(ethers.constants.AddressZero); - const hookConfigs = await concurrentMap( + const hookConfigs: DerivedHookConfig[] = await concurrentMap( this.concurrency, hooks, - async (hook) => this.deriveHookConfig(hook), + async (hook) => { + const hookConfig = await this.deriveHookConfig(hook); + assert(hookConfig, `No hook config found for ${hook}.`); + return hookConfig; + }, ); return { @@ -295,6 +318,10 @@ export class EvmHookReader implements HookReader { const fallbackHook = await hook.fallbackHook(); const fallbackHookConfig = await this.deriveHookConfig(fallbackHook); + assert( + fallbackHookConfig, + `No fallback hook config found for ${fallbackHook}.`, + ); return { owner, @@ -316,7 +343,9 @@ export class EvmHookReader implements HookReader { try { const domainHook = await hook.hooks(domainId); if (domainHook !== ethers.constants.AddressZero) { - domainHooks[chainName] = await this.deriveHookConfig(domainHook); + const hookConfig = await this.deriveHookConfig(domainHook); + assert(hookConfig, `No hook config found for ${domainHook}.`); + domainHooks[chainName] = hookConfig; } } catch (error) { this.logger.debug( @@ -345,4 +374,16 @@ export class EvmHookReader implements HookReader { type: HookType.PAUSABLE, }; } + + /** + * Conditionally sets the log level for a smart provider. + * + * @param level - The log level to set, e.g. 'debug', 'info', 'warn', 'error'. + */ + protected setSmartProviderLogLevel(level: string) { + if ('setLogLevel' in this.provider) { + //@ts-ignore + this.provider.setLogLevel(level); + } + } }