diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index 3f5bcd9f..ca911063 100644 --- a/app/vibenet/demos/validity/ValidityDemo.tsx +++ b/app/vibenet/demos/validity/ValidityDemo.tsx @@ -45,6 +45,7 @@ import { VIBENET_WS_URL } from '../../library/config'; import { ageRestoredOrders, maxBlockForExpiry, + minBlockForDelay, occupyingOrder, orderBlockExpired, orderWallClockExpired, @@ -52,7 +53,13 @@ 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 +131,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); @@ -577,8 +585,11 @@ function ValidityDemoInner() { ? clampNoncelessExpiry(expirySeconds) : Math.min(MAX_EXPIRY_SECONDS, expirySeconds); const maxBlock = maxBlockForExpiry(blockNumber, seconds); - return [...draft.predicates, blockExpiryPredicate(maxBlock)]; - }, [blockNumber, draft, expirySeconds, submitMode]); + const predicates = [...draft.predicates, blockExpiryPredicate(maxBlock)]; + const delay = Math.min(delaySeconds, Math.max(0, seconds - 1)); + if (delay > 0) predicates.push(blockDelayPredicate(minBlockForDelay(blockNumber, delay))); + return predicates; + }, [blockNumber, delaySeconds, draft, expirySeconds, submitMode]); const chartLevels = useMemo((): PriceLevel[] => { const levels: PriceLevel[] = []; @@ -723,7 +734,10 @@ function ValidityDemoInner() { : Math.min(MAX_EXPIRY_SECONDS, expirySeconds); const block = blockNumber ?? (await publicClient.getBlockNumber({ cacheTime: 0 })); const maxBlock = maxBlockForExpiry(block, seconds); + const delay = Math.min(delaySeconds, Math.max(0, seconds - 1)); + const minBlock = delay > 0 ? minBlockForDelay(block, delay) : undefined; const validity = [...draft.predicates, blockExpiryPredicate(maxBlock)]; + if (minBlock !== undefined) validity.push(blockDelayPredicate(minBlock)); const fromHead = headFeesRef.current; const estimated = fromHead ?? @@ -803,8 +817,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, @@ -939,6 +955,7 @@ function ValidityDemoInner() { side={side} offsetBps={offsetBps} expirySeconds={expirySeconds} + delaySeconds={delaySeconds} submitMode={submitMode} busy={busy} vibeBalance={vibeBalance} @@ -954,11 +971,16 @@ function ValidityDemoInner() { setPriceOverrideWad(null); }} onPriceOverride={setPriceOverrideWad} - onExpiry={setExpirySeconds} + onExpiry={(seconds) => { + setExpirySeconds(seconds); + if (delaySeconds >= seconds) setDelaySeconds(0); + }} + onDelay={setDelaySeconds} onSubmitMode={(mode) => { setSubmitMode(mode); if (mode === 'concurrent' && expirySeconds > MAX_NONCELESS_SECONDS) { setExpirySeconds(15); + if (delaySeconds >= 15) setDelaySeconds(0); } }} onSubmit={() => { @@ -1015,7 +1037,7 @@ function ValidityDemoInner() { {submitMode === 'concurrent' ? '8130 concurrent' : 'Replace resting nonce'} · expires in{' '} - {expirySeconds}s + {expirySeconds}s{delaySeconds > 0 ? ` · starts in ~${delaySeconds}s` : ''}
    diff --git a/app/vibenet/demos/validity/components/OrderList.tsx b/app/vibenet/demos/validity/components/OrderList.tsx index c86c01a0..91c105ef 100644 --- a/app/vibenet/demos/validity/components/OrderList.tsx +++ b/app/vibenet/demos/validity/components/OrderList.tsx @@ -129,6 +129,9 @@ export function OrderList({ orders, highlightedOrderId, onHighlight }: Props) { {formatClock(order.submittedAt)} {order.submitMode === 'concurrent' ? ' · 8130' : order.submitMode === 'replace' ? ' · replace' : 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..23aec197 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -14,6 +14,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 +24,7 @@ type Props = { side: Side; offsetBps: number; expirySeconds: number; + delaySeconds: number; submitMode: SubmitMode; busy: boolean; vibeBalance: bigint | null; @@ -33,6 +35,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 +51,7 @@ export function OrderTicket({ side, offsetBps, expirySeconds, + delaySeconds, submitMode, busy, vibeBalance, @@ -56,6 +60,7 @@ export function OrderTicket({ onSide, onOffset, onExpiry, + onDelay, onSubmitMode, onSubmit, onPriceOverride, @@ -234,32 +239,62 @@ export function OrderTicket({ : `8130 nonceless — stack several at once. Envelope max ${MAX_NONCELESS_SECONDS}s.`} -
    - - Expiry - -
    - {EXPIRIES.map((seconds) => { - const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS; - return ( - - ); - })} +
    +
    + + Expiry + +
    + {EXPIRIES.map((seconds) => { + const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS; + return ( + + ); + })} +
    +
    +
    + + Delay + +
    + {DELAYS.map((seconds) => { + const blocked = seconds > 0 && seconds >= expirySeconds; + return ( + + ); + })} +