diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index e53bfd2e..b7a65eda 100644 --- a/app/vibenet/demos/validity/ValidityDemo.tsx +++ b/app/vibenet/demos/validity/ValidityDemo.tsx @@ -46,6 +46,7 @@ import { VIBENET_WS_URL } from '../../library/config'; import { ageRestoredOrders, maxBlockForExpiry, + minBlockForDelay, occupyingOrder, orderBlockExpired, orderWallClockExpired, @@ -53,7 +54,7 @@ import { } from './lib/orders'; import { bumpReplacementFees, feesFromHead, isReplacementUnderpriced, padFees } from './lib/fees'; import { reviewClauses } from './lib/annotate'; -import { applyOffsetBps, blockExpiryPredicate, formatPrice, priceValidity } from './lib/predicates'; +import { applyOffsetBps, blockDelayPredicate, blockExpiryPredicate, formatPrice, priceValidity } from './lib/predicates'; import { ammPriceFromQuote, ammSide, @@ -124,6 +125,7 @@ function ValidityDemoInner() { const [offsetBps, setOffsetBps] = useState(100); const [priceOverrideWad, setPriceOverrideWad] = useState(null); const [expirySeconds, setExpirySeconds] = useState(15); + const [delaySeconds, setDelaySeconds] = useState(0); const [submitMode, setSubmitMode] = useState('concurrent'); const [orders, setOrders] = useState([]); const [hoveredOrderId, setHoveredOrderId] = useState(null); @@ -576,9 +578,14 @@ function ValidityDemoInner() { submitMode === 'concurrent' ? clampNoncelessExpiry(expirySeconds) : Math.min(MAX_EXPIRY_SECONDS, expirySeconds); - const maxBlock = maxBlockForExpiry(blockNumber, seconds); - return [...draft.predicates, blockExpiryPredicate(maxBlock)]; - }, [blockNumber, draft, expirySeconds, submitMode]); + const cap = submitMode === 'concurrent' ? MAX_NONCELESS_SECONDS : MAX_EXPIRY_SECONDS; + const delay = Math.max(0, Math.min(delaySeconds, cap - seconds)); + const delayBlock = delay > 0 ? minBlockForDelay(blockNumber, delay) : blockNumber; + const maxBlock = maxBlockForExpiry(delayBlock, seconds); + const predicates = [...draft.predicates, blockExpiryPredicate(maxBlock)]; + if (delay > 0) predicates.push(blockDelayPredicate(delayBlock)); + return predicates; + }, [blockNumber, delaySeconds, draft, expirySeconds, submitMode]); const chartLevels = useMemo((): PriceLevel[] => { const levels: PriceLevel[] = []; @@ -738,8 +745,12 @@ function ValidityDemoInner() { ? clampNoncelessExpiry(expirySeconds) : Math.min(MAX_EXPIRY_SECONDS, expirySeconds); const block = blockNumber ?? (await publicClient.getBlockNumber({ cacheTime: 0 })); - const maxBlock = maxBlockForExpiry(block, seconds); + const cap = submitMode === 'concurrent' ? MAX_NONCELESS_SECONDS : MAX_EXPIRY_SECONDS; + const delay = Math.max(0, Math.min(delaySeconds, cap - seconds)); + const minBlock = delay > 0 ? minBlockForDelay(block, delay) : undefined; + const maxBlock = maxBlockForExpiry(minBlock ?? block, seconds); const validity = [...draft.predicates, blockExpiryPredicate(maxBlock)]; + if (minBlock !== undefined) validity.push(blockDelayPredicate(minBlock)); const fromHead = headFeesRef.current; const estimated = fromHead ?? @@ -819,8 +830,10 @@ function ValidityDemoInner() { targetPriceWad: draft.priceWad, size: TRADE_VIBE, expirySeconds: seconds, + delaySeconds: delay > 0 ? delay : undefined, submitMode, maxBlock, + minBlock, submittedAt: Date.now(), txHash: hash, nonce, @@ -844,7 +857,7 @@ function ValidityDemoInner() { engine.pushActivity({ kind: 'transact', title: `Validity ${side} submitted`, - detail: submitMode === 'concurrent' ? '8130 concurrent' : '8130 replace', + detail: submitMode === 'concurrent' ? '8130 concurrent' : 'sequential replace', account: acct.address, txHash: hash, network: engine.chain.name, @@ -955,6 +968,7 @@ function ValidityDemoInner() { side={side} offsetBps={offsetBps} expirySeconds={expirySeconds} + delaySeconds={delaySeconds} submitMode={submitMode} busy={busy} vibeBalance={vibeBalance} @@ -971,11 +985,13 @@ function ValidityDemoInner() { }} onPriceOverride={setPriceOverrideWad} onExpiry={setExpirySeconds} + onDelay={setDelaySeconds} onSubmitMode={(mode) => { setSubmitMode(mode); - if (mode === 'concurrent' && expirySeconds > MAX_NONCELESS_SECONDS) { - setExpirySeconds(15); - } + if (mode !== 'concurrent') return; + const nextExpiry = expirySeconds > MAX_NONCELESS_SECONDS ? 15 : expirySeconds; + if (nextExpiry !== expirySeconds) setExpirySeconds(nextExpiry); + if (delaySeconds + nextExpiry > MAX_NONCELESS_SECONDS) setDelaySeconds(0); }} onSubmit={() => { setError(null); @@ -1030,8 +1046,10 @@ function ValidityDemoInner() { {draft.side === 'buy' ? '≤' : '≥'} ${formatPrice(draft.priceWad)} - {submitMode === 'concurrent' ? '8130 concurrent' : 'Replace resting nonce'} · expires in{' '} - {expirySeconds}s + {submitMode === 'concurrent' ? '8130 concurrent' : 'Sequential replace'} ·{' '} + {delaySeconds > 0 + ? `starts in ~${delaySeconds}s, expires ${expirySeconds}s after` + : `expires in ${expirySeconds}s`}
    diff --git a/app/vibenet/demos/validity/components/OrderList.tsx b/app/vibenet/demos/validity/components/OrderList.tsx index c86c01a0..6a775858 100644 --- a/app/vibenet/demos/validity/components/OrderList.tsx +++ b/app/vibenet/demos/validity/components/OrderList.tsx @@ -75,7 +75,7 @@ export function OrderList({ orders, highlightedOrderId, onHighlight }: Props) {
    Submitted - Conditional swaps land here. Concurrent 8130 orders stack; replace + Conditional swaps land here. Concurrent orders stack; sequential mode bumps the last nonce.
    @@ -128,7 +128,10 @@ export function OrderList({ orders, highlightedOrderId, onHighlight }: Props) { {formatClock(order.submittedAt)} - {order.submitMode === 'concurrent' ? ' · 8130' : order.submitMode === 'replace' ? ' · replace' : null} + {order.submitMode === 'concurrent' ? ' · concurrent' : order.submitMode === 'replace' ? ' · sequential' : null} + {order.status === 'pending' && order.delaySeconds + ? ` · starts ${formatClock(order.submittedAt + order.delaySeconds * 1000)}` + : null} {order.filledAt ? ` → ${formatClock(order.filledAt)}` : null} {filled && order.fillPriceWad !== undefined ? ` · ${formatPrice(order.fillPriceWad)}` diff --git a/app/vibenet/demos/validity/components/OrderTicket.tsx b/app/vibenet/demos/validity/components/OrderTicket.tsx index fe106a9e..e491342b 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -3,10 +3,11 @@ import { useState } from 'react'; import { Button } from '../../../../components/ui/Button'; +import { InfoTooltip } from '../../../../components/ui/InfoTooltip'; import { Slider } from '../../../../components/ui/Slider'; import { Text } from '../../../../components/ui/Text'; import { AnimatedAmount } from '../../_components/AnimatedAmount'; -import { MAX_NONCELESS_SECONDS, TRADE_VIBE } from '../lib/constants'; +import { MAX_EXPIRY_SECONDS, MAX_NONCELESS_SECONDS, TRADE_VIBE } from '../lib/constants'; import { applyOffsetBps, formatPrice, parsePriceWad } from '../lib/predicates'; import { formatTokenAmount, VIBE_SYMBOL } from '../lib/quote'; import type { Side, SubmitMode } from '../lib/types'; @@ -14,6 +15,7 @@ import type { Side, SubmitMode } from '../lib/types'; const TRADE_LABEL = formatTokenAmount(TRADE_VIBE); const EXPIRIES = [5, 15, 60] as const; +const DELAYS = [0, 5, 15] as const; const OFFSET_MAX_BPS = 500; const OFFSET_STEP_BPS = 10; const OFFSET_MARKS = [0, 100, 200, 300, 400, 500] as const; @@ -23,6 +25,7 @@ type Props = { side: Side; offsetBps: number; expirySeconds: number; + delaySeconds: number; submitMode: SubmitMode; busy: boolean; vibeBalance: bigint | null; @@ -33,6 +36,7 @@ type Props = { onOffset: (bps: number) => void; onPriceOverride: (wad: bigint | null) => void; onExpiry: (seconds: number) => void; + onDelay: (seconds: number) => void; onSubmitMode: (mode: SubmitMode) => void; onSubmit: () => void; canAfford: boolean; @@ -48,6 +52,7 @@ export function OrderTicket({ side, offsetBps, expirySeconds, + delaySeconds, submitMode, busy, vibeBalance, @@ -56,6 +61,7 @@ export function OrderTicket({ onSide, onOffset, onExpiry, + onDelay, onSubmitMode, onSubmit, onPriceOverride, @@ -201,9 +207,15 @@ export function OrderTicket({
    - - Mempool - +
    + + Mempool + + + Sequential resubmits on the same nonce, so a new order replaces the resting one. Concurrent + uses nonceless (EIP-8130) transactions so several orders can be pending at once. + +
    -
    - - Expiry - -
    - {EXPIRIES.map((seconds) => { - const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS; - return ( - - ); - })} +
    +
    +
    + + Delay + + + Holds the swap until this much time has passed. + +
    +
    + {DELAYS.map((seconds) => { + const cap = submitMode === 'concurrent' ? MAX_NONCELESS_SECONDS : MAX_EXPIRY_SECONDS; + const blocked = seconds > 0 && seconds + expirySeconds > cap; + return ( + + ); + })} +
    +
    +
    +
    + + Expiry + + + Keeps the swap eligible for this long once it starts. + +
    +
    + {EXPIRIES.map((seconds) => { + const cap = submitMode === 'concurrent' ? MAX_NONCELESS_SECONDS : MAX_EXPIRY_SECONDS; + const blocked = delaySeconds + seconds > cap; + return ( + + ); + })} +