@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
509 lines • 23.2 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, RateLimitedHook__factory, StaticAggregationHook__factory, StorageGasOracle__factory, } from '@hyperlane-xyz/core';
import { assert, concurrentMap, eqAddress, getLogLevel, isZeroishAddress, objMap, promiseObjAll, rootLogger, } from '@hyperlane-xyz/utils';
import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js';
import { HyperlaneReader } from '../utils/HyperlaneReader.js';
import { isMissingSelectorCallException, throwIfNotMissingSelector, } from '../utils/contract.js';
import { HookType, OnchainHookType, } from './types.js';
function isUnsupportedIgpDomainError(error, domainId) {
// Mirrors InterchainGasPaymaster.getExchangeRateAndGasPrice in
// solidity/contracts/hooks/igp/InterchainGasPaymaster.sol.
return (error instanceof Error &&
error.message.includes(`Configured IGP doesn't support domain ${domainId}`));
}
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;
case OnchainHookType.PREDICATE_ROUTER_WRAPPER:
derivedHookConfig = { type: HookType.PREDICATE, address };
this._cache.set(address, derivedHookConfig);
break;
case OnchainHookType.CCTP:
derivedHookConfig = { type: HookType.CCTP, address };
this._cache.set(address, derivedHookConfig);
break;
case OnchainHookType.RATE_LIMITED:
derivedHookConfig = await this.deriveRateLimitedHookConfig(address);
break;
default:
throw new Error(`Unsupported HookType: ${OnchainHookType[onchainHookType]}`);
}
}
catch (e) {
let customMessage = `Failed to derive ${onchainHookType} hook (${address})`;
if (!onchainHookType && isMissingSelectorCallException(e)) {
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) => {
const derived = await this.deriveHookConfig(hook);
return this.preserveUnredeployable(hook, derived);
}));
if (config.type === HookType.FALLBACK_ROUTING) {
const derived = await this.deriveHookConfig(config.fallback);
config.fallback = this.preserveUnredeployable(config.fallback, derived);
}
break;
case HookType.CCTP:
return config;
case HookType.AGGREGATION:
config.hooks = await Promise.all(config.hooks.map(async (hook) => {
const derived = await this.deriveHookConfig(hook);
return this.preserveUnredeployable(hook, derived);
}));
break;
case HookType.AMOUNT_ROUTING: {
const lowerOrig = config.lowerHook;
const upperOrig = config.upperHook;
const [lowerDerived, upperDerived] = await Promise.all([
this.deriveHookConfig(lowerOrig),
this.deriveHookConfig(upperOrig),
]);
config.lowerHook = this.preserveUnredeployable(lowerOrig, lowerDerived);
config.upperHook = this.preserveUnredeployable(upperOrig, upperDerived);
break;
}
}
return config;
}
// Returns original HookConfig for non-redeployable types (CCTP, PREDICATE) so that
// normalizeConfig — which strips 'address' from all objects — does not discard
// the address. Returns the address as a bare string so it survives normalizeConfig
// and deploy() reaches the string branch intact, regardless of whether the original
// was already a string or an object with an address field.
preserveUnredeployable(original, derived) {
if (derived.type !== HookType.CCTP && derived.type !== HookType.PREDICATE) {
return derived;
}
if (typeof original === 'string')
return original;
return derived.address;
}
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
const ccipHook = CCIPHook__factory.connect(address, this.provider);
try {
// This method only exists on CCIPHook
await ccipHook.ccipDestination();
}
catch (error) {
throwIfNotMissingSelector(error);
// Not a CCIP hook, try OPStack
const opStackHook = OPStackHook__factory.connect(address, this.provider);
try {
// This method only exists on OPStackHook
await opStackHook.l1Messenger();
}
catch (innerError) {
throwIfNotMissingSelector(innerError);
throw new Error(`Could not determine hook type - neither CCIP nor OPStack methods found`);
}
return this.deriveOpStackConfig(address);
}
return this.deriveCcipConfig(address);
}
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 deriveRateLimitedHookConfig(address) {
const hook = RateLimitedHook__factory.connect(address, this.provider);
const [hookType, maxCapacity, owner] = await Promise.all([
hook.hookType(),
hook.maxCapacity(),
hook.owner(),
]);
this.assertHookType(hookType, OnchainHookType.RATE_LIMITED);
const config = {
address,
type: HookType.RATE_LIMITED,
maxCapacity: maxCapacity.toString(),
owner,
};
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);
// Parallelize hookType and hooks list fetching
const [hookType, hooks] = await Promise.all([
hook.hookType(),
hook.hooks(ethers.constants.AddressZero),
]);
this.assertHookType(hookType, OnchainHookType.AGGREGATION);
const hookConfigs = await concurrentMap(this.concurrency, hooks, async (hookAddress) => {
const derived = await this.deriveHookConfigFromAddress(hookAddress);
return this.preserveUnredeployable(hookAddress, derived);
});
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);
// Parallelize initial RPC calls
const [hookType, owner, beneficiary, quoteSigners] = await Promise.all([
hook.hookType(),
hook.owner(),
hook.beneficiary(),
// quoteSigners() not available on IGP versions before offchain fee quoting
hook.quoteSigners().catch((error) => {
throwIfNotMissingSelector(error);
this.logger.debug('quoteSigners() not available on this IGP version, skipping');
return [];
}),
]);
this.assertHookType(hookType, OnchainHookType.INTERCHAIN_GAS_PAYMASTER);
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 (error) {
if (!isUnsupportedIgpDomainError(error, domainId))
throw error;
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,
...(quoteSigners.length > 0 ? { quoteSigners: [...quoteSigners] } : {}),
};
this._cache.set(address, config);
return config;
}
async deriveProtocolFeeConfig(address) {
const hook = ProtocolFee__factory.connect(address, this.provider);
// Parallelize all RPC calls
const [hookType, owner, maxProtocolFee, protocolFee, beneficiary] = await Promise.all([
hook.hookType(),
hook.owner(),
hook.MAX_PROTOCOL_FEE(),
hook.protocolFee(),
hook.beneficiary(),
]);
this.assertHookType(hookType, OnchainHookType.PROTOCOL_FEE);
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);
// Parallelize all RPC calls
const [hookType, owner, messengerContract, destinationDomain] = await Promise.all([
hook.hookType(),
hook.owner(),
hook.l1Messenger(),
hook.destinationDomain(),
]);
this.assertHookType(hookType, OnchainHookType.ID_AUTH_ISM);
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);
// Parallelize initial RPC calls
const [arbSys, destinationDomain, childHookAddress] = await Promise.all([
hook.arbSys(),
hook.destinationDomain(),
hook.childHook(),
]);
const destinationChainName = this.multiProvider.getChainName(destinationDomain);
const derivedChild = await this.deriveHookConfigFromAddress(childHookAddress);
const childHookConfig = this.preserveUnredeployable(childHookAddress, derivedChild);
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);
// Parallelize hookType, owner, and domain hooks fetching
const [hookType, owner, domainHooks] = await Promise.all([
hook.hookType(),
hook.owner(),
this.fetchDomainHooks(hook),
]);
this.assertHookType(hookType, OnchainHookType.ROUTING);
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);
// Parallelize hookType, owner, fallback hook address, and domain hooks fetching
const [hookType, owner, fallbackHookAddress, domainHooks] = await Promise.all([
hook.hookType(),
hook.owner(),
hook.fallbackHook(),
this.fetchDomainHooks(hook),
]);
this.assertHookType(hookType, OnchainHookType.FALLBACK_ROUTING);
const derivedFallback = await this.deriveHookConfigFromAddress(fallbackHookAddress);
const fallbackHookConfig = this.preserveUnredeployable(fallbackHookAddress, derivedFallback);
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);
const domainHook = await hook.hooks(domainId);
if (!isZeroishAddress(domainHook)) {
const derived = await this.deriveHookConfigFromAddress(domainHook);
domainHooks[chainName] = this.preserveUnredeployable(domainHook, derived);
}
});
return domainHooks;
}
async derivePausableConfig(address) {
const hook = PausableHook__factory.connect(address, this.provider);
// Parallelize all RPC calls
const [hookType, owner, paused] = await Promise.all([
hook.hookType(),
hook.owner(),
hook.paused(),
]);
this.assertHookType(hookType, OnchainHookType.PAUSABLE);
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);
// Parallelize initial RPC calls including hookType
const [hookType, threshold, lowerHookAddress, upperHookAddress] = await Promise.all([
hook.hookType(),
hook.threshold(),
hook.lower(),
hook.upper(),
]);
this.assertHookType(hookType, OnchainHookType.AMOUNT_ROUTING);
// Parallelize hook config derivation
const [lowerDerived, upperDerived] = await Promise.all([
this.deriveHookConfigFromAddress(lowerHookAddress),
this.deriveHookConfigFromAddress(upperHookAddress),
]);
const lowerHookConfig = this.preserveUnredeployable(lowerHookAddress, lowerDerived);
const upperHookConfig = this.preserveUnredeployable(upperHookAddress, upperDerived);
const config = {
address,
type: HookType.AMOUNT_ROUTING,
threshold: threshold.toNumber(),
lowerHook: lowerHookConfig,
upperHook: upperHookConfig,
};
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