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 ? (
-
- ) : (
-
- {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);
- }
- }}
- >
-
-
-
+
+
+ {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 && (
-
{
- e.stopPropagation();
- deleteActionFromSlot(slot.id, actionIndex);
- }}
- className="absolute right-2 bottom-2 text-zinc-500 transition-colors hover:text-zinc-300"
- title="Delete action"
- >
-
-
- )}
- );
- })}
-
- )}
- >
- ) : (
- <>
- {/* 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 (
-
-
+ ) : (
+
+ {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.action}
+
{action.protocol}
+
+ {/* Delete button - only in edit mode when slot is selected */}
+ {mode === 'edit' && selectedSlotId === slot.id && (
+
{
+ e.stopPropagation();
+ deleteActionFromSlot(slot.id, actionIndex);
+ }}
+ className="absolute bottom-2 right-2 text-zinc-500 transition-colors hover:text-zinc-300"
+ title="Delete action"
+ >
+
+
+ )}
+
+ );
+ })}
- );
- })}
-
- )}
- >
+ )}
+ >
+ ) : (
+ <>
+ {/* 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 (
+
+
+
+ );
+ })}
+
+ )}
+ >
+ )}
+
+
+
+
+ {/* Delete Button - only shown when slot is selected and in Edit mode */}
+ {mode === 'edit' && slots.length > 1 && selectedSlotId === slot.id && (
+ {
+ e.stopPropagation();
+ deleteSlot(slot.id);
+ }}
+ className="absolute -right-3 -top-3 flex h-8 w-8 items-center justify-center rounded-full bg-red-500 text-white shadow-lg transition-all hover:scale-110 hover:bg-red-600"
+ title="Delete slot"
+ >
+
+
)}
-
-
-
+
+
+
+ {/* 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 */}
{
- e.stopPropagation();
- deleteSlot(slot.id);
- }}
- className="absolute -top-3 -right-3 flex h-8 w-8 items-center justify-center rounded-full bg-red-500 text-white shadow-lg transition-all hover:scale-110 hover:bg-red-600"
- title="Delete slot"
+ onClick={() => insertSlotAt(index + 1)}
+ className="absolute z-10 flex h-8 w-8 items-center justify-center rounded-full bg-pink-500 text-white opacity-0 shadow-lg transition-all hover:scale-110 hover:bg-pink-600 group-hover/insert:opacity-100 group-hover/slot-wrapper:opacity-100"
+ style={{ top: '170px', left: '50%', transform: 'translateX(-50%)' }}
+ title="Insert slot here"
>
-
+
- )}
-
+
+ )}
-
- {/* 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 */}
-
insertSlotAt(index + 1)}
- className="absolute z-10 flex h-8 w-8 items-center justify-center rounded-full bg-pink-500 text-white opacity-0 shadow-lg transition-all group-hover/insert:opacity-100 group-hover/slot-wrapper:opacity-100 hover:scale-110 hover:bg-pink-600"
- style={{ top: '170px', left: '50%', transform: 'translateX(-50%)' }}
- title="Insert slot here"
- >
-
-
-
- )}
-
- );
- })}
+ );
+ })}
@@ -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({