@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
395 lines • 17.9 kB
JavaScript
import { ethers } from 'ethers';
import { AmountRoutingHook__factory, ArbL2ToL1Hook__factory, CCIPHook__factory, DefaultHook__factory, DomainRoutingHook__factory, FallbackDomainRoutingHook__factory, IPostDispatchHook__factory, InterchainGasPaymaster__factory, MerkleTreeHook__factory, OPStackHook__factory, PausableHook__factory, ProtocolFee__factory, StaticAggregationHook__factory, StorageGasOracle__factory, } from '@hyperlane-xyz/core';
import { assert, concurrentMap, eqAddress, getLogLevel, objMap, promiseObjAll, rootLogger, } from '@hyperlane-xyz/utils';
import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js';
import { HyperlaneReader } from '../utils/HyperlaneReader.js';
import { HookType, OnchainHookType, } from './types.js';
export class EvmHookReader extends HyperlaneReader {
multiProvider;
chain;
concurrency;
messageContext;
logger = rootLogger.child({ module: 'EvmHookReader' });
/**
* HookConfig cache for already retrieved configs. Useful to avoid recomputing configs
* when they have already been retrieved in previous calls where `deriveHookConfig` was called by
* the specific hook methods.
*/
_cache = new Map();
constructor(multiProvider, chain, concurrency = multiProvider.tryGetRpcConcurrency(chain) ?? DEFAULT_CONTRACT_READ_CONCURRENCY, messageContext) {
super(multiProvider, chain);
this.multiProvider = multiProvider;
this.chain = chain;
this.concurrency = concurrency;
this.messageContext = messageContext;
}
async deriveHookConfigFromAddress(address) {
this.logger.debug('Deriving HookConfig:', { address });
const cachedValue = this._cache.get(address);
if (cachedValue) {
this.logger.debug(`Cache hit for HookConfig on chain ${this.chain} at: ${address}`);
return cachedValue;
}
this.logger.debug(`Cache miss for HookConfig on chain ${this.chain} at: ${address}`);
let onchainHookType = undefined;
let derivedHookConfig;
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:
derivedHookConfig = await this.deriveDomainRoutingConfig(address);
break;
case OnchainHookType.AGGREGATION:
derivedHookConfig = await this.deriveAggregationConfig(address);
break;
case OnchainHookType.MERKLE_TREE:
derivedHookConfig = await this.deriveMerkleTreeConfig(address);
break;
case OnchainHookType.INTERCHAIN_GAS_PAYMASTER:
derivedHookConfig = await this.deriveIgpConfig(address);
break;
case OnchainHookType.FALLBACK_ROUTING:
derivedHookConfig = await this.deriveFallbackRoutingConfig(address);
break;
case OnchainHookType.PAUSABLE:
derivedHookConfig = await this.derivePausableConfig(address);
break;
case OnchainHookType.PROTOCOL_FEE:
derivedHookConfig = await this.deriveProtocolFeeConfig(address);
break;
case OnchainHookType.ID_AUTH_ISM:
derivedHookConfig = await this.deriveIdAuthIsmConfig(address);
break;
case OnchainHookType.ARB_L2_TO_L1:
derivedHookConfig = await this.deriveArbL2ToL1Config(address);
break;
case OnchainHookType.AMOUNT_ROUTING:
derivedHookConfig = await this.deriveAmountRoutingHookConfig(address);
break;
case OnchainHookType.MAILBOX_DEFAULT_HOOK:
derivedHookConfig =
await this.deriveMailboxDefaultHookConfig(address);
break;
default:
throw new Error(`Unsupported HookType: ${OnchainHookType[onchainHookType]}`);
}
}
catch (e) {
let customMessage = `Failed to derive ${onchainHookType} hook (${address})`;
if (!onchainHookType &&
e.message.includes('Invalid response from provider')) {
customMessage = customMessage.concat(` [The provided hook contract might be outdated and not support hookType()]`);
this.logger.info(`${customMessage}:\n\t${e}`);
}
else {
this.logger.debug(`${customMessage}:\n\t${e}`);
}
throw new Error(`${customMessage}:\n\t${e}`);
}
finally {
this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger
}
return derivedHookConfig;
}
/**
* Recursively resolves the HookConfigs as addresses, e.g.
* hook:
* type: aggregationHook
* hooks:
* - "0x7937CB2886f01F38210506491A69B0D107Ea0ad9"
* - beneficiary: "0x865BA5789D82F2D4C5595a3968dad729A8C3daE6"
* maxProtocolFee: "100000000000000000000"
* owner: "0x865BA5789D82F2D4C5595a3968dad729A8C3daE6"
* protocolFee: "50000000000000000"
* type: protocolFee
*
* This may throw if the Hook address is not a derivable hook (e.g. Custom Hook)
*/
async deriveHookConfig(config) {
if (typeof config === 'string')
return this.deriveHookConfigFromAddress(config);
// Extend the inner hooks
switch (config.type) {
case HookType.FALLBACK_ROUTING:
case HookType.ROUTING:
config.domains = await promiseObjAll(objMap(config.domains, async (_, hook) => this.deriveHookConfig(hook)));
if (config.type === HookType.FALLBACK_ROUTING)
config.fallback = await this.deriveHookConfig(config.fallback);
break;
case HookType.AGGREGATION:
config.hooks = await Promise.all(config.hooks.map(async (hook) => this.deriveHookConfig(hook)));
break;
case HookType.AMOUNT_ROUTING:
[config.lowerHook, config.upperHook] = await Promise.all([
this.deriveHookConfig(config.lowerHook),
this.deriveHookConfig(config.upperHook),
]);
break;
}
return config;
}
async deriveMailboxDefaultHookConfig(address) {
const hook = DefaultHook__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.MAILBOX_DEFAULT_HOOK);
const config = {
address,
type: HookType.MAILBOX_DEFAULT,
};
this._cache.set(address, config);
return config;
}
async deriveIdAuthIsmConfig(address) {
// First check if it's a CCIP hook
try {
const ccipHook = CCIPHook__factory.connect(address, this.provider);
// This method only exists on CCIPHook
await ccipHook.ccipDestination();
return this.deriveCcipConfig(address);
}
catch {
// Not a CCIP hook, try OPStack
try {
const opStackHook = OPStackHook__factory.connect(address, this.provider);
// This method only exists on OPStackHook
await opStackHook.l1Messenger();
return this.deriveOpStackConfig(address);
}
catch {
throw new Error(`Could not determine hook type - neither CCIP nor OPStack methods found`);
}
}
}
async deriveCcipConfig(address) {
const ccipHook = CCIPHook__factory.connect(address, this.provider);
const destinationDomain = await ccipHook.destinationDomain();
const destinationChain = this.multiProvider.getChainName(destinationDomain);
const config = {
address,
type: HookType.CCIP,
destinationChain,
};
this._cache.set(address, config);
return config;
}
async deriveMerkleTreeConfig(address) {
const hook = MerkleTreeHook__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.MERKLE_TREE);
const config = {
address,
type: HookType.MERKLE_TREE,
};
this._cache.set(address, config);
return config;
}
async deriveAggregationConfig(address) {
const hook = StaticAggregationHook__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.AGGREGATION);
const hooks = await hook.hooks(ethers.constants.AddressZero);
const hookConfigs = await concurrentMap(this.concurrency, hooks, (hook) => this.deriveHookConfig(hook));
const config = {
address,
type: HookType.AGGREGATION,
hooks: hookConfigs,
};
this._cache.set(address, config);
return config;
}
possibleDomainIds() {
const isTestnet = !!this.multiProvider.getChainMetadata(this.chain)
.isTestnet;
return this.messageContext
? [this.messageContext.parsed.destination]
: // filter to only domains that are the same testnet/mainnet
this.multiProvider
.getKnownChainNames()
.filter((chainName) => !!this.multiProvider.getChainMetadata(chainName).isTestnet ===
isTestnet)
.map((chainName) => this.multiProvider.getDomainId(chainName));
}
async deriveIgpConfig(address) {
const hook = InterchainGasPaymaster__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.INTERCHAIN_GAS_PAYMASTER);
const owner = await hook.owner();
const beneficiary = await hook.beneficiary();
const overhead = {};
const oracleConfig = {};
let oracleKey;
const allKeys = await concurrentMap(this.concurrency, this.possibleDomainIds(), async (domainId) => {
const { name: chainName, nativeToken } = this.multiProvider.getChainMetadata(domainId);
try {
const { tokenExchangeRate, gasPrice } = await hook.getExchangeRateAndGasPrice(domainId);
const domainGasOverhead = await hook.destinationGasLimit(domainId, 0);
overhead[chainName] = domainGasOverhead.toNumber();
oracleConfig[chainName] = {
tokenExchangeRate: tokenExchangeRate.toString(),
gasPrice: gasPrice.toString(),
tokenDecimals: nativeToken?.decimals,
};
const { gasOracle } = await hook.destinationGasConfigs(domainId);
const oracle = StorageGasOracle__factory.connect(gasOracle, this.provider);
return oracle.owner();
}
catch {
this.logger.debug('Domain not configured on IGP Hook', domainId, chainName);
return null;
}
});
const resolvedOracleKeys = allKeys.filter((key) => key !== null);
if (resolvedOracleKeys.length > 0) {
const allKeysMatch = resolvedOracleKeys.every((key) => eqAddress(resolvedOracleKeys[0], key));
assert(allKeysMatch, 'Not all oracle keys match');
oracleKey = resolvedOracleKeys[0];
}
const config = {
owner,
address,
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary,
oracleKey: oracleKey ?? owner,
overhead,
oracleConfig,
};
this._cache.set(address, config);
return config;
}
async deriveProtocolFeeConfig(address) {
const hook = ProtocolFee__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.PROTOCOL_FEE);
const owner = await hook.owner();
const maxProtocolFee = await hook.MAX_PROTOCOL_FEE();
const protocolFee = await hook.protocolFee();
const beneficiary = await hook.beneficiary();
const config = {
owner,
address,
type: HookType.PROTOCOL_FEE,
maxProtocolFee: maxProtocolFee.toString(),
protocolFee: protocolFee.toString(),
beneficiary,
};
this._cache.set(address, config);
return config;
}
async deriveOpStackConfig(address) {
const hook = OPStackHook__factory.connect(address, this.provider);
const owner = await hook.owner();
this.assertHookType(await hook.hookType(), OnchainHookType.ID_AUTH_ISM);
const messengerContract = await hook.l1Messenger();
const destinationDomain = await hook.destinationDomain();
const destinationChainName = this.multiProvider.getChainName(destinationDomain);
const config = {
owner,
address,
type: HookType.OP_STACK,
nativeBridge: messengerContract,
destinationChain: destinationChainName,
};
this._cache.set(address, config);
return config;
}
async deriveArbL2ToL1Config(address) {
const hook = ArbL2ToL1Hook__factory.connect(address, this.provider);
const arbSys = await hook.arbSys();
const destinationDomain = await hook.destinationDomain();
const destinationChainName = this.multiProvider.getChainName(destinationDomain);
const childHookAddress = await hook.childHook();
const childHookConfig = await this.deriveHookConfig(childHookAddress);
const config = {
address,
type: HookType.ARB_L2_TO_L1,
destinationChain: destinationChainName,
arbSys,
childHook: childHookConfig,
};
this._cache.set(address, config);
return config;
}
async deriveDomainRoutingConfig(address) {
const hook = DomainRoutingHook__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.ROUTING);
const owner = await hook.owner();
const domainHooks = await this.fetchDomainHooks(hook);
const config = {
owner,
address,
type: HookType.ROUTING,
domains: domainHooks,
};
this._cache.set(address, config);
return config;
}
async deriveFallbackRoutingConfig(address) {
const hook = FallbackDomainRoutingHook__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.FALLBACK_ROUTING);
const owner = await hook.owner();
const domainHooks = await this.fetchDomainHooks(hook);
const fallbackHook = await hook.fallbackHook();
const fallbackHookConfig = await this.deriveHookConfig(fallbackHook);
const config = {
owner,
address,
type: HookType.FALLBACK_ROUTING,
domains: domainHooks,
fallback: fallbackHookConfig,
};
this._cache.set(address, config);
return config;
}
async fetchDomainHooks(hook) {
const domainHooks = {};
await concurrentMap(this.concurrency, this.possibleDomainIds(), async (domainId) => {
const chainName = this.multiProvider.getChainName(domainId);
try {
const domainHook = await hook.hooks(domainId);
if (domainHook !== ethers.constants.AddressZero) {
domainHooks[chainName] = await this.deriveHookConfig(domainHook);
}
}
catch {
this.logger.debug(`Domain not configured on ${hook.constructor.name}`, domainId, chainName);
}
});
return domainHooks;
}
async derivePausableConfig(address) {
const hook = PausableHook__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.PAUSABLE);
const owner = await hook.owner();
const paused = await hook.paused();
const config = {
owner,
address,
paused,
type: HookType.PAUSABLE,
};
this._cache.set(address, config);
return config;
}
async deriveAmountRoutingHookConfig(address) {
const hook = AmountRoutingHook__factory.connect(address, this.provider);
this.assertHookType(await hook.hookType(), OnchainHookType.AMOUNT_ROUTING);
const [threshold, lowerHook, upperHook] = await Promise.all([
hook.threshold(),
hook.lower(),
hook.upper(),
]);
const config = {
address,
type: HookType.AMOUNT_ROUTING,
threshold: threshold.toNumber(),
lowerHook: await this.deriveHookConfig(lowerHook),
upperHook: await this.deriveHookConfig(upperHook),
};
this._cache.set(address, config);
return config;
}
assertHookType(hookType, expectedType) {
assert(hookType === expectedType, `expected hook type to be ${expectedType}, got ${hookType}`);
}
}
//# sourceMappingURL=EvmHookReader.js.map