From 784a07dd9fa27509aedc307e6bcf976da53cb167 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Wed, 2 Sep 2026 13:18:42 -0700 Subject: [PATCH 1/3] feat(vibenet): let Validity orders name a manual target price The target price in the order ticket becomes an editable input backed by a new parsePriceWad helper. Typing a price overrides the offset slider (dimmed with a reset hint) until the slider moves or the side flips, which clears the override. The mid-relative offset readout reflects the effective custom price, including wrong-side-of-mid prices that would fill immediately. Generated with Claude Code Co-Authored-By: Claude --- app/vibenet/demos/validity/ValidityDemo.tsx | 17 +++- .../demos/validity/components/OrderTicket.tsx | 88 ++++++++++++++----- .../demos/validity/lib/predicates.test.ts | 22 +++++ app/vibenet/demos/validity/lib/predicates.ts | 10 +++ 4 files changed, 112 insertions(+), 25 deletions(-) diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index 1769dedb..add58554 100644 --- a/app/vibenet/demos/validity/ValidityDemo.tsx +++ b/app/vibenet/demos/validity/ValidityDemo.tsx @@ -122,6 +122,7 @@ function ValidityDemoInner() { const [txHash, setTxHash] = useState(null); const [side, setSide] = useState('buy'); const [offsetBps, setOffsetBps] = useState(100); + const [priceOverrideWad, setPriceOverrideWad] = useState(null); const [expirySeconds, setExpirySeconds] = useState(15); const [submitMode, setSubmitMode] = useState('concurrent'); const [orders, setOrders] = useState([]); @@ -546,7 +547,7 @@ function ValidityDemoInner() { const draft = useMemo(() => { if (!state?.deployment || k === 0n || spot === 0n) return null; try { - const price = applyOffsetBps(spot, side, offsetBps); + const price = priceOverrideWad ?? applyOffsetBps(spot, side, offsetBps); const ammPrice = ammPriceFromQuote(price, vibeToken0); const built = priceValidity(state.deployment.pair, k, ammPrice, ammSide(side, vibeToken0)); return { @@ -559,7 +560,7 @@ function ValidityDemoInner() { } catch { return null; } - }, [k, offsetBps, side, spot, state?.deployment, vibeToken0]); + }, [k, offsetBps, priceOverrideWad, side, spot, state?.deployment, vibeToken0]); const reviewPredicates = useMemo(() => { if (!draft) return []; @@ -935,9 +936,17 @@ function ValidityDemoInner() { busy={busy} vibeBalance={vibeBalance} costHint={costHint} + priceOverrideWad={priceOverrideWad} canAfford={canAffordTrade} - onSide={setSide} - onOffset={setOffsetBps} + onSide={(next) => { + setSide(next); + setPriceOverrideWad(null); + }} + onOffset={(bps) => { + setOffsetBps(bps); + setPriceOverrideWad(null); + }} + onPriceOverride={setPriceOverrideWad} onExpiry={setExpirySeconds} onSubmitMode={(mode) => { setSubmitMode(mode); diff --git a/app/vibenet/demos/validity/components/OrderTicket.tsx b/app/vibenet/demos/validity/components/OrderTicket.tsx index 8469437b..a83ed97a 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -1,11 +1,13 @@ 'use client'; +import { useState } from 'react'; + import { Button } from '../../../../components/ui/Button'; 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 { applyOffsetBps, formatPrice } from '../lib/predicates'; +import { applyOffsetBps, formatPrice, parsePriceWad } from '../lib/predicates'; import { formatTokenAmount, VIBE_SYMBOL } from '../lib/quote'; import type { Side, SubmitMode } from '../lib/types'; @@ -25,8 +27,11 @@ type Props = { busy: boolean; vibeBalance: bigint | null; costHint: string | null; + /** Manual target price; set from the price input, cleared when the slider moves. */ + priceOverrideWad: bigint | null; onSide: (side: Side) => void; onOffset: (bps: number) => void; + onPriceOverride: (wad: bigint | null) => void; onExpiry: (seconds: number) => void; onSubmitMode: (mode: SubmitMode) => void; onSubmit: () => void; @@ -47,15 +52,32 @@ export function OrderTicket({ busy, vibeBalance, costHint, + priceOverrideWad, onSide, onOffset, onExpiry, onSubmitMode, onSubmit, + onPriceOverride, canAfford, }: Props) { - const target = applyOffsetBps(spotWad, side, offsetBps); - const signed = offsetBps === 0 ? '±0%' : side === 'buy' ? `−${formatBps(offsetBps)}` : `+${formatBps(offsetBps)}`; + const overrideActive = priceOverrideWad !== null; + const target = priceOverrideWad ?? applyOffsetBps(spotWad, side, offsetBps); + const effectiveBps = + spotWad > 0n ? Math.round((Number(target - spotWad) / Number(spotWad)) * 10_000) : 0; + const signed = overrideActive + ? effectiveBps === 0 + ? '±0%' + : `${effectiveBps > 0 ? '+' : '−'}${formatBps(Math.abs(effectiveBps))}` + : offsetBps === 0 + ? '±0%' + : side === 'buy' + ? `−${formatBps(offsetBps)}` + : `+${formatBps(offsetBps)}`; + // Raw text while the price input is being edited; null shows the computed target. + // Blur clears it, and anything that resets the override (slider, side flip) + // first steals focus from the input, so no sync with the override is needed. + const [priceText, setPriceText] = useState(null); return (
@@ -111,24 +133,31 @@ export function OrderTicket({
- {offsetBps === 0 ? 'At mid' : side === 'buy' ? 'Below mid' : 'Above mid'} + {target === spotWad ? 'At mid' : target < spotWad ? 'Below mid' : 'Above mid'} {signed}
- ({ - value: bps, - label: bps === 0 ? '0%' : formatBps(bps), - }))} - aria-label="Offset from mid" - /> +
+ ({ + value: bps, + label: bps === 0 ? '0%' : formatBps(bps), + }))} + aria-label="Offset from mid" + /> +
+ {overrideActive ? ( + + Custom price set — move the slider to clear it. + + ) : null}
Include when price is {side === 'buy' ? '≤' : '≥'} - - ${formatPrice(target)} - + $ + setPriceText(formatPrice(target))} + onChange={(event) => { + const text = event.target.value; + setPriceText(text); + const wad = parsePriceWad(text); + if (wad !== null) onPriceOverride(wad); + }} + onBlur={() => setPriceText(null)} + className="w-full min-w-0 bg-transparent tabular-nums outline-none" + /> +
mid {signed} + {overrideActive ? ' · custom price' : ''}
diff --git a/app/vibenet/demos/validity/lib/predicates.test.ts b/app/vibenet/demos/validity/lib/predicates.test.ts index d91d0a1f..30bd9e7b 100644 --- a/app/vibenet/demos/validity/lib/predicates.test.ts +++ b/app/vibenet/demos/validity/lib/predicates.test.ts @@ -4,6 +4,7 @@ import { RESERVE0_MASK, RESERVE1_MASK, RESERVE_BITS, WAD } from './constants'; import { applyOffsetBps, formatPrice, + parsePriceWad, prettyValidity, priceValidity, rectangleForTarget, @@ -27,6 +28,27 @@ describe('predicates', () => { expect(formatPrice(99n * 10n ** 16n)).toBe('0.9900'); }); + it('parses typed prices into wad', () => { + expect(parsePriceWad('1')).toBe(WAD); + expect(parsePriceWad('1.0000')).toBe(WAD); + expect(parsePriceWad('$0.99')).toBe(99n * 10n ** 16n); + expect(parsePriceWad('.5')).toBe(WAD / 2n); + expect(parsePriceWad('1,024.5')).toBe(1_024n * WAD + WAD / 2n); + expect(parsePriceWad(' 2.5 ')).toBe((5n * WAD) / 2n); + expect(parsePriceWad(formatPrice((3n * WAD) / 2n))).toBe((3n * WAD) / 2n); + }); + + it('rejects non-prices', () => { + expect(parsePriceWad('')).toBeNull(); + expect(parsePriceWad('0')).toBeNull(); + expect(parsePriceWad('0.000')).toBeNull(); + expect(parsePriceWad('-1')).toBeNull(); + expect(parsePriceWad('1e3')).toBeNull(); + expect(parsePriceWad('1.2.3')).toBeNull(); + expect(parsePriceWad('abc')).toBeNull(); + expect(parsePriceWad('1.0000000000000000001')).toBeNull(); + }); + it('offsets spot in basis points for buy and sell', () => { expect(applyOffsetBps(WAD, 'buy', 100)).toBe((99n * WAD) / 100n); expect(applyOffsetBps(WAD, 'sell', 100)).toBe((101n * WAD) / 100n); diff --git a/app/vibenet/demos/validity/lib/predicates.ts b/app/vibenet/demos/validity/lib/predicates.ts index 24c92991..3ae63edf 100644 --- a/app/vibenet/demos/validity/lib/predicates.ts +++ b/app/vibenet/demos/validity/lib/predicates.ts @@ -44,6 +44,16 @@ export function formatPrice(wad: bigint, digits = 4): string { return `${negative ? '-' : ''}${int.toString()}.${frac}`; } +/** Parse a typed price like "1.0421", "$0.98", or "1,024.5" into wad. Null if not a positive price. */ +export function parsePriceWad(input: string): bigint | null { + const cleaned = input.trim().replace(/^\$/, '').replace(/,/g, ''); + if (!/^(\d+(\.\d*)?|\.\d+)$/.test(cleaned)) return null; + const [intPart = '0', fracPart = ''] = cleaned.split('.'); + if (fracPart.length > 18) return null; + const wad = BigInt(intPart || '0') * WAD + BigInt(fracPart.padEnd(18, '0') || '0'); + return wad > 0n ? wad : null; +} + /** Apply a basis-point offset to spot. Buy is below (`-bps`), sell is above (`+bps`). 0 is at mid. */ export function applyOffsetBps(spotWad: bigint, side: Side, offsetBps: number): bigint { if (spotWad <= 0n) throw new Error('Need a live mid price.'); From 85e7ab6132bcf20355b6c45b8c23847f22f101ed Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Wed, 2 Sep 2026 13:23:02 -0700 Subject: [PATCH 2/3] fix(vibenet): make the Validity price input discoverable Give the target price a dashed editable underline that solidifies on focus, size it to its value, and label the idle state "type to set a price" so manual entry is visible without hunting for it. Generated with Claude Code Co-Authored-By: Claude --- app/vibenet/demos/validity/components/OrderTicket.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/vibenet/demos/validity/components/OrderTicket.tsx b/app/vibenet/demos/validity/components/OrderTicket.tsx index a83ed97a..fe106a9e 100644 --- a/app/vibenet/demos/validity/components/OrderTicket.tsx +++ b/app/vibenet/demos/validity/components/OrderTicket.tsx @@ -188,12 +188,16 @@ export function OrderTicket({ if (wad !== null) onPriceOverride(wad); }} onBlur={() => setPriceText(null)} - className="w-full min-w-0 bg-transparent tabular-nums outline-none" + className={`w-fit min-w-0 max-w-full cursor-text border-b border-dashed bg-transparent tabular-nums outline-none transition-colors focus:border-solid ${ + side === 'buy' + ? 'border-bds-green-70/40 hover:border-bds-green-70 focus:border-bds-green-70' + : 'border-bds-red-70/40 hover:border-bds-red-70 focus:border-bds-red-70' + }`} + size={Math.max((priceText ?? formatPrice(target)).length, 4)} />
- mid {signed} - {overrideActive ? ' · custom price' : ''} + mid {signed} · {overrideActive ? 'custom price' : 'type to set a price'}
From b18d481bf9f5b5a40555cd2c244c62582468d04c Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Wed, 2 Sep 2026 13:27:01 -0700 Subject: [PATCH 3/3] fix(vibenet): fill Validity orders already inside their price condition A buy priced above the mid (or a sell below it) used to anchor its reserve box at the target point alone, so the pool state never matched and the order quietly expired. Stretch the box along the hyperbola to the current point whenever the condition is already satisfied, keeping every corner within the named price bound, so those orders fill immediately instead. Generated with Claude Code Co-Authored-By: Claude --- app/vibenet/demos/validity/ValidityDemo.tsx | 9 ++++- .../demos/validity/lib/predicates.test.ts | 38 +++++++++++++++++++ app/vibenet/demos/validity/lib/predicates.ts | 32 +++++++++++++--- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index add58554..3f5bcd9f 100644 --- a/app/vibenet/demos/validity/ValidityDemo.tsx +++ b/app/vibenet/demos/validity/ValidityDemo.tsx @@ -549,7 +549,14 @@ function ValidityDemoInner() { try { const price = priceOverrideWad ?? applyOffsetBps(spot, side, offsetBps); const ammPrice = ammPriceFromQuote(price, vibeToken0); - const built = priceValidity(state.deployment.pair, k, ammPrice, ammSide(side, vibeToken0)); + const ammSpot = ammPriceFromQuote(spot, vibeToken0); + const built = priceValidity( + state.deployment.pair, + k, + ammPrice, + ammSide(side, vibeToken0), + ammSpot, + ); return { priceWad: price, side, diff --git a/app/vibenet/demos/validity/lib/predicates.test.ts b/app/vibenet/demos/validity/lib/predicates.test.ts index 30bd9e7b..ba312f32 100644 --- a/app/vibenet/demos/validity/lib/predicates.test.ts +++ b/app/vibenet/demos/validity/lib/predicates.test.ts @@ -56,6 +56,44 @@ describe('predicates', () => { expect(applyOffsetBps(WAD, 'sell', 0)).toBe(WAD); }); + it('stretches a satisfied buy box to the current point', () => { + const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0 + const current = WAD; + const target = 2n * WAD; // buy at ≤ 2 with mid at 1: already satisfied + const box = rectangleForTarget(k, target, 'buy', current); + const r0Now = sqrt((k * WAD) / current); + const r1Now = k / r0Now; + expect(box.r0Min <= r0Now && r0Now <= box.r0Max).toBe(true); + expect(box.r1Min <= r1Now && r1Now <= box.r1Max).toBe(true); + // Corners still respect price ≤ P. + expect((box.r1Max * WAD) / box.r0Min <= target).toBe(true); + }); + + it('stretches a satisfied sell box to the current point', () => { + const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0 + const current = WAD; + const target = WAD / 2n; // sell at ≥ 0.5 with mid at 1: already satisfied + const box = rectangleForTarget(k, target, 'sell', current); + const r0Now = sqrt((k * WAD) / current); + const r1Now = k / r0Now; + expect(box.r0Min <= r0Now && r0Now <= box.r0Max).toBe(true); + expect(box.r1Min <= r1Now && r1Now <= box.r1Max).toBe(true); + // Corners still respect price ≥ P. + expect((box.r1Min * WAD) / box.r0Max >= target).toBe(true); + }); + + it('keeps the resting box when the condition is not yet met', () => { + const k = 1_000n * WAD * (1_000n * WAD); // mid 1.0 + const buyTarget = (99n * WAD) / 100n; // buy below mid rests as before + expect(rectangleForTarget(k, buyTarget, 'buy', WAD)).toEqual( + rectangleForTarget(k, buyTarget, 'buy'), + ); + const sellTarget = (101n * WAD) / 100n; // sell above mid rests as before + expect(rectangleForTarget(k, sellTarget, 'sell', WAD)).toEqual( + rectangleForTarget(k, sellTarget, 'sell'), + ); + }); + it('buy box implies every corner has price ≤ P', () => { const k = 1_000n * WAD * (1_000n * WAD); const target = (99n * WAD) / 100n; diff --git a/app/vibenet/demos/validity/lib/predicates.ts b/app/vibenet/demos/validity/lib/predicates.ts index 3ae63edf..7446bacf 100644 --- a/app/vibenet/demos/validity/lib/predicates.ts +++ b/app/vibenet/demos/validity/lib/predicates.ts @@ -93,17 +93,33 @@ export function prettyValidity(predicates: ValidityPredicate[]): string { * sell (price ≥ P): A/s ≤ r0 ≤ A ∧ B ≤ r1 ≤ B·s with B/A ≥ P * * Four storage predicates, so a drained or wildly expanded pool cannot fill. + * + * When `currentPriceWad` already satisfies the condition (a buy priced above + * the mid, a sell priced below it), the box stretches along the hyperbola to + * the current point so the order fills without the mid retracing to target. */ -export function rectangleForTarget(k: bigint, targetPriceWad: bigint, side: Side): Rectangle { +export function rectangleForTarget( + k: bigint, + targetPriceWad: bigint, + side: Side, + currentPriceWad?: bigint, +): Rectangle { if (k === 0n || targetPriceWad <= 0n) { throw new Error('Need a live pool and a positive target price.'); } const a = sqrt((k * WAD) / targetPriceWad); if (a === 0n) throw new Error('Degenerate reserve bound.'); + const aCur = + currentPriceWad !== undefined && currentPriceWad > 0n + ? sqrt((k * WAD) / currentPriceWad) + : a; if (side === 'buy') { const b = (a * targetPriceWad) / WAD || 1n; - const r0Max = (a * BOX_SPAN_NUM) / BOX_SPAN_DEN; - const r1Min = (b * BOX_SPAN_DEN) / BOX_SPAN_NUM; + // Larger r0 means lower price; a satisfied buy sits at aOut > a. + const aOut = aCur > a ? aCur : a; + const bOut = k / aOut || 1n; + const r0Max = (aOut * BOX_SPAN_NUM) / BOX_SPAN_DEN; + const r1Min = (bOut * BOX_SPAN_DEN) / BOX_SPAN_NUM; return { r0Min: a, r0Max: r0Max > a ? r0Max : a + 1n, @@ -113,8 +129,11 @@ export function rectangleForTarget(k: bigint, targetPriceWad: bigint, side: Side }; } const b = (a * targetPriceWad + WAD - 1n) / WAD; - const r0Min = (a * BOX_SPAN_DEN) / BOX_SPAN_NUM; - const r1Max = (b * BOX_SPAN_NUM) / BOX_SPAN_DEN; + // Smaller r0 means higher price; a satisfied sell sits at aOut < a. + const aOut = aCur < a && aCur > 0n ? aCur : a; + const bOut = (k + aOut - 1n) / aOut; + const r0Min = (aOut * BOX_SPAN_DEN) / BOX_SPAN_NUM; + const r1Max = (bOut * BOX_SPAN_NUM) / BOX_SPAN_DEN; return { r0Min: r0Min < a ? r0Min : 1n, r0Max: a, @@ -151,8 +170,9 @@ export function priceValidity( k: bigint, targetPriceWad: bigint, side: Side, + currentPriceWad?: bigint, ): { rectangle: Rectangle; predicates: ValidityPredicate[] } { - const rectangle = rectangleForTarget(k, targetPriceWad, side); + const rectangle = rectangleForTarget(k, targetPriceWad, side, currentPriceWad); const r1MinValue = rectangle.r1Min << RESERVE_BITS; const r1MaxValue = rectangle.r1Max << RESERVE_BITS; const predicates: ValidityPredicate[] = [