UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

509 lines 23.2 kB
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