-
-
Save vincfurc/7fd3b88e95d6d7105fac3ca2dca075d5 to your computer and use it in GitHub Desktop.
Arbitrum time to withdrawal calculation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { ethers } from "ethers"; | |
| import dotenv from "dotenv"; | |
| import { fileURLToPath } from "url"; | |
| import { dirname, join } from "path"; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = dirname(__filename); | |
| dotenv.config({ path: join(__dirname, "../.env") }); | |
| // --- Constants --- | |
| const ARBYS_ADDRESS = "0x0000000000000000000000000000000000000064"; | |
| const L1_OUTBOX_ADDRESS = "0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840"; | |
| // --- Event ABIs --- | |
| const L2ToL1Tx_EVENT_ABI = ["event L2ToL1Tx(address caller, address indexed destination, uint256 indexed hash, uint256 indexed position, uint256 arbBlockNum, uint256 ethBlockNum, uint256 timestamp, uint256 callvalue, bytes data)"]; | |
| const SEND_ROOT_UPDATED_EVENT_ABI = ["event SendRootUpdated(bytes32 indexed outputRoot, bytes32 indexed l2BlockHash)"]; | |
| // --- Interfaces for parsing logs --- | |
| const arbSysInterface = new ethers.Interface(L2ToL1Tx_EVENT_ABI); | |
| const outboxInterface = new ethers.Interface(SEND_ROOT_UPDATED_EVENT_ABI); | |
| // Helper function to format seconds into human-readable string | |
| function formatDuration(totalSeconds) { | |
| if (totalSeconds === null || totalSeconds < 0) return "N/A"; | |
| const days = Math.floor(totalSeconds / (3600 * 24)); | |
| const hours = Math.floor((totalSeconds % (3600 * 24)) / 3600); | |
| const minutes = Math.floor((totalSeconds % 3600) / 60); | |
| const seconds = Math.floor(totalSeconds % 60); | |
| return `${days}d ${hours}h ${minutes}m ${seconds}s`; | |
| } | |
| async function main() { | |
| const l2TxHashArg = process.argv[2] || "0x55f2093b852e415c4de49b34c7d9901c5bbcf97950e772dab4ba846e2867c9d6"; | |
| if (!l2TxHashArg || !ethers.isHexString(l2TxHashArg, 32)) { | |
| console.error("Please provide a valid L2 transaction hash"); | |
| process.exit(1); | |
| } | |
| console.log(`Analyzing L2 transaction: ${l2TxHashArg}\n`); | |
| const l2RpcUrl = process.env.ARBITRUM_RPC_URL; | |
| const l1RpcUrl = process.env.ETHEREUM_RPC_URL; | |
| if (!l2RpcUrl || !l1RpcUrl) { | |
| console.error("Please set ARBITRUM_RPC_URL and ETHEREUM_RPC_URL in your .env file."); | |
| process.exit(1); | |
| } | |
| const l2Provider = new ethers.JsonRpcProvider(l2RpcUrl); | |
| const l1Provider = new ethers.JsonRpcProvider(l1RpcUrl); | |
| // --- Step 1: Get L2ToL1Tx Event Data --- | |
| console.log("Step 1: Fetching L2ToL1Tx event data..."); | |
| const l2TxReceipt = await l2Provider.getTransactionReceipt(l2TxHashArg); | |
| if (!l2TxReceipt) { | |
| console.error(`Failed to get L2 transaction receipt for ${l2TxHashArg}`); | |
| process.exit(1); | |
| } | |
| let l2ToL1Log; | |
| for (const log of l2TxReceipt.logs) { | |
| if (log.address.toLowerCase() === ARBYS_ADDRESS.toLowerCase()) { | |
| try { | |
| const parsedLog = arbSysInterface.parseLog(log); | |
| if (parsedLog && parsedLog.name === "L2ToL1Tx") { | |
| l2ToL1Log = parsedLog; | |
| break; | |
| } | |
| } catch (e) { | |
| // Not the event we're looking for | |
| } | |
| } | |
| } | |
| if (!l2ToL1Log) { | |
| console.error(`L2ToL1Tx event not found in transaction ${l2TxHashArg}`); | |
| process.exit(1); | |
| } | |
| const l2ArbBlockNum = BigInt(l2ToL1Log.args.arbBlockNum.toString()); | |
| const l2Timestamp = Number(l2ToL1Log.args.timestamp); // Get L2 timestamp for duration calculation | |
| console.log(`L2 Block Number: ${l2ArbBlockNum}`); | |
| console.log(`L2 Time: ${new Date(l2Timestamp * 1000).toUTCString()}`); | |
| // --- Step 2: Search for SendRootUpdated events --- | |
| console.log("\nStep 2: Searching for SendRootUpdated events..."); | |
| const currentL1Block = await l1Provider.getBlockNumber(); | |
| const BATCH_SIZE = 10_000; | |
| const ONE_MONTH_IN_SECONDS = 30 * 24 * 60 * 60; | |
| const BLOCKS_PER_MONTH = Math.floor(ONE_MONTH_IN_SECONDS / 12); // 12-second block time | |
| const startBlock = Math.max(0, currentL1Block - BLOCKS_PER_MONTH); | |
| console.log(`Scanning L1 blocks ${startBlock} to ${currentL1Block} (last month)...`); | |
| let executableEvents = []; // Track all matching events | |
| for (let endBlock = currentL1Block; endBlock > startBlock; endBlock -= BATCH_SIZE) { | |
| const batchStartBlock = Math.max(startBlock, endBlock - BATCH_SIZE); | |
| process.stdout.write(`\rScanning blocks ${batchStartBlock} to ${endBlock}...`); | |
| try { | |
| const sendRootUpdatedEvents = await l1Provider.getLogs({ | |
| address: L1_OUTBOX_ADDRESS, | |
| fromBlock: batchStartBlock, | |
| toBlock: endBlock, | |
| topics: [outboxInterface.getEvent("SendRootUpdated").topicHash] | |
| }); | |
| for (const log of sendRootUpdatedEvents) { | |
| const parsedLog = outboxInterface.parseLog(log); | |
| const l2BlockHashFromEvent = parsedLog.args.l2BlockHash; | |
| // Get the L2 block number for this hash | |
| const l2BlockInfo = await l2Provider.getBlock(l2BlockHashFromEvent); | |
| if (!l2BlockInfo) continue; | |
| const l2BlockNumberFromEvent = BigInt(l2BlockInfo.number); | |
| console.log(`\nFound SendRootUpdated at L1 block ${log.blockNumber}:`); | |
| console.log(`L2 Block Hash: ${l2BlockHashFromEvent}`); | |
| console.log(`L2 Block Number: ${l2BlockNumberFromEvent}`); | |
| // Check if this root covers our withdrawal | |
| if (l2ArbBlockNum <= l2BlockNumberFromEvent) { | |
| const timestamp = (await l1Provider.getBlock(log.blockNumber)).timestamp; | |
| executableEvents.push({ | |
| l1Block: log.blockNumber, | |
| l1Timestamp: timestamp, | |
| l2BlockNumber: l2BlockNumberFromEvent, | |
| outputRoot: parsedLog.args.outputRoot | |
| }); | |
| } | |
| } | |
| } catch (err) { | |
| console.error(`\nError scanning blocks ${batchStartBlock}-${endBlock}: ${err.message}`); | |
| } | |
| } | |
| if (executableEvents.length === 0) { | |
| console.log("\n❌ No SendRootUpdated event found that makes this withdrawal executable yet."); | |
| } else { | |
| // Sort by timestamp to find the earliest one | |
| executableEvents.sort((a, b) => a.l1Timestamp - b.l1Timestamp); | |
| const earliest = executableEvents[0]; | |
| console.log("\n🎉 Found earliest time withdrawal became executable:"); | |
| console.log(`L1 Block: ${earliest.l1Block}`); | |
| console.log(`Time: ${new Date(earliest.l1Timestamp * 1000).toUTCString()}`); | |
| console.log(`Output Root: ${earliest.outputRoot}`); | |
| console.log(`L2 Block Number: ${earliest.l2BlockNumber}`); | |
| // Calculate and display time to withdrawal | |
| const timeToWithdrawal = earliest.l1Timestamp - l2Timestamp; | |
| console.log(`\nTime from L2 withdrawal to L1 executability: ${formatDuration(timeToWithdrawal)}`); | |
| if (executableEvents.length > 1) { | |
| console.log(`\nFound ${executableEvents.length} total SendRootUpdated events covering this withdrawal.`); | |
| } | |
| } | |
| } | |
| main().catch(err => { | |
| console.error("\nUnhandled error:", err); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment