Skip to content

Commit eb21b0f

Browse files
Antoine de Chevignéclaude
andcommitted
Simplify OP Stack L1 parent selection to show network names
Instead of exposing internal workspace details (IDs, names), the OP Stack setup now shows user-friendly network names (e.g., "Ethereum Mainnet"). Changes: - Create run/lib/opNetworks.js with hardcoded supported networks - Update GET /availableOpParents to return network list instead of workspaces - Update POST /:id/opConfig to validate networkId and convert to parentChainId - Update ExplorerOpSettings.vue to use network dropdown - Network selection is disabled after config is created (cannot be changed) For now, only Ethereum Mainnet is supported. Additional networks can be added to SUPPORTED_OP_L1_NETWORKS in opNetworks.js. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 901d770 commit eb21b0f

File tree

4 files changed

+124
-49
lines changed

4 files changed

+124
-49
lines changed

run/api/explorers.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const { bulkEnqueue } = require('../lib/queue');
4545
const PM2 = require('../lib/pm2');
4646
const db = require('../lib/firebase');
4747
const { isChainAllowed } = require('../lib/chains');
48+
const { getSupportedOpNetworks, isOpNetworkSupported } = require('../lib/opNetworks');
4849
const authMiddleware = require('../middlewares/auth');
4950
const stripeMiddleware = require('../middlewares/stripe');
5051
const secretMiddleware = require('../middlewares/secret');
@@ -56,14 +57,15 @@ const secretMiddleware = require('../middlewares/secret');
5657
*/
5758

