diff --git a/apps/studio/src/components/svm/scenario-editor.tsx b/apps/studio/src/components/svm/scenario-editor.tsx index 048afa4..175ecb4 100644 --- a/apps/studio/src/components/svm/scenario-editor.tsx +++ b/apps/studio/src/components/svm/scenario-editor.tsx @@ -1,6 +1,5 @@ 'use client'; -import { Switch } from '@surfpool/ui'; import { useAppConfig } from '@/hooks/use-app-config'; import { ArrowDownTrayIcon, @@ -13,6 +12,7 @@ import { StopIcon, TrashIcon, } from '@heroicons/react/24/solid'; +import { Switch } from '@surfpool/ui'; import { AnimatePresence, motion } from 'framer-motion'; import React, { useEffect, useState } from 'react'; import TransactionInspector from './transaction-inspector'; @@ -758,7 +758,7 @@ export default function ScenarioEditor({ } }; - const handlePlay = async () => { + const buildScenario = () => { // Build scenario structure for RPC const overrides = slots.flatMap((slot) => slot.actions.map((action) => { @@ -825,7 +825,11 @@ export default function ScenarioEditor({ overrides, tags: [], }; + return scenario; + }; + const handlePlay = async () => { + const scenario = buildScenario(); // Register scenario with surfnet try { console.log('📤 Registering scenario:', scenario); @@ -917,17 +921,25 @@ export default function ScenarioEditor({ setCurrentPlaybackSlot(0); }; - const exportSnapshot = async () => { + const snapshotScenario = async () => { + const scenario = buildScenario(); + + // Call surfnet_exportSnapshot RPC try { const response = await fetch(rpcUrl, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'surfnet_exportSnapshot', + params: [ + { + scope: { + scenario: scenario, + }, + }, + ], }), }); @@ -993,285 +1005,289 @@ export default function ScenarioEditor({ {/* Vertical cursor line - Edit mode only */} {mode === 'edit' && mouseX !== null && (
)} {/* Timeline */} -
+
- {slots.map((slot, index) => { - // In play mode, determine slot visibility and state - const isCurrentSlot = mode === 'play' && index === currentPlaybackSlot; - const isPreviousSlot = mode === 'play' && index === currentPlaybackSlot - 1; - const isNextSlot = mode === 'play' && index === currentPlaybackSlot + 1; - const shouldExpand = (selectedSlotId === slot.id && mode === 'edit') || isCurrentSlot; - - // In play mode, only show previous, current, and next slots - if (mode === 'play' && !isPreviousSlot && !isCurrentSlot && !isNextSlot) { - return null; - } - - // Calculate position for play mode carousel - let playModePosition = 0; - if (mode === 'play') { - if (isPreviousSlot) playModePosition = -400; // Previous slot offset to the left - if (isCurrentSlot) playModePosition = 0; // Current slot centered - if (isNextSlot) playModePosition = 400; // Next slot offset to the right - } - - return ( - + {slots.map((slot, index) => { + // In play mode, determine slot visibility and state + const isCurrentSlot = mode === 'play' && index === currentPlaybackSlot; + const isPreviousSlot = mode === 'play' && index === currentPlaybackSlot - 1; + const isNextSlot = mode === 'play' && index === currentPlaybackSlot + 1; + const shouldExpand = (selectedSlotId === slot.id && mode === 'edit') || isCurrentSlot; + + // In play mode, only show previous, current, and next slots + if (mode === 'play' && !isPreviousSlot && !isCurrentSlot && !isNextSlot) { + return null; + } + + // Calculate position for play mode carousel + let playModePosition = 0; + if (mode === 'play') { + if (isPreviousSlot) playModePosition = -400; // Previous slot offset to the left + if (isCurrentSlot) playModePosition = 0; // Current slot centered + if (isNextSlot) playModePosition = 400; // Next slot offset to the right + } + + return ( - {/* Slot Height Label */} -
- {slots.length < 5 ? `Slot ${slot.height + 1}` : `${slot.height + 1}`} -
- - {/* Slot Card */} -
{ - e.stopPropagation(); - if (mode === 'read') { - setMode('edit'); - } - if (mode !== 'play') { - setSelectedSlotId(slot.id); - } + {/* Slot Height Label */} +
+ + {slots.length < 5 ? `Slot ${slot.height + 1}` : `${slot.height + 1}`} + +
+ + {/* Slot Card */} + - - { + e.stopPropagation(); + if (mode === 'read') { + setMode('edit'); + } + if (mode !== 'play') { + setSelectedSlotId(slot.id); + } + }} > - {shouldExpand ? ( - <> - {/* Actions in this slot - Expanded View */} - {slot.actions.length === 0 ? ( -
-
-
-
No overrides yet
-
-
- ) : ( -
- {slot.actions.map((action, actionIndex) => { - const localIconMap: Record = { - pyth: '/assets/pyth.svg', - switchboard: '/assets/switchboard.svg', - jupiter: '/assets/jupiter.svg', - raydium: '/assets/raydium.svg', - whirlpool: '/assets/whirlpool.svg', - drift: '/assets/drift.svg', - kamino: '/assets/kamino.svg', - }; - const iconSrc = localIconMap[action.protocolId] || '/assets/default.svg'; - - return ( -
{ - if (mode === 'edit' && selectedSlotId === slot.id) { - setEditingAction({ slotId: slot.id, actionIndex }); - - // Load the action's protocol and set it as selected - const protocol = protocols.find((p) => p.id === action.protocolId); - if (protocol) { - setSelectedProtocol(protocol); - - // Find the specific action within the protocol - const foundAction = protocol.actions.find((a) => a.id === action.actionId); - if (foundAction) { - setSelectedAction(foundAction); - // Fetch account data for this action - await handleActionSelect(foundAction); - - // Restore the overrides and modified fields after loading default data - if (action.overrides) { - setAccountData(action.overrides); - } - if (action.modifiedFields) { - setModifiedFields(new Set(action.modifiedFields)); - } - if (action.fetchBeforeUse !== undefined) { - setFetchBeforeUse(action.fetchBeforeUse); - } - } - } - - setShowProtocolPanel(true); - } - }} - > -
- {action.protocol} -
+ + + {shouldExpand ? ( + <> + {/* Actions in this slot - Expanded View */} + {slot.actions.length === 0 ? ( +
+
-
{action.action}
-
{action.protocol}
+
No overrides yet
- {/* Delete button - only in edit mode when slot is selected */} - {mode === 'edit' && selectedSlotId === slot.id && ( - - )}
- ); - })} -
- )} - - ) : ( - <> - {/* Actions in this slot - Collapsed Icon View */} - {slot.actions.length === 0 ? ( -
-
-
- ) : ( -
- {slot.actions.map((action, actionIndex) => { - const localIconMap: Record = { - pyth: '/assets/pyth.svg', - switchboard: '/assets/switchboard.svg', - jupiter: '/assets/jupiter.svg', - raydium: '/assets/raydium.svg', - whirlpool: '/assets/whirlpool.svg', - drift: '/assets/drift.svg', - kamino: '/assets/kamino.svg', - }; - const iconSrc = localIconMap[action.protocolId] || '/assets/default.svg'; - - return ( -
- {action.protocol} + ) : ( +
+ {slot.actions.map((action, actionIndex) => { + const localIconMap: Record = { + pyth: '/assets/pyth.svg', + switchboard: '/assets/switchboard.svg', + jupiter: '/assets/jupiter.svg', + raydium: '/assets/raydium.svg', + whirlpool: '/assets/whirlpool.svg', + drift: '/assets/drift.svg', + kamino: '/assets/kamino.svg', + }; + const iconSrc = localIconMap[action.protocolId] || '/assets/default.svg'; + + return ( +
{ + if (mode === 'edit' && selectedSlotId === slot.id) { + setEditingAction({ slotId: slot.id, actionIndex }); + + // Load the action's protocol and set it as selected + const protocol = protocols.find((p) => p.id === action.protocolId); + if (protocol) { + setSelectedProtocol(protocol); + + // Find the specific action within the protocol + const foundAction = protocol.actions.find( + (a) => a.id === action.actionId + ); + if (foundAction) { + setSelectedAction(foundAction); + // Fetch account data for this action + await handleActionSelect(foundAction); + + // Restore the overrides and modified fields after loading default data + if (action.overrides) { + setAccountData(action.overrides); + } + if (action.modifiedFields) { + setModifiedFields(new Set(action.modifiedFields)); + } + if (action.fetchBeforeUse !== undefined) { + setFetchBeforeUse(action.fetchBeforeUse); + } + } + } + + setShowProtocolPanel(true); + } + }} + > +
+ {action.protocol} +
+
+
{action.action}
+
{action.protocol}
+
+ {/* Delete button - only in edit mode when slot is selected */} + {mode === 'edit' && selectedSlotId === slot.id && ( + + )} +
+ ); + })}
- ); - })} -
- )} - + )} + + ) : ( + <> + {/* Actions in this slot - Collapsed Icon View */} + {slot.actions.length === 0 ? ( +
+
+
+ ) : ( +
+ {slot.actions.map((action, actionIndex) => { + const localIconMap: Record = { + pyth: '/assets/pyth.svg', + switchboard: '/assets/switchboard.svg', + jupiter: '/assets/jupiter.svg', + raydium: '/assets/raydium.svg', + whirlpool: '/assets/whirlpool.svg', + drift: '/assets/drift.svg', + kamino: '/assets/kamino.svg', + }; + const iconSrc = localIconMap[action.protocolId] || '/assets/default.svg'; + + return ( +
+ {action.protocol} +
+ ); + })} +
+ )} + + )} + + +
+ + {/* Delete Button - only shown when slot is selected and in Edit mode */} + {mode === 'edit' && slots.length > 1 && selectedSlotId === slot.id && ( + )} - - -
+
+
+ + {/* Gap with insert button - shown when hovering the slot before OR the gap itself, only in Edit mode */} + {mode === 'edit' && ( +
+ {/* Vertical line - shorter and positioned lower */} +
- {/* Delete Button - only shown when slot is selected and in Edit mode */} - {mode === 'edit' && slots.length > 1 && selectedSlotId === slot.id && ( + {/* Plus button - centered on the line */} - )} - +
+ )} - - {/* Gap with insert button - shown when hovering the slot before OR the gap itself, only in Edit mode */} - {mode === 'edit' && ( -
- {/* Vertical line - shorter and positioned lower */} -
- - {/* Plus button - centered on the line */} - -
- )} - - ); - })} + ); + })}
@@ -1311,7 +1327,7 @@ export default function ScenarioEditor({ placeholder="Search protocols..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - className="relative block h-12 w-full rounded-full border border-zinc-700/50 bg-zinc-900/40 pr-5 pl-14 text-base text-zinc-100 shadow-lg backdrop-blur-2xl transition-all placeholder:text-zinc-500 focus:border-zinc-500 focus:ring-2 focus:ring-zinc-500 focus:outline-none" + className="relative block h-12 w-full rounded-full border border-zinc-700/50 bg-zinc-900/40 pl-14 pr-5 text-base text-zinc-100 shadow-lg backdrop-blur-2xl transition-all placeholder:text-zinc-500 focus:border-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-500" />
@@ -1431,7 +1447,7 @@ export default function ScenarioEditor({ placeholder="Search overrides..." value={actionSearchQuery} onChange={(e) => setActionSearchQuery(e.target.value)} - className="block h-10 w-full rounded-lg border border-zinc-700/50 bg-zinc-800/40 pr-4 pl-11 text-sm text-zinc-100 transition-all placeholder:text-zinc-500 focus:border-zinc-500 focus:ring-1 focus:ring-zinc-500 focus:outline-none" + className="block h-10 w-full rounded-lg border border-zinc-700/50 bg-zinc-800/40 pl-11 pr-4 text-sm text-zinc-100 transition-all placeholder:text-zinc-500 focus:border-zinc-500 focus:outline-none focus:ring-1 focus:ring-zinc-500" />
@@ -1458,7 +1474,7 @@ export default function ScenarioEditor({ <>
-

+

Account Data

@@ -1757,7 +1773,7 @@ export default function ScenarioEditor({ setValue(fieldPath, newValue); }} placeholder={`Enter ${String(field.name)}...`} - className={`block w-full rounded-lg border px-4 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:ring-2 focus:outline-none ${ + className={`block w-full rounded-lg border px-4 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:ring-2 ${ fieldState === 'override' ? 'border-yellow-500 bg-yellow-500/5 focus:border-yellow-400 focus:ring-yellow-500/50' : fieldState === 'streamed' @@ -1857,7 +1873,7 @@ export default function ScenarioEditor({ style={{ left: `${position}%`, transform: 'translateX(-50%)' }} > @@ -1865,7 +1881,7 @@ export default function ScenarioEditor({ {/* Small triangle tick pointing down */}
@@ -1917,7 +1933,7 @@ export default function ScenarioEditor({ {/* Pink progress overlay (ready state) - shows when in play mode */} {mode === 'play' && (
{/* Green dashed segment - always 12.5% of full bar */}
@@ -2044,7 +2061,7 @@ export default function ScenarioEditor({