From 784a07dd9fa27509aedc307e6bcf976da53cb167 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Wed, 2 Sep 2026 13:18:42 -0700 Subject: [PATCH 1/2] 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 1769ded..add5855 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 8469437..a83ed97 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 d91d0a1..30bd9e7 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 24c9299..3ae63ed 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/2] 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 a83ed97..fe106a9 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'}