5859
/**
59-
* Get available L1 parent workspaces for OP Stack configuration
60-
* @returns {Promise<object>} - List of available parent workspaces
60+
* Get available L1 networks for OP Stack configuration
61+
* Returns hardcoded list of supported networks (not internal workspace details)
62+
* @returns {Promise<object>} - List of supported networks with networkId, name, explorerUrl
6163
*/
6264
router.get('/availableOpParents', authMiddleware, async (req, res, next) => {
6365
try {
64-
const availableParents = await db.getAvailableOpParents();
66+
const availableNetworks = getSupportedOpNetworks();
6567

66-
res.status(200).json({ availableParents });
68+
res.status(200).json({ availableNetworks });
6769
} catch (error) {
6870
return managedError(error, req, res);
6971
}
@@ -282,19 +284,24 @@ router.post('/:id/opConfig', authMiddleware, async (req, res, next) => {
282284
if (currentConfig)
283285
return managedError(new Error('There is already an OP config for this explorer.'), req, res);
284286

285-
if (!req.body.params.config.parentWorkspaceId && !req.body.params.config.parentChainId)
286-
return managedError(new Error('Parent workspace is required.'), req, res);
287+
// Validate networkId is provided and supported
288+
if (!req.body.params.config.networkId)
289+
return managedError(new Error('Network selection is required.'), req, res);
290+
291+
const networkId = parseInt(req.body.params.config.networkId);
292+
if (!isOpNetworkSupported(networkId))
293+
return managedError(new Error('Selected network is not supported. Only Ethereum Mainnet is currently available.'), req, res);
287294

288295
if (!req.body.params.config.batchInboxAddress)
289296
return managedError(new Error('Batch inbox address is required.'), req, res);
290297

291298
if (!req.body.params.config.optimismPortalAddress)
292299
return managedError(new Error('Optimism portal address is required.'), req, res);
293300

301+
// Convert networkId to parentChainId for model layer
294302
let configParams = { ...req.body.params.config };
295-
if (configParams.parentChainId) {
296-
configParams.parentChainId = parseInt(configParams.parentChainId);
297-
}
303+
configParams.parentChainId = networkId;
304+
delete configParams.networkId;
298305

299306
let config;
300307
try {

run/lib/opNetworks.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @fileoverview Supported L1 networks for OP Stack L2 chains.
3+
* @module lib/opNetworks
4+
*/
5+
6+
/**
7+
* Supported L1 parent chain networks for OP Stack.
8+
* Maps network ID to display configuration.
9+
*
10+
* @constant {Object.<number, {name: string, explorerUrl: string}>}
11+
*/
12+
const SUPPORTED_OP_L1_NETWORKS = {
13+
1: {
14+
name: 'Ethereum Mainnet',
15+
explorerUrl: 'https://etherscan.io'
16+
}
17+
// Future networks can be added here:
18+
// 11155111: { name: 'Sepolia Testnet', explorerUrl: 'https://sepolia.etherscan.io' }
19+
};
20+
21+
/**
22+
* Get list of supported networks for OP Stack L1 parent selection.
23+
* @returns {Array<{networkId: number, name: string, explorerUrl: string}>}
24+
*/
25+
function getSupportedOpNetworks() {
26+
return Object.entries(SUPPORTED_OP_L1_NETWORKS).map(([networkId, config]) => ({
27+
networkId: parseInt(networkId),
28+
name: config.name,
29+
explorerUrl: config.explorerUrl
30+
}));
31+
}
32+
33+
/**
34+
* Check if a network ID is supported as OP Stack L1 parent.
35+
* @param {number} networkId - The network ID to check
36+
* @returns {boolean}
37+
*/
38+
function isOpNetworkSupported(networkId) {
39+
return SUPPORTED_OP_L1_NETWORKS.hasOwnProperty(networkId);
40+
}
41+
42+
/**
43+
* Get network configuration by network ID.
44+
* @param {number} networkId - The network ID
45+
* @returns {Object|null} Network config or null if not supported
46+
*/
47+
function getOpNetworkConfig(networkId) {
48+
return SUPPORTED_OP_L1_NETWORKS[networkId] || null;
49+
}
50+
51+
module.exports = {
52+
SUPPORTED_OP_L1_NETWORKS,
53+
getSupportedOpNetworks,
54+
isOpNetworkSupported,
55+
getOpNetworkConfig
56+
};

src/components/ExplorerOpSettings.vue

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,28 @@
1717
</v-card-title>
1818

1919
<v-card-text>
20-
<v-form v-model="isConfigured" @submit.prevent="saveOrUpdateConfig">
20+
<v-form v-model="isFormValid" @submit.prevent="saveOrUpdateConfig">
2121
<v-row>
2222
<v-col cols="12">
2323
<v-alert class="mb-3" variant="tonal" type="info" density="compact">
2424
Configure the L1 parent chain details. This workspace should be your L2 OP Stack chain,
25-
and you need to specify the L1 parent chain contracts and RPC.
25+
and you need to specify the L1 parent chain contracts.
2626
</v-alert>
2727
</v-col>
2828

2929
<v-col cols="12" md="6">
3030
<v-select
31-
v-model="config.parentWorkspaceId"
32-
label="L1 Parent Workspace (required)"
33-
:items="parentOptions"
34-
item-title="displayName"
35-
item-value="id"
36-
:rules="parentWorkspaceRules"
37-
:disabled="loading || loadingParents"
38-
:loading="loadingParents"
39-
hint="Select the L1 chain workspace"
31+
v-model="config.networkId"
32+
label="L1 Parent Network (required)"
33+
:items="availableNetworks"
34+
item-title="name"
35+
item-value="networkId"
36+
:rules="networkRules"
37+
:disabled="loading || loadingNetworks || !!config.id"
38+
:loading="loadingNetworks"
39+
hint="Select the L1 chain for your OP Stack rollup"
4040
persistent-hint
41-
no-data-text="No L1 parent workspaces available. Mark a workspace as 'Top L1 Parent' first."
41+
no-data-text="No supported networks available."
4242
/>
4343
</v-col>
4444
</v-row>
@@ -200,12 +200,14 @@ const props = defineProps({
200200
const $server = inject('$server');
201201
202202
const loading = ref(false);
203-
const loadingParents = ref(false);
203+
const loadingNetworks = ref(false);
204204
const errorMessage = ref('');
205205
const successMessage = ref('');
206-
const availableParents = ref([]);
206+
const availableNetworks = ref([]);
207+
const isFormValid = ref(false);
207208
208209
const config = ref({
210+
networkId: null,
209211
outputVersion: 0,
210212
l2ToL1MessagePasserAddress: '0x4200000000000000000000000000000000000016'
211213
});
@@ -215,13 +217,6 @@ const outputVersionOptions = [
215217
{ text: 'Fault Proofs (DisputeGameFactory)', value: 1 }
216218
];
217219
218-
const parentOptions = computed(() =>
219-
availableParents.value.map(p => ({
220-
...p,
221-
displayName: `${p.name} (Chain ID: ${p.networkId})`
222-
}))
223-
);
224-
225220
const addressRules = [
226221
v => !!v || 'Address is required',
227222
v => /^0x[a-fA-F0-9]{40}$/.test(v) || 'Must be a valid Ethereum address'
@@ -231,24 +226,28 @@ const optionalAddressRules = [
231226
v => !v || /^0x[a-fA-F0-9]{40}$/.test(v) || 'Must be a valid Ethereum address'
232227
];
233228
234-
const parentWorkspaceRules = [
235-
v => !!v || 'Parent workspace is required'
229+
const networkRules = [
230+
v => !!v || 'Network selection is required'
236231
];
237232
238233
const isConfigured = computed(() => {
239-
return !!(config.value.parentWorkspaceId &&
234+
return !!(config.value.networkId &&
240235
config.value.optimismPortalAddress &&
241236
config.value.batchInboxAddress);
242237
});
243238
244-
function loadAvailableParents() {
245-
loadingParents.value = true;
239+
function loadAvailableNetworks() {
240+
loadingNetworks.value = true;
246241
$server.getAvailableOpParents()
247242
.then(({ data }) => {
248-
availableParents.value = data.availableParents || [];
243+
availableNetworks.value = data.availableNetworks || [];
244+
// Auto-select if only one network available and no config yet
245+
if (availableNetworks.value.length === 1 && !config.value.networkId && !config.value.id) {
246+
config.value.networkId = availableNetworks.value[0].networkId;
247+
}
249248
})
250249
.catch(console.log)
251-
.finally(() => loadingParents.value = false);
250+
.finally(() => loadingNetworks.value = false);
252251
}
253252
254253
function loadConfig() {
@@ -258,7 +257,9 @@ function loadConfig() {
258257
if (data.opConfig) {
259258
config.value = {
260259
...config.value,
261-
...data.opConfig
260+
...data.opConfig,
261+
// Map parentChainId to networkId for the form
262+
networkId: data.opConfig.parentChainId
262263
};
263264
}
264265
})
@@ -281,7 +282,11 @@ function createConfig() {
281282
282283
$server.createOpConfig(props.explorerId, config.value)
283284
.then(({ data: { config: newConfig } }) => {
284-
config.value = { ...config.value, ...newConfig };
285+
config.value = {
286+
...config.value,
287+
...newConfig,
288+
networkId: newConfig.parentChainId
289+
};
285290
successMessage.value = 'OP Stack configuration created.';
286291
})
287292
.catch(error => errorMessage.value = error.response && error.response.data || 'Error while creating configuration. Please retry.')
@@ -296,9 +301,17 @@ function updateConfig() {
296301
errorMessage.value = '';
297302
successMessage.value = '';
298303
299-
$server.updateOpConfig(props.explorerId, config.value)
304+
// Don't send networkId on updates - it can't be changed
305+
const updatePayload = { ...config.value };
306+
delete updatePayload.networkId;
307+
308+
$server.updateOpConfig(props.explorerId, updatePayload)
300309
.then(({ data: { config: updatedConfig } }) => {
301-
config.value = { ...config.value, ...updatedConfig };
310+
config.value = {
311+
...config.value,
312+
...updatedConfig,
313+
networkId: updatedConfig.parentChainId
314+
};
302315
successMessage.value = 'OP Stack configuration saved.';
303316
})
304317
.catch(error => errorMessage.value = error.response && error.response.data || 'Error while saving configuration. Please retry.')
@@ -309,7 +322,7 @@ function updateConfig() {
309322
}
310323
311324
onMounted(async () => {
312-
loadAvailableParents();
325+
loadAvailableNetworks();
313326
loadConfig();
314327
});
315328
</script>

tests/unit/components/ExplorerOpSettings.spec.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe('ExplorerOpSettings.vue', () => {
55
vi.spyOn(server, 'getOpConfig')
66
.mockResolvedValue({ data: null });
77
vi.spyOn(server, 'getAvailableOpParents')
8-
.mockResolvedValue({ data: { availableParents: [] } });
8+
.mockResolvedValue({ data: { availableNetworks: [] } });
99

1010
mount(ExplorerOpSettings, {
1111
global: {
@@ -19,16 +19,15 @@ describe('ExplorerOpSettings.vue', () => {
1919
expect(server.getAvailableOpParents).toHaveBeenCalled();
2020
});
2121

22-
it('Should display parent workspaces in dropdown', async () => {
23-
const mockParents = [
24-
{ id: 1, name: 'Ethereum Mainnet', networkId: 1 },
25-
{ id: 2, name: 'Sepolia', networkId: 11155111 }
22+
it('Should display network selection dropdown', async () => {
23+
const mockNetworks = [
24+
{ networkId: 1, name: 'Ethereum Mainnet', explorerUrl: 'https://etherscan.io' }
2625
];
2726

2827
vi.spyOn(server, 'getOpConfig')
2928
.mockResolvedValue({ data: null });
3029
vi.spyOn(server, 'getAvailableOpParents')
31-
.mockResolvedValue({ data: { availableParents: mockParents } });
30+
.mockResolvedValue({ data: { availableNetworks: mockNetworks } });
3231

3332
const wrapper = mount(ExplorerOpSettings, {
3433
global: {
@@ -38,6 +37,6 @@ describe('ExplorerOpSettings.vue', () => {
3837

3938
await new Promise(process.nextTick);
4039

41-
expect(wrapper.text()).toContain('L1 Parent Workspace');
40+
expect(wrapper.text()).toContain('L1 Parent Network');
4241
});
4342
});

0 commit comments

Comments
 (0)