From ca50ea17ca425f0d0aafc8238f6bf9180391ce7e Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Thu, 3 Sep 2026 14:47:45 -0700 Subject: [PATCH 1/7] polish(vibenet): rename validity mempool modes, restore delay, add tooltips - Rename Mempool submit-mode labels/copy: Replace -> Sequential, and drop the fee-bump framing ("One transaction at a time via sequential nonces" / "Submit multiple nonceless transactions simultaneously. Max expiry 20s"). - Re-add the not-before Delay control (Off/5s/15s) that was dropped when the v3 rewrite branched before its stacked PR (#137) reached main: block_number >= predicate, forced shorter than expiry, reflected in the review modal and Submitted list. - Add concise InfoTooltip hover explainers for Mempool and Delay, matching the b20/Publish Announcement pattern. - Replace remaining 8130/replace jargon in the Submitted history list with concurrent/sequential. --- app/vibenet/demos/validity/ValidityDemo.tsx | 30 +++-- .../demos/validity/components/OrderList.tsx | 7 +- .../demos/validity/components/OrderTicket.tsx | 112 +++++++++++++----- .../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 | 5 + app/vibenet/demos/validity/lib/store.ts | 3 + app/vibenet/demos/validity/lib/types.ts | 2 + 10 files changed, 149 insertions(+), 45 deletions(-) diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index e53bfd2e..88a7f70d 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); @@ -577,8 +579,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[] = []; @@ -739,7 +744,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 ?? @@ -819,8 +827,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 +854,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 +965,7 @@ function ValidityDemoInner() { side={side} offsetBps={offsetBps} expirySeconds={expirySeconds} + delaySeconds={delaySeconds} submitMode={submitMode} busy={busy} vibeBalance={vibeBalance} @@ -970,11 +981,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={() => { @@ -1030,8 +1046,8 @@ 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'} · expires in{' '} + {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..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..5abd769e 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -3,6 +3,7 @@ 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'; @@ -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 ( - - ); - })} +
    +
    + + Expiry + +
    + {EXPIRIES.map((seconds) => { + const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS; + return ( + + ); + })} +
    +
    +
    +
    + + Delay + + + Adds a minimum wait before the swap can be included — a floor to pair with the expiry + ceiling above. + +
    +
    + {DELAYS.map((seconds) => { + const blocked = seconds > 0 && seconds >= expirySeconds; + return ( + + ); + })} +
    - - Expiry - +
    + + Expiry + + + Drops the swap from the mempool once this much time has passed — the ceiling paired + with delay's floor. + +
    {EXPIRIES.map((seconds) => { const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS; @@ -281,8 +287,8 @@ export function OrderTicket({ Delay - Adds a minimum wait before the swap can be included — a floor to pair with the expiry - ceiling above. + Holds the swap until this much time has passed — the floor paired with expiry's + ceiling.
    From 9ebaf96bb8b9ed7bf7f0f662cebc3eea6e3f39a5 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Thu, 3 Sep 2026 14:59:38 -0700 Subject: [PATCH 3/7] fix(vibenet): avoid unescaped apostrophes in tooltip copy lint (react/no-unescaped-entities) failed on the possessives in the new Expiry/Delay tooltip text; reworded instead of escaping. --- app/vibenet/demos/validity/components/OrderTicket.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/vibenet/demos/validity/components/OrderTicket.tsx b/app/vibenet/demos/validity/components/OrderTicket.tsx index e702ecb0..5bd11f85 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -254,7 +254,7 @@ export function OrderTicket({ Drops the swap from the mempool once this much time has passed — the ceiling paired - with delay's floor. + with the delay floor.
    @@ -287,7 +287,7 @@ export function OrderTicket({ Delay - Holds the swap until this much time has passed — the floor paired with expiry's + Holds the swap until this much time has passed — the floor paired with the expiry ceiling.
    From 14fa13438f14e12724effb6bc8b7a2ebeb809434 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Thu, 3 Sep 2026 15:02:17 -0700 Subject: [PATCH 4/7] polish(vibenet): simplify Expiry/Delay tooltip copy Each tooltip stands on its own now instead of cross-referencing the other control. --- app/vibenet/demos/validity/components/OrderTicket.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/vibenet/demos/validity/components/OrderTicket.tsx b/app/vibenet/demos/validity/components/OrderTicket.tsx index 5bd11f85..098758a7 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -253,8 +253,7 @@ export function OrderTicket({ Expiry - Drops the swap from the mempool once this much time has passed — the ceiling paired - with the delay floor. + Drops the swap from the mempool once this much time has passed.
    @@ -287,8 +286,7 @@ export function OrderTicket({ Delay - Holds the swap until this much time has passed — the floor paired with the expiry - ceiling. + Holds the swap until this much time has passed.
    From 044e61c8a4d2b825f2673a6f39fae93c159e0607 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Thu, 3 Sep 2026 15:08:00 -0700 Subject: [PATCH 5/7] fix(vibenet): allow validity Delay to equal Expiry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delay only needs to stay at or under the expiry window, not strictly under it — a matching not-before/not-after bound just pins the swap to a single block, which is a valid (if narrow) predicate. Relaxes the UI gating and the delay clamp used when building predicates. --- app/vibenet/demos/validity/ValidityDemo.tsx | 8 ++++---- app/vibenet/demos/validity/components/OrderTicket.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index 88a7f70d..d95f56c8 100644 --- a/app/vibenet/demos/validity/ValidityDemo.tsx +++ b/app/vibenet/demos/validity/ValidityDemo.tsx @@ -580,7 +580,7 @@ function ValidityDemoInner() { : Math.min(MAX_EXPIRY_SECONDS, expirySeconds); const maxBlock = maxBlockForExpiry(blockNumber, seconds); const predicates = [...draft.predicates, blockExpiryPredicate(maxBlock)]; - const delay = Math.min(delaySeconds, Math.max(0, seconds - 1)); + const delay = Math.min(delaySeconds, seconds); if (delay > 0) predicates.push(blockDelayPredicate(minBlockForDelay(blockNumber, delay))); return predicates; }, [blockNumber, delaySeconds, draft, expirySeconds, submitMode]); @@ -744,7 +744,7 @@ 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 delay = Math.min(delaySeconds, seconds); const minBlock = delay > 0 ? minBlockForDelay(block, delay) : undefined; const validity = [...draft.predicates, blockExpiryPredicate(maxBlock)]; if (minBlock !== undefined) validity.push(blockDelayPredicate(minBlock)); @@ -983,14 +983,14 @@ function ValidityDemoInner() { onPriceOverride={setPriceOverrideWad} onExpiry={(seconds) => { setExpirySeconds(seconds); - if (delaySeconds >= seconds) setDelaySeconds(0); + 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); + if (delaySeconds > 15) setDelaySeconds(0); } }} onSubmit={() => { diff --git a/app/vibenet/demos/validity/components/OrderTicket.tsx b/app/vibenet/demos/validity/components/OrderTicket.tsx index 098758a7..b175324a 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -291,13 +291,13 @@ export function OrderTicket({
    {DELAYS.map((seconds) => { - const blocked = seconds > 0 && seconds >= expirySeconds; + const blocked = seconds > 0 && seconds > expirySeconds; return (
      diff --git a/app/vibenet/demos/validity/components/OrderTicket.tsx b/app/vibenet/demos/validity/components/OrderTicket.tsx index b175324a..e491342b 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -7,7 +7,7 @@ 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'; @@ -243,38 +243,39 @@ export function OrderTicket({ {submitMode === 'replace' ? 'One transaction at a time via sequential nonces.' - : `Submit multiple nonceless transactions simultaneously. Max expiry ${MAX_NONCELESS_SECONDS}s.`} + : `Submit multiple nonceless transactions simultaneously. Delay + expiry ≤ ${MAX_NONCELESS_SECONDS}s.`}
    - Expiry + Delay - - Drops the swap from the mempool once this much time has passed. + + Holds the swap until this much time has passed.
    - {EXPIRIES.map((seconds) => { - const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS; + {DELAYS.map((seconds) => { + const cap = submitMode === 'concurrent' ? MAX_NONCELESS_SECONDS : MAX_EXPIRY_SECONDS; + const blocked = seconds > 0 && seconds + expirySeconds > cap; return ( ); })} @@ -283,31 +284,32 @@ export function OrderTicket({
    - Delay + Expiry - - Holds the swap until this much time has passed. + + Keeps the swap eligible for this long once it starts.
    - {DELAYS.map((seconds) => { - const blocked = seconds > 0 && seconds > expirySeconds; + {EXPIRIES.map((seconds) => { + const cap = submitMode === 'concurrent' ? MAX_NONCELESS_SECONDS : MAX_EXPIRY_SECONDS; + const blocked = delaySeconds + seconds > cap; return ( ); })} diff --git a/app/vibenet/demos/validity/lib/orders.test.ts b/app/vibenet/demos/validity/lib/orders.test.ts index ffcbd029..579d9e15 100644 --- a/app/vibenet/demos/validity/lib/orders.test.ts +++ b/app/vibenet/demos/validity/lib/orders.test.ts @@ -13,6 +13,12 @@ describe('orderWallClockExpired', () => { const order = { status: 'filled' as const, submittedAt: 1_000, expirySeconds: 5 }; expect(orderWallClockExpired(order, 1_000 + 60_000)).toBe(false); }); + + it('adds delaySeconds to the window before expiring', () => { + const order = { status: 'pending' as const, submittedAt: 1_000, expirySeconds: 5, delaySeconds: 10 }; + expect(orderWallClockExpired(order, 1_000 + 15_000 + 400)).toBe(false); + expect(orderWallClockExpired(order, 1_000 + 15_000 + 401)).toBe(true); + }); }); describe('ageRestoredOrders', () => { diff --git a/app/vibenet/demos/validity/lib/orders.ts b/app/vibenet/demos/validity/lib/orders.ts index 446c6068..841f0e8a 100644 --- a/app/vibenet/demos/validity/lib/orders.ts +++ b/app/vibenet/demos/validity/lib/orders.ts @@ -4,11 +4,12 @@ import type { PlacedOrder } from './types'; const WALL_CLOCK_GRACE_MS = 400; export function orderWallClockExpired( - order: Pick, + order: Pick, now = Date.now(), ): boolean { if (order.status !== 'pending') return false; - return now > order.submittedAt + order.expirySeconds * 1000 + WALL_CLOCK_GRACE_MS; + const totalSeconds = (order.delaySeconds ?? 0) + order.expirySeconds; + return now > order.submittedAt + totalSeconds * 1000 + WALL_CLOCK_GRACE_MS; } export function orderBlockExpired( From f23b9e7864e2ee528c68c7eb18633cdc3e34a504 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Thu, 3 Sep 2026 16:02:46 -0700 Subject: [PATCH 7/7] polish(vibenet): make the predicate JSON copy control icon-only Drop the copy json / copied text label in the Advanced Details panel, keeping just the MorphIcon button. The accessible name now updates between Copy predicate JSON and Copied predicate JSON via aria-label, matching the icon-only CopyButton pattern used elsewhere. --- app/vibenet/demos/validity/components/ValidityJson.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/vibenet/demos/validity/components/ValidityJson.tsx b/app/vibenet/demos/validity/components/ValidityJson.tsx index bb44e8ad..d5469f00 100644 --- a/app/vibenet/demos/validity/components/ValidityJson.tsx +++ b/app/vibenet/demos/validity/components/ValidityJson.tsx @@ -80,10 +80,9 @@ export function ValidityJson({