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
10 changes: 10 additions & 0 deletions src/components/CollapsibleLeftPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ const CollapsibleLeftPanel = ({
handleTokensChange,
handleWithWrapToggle,
handleStagingToggle,
handleTestEnvToggle,
handleTestEnvUrlChange,
handleTestEnvBlockNumberChange,
handleQuantizedModeToggle,
handleDebugIntermediateToggle,
testEnvSession,
onDestroySession,
handleFromTokensExclusionToggle,
handleToTokensExclusionToggle,
onFindPath,
Expand Down Expand Up @@ -96,8 +101,13 @@ const CollapsibleLeftPanel = ({
handleTokensChange={handleTokensChange}
handleWithWrapToggle={handleWithWrapToggle}
handleStagingToggle={handleStagingToggle}
handleTestEnvToggle={handleTestEnvToggle}
handleTestEnvUrlChange={handleTestEnvUrlChange}
handleTestEnvBlockNumberChange={handleTestEnvBlockNumberChange}
handleQuantizedModeToggle={handleQuantizedModeToggle}
handleDebugIntermediateToggle={handleDebugIntermediateToggle}
testEnvSession={testEnvSession}
onDestroySession={onDestroySession}
handleFromTokensExclusionToggle={handleFromTokensExclusionToggle}
handleToTokensExclusionToggle={handleToTokensExclusionToggle}
onFindPath={onFindPath}
Expand Down
47 changes: 45 additions & 2 deletions src/components/FlowVisualization.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { usePerformance } from '@/contexts/PerformanceContext';
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
import { decomposeFlow, transfersFromRoutes } from '@/utils/flowDecomposition';
import { parseAddressList } from '@/services/circlesApi';
import { getOrCreateSession, destroySession } from '@/services/testEnvService';
import Header from '@/components/ui/header';
import CollapsibleLeftPanel from '@/components/CollapsibleLeftPanel';
import CytoscapeVisualization from '@/components/CytoscapeVisualization';
Expand Down Expand Up @@ -49,6 +50,9 @@ const FlowVisualization = () => {
handleTokensChange,
handleWithWrapToggle,
handleStagingToggle,
handleTestEnvToggle,
handleTestEnvUrlChange,
handleTestEnvBlockNumberChange,
handleQuantizedModeToggle,
handleDebugIntermediateToggle,
handleFromTokensExclusionToggle,
Expand All @@ -58,7 +62,13 @@ const FlowVisualization = () => {
validateFormData,
} = useFormData();
const [formWarnings, setFormWarnings] = useState([]);

const [testEnvSession, setTestEnvSession] = useState(null);

const handleDestroySession = useCallback(async () => {
await destroySession();
setTestEnvSession(null);
}, []);

const {
pathData,
rawPathData,
Expand Down Expand Up @@ -337,8 +347,35 @@ const FlowVisualization = () => {
setSelectedTransactionId(null);
clearHighlights();

// If test-env mode, ensure we have a valid session attached
if (requestData.UseTestEnv) {
if (!requestData.TestEnvBlockNumber) {
setFormWarnings(['Enter a block number for test environment mode']);
return;
}
const parsedBlock = Number(requestData.TestEnvBlockNumber);
if (!Number.isInteger(parsedBlock) || parsedBlock < 0) {
setFormWarnings(['Block number must be a non-negative integer']);
return;
}
try {
const session = await getOrCreateSession(requestData.TestEnvUrl, parsedBlock);
setTestEnvSession(session);
await loadPathData({ ...requestData, testEnvSession: session });
} catch (err) {
const isMaxSessions = err.message.includes('Maximum concurrent sessions');
const hint = isMaxSessions
? ' — close unused sessions or wait for them to expire (30 min TTL).'
: '';
setFormWarnings([`Test-env session error: ${err.message}${hint}`]);
setTestEnvSession(null);
}
Comment on lines +361 to +372

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Separate session creation failures from query failures.

loadPathData(...) is inside the same try as getOrCreateSession(...), so any downstream RPC/pathfinder failure is reported as a test-env session error and testEnvSession is cleared even though the session was created successfully. Catch only the session acquisition step here and let query failures surface through the normal request error path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/FlowVisualization.jsx` around lines 361 - 372, The current
try/catch wraps both getOrCreateSession and loadPathData so any loadPathData
failure is misreported as a session error; change it to try/catch only around
getOrCreateSession (call getOrCreateSession, setTestEnvSession(session) inside
that try) and keep the existing error handling with the isMaxSessions hint and
setTestEnvSession(null) only in that catch; then call await
loadPathData({...requestData, testEnvSession: session}) outside that catch so
its errors propagate through the normal request error path (do not clear
setTestEnvSession on loadPathData failures).

return;
}

setTestEnvSession(null);
await loadPathData(requestData);
Comment on lines +376 to 377

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Destroy the live session when leaving test-env mode.

This branch only clears the local reference. Because PathFinderForm hides the session UI as soon as UseTestEnv is off, any still-live server session becomes unreachable and continues consuming one of the limited session slots until TTL expiry. Tear the session down before hiding the mode, or watch UseTestEnv/UseStaging and destroy on exit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/FlowVisualization.jsx` around lines 376 - 377, The code
currently only clears the local reference via setTestEnvSession(null) and then
calls loadPathData(requestData), leaving the remote test session alive; update
the logic to first check the existing test session (the state referenced by
setTestEnvSession/testEnvSession) and call its shutdown method (e.g.,
destroy/close/terminate API on that session) and await its completion, handling
errors, before nulling it and calling loadPathData; also add the same teardown
when toggling UseTestEnv/UseStaging off in PathFinderForm (watch the toggles and
destroy the live session on exit) so no orphaned server sessions remain.

}, [loadPathData, clearHighlights]);
}, [loadPathData, clearHighlights, setFormWarnings, setTestEnvSession]);

const selectQuickTokensByPredicate = useCallback(async (predicate) => {
const allTokens = Array.from(new Set(
Expand Down Expand Up @@ -416,6 +453,7 @@ const FlowVisualization = () => {
if (!validation.isValid) {
return;
}

await executeFindPath(baseData);
}, [formData, executeFindPath, validateFormData]);

Expand Down Expand Up @@ -836,6 +874,11 @@ const FlowVisualization = () => {
handleTokensChange={handleTokensChange}
handleWithWrapToggle={handleWithWrapToggle}
handleStagingToggle={handleStagingToggle}
handleTestEnvToggle={handleTestEnvToggle}
handleTestEnvUrlChange={handleTestEnvUrlChange}
handleTestEnvBlockNumberChange={handleTestEnvBlockNumberChange}
testEnvSession={testEnvSession}
onDestroySession={handleDestroySession}
handleQuantizedModeToggle={handleQuantizedModeToggle}
handleDebugIntermediateToggle={handleDebugIntermediateToggle}
handleFromTokensExclusionToggle={handleFromTokensExclusionToggle}
Expand Down
70 changes: 70 additions & 0 deletions src/components/PathFinderForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ const PathFinderForm = ({
handleTokensChange,
handleWithWrapToggle,
handleStagingToggle,
handleTestEnvToggle,
handleTestEnvUrlChange,
handleTestEnvBlockNumberChange,
handleQuantizedModeToggle,
handleDebugIntermediateToggle,
testEnvSession,
onDestroySession,
handleFromTokensExclusionToggle,
handleToTokensExclusionToggle,
onFindPath,
Expand Down Expand Up @@ -358,6 +363,66 @@ const PathFinderForm = ({
/>
<InfoTip text={`Prod: rpc.aboutcircles.com\nStaging: staging.circlesubi.network\n\nCurrently using: ${formData.UseStaging ? 'staging.circlesubi.network' : 'rpc.aboutcircles.com'}`} />
</div>
{formData.UseStaging && (
<div className="flex items-center">
<ToggleSwitch
isEnabled={formData.UseTestEnv}
onToggle={handleTestEnvToggle}
label="Test Environment"
/>
<InfoTip text="Run pathfinder against historical blockchain state at a specific block number. Creates a test-env session (30 min TTL, max 10 concurrent) that filters all data to that block." />
</div>
)}
{formData.UseTestEnv && (
<div className="ml-4 space-y-2 border-l-2 border-blue-500/30 pl-3">
<div>
<label className="block text-xs font-medium mb-1 text-gray-400">Test-Env URL</label>
<Input
type="text"
value={formData.TestEnvUrl}
onChange={handleTestEnvUrlChange}
className="text-xs"
placeholder="https://staging.circlesubi.network/test-env"
/>
</div>
<div>
<label className="block text-xs font-medium mb-1 text-gray-400">Block Number</label>
<Input
type="text"
value={formData.TestEnvBlockNumber}
onChange={handleTestEnvBlockNumberChange}
className="text-xs"
placeholder="e.g. 43193632"
/>
</div>
{testEnvSession && (
<div className="text-xs text-gray-300 bg-gray-800/60 border border-gray-700 rounded p-2 flex items-center justify-between">
<span>
<span className="text-blue-400">Block {testEnvSession.blockNumber}</span>
{testEnvSession.expiresAt && (
<span className="text-gray-500 ml-1">
· expires {new Date(testEnvSession.expiresAt).toLocaleTimeString()}
</span>
)}
</span>
<button
type="button"
onClick={onDestroySession}
className="ml-2 text-gray-400 hover:text-gray-200 text-xs underline"
title="Close session early to free up a slot"
>
Close
</button>
</div>
)}
{!testEnvSession && (
<div className="text-xs text-gray-500">Sessions last 30 minutes. Max 10 concurrent sessions on the server.</div>
)}
{formData.UseTestEnv && !formData.TestEnvBlockNumber && (
<div className="text-xs text-yellow-400">Enter a block number to query historical state</div>
)}
</div>
)}
<div className="flex items-center opacity-40 pointer-events-none" title="Not yet supported by SDK">
<ToggleSwitch
isEnabled={formData.QuantizedMode}
Expand Down Expand Up @@ -630,7 +695,12 @@ PathFinderForm.propTypes = {
handleTokensChange: PropTypes.func.isRequired,
handleWithWrapToggle: PropTypes.func.isRequired,
handleStagingToggle: PropTypes.func.isRequired,
handleTestEnvToggle: PropTypes.func.isRequired,
handleTestEnvUrlChange: PropTypes.func.isRequired,
handleTestEnvBlockNumberChange: PropTypes.func.isRequired,
handleQuantizedModeToggle: PropTypes.func.isRequired,
testEnvSession: PropTypes.object,
onDestroySession: PropTypes.func.isRequired,
handleDebugIntermediateToggle: PropTypes.func.isRequired,
handleFromTokensExclusionToggle: PropTypes.func.isRequired,
handleToTokensExclusionToggle: PropTypes.func.isRequired,
Expand Down
33 changes: 31 additions & 2 deletions src/hooks/useFormData.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { ethToWei } from '../services/circlesApi';
import { ethToWei, DEFAULT_TEST_ENV_URL } from '../services/circlesApi';

const STORAGE_KEY = 'flow-viz-form';

Expand All @@ -9,6 +9,10 @@ function loadSavedForm() {
if (saved) {
const parsed = JSON.parse(saved);
const merged = { ...DEFAULTS, ...parsed };
// Test-env requires staging — normalize on load
if (merged.UseTestEnv && !merged.UseStaging) {
merged.UseStaging = true;
}
console.log('[form-persist] loaded:', {
FromTokens: merged.FromTokens,
ToTokens: merged.ToTokens,
Expand All @@ -34,6 +38,9 @@ const DEFAULTS = {
Amount: '1000000000000000000000',
WithWrap: true,
UseStaging: false,
UseTestEnv: false,
TestEnvUrl: DEFAULT_TEST_ENV_URL,
TestEnvBlockNumber: '',
MaxTransfers: '10',
IsFromTokensExcluded: false,
IsToTokensExcluded: false,
Expand Down Expand Up @@ -202,10 +209,29 @@ export const useFormData = () => {
const handleStagingToggle = () => {
setFormData(prev => ({
...prev,
UseStaging: !prev.UseStaging
UseStaging: !prev.UseStaging,
// Turning off staging also turns off test-env (test-env requires staging)
UseTestEnv: !prev.UseStaging ? prev.UseTestEnv : false,
}));
};

const handleTestEnvToggle = () => {
setFormData(prev => ({
...prev,
UseTestEnv: !prev.UseTestEnv,
// Test-env requires staging — auto-enable it
UseStaging: !prev.UseTestEnv ? true : prev.UseStaging,
}));
};

const handleTestEnvUrlChange = (e) => {
setFormData(prev => ({ ...prev, TestEnvUrl: e.target.value }));
};

const handleTestEnvBlockNumberChange = (e) => {
setFormData(prev => ({ ...prev, TestEnvBlockNumber: e.target.value }));
};

const handleQuantizedModeToggle = () => {
setFormData(prev => ({
...prev,
Expand Down Expand Up @@ -365,6 +391,9 @@ export const useFormData = () => {
handleTokensChange,
handleWithWrapToggle,
handleStagingToggle,
handleTestEnvToggle,
handleTestEnvUrlChange,
handleTestEnvBlockNumberChange,
handleQuantizedModeToggle,
handleDebugIntermediateToggle,
handleFromTokensExclusionToggle,
Expand Down
Loading