UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

246 lines 13.1 kB
import { ethers } from 'ethers'; import { IMessageRecipient__factory, MailboxClient__factory, Mailbox__factory, } from '@hyperlane-xyz/core'; import { ProtocolType, addBufferToGasLimit, addressToBytes32, assert, bytes32ToAddress, isZeroishAddress, messageId, objFilter, objMap, parseMessage, pollAsync, promiseObjAll, } from '@hyperlane-xyz/utils'; import { HyperlaneApp } from '../app/HyperlaneApp.js'; import { appFromAddressesMapHelper } from '../contracts/contracts.js'; import { EvmHookReader } from '../hook/EvmHookReader.js'; import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { findMatchingLogEvents } from '../utils/logUtils.js'; import { coreFactories } from './contracts.js'; // If no metadata is provided, ensure we provide a default of 0x0001. // We set to 0x0001 instead of 0x0 to ensure it does not break on zksync. const DEFAULT_METADATA = '0x0001'; export class HyperlaneCore extends HyperlaneApp { static fromAddressesMap(addressesMap, multiProvider) { const helper = appFromAddressesMapHelper(addressesMap, coreFactories, multiProvider); return new HyperlaneCore(helper.contractsMap, helper.multiProvider); } getRouterConfig = (owners) => { // filter for EVM chains const evmContractsMap = objFilter(this.contractsMap, (chainName, _) => this.multiProvider.getProtocol(chainName) === ProtocolType.Ethereum); // get config const config = objMap(evmContractsMap, (chain, contracts) => ({ mailbox: contracts.mailbox.address, owner: typeof owners === 'string' ? owners : owners[chain].owner, ownerOverrides: typeof owners === 'string' ? undefined : owners[chain].ownerOverrides, })); return config; }; quoteGasPayment = (origin, destination, recipient, body, metadata, hook) => { const destinationId = this.multiProvider.getDomainId(destination); return this.contractsMap[origin].mailbox['quoteDispatch(uint32,bytes32,bytes,bytes,address)'](destinationId, recipient, body, metadata || DEFAULT_METADATA, hook || ethers.constants.AddressZero); }; getDestination(message) { return this.multiProvider.getChainName(message.parsed.destination); } getOrigin(message) { return this.multiProvider.getChainName(message.parsed.origin); } async getRecipientIsmAddress(message) { const destinationMailbox = this.contractsMap[this.getDestination(message)]; const ethAddress = bytes32ToAddress(message.parsed.recipient); return destinationMailbox.mailbox.recipientIsm(ethAddress); } async getHookAddress(message) { const destinationMailbox = this.contractsMap[this.getOrigin(message)]; /* TODO: requiredHook() account for here: https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3693 */ return destinationMailbox.mailbox.defaultHook(); } async getRecipientIsmConfig(message) { const destinationChain = this.getDestination(message); const ismReader = new EvmIsmReader(this.multiProvider, destinationChain); const address = await this.getRecipientIsmAddress(message); return ismReader.deriveIsmConfig(address); } async getHookConfig(message) { const originChain = this.getOrigin(message); const hookReader = new EvmHookReader(this.multiProvider, originChain); const address = await this.getHookAddress(message); return hookReader.deriveHookConfig(address); } async sendMessage(origin, destination, recipient, body, hook, metadata) { const mailbox = this.getContracts(origin).mailbox; const destinationDomain = this.multiProvider.getDomainId(destination); const recipientBytes32 = addressToBytes32(recipient); const quote = await this.quoteGasPayment(origin, destination, recipientBytes32, body, metadata, hook); const dispatchParams = [ destinationDomain, recipientBytes32, body, metadata || DEFAULT_METADATA, hook || ethers.constants.AddressZero, ]; const estimateGas = await mailbox.estimateGas['dispatch(uint32,bytes32,bytes,bytes,address)'](...dispatchParams, { value: quote }); const dispatchTx = await this.multiProvider.handleTx(origin, mailbox['dispatch(uint32,bytes32,bytes,bytes,address)'](...dispatchParams, { ...this.multiProvider.getTransactionOverrides(origin), value: quote, gasLimit: addBufferToGasLimit(estimateGas), })); return { dispatchTx, message: this.getDispatchedMessages(dispatchTx)[0], }; } onDispatch(handler, chains = Object.keys(this.contractsMap)) { chains.map((originChain) => { const mailbox = this.contractsMap[originChain].mailbox; this.logger.debug(`Listening for dispatch on ${originChain}`); mailbox.on(mailbox.filters.Dispatch(), (_sender, _destination, _recipient, message, event) => { const dispatched = HyperlaneCore.parseDispatchedMessage(message); // add human readable chain names dispatched.parsed.originChain = this.getOrigin(dispatched); dispatched.parsed.destinationChain = this.getDestination(dispatched); this.logger.info(`Observed message ${dispatched.id} on ${originChain} to ${dispatched.parsed.destinationChain}`); return handler(dispatched, event); }); }); return { removeHandler: (removeChains) => (removeChains ?? chains).map((originChain) => { this.contractsMap[originChain].mailbox.removeAllListeners('Dispatch'); this.logger.debug(`Stopped listening for dispatch on ${originChain}`); }), }; } getDefaults() { return promiseObjAll(objMap(this.contractsMap, async (_, contracts) => ({ ism: await contracts.mailbox.defaultIsm(), hook: await contracts.mailbox.defaultHook(), }))); } getIsm(destinationChain, recipientAddress) { const destinationMailbox = this.contractsMap[destinationChain]; return destinationMailbox.mailbox.recipientIsm(recipientAddress); } getRecipient(message) { return IMessageRecipient__factory.connect(bytes32ToAddress(message.parsed.recipient), this.multiProvider.getProvider(this.getDestination(message))); } async estimateHandle(message) { // This estimation overrides transaction.from which requires a funded signer // on ZkSync-based chains. We catch estimation failures and return '0' to // allow the caller to handle gas estimation differently. try { return (await this.getRecipient(message).estimateGas.handle(message.parsed.origin, message.parsed.sender, message.parsed.body, { from: this.getAddresses(this.getDestination(message)).mailbox })).toString(); } catch (error) { this.logger.debug({ error, destination: this.getDestination(message) }, 'Failed to estimate handle gas, returning 0'); return '0'; } } deliver(message, ismMetadata) { const destinationChain = this.getDestination(message); const txOverrides = this.multiProvider.getTransactionOverrides(destinationChain); return this.multiProvider.handleTx(destinationChain, this.getContracts(destinationChain).mailbox.process(ismMetadata, message.message, { ...txOverrides })); } async getHook(originChain, senderAddress) { const provider = this.multiProvider.getProvider(originChain); try { const client = MailboxClient__factory.connect(senderAddress, provider); const hook = await client.hook(); if (!isZeroishAddress(hook)) { return hook; } } catch (e) { this.logger.debug(`MailboxClient hook not found for ${senderAddress}`); this.logger.trace({ e }); } const originMailbox = this.contractsMap[originChain].mailbox; return originMailbox.defaultHook(); } isDelivered(message) { const destinationChain = this.getDestination(message); return this.getContracts(destinationChain).mailbox.delivered(message.id); } async getSenderHookAddress(message) { const originChain = this.getOrigin(message); const senderAddress = bytes32ToAddress(message.parsed.sender); return this.getHook(originChain, senderAddress); } async getProcessedReceipt(message) { const destinationChain = this.getDestination(message); const mailbox = this.getContracts(destinationChain).mailbox; const processedBlock = await mailbox.processedAt(message.id); const events = await mailbox.queryFilter(mailbox.filters.ProcessId(message.id), processedBlock, processedBlock); assert(events.length === 1, `Expected exactly one process event, got ${events.length}`); const processedEvent = events[0]; return processedEvent.getTransactionReceipt(); } waitForProcessReceipt(message) { const id = messageId(message.message); const destinationChain = this.getDestination(message); const mailbox = this.contractsMap[destinationChain].mailbox; const filter = mailbox.filters.ProcessId(id); return new Promise((resolve, reject) => { mailbox.once(filter, (emittedId, event) => { if (id !== emittedId) { reject(`Expected message id ${id} but got ${emittedId}`); } resolve(this.multiProvider.handleTx(destinationChain, event.getTransaction())); }); }); } async waitForMessageIdProcessed(messageId, destination, delayMs, maxAttempts) { await pollAsync(async () => { this.logger.debug(`Checking if message ${messageId} was processed`); const mailbox = this.contractsMap[destination].mailbox; const delivered = await mailbox.delivered(messageId); if (delivered) { this.logger.info(`Message ${messageId} was processed`); return true; } else { throw new Error(`Message ${messageId} not yet processed`); } }, delayMs, maxAttempts); return true; } waitForMessageProcessing(sourceTx) { const messages = HyperlaneCore.getDispatchedMessages(sourceTx); return Promise.all(messages.map((msg) => this.waitForProcessReceipt(msg))); } // TODO consider renaming this, all the waitForMessage* methods are confusing async waitForMessageProcessed(sourceTx, delay, maxAttempts) { const messages = HyperlaneCore.getDispatchedMessages(sourceTx); await Promise.all(messages.map((msg) => this.waitForMessageIdProcessed(msg.id, this.getDestination(msg), delay, maxAttempts))); this.logger.info(`All messages processed for tx ${sourceTx.transactionHash}`); } // Redundant with static method but keeping for backwards compatibility getDispatchedMessages(sourceTx) { const messages = HyperlaneCore.getDispatchedMessages(sourceTx); return messages.map(({ parsed, ...other }) => { const originChain = this.multiProvider.tryGetChainName(parsed.origin) ?? undefined; const destinationChain = this.multiProvider.tryGetChainName(parsed.destination) ?? undefined; return { parsed: { ...parsed, originChain, destinationChain }, ...other }; }); } async getDispatchTx(originChain, messageId, blockNumber) { const mailbox = this.contractsMap[originChain].mailbox; const filter = mailbox.filters.DispatchId(messageId); const { fromBlock, toBlock } = blockNumber ? { toBlock: blockNumber, fromBlock: blockNumber } : await this.multiProvider.getLatestBlockRange(originChain); const matching = await mailbox.queryFilter(filter, fromBlock, toBlock); if (matching.length === 0) { throw new Error(`No dispatch event found for message ${messageId}`); } assert(matching.length === 1, 'Multiple dispatch events found'); const event = matching[0]; // only 1 event per message ID return event.getTransactionReceipt(); } static parseDispatchedMessage(message) { const parsed = parseMessage(message); const id = messageId(message); return { id, message, parsed }; } static getDispatchedMessages(sourceTx) { const mailbox = Mailbox__factory.createInterface(); const dispatchLogs = findMatchingLogEvents(sourceTx.logs, mailbox, 'Dispatch'); return dispatchLogs.map((log) => { const message = log.args['message']; const parsed = parseMessage(message); const id = messageId(message); return { id, message, parsed }; }); } } //# sourceMappingURL=HyperlaneCore.js.map