Skip to content

Commit b3ccb04

Browse files
Antoine de Chevignéclaude
andcommitted
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]>
1 parent bc50011 commit b3ccb04

File tree

7 files changed

+122
-7
lines changed

7 files changed

+122
-7
lines changed

run/api/opBatches.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,25 @@ router.get('/:batchIndex/transactions', workspaceAuthMiddleware, async (req, res
6767
}
6868
});
6969

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+
7091
module.exports = router;

run/lib/firebase.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ const getOpBatch = async (workspaceId, batchIndex) => {
546546
});
547547

548548
const result = batch.toJSON();
549-
result.parentChainExplorer = opConfig?.parentChainExplorer || 'https://etherscan.io';
549+
result.parentChainExplorer = opConfig?.parentChainExplorer || 'https://eth.blockscout.com';
550550
return result;
551551
};
552552

@@ -594,6 +594,50 @@ const getOpBatchTransactions = async (workspaceId, batchIndex, page, itemsPerPag
594594
return { total: 0, items: [] };
595595
};
596596

597+
/**
598+
* Retrieves L2 blocks for a specific OP batch
599+
* @param {string} workspaceId - The workspace id
600+
* @param {number} batchIndex - The batch index
601+
* @param {number} page - The page number
602+
* @param {number} itemsPerPage - The number of items per page
603+
* @param {string} order - The order to sort by
604+
* @returns {Promise<Object>} - List of blocks and total count
605+
*/
606+
const getOpBatchBlocks = async (workspaceId, batchIndex, page, itemsPerPage, order) => {
607+
if (!workspaceId || batchIndex === undefined)
608+
throw new Error('Missing parameter');
609+
610+
const batch = await OpBatch.findOne({
611+
where: {
612+
workspaceId,
613+
batchIndex: parseInt(batchIndex)
614+
}
615+
});
616+
617+
if (!batch)
618+
throw new Error('Could not find batch');
619+
620+
// If batch has L2 block range, query blocks in that range
621+
if (batch.l2BlockStart !== null && batch.l2BlockEnd !== null) {
622+
const sanitizedOrder = order === 'ASC' ? 'ASC' : 'DESC';
623+
const result = await Block.findAndCountAll({
624+
where: {
625+
workspaceId,
626+
number: {
627+
[Op.between]: [batch.l2BlockStart, batch.l2BlockEnd]
628+
}
629+
},
630+
order: [['number', sanitizedOrder]],
631+
limit: parseInt(itemsPerPage) || 10,
632+
offset: ((parseInt(page) || 1) - 1) * (parseInt(itemsPerPage) || 10)
633+
});
634+
return { total: result.count, items: result.rows };
635+
}
636+
637+
// Otherwise return empty result (batch blocks not yet linked)
638+
return { total: 0, items: [] };
639+
};
640+
597641
/**
598642
* Retrieves a list of OP outputs for a workspace
599643
* @param {string} workspaceId - The workspace id
@@ -4972,6 +5016,7 @@ module.exports = {
49725016
getWorkspaceOpBatches: getWorkspaceOpBatches,
49735017
getOpBatch: getOpBatch,
49745018
getOpBatchTransactions: getOpBatchTransactions,
5019+
getOpBatchBlocks: getOpBatchBlocks,
49755020
getWorkspaceOpOutputs: getWorkspaceOpOutputs,
49765021
getOpOutput: getOpOutput,
49775022
getWorkspaceOpDeposits: getWorkspaceOpDeposits,

src/components/BlockList.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
*
7575
* @prop {boolean} [dense] - Whether to use compact table styling
7676
* @prop {number} [batchNumber] - Orbit batch number to filter blocks by
77+
* @prop {number} [opBatchIndex] - OP batch index to filter blocks by
7778
*/
7879
<script setup>
7980
import { ref, reactive, onMounted, onBeforeUnmount, inject } from 'vue';
@@ -83,6 +84,10 @@ const props = defineProps({
8384
batchNumber: {
8485
type: Number,
8586
required: false
87+
},
88+
opBatchIndex: {
89+
type: Number,
90+
required: false
8691
}
8792
});
8893
@@ -142,9 +147,14 @@ const getBlocks = ({ page, itemsPerPage, sortBy } = {}) => {
142147
sortBy
143148
});
144149
145-
const fn = props.batchNumber ?
146-
$server.getOrbitBatchBlocks({ batchNumber: props.batchNumber, page, itemsPerPage, orderBy: sortBy[0].key, order: sortBy[0].order }) :
147-
$server.getBlocks({ page, itemsPerPage, orderBy: sortBy[0].key, order: sortBy[0].order });
150+
let fn;
151+
if (props.batchNumber) {
152+
fn = $server.getOrbitBatchBlocks({ batchNumber: props.batchNumber, page, itemsPerPage, orderBy: sortBy[0].key, order: sortBy[0].order });
153+
} else if (props.opBatchIndex !== undefined && props.opBatchIndex !== null) {
154+
fn = $server.getOpBatchBlocks({ batchIndex: props.opBatchIndex, page, itemsPerPage, orderBy: sortBy[0].key, order: sortBy[0].order });
155+
} else {
156+
fn = $server.getBlocks({ page, itemsPerPage, orderBy: sortBy[0].key, order: sortBy[0].order });
157+
}
148158
149159
fn.then(({ data }) => {
150160
blocks.value = data.items;

src/components/OpBatchBlocks.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<template>
2+
<v-card>
3+
<v-card-text>
4+
<BlockList :opBatchIndex="batchIndex" />
5+
</v-card-text>
6+
</v-card>
7+
</template>
8+
9+
<script setup>
10+
import BlockList from './BlockList.vue';
11+
12+
const props = defineProps({
13+
batchIndex: {
14+
type: Number,
15+
required: true
16+
}
17+
});
18+
</script>

src/components/OpBatchDetail.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<template v-else-if="batch && !loading">
1818
<BaseChipGroup v-model="selectedTab" mandatory>
1919
<v-chip label size="small" value="overview">Overview</v-chip>
20+
<v-chip label size="small" value="blocks">Blocks</v-chip>
2021
<v-chip label size="small" value="transactions">Transactions</v-chip>
2122
</BaseChipGroup>
2223

@@ -25,6 +26,11 @@
2526
:batch="batch"
2627
/>
2728

29+
<OpBatchBlocks
30+
v-if="selectedTab === 'blocks'"
31+
:batchIndex="Number(batch.batchIndex)"
32+
/>
33+
2834
<OpBatchTransactions
2935
v-if="selectedTab === 'transactions'"
3036
:batchIndex="Number(batch.batchIndex)"
@@ -45,6 +51,7 @@ import { ref, onMounted, inject, watch } from 'vue';
4551
import { useRouter } from 'vue-router';
4652
import BaseChipGroup from './base/BaseChipGroup.vue';
4753
import OpBatchOverview from './OpBatchOverview.vue';
54+
import OpBatchBlocks from './OpBatchBlocks.vue';
4855
import OpBatchTransactions from './OpBatchTransactions.vue';
4956
5057
const props = defineProps({
@@ -76,7 +83,9 @@ function loadBatch() {
7683
}
7784
7885
const checkUrlHash = () => {
79-
if (window.location.hash === '#transactions') {
86+
if (window.location.hash === '#blocks') {
87+
selectedTab.value = 'blocks';
88+
} else if (window.location.hash === '#transactions') {
8089
selectedTab.value = 'transactions';
8190
} else {
8291
selectedTab.value = 'overview';
@@ -97,7 +106,9 @@ watch(() => selectedTab.value, (newTab) => {
97106
const currentPath = router.currentRoute.value.fullPath.split('#')[0];
98107
let hash = '';
99108
100-
if (newTab === 'transactions') {
109+
if (newTab === 'blocks') {
110+
hash = '#blocks';
111+
} else if (newTab === 'transactions') {
101112
hash = '#transactions';
102113
}
103114

src/components/OpBatchOverview.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ const dataContainerLabels = {
152152
};
153153
154154
const parentChainExplorer = computed(() => {
155-
return props.batch.parentChainExplorer || 'https://etherscan.io';
155+
return props.batch.parentChainExplorer || 'https://eth.blockscout.com';
156156
});
157157
158158
const l1TransactionUrl = computed(() => {

src/plugins/server.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,16 @@ export default {
588588
return axios.get(resource, { params });
589589
},
590590

591+
getOpBatchBlocks(options) {
592+
const params = {
593+
firebaseUserId: firebaseUserId.value,
594+
workspace: workspace.value,
595+
...options
596+
};
597+
const resource = `${envStore.apiRoot}/api/opBatches/${options.batchIndex}/blocks`;
598+
return axios.get(resource, { params });
599+
},
600+
591601
getOpOutputs(options) {
592602
const params = {
593603
firebaseUserId: firebaseUserId.value,

0 commit comments

Comments
 (0)