Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions app/vibenet/demos/validity/ValidityDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,21 @@ import { VIBENET_WS_URL } from '../../library/config';
import {
ageRestoredOrders,
maxBlockForExpiry,
minBlockForDelay,
occupyingOrder,
orderBlockExpired,
orderWallClockExpired,
restingOrderToReplace,
} 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,
Expand Down Expand Up @@ -124,6 +131,7 @@ function ValidityDemoInner() {
const [offsetBps, setOffsetBps] = useState(100);
const [priceOverrideWad, setPriceOverrideWad] = useState<bigint | null>(null);
const [expirySeconds, setExpirySeconds] = useState(15);
const [delaySeconds, setDelaySeconds] = useState(0);
const [submitMode, setSubmitMode] = useState<SubmitMode>('concurrent');
const [orders, setOrders] = useState<PlacedOrder[]>([]);
const [hoveredOrderId, setHoveredOrderId] = useState<string | null>(null);
Expand Down Expand Up @@ -577,8 +585,11 @@ function ValidityDemoInner() {
? clampNoncelessExpiry(expirySeconds)
: Math.min(MAX_EXPIRY_SECONDS, expirySeconds);
const maxBlock = maxBlockForExpiry(blockNumber, seconds);
return [...draft.predicates, blockExpiryPredicate(maxBlock)];
}, [blockNumber, draft, expirySeconds, submitMode]);
const predicates = [...draft.predicates, blockExpiryPredicate(maxBlock)];
const delay = Math.min(delaySeconds, Math.max(0, seconds - 1));
if (delay > 0) predicates.push(blockDelayPredicate(minBlockForDelay(blockNumber, delay)));
return predicates;
}, [blockNumber, delaySeconds, draft, expirySeconds, submitMode]);

