Skip to content

Commit f7ca698

Browse files
Antoine de Chevignéclaude
andcommitted
Add OP Stack batch enhancements and deposit/output tracking
- Add dataContainer field to op_batches (in_blob4844 vs in_calldata) - Add clickable links to parent chain explorer for blob hash, L1 tx, and L1 block - Implement deposit and output event detection in receiptSync - Add storeOpDeposit and storeOpOutput jobs for event processing - Add backfill jobs for historical deposits and outputs - Add opEvents.js library for OP Stack event parsing - Update TransactionsList to support opBatchIndex filtering - Add beaconUrl field to OpChainConfig for blob fetching 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent d961aab commit f7ca698

19 files changed

+1311
-123
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* @fileoverview Backfill job for OP batch block ranges.
3+
* Uses sequential approach like Blockscout - each batch covers a contiguous
4+
* range of L2 blocks. Block ranges are calculated by distributing L2 blocks
5+
* proportionally across batches based on their timestamps.
6+
* @module jobs/backfillOpBatchBlockRanges
7+
*/
8+
9+
const { OpBatch, OpChainConfig, Block } = require('../models');
10+
const { Op } = require('sequelize');
11+
const logger = require('../lib/logger');
12+
13+
module.exports = async job => {
14+
const data = job.data || {};
15+
const { workspaceId } = data;
16+
17+
try {
18+
// Get all OP chain configs (or specific one if workspaceId provided)
19+
const configWhere = workspaceId ? { workspaceId } : {};
20+
const opConfigs = await OpChainConfig.findAll({ where: configWhere });
21+
22+
if (opConfigs.length === 0) {
23+
return 'No OP chain configs found';
24+
}
25+
26+
let totalUpdated = 0;
27+
let totalFailed = 0;
28+
29+
for (const opConfig of opConfigs) {
30+
try {
31+
// Get all batches for this workspace, ordered by batchIndex
32+
const batches = await OpBatch.findAll({
33+
where: { workspaceId: opConfig.workspaceId },
34+
order: [['batchIndex', 'ASC']]
35+
});
36+
37+
if (batches.length === 0) {
38+
logger.info(`No batches found for workspace ${opConfig.workspaceId}`, { location: 'jobs.backfillOpBatchBlockRanges' });
39+
continue;
40+
}
41+
42+
// Get the latest L2 block for this workspace
43+
const latestL2Block = await Block.findOne({
44+
where: { workspaceId: opConfig.workspaceId },
45+
order: [['number', 'DESC']],
46+
attributes: ['number']
47+
});
48+
49+
if (!latestL2Block) {
50+
logger.info(`No L2 blocks found for workspace ${opConfig.workspaceId}`, { location: 'jobs.backfillOpBatchBlockRanges' });
51+
continue;
52+
}
53+
54+
const latestBlockNumber = latestL2Block.number;
55+
const batchCount = batches.length;
56+
57+
logger.info(`Backfilling ${batchCount} batches for workspace ${opConfig.workspaceId}, latest L2 block: ${latestBlockNumber}`, { location: 'jobs.backfillOpBatchBlockRanges' });
58+
59+
// Calculate block ranges for each batch
60+
// Approach: Distribute blocks proportionally across batches
61+
// Each batch covers: (latestBlockNumber + 1) / batchCount blocks on average
62+
const avgBlocksPerBatch = Math.ceil((latestBlockNumber + 1) / batchCount);
63+
64+
let currentBlockStart = 0;
65+
let updated = 0;
66+
67+
for (let i = 0; i < batches.length; i++) {
68+
const batch = batches[i];
69+
70+
// Skip if already has block range
71+
if (batch.l2BlockStart !== null && batch.l2BlockEnd !== null) {
72+
currentBlockStart = batch.l2BlockEnd + 1;
73+
continue;
74+
}
75+
76+
// Calculate end block for this batch
77+
let l2BlockEnd;
78+
if (i === batches.length - 1) {
79+
// Last batch gets all remaining blocks
80+
l2BlockEnd = latestBlockNumber;
81+
} else {
82+
// Use average, but make sure we don't exceed the latest block
83+
l2BlockEnd = Math.min(currentBlockStart + avgBlocksPerBatch - 1, latestBlockNumber);
84+
}
85+
86+
// Ensure valid range
87+
if (currentBlockStart > latestBlockNumber) {
88+
logger.warn(`Batch ${batch.batchIndex} start block ${currentBlockStart} exceeds latest L2 block ${latestBlockNumber}`, { location: 'jobs.backfillOpBatchBlockRanges' });
89+
totalFailed++;
90+
continue;
91+
}
92+
93+
const txCount = l2BlockEnd - currentBlockStart + 1;
94+
95+
await batch.update({
96+
l2BlockStart: currentBlockStart,
97+
l2BlockEnd: l2BlockEnd,
98+
txCount: txCount
99+
});
100+
101+
logger.info(`Updated batch ${batch.batchIndex}: blocks ${currentBlockStart}-${l2BlockEnd} (${txCount} blocks)`, { location: 'jobs.backfillOpBatchBlockRanges' });
102+
103+
currentBlockStart = l2BlockEnd + 1;
104+
updated++;
105+
}
106+
107+
totalUpdated += updated;
108+
logger.info(`Workspace ${opConfig.workspaceId}: Updated ${updated} batches`, { location: 'jobs.backfillOpBatchBlockRanges' });
109+
110+
} catch (error) {
111+
logger.error(`Error processing workspace ${opConfig.workspaceId}: ${error.message}`, { location: 'jobs.backfillOpBatchBlockRanges', error });
112+
totalFailed++;
113+
}
114+
}
115+
116+
return `Backfilled ${totalUpdated} batches, ${totalFailed} failed`;
117+
} catch (error) {
118+
logger.error(`Backfill job failed: ${error.message}`, { location: 'jobs.backfillOpBatchBlockRanges', error });
119+
throw error;
120+
}
121+
};

