fix(cli): RPC errors while sending test messages (#4055)

### Description
This PR fixes RPC errors when trying to send a test message using the
CLI for older chains that don't have tokenType (e.g., `hyperlane send
message --relay --origin fuji --destination betaop`)

Additional details:
https://discord.com/channels/935678348330434570/1254873902480359435/1255215832959811687

### Backward compatibility
Yes

### Testing
- tested with `hyperlane warp deploy` and then `hyperlane warp send
--relay --warp
$HOME/.hyperlane/deployments/warp_routes/ETH/alfajores-betaop-config.yaml`
- tested `hyperlane send message --relay --origin fuji --destination
betaop`
- tested `hyperlane send message --relay --origin alfrajores
--destination betaop`
- tested `hyperlane send message --relay --origin holesky --destination
betaop`
- tested `hyperlane core read --mailbox
0xEf9F292fcEBC3848bF4bB92a96a04F9ECBb78E59 --chain alfajores `

---------

Co-authored-by: Noah Bayindirli 🥂 <noah@primeprotocol.xyz>
pull/4063/head
Lee 5 months ago committed by GitHub
parent 4dd2651ee9
commit b05ae38ac0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/calm-eels-care.md
  2. 2
      typescript/cli/src/deploy/core.ts
  3. 5
      typescript/sdk/src/core/HyperlaneCore.ts
  4. 16
      typescript/sdk/src/hook/EvmHookModule.ts
  5. 21
      typescript/sdk/src/hook/EvmHookReader.test.ts
  6. 105
      typescript/sdk/src/hook/EvmHookReader.ts

@ -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.

@ -83,8 +83,6 @@ export async function runCoreDeploy({
chainName: chain,
addresses: deployedAddresses,
});
// @TODO implement writeAgentConfig
}
logGreen('✅ Core contract deployments complete:\n');

@ -8,6 +8,7 @@ import {
AddressBytes32,
ProtocolType,
addressToBytes32,
assert,
bytes32ToAddress,
messageId,
objFilter,
@ -116,7 +117,9 @@ export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
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(

@ -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<HookConfig> {
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<AnnotatedEV5Transaction[]> {

@ -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:

@ -20,6 +20,7 @@ import {
assert,
concurrentMap,
eqAddress,
getLogLevel,
rootLogger,
} from '@hyperlane-xyz/utils';
@ -45,7 +46,9 @@ import {
export type DerivedHookConfig = WithAddress<Exclude<HookConfig, Address>>;
export interface HookReader {
deriveHookConfig(address: Address): Promise<WithAddress<HookConfig>>;
deriveHookConfig(
address: Address,
): Promise<WithAddress<HookConfig> | undefined>;
deriveMerkleTreeConfig(
address: Address,
): Promise<WithAddress<MerkleTreeHookConfig>>;
@ -84,35 +87,51 @@ export class EvmHookReader implements HookReader {
this.provider = multiProvider.getProvider(chain);
}
async deriveHookConfig(address: Address): Promise<DerivedHookConfig> {
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<DerivedHookConfig | undefined> {
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);
}
}
}

Loading…
Cancel
Save