UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

257 lines 10.5 kB
import { z } from 'zod'; import { assert, bytes32ToAddress, messageId, objMap, objMerge, parseMessage, promiseObjAll, sleep, } from '@hyperlane-xyz/utils'; import { EvmHookReader } from '../hook/EvmHookReader.js'; import { HookConfigSchema } from '../hook/types.js'; import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { BaseMetadataBuilder } from '../ism/metadata/builder.js'; import { IsmConfigSchema } from '../ism/types.js'; import { HyperlaneCore } from './HyperlaneCore.js'; const WithAddressSchema = z.object({ address: z.string(), }); const DerivedHookConfigWithAddressSchema = HookConfigSchema.and(WithAddressSchema); const DerivedIsmConfigWithAddressSchema = IsmConfigSchema.and(WithAddressSchema); const BacklogMessageSchema = z.object({ attempts: z.number(), lastAttempt: z.number(), message: z.string(), dispatchTx: z.string(), }); const MessageBacklogSchema = z.array(BacklogMessageSchema); export const RelayerCacheSchema = z.object({ hook: z.record(z.record(DerivedHookConfigWithAddressSchema)), ism: z.record(z.record(DerivedIsmConfigWithAddressSchema)), backlog: MessageBacklogSchema, }); // message must have origin and destination chains in the whitelist // if whitelist has non-empty address set for chain, message must have sender and recipient in the set export function messageMatchesWhitelist(whitelist, message) { const originAddresses = whitelist[message.originChain ?? message.origin]; if (!originAddresses) { return false; } const sender = bytes32ToAddress(message.sender); if (originAddresses.size !== 0 && !originAddresses.has(sender)) { return false; } const destinationAddresses = whitelist[message.destinationChain ?? message.destination]; if (!destinationAddresses) { return false; } const recipient = bytes32ToAddress(message.recipient); if (destinationAddresses.size !== 0 && !destinationAddresses.has(recipient)) { return false; } return true; } export class HyperlaneRelayer { multiProvider; metadataBuilder; core; retryTimeout; whitelist; backlog; cache; stopRelayingHandler; logger; constructor({ core, caching = true, retryTimeout = 1000, whitelist = undefined, }) { this.core = core; this.retryTimeout = retryTimeout; this.logger = core.logger.child({ module: 'Relayer' }); this.metadataBuilder = new BaseMetadataBuilder(core); this.multiProvider = core.multiProvider; if (whitelist) { this.whitelist = objMap(whitelist, (_chain, addresses) => new Set(addresses)); } this.backlog = []; if (caching) { this.cache = { hook: {}, ism: {}, backlog: [], }; } } async getHookConfig(chain, hook, messageContext) { let config; if (this.cache?.hook[chain]?.[hook]) { config = this.cache.hook[chain][hook]; } else { const evmHookReader = new EvmHookReader(this.multiProvider, chain, undefined, messageContext); config = await evmHookReader.deriveHookConfig(hook); } if (!config) { throw new Error(`Hook config not found for ${hook}`); } if (this.cache) { this.cache.hook[chain] ??= {}; this.cache.hook[chain][hook] = config; } return config; } async getIsmConfig(chain, ism, messageContext) { let config; if (this.cache?.ism[chain]?.[ism]) { config = this.cache.ism[chain][ism]; } else { const evmIsmReader = new EvmIsmReader(this.multiProvider, chain, undefined, messageContext); config = await evmIsmReader.deriveIsmConfig(ism); } if (!config) { throw new Error(`ISM config not found for ${ism}`); } if (this.cache) { this.cache.ism[chain] ??= {}; this.cache.ism[chain][ism] = config; } return config; } async getSenderHookConfig(message) { const originChain = this.core.getOrigin(message); const hook = await this.core.getSenderHookAddress(message); return this.getHookConfig(originChain, hook, message); } async getRecipientIsmConfig(message) { const destinationChain = this.core.getDestination(message); const ism = await this.core.getRecipientIsmAddress(message); return this.getIsmConfig(destinationChain, ism, message); } async relayAll(dispatchTx, messages = HyperlaneCore.getDispatchedMessages(dispatchTx)) { const destinationMap = {}; messages.forEach((message) => { destinationMap[message.parsed.destination] ??= []; destinationMap[message.parsed.destination].push(message); }); // parallelize relaying to different destinations return promiseObjAll(objMap(destinationMap, async (_destination, messages) => { const receipts = []; // serially relay messages to the same destination for (const message of messages) { try { const receipt = await this.relayMessage(dispatchTx, undefined, message); receipts.push(receipt); } catch (e) { this.logger.error(`Failed to relay message ${message.id}, ${e}`); } } return receipts; })); } async relayMessage(dispatchTx, messageIndex = 0, message = HyperlaneCore.getDispatchedMessages(dispatchTx)[messageIndex]) { if (this.whitelist) { // add human readable names for use in whitelist checks message.parsed = { originChain: this.core.getOrigin(message), destinationChain: this.core.getDestination(message), ...message.parsed, }; assert(messageMatchesWhitelist(this.whitelist, message.parsed), `Message ${message.id} does not match whitelist`); } this.logger.info(`Preparing to relay message ${message.id}`); const isDelivered = await this.core.isDelivered(message); if (isDelivered) { this.logger.info(`Message ${message.id} already delivered`); return this.core.getProcessedReceipt(message); } this.logger.debug({ message }, `Simulating recipient message handling`); await this.core.estimateHandle(message); // parallelizable because configs are on different chains const [ism, hook] = await Promise.all([ this.getRecipientIsmConfig(message), this.getSenderHookConfig(message), ]); this.logger.debug({ ism, hook }, `Retrieved ISM and hook configs`); const metadata = await this.metadataBuilder.build({ message, ism, hook, dispatchTx, }); this.logger.info(`Relaying message ${message.id}`); return this.core.deliver(message, metadata); } hydrate(cache) { assert(this.cache, 'Caching not enabled'); this.cache = objMerge(this.cache, cache); } // fill cache with default ISM and hook configs for quicker relaying (optional) async hydrateDefaults() { assert(this.cache, 'Caching not enabled'); const defaults = await this.core.getDefaults(); await promiseObjAll(objMap(defaults, async (chain, { ism, hook }) => { this.logger.debug(`Hydrating ${chain} cache with default ISM and hook configs`); await this.getHookConfig(chain, hook); await this.getIsmConfig(chain, ism); })); } async flushBacklog() { while (this.stopRelayingHandler) { const backlogMsg = this.backlog.shift(); if (!backlogMsg) { this.logger.trace('Backlog empty, waiting 1s'); await sleep(1000); continue; } // linear backoff (attempts * retryTimeout) if (Date.now() < backlogMsg.lastAttempt + backlogMsg.attempts * this.retryTimeout) { this.backlog.push(backlogMsg); continue; } const { message, dispatchTx, attempts } = backlogMsg; const id = messageId(message); const parsed = parseMessage(message); const dispatchMsg = { id, message, parsed }; try { const dispatchReceipt = await this.multiProvider .getProvider(parsed.origin) .getTransactionReceipt(dispatchTx); // TODO: handle batching await this.relayMessage(dispatchReceipt, undefined, dispatchMsg); } catch { this.logger.error(`Failed to relay message ${id} (attempt #${attempts + 1})`); this.backlog.push({ ...backlogMsg, attempts: attempts + 1, lastAttempt: Date.now(), }); } } } whitelistChains() { return this.whitelist ? Object.keys(this.whitelist) : undefined; } start() { assert(!this.stopRelayingHandler, 'Relayer already started'); this.backlog = this.cache?.backlog ?? []; const { removeHandler } = this.core.onDispatch(async (message, event) => { if (this.whitelist && !messageMatchesWhitelist(this.whitelist, message.parsed)) { this.logger.debug({ message, whitelist: this.whitelist }, `Skipping message ${message.id} not matching whitelist`); return; } this.backlog.push({ attempts: 0, lastAttempt: Date.now(), message: message.message, dispatchTx: event.transactionHash, }); }, this.whitelistChains()); this.stopRelayingHandler = removeHandler; // start flushing backlog void this.flushBacklog(); } stop() { assert(this.stopRelayingHandler, 'Relayer not started'); this.stopRelayingHandler(this.whitelistChains()); this.stopRelayingHandler = undefined; if (this.cache) { this.cache.backlog = this.backlog; } } } //# sourceMappingURL=HyperlaneRelayer.js.map