diff --git a/app/vibenet/demos/catalogue.test.ts b/app/vibenet/demos/catalogue.test.ts index 12f029d..d646ad0 100644 --- a/app/vibenet/demos/catalogue.test.ts +++ b/app/vibenet/demos/catalogue.test.ts @@ -44,10 +44,11 @@ describe('DEMOS', () => { expect(new Set(hrefs).size).toBe(hrefs.length); }); - it('lists Validity Transactions as a top-level group', () => { - const validity = listedDemos().find((demo) => demo.href === '/vibenet/demos/validity'); + 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 6aa8b24..16fa370 100644 --- a/app/vibenet/demos/catalogue.ts +++ b/app/vibenet/demos/catalogue.ts @@ -67,19 +67,8 @@ export const DEMOS: DemoEntry[] = [ 'Build intent-like flows from ordinary account transactions', ], 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', @@ -92,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/lib/artifacts/ConditionalWithdrawal.json b/app/vibenet/demos/validity/lib/artifacts/ConditionalWithdrawal.json index 9953d93..fbc08f9 100644 --- a/app/vibenet/demos/validity/lib/artifacts/ConditionalWithdrawal.json +++ b/app/vibenet/demos/validity/lib/artifacts/ConditionalWithdrawal.json @@ -12,13 +12,11 @@ "source": "../contracts/ConditionalWithdrawal.sol", "abi": [ {"inputs":[{"internalType":"contract IERC20","name":"vibe","type":"address"}],"stateMutability":"nonpayable","type":"constructor"}, - {"inputs":[],"name":"ENABLED_SLOT","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"}, {"inputs":[],"name":"VIBE","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"}, {"inputs":[],"name":"WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}, - {"inputs":[],"name":"enabled","outputs":[{"internalType":"bool","name":"value","type":"bool"}],"stateMutability":"view","type":"function"}, - {"inputs":[],"name":"flip","outputs":[{"internalType":"bool","name":"value","type":"bool"}],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[],"name":"enabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"bool","name":"value","type":"bool"}],"name":"setEnabled","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"} ], - "bytecode": "0x60a060405234801561000f575f80fd5b506040516103b03803806103b083398101604081905261002e9161003f565b6001600160a01b031660805261006c565b5f6020828403121561004f575f80fd5b81516001600160a01b0381168114610065575f80fd5b9392505050565b60805161032661008a5f395f8181608301526101d101526103265ff3fe608060405234801561000f575f80fd5b506004361061007a575f3560e01c8063328d8f7211610058578063328d8f72146101065780633ccfd60b14610127578063848606331461012f578063cde4efa91461013e575f80fd5b806304a3b7cd1461007e57806304c879d6146100c2578063238dafe0146100e4575b5f80fd5b6100a57f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b6100d65f805160206102d183398151915281565b6040519081526020016100b9565b5f805160206102d18339815191525415155b60405190151581526020016100b9565b610125610114366004610293565b5f805160206102d183398151915255565b005b610125610158565b6100d6670de0b6b3a764000081565b5f805160206102d1833981519152805415908190556100f6565b5f805160206102d1833981519152546101ae5760405162461bcd60e51b81526020600482015260136024820152721dda5d1a191c985dd85b08191a5cd8589b1959606a1b60448201526064015b60405180910390fd5b60405163a9059cbb60e01b8152336004820152670de0b6b3a764000060248201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303815f875af115801561021f573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061024391906102b5565b6102815760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016101a5565b565b8015158114610290575f80fd5b50565b5f602082840312156102a3575f80fd5b81356102ae81610283565b9392505050565b5f602082840312156102c5575f80fd5b81516102ae8161028356fea91a9aee734204743335c443df931dcb220441d8aa6c1355dc61503a4bec3129a264697066735822122086d03dd0ac93876dcc7d8450420e92071a584f9b6452b41d07282203bd45a11f64736f6c63430008180033" + "bytecode": "0x60a060405234801561000f575f80fd5b5060405161032e38038061032e83398101604081905261002e9161003f565b6001600160a01b031660805261006c565b5f6020828403121561004f575f80fd5b81516001600160a01b0381168114610065575f80fd5b9392505050565b6080516102a461008a5f395f8181605e015261016f01526102a45ff3fe608060405234801561000f575f80fd5b5060043610610055575f3560e01c806304a3b7cd14610059578063238dafe01461009d578063328d8f72146100b95780633ccfd60b146100db57806384860633146100e3575b5f80fd5b6100807f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b5f546100a99060ff1681565b6040519015158152602001610094565b6100d96100c7366004610231565b5f805460ff1916911515919091179055565b005b6100d9610100565b6100f2670de0b6b3a764000081565b604051908152602001610094565b5f5460ff1661014c5760405162461bcd60e51b81526020600482015260136024820152721dda5d1a191c985dd85b08191a5cd8589b1959606a1b60448201526064015b60405180910390fd5b60405163a9059cbb60e01b8152336004820152670de0b6b3a764000060248201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303815f875af11580156101bd573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906101e19190610253565b61021f5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606401610143565b565b801515811461022e575f80fd5b50565b5f60208284031215610241575f80fd5b813561024c81610221565b9392505050565b5f60208284031215610263575f80fd5b815161024c8161022156fea26469706673582212204d29e01f9634f2f7d9fe8f91c0bdd1dafbb7f3368af71bef06b019d1634aacf764736f6c63430008180033" } diff --git a/app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts b/app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts index ab7e459..01077df 100644 --- a/app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts +++ b/app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts @@ -1,4 +1,4 @@ -import { decodeFunctionData, keccak256, toBytes } from 'viem'; +import { decodeFunctionData } from 'viem'; import { describe, expect, it } from 'vitest'; import artifact from './artifacts/ConditionalWithdrawal.json'; @@ -14,7 +14,6 @@ import { conditionalWithdrawalFundingAmount, encodeConditionalWithdrawalFunding, encodeConditionalWithdraw, - encodeFlipConditionalWithdrawal, encodeSetConditionalWithdrawalEnabled, predictConditionalWithdrawal, } from './conditionalWithdrawal'; @@ -39,21 +38,19 @@ describe('conditional withdrawal contract', () => { expect(CONDITIONAL_WITHDRAWAL_SALT).toBe( '0x75dea569b8cc7d9ea45d7d95a5d6bed33e1e378a31715724342462f8226adc8b', ); - expect(predictConditionalWithdrawal(VIBE)).toBe('0x7AE1BFB6116D154a0a27961a5d19C544D02015a9'); + expect(predictConditionalWithdrawal(VIBE)).toBe('0xC655E339224d57B087A02Df7827A4068ae69aba1'); expect(predictConditionalWithdrawal(OTHER_VIBE)).not.toBe(predictConditionalWithdrawal(VIBE)); }); - it('pins the stable storage word and exact enabled=true EIP-8130 predicate', () => { - expect(CONDITIONAL_WITHDRAWAL_ENABLED_SLOT).toBe( - keccak256(toBytes('vibenet.validity.conditional-withdrawal.enabled.v1')), - ); - expect(CONDITIONAL_WITHDRAWAL_ENABLED_MASK).toBe((1n << 256n) - 1n); + it('reads bool public enabled from slot 0 in the EIP-8130 predicate', () => { + expect(CONDITIONAL_WITHDRAWAL_ENABLED_SLOT).toBe(0n); + expect(CONDITIONAL_WITHDRAWAL_ENABLED_MASK).toBe(0xffn); expect(conditionalWithdrawalEnabledPredicate(WITHDRAWAL)).toEqual({ type: 'storage', params: { address: WITHDRAWAL, - slot: CONDITIONAL_WITHDRAWAL_ENABLED_SLOT, - mask: toWord((1n << 256n) - 1n), + slot: toWord(0n), + mask: toWord(0xffn), op: '=', value: toWord(1n), }, @@ -61,6 +58,14 @@ describe('conditional withdrawal contract', () => { }); it('encodes condition and fixed-withdrawal calls exactly', () => { + const functionNames = artifact.abi + .filter((item) => item.type === 'function') + .map((item) => item.name); + expect(functionNames).not.toContain('ENABLED_SLOT'); + expect(functionNames).not.toContain('flip'); + expect(functionNames).toContain('enabled'); + expect(functionNames).toContain('setEnabled'); + expect(functionNames).toContain('withdraw'); expect(encodeSetConditionalWithdrawalEnabled(WITHDRAWAL, true)).toEqual({ to: WITHDRAWAL, data: `0x328d8f72${'0'.repeat(63)}1`, @@ -68,10 +73,6 @@ describe('conditional withdrawal contract', () => { expect(encodeSetConditionalWithdrawalEnabled(WITHDRAWAL, false).data).toBe( `0x328d8f72${'0'.repeat(64)}`, ); - expect(encodeFlipConditionalWithdrawal(WITHDRAWAL)).toEqual({ - to: WITHDRAWAL, - data: '0xcde4efa9', - }); expect(encodeConditionalWithdraw(WITHDRAWAL)).toEqual({ to: WITHDRAWAL, data: '0x3ccfd60b', diff --git a/app/vibenet/demos/validity/lib/conditionalWithdrawal.ts b/app/vibenet/demos/validity/lib/conditionalWithdrawal.ts index 0d26477..7959aeb 100644 --- a/app/vibenet/demos/validity/lib/conditionalWithdrawal.ts +++ b/app/vibenet/demos/validity/lib/conditionalWithdrawal.ts @@ -24,9 +24,9 @@ import type { StoragePredicate } from './types'; export const conditionalWithdrawalAbi = artifact.abi as Abi; export const conditionalWithdrawalBytecode = artifact.bytecode as Hex; -export const CONDITIONAL_WITHDRAWAL_ENABLED_SLOT = - '0xa91a9aee734204743335c443df931dcb220441d8aa6c1355dc61503a4bec3129' as Hex; -export const CONDITIONAL_WITHDRAWAL_ENABLED_MASK = (1n << 256n) - 1n; +/** `bool public enabled` is the contract's first storage variable. */ +export const CONDITIONAL_WITHDRAWAL_ENABLED_SLOT = 0n; +export const CONDITIONAL_WITHDRAWAL_ENABLED_MASK = 0xffn; export const CONDITIONAL_WITHDRAWAL_AMOUNT = WAD; export const CONDITIONAL_WITHDRAWAL_REFILL_THRESHOLD = 1_000_000n * WAD; export const CONDITIONAL_WITHDRAWAL_FUNDING_TARGET = 2_000_000n * WAD; @@ -171,13 +171,6 @@ export function encodeSetConditionalWithdrawalEnabled( }; } -export function encodeFlipConditionalWithdrawal(withdrawal: Address): { to: Address; data: Hex } { - return { - to: withdrawal, - data: encodeFunctionData({ abi: conditionalWithdrawalAbi, functionName: 'flip' }), - }; -} - export function encodeConditionalWithdraw(withdrawal: Address): { to: Address; data: Hex } { return { to: withdrawal, @@ -185,11 +178,11 @@ export function encodeConditionalWithdraw(withdrawal: Address): { to: Address; d }; } -/** EIP-8130 condition requiring the stable enabled bit to equal true. */ +/** EIP-8130 condition requiring `bool public enabled` in storage slot 0 to be true. */ export function conditionalWithdrawalEnabledPredicate(withdrawal: Address): StoragePredicate { return storagePredicate( withdrawal, - BigInt(CONDITIONAL_WITHDRAWAL_ENABLED_SLOT), + CONDITIONAL_WITHDRAWAL_ENABLED_SLOT, CONDITIONAL_WITHDRAWAL_ENABLED_MASK, '=', 1n, diff --git a/app/vibenet/demos/validity/lib/contracts/ConditionalWithdrawal.sol b/app/vibenet/demos/validity/lib/contracts/ConditionalWithdrawal.sol index 253fc64..518ff66 100644 --- a/app/vibenet/demos/validity/lib/contracts/ConditionalWithdrawal.sol +++ b/app/vibenet/demos/validity/lib/contracts/ConditionalWithdrawal.sol @@ -5,43 +5,23 @@ interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); } -/// @notice Permissionless condition switch backed by one stable storage word. +/// @notice Permissionless VIBE withdrawal controlled by a boolean condition. contract ConditionalWithdrawal { - /// @dev Stable forever: keccak256("vibenet.validity.conditional-withdrawal.enabled.v1"). - bytes32 public constant ENABLED_SLOT = - 0xa91a9aee734204743335c443df931dcb220441d8aa6c1355dc61503a4bec3129; uint256 public constant WITHDRAWAL_AMOUNT = 1 ether; IERC20 public immutable VIBE; + bool public enabled; constructor(IERC20 vibe) { VIBE = vibe; } - function enabled() public view returns (bool value) { - bytes32 slot = ENABLED_SLOT; - assembly { - value := iszero(iszero(sload(slot))) - } - } - function setEnabled(bool value) external { - bytes32 slot = ENABLED_SLOT; - assembly { - sstore(slot, value) - } - } - - function flip() external returns (bool value) { - bytes32 slot = ENABLED_SLOT; - assembly { - value := iszero(sload(slot)) - sstore(slot, value) - } + enabled = value; } function withdraw() external { - require(enabled(), "withdrawal disabled"); + require(enabled, "withdrawal disabled"); require(VIBE.transfer(msg.sender, WITHDRAWAL_AMOUNT), "transfer failed"); } } diff --git a/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx b/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx index c29c528..0dd1e94 100644 --- a/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx +++ b/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx @@ -20,6 +20,7 @@ import { VIBENET_EXPLORER_PATH, VIBENET_WS_URL } from '../../../library/config'; import { CopyableValue } from '../../../components/CopyableValue'; import { AccountDemoShell } from '../../_components/AccountDemoShell'; import { DemoHeader } from '../../_components/DemoHeader'; +import { ChevronIcon } from '../../_shared/dropdown'; import { newCallRow } from '../../account/library/calls'; import type { StoredAccount } from '../../account/library/model'; import { aaReceiptSucceeded, type AaReceiptLike } from '../../account/library/receipt'; @@ -46,10 +47,8 @@ import { probeSingleton } from '../lib/singleton'; import { connectJsonRpcStream, headNumber, type StreamHead } from '../lib/stream'; import { attemptHistoryRows, - canResetRace, canSubmitManual, canSubmitValidity, - comparisonResult, isAttemptTerminal, preserveCompletedAttempt, randomAgentDwellMs, @@ -76,6 +75,28 @@ type Observation = { enabled: boolean; block: bigint; at: number }; type AgentPhase = 'Waiting' | 'Scheduling' | 'Opening' | 'Closing' | 'Retrying'; const EMPTY_ATTEMPT: Attempt = { status: 'idle' }; +const CONTRACT_SNIPPET = `interface IERC20 { + function transfer(address to, uint256 amount) external returns (bool); +} + +contract ConditionalWithdrawal { + uint256 public constant WITHDRAWAL_AMOUNT = 1 ether; + IERC20 public immutable VIBE; + bool public enabled; + + constructor(IERC20 vibe) { + VIBE = vibe; + } + + function setEnabled(bool value) external { + enabled = value; + } + + function withdraw() external { + require(enabled, "withdrawal disabled"); + require(VIBE.transfer(msg.sender, WITHDRAWAL_AMOUNT), "transfer failed"); + } +}`; function rootAccount(account: StoredAccount, accounts: StoredAccount[]): StoredAccount { let current = account; @@ -116,7 +137,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); @@ -134,7 +154,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); @@ -144,7 +163,6 @@ function RaceTheAgentDemoInner() { const rpcSendRef = useRef(null); const engineRef = useRef(engine); engineRef.current = engine; - validityRef.current = validity; observedRef.current = observed; useEffect(() => { @@ -256,7 +274,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); }; @@ -277,7 +294,6 @@ function RaceTheAgentDemoInner() { stream.close(); return; } - setStreamLive(true); void syncState(); }; @@ -296,7 +312,6 @@ function RaceTheAgentDemoInner() { rpcSendRef.current = null; if (pollId !== undefined) window.clearInterval(pollId); stream?.close(); - setStreamLive(false); }; }, [applyObservation, client, vibe, withdrawal]); @@ -748,25 +763,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, @@ -778,63 +774,32 @@ function RaceTheAgentDemoInner() { }); const validityAttempts = attemptHistoryRows(validity, validityHistory); const manualAttempts = attemptHistoryRows(manual, manualHistory); + const predicateSnippet = withdrawal + ? JSON.stringify(conditionalWithdrawalEnabledPredicate(withdrawal), null, 2) + : 'Resolving the conditional withdrawal address…'; return ( - 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) ? (
@@ -908,39 +885,51 @@ function RaceTheAgentDemoInner() {
) : null} +
-
-
+
+
+
- Comparison - Same call, different timing model + Advanced details + Contract and validity predicate +
+ +
+
+
+ + 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} +
+
+ +
- -
-
- - -
-
- -
-
+ -
+
Observed chain state
@@ -967,20 +956,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} -
+ ); } @@ -1054,29 +1031,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) => ( @@ -1102,6 +1064,62 @@ function AttemptHistoryCard({ title, attempts }: { title: string; attempts: Atte ); } +type CodeLanguage = 'json' | 'solidity'; +type CodeToken = { text: string; kind: 'comment' | 'key' | 'keyword' | 'literal' | 'number' | 'plain' | 'string' | 'type' }; + +const CODE_TOKEN_CLASS: Record = { + comment: 'text-bds-gray-50 dark:text-[#7f8c98]', + key: 'text-base-blue dark:text-[#7eb8ff]', + keyword: 'text-purple-700 dark:text-[#c792ea]', + literal: 'text-bds-orange-70 dark:text-[#ff9d76]', + number: 'text-bds-orange-70 dark:text-[#f5c542]', + plain: 'text-bds-gray-80 dark:text-[#d6deeb]', + string: 'text-bds-green-70 dark:text-[#7ee0a8]', + type: 'text-base-blue dark:text-[#82aaff]', +}; + +function tokenizeCode(source: string, language: CodeLanguage): CodeToken[] { + const pattern = language === 'json' + ? /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?|[{}[\]:,])/g + : /(\/\/[^\n]*|"(?:\\.|[^"\\])*"|\b(?:contract|interface|function|external|public|immutable|constant|returns|return|require|bool|uint256|address)\b|\b(?:IERC20|ConditionalWithdrawal)\b|\b\d+(?:\s+ether)?\b)/g; + const tokens: CodeToken[] = []; + let last = 0; + for (const hit of source.matchAll(pattern)) { + const text = hit[0]; + const index = hit.index ?? 0; + if (index > last) tokens.push({ text: source.slice(last, index), kind: 'plain' }); + let kind: CodeToken['kind'] = 'plain'; + if (language === 'json') { + if (text.startsWith('"')) kind = text.endsWith(':') ? 'key' : 'string'; + else if (text === 'true' || text === 'false' || text === 'null') kind = 'literal'; + else if (/^-?\d/.test(text)) kind = 'number'; + } else if (text.startsWith('//')) kind = 'comment'; + else if (text.startsWith('"')) kind = 'string'; + else if (/^\d/.test(text)) kind = 'number'; + else if (text === 'IERC20' || text === 'ConditionalWithdrawal') kind = 'type'; + else kind = 'keyword'; + tokens.push({ text, kind }); + last = index + text.length; + } + if (last < source.length) tokens.push({ text: source.slice(last), kind: 'plain' }); + return tokens; +} + +function CodeSnippet({ label, code, language }: { label: string; code: string; language: CodeLanguage }) { + return ( + + {label} +
+        
+          {tokenizeCode(code, language).map((token, index) => (
+            {token.text}
+          ))}
+        
+      
+
+ ); +} + function Metric({ label, value }: { label: string; value: string }) { return (
@@ -1129,7 +1147,7 @@ function RaceStep({ return (
@@ -1137,54 +1155,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'; @@ -1192,7 +1169,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'; }