Skip to content

Commit 0ea534c

Browse files
feat(vibenet): rest concurrent Validity conditions on 8130
Denim is 200ms, so predicates and the tape need that clock. Nonceless 8130 lets several conditions sit in the Vibenet mempool without replacing each other. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 31ed151 commit 0ea534c

14 files changed

Lines changed: 283 additions & 95 deletions

File tree

.env.example

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ NEXT_PUBLIC_VIBENET_RPC_URL=https://rpc.vibes.base.org
5353
# NEXT_PUBLIC_BENCHMARK_API_BASE_URL=
5454

5555
# Validity demo (/vibenet/demos/validity). Server-side RPC proxy only.
56-
# Defaults to the public Vibenet RPC (`NEXT_PUBLIC_VIBENET_RPC_URL`). ETH comes
57-
# from the Vibenet faucet — do not set a funder key. Override only for a local
58-
# node with --enable-experimental-validity-transactions.
56+
# Defaults to the public Vibenet RPC (`NEXT_PUBLIC_VIBENET_RPC_URL`) for both
57+
# reads and `base_sendRawTransactionValidity` submits. ETH comes from the
58+
# Vibenet faucet — do not set a funder key.
5959
# VALIDITY_DEMO_RPC_URL=https://rpc.vibes.base.org
60-
# Local Anvil / just devnet:
60+
# VALIDITY_DEMO_SUBMIT_RPC_URL=https://rpc.vibes.base.org
61+
# Local node with --enable-experimental-validity-transactions:
6162
# VALIDITY_DEMO_RPC_URL=http://127.0.0.1:8545

app/vibenet/demos/catalogue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const DEMOS: DemoEntry[] = [
5555
points: [
5656
'Add storage and block-number conditions to an ordinary swap',
5757
'A simulated AMM makes those conditions visible on a moving mid',
58-
'Optional 5s / 15s / 60s bound so a stale condition cannot fire later',
58+
'Stack several 8130 conditions at once, or replace the resting one',
5959
],
6060
available: true,
6161
},

app/vibenet/demos/validity/ValidityDemo.tsx

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4-
import type { Account, PublicClient, WalletClient } from 'viem';
4+
import type { Account, Hex, PublicClient, WalletClient } from 'viem';
55
import { formatEther } from 'viem';
66

