UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

133 lines 6.85 kB
import { ArbitrumProvider, ChildToParentMessageReader, ChildToParentMessageStatus, } from '@arbitrum/sdk'; import { BigNumber, providers, utils } from 'ethers'; import { AbstractMessageIdAuthorizedIsm__factory, ArbSys__factory, IOutbox__factory, } from '@hyperlane-xyz/core'; import { assert, rootLogger } from '@hyperlane-xyz/utils'; import { findMatchingLogEvents } from '../../utils/logUtils.js'; import { IsmType } from '../types.js'; const ArbSys = ArbSys__factory.createInterface(); export class ArbL2ToL1MetadataBuilder { core; logger; constructor(core, logger = rootLogger.child({ module: 'ArbL2ToL1MetadataBuilder', })) { this.core = core; this.logger = logger; } async build(context) { assert(context.ism.type === IsmType.ARB_L2_TO_L1, 'Invalid ISM type'); this.logger.debug({ context }, 'Building ArbL2ToL1 metadata'); // if the executeTransaction call is already successful, we can call with null metadata const ism = AbstractMessageIdAuthorizedIsm__factory.connect(context.ism.address, this.core.multiProvider.getSigner(context.message.parsed.destination)); const verified = await ism.isVerified(context.message.id); if (verified) { this.logger.debug('Message is already verified, relaying without metadata...'); return '0x'; } // else build the metadata for outbox.executeTransaction call const metadata = await this.buildArbitrumBridgeCalldata(context); return ArbL2ToL1MetadataBuilder.encodeArbL2ToL1Metadata(metadata); } async buildArbitrumBridgeCalldata(context) { const matchingL2TxEvent = findMatchingLogEvents(context.dispatchTx.logs, ArbSys, 'L2ToL1Tx').find((log) => { const calldata = log.args.data; const messageIdHex = context.message.id.slice(2); return calldata && calldata.includes(messageIdHex); }); assert(matchingL2TxEvent, 'No matching L2ToL1Tx event found'); this.logger.debug({ matchingL2TxEvent }, 'Found matching L2ToL1Tx event'); if (matchingL2TxEvent) { const [caller, destination, hash, position, arbBlockNum, ethBlockNum, timestamp, callvalue, data,] = matchingL2TxEvent.args; const l2ToL1TxEvent = { caller, destination, hash, position, arbBlockNum, ethBlockNum, timestamp, callvalue, data, }; const reader = new ChildToParentMessageReader(this.core.multiProvider.getProvider(context.hook.destinationChain), l2ToL1TxEvent); const originChainMetadata = this.core.multiProvider.getChainMetadata(context.message.parsed.origin); if (typeof originChainMetadata.chainId == 'string') { throw new Error(`Invalid chainId for ${originChainMetadata.name}: ${originChainMetadata.chainId}`); } const baseProvider = new providers.JsonRpcProvider(originChainMetadata.rpcUrls[0].http); const arbProvider = new ArbitrumProvider(baseProvider, { name: originChainMetadata.name, chainId: originChainMetadata.chainId, }); const status = await this.getArbitrumBridgeStatus(reader, arbProvider); // need to wait for the challenge period to pass before relaying if (status == ChildToParentMessageStatus.UNCONFIRMED) { const waitingPeriod = await this.getWaitingBlocksUntilReady(reader, arbProvider); throw new Error(`Arbitrum L2ToL1 message isn't ready for relay. Wait ${waitingPeriod} blocks until the challenge period before relaying again.`); } else if (status == ChildToParentMessageStatus.EXECUTED) { throw new Error('Arbitrum L2ToL1 message has already been executed'); } const outboxProof = await this.getArbitrumOutboxProof(reader, arbProvider); const metadata = { ...l2ToL1TxEvent, proof: outboxProof, }; return metadata; } else { throw new Error('Error in building calldata for Arbitrum native bridge call'); } } // waiting period left until the challenge period is over async getWaitingBlocksUntilReady(reader, provider) { const firstBlock = await reader.getFirstExecutableBlock(provider); if (!firstBlock) { throw new Error('No first executable block found'); } const currentBlock = BigNumber.from(await provider.getBlockNumber()); if (currentBlock.gt(firstBlock)) { throw new Error('First executable block is in the past'); } const waitingPeriod = firstBlock.sub(currentBlock); return waitingPeriod; } async getArbitrumBridgeStatus(reader, provider) { return reader.status(provider); } async getArbitrumOutboxProof(reader, provider) { const proof = (await reader.getOutboxProof(provider)) ?? []; if (!proof) { throw new Error('No outbox proof found'); } return 'proof' in proof ? proof.proof : proof; } static decode(metadata, _) { const abiCoder = new utils.AbiCoder(); const outboxInterface = IOutbox__factory.createInterface(); const executeTransactionInputs = outboxInterface.functions['executeTransaction(bytes32[],uint256,address,address,uint256,uint256,uint256,uint256,bytes)'].inputs; const executeTransactionTypes = executeTransactionInputs .map((input) => input.type) .filter((_, index, array) => index !== array.length - 2); // remove callvalue from types (because the ArbL2ToL1Ism doesn't allow it) const decoded = abiCoder.decode(executeTransactionTypes, metadata); return Object.fromEntries(Object.keys({}).map((key, i) => [key, decoded[i]])); } static encodeArbL2ToL1Metadata(metadata) { const abiCoder = new utils.AbiCoder(); const outboxInterface = IOutbox__factory.createInterface(); const executeTransactionInputs = outboxInterface.functions['executeTransaction(bytes32[],uint256,address,address,uint256,uint256,uint256,uint256,bytes)'].inputs; const executeTransactionTypes = executeTransactionInputs.map((input) => input.type); return abiCoder.encode(executeTransactionTypes, [ metadata.proof, metadata.position, metadata.caller, metadata.destination, metadata.arbBlockNum, metadata.ethBlockNum, metadata.timestamp, metadata.callvalue, metadata.data, ]); } } //# sourceMappingURL=arbL2ToL1.js.map