Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ edb
*.spec.js
*.spec.mjs
*.spec.mts

# Local task/scratch artifacts (plans, benches, reports) — kept out of the repo
tasks/
/tmp
video/out
.superpowers
Expand Down
2 changes: 1 addition & 1 deletion api/edb-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const config = {

const MAX_BODY_BYTES = 50 * 1024 * 1024; // 50 MB (artifacts_inline can be large)
const FETCH_TIMEOUT_MS = 120_000; // 2 min for regular requests
const ALLOWED_METHODS = new Set(["GET", "POST", "OPTIONS", "HEAD"]);
const ALLOWED_METHODS = new Set(["GET", "POST", "OPTIONS"]);

// CORS allowlist — dev servers by default; extend via EDB_CORS_ALLOWED_ORIGINS (comma-separated).
const DEFAULT_ALLOWED_ORIGINS = new Set([
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore HEAD to the EDB method allowlist

When a client or health checker sends a HEAD request to /api/edb/*, this allowlist now rejects it with 405 before the proxy reaches the existing HEAD-specific path below (the CORS header still advertises HEAD, and the body handling explicitly skips bodies for HEAD). This makes HEAD probes fail even though the rest of the handler is still written to forward them safely.

Useful? React with 👍 / 👎.

Expand Down
2 changes: 1 addition & 1 deletion api/lifi-composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const config = {

const LIFI_BASE = "https://li.quest";
const LIFI_API_KEY = process.env.LIFI_API_KEY || "";
const ALLOWED_METHODS = new Set(["GET", "OPTIONS", "HEAD"]);
const ALLOWED_METHODS = new Set(["GET", "OPTIONS"]);
const ALLOWED_ORIGINS = new Set(
(process.env.ALLOWED_ORIGINS || "").split(",").filter(Boolean)
);
Expand Down
2 changes: 1 addition & 1 deletion api/lifi-earn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const config = {

const LIFI_EARN_BASE = "https://earn.li.fi";
const LIFI_API_KEY = process.env.LIFI_API_KEY || "";
const ALLOWED_METHODS = new Set(["GET", "OPTIONS", "HEAD"]);
const ALLOWED_METHODS = new Set(["GET", "OPTIONS"]);
const ALLOWED_ORIGINS = new Set(
(process.env.ALLOWED_ORIGINS || "").split(",").filter(Boolean)
);
Expand Down
5 changes: 0 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@ export default tseslint.config(
ignores: [
'dist/**',
'node_modules/**',
'current_bundle/**',
'test-results/**',
'scripts/**',
'public/**',
'tests/**',
'test-*/**',
'**/test-*.js',
'test-app-*/**',
'.claude/worktrees/**',
'edb/**',
'starknet-sim/**',
Expand Down
1 change: 0 additions & 1 deletion scripts/bridge-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export const TRACE_DETAIL_GZIP_MIN_BYTES = Number(
);
export const TRACE_STRIP_OPCODE_LINES = process.env.TRACE_DETAIL_STRIP_OPCODE_LINES === "true";
export const TRACE_DETAIL_STRIP_OPCODE_TRACE = process.env.TRACE_DETAIL_STRIP_OPCODE_TRACE !== "false";
export const TRACE_DETAIL_COMPACT_ARTIFACTS = process.env.TRACE_DETAIL_COMPACT_ARTIFACTS !== "false";
export const TRACE_V2_BRIDGE_JS_FALLBACK = process.env.SIM_TRACE_V2_BRIDGE_JS_FALLBACK === "true";
export const TRACE_LITE_TRANSPORT_ENABLED =
process.env.SIM_TRACE_V2_LITE_TRANSPORT !== "false";
Expand Down
16 changes: 0 additions & 16 deletions scripts/bridge-security.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,3 @@ export function redactRpcUrl(url) {
}
}

/**
* Sanitize an object for logging - redacts sensitive fields
* @param {Object} obj
* @returns {Object}
*/
export function sanitizeForLogging(obj) {
if (!obj || typeof obj !== "object") return obj;
const sanitized = { ...obj };
if ("rpcUrl" in sanitized) {
sanitized.rpcUrl = redactRpcUrl(sanitized.rpcUrl);
}
if ("rpc_url" in sanitized) {
sanitized.rpc_url = redactRpcUrl(sanitized.rpc_url);
}
return sanitized;
}
8 changes: 6 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import PersistentTools from "./components/PersistentTools";
import { ToolkitProvider } from "./contexts/ToolkitContext";
import { SimulationProvider } from "./contexts/SimulationContext";
import { DebugProvider } from "./contexts/DebugContext";
import Navigation from "./components/Navigation";
import ErrorBoundary from "./components/ErrorBoundary";
import { NotificationProvider } from "./components/NotificationManager";
import { RouteMetaTags } from "./components/shared/RouteMetaTags";
Expand All @@ -21,6 +20,7 @@ import HomePage from "./components/HomePage";
import MobileDrawer from "./components/MobileDrawer";
import { useBreakpoint } from "./hooks/useBreakpoint";

const Navigation = React.lazy(() => import("./components/Navigation"));
const SimulationResultsPage = React.lazy(() => import("./components/SimulationResultsPage"));
const RpcSettingsModal = React.lazy(() => import("./components/RpcSettingsModal"));
const StorageManagerModal = React.lazy(() => import("./components/StorageManagerModal"));
Expand Down Expand Up @@ -80,7 +80,11 @@ function App() {
</Routes>
) : (
<div className={cn("app", (location.pathname.startsWith("/explorer") || location.pathname.startsWith("/builder") || location.pathname.startsWith("/integrations")) && "app-fullwidth")}>
{!isMobile && <Navigation />}
{!isMobile && (
<Suspense fallback={null}>
<Navigation />
</Suspense>
)}

<main className="content">
<Routes>
Expand Down
10 changes: 4 additions & 6 deletions src/components/InlineFacetLoader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useCallback, useEffect, useRef } from "react";
import {
fetchDiamondFacets,
getDiamondFacetAddresses,
getDiamondFacetAddressesWithSelectors,
type DiamondFacet,
} from "../utils/diamondFacetFetcher";
import { networkConfigManager } from "../config/networkConfig";
Expand Down Expand Up @@ -91,10 +91,8 @@ export const InlineFacetLoader: React.FC<InlineFacetLoaderProps> = ({
setShowDetails(false);
setFacetDetails([]);

const facetAddresses = await getDiamondFacetAddresses(
chain,
diamondAddress
);
const { addresses: facetAddresses, loupeSelectors } =
await getDiamondFacetAddressesWithSelectors(chain, diamondAddress);

if (requestIdRef.current !== requestId) {
return;
Expand Down Expand Up @@ -170,7 +168,7 @@ export const InlineFacetLoader: React.FC<InlineFacetLoaderProps> = ({
});
onProgressChange?.(p);
},
{ etherscanApiKey }
{ etherscanApiKey, loupeSelectors }
);

if (requestIdRef.current !== requestId) {
Expand Down
92 changes: 20 additions & 72 deletions src/components/SimulationHistoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import {
type StoredSimulation,
type SimulationHistoryFilter
} from '../services/SimulationHistoryService';
import { traceVaultService, recomputeHierarchy } from '../services/TraceVaultService';
import {
loadStoredSimulation,
deleteStoredSimulation,
deleteStoredSimulations,
clearStoredSimulations,
} from '../services/simulationStore';
import { useSimulation } from '../contexts/SimulationContext';
import { SUPPORTED_CHAINS } from '../utils/chains';
import { Button } from './ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './ui/table';
import { Checkbox } from './ui/checkbox';
import { shortenAddress } from './shared/AddressDisplay';
import { hasInternalInfo } from './simulation-results/useSimulationPageHelpers';
import '../styles/SimulationHistory.css';

// Helper to format timestamp
Expand Down Expand Up @@ -169,76 +173,20 @@ const SimulationHistoryPage: React.FC = () => {
const handleViewSimulation = useCallback(async (sim: StoredSimulation) => {
// In lightweight mode, we don't have result/contractContext - need to fetch
try {
const fullSim = await simulationHistoryService.getSimulation(sim.id);
if (fullSim?.result && fullSim?.contractContext) {
const loaded = await loadStoredSimulation(sim.id);
if (loaded) {
// Set simulation in context and navigate to results
// Pass skipHistorySave to avoid creating duplicate history entries
setSimulation(fullSim.result, fullSim.contractContext, { skipHistorySave: true });

let restoredFromVault = false;
try {
const traceBundle = await traceVaultService.loadDecodedTrace(sim.id, {
includeHeavy: false,
});

const opfsRowCount = traceBundle?.rows?.length ?? 0;
const indexedDbRowCount = fullSim.decodedTraceRows?.length ?? 0;
const opfsHasInternal = hasInternalInfo(traceBundle?.rows);
const indexedDbHasInternal = hasInternalInfo(fullSim.decodedTraceRows);

// Prefer OPFS if it has rows with hierarchy info
// Fall back to IndexedDB only if OPFS is empty/missing hierarchy but IndexedDB has it
let rowsToUse: any[] | undefined;
let sourceLabel: string = 'unknown';

if (opfsRowCount > 0 && opfsHasInternal) {
// OPFS has full data with hierarchy - use it
rowsToUse = traceBundle!.rows;
sourceLabel = 'OPFS';
} else if (indexedDbRowCount > 0 && indexedDbHasInternal) {
// IndexedDB has hierarchy but OPFS doesn't - use IndexedDB
rowsToUse = fullSim.decodedTraceRows;
sourceLabel = 'IndexedDB';
} else if (opfsRowCount > 0) {
// OPFS has rows (even without hierarchy) - use it
rowsToUse = traceBundle!.rows;
sourceLabel = 'OPFS (no hierarchy)';
} else if (indexedDbRowCount > 0) {
// IndexedDB has rows as last resort
rowsToUse = fullSim.decodedTraceRows;
sourceLabel = 'IndexedDB (no hierarchy)';
}

if (rowsToUse && rowsToUse.length > 0) {
// Recompute hierarchy from depth relationships to fix traces where
// hasChildren wasn't computed correctly for nested call frames
const fixedRows = recomputeHierarchy(rowsToUse);
setDecodedTraceRows(fixedRows);
if (traceBundle?.sourceTexts && Object.keys(traceBundle.sourceTexts).length > 0) {
setSourceTexts(traceBundle.sourceTexts);
}
// Set trace metadata including rawEvents for TokenMovementsPanel
setDecodedTraceMeta({
sourceLines: traceBundle?.sourceLines ?? [],
callMeta: traceBundle?.callMeta,
rawEvents: traceBundle?.rawEvents ?? [],
implementationToProxy: traceBundle?.implementationToProxy ?? new Map<string, string>(),
});
restoredFromVault = true;
}
} catch {
// Fallback: restore decoded rows from IndexedDB on OPFS failure
if (fullSim.decodedTraceRows && fullSim.decodedTraceRows.length > 0) {
const fixedRows = recomputeHierarchy(fullSim.decodedTraceRows);
setDecodedTraceRows(fixedRows);
restoredFromVault = true;
}
}
setSimulation(loaded.result, loaded.contractContext, { skipHistorySave: true });

// Final fallback: restore legacy decoded rows from IndexedDB (should rarely hit this)
if (!restoredFromVault && fullSim.decodedTraceRows && fullSim.decodedTraceRows.length > 0) {
const fixedRows = recomputeHierarchy(fullSim.decodedTraceRows);
setDecodedTraceRows(fixedRows);
if (loaded.decodedRows && loaded.decodedRows.length > 0) {
setDecodedTraceRows(loaded.decodedRows);
}
if (loaded.sourceTexts) {
setSourceTexts(loaded.sourceTexts);
}
if (loaded.meta) {
setDecodedTraceMeta(loaded.meta);
}

// Set the simulation ID in context for consistency
Expand Down Expand Up @@ -269,7 +217,7 @@ const SimulationHistoryPage: React.FC = () => {
// Handle delete simulation
const handleDeleteSimulation = useCallback(async (id: string) => {
try {
await simulationHistoryService.deleteSimulation(id);
await deleteStoredSimulation(id);
setSimulations(prev => prev.filter(s => s.id !== id));
setSelectedIds(prev => {
const next = new Set(prev);
Expand All @@ -289,7 +237,7 @@ const SimulationHistoryPage: React.FC = () => {
if (!confirmed) return;

try {
await simulationHistoryService.deleteSimulations(Array.from(selectedIds));
await deleteStoredSimulations(Array.from(selectedIds));
setSimulations(prev => prev.filter(s => !selectedIds.has(s.id)));
setSelectedIds(new Set());
} catch {
Expand All @@ -303,7 +251,7 @@ const SimulationHistoryPage: React.FC = () => {
if (!confirmed) return;

try {
await simulationHistoryService.clearAll();
await clearStoredSimulations();
setSimulations([]);
setSelectedIds(new Set());
} catch {
Expand Down
18 changes: 4 additions & 14 deletions src/components/TransactionBuilderHub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,20 @@ import { LayoutTransitionWrapper } from "./ui/animated-tabs";
import { useSimulation } from "../contexts/SimulationContext";
import { AnimatedZapIcon, AnimatedPlayIcon } from "./icons/IconLibrary";
import { SUPPORTED_CHAINS } from "../utils/chains";
import { TXHASH_REPLAY_KEY, parseBuilderIntent } from "./transaction-builder/types";

type BuilderMode = "live" | "simulation";
type BuilderIntentMode = "live" | "simulation" | "replay";

const TXHASH_REPLAY_KEY = 'web3-toolkit:txhash-replay';

const loadSimpleGridUI = () => import("./simple-grid");
const loadTransactionBuilderWagmi = () => import("./TransactionBuilderWagmi");

const SimpleGridUI = React.lazy(loadSimpleGridUI);
const TransactionBuilderWagmi = React.lazy(loadTransactionBuilderWagmi);

function parseBuilderIntentMode(search: string): BuilderIntentMode | null {
const mode = new URLSearchParams(search).get('mode');
if (mode === 'live' || mode === 'simulation' || mode === 'replay') {
return mode;
}
return null;
}

const TransactionBuilderHub: React.FC = () => {
const { contractContext } = useSimulation();
const location = useLocation();
const urlIntentMode = parseBuilderIntentMode(location.search);
const urlIntentMode = parseBuilderIntent(location.search).mode;
const builderSearchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);

const liveInitialContractData = useMemo(() => {
Expand Down Expand Up @@ -59,9 +49,9 @@ const TransactionBuilderHub: React.FC = () => {

// Initialize mode based on whether there's simulation context or clone/replay data
const [mode, setMode] = useState<BuilderMode>(() => {
const initialIntentMode = parseBuilderIntentMode(location.search);
const initialIntentMode = parseBuilderIntent(location.search).mode;
if (initialIntentMode === 'live') return 'live';
if (initialIntentMode === 'simulation' || initialIntentMode === 'replay') return 'simulation';
if (initialIntentMode === 'simulation') return 'simulation';

// Check for clone query param (set by SimulationHistoryPage)
if (hasCloneParam) {
Expand Down
22 changes: 7 additions & 15 deletions src/components/TransactionBuilderWagmi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TXHASH_REPLAY_KEY,
TXHASH_REPLAY_EVENT,
TXHASH_REPLAY_LAST_INTENT_KEY,
parseBuilderIntent,
} from "./transaction-builder/types";
import { TransactionReplayView } from "./transaction-builder/TransactionReplayView";
import { renderModeToggle } from "./transaction-builder/renderModeToggle";
Expand Down Expand Up @@ -86,13 +87,9 @@ const TransactionBuilderWagmi: React.FC = () => {

// Initialize viewMode: "replay" if txHash replay data exists, otherwise "builder"
const [viewMode, setViewMode] = useState<SimulationViewMode>(() => {
const params = new URLSearchParams(location.search);
const requestedMode = params.get('mode');
if (requestedMode === 'replay' || params.get('replay') === 'txhash') {
return "replay";
}
if (requestedMode === 'simulation') {
return "builder";
const intentViewMode = parseBuilderIntent(location.search).viewMode;
if (intentViewMode) {
return intentViewMode;
}

// Check for txHash replay data (set by SimulationResultsPage for re-simulation)
Expand Down Expand Up @@ -245,14 +242,9 @@ const TransactionBuilderWagmi: React.FC = () => {

// Keep route intent in sync while this component stays mounted in PersistentTools.
useEffect(() => {
const params = new URLSearchParams(location.search);
const requestedMode = params.get('mode');
if (requestedMode === 'replay' || params.get('replay') === 'txhash') {
setViewMode('replay');
return;
}
if (requestedMode === 'simulation') {
setViewMode('builder');
const intentViewMode = parseBuilderIntent(location.search).viewMode;
if (intentViewMode) {
setViewMode(intentViewMode);
}
}, [location.search]);

Expand Down
2 changes: 1 addition & 1 deletion src/components/explorer/SlotRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ const InlineInspector: React.FC<InlineInspectorProps> = ({ slot }) => {
Slot Packing Layout
</div>
<div className="bg-muted/10 rounded border border-border/20 p-2">
<PackingVisualizer fields={slot.decodedFields!} rawHex={slot.value} />
<PackingVisualizer fields={slot.decodedFields!} />
</div>
</div>
)}
Expand Down
Loading