diff --git a/app/vibenet/demos/catalogue.test.ts b/app/vibenet/demos/catalogue.test.ts index a0ffd52..d646ad0 100644 --- a/app/vibenet/demos/catalogue.test.ts +++ b/app/vibenet/demos/catalogue.test.ts @@ -47,7 +47,7 @@ describe('DEMOS', () => { it('keeps Validity Transactions reachable but hidden from the top-level grid', () => { const validity = DEMOS.find((demo) => demo.href === '/vibenet/demos/validity'); expect(validity?.title).toBe('Validity Transactions'); - expect(validity?.children?.map((demo) => demo.title)).toEqual(['Conditional Swaps', 'Race the Agent']); + expect(validity?.children?.map((demo) => demo.title)).toEqual(['Race the Agent', 'Conditional Swaps']); expect(listedDemos()).not.toContain(validity); }); }); diff --git a/app/vibenet/demos/catalogue.ts b/app/vibenet/demos/catalogue.ts index 83faa9d..16fa370 100644 --- a/app/vibenet/demos/catalogue.ts +++ b/app/vibenet/demos/catalogue.ts @@ -69,18 +69,6 @@ export const DEMOS: DemoEntry[] = [ available: true, listed: false, children: [ - { - href: '/vibenet/demos/validity/conditional-swaps', - title: 'Conditional Swaps', - summary: - 'Place a swap that waits for a target price, then lands or expires as a shared simulated market moves through its validity window.', - points: [ - 'Set a buy or sell price against a live VIBE/USDV pool', - 'Inspect the EIP-8130 predicates attached to the swap', - 'Watch pending orders fill, expire, or get replaced', - ], - available: true, - }, { href: '/vibenet/demos/validity/race-the-agent', title: 'Race the Agent', @@ -93,6 +81,18 @@ export const DEMOS: DemoEntry[] = [ ], available: true, }, + { + href: '/vibenet/demos/validity/conditional-swaps', + title: 'Conditional Swaps', + summary: + 'Place a swap that waits for a target price, then lands or expires as a shared simulated market moves through its validity window.', + points: [ + 'Set a buy or sell price against a live VIBE/USDV pool', + 'Inspect the EIP-8130 predicates attached to the swap', + 'Watch pending orders fill, expire, or get replaced', + ], + available: true, + }, ], }, ]; diff --git a/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx b/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx index 61a9844..8da3449 100644 --- a/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx +++ b/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx @@ -48,10 +48,8 @@ import { ensureSingleton, probeSingleton } from '../lib/singleton'; import { connectJsonRpcStream, headNumber, type StreamHead } from '../lib/stream'; import { attemptHistoryRows, - canResetRace, canSubmitManual, canSubmitValidity, - comparisonResult, isAttemptTerminal, preserveCompletedAttempt, randomAgentDwellMs, @@ -128,7 +126,6 @@ function RaceTheAgentDemoInner() { const [agentPhase, setAgentPhase] = useState('Waiting'); const [agentRestartToken, setAgentRestartToken] = useState(0); const [agentError, setAgentError] = useState(null); - const [streamLive, setStreamLive] = useState(false); const [prepared, setPrepared] = useState(false); const [setupRunning, setSetupRunning] = useState(false); const [setupError, setSetupError] = useState(null); @@ -146,7 +143,6 @@ function RaceTheAgentDemoInner() { const generationRef = useRef(0); const observedRef = useRef(null); - const validityRef = useRef(validity); const accountKeyRef = useRef(null); const setupInFlightKeyRef = useRef(null); const setupReadyKeyRef = useRef(null); @@ -156,7 +152,6 @@ function RaceTheAgentDemoInner() { const rpcSendRef = useRef(null); const engineRef = useRef(engine); engineRef.current = engine; - validityRef.current = validity; observedRef.current = observed; useEffect(() => { @@ -268,7 +263,6 @@ function RaceTheAgentDemoInner() { const startPoll = () => { if (pollId !== undefined) return; rpcSendRef.current = null; - setStreamLive(false); void syncState(); pollId = window.setInterval(() => void syncState(), STATE_FALLBACK_POLL_MS); }; @@ -289,7 +283,6 @@ function RaceTheAgentDemoInner() { stream.close(); return; } - setStreamLive(true); void syncState(); }; @@ -308,7 +301,6 @@ function RaceTheAgentDemoInner() { rpcSendRef.current = null; if (pollId !== undefined) window.clearInterval(pollId); stream?.close(); - setStreamLive(false); }; }, [applyObservation, client, vibe, withdrawal]); @@ -765,25 +757,6 @@ function RaceTheAgentDemoInner() { } }; - const reset = () => { - if (!canResetRace(validityRef.current, validBefore)) { - setError(`This validity transaction is still pending and can land until its ${RACE_VALIDITY_SECONDS}-second expiry. Keep watching the receipt and chain state.`); - return; - } - setValidity(EMPTY_ATTEMPT); - setValidityHistory([]); - setValidityAttemptCount(0); - setManual(EMPTY_ATTEMPT); - setManualHistory([]); - setManualAttemptCount(0); - setValidBefore(null); - setError(null); - setAgentError(null); - setObservations(observedRef.current ? [observedRef.current] : []); - }; - - const result = comparisonResult(validity, manual); - const resetAllowed = canResetRace(validity, validBefore); const readyToSubmit = prepared && observed?.enabled === false && canSubmitValidity(validity.status); const readyToWithdraw = canSubmitManual({ status: manual.status, @@ -803,58 +776,24 @@ function RaceTheAgentDemoInner() { - Reset race - - ) : undefined} /> - -
- +
+
- - {prepared ? 'Race setup ready' : setupError ? 'Automatic setup needs attention' : setupRunning ? 'Preparing race automatically' : 'Waiting to start setup'} - - - {setupError ?? progress ?? (prepared - ? agentRunning ? `Condition agent: ${agentPhase}` : 'Singleton funded; restarting condition agent.' - : 'Waiting for account and chain state.')} - -
-
- {setupError ? : null} -
- -
- -
-
- Shared onchain switch - Withdrawal condition -
- + Shared onchain switch + Withdrawal condition
-
+
{observed ? (observed.enabled ? '1' : '0') : '—'}
+
+ + +
-
- - - - +
+
+ Comparison + Same call, different timing. +
+ + Manual attempts begin on your click. Validity attempts can already be waiting when withdrawals open. + +
+ + +
- - {streamLive - ? 'WebSocket observations follow each Vibenet head at roughly 200ms cadence.' - : 'WebSocket unavailable; state observations are polling every second.'}{' '} - Inclusion blocks below remain the primary ordering evidence. - - +
Guided race - Submit first. React second. + Race first. Then submit ahead.
-
+
- - {agentRunning ? agentPhase : setupRunning ? 'Starting after setup' : 'Waiting for setup'} - + {setupError ? ( +
+ {setupError} + +
+ ) : ( + + {progress ?? (agentRunning ? agentPhase : setupRunning ? 'Starting after setup' : 'Waiting for setup')} + + )}
0} - > - - - + 0} + > + +
{(error || agentError) ? (
@@ -928,39 +879,11 @@ function RaceTheAgentDemoInner() {
) : null} -
-
-
-
- Comparison - Same call, different timing model -
- -
-
- - -
-
- - -
-
+
+
Advanced details @@ -969,10 +892,30 @@ function RaceTheAgentDemoInner() {
- - Solidity stores the first state variable, enabled, - in slot 0. The validity transaction reads that slot directly and becomes eligible only when the boolean is true. - +
+ + The contract stores the first state variable, enabled, + in slot 0. The validity transaction reads that slot directly and becomes eligible only when the boolean is true. + + {withdrawal ? ( +
+
+ Singleton contract + +
+
+ Contract balance + + {contractBalance === null ? '—' : `${formatCompactVibe(contractBalance)} VIBE`} + +
+
+ ) : null} +
@@ -980,7 +923,7 @@ function RaceTheAgentDemoInner() {
-
+
Observed chain state
@@ -1007,20 +950,8 @@ function RaceTheAgentDemoInner() {
- - What the result means - Blocks beat stopwatches. - - The timestamps show when this browser sampled state or received a receipt. They are useful context, not authoritative sequencing. The lower included block landed first; the same block is a tie at this resolution. - - {withdrawal ? ( -
- Global singleton - -
- ) : null} -
+ ); } @@ -1094,29 +1025,14 @@ function formatCompactVibe(value: bigint): string { return Number(whole).toLocaleString(undefined, { notation: 'compact', maximumFractionDigits: 1 }); } -function ConditionPill({ enabled }: { enabled: boolean | null }) { - return ( - - {enabled === null ? 'unobserved' : enabled ? 'enabled · 1' : 'disabled · 0'} - - ); -} - function AttemptHistoryCard({ title, attempts }: { title: string; attempts: Attempt[] }) { return ( - +
{title} {attempts.length} total
-
+
{attempts.length === 0 ? ( No previous attempts yet. ) : attempts.map((attempt, index) => ( @@ -1225,7 +1141,7 @@ function RaceStep({ return (
@@ -1233,54 +1149,13 @@ function RaceStep({ {number}
- {title} - {detail} -
{children}
+ {title} + {detail} +
{children}
); } -function AttemptCard({ - label, - headline, - description, - attempt, - accent, -}: { - label: string; - headline: string; - description: string; - attempt: Attempt; - accent: 'blue' | 'green'; -}) { - const explorer = attempt.hash ? `${VIBENET_EXPLORER_PATH}/tx/${attempt.hash}` : null; - return ( - - -
-
- {label} - {headline} -
- -
- {description} -
- - - - -
- {attempt.error ? {attempt.error} : null} - {explorer ? ( - - View transaction in explorer - - ) : null} -
- ); -} - function StatusPill({ status }: { status: Attempt['status'] }) { const positive = status === 'success'; const negative = status === 'reverted' || status === 'expired' || status === 'error'; @@ -1288,7 +1163,7 @@ function StatusPill({ status }: { status: Attempt['status'] }) { ); } - -function ResultPill({ result, validity, manual }: { result: ReturnType; validity: Attempt; manual: Attempt }) { - let label = 'Race in progress'; - if (result === 'validity-first') label = 'Validity landed first'; - if (result === 'manual-first') label = 'Manual landed first'; - if (result === 'same-block') label = 'Same inclusion block'; - if (result === 'validity-only') label = 'Only validity succeeded'; - if (result === 'manual-only') label = 'Only manual succeeded'; - if (result === 'neither-succeeded') label = 'Neither transaction succeeded'; - if (result === 'none' && validity.status === 'idle' && manual.status === 'idle') label = 'Not started'; - return {label}; -} diff --git a/app/vibenet/demos/validity/race-the-agent/comparison.test.ts b/app/vibenet/demos/validity/race-the-agent/comparison.test.ts index 821037f..e007c69 100644 --- a/app/vibenet/demos/validity/race-the-agent/comparison.test.ts +++ b/app/vibenet/demos/validity/race-the-agent/comparison.test.ts @@ -4,10 +4,8 @@ import { AGENT_DISABLED_DWELL_MAX_MS, AGENT_DISABLED_DWELL_MIN_MS, attemptHistoryRows, - canResetRace, canSubmitManual, canSubmitValidity, - comparisonResult, isAttemptTerminal, preserveCompletedAttempt, randomAgentDwellMs, @@ -24,27 +22,6 @@ import { blockNumberPredicate } from '../lib/predicates'; const WITHDRAWAL = '0x1111111111111111111111111111111111111111'; -function success(block: bigint): Attempt { - return { status: 'success', includedBlock: block }; -} - -describe('comparisonResult', () => { - it('uses inclusion block ordering instead of browser timestamps', () => { - expect(comparisonResult(success(10n), { ...success(11n), includedAt: 1 })).toBe('validity-first'); - expect(comparisonResult({ ...success(12n), includedAt: 1 }, success(11n))).toBe('manual-first'); - expect(comparisonResult(success(12n), success(12n))).toBe('same-block'); - }); - - it('describes one-sided and unfinished outcomes', () => { - expect(comparisonResult(success(10n), { status: 'reverted' })).toBe('validity-only'); - expect(comparisonResult({ status: 'expired' }, success(10n))).toBe('manual-only'); - expect(comparisonResult(success(10n), { status: 'idle' })).toBe('none'); - expect(comparisonResult({ status: 'pending' }, { status: 'idle' })).toBe('none'); - expect(comparisonResult({ status: 'expired' }, { status: 'idle' })).toBe('neither-succeeded'); - expect(comparisonResult({ status: 'reverted' }, { status: 'error' })).toBe('neither-succeeded'); - }); -}); - describe('isAttemptTerminal', () => { it('only stops on final receipt or expiry states', () => { expect(isAttemptTerminal('success')).toBe(true); @@ -63,13 +40,6 @@ describe('race lifecycle predicates', () => { expect(fields.validBefore).toBe(BigInt(now + 15_000)); }); - it('blocks reset only while a submitted validity transaction can still land', () => { - expect(canResetRace({ status: 'pending' }, 20_000, 10_000)).toBe(false); - expect(canResetRace({ status: 'pending' }, null, 20_001)).toBe(false); - expect(canResetRace({ status: 'pending' }, 20_000, 20_001)).toBe(true); - expect(canResetRace({ status: 'expired' }, 20_000, 10_000)).toBe(true); - }); - it('runs the condition agent only when automatic setup resources are ready', () => { expect(shouldRunConditionAgent({ prepared: true, hasAgent: true, hasClient: true, hasContract: true })).toBe(true); expect(shouldRunConditionAgent({ prepared: false, hasAgent: true, hasClient: true, hasContract: true })).toBe(false); diff --git a/app/vibenet/demos/validity/race-the-agent/comparison.ts b/app/vibenet/demos/validity/race-the-agent/comparison.ts index 2371db9..d2cd2b4 100644 --- a/app/vibenet/demos/validity/race-the-agent/comparison.ts +++ b/app/vibenet/demos/validity/race-the-agent/comparison.ts @@ -22,43 +22,10 @@ export type Attempt = { error?: string; }; -export type ComparisonResult = - | 'validity-first' - | 'manual-first' - | 'same-block' - | 'validity-only' - | 'manual-only' - | 'neither-succeeded' - | 'none'; - -export function comparisonResult(validity: Attempt, manual: Attempt): ComparisonResult { - const validityLanded = validity.status === 'success' && validity.includedBlock !== undefined; - const manualLanded = manual.status === 'success' && manual.includedBlock !== undefined; - if (validityLanded && manualLanded) { - if (validity.includedBlock! < manual.includedBlock!) return 'validity-first'; - if (manual.includedBlock! < validity.includedBlock!) return 'manual-first'; - return 'same-block'; - } - if (validityLanded && isAttemptTerminal(manual.status)) return 'validity-only'; - if (manualLanded && isAttemptTerminal(validity.status)) return 'manual-only'; - if ( - (!validityLanded && isAttemptTerminal(validity.status) && manual.status === 'idle') || - (!validityLanded && !manualLanded && isAttemptTerminal(validity.status) && isAttemptTerminal(manual.status)) - ) { - return 'neither-succeeded'; - } - return 'none'; -} - export function isAttemptTerminal(status: AttemptStatus): boolean { return status === 'success' || status === 'reverted' || status === 'expired' || status === 'error'; } -export function canResetRace(validity: Attempt, validBefore: number | null, now = Date.now()): boolean { - if (validity.status !== 'pending') return true; - return validBefore !== null && now > validBefore; -} - export function canSubmitAttempt(status: AttemptStatus): boolean { return status !== 'pending' && status !== 'submitting'; }