const chartLevels = useMemo((): PriceLevel[] => {
const levels: PriceLevel[] = [];
Expand Down Expand Up @@ -723,7 +734,10 @@ function ValidityDemoInner() {
: Math.min(MAX_EXPIRY_SECONDS, expirySeconds);
const block = blockNumber ?? (await publicClient.getBlockNumber({ cacheTime: 0 }));
const maxBlock = maxBlockForExpiry(block, seconds);
const delay = Math.min(delaySeconds, Math.max(0, seconds - 1));
const minBlock = delay > 0 ? minBlockForDelay(block, delay) : undefined;
const validity = [...draft.predicates, blockExpiryPredicate(maxBlock)];
if (minBlock !== undefined) validity.push(blockDelayPredicate(minBlock));
const fromHead = headFeesRef.current;
const estimated =
fromHead ??
Expand Down Expand Up @@ -803,8 +817,10 @@ function ValidityDemoInner() {
targetPriceWad: draft.priceWad,
size: TRADE_VIBE,
expirySeconds: seconds,
delaySeconds: delay > 0 ? delay : undefined,
submitMode,
maxBlock,
minBlock,
submittedAt: Date.now(),
txHash: hash,
nonce,
Expand Down Expand Up @@ -939,6 +955,7 @@ function ValidityDemoInner() {
side={side}
offsetBps={offsetBps}
expirySeconds={expirySeconds}
delaySeconds={delaySeconds}
submitMode={submitMode}
busy={busy}
vibeBalance={vibeBalance}
Expand All @@ -954,11 +971,16 @@ function ValidityDemoInner() {
setPriceOverrideWad(null);
}}
onPriceOverride={setPriceOverrideWad}
onExpiry={setExpirySeconds}
onExpiry={(seconds) => {
setExpirySeconds(seconds);
if (delaySeconds >= seconds) setDelaySeconds(0);
}}
onDelay={setDelaySeconds}
onSubmitMode={(mode) => {
setSubmitMode(mode);
if (mode === 'concurrent' && expirySeconds > MAX_NONCELESS_SECONDS) {
setExpirySeconds(15);
if (delaySeconds >= 15) setDelaySeconds(0);
}
}}
onSubmit={() => {
Expand Down Expand Up @@ -1015,7 +1037,7 @@ function ValidityDemoInner() {
</Text>
<Text variant="footnote" tone="muted" className="mt-1">
{submitMode === 'concurrent' ? '8130 concurrent' : 'Replace resting nonce'} · expires in{' '}
{expirySeconds}s
{expirySeconds}s{delaySeconds > 0 ? ` · starts in ~${delaySeconds}s` : ''}
</Text>
</div>
<ul className="flex flex-col gap-2">
Expand Down
3 changes: 3 additions & 0 deletions app/vibenet/demos/validity/components/OrderList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ export function OrderList({ orders, highlightedOrderId, onHighlight }: Props) {
<Text variant="footnote" tone="muted" className="tabular-nums">
{formatClock(order.submittedAt)}
{order.submitMode === 'concurrent' ? ' · 8130' : order.submitMode === 'replace' ? ' · replace' : null}
{order.status === 'pending' && order.delaySeconds
? ` · starts ${formatClock(order.submittedAt + order.delaySeconds * 1000)}`
: null}
{order.filledAt ? ` → ${formatClock(order.filledAt)}` : null}
{filled && order.fillPriceWad !== undefined
? ` · ${formatPrice(order.fillPriceWad)}`
Expand Down
87 changes: 61 additions & 26 deletions app/vibenet/demos/validity/components/OrderTicket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { Side, SubmitMode } from '../lib/types';
const TRADE_LABEL = formatTokenAmount(TRADE_VIBE);

const EXPIRIES = [5, 15, 60] as const;
const DELAYS = [0, 5, 15] as const;
const OFFSET_MAX_BPS = 500;
const OFFSET_STEP_BPS = 10;
const OFFSET_MARKS = [0, 100, 200, 300, 400, 500] as const;
Expand All @@ -23,6 +24,7 @@ type Props = {
side: Side;
offsetBps: number;
expirySeconds: number;
delaySeconds: number;
submitMode: SubmitMode;
busy: boolean;
vibeBalance: bigint | null;
Expand All @@ -33,6 +35,7 @@ type Props = {
onOffset: (bps: number) => void;
onPriceOverride: (wad: bigint | null) => void;
onExpiry: (seconds: number) => void;
onDelay: (seconds: number) => void;
onSubmitMode: (mode: SubmitMode) => void;
onSubmit: () => void;
canAfford: boolean;
Expand All @@ -48,6 +51,7 @@ export function OrderTicket({
side,
offsetBps,
expirySeconds,
delaySeconds,
submitMode,
busy,
vibeBalance,
Expand All @@ -56,6 +60,7 @@ export function OrderTicket({
onSide,
onOffset,
onExpiry,
onDelay,
onSubmitMode,
onSubmit,
onPriceOverride,
Expand Down Expand Up @@ -234,32 +239,62 @@ export function OrderTicket({
: `8130 nonceless — stack several at once. Envelope max ${MAX_NONCELESS_SECONDS}s.`}
</Text>
</div>
<div className="flex flex-col gap-2">
<Text variant="caption" tone="muted">
Expiry
</Text>
<div className="flex gap-2">
{EXPIRIES.map((seconds) => {
const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS;
return (
<button
key={seconds}
type="button"
disabled={blocked}
title={blocked ? `8130 nonceless max is ${MAX_NONCELESS_SECONDS}s` : undefined}
onClick={() => onExpiry(seconds)}
className={
blocked
? 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] text-bds-gray-40 dark:border-white/10'
: seconds === expirySeconds
? 'rounded-full bg-foreground px-3 py-1 text-[12px] text-background'
: 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] dark:border-white/10'
}
>
{seconds}s
</button>
);
})}
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-2">
<Text variant="caption" tone="muted">
Expiry
</Text>
<div className="flex flex-wrap gap-2">
{EXPIRIES.map((seconds) => {
const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS;
return (
<button
key={seconds}
type="button"
disabled={blocked}
title={blocked ? `8130 nonceless max is ${MAX_NONCELESS_SECONDS}s` : undefined}
onClick={() => onExpiry(seconds)}
className={
blocked
? 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] text-bds-gray-40 dark:border-white/10'
: seconds === expirySeconds
? 'rounded-full bg-foreground px-3 py-1 text-[12px] text-background'
: 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] dark:border-white/10'
}
>
{seconds}s
</button>
);
})}
</div>
</div>
<div className="flex flex-col gap-2">
<Text variant="caption" tone="muted">
Delay
</Text>
<div className="flex flex-wrap gap-2">
{DELAYS.map((seconds) => {
const blocked = seconds > 0 && seconds >= expirySeconds;
return (
<button
key={seconds}
type="button"
disabled={blocked}
title={blocked ? 'Delay must be shorter than expiry' : undefined}
onClick={() => onDelay(seconds)}
className={
blocked
? 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] text-bds-gray-40 dark:border-white/10'
: seconds === delaySeconds
? 'rounded-full bg-foreground px-3 py-1 text-[12px] text-background'
: 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] dark:border-white/10'
}
>
{seconds === 0 ? 'Off' : `${seconds}s`}
</button>
);
})}
</div>
</div>
</div>
<Button onClick={onSubmit} disabled={busy || !canAfford} className="w-full">
Expand Down
10 changes: 9 additions & 1 deletion app/vibenet/demos/validity/lib/annotate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';

import { annotatedValidity, reviewClauses } from './annotate';
import { WAD } from './constants';
import { blockExpiryPredicate, priceValidity } from './predicates';
import { blockDelayPredicate, blockExpiryPredicate, priceValidity } from './predicates';

const PAIR = '0x1111111111111111111111111111111111111111';

Expand Down Expand Up @@ -41,6 +41,14 @@ describe('annotatedValidity', () => {
expect(notes).toContain('L2 block 18422105');
expect(notes.some((note) => note?.includes('at most'))).toBe(true);
});

it('decodes a block-number delay as a not-before bound', () => {
const rows = annotatedValidity([blockDelayPredicate(18_422_105n)]);
const notes = rows.map((row) => row.note).filter(Boolean);
expect(notes).toContain('Block-number delay');
expect(notes).toContain('L2 block 18422105');
expect(notes.some((note) => note?.includes('once') && note.includes('at least'))).toBe(true);
});
});

describe('reviewClauses', () => {
Expand Down
5 changes: 3 additions & 2 deletions app/vibenet/demos/validity/lib/annotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ function storageNotes(predicate: StoragePredicate, vibeToken0: boolean): Record<
function notesFor(predicate: ValidityPredicate, vibeToken0: boolean): Record<string, string> {
if (predicate.type === 'storage') return storageNotes(predicate, vibeToken0);
const block = BigInt(predicate.params.value);
const lowerBound = predicate.params.op === '>=' || predicate.params.op === '>';
return {
type: 'Block-number expiry',
op: `Include only while the head is ${comparePhrase(predicate.params.op)}`,
type: lowerBound ? 'Block-number delay' : 'Block-number expiry',
op: `Include only ${lowerBound ? 'once' : 'while'} the head is ${comparePhrase(predicate.params.op)}`,
value: `L2 block ${block.toString()}`,
};
}
Expand Down
13 changes: 12 additions & 1 deletion app/vibenet/demos/validity/lib/orders.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { ageRestoredOrders, occupyingOrder, maxBlockForExpiry, orderBlockExpired, orderWallClockExpired, restingOrderToReplace } from './orders';
import { ageRestoredOrders, occupyingOrder, maxBlockForExpiry, minBlockForDelay, orderBlockExpired, orderWallClockExpired, restingOrderToReplace } from './orders';

describe('orderWallClockExpired', () => {
it('expires a resting order after the window plus grace', () => {
Expand Down Expand Up @@ -45,6 +45,17 @@ describe('maxBlockForExpiry', () => {
});
});

describe('minBlockForDelay', () => {
it('mirrors the expiry block math', () => {
expect(minBlockForDelay(1_000n, 5)).toBe(1_025n);
expect(minBlockForDelay(1_000n, 15)).toBe(1_075n);
});

it('is a no-op without a delay', () => {
expect(minBlockForDelay(1_000n, 0)).toBe(1_000n);
});
});

const fees = { nonce: 3, maxFeePerGas: 1n, maxPriorityFeePerGas: 1n, side: 'buy' as const };

describe('occupyingOrder', () => {
Expand Down
7 changes: 7 additions & 0 deletions app/vibenet/demos/validity/lib/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export function maxBlockForExpiry(currentBlock: bigint, expirySeconds: number):
return currentBlock + BigInt(blocks);
}

/** Inclusive first L2 block a delayed validity tx may land in. */
export function minBlockForDelay(currentBlock: bigint, delaySeconds: number): bigint {
if (delaySeconds <= 0) return currentBlock;
const blocks = Math.max(1, Math.ceil(delaySeconds / BLOCK_SECONDS));
return currentBlock + BigInt(blocks);
}

export function occupyingOrder(
orders: Pick<PlacedOrder, 'id' | 'nonce' | 'status' | 'side' | 'maxFeePerGas' | 'maxPriorityFeePerGas'>[],
nonce: number,
Expand Down
8 changes: 8 additions & 0 deletions app/vibenet/demos/validity/lib/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,11 @@ export function blockExpiryPredicate(maxBlock: bigint): ValidityPredicate {
params: { op: '<=', value: toWord(maxBlock) },
};
}

/** Not-before bound: the sequencer holds the tx until this block. */
export function blockDelayPredicate(minBlock: bigint): ValidityPredicate {
return {
type: 'block_number',
params: { op: '>=', value: toWord(minBlock) },
};
}
3 changes: 3 additions & 0 deletions app/vibenet/demos/validity/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ function parseOrder(value: unknown): PlacedOrder | undefined {
validity,
};
if (row.submitMode === 'replace' || row.submitMode === 'concurrent') order.submitMode = row.submitMode;
if (typeof row.delaySeconds === 'number') order.delaySeconds = row.delaySeconds;
const maxBlock = asBigint(row.maxBlock);
if (maxBlock !== undefined) order.maxBlock = maxBlock;
const minBlock = asBigint(row.minBlock);
if (minBlock !== undefined) order.minBlock = minBlock;
if (typeof row.txHash === 'string' && /^0x[0-9a-fA-F]+$/.test(row.txHash)) {
order.txHash = row.txHash as PlacedOrder['txHash'];
}
Expand Down
2 changes: 2 additions & 0 deletions app/vibenet/demos/validity/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ export type PlacedOrder = {
targetPriceWad: bigint;
size: bigint;
expirySeconds: number;
delaySeconds?: number;
submitMode?: SubmitMode;
maxBlock?: bigint;
minBlock?: bigint;
submittedAt: number;
txHash?: Hex;
nonce?: number;
Expand Down
Loading