Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions src/app/api/install-deps/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export async function POST(req: NextRequest) {
const actualNetwork = network.toLowerCase();
const shouldForceInstall = Boolean(forceInstall);

// Validate inputs to prevent path traversal attacks
// Only allow alphanumeric characters, hyphens, and underscores
const safePathPattern = /^[a-zA-Z0-9_-]+$/;
if (!safePathPattern.test(actualNetwork) || !safePathPattern.test(upgradeId)) {
return NextResponse.json(
Expand All @@ -46,9 +44,9 @@ export async function POST(req: NextRequest) {
);
}

// Construct the path to the upgrade folder and lib subdirectory
const contractDeploymentsPath = findContractDeploymentsRoot();
const upgradePath = path.join(contractDeploymentsPath, actualNetwork, upgradeId);
const upgradePath = path.join(contractDeploymentsPath, 'active', 'evm');
const taskPath = path.join(upgradePath, 'tasks', upgradeId);

// Verify the resolved path is within the allowed directory
let resolvedUpgradePath: string;
Expand All @@ -59,12 +57,12 @@ export async function POST(req: NextRequest) {
}

const libPath = path.join(resolvedUpgradePath, 'lib');
const resolvedTaskPath = assertWithinDir(taskPath, contractDeploymentsPath);

// Check if the upgrade folder exists
const upgradePathExists = await pathExists(resolvedUpgradePath);
if (!upgradePathExists) {
const taskPathExists = await pathExists(resolvedTaskPath);
if (!taskPathExists) {
return NextResponse.json(
{ error: `Upgrade folder not found: ${network}/${upgradeId}` },
{ error: `Task folder not found: ${path.relative(contractDeploymentsPath, taskPath)}` },
{ status: 404 }
);
}
Expand All @@ -91,7 +89,6 @@ export async function POST(req: NextRequest) {
`Installing dependencies for ${actualNetwork}/${upgradeId} (cwd: ${resolvedUpgradePath})`
);

// Run make deps in the upgrade folder
const { stdout, stderr } = await execAsync('make deps', {
cwd: resolvedUpgradePath,
timeout: INSTALL_DEPS_TIMEOUT_MS,
Expand Down
42 changes: 11 additions & 31 deletions src/app/api/upgrade-config/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,14 @@ describe('GET /api/upgrade-config', () => {
const res = await GET(createRequest({ upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/missing required parameters/i);
expect(body.error).toMatch(/missing required parameter/i);
});

it('returns 400 when upgradeId is missing', async () => {
const res = await GET(createRequest({ network: 'mainnet' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/missing required parameters/i);
});

it('returns 400 when both are missing', async () => {
const res = await GET(createRequest({}));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/missing required parameters/i);
expect(body.error).toMatch(/missing required parameter/i);
});
});

Expand All @@ -80,55 +73,42 @@ describe('GET /api/upgrade-config', () => {
const res = await GET(createRequest({ network: '..', upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network or upgradeId/i);
expect(body.error).toMatch(/invalid network/i);
});

it('returns 400 when network contains "/"', async () => {
const res = await GET(createRequest({ network: 'foo/bar', upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network or upgradeId/i);
});

it('returns 400 when upgradeId contains ".."', async () => {
const res = await GET(createRequest({ network: 'mainnet', upgradeId: '..' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network or upgradeId/i);
expect(body.error).toMatch(/invalid network/i);
});

it('returns 400 when network contains spaces', async () => {
const res = await GET(createRequest({ network: 'main net', upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network or upgradeId/i);
expect(body.error).toMatch(/invalid network/i);
});

it('returns 400 when upgradeId contains "@"', async () => {
const res = await GET(createRequest({ network: 'mainnet', upgradeId: 'test@1' }));
it('returns 400 when network contains "." (single dot)', async () => {
const res = await GET(createRequest({ network: '.', upgradeId: 'test' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network or upgradeId/i);
expect(body.error).toMatch(/invalid network/i);
});

it('returns 400 when network contains "." (single dot)', async () => {
const res = await GET(createRequest({ network: '.', upgradeId: 'test' }));
it('returns 400 when upgradeId contains path separators', async () => {
const res = await GET(createRequest({ network: 'mainnet', upgradeId: 'foo/bar' }));
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/invalid network or upgradeId/i);
});
});

describe('valid safe characters', () => {
it('accepts hyphens (e.g., "op-mainnet")', async () => {
it('accepts hyphens in network names', async () => {
const res = await GET(createRequest({ network: 'op-mainnet', upgradeId: 'test' }));
expect(res.status).toBe(200);
});

it('accepts underscores (e.g., "upgrade_1")', async () => {
const res = await GET(createRequest({ network: 'mainnet', upgradeId: 'upgrade_1' }));
expect(res.status).toBe(200);
});
});

describe('happy path', () => {
Expand Down
12 changes: 7 additions & 5 deletions src/app/api/upgrade-config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export async function GET(req: NextRequest) {
);
}

// Validate inputs to prevent path traversal attacks
const safePathPattern = /^[a-zA-Z0-9_-]+$/;
if (!safePathPattern.test(network) || !safePathPattern.test(upgradeId)) {
return NextResponse.json(
Expand All @@ -38,8 +37,12 @@ export async function GET(req: NextRequest) {
const contractDeploymentsRoot = findContractDeploymentsRoot();
const validationsJoinedPath = path.join(
contractDeploymentsRoot,
network,
'active',
'evm',
'tasks',
upgradeId,
'config',
network,
'validations'
);

Expand Down Expand Up @@ -89,10 +92,9 @@ export async function GET(req: NextRequest) {
);

console.log(
'Found %d config options for %s/%s:',
'Found %d config options for %s:',
configOptions.length,
network,
upgradeId,
`${network}/${upgradeId}`,
configOptions.map(c => c.displayName)
);

Expand Down
40 changes: 40 additions & 0 deletions src/app/api/upgrades/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,46 @@ describe('GET /api/upgrades', () => {
expect(body[0].network).toBe(NetworkType.Zeronet);
});

it('sorts aggregated ready-to-sign tasks newest-first across networks', async () => {
// Older task on an earlier network, newer task on a later network: without a final
// sort the aggregate would follow network order (older first).
mockGetUpgradeOptions.mockImplementation((network: NetworkType) => {
if (network === NetworkType.Mainnet) {
return [
{
id: '2025-01-01-upgrade-old',
name: 'Old',
description: '',
date: '2025-01-01',
network: NetworkType.Mainnet,
status: TaskStatus.ReadyToSign,
},
];
}
if (network === NetworkType.Zeronet) {
return [
{
id: '2025-12-31-upgrade-new',
name: 'New',
description: '',
date: '2025-12-31',
network: NetworkType.Zeronet,
status: TaskStatus.ReadyToSign,
},
];
}
return [];
});

const res = GET(createRequest({ readyToSign: 'true' }));
const body = await res.json();

expect(body.map((t: DeploymentInfo) => t.id)).toEqual([
'2025-12-31-upgrade-new',
'2025-01-01-upgrade-old',
]);
});

it('includes zeronet when aggregating ready-to-sign tasks', async () => {
const res = GET(createRequest({ readyToSign: 'true' }));

Expand Down
8 changes: 3 additions & 5 deletions src/app/api/upgrades/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@ export function GET(req: NextRequest) {
network: network,
}));
});
// Sort by date (most recent first)
return NextResponse.json(
allReadyToSignTasks.sort((a, b) => b.id.localeCompare(a.id)),
{ status: 200 }
);
// Sort by id (newest first) across all networks; per-network lists are sorted independently.
allReadyToSignTasks.sort((a, b) => b.id.localeCompare(a.id));
return NextResponse.json(allReadyToSignTasks, { status: 200 });
}
return NextResponse.json({ error: 'Missing required network parameter' }, { status: 400 });
}
Expand Down
38 changes: 10 additions & 28 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ConfigOption } from '@/components/UserSelection';
import { LedgerSigningResult } from '@/lib/ledger-signing';

type Step = 'upgrade' | 'user' | 'validation' | 'ledger' | 'signing';
type StepStatus = 'current' | 'completed' | 'pending';

const STEP_LAYOUT_WIDTH: Record<Step, string> = {
upgrade: 'max-w-4xl',
Expand Down Expand Up @@ -80,16 +81,11 @@ export default function Home() {
setCurrentStep('user');
};

const canEditUpgrade =
currentStep === 'user' ||
currentStep === 'validation' ||
currentStep === 'ledger' ||
currentStep === 'signing';
const canEditUpgrade = currentStep !== 'upgrade';

const canEditUser =
currentStep === 'validation' || currentStep === 'ledger' || currentStep === 'signing';

// Map current step to step indicator format
const steps = [
{
id: 'upgrade',
Expand All @@ -105,38 +101,28 @@ export default function Home() {
status:
currentStep === 'user'
? 'current'
: ((selectedUser ? 'completed' : selectedUpgrade ? 'pending' : 'pending') as
| 'current'
| 'completed'
| 'pending'),
: ((selectedUser ? 'completed' : 'pending') as StepStatus),
},
{
id: 'validation',
label: 'Review & Validate',
status:
currentStep === 'validation'
? 'current'
: ((validationData ? 'completed' : selectedUser ? 'pending' : 'pending') as
| 'current'
| 'completed'
| 'pending'),
: ((validationData ? 'completed' : 'pending') as StepStatus),
},
{
id: 'ledger',
label: 'Sign with Ledger',
status:
currentStep === 'ledger'
? 'current'
: ((signingData ? 'completed' : validationData ? 'pending' : 'pending') as
| 'current'
| 'completed'
| 'pending'),
: ((signingData ? 'completed' : 'pending') as StepStatus),
},
{
id: 'signing',
label: 'Confirmation',
status:
currentStep === 'signing' ? 'current' : ('pending' as 'current' | 'completed' | 'pending'),
status: currentStep === 'signing' ? 'current' : ('pending' as StepStatus),
},
];

Expand All @@ -158,8 +144,8 @@ export default function Home() {

{currentStep === 'upgrade' && (
<UpgradeSelection
selectedWallet={selectedUpgrade?.id || null}
selectedNetwork={selectedNetwork}
selectedUpgradeId={selectedUpgrade?.id ?? null}
selectedNetwork={selectedNetwork ?? null}
onSelect={handleUpgradeSelection}
/>
)}
Expand All @@ -172,14 +158,11 @@ export default function Home() {
/>
)}

{currentStep === 'validation' && (
{currentStep === 'validation' && selectedUpgrade && (
<ValidationResults
userType={selectedUser?.fileName || ''}
network={selectedNetwork || ''}
selectedUpgrade={{
id: selectedUpgrade?.id || '',
name: selectedUpgrade?.name || '',
}}
upgradeId={selectedUpgrade.id}
onProceedToLedgerSigning={handleProceedToLedgerSigning}
/>
)}
Expand All @@ -198,7 +181,6 @@ export default function Home() {
user={selectedUser}
network={selectedNetwork || ''}
selectedUpgrade={{
id: selectedUpgrade?.id || '',
name: selectedUpgrade?.name || '',
}}
signingData={signingData}
Expand Down
27 changes: 0 additions & 27 deletions src/components/NetworkSelection.tsx

This file was deleted.

1 change: 0 additions & 1 deletion src/components/SigningConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface SigningConfirmationProps {
user?: ConfigOption;
network: string;
selectedUpgrade: {
id: string;
name: string;
};
signingData?: LedgerSigningResult | null;
Expand Down
Loading
Loading