Skip to content

Commit 414b2f2

Browse files
antoinedcAntoine de Chevignéclaude
authored
Add OP Stack integration for L2 chain tracking (#433)
* Add OP Stack integration for L2 chain tracking Implement full OP Stack support to track batches, state roots, deposits, and withdrawals between custom L2 OP Stack chains and L1. Features: - Database models: OpChainConfig, OpBatch, OpOutput, OpDeposit, OpWithdrawal - Event detection for deposits, outputs, withdrawals, and batches - Support for both legacy (L2OutputOracle) and modern (DisputeGameFactory) chains - Background jobs for output finalization and deposit-to-L2 linking - API endpoints for all OP Stack entities - Frontend pages: deposits, withdrawals, batches, outputs with detail views - Transaction lifecycle component showing L2 tx progression - Settings page for OP Stack configuration - Shared isTopL1Parent field for both Orbit and OP Stack parent workspaces 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix code review suggestions for OP Stack integration - Fix unmanagedError(error, req, res) → next in explorers.js - Use SHA-256 instead of keccak256 for blob versioned hashes (EIP-4844) - Fix gameType BigNumber conversion in opOutputs.js - Fix timestamp setter to return Date instead of string - Update deriveL2TransactionHash to include l1BlockNumber - Add onDelete/onUpdate behavior to migration foreign keys - Add DROP TYPE to down migrations for enums - Add l1TransactionHash lowercase normalization setters - Fix JSDoc return type in opDeposits.js - Add sortBy[0].order guard in OpWithdrawals.vue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Address additional code review feedback for OP Stack - Fix computeBlobVersionedHash slicing (handle 0x prefix correctly) - Add node-fetch import for beacon API calls - Add try/catch error handling to all parseLog functions - Add lowercase normalization setters for all address fields in OpChainConfig - Fix safeUpdate to only validate parentChainId when provided 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Address code review suggestions for OP Stack - Fix race condition in batch index assignment using transaction with lock - Add input validation and sanitization for pagination parameters - Add named constants for EIP-4844 versioned hash calculations - Add 404 handling when OP entities are not found - Add optional chaining for template property access in Vue components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Add OP Stack parent workspace and UI improvements - Add getAvailableOpParents API endpoint for L1 workspace selection - Add parent workspace dropdown to ExplorerOpSettings component - Include opConfig in workspace queries for proper data flow - Add parentChainExplorer and parentWorkspaceId to explorer responses - Update CLAUDE.md with OP Stack testing documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Add OP Stack L1 transaction filtering in blockSync - Filter L1 transactions to only OP-relevant contracts when workspace has opChildConfigs (similar to existing Orbit filtering) - Include opChildConfigs in initial workspace query to avoid extra DB call - Reuse loaded opChildConfigs for batch detection instead of re-querying Contracts filtered: batchInboxAddress, optimismPortalAddress, l2OutputOracleAddress, disputeGameFactoryAddress, systemConfigAddress 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Implement OP Stack batch parsing for L2 block range extraction - Add full batch data parsing with zlib/brotli decompression - Parse span batch and singular batch formats per OP Stack derivation spec - Extract L2 block count and calculate block range from timestamps - Add finalizePendingOpBatches job to confirm batches on safe block - Add l2BlockTime field to OpChainConfig model - Fix finalizePendingOrbitBatches to include orbitChildConfigs association - Fix hex-to-integer conversion for l1BlockNumber in batch creation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Refactor OP Stack code based on PR review - Extract sanitizePagination to shared utility in lib/utils.js - Remove duplicate sanitizePagination from OP API files - Move timestamp conversion outside loop in blockSync.js - Add named constants for magic numbers in opBatches.js - Add logging to batch parsing error handlers - Move requires to top of opBatches.js to avoid potential circular deps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * 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]> * Fix code review issues for OP Stack integration - Clean up event signatures: remove dead code, use single computed source - Lower default backfill batch size from 10000 to 10 for RPC compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Refactor OpBatchDetail to match OrbitBatchDetail structure - Add tabbed interface with Overview and Transactions tabs - Create OpBatchOverview component with v-list layout - Create OpBatchTransactions wrapper component - Add skeleton loader for loading state - Support URL hash navigation between tabs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Add Blocks tab to OP batch detail page - Add getOpBatchBlocks function to firebase.js - Add /opBatches/:batchIndex/blocks API endpoint - Add getOpBatchBlocks method to server.js plugin - Add opBatchIndex prop to BlockList.vue component - Create OpBatchBlocks.vue wrapper component - Add Blocks tab to OpBatchDetail.vue with URL hash support - Change default blob explorer URL from Etherscan to Blockscout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Address code review feedback for OP Stack integration - Remove unused managedError imports from opDeposits.js and opOutputs.js - Add pagination to backfillOpBatchBlockRanges job (chunk size 1000) - Add test for GET /:batchIndex/blocks endpoint - Add unit tests for OpBatchBlocks and OpBatchOverview components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix OP dispute game output creation - Make l2BlockNumber nullable for dispute game outputs (fault proofs don't include L2 block number in DisputeGameCreated event) - Fix priority parameter in receiptSync enqueue calls (use integer 1 instead of string 'high') - Add migration to alter op_outputs.l2BlockNumber column 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Refactor OpOutputDetail to match OpBatchDetail design - Create OpOutputOverview component with v-list layout - Update OpOutputDetail to use tabbed interface with BaseChipGroup - Add parentChainExplorer to getOpOutput API response - Add game type labels for dispute games (Cannon, Permissioned Cannon, etc) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix backend tests * update frontend tests * fix frontend tests --------- Co-authored-by: Antoine de Chevigné <[email protected]> Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent d4b3c8f commit 414b2f2

File tree

103 files changed

+9135
-940
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+9135
-940
lines changed

run/api/explorers.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ const secretMiddleware = require('../middlewares/secret');
5555
* @returns {Promise<object>} - The orbit config
5656
*/
5757

58+
/**
59+
* Get available L1 parent workspaces for OP Stack configuration
60+
* @returns {Promise<object>} - List of available parent workspaces
61+
*/
62+
router.get('/availableOpParents', authMiddleware, async (req, res, next) => {
63+
try {
64+
const availableParents = await db.getAvailableOpParents();
65+
66+
res.status(200).json({ availableParents });
67+
} catch (error) {
68+
return managedError(error, req, res);
69+
}
70+
});
71+
5872
router.get('/:id/orbitConfig', authMiddleware, async (req, res, next) => {
5973
const data = { ...req.query, ...req.params };
6074

@@ -186,6 +200,115 @@ router.post('/:id/orbitConfig', authMiddleware, async (req, res, next) => {
186200
}
187201
});
188202

203+
/**
204+
* Get OP Stack config
205+
* @returns {Promise<object>} - The OP config
206+
*/
207+
router.get('/:id/opConfig', authMiddleware, async (req, res, next) => {
208+
const data = { ...req.query, ...req.params };
209+
210+
try {
211+
const opConfig = await db.getOpConfig(req.body.data.user.id, data.id);
212+
213+
res.status(200).json({ opConfig });
214+
} catch (error) {
215+
return managedError(error, req, res);
216+
}
217+
});
218+
219+
/**
220+
* Update OP Stack config
221+
* @param {object} config - The OP config to update
222+
* @param {string} config.batchInboxAddress - Batch inbox address
223+
* @param {string} config.optimismPortalAddress - Optimism portal address
224+
* @param {string} config.l2OutputOracleAddress - L2 output oracle address (legacy)
225+
* @param {string} config.disputeGameFactoryAddress - Dispute game factory address (modern)
226+
* @param {string} config.systemConfigAddress - System config address
227+
* @param {string} config.l2ToL1MessagePasserAddress - L2 to L1 message passer address
228+
* @param {number} config.outputVersion - Output version (0 = legacy, 1 = fault proofs)
229+
* @param {number} config.submissionInterval - Blocks between output submissions
230+
* @param {number} config.finalizationPeriodSeconds - Challenge period in seconds
231+
* @param {string} config.parentChainExplorer - Parent chain explorer URL
232+
* @returns {Promise<object>} - The updated OP config
233+
*/
234+
router.put('/:id/opConfig', authMiddleware, async (req, res, next) => {
235+
const data = { ...req.query, ...req.params };
236+
237+
try {
238+
const currentConfig = await db.getOpConfig(req.body.data.user.id, data.id);
239+
if (!currentConfig)
240+
return managedError(new Error('There is no OP config for this explorer.'), req, res);
241+
242+
let params = { ...req.body.params };
243+
244+
if (params.config.parentChainId) {
245+
params.config.parentChainId = parseInt(params.config.parentChainId);
246+
}
247+
248+
let config;
249+
try {
250+
config = await db.updateOpConfig(req.body.data.user.id, data.id, params.config);
251+
} catch(error) {
252+
return managedError(error, req, res);
253+
}
254+
255+
res.status(200).json({ config });
256+
} catch (error) {
257+
unmanagedError(error, req, next);
258+
}
259+
});
260+
261+
/**
262+
* Create OP Stack config
263+
* @param {object} config - The OP config to create
264+
* @param {number} config.parentChainId - Parent chain network ID (e.g., 1 for Ethereum mainnet)
265+
* @param {string} config.batchInboxAddress - Batch inbox address
266+
* @param {string} config.optimismPortalAddress - Optimism portal address
267+
* @param {string} config.l2OutputOracleAddress - L2 output oracle address (legacy)
268+
* @param {string} config.disputeGameFactoryAddress - Dispute game factory address (modern)
269+
* @param {string} config.systemConfigAddress - System config address
270+
* @param {string} config.l2ToL1MessagePasserAddress - L2 to L1 message passer address
271+
* @param {number} config.outputVersion - Output version (0 = legacy, 1 = fault proofs)
272+
* @param {number} config.submissionInterval - Blocks between output submissions
273+
* @param {number} config.finalizationPeriodSeconds - Challenge period in seconds
274+
* @param {string} config.parentChainExplorer - Parent chain explorer URL
275+
* @returns {Promise<object>} - The created OP config
276+
*/
277+
router.post('/:id/opConfig', authMiddleware, async (req, res, next) => {
278+
const data = { ...req.query, ...req.params };
279+
280+
try {
281+
const currentConfig = await db.getOpConfig(req.body.data.user.id, data.id);
282+
if (currentConfig)
283+
return managedError(new Error('There is already an OP config for this explorer.'), req, res);
284+
285+
if (!req.body.params.config.parentWorkspaceId && !req.body.params.config.parentChainId)
286+
return managedError(new Error('Parent workspace is required.'), req, res);
287+
288+
if (!req.body.params.config.batchInboxAddress)
289+
return managedError(new Error('Batch inbox address is required.'), req, res);
290+
291+
if (!req.body.params.config.optimismPortalAddress)
292+
return managedError(new Error('Optimism portal address is required.'), req, res);
293+
294+
let configParams = { ...req.body.params.config };
295+
if (configParams.parentChainId) {
296+
configParams.parentChainId = parseInt(configParams.parentChainId);
297+
}
298+
299+
let config;
300+
try {
301+
config = await db.createOpConfig(req.body.data.user.id, data.id, configParams);
302+
} catch(error) {
303+
return managedError(error, req, res);
304+
}
305+
306+
res.status(200).json({ config });
307+
} catch (error) {
308+
unmanagedError(error, req, next);
309+
}
310+
});
311+
189312
router.post('/:id/v2_dexes', authMiddleware, async (req, res, next) => {
190313
const data = req.body.data;
191314

run/api/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const caddy = require('./caddy');
3232
const orbitBatches = require('./orbitBatches');
3333
const orbitWithdrawals = require('./orbitWithdrawals');
3434
const orbitDeposits = require('./orbitDeposits');
35+
const opBatches = require('./opBatches');
36+
const opOutputs = require('./opOutputs');
37+
const opDeposits = require('./opDeposits');
38+
const opWithdrawals = require('./opWithdrawals');
3539

3640
router.use('/blocks', blocks);
3741
router.use('/contracts', contracts);
@@ -58,6 +62,10 @@ router.use('/caddy', caddy);
5862
router.use('/orbitBatches', orbitBatches);
5963
router.use('/orbitWithdrawals', orbitWithdrawals);
6064
router.use('/orbitDeposits', orbitDeposits);
65+
router.use('/opBatches', opBatches);
66+
router.use('/opOutputs', opOutputs);
67+
router.use('/opDeposits', opDeposits);
68+
router.use('/opWithdrawals', opWithdrawals);
6169

6270
if (isDemoEnabled()) {
6371
const demo = require('./demo');

run/api/opBatches.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const workspaceAuthMiddleware = require('../middlewares/workspaceAuth');
4+
const db = require('../lib/firebase');
5+
const { unmanagedError, managedError } = require('../lib/errors');
6+
const { sanitizePagination } = require('../lib/utils');
7+
8+
/**
9+
* Get paginated list of OP batches for a workspace
10+
* @param {number} page - The page number
11+
* @param {number} itemsPerPage - The number of items per page
12+
* @param {string} order - The order to sort by
13+
* @returns {Promise<Array>} - A list of OP batches
14+
*/
15+
router.get('/', workspaceAuthMiddleware, async (req, res, next) => {
16+
const data = { ...req.query, ...req.params };
17+
18+
try {
19+
const { page, itemsPerPage, order } = sanitizePagination(data.page, data.itemsPerPage, data.order);
20+
const { rows: items, count: total } = await db.getWorkspaceOpBatches(data.workspace.id, page, itemsPerPage, order);
21+
22+
res.status(200).json({ items, total });
23+
} catch (error) {
24+
unmanagedError(error, req, next);
25+
}
26+
});
27+
28+
/**
29+
* Get detailed information for a specific OP batch
30+
* @param {number} batchIndex - The batch index
31+
* @returns {Promise<Object>} - The OP batch
32+
*/
33+
router.get('/:batchIndex', workspaceAuthMiddleware, async (req, res, next) => {
34+
const data = { ...req.query, ...req.params };
35+
36+
try {
37+
const batch = await db.getOpBatch(data.workspace.id, data.batchIndex);
38+
39+
if (!batch) {
40+
return res.status(404).json({ error: 'Batch not found' });
41+
}
42+
43+
res.status(200).json(batch);
44+
} catch (error) {
45+
unmanagedError(error, req, next);
46+
}
47+
});
48+
49+
/**
50+
* Get paginated list of L2 transactions for a specific OP batch
51+
* @param {number} batchIndex - The batch index
52+
* @param {number} page - The page number
53+
* @param {number} itemsPerPage - The number of items per page
54+
* @param {string} order - The order to sort by
55+
* @returns {Promise<Array>} - A list of L2 transactions in the batch
56+
*/
57+
router.get('/:batchIndex/transactions', workspaceAuthMiddleware, async (req, res, next) => {
58+
const data = { ...req.query, ...req.params };
59+
60+
try {
61+
const { page, itemsPerPage, order } = sanitizePagination(data.page, data.itemsPerPage, data.order);
62+
const { total, items } = await db.getOpBatchTransactions(data.workspace.id, data.batchIndex, page, itemsPerPage, order);
63+
64+
res.status(200).json({ total, items });
65+
} catch (error) {
66+
unmanagedError(error, req, next);
67+
}
68+
});
69+
70+
/**
71+
* Get paginated list of L2 blocks for a specific OP batch
72+
* @param {number} batchIndex - The batch index
73+
* @param {number} page - The page number
74+
* @param {number} itemsPerPage - The number of items per page
75+
* @param {string} order - The order to sort by
76+
* @returns {Promise<Array>} - A list of L2 blocks in the batch
77+
*/
78+
router.get('/:batchIndex/blocks', workspaceAuthMiddleware, async (req, res, next) => {
79+
const data = { ...req.query, ...req.params };
80+
81+
try {
82+
const { page, itemsPerPage, order } = sanitizePagination(data.page, data.itemsPerPage, data.order);
83+
const { total, items } = await db.getOpBatchBlocks(data.workspace.id, data.batchIndex, page, itemsPerPage, order);
84+
85+
res.status(200).json({ total, items });
86+
} catch (error) {
87+
unmanagedError(error, req, next);
88+
}
89+
});
90+
91+
module.exports = router;

run/api/opDeposits.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const workspaceAuthMiddleware = require('../middlewares/workspaceAuth');
4+
const db = require('../lib/firebase');
5+
const { unmanagedError } = require('../lib/errors');
6+
const { sanitizePagination } = require('../lib/utils');
7+
8+
/**
9+
* Get paginated list of OP deposits for a workspace
10+
* @param {number} page - The page number
11+
* @param {number} itemsPerPage - The number of items per page
12+
* @param {string} order - The order to sort by
13+
* @returns {Promise<Object>} - An object containing items (array of deposits) and total count
14+
*/
15+
router.get('/', workspaceAuthMiddleware, async (req, res, next) => {
16+
const data = { ...req.query, ...req.params };
17+
18+
try {
19+
const { page, itemsPerPage, order } = sanitizePagination(data.page, data.itemsPerPage, data.order);
20+
const { rows: items, count: total } = await db.getWorkspaceOpDeposits(data.workspace.id, page, itemsPerPage, order);
21+
22+
res.status(200).json({ items, total });
23+
} catch (error) {
24+
unmanagedError(error, req, next);
25+
}
26+
});
27+
28+
/**
29+
* Get a specific OP deposit by L1 transaction hash
30+
* @param {string} hash - The L1 transaction hash
31+
* @returns {Promise<Object>} - The OP deposit
32+
*/
33+
router.get('/:hash', workspaceAuthMiddleware, async (req, res, next) => {
34+
const data = { ...req.query, ...req.params };
35+
36+
try {
37+
const deposit = await db.getOpDepositByL1Hash(data.workspace.id, data.hash);
38+
39+
if (!deposit) {
40+
return res.status(404).json({ error: 'Deposit not found' });
41+
}
42+
43+
res.status(200).json(deposit);
44+
} catch (error) {
45+
unmanagedError(error, req, next);
46+
}
47+
});
48+
49+
module.exports = router;

run/api/opOutputs.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const workspaceAuthMiddleware = require('../middlewares/workspaceAuth');
4+
const db = require('../lib/firebase');
5+
const { unmanagedError } = require('../lib/errors');
6+
const { sanitizePagination } = require('../lib/utils');
7+
8+
/**
9+
* Get paginated list of OP outputs for a workspace
10+
* @param {number} page - The page number
11+
* @param {number} itemsPerPage - The number of items per page
12+
* @param {string} order - The order to sort by
13+
* @returns {Promise<Array>} - A list of OP outputs
14+
*/
15+
router.get('/', workspaceAuthMiddleware, async (req, res, next) => {
16+
const data = { ...req.query, ...req.params };
17+
18+
try {
19+
const { page, itemsPerPage, order } = sanitizePagination(data.page, data.itemsPerPage, data.order);
20+
const { rows: items, count: total } = await db.getWorkspaceOpOutputs(data.workspace.id, page, itemsPerPage, order);
21+
22+
res.status(200).json({ items, total });
23+
} catch (error) {
24+
unmanagedError(error, req, next);
25+
}
26+
});
27+
28+
/**
29+
* Get detailed information for a specific OP output
30+
* @param {number} outputIndex - The output index
31+
* @returns {Promise<Object>} - The OP output
32+
*/
33+
router.get('/:outputIndex', workspaceAuthMiddleware, async (req, res, next) => {
34+
const data = { ...req.query, ...req.params };
35+
36+
try {
37+
const output = await db.getOpOutput(data.workspace.id, data.outputIndex);
38+
39+
if (!output) {
40+
return res.status(404).json({ error: 'Output not found' });
41+
}
42+
43+
res.status(200).json(output);
44+
} catch (error) {
45+
unmanagedError(error, req, next);
46+
}
47+
});
48+
49+
module.exports = router;

0 commit comments

Comments
 (0)