@tokamak-network/thanos-sdk
Version:
Tools for working with Thanos
549 lines • 25.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Portals = void 0;
const abstract_provider_1 = require("@ethersproject/abstract-provider");
const ethers_1 = require("ethers");
const core_utils_1 = require("@tokamak-network/core-utils");
const semver_1 = __importDefault(require("semver"));
const interfaces_1 = require("./interfaces");
const utils_1 = require("./utils");
class Portals {
constructor(opts) {
var _a;
this._outputCache = [];
this.populateTransaction = {
depositTransaction: async (request) => {
return this.contracts.l1.OptimismPortal.populateTransaction.depositTransaction(request.to, request.mint, request.value, request.gasLimit, request.isCreation, request.data);
},
initiateWithdrawal: async (request) => {
return this.contracts.l2.BedrockMessagePasser.populateTransaction.initiateWithdrawal(request.target, request.gasLimit, request.data, {
value: request.value,
});
},
proveWithdrawalTransaction: async (message, opts) => {
const proof = await this.getBedrockMessageProof(message);
const args = [
[
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message,
],
proof.l2OutputIndex,
[
proof.outputRootProof.version,
proof.outputRootProof.stateRoot,
proof.outputRootProof.messagePasserStorageRoot,
proof.outputRootProof.latestBlockhash,
],
proof.withdrawalProof,
(opts === null || opts === void 0 ? void 0 : opts.overrides) || {},
];
return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction(...args);
},
finalizeWithdrawalTransaction: async (message) => {
return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction([
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message,
]);
},
};
this.estimateGas = {
depositTransaction: async (request) => {
const tx = await this.populateTransaction.depositTransaction(request);
return this.l1Provider.estimateGas(tx);
},
initiateWithdrawal: async (request) => {
const tx = await this.populateTransaction.initiateWithdrawal(request);
return this.l2Provider.estimateGas(tx);
},
proveWithdrawalTransaction: async (message, opts) => {
const tx = await this.populateTransaction.proveWithdrawalTransaction(message, opts);
return this.l1Provider.estimateGas(tx);
},
finalizeWithdrawalTransaction: async (message) => {
const tx = await this.populateTransaction.finalizeWithdrawalTransaction(message);
return this.l1Provider.estimateGas(tx);
},
};
this.bedrock = (_a = opts.bedrock) !== null && _a !== void 0 ? _a : true;
this.l1SignerOrProvider = (0, utils_1.toSignerOrProvider)(opts.l1SignerOrProvider);
this.l2SignerOrProvider = (0, utils_1.toSignerOrProvider)(opts.l2SignerOrProvider);
try {
this.l1ChainId = (0, utils_1.toNumber)(opts.l1ChainId);
}
catch (err) {
throw new Error(`L1 chain ID is missing or invalid: ${opts.l1ChainId}`);
}
try {
this.l2ChainId = (0, utils_1.toNumber)(opts.l2ChainId);
}
catch (err) {
throw new Error(`L2 chain ID is missing or invalid: ${opts.l2ChainId}`);
}
this.depositConfirmationBlocks =
(opts === null || opts === void 0 ? void 0 : opts.depositConfirmationBlocks) !== undefined
? (0, utils_1.toNumber)(opts.depositConfirmationBlocks)
: utils_1.DEPOSIT_CONFIRMATION_BLOCKS[this.l2ChainId] || 0;
this.l1BlockTimeSeconds =
(opts === null || opts === void 0 ? void 0 : opts.l1BlockTimeSeconds) !== undefined
? (0, utils_1.toNumber)(opts.l1BlockTimeSeconds)
: utils_1.CHAIN_BLOCK_TIMES[this.l1ChainId] || 1;
this.contracts = (0, utils_1.getPortalsContracts)(this.l2ChainId, {
l1SignerOrProvider: this.l1SignerOrProvider,
l2SignerOrProvider: this.l2SignerOrProvider,
overrides: opts.contracts,
});
}
get l1Provider() {
if (abstract_provider_1.Provider.isProvider(this.l1SignerOrProvider)) {
return this.l1SignerOrProvider;
}
else {
return this.l1SignerOrProvider.provider;
}
}
get l2Provider() {
if (abstract_provider_1.Provider.isProvider(this.l2SignerOrProvider)) {
return this.l2SignerOrProvider;
}
else {
return this.l2SignerOrProvider.provider;
}
}
get l1Signer() {
if (abstract_provider_1.Provider.isProvider(this.l1SignerOrProvider)) {
throw new Error(`messenger has no L1 signer`);
}
else {
return this.l1SignerOrProvider;
}
}
get l2Signer() {
if (abstract_provider_1.Provider.isProvider(this.l2SignerOrProvider)) {
throw new Error(`messenger has no L2 signer`);
}
else {
return this.l2SignerOrProvider;
}
}
async getL1BlockNumber() {
return this.contracts.l2.OVM_L1BlockNumber.getL1BlockNumber();
}
async getMessageStatus(txReceipt) {
return txReceipt.to === core_utils_1.predeploys.L2ToL1MessagePasser
? this.getL2ToL1MessageStatusByReceipt(txReceipt)
: this.getL1ToL2MessageStatusByReceipt(txReceipt);
}
async getL1ToL2MessageStatusByReceipt(txReceipt) {
const l1BlockNumber = await this.getL1BlockNumber();
return txReceipt.blockNumber > l1BlockNumber
? interfaces_1.MessageStatus.UNCONFIRMED_L1_TO_L2_MESSAGE
: interfaces_1.MessageStatus.RELAYED;
}
async getStateBatchAppendedEventByBatchIndex(batchIndex) {
const events = await this.contracts.l1.StateCommitmentChain.queryFilter(this.contracts.l1.StateCommitmentChain.filters.StateBatchAppended(batchIndex));
if (events.length === 0) {
return null;
}
else if (events.length > 1) {
throw new Error(`found more than one StateBatchAppended event`);
}
else {
return events[0];
}
}
async getStateBatchAppendedEventByTransactionIndex(transactionIndex) {
const isEventHi = (event, index) => {
const prevTotalElements = event.args._prevTotalElements.toNumber();
return index < prevTotalElements;
};
const isEventLo = (event, index) => {
const prevTotalElements = event.args._prevTotalElements.toNumber();
const batchSize = event.args._batchSize.toNumber();
return index >= prevTotalElements + batchSize;
};
const totalBatches = await this.contracts.l1.StateCommitmentChain.getTotalBatches();
if (totalBatches.eq(0)) {
return null;
}
let lowerBound = 0;
let upperBound = totalBatches.toNumber() - 1;
let batchEvent = await this.getStateBatchAppendedEventByBatchIndex(upperBound);
if (batchEvent === null) {
return null;
}
if (isEventLo(batchEvent, transactionIndex)) {
return null;
}
else if (!isEventHi(batchEvent, transactionIndex)) {
return batchEvent;
}
while (lowerBound < upperBound) {
const middleOfBounds = Math.floor((lowerBound + upperBound) / 2);
batchEvent = await this.getStateBatchAppendedEventByBatchIndex(middleOfBounds);
if (isEventHi(batchEvent, transactionIndex)) {
upperBound = middleOfBounds;
}
else if (isEventLo(batchEvent, transactionIndex)) {
lowerBound = middleOfBounds;
}
else {
break;
}
}
return batchEvent;
}
async getStateRootBatchByTransactionIndex(transactionIndex) {
const stateBatchAppendedEvent = await this.getStateBatchAppendedEventByTransactionIndex(transactionIndex);
if (stateBatchAppendedEvent === null) {
return null;
}
const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction();
const [stateRoots] = this.contracts.l1.StateCommitmentChain.interface.decodeFunctionData('appendStateBatch', stateBatchTransaction.data);
return {
blockNumber: stateBatchAppendedEvent.blockNumber,
stateRoots,
header: {
batchIndex: stateBatchAppendedEvent.args._batchIndex,
batchRoot: stateBatchAppendedEvent.args._batchRoot,
batchSize: stateBatchAppendedEvent.args._batchSize,
prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements,
extraData: stateBatchAppendedEvent.args._extraData,
},
};
}
async getMessageStateRoot(transactionHash) {
const messageTxReceipt = await this.l2Provider.getTransactionReceipt(transactionHash);
const messageTxIndex = messageTxReceipt.blockNumber - 1;
const stateRootBatch = await this.getStateRootBatchByTransactionIndex(messageTxIndex);
if (stateRootBatch === null) {
return null;
}
const indexInBatch = messageTxIndex - stateRootBatch.header.prevTotalElements.toNumber();
if (stateRootBatch.stateRoots.length <= indexInBatch) {
throw new Error(`state root does not exist in batch`);
}
return {
stateRoot: stateRootBatch.stateRoots[indexInBatch],
stateRootIndexInBatch: indexInBatch,
batch: stateRootBatch,
};
}
async getMessageBedrockOutput(l2BlockNumber) {
let proposal;
let l2OutputIndex;
if (await this.fpac()) {
const gameType = await this.contracts.l1.OptimismPortal2.respectedGameType();
const gameCount = await this.contracts.l1.DisputeGameFactory.gameCount();
const latestGames = await this.contracts.l1.DisputeGameFactory.findLatestGames(gameType, Math.max(0, gameCount.sub(1).toNumber()), Math.min(100, gameCount.toNumber()));
const matches = [];
for (const game of latestGames) {
try {
const [blockNumber] = ethers_1.ethers.utils.defaultAbiCoder.decode(['uint256'], game.extraData);
if (blockNumber.gte(l2BlockNumber)) {
matches.push(Object.assign(Object.assign({}, game), { l2BlockNumber: blockNumber }));
}
}
catch (err) {
continue;
}
}
for (let i = matches.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[matches[i], matches[j]] = [matches[j], matches[i]];
}
let match;
for (const option of matches) {
if (await this.isValidOutputRoot(option.rootClaim, option.l2BlockNumber)) {
match = option;
break;
}
}
if (!match) {
return null;
}
l2OutputIndex = match.index;
proposal = {
outputRoot: match.rootClaim,
timestamp: match.timestamp,
l2BlockNumber: match.l2BlockNumber,
};
}
else {
try {
l2OutputIndex =
await this.contracts.l1.L2OutputOracle.getL2OutputIndexAfter(l2BlockNumber);
}
catch (err) {
if (err.message.includes('L2OutputOracle: cannot get output')) {
return null;
}
else {
throw err;
}
}
proposal = await this.contracts.l1.L2OutputOracle.getL2Output(l2OutputIndex);
}
return {
outputRoot: proposal.outputRoot,
l1Timestamp: proposal.timestamp.toNumber(),
l2BlockNumber: proposal.l2BlockNumber.toNumber(),
l2OutputIndex: l2OutputIndex.toNumber(),
};
}
async getL2ToL1MessageStatusByReceipt(txReceipt) {
const withdrawalMessageInfo = await this.calculateWithdrawalMessage(txReceipt);
if (!withdrawalMessageInfo) {
throw Error('withdrawal message not found');
}
const finalizedMessage = await this.getFinalizedWithdrawalStatus(withdrawalMessageInfo.withdrawalHash);
if (finalizedMessage) {
return interfaces_1.MessageStatus.RELAYED;
}
let timestamp;
if (this.bedrock) {
const output = await this.getMessageBedrockOutput(withdrawalMessageInfo.l2BlockNumber);
if (output === null) {
return interfaces_1.MessageStatus.STATE_ROOT_NOT_PUBLISHED;
}
const provenWithdrawal = await this.getProvenWithdrawal(withdrawalMessageInfo.withdrawalHash);
if (!provenWithdrawal || provenWithdrawal.timestamp.toNumber() === 0) {
return interfaces_1.MessageStatus.READY_TO_PROVE;
}
timestamp = provenWithdrawal.timestamp.toNumber();
}
else {
const stateRoot = await this.getMessageStateRoot(txReceipt.transactionHash);
if (stateRoot === null) {
return interfaces_1.MessageStatus.STATE_ROOT_NOT_PUBLISHED;
}
const bn = stateRoot.batch.blockNumber;
const block = await this.l1Provider.getBlock(bn);
timestamp = block.timestamp;
}
if (await this.fpac()) {
const provenWithdrawal = await this.getProvenWithdrawal(withdrawalMessageInfo.withdrawalHash);
if (provenWithdrawal === null) {
console.warn('Unexpected code path reached in getMessageStatus, returning READY_TO_PROVE');
return interfaces_1.MessageStatus.READY_TO_PROVE;
}
if (!('proofSubmitter' in provenWithdrawal)) {
throw new Error(`expected to get FPAC withdrawal but got legacy withdrawal`);
}
try {
await this.contracts.l1.OptimismPortal2.checkWithdrawal((0, utils_1.hashLowLevelMessage)(withdrawalMessageInfo), provenWithdrawal.proofSubmitter);
return interfaces_1.MessageStatus.READY_FOR_RELAY;
}
catch (err) {
return interfaces_1.MessageStatus.IN_CHALLENGE_PERIOD;
}
}
else {
const challengePeriod = await this.getChallengePeriodSeconds();
const latestBlock = await this.l1Provider.getBlock('latest');
if (timestamp + challengePeriod > latestBlock.timestamp) {
return interfaces_1.MessageStatus.IN_CHALLENGE_PERIOD;
}
else {
return interfaces_1.MessageStatus.READY_FOR_RELAY;
}
}
}
async calculateRelayedDepositTxID(txReceipt) {
if (txReceipt.status !== 1) {
return null;
}
let promiseString = null;
txReceipt.logs.forEach((log) => {
if (log.topics[0] ===
ethers_1.ethers.utils.id('TransactionDeposited(address,address,uint256,bytes)')) {
const depositTx = core_utils_1.DepositTx.fromL1Log(log);
promiseString = Promise.resolve(depositTx.hash());
}
});
return promiseString;
}
async calculateWithdrawalMessage(txReceipt) {
if (txReceipt.status !== 1) {
return null;
}
let promiseMessage = null;
const withdrawalMessage = (0, utils_1.calculateWithdrawalMessageUsingRecept)(txReceipt);
promiseMessage = Promise.resolve(withdrawalMessage);
return promiseMessage;
}
async calculateWithdrawalMessageByL2TxHash(transactionHash) {
const txReceipt = await this.l2Provider.getTransactionReceipt(transactionHash);
return this.calculateWithdrawalMessage(txReceipt);
}
async waitForMessageStatus(txReceipt, status, opts) {
let totalTimeMs = 0;
while (totalTimeMs < ((opts === null || opts === void 0 ? void 0 : opts.timeoutMs) || Infinity)) {
const tick = Date.now();
const currentStatus = await this.getMessageStatus(txReceipt);
if (currentStatus >= status) {
return;
}
await (0, core_utils_1.sleep)((opts === null || opts === void 0 ? void 0 : opts.pollIntervalMs) || 1000);
totalTimeMs += Date.now() - tick;
}
throw new Error(`timed out waiting for relayed deposit transaction`);
}
async getChallengePeriodSeconds() {
if (!this.bedrock) {
return (await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()).toNumber();
}
const oracleVersion = await this.contracts.l1.L2OutputOracle.version();
const challengePeriod = oracleVersion === '1.0.0'
?
ethers_1.BigNumber.from(await this.contracts.l1.OptimismPortal.provider.call({
to: this.contracts.l1.OptimismPortal.address,
data: '0xf4daa291',
}))
: await this.contracts.l1.L2OutputOracle.FINALIZATION_PERIOD_SECONDS();
return challengePeriod.toNumber();
}
async getBedrockMessageProof(withdrawalMessageInfo) {
const output = await this.getMessageBedrockOutput(withdrawalMessageInfo.l2BlockNumber);
if (output === null) {
throw new Error(`state root for message not yet published`);
}
const hash = (0, utils_1.hashLowLevelMessage)(withdrawalMessageInfo);
const messageSlot = (0, utils_1.hashMessageHash)(hash);
const provider = (0, utils_1.toJsonRpcProvider)(this.l2Provider);
const stateTrieProof = await (0, utils_1.makeStateTrieProof)(provider, output.l2BlockNumber, this.contracts.l2.BedrockMessagePasser.address, messageSlot);
const block = await provider.send('eth_getBlockByNumber', [
(0, core_utils_1.toRpcHexString)(output.l2BlockNumber),
false,
]);
return {
outputRootProof: {
version: ethers_1.ethers.constants.HashZero,
stateRoot: block.stateRoot,
messagePasserStorageRoot: stateTrieProof.storageRoot,
latestBlockhash: block.hash,
},
withdrawalProof: stateTrieProof.storageProof,
l2OutputIndex: output.l2OutputIndex,
};
}
async getProvenWithdrawal(withdrawalHash) {
if (!this.bedrock) {
throw new Error('message proving only applies after the bedrock upgrade');
}
if (await this.fpac()) {
const numProofSubmitters = ethers_1.BigNumber.from(await this.contracts.l1.OptimismPortal2.numProofSubmitters(withdrawalHash)).toNumber();
for (let i = 0; i < numProofSubmitters; i++) {
const proofSubmitter = await this.contracts.l1.OptimismPortal2.proofSubmitters(withdrawalHash, i);
const provenWithdrawal = await this.contracts.l1.OptimismPortal2.provenWithdrawals(withdrawalHash, proofSubmitter);
const game = new ethers_1.ethers.Contract(provenWithdrawal.disputeGameProxy, (0, utils_1.getContractInterfaceBedrock)('FaultDisputeGame'), this.l1SignerOrProvider);
const status = await game.status();
if (status === 1) {
continue;
}
else if (status === 2) {
return Object.assign(Object.assign({}, provenWithdrawal), { proofSubmitter });
}
else if (status > 2) {
throw new Error('got invalid game status');
}
const extraData = await game.extraData();
let l2BlockNumber;
try {
;
[l2BlockNumber] = ethers_1.ethers.utils.defaultAbiCoder.decode(['uint256'], extraData);
}
catch (err) {
continue;
}
if (await this.isValidOutputRoot(await game.rootClaim(), l2BlockNumber)) {
return Object.assign(Object.assign({}, provenWithdrawal), { proofSubmitter });
}
}
return null;
}
else {
return this.contracts.l1.OptimismPortal.provenWithdrawals(withdrawalHash);
}
}
async isValidOutputRoot(outputRoot, l2BlockNumber) {
const cached = this._outputCache.find((other) => {
return other.root === outputRoot;
});
if (cached) {
return cached.valid;
}
if (this._outputCache.length > 10000) {
this._outputCache = this._outputCache.slice(5000);
}
try {
const provider = (0, utils_1.toJsonRpcProvider)(this.l2Provider);
const [block, proof] = await Promise.all([
provider.send('eth_getBlockByNumber', [
(0, core_utils_1.toRpcHexString)(l2BlockNumber),
false,
]),
(0, utils_1.makeStateTrieProof)(provider, l2BlockNumber, this.contracts.l2.OVM_L2ToL1MessagePasser.address, ethers_1.ethers.constants.HashZero),
]);
const output = ethers_1.ethers.utils.solidityKeccak256(['bytes32', 'bytes32', 'bytes32', 'bytes32'], [
ethers_1.ethers.constants.HashZero,
block.stateRoot,
proof.storageRoot,
block.hash,
]);
const valid = output === outputRoot;
this._outputCache.push({ root: outputRoot, valid });
return valid;
}
catch (err) {
return false;
}
}
async getFinalizedWithdrawalStatus(withdrawalHash) {
return this.contracts.l1.OptimismPortal.finalizedWithdrawals(withdrawalHash);
}
async depositTransaction(request, opts) {
const tx = await this.populateTransaction.depositTransaction(request);
return ((opts === null || opts === void 0 ? void 0 : opts.signer) || this.l1Signer).sendTransaction(tx);
}
async initiateWithdrawal(request, opts) {
const tx = await this.populateTransaction.initiateWithdrawal(request);
return ((opts === null || opts === void 0 ? void 0 : opts.signer) || this.l2Signer).sendTransaction(tx);
}
async proveWithdrawalTransaction(message, opts) {
const tx = await this.populateTransaction.proveWithdrawalTransaction(message);
return ((opts === null || opts === void 0 ? void 0 : opts.signer) || this.l1Signer).sendTransaction(tx);
}
async proveWithdrawalTransactionUsingL2Tx(transactionHash, opts) {
const message = await this.calculateWithdrawalMessageByL2TxHash(transactionHash);
return this.proveWithdrawalTransaction(message, opts);
}
async finalizeWithdrawalTransaction(message, opts) {
const tx = await this.populateTransaction.finalizeWithdrawalTransaction(message);
return ((opts === null || opts === void 0 ? void 0 : opts.signer) || this.l1Signer).sendTransaction(tx);
}
async finalizeWithdrawalTransactionUsingL2Tx(transactionHash, opts) {
const message = await this.calculateWithdrawalMessageByL2TxHash(transactionHash);
return this.finalizeWithdrawalTransaction(message);
}
async fpac() {
if (this.contracts.l1.OptimismPortal.address === ethers_1.ethers.constants.AddressZero) {
return false;
}
else {
return semver_1.default.gte(await this.contracts.l1.OptimismPortal.version(), '3.0.0');
}
}
}
exports.Portals = Portals;
//# sourceMappingURL=portals.js.map