From 3f7dbc198aae6403690a3ee0e4bd99ad6613c503 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Wed, 2 Sep 2026 12:06:37 -0700 Subject: [PATCH 1/2] feat(vibenet): make the Validity demo a read-only observer of the central AMM (BASE-422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AMM pool and its maker flow now run on central vibenet infrastructure (the actor system), so the demo no longer runs actors in the browser. - Remove the client-side maker subaccounts, the startBots swap loop, and the client-side ensureSingleton pool deploy (+ the "Deploy shared pool" button). - Keep read-only probeSingleton discovery — deterministic CREATE2 means the browser rediscovers exactly what vibenet-setup deployed — plus the candle/ price display and the user's own inventory + conditional orders. - Show a "pool is coming online" state for the window before the actors are live. Delete the now-dead code the cutover leaves behind: - lib/bots.ts + lib/makers.ts (and their tests) — no longer referenced. - ensureSingleton() and its write-path helpers in lib/singleton.ts; keep the probeSingleton()/predictSingleton() read chain (still used + tested). - encodeSwapLegs() in lib/amm.ts (only the deleted bots.ts used it). - The now-unwritten accountId / makerAccountIds fields in the persisted store. Co-Authored-By: Claude --- app/vibenet/demos/validity/ValidityDemo.tsx | 297 +------------- app/vibenet/demos/validity/lib/amm.ts | 28 -- app/vibenet/demos/validity/lib/bots.test.ts | 85 ---- app/vibenet/demos/validity/lib/bots.ts | 216 ---------- app/vibenet/demos/validity/lib/makers.test.ts | 43 -- app/vibenet/demos/validity/lib/makers.ts | 48 --- app/vibenet/demos/validity/lib/singleton.ts | 369 +----------------- app/vibenet/demos/validity/lib/store.test.ts | 4 - app/vibenet/demos/validity/lib/store.ts | 10 - 9 files changed, 25 insertions(+), 1075 deletions(-) delete mode 100644 app/vibenet/demos/validity/lib/bots.test.ts delete mode 100644 app/vibenet/demos/validity/lib/bots.ts delete mode 100644 app/vibenet/demos/validity/lib/makers.test.ts delete mode 100644 app/vibenet/demos/validity/lib/makers.ts diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index 10ee954..2440ef0 100644 --- a/app/vibenet/demos/validity/ValidityDemo.tsx +++ b/app/vibenet/demos/validity/ValidityDemo.tsx @@ -1,8 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { formatEther, parseEther, type Hex, type PublicClient } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; +import { formatEther, type Hex, type PublicClient } from 'viem'; import { trackValidityOrder } from '../../../analytics/events'; import { Button } from '../../../components/ui/Button'; @@ -13,7 +12,6 @@ import { AccountDemoShell } from '../_components/AccountDemoShell'; import { AnimatedAmount } from '../_components/AnimatedAmount'; import { DemoHeader } from '../_components/DemoHeader'; import { newCallRow } from '../account/library/calls'; -import type { StoredAccount } from '../account/library/model'; import { ActivityLog } from '../account/components/ActivityLog'; import { AccountEngineProvider, useAccountEngine } from '../account/useAccountEngine'; import { VIBENET_EXPLORER_PATH } from '../../library/config'; @@ -37,7 +35,6 @@ import { tokenBalance, } from './lib/amm'; import { clampNoncelessExpiry, noncelessFields } from '../../library/aa'; -import { startBots, allNeedGas, shouldFlagMakersDry } from './lib/bots'; import { CANDLE_SAMPLE_MS, MAX_EXPIRY_SECONDS, @@ -46,7 +43,6 @@ import { } from './lib/constants'; import { VibenetApiError } from '../../library/client'; import { VIBENET_WS_URL } from '../../library/config'; -import { ensureMakers, rootAccount } from './lib/makers'; import { ageRestoredOrders, maxBlockForExpiry, @@ -75,14 +71,13 @@ import { describeValidityError, fetchTape, makePublicClient, - makeWalletClient, publishTape, sendValidityTransaction, VIBENET_CHAIN, type RpcSend, } from './lib/rpc'; import { connectJsonRpcStream, headNumber, type StreamHead, type StreamLog } from './lib/stream'; -import { ensureSingleton, probeSingleton } from './lib/singleton'; +import { probeSingleton } from './lib/singleton'; import { mergeTape } from './lib/tape'; import { createState, loadState, saveState, type StoredState } from './lib/store'; import type { PlacedOrder, Rectangle, Reserves, Side, SubmitMode } from './lib/types'; @@ -91,8 +86,6 @@ import type { PlacedOrder, Rectangle, Reserves, Side, SubmitMode } from './lib/t * The socket carries heads, pair logs, and remaining reads (balances, receipts). */ const SYNC_MS = 1_000; const BALANCE_MS = 5_000; -const OWNER_DEPLOY_GAS = parseEther('0.08'); -const OWNER_DEPLOY_SEND = '0.1'; function wadToNumber(wad: bigint): number { return Number(wad) / 1e18; @@ -135,22 +128,14 @@ function ValidityDemoInner() { const [orders, setOrders] = useState([]); const [hoveredOrderId, setHoveredOrderId] = useState(null); const [samples, setSamples] = useState([]); - const [makerError, setMakerError] = useState(null); - const [makersDry, setMakersDry] = useState(false); const [blockNumber, setBlockNumber] = useState(null); const [streamLive, setStreamLive] = useState(false); const publicRef = useRef(null); const rpcSendRef = useRef(null); const headFeesRef = useRef>(null); - const makerNonceRef = useRef<(bigint | null)[]>([]); - const makerDeployedRef = useRef([]); const engineRef = useRef(engine); engineRef.current = engine; - const lastMakerPriceAtRef = useRef(0); - const makersRef = useRef([]); - const makerEthRef = useRef<(bigint | null)[]>([]); - const makerTokenRef = useRef>({}); const refreshBalancesRef = useRef<() => void>(() => {}); const ordersRef = useRef([]); @@ -239,22 +224,6 @@ function ValidityDemoInner() { }; }, [hydrated, pair]); - const parent = useMemo( - () => (acct ? rootAccount(acct, engine.accounts) : null), - [acct, engine.accounts], - ); - - const makers = useMemo(() => { - if (!parent) return [] as StoredAccount[]; - const ids = state?.makerAccountIds; - const resolved = (ids ?? []) - .map((id) => engine.accounts.find((item) => item.id === id)) - .filter((item): item is StoredAccount => Boolean(item)); - if (resolved.length === 2) return resolved; - return engine.accounts.filter((item) => item.parentId === parent.id && item.label.startsWith('Validity maker')); - }, [engine.accounts, parent, state?.makerAccountIds]); - makersRef.current = makers; - useEffect(() => { let cancelled = false; const client = makePublicClient(() => rpcSendRef.current); @@ -410,32 +379,8 @@ function ValidityDemoInner() { let stream: ReturnType | undefined; const logsByTx = new Map(); - const applyMakerParts = (deployment: StoredState['deployment'], makerParts: unknown[]) => { - const makerList = makersRef.current; - const stride = deployment ? 3 : 1; - makerEthRef.current = makerList.map((_, index) => { - const value = makerParts[index * stride]; - return typeof value === 'bigint' ? value : null; - }); - const tokens: Record = {}; - if (deployment) { - makerList.forEach((maker, index) => { - const vibe = makerParts[index * stride + 1]; - const usdv = makerParts[index * stride + 2]; - if (typeof vibe === 'bigint') tokens[`${maker.address}:${deployment.tokenA}`] = vibe; - if (typeof usdv === 'bigint') tokens[`${maker.address}:${deployment.tokenB}`] = usdv; - }); - } - makerTokenRef.current = tokens; - const known = makerEthRef.current.filter((value): value is bigint => value !== null); - if (known.length === makerList.length && makerList.length > 0 && !allNeedGas(known)) { - setMakersDry(false); - } - }; - const pullBalances = async (includeReserves: boolean) => { const deployment = stateRef.current?.deployment; - const makerList = makersRef.current; const jobs: Promise[] = [client.getBalance({ address: acct.address })]; if (includeReserves) { jobs.push(deployment ? getReserves(client, deployment.pair).catch(() => null) : Promise.resolve(null)); @@ -444,13 +389,6 @@ function ValidityDemoInner() { jobs.push(tokenBalance(client, deployment.tokenA, acct.address).catch(() => null)); jobs.push(tokenBalance(client, deployment.tokenB, acct.address).catch(() => null)); } - for (const maker of makerList) { - jobs.push(client.getBalance({ address: maker.address }).catch(() => null)); - if (deployment) { - jobs.push(tokenBalance(client, deployment.tokenA, maker.address).catch(() => null)); - jobs.push(tokenBalance(client, deployment.tokenB, maker.address).catch(() => null)); - } - } const [eth, ...rest] = await Promise.all(jobs); if (cancelled) return; if (typeof eth === 'bigint') setEthBalance(eth); @@ -469,7 +407,6 @@ function ValidityDemoInner() { if (typeof vibe === 'bigint') setVibeBalance(vibe); if (typeof usdv === 'bigint') setUsdvBalance(usdv); } - applyMakerParts(deployment, rest.slice(offset)); }; refreshBalancesRef.current = () => { void pullBalances(false).catch(() => {}); @@ -697,137 +634,24 @@ function ValidityDemoInner() { } }, [engine]); - const deploy = async () => { - if (!acct || !parent || !genesisHash) return; - const publicClient = publicRef.current; - if (!publicClient) return; - const k1 = engine.ownerSigners.find((signer) => signer.kind === 'k1' && signer.privateKey); - if (!k1?.privateKey) { - setError('Pool deploy needs a K1 owner key on this account. Add one in Accounts.'); - return; - } - setBusy(true); - setError(null); - try { - const [makerA, makerB] = ensureMakers( - parent, - engine.accounts, - state?.makerAccountIds, - engine.doCreateSubAccount, - ); - persist({ - ...(state ?? createState(VIBENET_CHAIN.id, genesisHash)), - accountId: parent.id, - makerAccountIds: [makerA.id, makerB.id], - }); - - const eoa = privateKeyToAccount(k1.privateKey); - const eoaBal = await publicClient.getBalance({ address: eoa.address }); - if (eoaBal < OWNER_DEPLOY_GAS) { - setProgress('Sending ETH to the owner key for contract creates'); - await engine.sendActiveCalls({ - calls: [{ to: eoa.address, data: '0x', value: OWNER_DEPLOY_SEND }], - metadata: 'Validity deploy gas', - }); - } - - const wallet = makeWalletClient(eoa); - const deployment = await ensureSingleton({ - wallet, - publicClient, - account: eoa, - onProgress: setProgress, - }); - - setProgress('Minting inventory and approving the helper'); - const starter = await inventoryMints(publicClient, deployment, [ - { to: acct.address }, - { to: makerA.address, mintVibe: true }, - { to: makerB.address, mintVibe: true }, - ]); - const approves = await helperApproveCalls(publicClient, deployment, acct.address); - if (starter.length + approves.length > 0) { - await engine.sendActiveCalls({ - calls: [...starter, ...approves], - metadata: 'Validity inventory', - }); - } - inventoryKeyRef.current = `${deployment.pair}:${acct.id}:${makerA.id},${makerB.id}`; - - setMakersDry(false); - setMakerError(null); - lastMakerPriceAtRef.current = 0; - persist({ - ...(state ?? createState(VIBENET_CHAIN.id, genesisHash)), - v: 2, - chainId: VIBENET_CHAIN.id, - genesisHash, - accountId: parent.id, - makerAccountIds: [makerA.id, makerB.id], - deployment, - }); - engine.pushActivity({ - kind: 'transact', - title: 'Validity shared pool ready', - detail: `Pair ${deployment.pair}`, - account: acct.address, - network: engine.chain.name, - mode: engine.chain.mode, - }); - } catch (err) { - setError(err instanceof Error ? err.message : 'Deploy failed'); - } finally { - setBusy(false); - setProgress(null); - } - }; - - const makerKey = makers.map((maker) => maker.id).join(','); const inventoryKeyRef = useRef(''); + // Mint USDV inventory + approve the helper for the user's own account so + // their conditional orders can fill. The pool itself and the maker flow are + // now run centrally by the vibenet actor system, so there is no client-side + // maker creation, funding, or swap loop here anymore. useEffect(() => { - if (!hydrated || !engine.hydrated || !genesisHash || !acct || !parent || !state?.deployment) return; - if (makers.length === 2) return; - const [makerA, makerB] = ensureMakers( - parent, - engine.accounts, - state.makerAccountIds, - engine.doCreateSubAccount, - ); - persist({ - ...state, - accountId: parent.id, - makerAccountIds: [makerA.id, makerB.id], - }); - }, [ - acct, - engine.accounts, - engine.doCreateSubAccount, - engine.hydrated, - hydrated, - makers.length, - parent, - persist, - state, - genesisHash, - ]); - - useEffect(() => { - if (!hydrated || !state?.deployment || !acct || makers.length !== 2 || busy) return; + if (!hydrated || !state?.deployment || !acct || busy) return; if (!ethBalance || ethBalance === 0n) return; const client = publicRef.current; if (!client) return; - const key = `${state.deployment.pair}:${acct.id}:${makerKey}`; + const key = `${state.deployment.pair}:${acct.id}`; if (inventoryKeyRef.current === key) return; const deployment = state.deployment; - const recipients = [ - { to: acct.address }, - ...makers.map((maker) => ({ to: maker.address, mintVibe: true as const })), - ]; let cancelled = false; void (async () => { try { - const starter = await inventoryMints(client, deployment, recipients); + const starter = await inventoryMints(client, deployment, [{ to: acct.address }]); const approves = await helperApproveCalls(client, deployment, acct.address); if (cancelled) return; if (starter.length + approves.length === 0) { @@ -849,87 +673,7 @@ function ValidityDemoInner() { return () => { cancelled = true; }; - }, [acct, busy, ethBalance, hydrated, makerKey, makers, state?.deployment]); - - useEffect(() => { - if (!hydrated || !genesisHash || !state?.deployment || makersRef.current.length !== 2) return; - makerNonceRef.current = []; - makerDeployedRef.current = []; - makerEthRef.current = makersRef.current.map(() => null); - setMakersDry(false); - const deployment = state.deployment; - const stop = startBots({ - addresses: makersRef.current.map((maker) => maker.address), - deployment, - reserves: () => reservesRef.current, - ethBalance: (index) => makerEthRef.current[index] ?? null, - tokenBalance: (index, token) => { - const maker = makersRef.current[index]; - if (!maker) return null; - return makerTokenRef.current[`${maker.address}:${token}`] ?? null; - }, - sendSwap: async (index, calls) => { - const maker = makersRef.current[index]; - if (!maker) throw new Error('maker missing'); - const client = publicRef.current; - let nonce = makerNonceRef.current[index] ?? null; - if (nonce === null && client) { - nonce = BigInt(await client.getTransactionCount({ address: maker.address })); - } - const nonceSequence = nonce ?? 0n; - const rows = calls.map((call) => ({ ...call, value: '0' as const })); - const send = (deployed: boolean) => - engineRef.current.sendAccountCalls({ - account: maker, - calls: rows, - // First swap carries `create` and must land before we pin nonces. - wait: !deployed, - seqOpt: { - nonceSequence, - ...(deployed ? { assumeDeployed: true } : {}), - }, - }); - try { - const deployed = makerDeployedRef.current[index] === true; - try { - await send(deployed); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - if (!deployed || !/actor is not bound/i.test(message)) throw err; - // Replica still missing the create, or we guessed deployed too early. - makerDeployedRef.current[index] = false; - await send(false); - } - makerDeployedRef.current[index] = true; - makerNonceRef.current[index] = nonceSequence + 1n; - } catch (err) { - makerNonceRef.current[index] = null; - throw err; - } - }, - enabled: () => true, - onPrice: () => { - lastMakerPriceAtRef.current = Date.now(); - setMakerError(null); - setMakersDry(false); - }, - onError: setMakerError, - onGasLow: () => { - for (const maker of makersRef.current) engineRef.current.autoFundNewAccount(maker.address); - if ( - shouldFlagMakersDry( - makerEthRef.current, - makersRef.current.length, - lastMakerPriceAtRef.current, - ) - ) { - setMakersDry(true); - setMakerError('need ETH'); - } - }, - }); - return stop; - }, [genesisHash, hydrated, makerKey, state?.deployment]); + }, [acct, busy, ethBalance, hydrated, state?.deployment]); const placeOrder = async (): Promise => { if (!draft || !acct || !state?.deployment || !reserves || !engine.activeSigner) return; @@ -1109,7 +853,6 @@ function ValidityDemoInner() { }; const address = acct?.address; - const funded = (ethBalance ?? 0n) > 0n; const deployed = Boolean(state?.deployment); const tradeLabel = formatTokenAmount(TRADE_VIBE); const canAffordTrade = (() => { @@ -1136,14 +879,6 @@ function ValidityDemoInner() { description="A transaction can carry predicates the sequencer checks before inclusion. Everyone shares one VIBE/USDV pool — VIBE is a B20, USDV is the faucet stablecoin — so you can watch a swap wait for a price condition, then land or expire." /> - {makersDry ? ( - - Simulated flow ran out of ETH. Top up the account so the makers can keep walking the mid. - - ) : makerError ? ( - {makerError} - ) : null} - {statusError ? ( {statusError} ) : null} @@ -1152,10 +887,11 @@ function ValidityDemoInner() { Shared pool - Your Vibenet account signs the swaps. The first visitor publishes a - network-wide pair of VIBE (a B20) and the faucet USDV. Everyone - else attaches to the same factory. Makers mint a starter bag and - buy or sell against that pool. + The shared VIBE/USDV pool (VIBE is a B20, USDV is the faucet + stablecoin) runs on Vibenet infrastructure — a central actor system + keeps a live market moving. It’s coming online; this page will fill + in automatically. Meanwhile, top up your account so you’re ready to + place a conditional order. {address ? (
@@ -1177,9 +913,6 @@ function ValidityDemoInner() { -
) : ( diff --git a/app/vibenet/demos/validity/lib/amm.ts b/app/vibenet/demos/validity/lib/amm.ts index 1912469..1121c4c 100644 --- a/app/vibenet/demos/validity/lib/amm.ts +++ b/app/vibenet/demos/validity/lib/amm.ts @@ -214,34 +214,6 @@ export function encodeApprove(token: Address, spender: Address): { to: Address; }; } -export function encodeSwapLegs(args: { - tokenIn: Address; - pair: Address; - recipient: Address; - amountIn: bigint; - amount0Out: bigint; - amount1Out: bigint; -}): { to: Address; data: Hex }[] { - return [ - { - to: args.tokenIn, - data: encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [args.pair, args.amountIn], - }), - }, - { - to: args.pair, - data: encodeFunctionData({ - abi: pairAbi, - functionName: 'swap', - args: [args.amount0Out, args.amount1Out, args.recipient, '0x'], - }), - }, - ]; -} - export function encodeHelperSwap(args: { helper: Address; tokenIn: Address; diff --git a/app/vibenet/demos/validity/lib/bots.test.ts b/app/vibenet/demos/validity/lib/bots.test.ts deleted file mode 100644 index 7fb666c..0000000 --- a/app/vibenet/demos/validity/lib/bots.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { parseEther } from 'viem'; -import { describe, expect, it } from 'vitest'; - -import { - BOT_GAS_FLOOR, - MAKER_DRY_GRACE_MS, - allNeedGas, - botNeedsGas, - fractionForPriceMove, - makerTargetPrice, - planSwap, - shouldFlagMakersDry, -} from './bots'; - -describe('fractionForPriceMove', () => { - it('sizes a 1% price step at about half a percent of reserves', () => { - const fraction = fractionForPriceMove(0.01); - expect(fraction).toBeGreaterThan(0.0045); - expect(fraction).toBeLessThan(0.0056); - }); -}); - -describe('makerTargetPrice', () => { - it('wanders around the VIBE/USDV anchor inside $0.01–$1', () => { - const prices = Array.from({ length: 120 }, (_, i) => makerTargetPrice(i * 250, 0.07)); - expect(Math.max(...prices) / Math.min(...prices)).toBeGreaterThan(1.02); - expect(Math.min(...prices)).toBeGreaterThan(0.01); - expect(Math.max(...prices)).toBeLessThan(1); - expect(prices.some((price) => price < 0.07)).toBe(true); - expect(prices.some((price) => price > 0.07)).toBe(true); - }); -}); - -describe('planSwap', () => { - it('sizes near a 1% price impact', () => { - const plan = planSwap(0.08, 0.07, 0); - expect(plan.fraction).toBeGreaterThan(0.0045); - expect(plan.fraction).toBeLessThan(0.0056); - }); - - it('buys VIBE when the quote is stretched cheap', () => { - expect(planSwap(0.012, 0.07, 0).sellVibe).toBe(false); - }); -}); - -describe('botNeedsGas', () => { - it('is true below the floor', () => { - expect(botNeedsGas(0n)).toBe(true); - expect(botNeedsGas(BOT_GAS_FLOOR)).toBe(false); - }); -}); - -describe('allNeedGas', () => { - it('is only true when every maker is below the floor', () => { - expect(allNeedGas([])).toBe(false); - expect(allNeedGas([0n, BOT_GAS_FLOOR])).toBe(false); - expect(allNeedGas([0n, BOT_GAS_FLOOR - 1n])).toBe(true); - }); -}); - -describe('shouldFlagMakersDry', () => { - const now = 1_000_000; - const afterGrace = now - MAKER_DRY_GRACE_MS - 1; - - it('is false before the first successful maker swap', () => { - expect(shouldFlagMakersDry([0n, 0n], 2, 0, now)).toBe(false); - expect(shouldFlagMakersDry([null, null], 2, 0, now)).toBe(false); - expect(shouldFlagMakersDry([], 2, 0, now)).toBe(false); - }); - - it('is false while a swap landed inside the grace window', () => { - expect(shouldFlagMakersDry([0n, 0n], 2, now - MAKER_DRY_GRACE_MS + 1, now)).toBe(false); - }); - - it('is false when any maker still has gas or a balance is missing', () => { - expect(shouldFlagMakersDry([0n, parseEther('0.1')], 2, afterGrace, now)).toBe(false); - expect(shouldFlagMakersDry([0n, null], 2, afterGrace, now)).toBe(false); - expect(shouldFlagMakersDry([0n], 2, afterGrace, now)).toBe(false); - expect(shouldFlagMakersDry([], 0, afterGrace, now)).toBe(false); - }); - - it('is true only after a swap and every maker is below the floor', () => { - expect(shouldFlagMakersDry([0n, BOT_GAS_FLOOR - 1n], 2, afterGrace, now)).toBe(true); - }); -}); diff --git a/app/vibenet/demos/validity/lib/bots.ts b/app/vibenet/demos/validity/lib/bots.ts deleted file mode 100644 index 72f7645..0000000 --- a/app/vibenet/demos/validity/lib/bots.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { parseEther, type Address } from 'viem'; - -import { amountOut, encodeSwapLegs } from './amm'; -import { - quoteWad, - swapOuts, - tokenInFor, - usdvReserve, - vibeIsToken0, - vibeReserve, -} from './quote'; -import type { Deployment, Reserves } from './types'; - -const ANCHOR = 0.07; -const SLOW_PERIOD_MS = 24_000; -const SLOW_AMPLITUDE = 0.05; -const FAST_PERIOD_MS = 3_000; -const FAST_AMPLITUDE = 0.012; -const PRICE_MOVE = 0.01; -const HARD_LO = 0.01; -const HARD_HI = 1; -/** One maker swap per second is enough to walk the mid. */ -const TICK_MS = 1_000; -export const BOT_GAS_FLOOR = parseEther('0.002'); -/** Ignore gas-low while a maker swap just landed — balances can lag the send. */ -export const MAKER_DRY_GRACE_MS = 2_500; -const GAS_LOW_MS = 4_000; - -function clamp(n: number, lo: number, hi: number): number { - return Math.min(hi, Math.max(lo, n)); -} - -export function botNeedsGas(balance: bigint, floor = BOT_GAS_FLOOR): boolean { - return balance < floor; -} - -export function allNeedGas(balances: readonly bigint[]): boolean { - return balances.length > 0 && balances.every((balance) => botNeedsGas(balance)); -} - -/** - * Banner only after the simulation has swapped and then every known maker - * balance is below the floor. `lastSwapAt === 0` means no swap yet — empty, - * null, or pre-fund 0n readings must not look like "ran out of ETH". - */ -export function shouldFlagMakersDry( - balances: readonly (bigint | null)[], - makerCount: number, - lastSwapAt: number, - now = Date.now(), -): boolean { - if (lastSwapAt === 0 || now - lastSwapAt < MAKER_DRY_GRACE_MS) return false; - if (makerCount <= 0) return false; - const known = balances.filter((value): value is bigint => value !== null); - return known.length === makerCount && allNeedGas(known); -} - -/** - * Reserve-in fraction that moves Uni v2 mid by `move` (0.01 = 1%). - * Because p ∝ 1/r0², a 1% price step is about 0.5% of the input reserve. - */ -export function fractionForPriceMove(move: number): number { - const abs = clamp(Math.abs(move), 0.002, 0.2); - return 1 / Math.sqrt(1 - abs) - 1; -} - -/** Slow ±5% wander around the VIBE/USDV anchor, plus a faster ±1.2% wobble. */ -export function makerTargetPrice(nowMs: number, anchor = ANCHOR): number { - const slow = SLOW_AMPLITUDE * Math.sin((2 * Math.PI * nowMs) / SLOW_PERIOD_MS); - const fast = FAST_AMPLITUDE * Math.sin((2 * Math.PI * nowMs) / FAST_PERIOD_MS + 0.6); - return clamp(anchor * (1 + slow + fast), HARD_LO, HARD_HI); -} - -export function planSwap( - spot: number, - desired: number, - noise: number, -): { sellVibe: boolean; fraction: number } { - const towardSellVibe = desired < spot; - const stretched = - spot <= HARD_LO * 1.2 || spot >= HARD_HI * 0.85 || Math.abs(spot - desired) / Math.max(desired, 1e-9) > 0.07; - let sellVibe: boolean; - if (stretched) { - sellVibe = spot > desired; - } else if (Math.random() < 0.78) { - sellVibe = towardSellVibe; - } else { - sellVibe = !towardSellVibe; - } - const move = PRICE_MOVE * (1 + noise); - return { sellVibe, fraction: fractionForPriceMove(move) }; -} - -/** - * One ~1% swap per second toward a shared USDV/VIBE target. Reserves, gas, and - * inventory come from the demo sync so this loop does not add its own reads. - */ -export function startBots(args: { - addresses: Address[]; - deployment: Deployment; - reserves: () => Reserves | null; - ethBalance: (index: number) => bigint | null; - tokenBalance: (index: number, token: Address) => bigint | null; - sendSwap: (index: number, calls: { to: Address; data: `0x${string}` }[]) => Promise; - enabled: () => boolean; - onPrice?: (price: number) => void; - onError?: (message: string) => void; - onGasLow?: () => void; -}): () => void { - const { - addresses, - deployment, - reserves: readReserves, - ethBalance, - tokenBalance, - sendSwap, - enabled, - onPrice, - onError, - onGasLow, - } = args; - let stopped = false; - let timer: ReturnType | undefined; - let turn = 0; - let anchor = ANCHOR; - let anchored = false; - let lastGasLow = 0; - const vibeToken0 = vibeIsToken0(deployment); - - const signalGasLow = () => { - const now = Date.now(); - if (now - lastGasLow < GAS_LOW_MS) return; - lastGasLow = now; - onGasLow?.(); - }; - - const tick = async (index: number) => { - if (stopped || !enabled()) return; - const eth = ethBalance(index); - if (eth === null) return; - if (botNeedsGas(eth)) { - signalGasLow(); - return; - } - const latest = readReserves(); - if (!latest || latest.reserve0 === 0n || latest.reserve1 === 0n) return; - const { reserve0, reserve1 } = latest; - const spot = Number(quoteWad(reserve0, reserve1, vibeToken0)) / 1e18; - if (!Number.isFinite(spot) || spot <= 0) return; - if (!anchored) { - anchor = spot; - anchored = true; - } - const noise = (Math.random() - 0.5) * 0.4; - const { sellVibe, fraction } = planSwap(spot, makerTargetPrice(Date.now(), anchor), noise); - const poolIn = sellVibe - ? vibeReserve(reserve0, reserve1, vibeToken0) - : usdvReserve(reserve0, reserve1, vibeToken0); - const tokenIn = tokenInFor(deployment, sellVibe); - const amountIn = (poolIn * BigInt(Math.floor(fraction * 10_000))) / 10_000n; - if (amountIn === 0n) return; - const bal = tokenBalance(index, tokenIn); - if (bal === null) return; - const used = amountIn <= bal ? amountIn : (bal * 8n) / 10n; - if (used === 0n) throw new Error('maker inventory empty'); - const reserveIn = poolIn; - const reserveOut = sellVibe - ? usdvReserve(reserve0, reserve1, vibeToken0) - : vibeReserve(reserve0, reserve1, vibeToken0); - const exactOut = amountOut(used, reserveIn, reserveOut); - const out = exactOut > 1n ? exactOut - 1n : exactOut; - if (out === 0n) return; - const outs = swapOuts({ vibeToken0, sellVibe, amountOut: out }); - await sendSwap( - index, - encodeSwapLegs({ - tokenIn, - pair: deployment.pair, - recipient: addresses[index], - amountIn: used, - amount0Out: outs.amount0Out, - amount1Out: outs.amount1Out, - }), - ); - const nextVibe = sellVibe - ? vibeReserve(reserve0, reserve1, vibeToken0) + used - : vibeReserve(reserve0, reserve1, vibeToken0) - exactOut; - const nextUsdv = sellVibe - ? usdvReserve(reserve0, reserve1, vibeToken0) - exactOut - : usdvReserve(reserve0, reserve1, vibeToken0) + used; - if (nextVibe > 0n && nextUsdv > 0n) { - const next0 = vibeToken0 ? nextVibe : nextUsdv; - const next1 = vibeToken0 ? nextUsdv : nextVibe; - const next = Number(quoteWad(next0, next1, vibeToken0)) / 1e18; - if (Number.isFinite(next) && next > 0) onPrice?.(next); - } - }; - - const loop = async () => { - if (stopped) return; - try { - if (enabled() && addresses.length > 0) await tick(turn % addresses.length); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'maker swap failed'; - onError?.(message.split('\n')[0] ?? message); - } - turn += 1; - if (!stopped) timer = setTimeout(loop, TICK_MS); - }; - timer = setTimeout(loop, 400); - - return () => { - stopped = true; - if (timer) clearTimeout(timer); - }; -} diff --git a/app/vibenet/demos/validity/lib/makers.test.ts b/app/vibenet/demos/validity/lib/makers.test.ts deleted file mode 100644 index 88e761f..0000000 --- a/app/vibenet/demos/validity/lib/makers.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import type { StoredAccount } from '../../account/library/model'; -import { ensureMakers, MAKER_LABELS, rootAccount } from './makers'; - -function account(partial: Partial & Pick): StoredAccount { - return { - saltField: '', - salt: '0x', - address: '0x0000000000000000000000000000000000000001', - initialActors: [], - owners: [], - deployed: false, - configSeq: 0, - sessionKeys: [], - subAccounts: [], - createdAt: 0, - ...partial, - }; -} - -describe('rootAccount', () => { - it('walks up to the parent', () => { - const root = account({ id: 'root', label: 'Root' }); - const child = account({ id: 'child', label: 'Child', parentId: 'root' }); - expect(rootAccount(child, [root, child]).id).toBe('root'); - }); -}); - -describe('ensureMakers', () => { - it('reuses stored ids and creates the rest', () => { - const parent = account({ id: 'p', label: 'Parent' }); - const existing = account({ id: 'm1', label: MAKER_LABELS[0], parentId: 'p' }); - const created: string[] = []; - const [a, b] = ensureMakers(parent, [parent, existing], ['m1', 'missing'], (label) => { - created.push(label); - return { account: account({ id: 'm2', label, parentId: 'p' }) }; - }); - expect(a.id).toBe('m1'); - expect(b.id).toBe('m2'); - expect(created).toEqual([MAKER_LABELS[1]]); - }); -}); diff --git a/app/vibenet/demos/validity/lib/makers.ts b/app/vibenet/demos/validity/lib/makers.ts deleted file mode 100644 index 17e8c73..0000000 --- a/app/vibenet/demos/validity/lib/makers.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { StoredAccount } from '../../account/library/model'; - -export const MAKER_LABELS = ['Validity maker A', 'Validity maker B'] as const; - -export function rootAccount(account: StoredAccount, accounts: StoredAccount[]): StoredAccount { - let current = account; - const seen = new Set([current.id]); - while (current.parentId) { - const parent = accounts.find((item) => item.id === current.parentId); - if (!parent || seen.has(parent.id)) break; - seen.add(parent.id); - current = parent; - } - return current; -} - -type CreateSub = ( - label: string, - opts?: { withSpareKey?: boolean; parent?: StoredAccount }, -) => { account: StoredAccount } | null; - -/** Find or create the two delegated maker subaccounts under `parent`. */ -export function ensureMakers( - parent: StoredAccount, - accounts: StoredAccount[], - existingIds: [string, string] | undefined, - create: CreateSub, -): [StoredAccount, StoredAccount] { - const found: StoredAccount[] = []; - for (const id of existingIds ?? []) { - const match = accounts.find((item) => item.id === id); - if (match) found.push(match); - } - for (const label of MAKER_LABELS) { - if (found.length >= 2) break; - const existing = accounts.find( - (item) => item.parentId === parent.id && item.label === label && !found.some((row) => row.id === item.id), - ); - if (existing) found.push(existing); - } - while (found.length < 2) { - const label = MAKER_LABELS[found.length] ?? `Validity maker ${found.length + 1}`; - const created = create(label, { withSpareKey: true, parent }); - if (!created) throw new Error('Could not create a maker subaccount.'); - found.push(created.account); - } - return [found[0], found[1]]; -} diff --git a/app/vibenet/demos/validity/lib/singleton.ts b/app/vibenet/demos/validity/lib/singleton.ts index 97c1827..d8aeb6c 100644 --- a/app/vibenet/demos/validity/lib/singleton.ts +++ b/app/vibenet/demos/validity/lib/singleton.ts @@ -1,32 +1,17 @@ import { - concat, encodeDeployData, - encodeFunctionData, getContractAddress, keccak256, - parseEther, toBytes, zeroAddress, - type Account, type Address, type Hex, type PublicClient, - type TransactionReceipt, - type WalletClient, } from 'viem'; -import { - ACTIVATION_REGISTRY, - activationAbi, - B20_FACTORY, - encodeDeploymentParams, - encodeRoleGrant, - factoryAbi as b20FactoryAbi, - featureId, -} from '../../b20/lib/protocol'; +import { B20_FACTORY, factoryAbi as b20FactoryAbi } from '../../b20/lib/protocol'; import { vibenetApi } from '../../../library/client'; import { - erc20Abi, factoryAbi, factoryBytecode, helperAbi, @@ -34,22 +19,16 @@ import { minterAbi, minterBytecode, pairAbi, - SEED_USDV, - SEED_VIBE, } from './constants'; -import { VIBE_NAME, VIBE_SYMBOL } from './quote'; import type { Deployment } from './types'; /** - * Arachnid deterministic-deployment proxy. Already live on Vibenet; the - * keyless tx is only broadcast when a fresh chain is missing it. - * Address is CREATE(nickSigner, nonce=0). + * Arachnid deterministic-deployment proxy. Already live on Vibenet; used here + * only to re-derive the shared singleton CREATE2 addresses so the demo can + * discover the pool the central actor system deployed. Address is + * CREATE(nickSigner, nonce=0). */ export const CREATE2_DEPLOYER = '0x4e59b44847b379578588920cA78FbF26c0B4956C' as Address; -export const CREATE2_DEPLOYER_SIGNER = '0x3fab184622dc19b6109349b94811493bf2a45362' as Address; -export const CREATE2_DEPLOYER_FUND = parseEther('0.02'); -export const CREATE2_DEPLOYER_TX = - '0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222' as Hex; /** Factory `feeToSetter` is fixed so the CREATE2 address does not depend on who deploys. */ export const FACTORY_FEE_TO_SETTER = zeroAddress; @@ -113,76 +92,6 @@ export async function hasCode(client: PublicClient, address: Address): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - -async function wait(publicClient: PublicClient, hash: Hex): Promise { - const receipt = await publicClient.waitForTransactionReceipt({ - hash, - timeout: 120_000, - pollingInterval: 1_000, - }); - if (receipt.status === 'reverted') { - throw new Error(`Transaction reverted (${hash})`); - } - return receipt; -} - -async function waitForBytecode( - publicClient: PublicClient, - address: Address, - label: string, -): Promise { - const deadline = Date.now() + 60_000; - while (Date.now() < deadline) { - if (await hasCode(publicClient, address)) return; - await sleep(400); - } - throw new Error(`${label} bytecode not visible on the read RPC yet (${address}).`); -} - -/** Vibenet blocks are 6M gas; never ask the node for more than the current head allows. */ -async function capGas(publicClient: PublicClient, requested: bigint): Promise { - const block = await publicClient.getBlock({ blockTag: 'latest' }); - const max = block.gasLimit > 100_000n ? block.gasLimit - 100_000n : block.gasLimit; - return requested < max ? requested : max; -} - -async function send( - wallet: WalletClient, - publicClient: PublicClient, - account: Account, - request: { to?: Address; data: Hex; gas?: bigint; value?: bigint }, -): Promise { - const gas = request.gas !== undefined ? await capGas(publicClient, request.gas) : undefined; - const hash = await wallet.sendTransaction({ - account, - chain: wallet.chain, - ...request, - ...(gas !== undefined ? { gas } : {}), - }); - return wait(publicClient, hash); -} - -async function readPair( - publicClient: PublicClient, - factory: Address, - tokenA: Address, - tokenB: Address, -): Promise
{ - const pair = (await publicClient.readContract({ - address: factory, - abi: factoryAbi, - functionName: 'getPair', - args: [tokenA, tokenB], - })) as Address; - if (!pair || pair === zeroAddress) return null; - return pair; -} - async function pairTokens( client: PublicClient, pair: Address, @@ -250,7 +159,11 @@ function otherToken( return null; } -/** Live shared pool against faucet USDV, or null if this chain still needs the first deploy. */ +/** + * Live shared pool against faucet USDV, or null if the central actor system + * has not deployed + seeded it yet. Read-only: the demo never deploys — the + * fixtures are created by vibenet-setup and driven by the actor system. + */ export async function probeSingleton( client: PublicClient, usdv?: Address, @@ -273,265 +186,3 @@ export async function probeSingleton( if (!tokenA || !(await isB20Token(client, tokenA))) return null; return { ...predicted, tokenA, tokenB, token0: hit.token0, token1: hit.token1, pair: hit.pair }; } - -export async function ensureCreate2Deployer( - wallet: WalletClient, - publicClient: PublicClient, - account: Account, - onProgress?: (label: string) => void, -): Promise { - if (await hasCode(publicClient, CREATE2_DEPLOYER)) return; - onProgress?.('Publishing the CREATE2 deployer'); - const signerBal = await publicClient.getBalance({ address: CREATE2_DEPLOYER_SIGNER }); - if (signerBal < CREATE2_DEPLOYER_FUND) { - await send(wallet, publicClient, account, { - to: CREATE2_DEPLOYER_SIGNER, - data: '0x', - value: CREATE2_DEPLOYER_FUND - signerBal, - }); - } - const hash = (await publicClient.request({ - method: 'eth_sendRawTransaction', - params: [CREATE2_DEPLOYER_TX], - })) as Hex; - await wait(publicClient, hash); - await waitForBytecode(publicClient, CREATE2_DEPLOYER, 'CREATE2 deployer'); -} - -async function ensureCreate2Contract( - wallet: WalletClient, - publicClient: PublicClient, - account: Account, - salt: Hex, - initCode: Hex, - label: string, - gas: bigint, -): Promise
{ - const address = create2Address(salt, initCode); - if (await hasCode(publicClient, address)) return address; - await send(wallet, publicClient, account, { - to: CREATE2_DEPLOYER, - data: concat([salt, initCode]), - gas, - }); - await waitForBytecode(publicClient, address, label); - return address; -} - -async function seedPair( - wallet: WalletClient, - publicClient: PublicClient, - account: Account, - tokenA: Address, - tokenB: Address, - pair: Address, - minter: Address, -): Promise { - const mintUsdV = (to: Address, amount: bigint) => - send(wallet, publicClient, account, { - to: tokenB, - data: encodeFunctionData({ - abi: erc20Abi, - functionName: 'mint', - args: [to, amount], - }), - }); - await send(wallet, publicClient, account, { - to: minter, - data: encodeFunctionData({ - abi: minterAbi, - functionName: 'mint', - args: [tokenA, account.address, SEED_VIBE], - }), - }); - await mintUsdV(account.address, SEED_USDV); - await send(wallet, publicClient, account, { - to: tokenA, - data: encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [pair, SEED_VIBE], - }), - }); - await send(wallet, publicClient, account, { - to: tokenB, - data: encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [pair, SEED_USDV], - }), - }); - await send(wallet, publicClient, account, { - to: pair, - data: encodeFunctionData({ - abi: pairAbi, - functionName: 'mint', - args: [account.address], - }), - gas: 500_000n, - }); -} - -/** - * Deploy any missing singleton pieces and seed the pair once. - * Later callers no-op once `probeSingleton` would succeed. - */ -export async function ensureSingleton(args: { - wallet: WalletClient; - publicClient: PublicClient; - account: Account; - onProgress?: (label: string) => void; -}): Promise { - const { wallet, publicClient, account, onProgress } = args; - const tokenB = await resolveVibenetUsdv(); - const live = await probeSingleton(publicClient, tokenB); - if (live) return live; - - const note = (label: string) => onProgress?.(label); - await ensureCreate2Deployer(wallet, publicClient, account, onProgress); - const init = singletonInitCodes(); - const predicted = predictSingleton(); - - note('Deploying shared Uniswap V2 factory'); - const factory = await ensureCreate2Contract( - wallet, - publicClient, - account, - SINGLETON_SALTS.factory, - init.factory, - 'Factory', - 5_800_000n, - ); - note('Deploying shared swap helper'); - const helper = await ensureCreate2Contract( - wallet, - publicClient, - account, - SINGLETON_SALTS.helper, - init.helper, - 'Swap helper', - 1_000_000n, - ); - note('Deploying VIBE minter'); - const minter = await ensureCreate2Contract( - wallet, - publicClient, - account, - SINGLETON_SALTS.minter, - init.minter, - 'VIBE minter', - 1_000_000n, - ); - if ( - factory.toLowerCase() !== predicted.factory.toLowerCase() || - helper.toLowerCase() !== predicted.helper.toLowerCase() || - minter.toLowerCase() !== predicted.minter.toLowerCase() - ) { - throw new Error('CREATE2 address did not match the predicted singleton.'); - } - - const existing = await listPairs(publicClient, factory); - const usdvPair = existing.find((row) => otherToken(row.token0, row.token1, tokenB)); - let pair = usdvPair?.pair ?? null; - let tokenA = usdvPair ? otherToken(usdvPair.token0, usdvPair.token1, tokenB) : null; - if (!tokenA) { - for (const row of existing) { - for (const candidate of [row.token0, row.token1]) { - if (candidate.toLowerCase() === tokenB.toLowerCase()) continue; - if (await isB20Token(publicClient, candidate)) { - tokenA = candidate; - break; - } - } - if (tokenA) break; - } - } - if (pair && tokenA) { - // Official USDV pair already exists (maybe unseeded). - } else if (tokenA) { - note('Creating the shared pair'); - await send(wallet, publicClient, account, { - to: factory, - data: encodeFunctionData({ - abi: factoryAbi, - functionName: 'createPair', - args: [tokenA, tokenB], - }), - gas: 5_000_000n, - }); - const pairDeadline = Date.now() + 60_000; - while (!pair && Date.now() < pairDeadline) { - pair = await readPair(publicClient, factory, tokenA, tokenB); - if (!pair) await sleep(400); - } - if (!pair) throw new Error('Factory returned no pair.'); - } else { - const active = await publicClient.readContract({ - address: ACTIVATION_REGISTRY, - abi: activationAbi, - functionName: 'isActivated', - args: [featureId('asset')], - }); - if (!active) throw new Error('Creating B20 asset tokens is not available on this network right now.'); - note('Creating shared VIBE (B20)'); - const params = encodeDeploymentParams('asset', VIBE_NAME, VIBE_SYMBOL, account.address, 18, ''); - tokenA = (await publicClient.readContract({ - address: B20_FACTORY, - abi: b20FactoryAbi, - functionName: 'getB20Address', - args: [0, account.address, SINGLETON_SALTS.vibe], - })) as Address; - await send(wallet, publicClient, account, { - to: B20_FACTORY, - data: encodeFunctionData({ - abi: b20FactoryAbi, - functionName: 'createB20', - args: [0, SINGLETON_SALTS.vibe, params, []], - }), - gas: 4_000_000n, - }); - const deadline = Date.now() + 60_000; - while (Date.now() < deadline) { - const ready = await publicClient - .readContract({ - address: B20_FACTORY, - abi: b20FactoryAbi, - functionName: 'isB20Initialized', - args: [tokenA], - }) - .catch(() => false); - if (ready) break; - await sleep(400); - } - await send(wallet, publicClient, account, { - to: tokenA, - data: encodeRoleGrant('MINT_ROLE', minter), - }); - note('Creating the shared pair'); - await send(wallet, publicClient, account, { - to: factory, - data: encodeFunctionData({ - abi: factoryAbi, - functionName: 'createPair', - args: [tokenA, tokenB], - }), - gas: 5_000_000n, - }); - const pairDeadline = Date.now() + 60_000; - while (!pair && Date.now() < pairDeadline) { - pair = await readPair(publicClient, factory, tokenA, tokenB); - if (!pair) await sleep(400); - } - if (!pair) throw new Error('Factory returned no pair.'); - } - if (!tokenA) throw new Error('Could not resolve the shared VIBE token.'); - await waitForBytecode(publicClient, pair, 'Pair'); - - const { token0, token1, reserve0, reserve1 } = await pairTokens(publicClient, pair); - if (reserve0 === 0n || reserve1 === 0n) { - note('Seeding VIBE/USDV (~$0.07)'); - await seedPair(wallet, publicClient, account, tokenA, tokenB, pair, minter); - } - - return { tokenA, tokenB, token0, token1, factory, pair, helper, minter }; -} diff --git a/app/vibenet/demos/validity/lib/store.test.ts b/app/vibenet/demos/validity/lib/store.test.ts index 26b8d84..3ab4718 100644 --- a/app/vibenet/demos/validity/lib/store.test.ts +++ b/app/vibenet/demos/validity/lib/store.test.ts @@ -9,16 +9,12 @@ describe('parseStored', () => { v: 2, chainId: 84538453, genesisHash: '0xabc', - accountId: 'acct-1', - makerAccountIds: ['m1', 'm2'], }), ); expect(parsed).toEqual({ v: 2, chainId: 84538453, genesisHash: '0xabc', - accountId: 'acct-1', - makerAccountIds: ['m1', 'm2'], deployment: undefined, orders: undefined, }); diff --git a/app/vibenet/demos/validity/lib/store.ts b/app/vibenet/demos/validity/lib/store.ts index 1f4f274..377b282 100644 --- a/app/vibenet/demos/validity/lib/store.ts +++ b/app/vibenet/demos/validity/lib/store.ts @@ -7,9 +7,6 @@ export type StoredState = { v: 2; chainId: number; genesisHash: string; - /** Shared account that deployed this pool (makers are its subaccounts). */ - accountId?: string; - makerAccountIds?: [string, string]; deployment?: Deployment; orders?: PlacedOrder[]; }; @@ -159,11 +156,6 @@ export function loadState(): StoredState | null { } } -function parseMakerIds(value: unknown): [string, string] | undefined { - if (!Array.isArray(value) || !isId(value[0]) || !isId(value[1])) return undefined; - return [value[0], value[1]]; -} - export function parseStored(raw: string): StoredState | null { const parsed = JSON.parse(raw, bnReviver) as Partial & { v?: number }; if (typeof parsed.chainId !== 'number' || typeof parsed.genesisHash !== 'string') return null; @@ -172,8 +164,6 @@ export function parseStored(raw: string): StoredState | null { v: 2, chainId: parsed.chainId, genesisHash: parsed.genesisHash, - accountId: isId(parsed.accountId) ? parsed.accountId : undefined, - makerAccountIds: parseMakerIds(parsed.makerAccountIds), deployment: parseDeployment(parsed.deployment), orders: parseOrders(parsed.orders), }; From 9b7792a953c047bd7443fecbc4c2c8bf4afb4373 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Wed, 2 Sep 2026 13:00:59 -0700 Subject: [PATCH 2/2] refactor(vibenet): size the Validity price chart to its container Replace the fixed 960x440 viewBox with a ResizeObserver-measured canvas so the chart draws in CSS pixels at any container shape. Prepares the chart to sit beside the order ticket without letterboxing; visually a no-op at today's full-width layout. Generated with Claude Code Co-Authored-By: Claude --- .../validity/components/PriceCandles.tsx | 276 +++++++++--------- 1 file changed, 145 insertions(+), 131 deletions(-) diff --git a/app/vibenet/demos/validity/components/PriceCandles.tsx b/app/vibenet/demos/validity/components/PriceCandles.tsx index 2617b33..42edcfa 100644 --- a/app/vibenet/demos/validity/components/PriceCandles.tsx +++ b/app/vibenet/demos/validity/components/PriceCandles.tsx @@ -10,8 +10,6 @@ const BUY_PLOT = '#22ad73'; const SELL_PLOT = '#ed5966'; const BUCKET_MS = CANDLE_BUCKET_MS; const WINDOW_MS = CANDLE_WINDOW_MS; -const WIDTH = 960; -const HEIGHT = 440; const PAD = { top: 20, right: 20, bottom: 40, left: 68 }; export type PriceSample = { t: number; price: number }; @@ -131,13 +129,25 @@ export function PriceCandles({ samples, levels = [], fills = [] }: Props) { return () => window.clearInterval(id); }, []); const candles = useMemo(() => toCandles(samples ?? [], { now }), [now, samples]); - const innerW = WIDTH - PAD.left - PAD.right; - const innerH = HEIGHT - PAD.top - PAD.bottom; + // Draw in measured CSS pixels so text stays crisp at any container shape. + const [chartEl, setChartEl] = useState(null); + const [size, setSize] = useState<{ w: number; h: number } | null>(null); + useEffect(() => { + if (!chartEl) return; + const update = () => + setSize({ w: Math.round(chartEl.clientWidth), h: Math.round(chartEl.clientHeight) }); + update(); + const observer = new ResizeObserver(update); + observer.observe(chartEl); + return () => observer.disconnect(); + }, [chartEl]); + const innerW = size ? size.w - PAD.left - PAD.right : 0; + const innerH = size ? size.h - PAD.top - PAD.bottom : 0; const visibleLevels = levels.filter((level) => Number.isFinite(level.price) && level.price > 0); const visibleFills = fills.filter((fill) => Number.isFinite(fill.price) && fill.price > 0 && fill.t > 0); const layout = useMemo(() => { - if (candles.length === 0) return null; + if (candles.length === 0 || innerW <= 0 || innerH <= 0) return null; let lo = candles[0].l; let hi = candles[0].h; for (const candle of candles) { @@ -172,7 +182,7 @@ export function PriceCandles({ samples, levels = [], fills = [] }: Props) { }, [candles, innerH, innerW, visibleFills, visibleLevels]); const firstOpen = candles[0]?.o; - const lastClose = layout?.last; + const lastClose = candles[candles.length - 1]?.c; const change = firstOpen && lastClose ? ((lastClose - firstOpen) / firstOpen) * 100 : 0; const up = change >= 0; @@ -189,141 +199,145 @@ export function PriceCandles({ samples, levels = [], fills = [] }: Props) {
- {layout ? formatAxisPrice(layout.last) : '—'} + {lastClose !== undefined ? formatAxisPrice(lastClose) : '—'}
- {layout ? `${up ? '+' : ''}${change.toFixed(2)}%` : ''} + {lastClose !== undefined ? `${up ? '+' : ''}${change.toFixed(2)}%` : ''}
- {layout ? ( - - - {layout.yTicks.map((tick) => ( - - - - {formatAxisPrice(tick)} - - - ))} - {layout.xTicks.map((tick) => ( - - {formatAxisTime(tick)} - - ))} - - USDV - - {candles.map((candle, index) => { - const color = isUpCandle(candle, candles[index - 1]) ? BUY_PLOT : SELL_PLOT; - const cx = layout.x(candle.t + BUCKET_MS / 2); - const highY = layout.y(candle.h); - const lowY = layout.y(candle.l); - const bodyTop = layout.y(Math.max(candle.o, candle.c)); - const bodyBot = layout.y(Math.min(candle.o, candle.c)); - const rawBody = Math.max(bodyBot - bodyTop, 0); - const doji = rawBody < 0.8; - const bodyH = doji ? 1.6 : Math.max(rawBody, 2); - const bodyW = Math.min(Math.max(layout.slot * 0.55, 4), 14); - return ( - - - - - ); - })} - {visibleLevels.map((level) => { - const y = layout.y(level.price); - const color = level.side === 'buy' ? BUY_PLOT : SELL_PLOT; - const draft = level.kind === 'draft'; - return ( - - + {candles.length > 0 ? ( +
+ {layout && size ? ( + + + {layout.yTicks.map((tick) => ( + + + + {formatAxisPrice(tick)} + + + ))} + {layout.xTicks.map((tick) => ( - {draft ? 'draft' : level.side} {formatAxisPrice(level.price)} + {formatAxisTime(tick)} - - ); - })} - {visibleFills.map((fill) => { - const cx = layout.x(fill.t); - const cy = layout.y(fill.price); - if (cx < -8 || cx > innerW + 8) return null; - const color = fill.side === 'buy' ? BUY_PLOT : SELL_PLOT; - const r = fill.highlighted ? 7 : 4.5; - return ( - - {fill.highlighted ? ( - - ) : null} - - - {fill.highlighted ? ( - innerW * 0.62 ? cx - 10 : cx + 10} - y={cy - 10} - textAnchor={cx > innerW * 0.62 ? 'end' : 'start'} - fill={color} - fontSize={10} - fontFamily="ui-monospace, monospace" - > - included {formatAxisPrice(fill.price)} - - ) : null} - - ); - })} - - + ))} + + USDV + + {candles.map((candle, index) => { + const color = isUpCandle(candle, candles[index - 1]) ? BUY_PLOT : SELL_PLOT; + const cx = layout.x(candle.t + BUCKET_MS / 2); + const highY = layout.y(candle.h); + const lowY = layout.y(candle.l); + const bodyTop = layout.y(Math.max(candle.o, candle.c)); + const bodyBot = layout.y(Math.min(candle.o, candle.c)); + const rawBody = Math.max(bodyBot - bodyTop, 0); + const doji = rawBody < 0.8; + const bodyH = doji ? 1.6 : Math.max(rawBody, 2); + const bodyW = Math.min(Math.max(layout.slot * 0.55, 4), 14); + return ( + + + + + ); + })} + {visibleLevels.map((level) => { + const y = layout.y(level.price); + const color = level.side === 'buy' ? BUY_PLOT : SELL_PLOT; + const draft = level.kind === 'draft'; + return ( + + + + {draft ? 'draft' : level.side} {formatAxisPrice(level.price)} + + + ); + })} + {visibleFills.map((fill) => { + const cx = layout.x(fill.t); + const cy = layout.y(fill.price); + if (cx < -8 || cx > innerW + 8) return null; + const color = fill.side === 'buy' ? BUY_PLOT : SELL_PLOT; + const r = fill.highlighted ? 7 : 4.5; + return ( + + {fill.highlighted ? ( + + ) : null} + + + {fill.highlighted ? ( + innerW * 0.62 ? cx - 10 : cx + 10} + y={cy - 10} + textAnchor={cx > innerW * 0.62 ? 'end' : 'start'} + fill={color} + fontSize={10} + fontFamily="ui-monospace, monospace" + > + included {formatAxisPrice(fill.price)} + + ) : null} + + ); + })} + + + ) : null} +
) : (

Tape starts once the simulated pool prints a mid.