From 99fdc6f7c10d0642c0513025e926ca92d6c8831c Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Wed, 2 Sep 2026 13:32:48 -0700 Subject: [PATCH] feat(vibenet): add an optional delay to Validity orders A not-before block predicate (block_number >=) joins the expiry bound: pick Off/5s/15s in a Delay chip row beside Expiry, with delay forced shorter than expiry and both under the 8130 envelope in concurrent mode. Annotations explain the lower bound, the review modal shows the start time, and delayed orders carry delaySeconds/minBlock through storage and the Submitted list. Generated with Claude Code Co-Authored-By: Claude --- app/vibenet/demos/validity/ValidityDemo.tsx | 32 +++++-- .../demos/validity/components/OrderList.tsx | 3 + .../demos/validity/components/OrderTicket.tsx | 87 +++++++++++++------ .../demos/validity/lib/annotate.test.ts | 10 ++- app/vibenet/demos/validity/lib/annotate.ts | 5 +- app/vibenet/demos/validity/lib/orders.test.ts | 13 ++- app/vibenet/demos/validity/lib/orders.ts | 7 ++ app/vibenet/demos/validity/lib/predicates.ts | 8 ++ app/vibenet/demos/validity/lib/store.ts | 3 + app/vibenet/demos/validity/lib/types.ts | 2 + 10 files changed, 135 insertions(+), 35 deletions(-) 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 ( + + ); + })} +