@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
133 lines • 6.85 kB
JavaScript
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