run/jobs/backfillOpDeposits.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @fileoverview Backfill job for OP Stack deposits.
3+
* Fetches TransactionDeposited events directly from L1 RPC.
4+
* @module jobs/backfillOpDeposits
5+
*/
6+
7+
const { OpChainConfig, OpDeposit, Workspace } = require('../models');
8+
const { ProviderConnector } = require('../lib/rpc');
9+
const { parseTransactionDeposited, EVENT_SIGNATURES } = require('../lib/opEvents');
10+
const logger = require('../lib/logger');
11+
12+
module.exports = async job => {
13+
const data = job.data || {};
14+
const { workspaceId, fromBlock, toBlock, batchSize = 10000 } = data;
15+
16+
try {
17+
// Get OP chain config
18+
const configWhere = workspaceId ? { workspaceId } : {};
19+
const opConfigs = await OpChainConfig.findAll({
20+
where: configWhere,
21+
include: [{
22+
model: Workspace,
23+
as: 'parentWorkspace',
24+
attributes: ['id', 'rpcServer']
25+
}]
26+
});
27+
28+
if (opConfigs.length === 0) {
29+
return 'No OP chain configs found';
30+
}
31+
32+
let totalDeposits = 0;
33+
34+
for (const opConfig of opConfigs) {
35+
if (!opConfig.optimismPortalAddress || !opConfig.parentWorkspace) {
36+
logger.info(`Skipping config ${opConfig.id}: missing portal address or parent workspace`, { location: 'jobs.backfillOpDeposits' });
37+
continue;
38+
}
39+
40+
const providerConnector = new ProviderConnector(opConfig.parentWorkspace.rpcServer);
41+
42+
// Get block range
43+
let startBlock = fromBlock;
44+
let endBlock = toBlock;
45+
46+
if (!startBlock) {
47+
// Start from earliest deposit or block 0
48+
const earliestDeposit = await OpDeposit.findOne({
49+
where: { workspaceId: opConfig.workspaceId },
50+
order: [['l1BlockNumber', 'ASC']]
51+
});
52+
startBlock = earliestDeposit ? earliestDeposit.l1BlockNumber : 0;
53+
}
54+
55+
if (!endBlock) {
56+
// End at latest L1 block
57+
try {
58+
const latestBlock = await providerConnector.fetchLatestBlock();
59+
endBlock = latestBlock.number;
60+
} catch (error) {
61+
logger.error(`Failed to get latest block: ${error.message}`, { location: 'jobs.backfillOpDeposits' });
62+
continue;
63+
}
64+
}
65+
66+
logger.info(`Backfilling deposits for workspace ${opConfig.workspaceId} from block ${startBlock} to ${endBlock}`, { location: 'jobs.backfillOpDeposits' });
67+
68+
// Fetch logs in batches
69+
for (let currentBlock = startBlock; currentBlock <= endBlock; currentBlock += batchSize) {
70+
const batchEnd = Math.min(currentBlock + batchSize - 1, endBlock);
71+
72+
try {
73+
const logs = await providerConnector.provider.getLogs({
74+
address: opConfig.optimismPortalAddress,
75+
topics: [EVENT_SIGNATURES.TRANSACTION_DEPOSITED],
76+
fromBlock: currentBlock,
77+
toBlock: batchEnd
78+
});
79+
80+
logger.info(`Found ${logs.length} deposit events in blocks ${currentBlock}-${batchEnd}`, { location: 'jobs.backfillOpDeposits' });
81+
82+
for (const log of logs) {
83+
try {
84+
const depositData = parseTransactionDeposited(log);
85+
86+
// Check if deposit already exists
87+
const existing = await OpDeposit.findOne({
88+
where: {
89+
workspaceId: opConfig.workspaceId,
90+
l1TransactionHash: log.transactionHash.toLowerCase()
91+
}
92+
});
93+
94+
if (existing) continue;
95+
96+
// Get block timestamp
97+
let timestamp = new Date();
98+
try {
99+
const block = await providerConnector.provider.getBlock(log.blockNumber);
100+
if (block && block.timestamp) {
101+
timestamp = new Date(block.timestamp * 1000);
102+
}
103+
} catch (e) {
104+
// Use current time if block fetch fails
105+
}
106+
107+
await OpDeposit.create({
108+
workspaceId: opConfig.workspaceId,
109+
l1BlockNumber: log.blockNumber,
110+
l1TransactionHash: log.transactionHash,
111+
from: depositData.from,
112+
to: depositData.to,
113+
value: depositData.value,
114+
gasLimit: depositData.gasLimit,
115+
data: depositData.data,
116+
isCreation: depositData.isCreation,
117+
timestamp,
118+
status: 'pending'
119+
});
120+
121+
totalDeposits++;
122+
} catch (error) {
123+
logger.error(`Error storing deposit from tx ${log.transactionHash}: ${error.message}`, { location: 'jobs.backfillOpDeposits', error });
124+
}
125+
}
126+
} catch (error) {
127+
logger.error(`Error fetching logs for blocks ${currentBlock}-${batchEnd}: ${error.message}`, { location: 'jobs.backfillOpDeposits', error });
128+
}
129+
}
130+
}
131+
132+
return `Backfilled ${totalDeposits} deposits`;
133+
} catch (error) {
134+
logger.error(`Backfill deposits job failed: ${error.message}`, { location: 'jobs.backfillOpDeposits', error });
135+
throw error;
136+
}
137+
};

0 commit comments

Comments
 (0)