Skip to content

Commit

Permalink
fix(cli): RPC errors while sending test messages (#4055)
Browse files Browse the repository at this point in the history
### 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>
  • Loading branch information
ltyu and nbayindirli authored Jun 26, 2024
1 parent 4dd2651 commit b05ae38
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 38 deletions.
6 changes: 6 additions & 0 deletions .changeset/calm-eels-care.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 0 additions & 2 deletions typescript/cli/src/deploy/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ export async function runCoreDeploy({
chainName: chain,
addresses: deployedAddresses,
});

// @TODO implement writeAgentConfig
}

logGreen('✅ Core contract deployments complete:\n');
Expand Down
5 changes: 4 additions & 1 deletion typescript/sdk/src/core/HyperlaneCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
AddressBytes32,
ProtocolType,
addressToBytes32,
assert,
bytes32ToAddress,
messageId,
objFilter,
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 13 additions & 3 deletions typescript/sdk/src/hook/EvmHookModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Address,
ProtocolType,
addressToBytes32,
assert,
configDeepEquals,
rootLogger,
} from '@hyperlane-xyz/utils';
Expand Down Expand Up @@ -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[]> {
Expand Down
21 changes: 21 additions & 0 deletions typescript/sdk/src/hook/EvmHookReader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
105 changes: 73 additions & 32 deletions typescript/sdk/src/hook/EvmHookReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
assert,
concurrentMap,
eqAddress,
getLogLevel,
rootLogger,
} from '@hyperlane-xyz/utils';

Expand All @@ -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>>;
Expand Down Expand Up @@ -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(
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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);
}
}
}

0 comments on commit b05ae38

Please sign in to comment.