77
import { trackValidityOrder } from '../../../analytics/events';
@@ -25,8 +25,9 @@ import {
2525
signCall,
2626
tokenBalance,
2727
} from './lib/amm';
28+
import { clampNoncelessExpiry, signNoncelessCall } from './lib/aa';
2829
import { startBots, allNeedGas, botNeedsGas, refuelValue } from './lib/bots';
29-
import { MAX_EXPIRY_SECONDS } from './lib/constants';
30+
import { BLOCK_MS, MAX_EXPIRY_SECONDS, MAX_NONCELESS_SECONDS } from './lib/constants';
3031
import { faucetErrorMessage, seedEthFromFaucet } from './lib/faucet';
3132
import {
3233
maxBlockForExpiry,
@@ -56,11 +57,11 @@ import {
5657
sendValidityTransaction,
5758
} from './lib/rpc';
5859
import { accountsFrom, createState, dropDeployment, loadState, saveState, type StoredState } from './lib/store';
59-
import type { ChainStatus, PlacedOrder, Rectangle, Reserves, Side } from './lib/types';
60+
import type { ChainStatus, PlacedOrder, Rectangle, Reserves, Side, SubmitMode } from './lib/types';
6061

61-
const POLL_MS = 400;
62-
/** L2 blocks are ~2s. viem's default 4s block cache made this skip 2–3 heads. */
63-
const BLOCK_POLL_MS = 1_000;
62+
const POLL_MS = BLOCK_MS;
63+
/** Denim heads are 200ms. viem's default 4s block cache would skip dozens. */
64+
const BLOCK_POLL_MS = BLOCK_MS;
6465
const DEFAULT_SIZE_FRACTION = 50n; // 1/50 of inventory
6566

6667
function wadToNumber(wad: bigint): number {
@@ -85,7 +86,8 @@ export function ValidityDemo() {
8586
const [hoverPrice, setHoverPrice] = useState<bigint | null>(null);
8687
const [side, setSide] = useState<Side>('buy');
8788
const [offsetBps, setOffsetBps] = useState(100);
88-
const [expirySeconds, setExpirySeconds] = useState(60);
89+
const [expirySeconds, setExpirySeconds] = useState(15);
90+
const [submitMode, setSubmitMode] = useState<SubmitMode>('concurrent');
8991
const [orders, setOrders] = useState<PlacedOrder[]>([]);
9092
const [hoveredOrderId, setHoveredOrderId] = useState<string | null>(null);
9193
const [samples, setSamples] = useState<PriceSample[]>([]);
@@ -429,7 +431,7 @@ export function ValidityDemo() {
429431

430432
const id = window.setInterval(() => {
431433
void tick();
432-
}, 700);
434+
}, 250);
433435
void tick();
434436
return () => {
435437
cancelled = true;
@@ -613,12 +615,16 @@ export function ValidityDemo() {
613615
amount0Out,
614616
amount1Out,
615617
});
616-
const confirmedNonce = await publicClient.getTransactionCount({
617-
address: account.address,
618-
blockTag: 'latest',
619-
});
620-
const occupant = occupyingOrder(ordersRef.current, confirmedNonce);
621-
const replaced = restingOrderToReplace(ordersRef.current, confirmedNonce);
618+
const seconds =
619+
submitMode === 'concurrent'
620+
? clampNoncelessExpiry(expirySeconds)
621+
: Math.min(MAX_EXPIRY_SECONDS, expirySeconds);
622+
const block = await publicClient.getBlockNumber({ cacheTime: 0 });
623+
const maxBlock = maxBlockForExpiry(block, seconds);
624+
const validity = [...draft.predicates];
625+
if (status?.blockNumberPredicate) {
626+
validity.push(blockExpiryPredicate(maxBlock));
627+
}
622628
const estimated = await publicClient.estimateFeesPerGas().catch(() => null);
623629
const padded =
624630
estimated?.maxFeePerGas !== undefined && estimated.maxPriorityFeePerGas !== undefined
@@ -627,55 +633,73 @@ export function ValidityDemo() {
627633
maxPriorityFeePerGas: estimated.maxPriorityFeePerGas,
628634
})
629635
: null;
636+
trackValidityOrder(side, 'submitted');
637+
let hash: Hex;
638+
let nonce: number | undefined;
630639
let fees = padded;
631-
if (occupant?.maxFeePerGas !== undefined && occupant.maxPriorityFeePerGas !== undefined) {
632-
fees = bumpReplacementFees(
633-
{
634-
maxFeePerGas: occupant.maxFeePerGas,
635-
maxPriorityFeePerGas: occupant.maxPriorityFeePerGas,
636-
},
637-
padded,
638-
);
639-
}
640-
const sign = (nextFees: typeof fees) =>
641-
signCall({
642-
wallet,
643-
publicClient,
644-
account,
640+
let replaced: ReturnType<typeof restingOrderToReplace>;
641+
if (submitMode === 'concurrent') {
642+
replaced = undefined;
643+
const signed = await signNoncelessCall({
644+
privateKey: state.userKey,
645+
chainId: status?.chainId ?? state.chainId,
645646
to: call.to,
646647
data: call.data,
647-
nonce: confirmedNonce,
648-
fees: nextFees,
648+
expiresIn: seconds,
649+
fees: padded,
650+
publicClient,
649651
});
650-
let signedResult = await sign(fees);
651-
const seconds = Math.min(MAX_EXPIRY_SECONDS, expirySeconds);
652-
const block = await publicClient.getBlockNumber({ cacheTime: 0 });
653-
const maxBlock = maxBlockForExpiry(block, seconds);
654-
const validity = [...draft.predicates];
655-
if (status?.blockNumberPredicate) {
656-
validity.push(blockExpiryPredicate(maxBlock));
657-
}
658-
trackValidityOrder(side, 'submitted');
659-
let hash;
660-
try {
661-
hash = await sendValidityTransaction(publicClient, signedResult.signed, validity);
662-
} catch (err) {
663-
if (!isReplacementUnderpriced(err) || !signedResult.fees) throw err;
664-
signedResult = await sign(bumpReplacementFees(signedResult.fees, padded));
665-
hash = await sendValidityTransaction(publicClient, signedResult.signed, validity);
652+
hash = await sendValidityTransaction(publicClient, signed.signed, validity);
653+
} else {
654+
const confirmedNonce = await publicClient.getTransactionCount({
655+
address: account.address,
656+
blockTag: 'latest',
657+
});
658+
const occupant = occupyingOrder(ordersRef.current, confirmedNonce);
659+
replaced = restingOrderToReplace(ordersRef.current, confirmedNonce);
660+
if (occupant?.maxFeePerGas !== undefined && occupant.maxPriorityFeePerGas !== undefined) {
661+
fees = bumpReplacementFees(
662+
{
663+
maxFeePerGas: occupant.maxFeePerGas,
664+
maxPriorityFeePerGas: occupant.maxPriorityFeePerGas,
665+
},
666+
padded,
667+
);
668+
}
669+
const sign = (nextFees: typeof fees) =>
670+
signCall({
671+
wallet,
672+
publicClient,
673+
account,
674+
to: call.to,
675+
data: call.data,
676+
nonce: confirmedNonce,
677+
fees: nextFees,
678+
});
679+
let signedResult = await sign(fees);
680+
try {
681+
hash = await sendValidityTransaction(publicClient, signedResult.signed, validity);
682+
} catch (err) {
683+
if (!isReplacementUnderpriced(err) || !signedResult.fees) throw err;
684+
signedResult = await sign(bumpReplacementFees(signedResult.fees, padded));
685+
hash = await sendValidityTransaction(publicClient, signedResult.signed, validity);
686+
}
687+
nonce = signedResult.nonce;
688+
fees = signedResult.fees;
666689
}
667690
const order: PlacedOrder = {
668691
id: newId(),
669692
side,
670693
targetPriceWad: draft.priceWad,
671694
size: amountIn,
672695
expirySeconds: seconds,
696+
submitMode,
673697
maxBlock: status?.blockNumberPredicate ? maxBlock : undefined,
674698
submittedAt: Date.now(),
675699
txHash: hash,
676-
nonce: signedResult.nonce,
677-
maxFeePerGas: signedResult.fees?.maxFeePerGas,
678-
maxPriorityFeePerGas: signedResult.fees?.maxPriorityFeePerGas,
700+
nonce,
701+
maxFeePerGas: fees?.maxFeePerGas,
702+
maxPriorityFeePerGas: fees?.maxPriorityFeePerGas,
679703
status: 'pending',
680704
rectangle: draft.rectangle,
681705
validity,
@@ -747,6 +771,7 @@ export function ValidityDemo() {
747771
<div className="flex flex-col gap-2">
748772
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 font-mono text-[12px] text-bds-gray-60">
749773
<span>{status?.readHost ?? 'no rpc'}</span>
774+
<span>200ms blocks</span>
750775
<span>validity {status?.validitySupported ? 'on' : 'unavailable'}</span>
751776
{status?.blockNumberPredicate ? <span>block bounds on</span> : <span>client-side expiry only</span>}
752777
<span>simulation {botsOn ? (makersDry ? 'out of ETH' : 'live') : 'paused'}</span>
@@ -782,9 +807,10 @@ export function ValidityDemo() {
782807
<Card className="flex flex-col gap-4 bg-background p-6 dark:bg-white/5">
783808
<Text variant="title3">Simulated pool</Text>
784809
<Text variant="label.regular" tone="muted">
785-
A local EOA (not the Vibenet 8130 account) signs the swaps. The faucet
786-
funds it, then you deploy a VIBE/USDV pool. Simulated flow moves the mid
787-
so you can see a price condition fire — or expire unused.
810+
A local key signs the swaps — type-2 replacements, or 8130 nonceless
811+
txs so several conditions can rest at once. The faucet funds it, then
812+
you deploy a VIBE/USDV pool. Simulated flow moves the mid so you can
813+
see a price condition fire — or expire unused.
788814
</Text>
789815
{address ? (
790816
<div className="flex items-center justify-between gap-3">
@@ -836,11 +862,18 @@ export function ValidityDemo() {
836862
side={side}
837863
offsetBps={offsetBps}
838864
expirySeconds={expirySeconds}
865+
submitMode={submitMode}
839866
busy={busy}
840867
validitySupported={Boolean(status?.validitySupported)}
841868
onSide={setSide}
842869
onOffset={setOffsetBps}
843870
onExpiry={setExpirySeconds}
871+
onSubmitMode={(mode) => {
872+
setSubmitMode(mode);
873+
if (mode === 'concurrent' && expirySeconds > MAX_NONCELESS_SECONDS) {
874+
setExpirySeconds(15);
875+
}
876+
}}
844877
onSubmit={() => void placeOrder()}
845878
/>
846879
) : (

app/vibenet/demos/validity/components/OrderList.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ export function OrderList({ orders, highlightedOrderId, onHighlight }: Props) {
7474
<div className="flex flex-col gap-2">
7575
<Text variant="title3">Submitted</Text>
7676
<Text variant="footnote" tone="muted">
77-
Conditional swaps land here. They also draw as a dashed line on the tape.
77+
Conditional swaps land here. Concurrent 8130 orders stack; replace
78+
mode bumps the last nonce.
7879
</Text>
7980
</div>
8081
);
@@ -125,6 +126,7 @@ export function OrderList({ orders, highlightedOrderId, onHighlight }: Props) {
125126
</div>
126127
<Text variant="footnote" tone="muted" className="tabular-nums">
127128
{formatClock(order.submittedAt)}
129+
{order.submitMode === 'concurrent' ? ' · 8130' : order.submitMode === 'replace' ? ' · replace' : null}
128130
{order.filledAt ? ` → ${formatClock(order.filledAt)}` : null}
129131
{filled && order.fillPriceWad !== undefined
130132
? ` · ${formatPrice(order.fillPriceWad)}`

app/vibenet/demos/validity/components/OrderTicket.tsx

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import { Button } from '../../../../components/ui/Button';
44
import { Text } from '../../../../components/ui/Text';
5+
import { MAX_NONCELESS_SECONDS } from '../lib/constants';
56
import { applyOffsetBps, formatPrice } from '../lib/predicates';
6-
import type { Side } from '../lib/types';
7+
import type { Side, SubmitMode } from '../lib/types';
78

89
const EXPIRIES = [5, 15, 60] as const;
910
const OFFSETS = [0, 50, 100, 200, 500] as const;
@@ -13,11 +14,13 @@ type Props = {
1314
side: Side;
1415
offsetBps: number;
1516
expirySeconds: number;
17+
submitMode: SubmitMode;
1618
busy: boolean;
1719
validitySupported: boolean;
1820
onSide: (side: Side) => void;
1921
onOffset: (bps: number) => void;
2022
onExpiry: (seconds: number) => void;
23+
onSubmitMode: (mode: SubmitMode) => void;
2124
onSubmit: () => void;
2225
};
2326

@@ -31,11 +34,13 @@ export function OrderTicket({
3134
side,
3235
offsetBps,
3336
expirySeconds,
37+
submitMode,
3438
busy,
3539
validitySupported,
3640
onSide,
3741
onOffset,
3842
onExpiry,
43+
onSubmitMode,
3944
onSubmit,
4045
}: Props) {
4146
const target = applyOffsetBps(spotWad, side, offsetBps);
@@ -114,25 +119,66 @@ export function OrderTicket({
114119
mid {signed}
115120
</Text>
116121
</div>
122+
<div className="flex flex-col gap-2">
123+
<Text variant="caption" tone="muted">
124+
Mempool
125+
</Text>
126+
<div className="grid grid-cols-2 gap-2">
127+
<button
128+
type="button"
129+
onClick={() => onSubmitMode('replace')}
130+
className={
131+
submitMode === 'replace'
132+
? 'rounded-xl bg-foreground px-3 py-2 text-[13px] font-medium text-background'
133+
: 'rounded-xl border border-bds-gray-10 px-3 py-2 text-[13px] dark:border-white/10'
134+
}
135+
>
136+
Replace
137+
</button>
138+
<button
139+
type="button"
140+
onClick={() => onSubmitMode('concurrent')}
141+
className={
142+
submitMode === 'concurrent'
143+
? 'rounded-xl bg-foreground px-3 py-2 text-[13px] font-medium text-background'
144+
: 'rounded-xl border border-bds-gray-10 px-3 py-2 text-[13px] dark:border-white/10'
145+
}
146+
>
147+
Concurrent
148+
</button>
149+
</div>
150+
<Text variant="footnote" tone="muted">
151+
{submitMode === 'replace'
152+
? 'Same nonce, fee bump. The new swap takes the resting slot.'
153+
: `8130 nonceless — stack several at once. Envelope max ${MAX_NONCELESS_SECONDS}s.`}
154+
</Text>
155+
</div>
117156
<div className="flex flex-col gap-2">
118157
<Text variant="caption" tone="muted">
119158
Expiry
120159
</Text>
121160
<div className="flex gap-2">
122-
{EXPIRIES.map((seconds) => (
123-
<button
124-
key={seconds}
125-
type="button"
126-
onClick={() => onExpiry(seconds)}
127-
className={
128-
seconds === expirySeconds
129-
? 'rounded-full bg-foreground px-3 py-1 text-[12px] text-background'
130-
: 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] dark:border-white/10'
131-
}
132-
>
133-
{seconds}s
134-
</button>
135-
))}
161+
{EXPIRIES.map((seconds) => {
162+
const blocked = submitMode === 'concurrent' && seconds > MAX_NONCELESS_SECONDS;
163+
return (
164+
<button
165+
key={seconds}
166+
type="button"
167+
disabled={blocked}
168+
title={blocked ? `8130 nonceless max is ${MAX_NONCELESS_SECONDS}s` : undefined}
169+
onClick={() => onExpiry(seconds)}
170+
className={
171+
blocked
172+
? 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] text-bds-gray-40 dark:border-white/10'
173+
: seconds === expirySeconds
174+
? 'rounded-full bg-foreground px-3 py-1 text-[12px] text-background'
175+
: 'rounded-full border border-bds-gray-10 px-3 py-1 text-[12px] dark:border-white/10'
176+
}
177+
>
178+
{seconds}s
179+
</button>
180+
);
181+
})}
136182
</div>
137183
</div>
138184
{!validitySupported ? (

0 commit comments

Comments
 (0)