diff --git a/AGENTS.md b/AGENTS.md
index f60d6264..19daf88a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -125,7 +125,7 @@ This app uses Vercel Web Analytics. Two things must stay in place:
| `trackExplorerChainSelect(chain)` | `app/internal-explorer/components/ChainToggle.tsx` — chain toggle |
| `trackExplorerActiveBlockJump(chain, jump)` | `app/internal-explorer/components/ActiveBlockButton.tsx` — zeronet latest/previous active block |
| `trackValidityOrder(side, status)` | `app/vibenet/demos/validity/ValidityDemo.tsx` — conditional swap submit / include / expiry / replace |
- | `trackValidityRace(attempt, status)` | `app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx` — validity/manual comparison and condition agent lifecycle |
+ | `trackValidityRace(attempt, status)` | `app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx` — validity/manual comparison attempts |
Add a helper (and a row here) for a new key journey; remove the helper if you
remove its surface. Confirm the wiring with `grep -rn "analytics/events" app`.
diff --git a/app/analytics/events.ts b/app/analytics/events.ts
index 2a4fdebe..b65d81da 100644
--- a/app/analytics/events.ts
+++ b/app/analytics/events.ts
@@ -86,8 +86,8 @@ export function trackValidityOrder(
}
export function trackValidityRace(
- attempt: 'validity' | 'manual' | 'agent',
- status: 'started' | 'submitted' | 'success' | 'reverted' | 'expired' | 'stopped' | 'error',
+ attempt: 'validity' | 'manual',
+ status: 'submitted' | 'success' | 'reverted' | 'expired' | 'error',
): void {
track('validity_race', { attempt, status });
}
diff --git a/app/vibenet/demos/catalogue.ts b/app/vibenet/demos/catalogue.ts
index 16fa3703..4b0746e6 100644
--- a/app/vibenet/demos/catalogue.ts
+++ b/app/vibenet/demos/catalogue.ts
@@ -76,7 +76,7 @@ export const DEMOS: DemoEntry[] = [
'Submit a withdrawal before it is valid, then race a randomized onchain condition with an ordinary transaction sent by hand.',
points: [
'Compare the same permissionless withdrawal call two ways',
- 'Watch a dedicated agent subaccount flip shared chain state',
+ 'Watch a shared background Vibenet agent flip chain state',
'Judge the result by inclusion blocks, not browser timing',
],
available: true,
diff --git a/app/vibenet/demos/validity/lib/conditionalWithdrawal.ensure.test.ts b/app/vibenet/demos/validity/lib/conditionalWithdrawal.ensure.test.ts
deleted file mode 100644
index 2cfa750e..00000000
--- a/app/vibenet/demos/validity/lib/conditionalWithdrawal.ensure.test.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-const mocks = vi.hoisted(() => ({
- hasCode: vi.fn(),
-}));
-
-vi.mock('./singleton', () => ({
- CREATE2_DEPLOYER: '0x3333333333333333333333333333333333333333',
- create2Address: () => '0x2222222222222222222222222222222222222222',
- hasCode: mocks.hasCode,
- singletonSalt: () => `0x${'11'.repeat(32)}`,
-}));
-
-import { ensureConditionalWithdrawal } from './conditionalWithdrawal';
-
-const VIBE = '0x1111111111111111111111111111111111111111';
-const WITHDRAWAL = '0x2222222222222222222222222222222222222222';
-
-describe('ensureConditionalWithdrawal', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it('accepts a correctly configured deployment created concurrently by another visitor', async () => {
- mocks.hasCode.mockResolvedValueOnce(false).mockResolvedValueOnce(true).mockResolvedValueOnce(true);
- const publicClient = {
- readContract: vi.fn().mockResolvedValue(VIBE),
- };
- const wallet = {
- chain: {},
- sendTransaction: vi.fn().mockRejectedValue(new Error('CREATE2 duplicate')),
- };
-
- await expect(ensureConditionalWithdrawal({
- wallet: wallet as never,
- publicClient: publicClient as never,
- account: {} as never,
- vibe: VIBE,
- })).resolves.toBe(WITHDRAWAL);
- expect(publicClient.readContract).toHaveBeenCalledWith(expect.objectContaining({
- address: WITHDRAWAL,
- functionName: 'VIBE',
- }));
- expect(wallet.sendTransaction).toHaveBeenCalled();
- });
-});
diff --git a/app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts b/app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts
index 01077df4..0680f58c 100644
--- a/app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts
+++ b/app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts
@@ -1,28 +1,23 @@
import { decodeFunctionData } from 'viem';
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
import artifact from './artifacts/ConditionalWithdrawal.json';
import {
CONDITIONAL_WITHDRAWAL_AMOUNT,
CONDITIONAL_WITHDRAWAL_ENABLED_MASK,
CONDITIONAL_WITHDRAWAL_ENABLED_SLOT,
- CONDITIONAL_WITHDRAWAL_FUNDING_TARGET,
- CONDITIONAL_WITHDRAWAL_REFILL_THRESHOLD,
CONDITIONAL_WITHDRAWAL_SALT,
conditionalWithdrawalAbi,
conditionalWithdrawalEnabledPredicate,
- conditionalWithdrawalFundingAmount,
- encodeConditionalWithdrawalFunding,
encodeConditionalWithdraw,
- encodeSetConditionalWithdrawalEnabled,
predictConditionalWithdrawal,
+ probeConditionalWithdrawal,
} from './conditionalWithdrawal';
-import { minterAbi, WAD } from './constants';
+import { WAD } from './constants';
import { toWord } from './predicates';
const VIBE = '0x1111111111111111111111111111111111111111';
const OTHER_VIBE = '0x2222222222222222222222222222222222222222';
-const MINTER = '0x3333333333333333333333333333333333333333';
const WITHDRAWAL = '0x4444444444444444444444444444444444444444';
describe('conditional withdrawal contract', () => {
@@ -42,6 +37,24 @@ describe('conditional withdrawal contract', () => {
expect(predictConditionalWithdrawal(OTHER_VIBE)).not.toBe(predictConditionalWithdrawal(VIBE));
});
+ it('discovers only an existing singleton configured for the shared VIBE token', async () => {
+ const client = {
+ getCode: vi.fn().mockResolvedValue('0x1234'),
+ readContract: vi.fn().mockResolvedValue(VIBE),
+ };
+
+ await expect(probeConditionalWithdrawal(client as never, VIBE)).resolves.toBe(
+ predictConditionalWithdrawal(VIBE),
+ );
+ expect(client.readContract).toHaveBeenCalledWith(expect.objectContaining({
+ address: predictConditionalWithdrawal(VIBE),
+ functionName: 'VIBE',
+ }));
+
+ client.readContract.mockResolvedValueOnce(OTHER_VIBE);
+ await expect(probeConditionalWithdrawal(client as never, VIBE)).resolves.toBeNull();
+ });
+
it('reads bool public enabled from slot 0 in the EIP-8130 predicate', () => {
expect(CONDITIONAL_WITHDRAWAL_ENABLED_SLOT).toBe(0n);
expect(CONDITIONAL_WITHDRAWAL_ENABLED_MASK).toBe(0xffn);
@@ -57,7 +70,7 @@ describe('conditional withdrawal contract', () => {
});
});
- it('encodes condition and fixed-withdrawal calls exactly', () => {
+ it('encodes the fixed withdrawal call exactly', () => {
const functionNames = artifact.abi
.filter((item) => item.type === 'function')
.map((item) => item.name);
@@ -66,13 +79,6 @@ describe('conditional withdrawal contract', () => {
expect(functionNames).toContain('enabled');
expect(functionNames).toContain('setEnabled');
expect(functionNames).toContain('withdraw');
- expect(encodeSetConditionalWithdrawalEnabled(WITHDRAWAL, true)).toEqual({
- to: WITHDRAWAL,
- data: `0x328d8f72${'0'.repeat(63)}1`,
- });
- expect(encodeSetConditionalWithdrawalEnabled(WITHDRAWAL, false).data).toBe(
- `0x328d8f72${'0'.repeat(64)}`,
- );
expect(encodeConditionalWithdraw(WITHDRAWAL)).toEqual({
to: WITHDRAWAL,
data: '0x3ccfd60b',
@@ -84,37 +90,3 @@ describe('conditional withdrawal contract', () => {
).toBe('withdraw');
});
});
-
-describe('conditional withdrawal funding', () => {
- it('refills to two million VIBE only below the one million threshold', () => {
- expect(CONDITIONAL_WITHDRAWAL_REFILL_THRESHOLD).toBe(1_000_000n * WAD);
- expect(CONDITIONAL_WITHDRAWAL_FUNDING_TARGET).toBe(2_000_000n * WAD);
- expect(conditionalWithdrawalFundingAmount(0n)).toBe(2_000_000n * WAD);
- expect(conditionalWithdrawalFundingAmount(1_000_000n * WAD - 1n)).toBe(1_000_000n * WAD + 1n);
- expect(conditionalWithdrawalFundingAmount(1_000_000n * WAD)).toBe(0n);
- expect(conditionalWithdrawalFundingAmount(2_000_000n * WAD)).toBe(0n);
- expect(() => conditionalWithdrawalFundingAmount(-1n)).toThrow(/cannot be negative/);
- });
-
- it('targets the existing open minter and mints directly to the singleton', () => {
- const call = encodeConditionalWithdrawalFunding({
- minter: MINTER,
- vibe: VIBE,
- withdrawal: WITHDRAWAL,
- balance: 0n,
- });
- expect(call?.to).toBe(MINTER);
- expect(call).not.toBeNull();
- const decoded = decodeFunctionData({ abi: minterAbi, data: call!.data });
- expect(decoded.functionName).toBe('mint');
- expect(decoded.args).toEqual([VIBE, WITHDRAWAL, CONDITIONAL_WITHDRAWAL_FUNDING_TARGET]);
- expect(
- encodeConditionalWithdrawalFunding({
- minter: MINTER,
- vibe: VIBE,
- withdrawal: WITHDRAWAL,
- balance: CONDITIONAL_WITHDRAWAL_REFILL_THRESHOLD,
- }),
- ).toBeNull();
- });
-});
diff --git a/app/vibenet/demos/validity/lib/conditionalWithdrawal.ts b/app/vibenet/demos/validity/lib/conditionalWithdrawal.ts
index 7959aeb5..4b950254 100644
--- a/app/vibenet/demos/validity/lib/conditionalWithdrawal.ts
+++ b/app/vibenet/demos/validity/lib/conditionalWithdrawal.ts
@@ -1,20 +1,16 @@
import {
- concat,
encodeDeployData,
encodeFunctionData,
type Abi,
- type Account,
type Address,
type Hex,
type PublicClient,
- type WalletClient,
} from 'viem';
import artifact from './artifacts/ConditionalWithdrawal.json';
-import { erc20Abi, minterAbi, WAD } from './constants';
+import { erc20Abi, WAD } from './constants';
import { storagePredicate } from './predicates';
import {
- CREATE2_DEPLOYER,
create2Address,
hasCode,
singletonSalt,
@@ -28,8 +24,6 @@ export const conditionalWithdrawalBytecode = artifact.bytecode as Hex;
export const CONDITIONAL_WITHDRAWAL_ENABLED_SLOT = 0n;
export const CONDITIONAL_WITHDRAWAL_ENABLED_MASK = 0xffn;
export const CONDITIONAL_WITHDRAWAL_AMOUNT = WAD;
-export const CONDITIONAL_WITHDRAWAL_REFILL_THRESHOLD = 1_000_000n * WAD;
-export const CONDITIONAL_WITHDRAWAL_FUNDING_TARGET = 2_000_000n * WAD;
export const CONDITIONAL_WITHDRAWAL_SALT = singletonSalt('conditional-withdrawal');
export function conditionalWithdrawalInitCode(vibe: Address): Hex {
@@ -58,84 +52,6 @@ export async function probeConditionalWithdrawal(
: null;
}
-export async function ensureConditionalWithdrawal(args: {
- wallet: WalletClient;
- publicClient: PublicClient;
- account: Account;
- vibe: Address;
- onProgress?: (label: string) => void;
-}): Promise
{
- const { wallet, publicClient, account, vibe, onProgress } = args;
- const live = await probeConditionalWithdrawal(publicClient, vibe);
- if (live) return live;
-
- if (!(await hasCode(publicClient, CREATE2_DEPLOYER))) {
- throw new Error('Vibenet CREATE2 deployer is not available.');
- }
- onProgress?.('Deploying conditional withdrawal');
- try {
- const hash = await wallet.sendTransaction({
- account,
- chain: wallet.chain,
- to: CREATE2_DEPLOYER,
- data: concat([CONDITIONAL_WITHDRAWAL_SALT, conditionalWithdrawalInitCode(vibe)]),
- gas: 750_000n,
- });
- const receipt = await publicClient.waitForTransactionReceipt({ hash, pollingInterval: 500 });
- if (receipt.status === 'reverted') throw new Error('Conditional withdrawal deployment reverted.');
- } catch (error) {
- // Another visitor may win the same CREATE2 deployment between our probe and send.
- const deadline = Date.now() + 6_000;
- while (Date.now() < deadline) {
- const concurrent = await probeConditionalWithdrawal(publicClient, vibe);
- if (concurrent) return concurrent;
- await new Promise((resolve) => setTimeout(resolve, 300));
- }
- throw error;
- }
- const configured = await probeConditionalWithdrawal(publicClient, vibe);
- if (!configured) throw new Error('Conditional withdrawal deployed with an unexpected VIBE configuration.');
- return configured;
-}
-
-export function conditionalWithdrawalFundingAmount(balance: bigint): bigint {
- if (balance < 0n) throw new Error('Conditional withdrawal balance cannot be negative.');
- return balance < CONDITIONAL_WITHDRAWAL_REFILL_THRESHOLD
- ? CONDITIONAL_WITHDRAWAL_FUNDING_TARGET - balance
- : 0n;
-}
-
-export function encodeConditionalWithdrawalFunding(args: {
- minter: Address;
- vibe: Address;
- withdrawal: Address;
- balance: bigint;
-}): { to: Address; data: Hex } | null {
- const amount = conditionalWithdrawalFundingAmount(args.balance);
- if (amount === 0n) return null;
- return {
- to: args.minter,
- data: encodeFunctionData({
- abi: minterAbi,
- functionName: 'mint',
- args: [args.vibe, args.withdrawal, amount],
- }),
- };
-}
-
-export async function prepareConditionalWithdrawalFunding(
- client: PublicClient,
- args: { minter: Address; vibe: Address; withdrawal: Address },
-): Promise<{ to: Address; data: Hex } | null> {
- const balance = (await client.readContract({
- address: args.vibe,
- abi: erc20Abi,
- functionName: 'balanceOf',
- args: [args.withdrawal],
- })) as bigint;
- return encodeConditionalWithdrawalFunding({ ...args, balance });
-}
-
export async function readConditionalWithdrawalState(
client: PublicClient,
vibe: Address,
@@ -157,20 +73,6 @@ export async function readConditionalWithdrawalState(
return { address, enabled, balance };
}
-export function encodeSetConditionalWithdrawalEnabled(
- withdrawal: Address,
- enabled: boolean,
-): { to: Address; data: Hex } {
- return {
- to: withdrawal,
- data: encodeFunctionData({
- abi: conditionalWithdrawalAbi,
- functionName: 'setEnabled',
- args: [enabled],
- }),
- };
-}
-
export function encodeConditionalWithdraw(withdrawal: Address): { to: Address; data: Hex } {
return {
to: withdrawal,
diff --git a/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx b/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx
index 0dd1e94e..1adcc599 100644
--- a/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx
+++ b/app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx
@@ -1,15 +1,13 @@
'use client';
import { getTransactionReceipt as getAaTransactionReceipt } from '@aa';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import {
formatUnits,
- parseEther,
type Address,
type Hex,
type PublicClient,
} from 'viem';
-import { privateKeyToAccount } from 'viem/accounts';
import { trackValidityRace } from '../../../../analytics/events';
import { Button } from '../../../../components/ui/Button';
@@ -22,15 +20,11 @@ import { AccountDemoShell } from '../../_components/AccountDemoShell';
import { DemoHeader } from '../../_components/DemoHeader';
import { ChevronIcon } from '../../_shared/dropdown';
import { newCallRow } from '../../account/library/calls';
-import type { StoredAccount } from '../../account/library/model';
import { aaReceiptSucceeded, type AaReceiptLike } from '../../account/library/receipt';
import { AccountEngineProvider, TxPendingError, useAccountEngine } from '../../account/useAccountEngine';
import {
conditionalWithdrawalEnabledPredicate,
encodeConditionalWithdraw,
- encodeSetConditionalWithdrawalEnabled,
- ensureConditionalWithdrawal,
- prepareConditionalWithdrawalFunding,
probeConditionalWithdrawal,
readConditionalWithdrawalState,
} from '../lib/conditionalWithdrawal';
@@ -38,9 +32,7 @@ import { noncelessFields } from '../../../library/aa';
import {
describeValidityError,
makePublicClient,
- makeWalletClient,
sendValidityTransaction,
- VIBENET_CHAIN,
type RpcSend,
} from '../lib/rpc';
import { probeSingleton } from '../lib/singleton';
@@ -51,28 +43,16 @@ import {
canSubmitValidity,
isAttemptTerminal,
preserveCompletedAttempt,
- randomAgentDwellMs,
RACE_VALIDITY_SECONDS,
- scheduledAgentOpenBlock,
- scheduledAgentPredicates,
- shouldRestartConditionAgent,
- shouldRunConditionAgent,
shortHash,
type Attempt,
} from './comparison';
-const AGENT_LABEL = 'Validity condition agent';
-const OWNER_DEPLOY_GAS = parseEther('0.08');
-const OWNER_DEPLOY_SEND = '0.1';
-const AGENT_GAS_FLOOR = parseEther('0.01');
-const AGENT_GAS_SEND = '0.02';
-const ACTIVE_ACCOUNT_FUNDING_FLOOR = parseEther('0.2');
const RECEIPT_POLL_MS = 1_000;
const STATE_FALLBACK_POLL_MS = 1_000;
-const AGENT_RETRY_MS = 750;
+const SHARED_INFRA_RETRY_MS = 1_000;
type Observation = { enabled: boolean; block: bigint; at: number };
-type AgentPhase = 'Waiting' | 'Scheduling' | 'Opening' | 'Closing' | 'Retrying';
const EMPTY_ATTEMPT: Attempt = { status: 'idle' };
const CONTRACT_SNIPPET = `interface IERC20 {
@@ -98,18 +78,6 @@ contract ConditionalWithdrawal {
}
}`;
-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;
-}
-
export function RaceTheAgentDemo() {
return (
@@ -121,28 +89,16 @@ export function RaceTheAgentDemo() {
function RaceTheAgentDemoInner() {
const engine = useAccountEngine();
const acct = engine.acct;
- const parent = useMemo(
- () => (acct ? rootAccount(acct, engine.accounts) : null),
- [acct, engine.accounts],
- );
- const [genesisHash, setGenesisHash] = useState(null);
const [client, setClient] = useState(null);
const [withdrawal, setWithdrawal] = useState(null);
const [vibe, setVibe] = useState(null);
const [contractBalance, setContractBalance] = useState(null);
const [observed, setObserved] = useState(null);
const [observations, setObservations] = useState([]);
- const [agent, setAgent] = useState(null);
- const [agentRunning, setAgentRunning] = useState(false);
- const [agentPhase, setAgentPhase] = useState('Waiting');
- const [agentRestartToken, setAgentRestartToken] = useState(0);
- const [agentError, setAgentError] = useState(null);
const [prepared, setPrepared] = useState(false);
const [setupRunning, setSetupRunning] = useState(false);
const [setupError, setSetupError] = useState(null);
- const [setupRetry, setSetupRetry] = useState(0);
const [busy, setBusy] = useState(false);
- const [progress, setProgress] = useState(null);
const [error, setError] = useState(null);
const [validity, setValidity] = useState(EMPTY_ATTEMPT);
const [validityHistory, setValidityHistory] = useState([]);
@@ -152,38 +108,20 @@ function RaceTheAgentDemoInner() {
const [manualAttemptCount, setManualAttemptCount] = useState(0);
const [validBefore, setValidBefore] = useState(null);
- const generationRef = useRef(0);
const observedRef = useRef(null);
const accountKeyRef = useRef(null);
- const setupInFlightKeyRef = useRef(null);
- const setupReadyKeyRef = useRef(null);
- const setupFailedKeyRef = useRef(null);
- const setupGenerationRef = useRef(0);
const observationsScrollRef = useRef(null);
const rpcSendRef = useRef(null);
- const engineRef = useRef(engine);
- engineRef.current = engine;
observedRef.current = observed;
useEffect(() => {
- const accountKey = acct && parent ? `${acct.id}:${parent.id}` : null;
+ const accountKey = acct?.id ?? null;
if (accountKeyRef.current === null) {
accountKeyRef.current = accountKey;
return;
}
if (accountKeyRef.current === accountKey) return;
accountKeyRef.current = accountKey;
- generationRef.current += 1;
- setAgentRunning(false);
- setAgentPhase('Waiting');
- setupGenerationRef.current += 1;
- setupInFlightKeyRef.current = null;
- setupReadyKeyRef.current = null;
- setupFailedKeyRef.current = null;
- setAgent(null);
- setPrepared(false);
- setSetupRunning(false);
- setSetupError(null);
setValidity(EMPTY_ATTEMPT);
setValidityHistory([]);
setValidityAttemptCount(0);
@@ -192,9 +130,8 @@ function RaceTheAgentDemoInner() {
setManualAttemptCount(0);
setValidBefore(null);
setError(null);
- setAgentError(null);
setObservations(observedRef.current ? [observedRef.current] : []);
- }, [acct, parent]);
+ }, [acct]);
const applyObservation = useCallback((next: Observation) => {
observedRef.current = next;
@@ -214,33 +151,45 @@ function RaceTheAgentDemoInner() {
useEffect(() => {
let cancelled = false;
+ let retryId: number | undefined;
const nextClient = makePublicClient(() => rpcSendRef.current);
- void (async () => {
- const genesis = await nextClient.getBlock({ blockNumber: 0n });
- if (!genesis.hash) throw new Error('RPC did not return a genesis hash.');
- if (cancelled) return;
- setGenesisHash(genesis.hash);
- setClient(nextClient);
- const deployment = await probeSingleton(nextClient).catch(() => null);
- if (cancelled || !deployment) return;
- setVibe(deployment.tokenA);
- const live = await probeConditionalWithdrawal(nextClient, deployment.tokenA).catch(() => null);
- if (cancelled || !live) return;
- setWithdrawal(live);
- const [state, block] = await Promise.all([
- readConditionalWithdrawalState(nextClient, deployment.tokenA),
- nextClient.getBlockNumber({ cacheTime: 0 }),
- ]);
- if (cancelled) return;
- setContractBalance(state.balance);
- applyObservation({ enabled: state.enabled, block, at: Date.now() });
- })()
- .catch((err: unknown) => {
- if (!cancelled) setError(err instanceof Error ? err.message : 'Could not reach Vibenet.');
- });
+ setClient(nextClient);
+
+ const discover = async () => {
+ setSetupRunning(true);
+ try {
+ const deployment = await probeSingleton(nextClient);
+ const live = deployment
+ ? await probeConditionalWithdrawal(nextClient, deployment.tokenA)
+ : null;
+ if (cancelled) return;
+ if (!deployment || !live) {
+ setSetupError(null);
+ retryId = window.setTimeout(() => void discover(), SHARED_INFRA_RETRY_MS);
+ return;
+ }
+ const [state, block] = await Promise.all([
+ readConditionalWithdrawalState(nextClient, deployment.tokenA),
+ nextClient.getBlockNumber({ cacheTime: 0 }),
+ ]);
+ if (cancelled) return;
+ setVibe(deployment.tokenA);
+ setWithdrawal(live);
+ setContractBalance(state.balance);
+ applyObservation({ enabled: state.enabled, block, at: Date.now() });
+ setPrepared(true);
+ setSetupError(null);
+ setSetupRunning(false);
+ } catch (err) {
+ if (cancelled) return;
+ setSetupError(err instanceof Error ? err.message : 'Could not reach shared Vibenet infrastructure.');
+ retryId = window.setTimeout(() => void discover(), SHARED_INFRA_RETRY_MS);
+ }
+ };
+ void discover();
return () => {
cancelled = true;
- generationRef.current += 1;
+ if (retryId !== undefined) window.clearTimeout(retryId);
};
}, [applyObservation]);
@@ -382,178 +331,6 @@ function RaceTheAgentDemoInner() {
};
}, [client, manual.hash, manual.status, settleFromReceipt]);
- useEffect(() => {
- if (!engine.hydrated || !acct || !parent || !genesisHash || !client) return;
- const setupKey = `${VIBENET_CHAIN.id}:${genesisHash}:${acct.id}:${parent.id}`;
- if (setupReadyKeyRef.current === setupKey || setupInFlightKeyRef.current === setupKey) return;
- if (setupFailedKeyRef.current === setupKey) return;
-
- const generation = setupGenerationRef.current + 1;
- setupGenerationRef.current = generation;
- setupInFlightKeyRef.current = setupKey;
- setPrepared(false);
- setSetupRunning(true);
- setSetupError(null);
- setProgress('Checking shared contracts');
-
- const isCurrent = () => setupGenerationRef.current === generation;
- void (async () => {
- try {
- const currentEngine = engineRef.current;
- const k1 = currentEngine.ownerSigners.find((signer) => signer.kind === 'k1' && signer.privateKey);
- if (!k1?.privateKey) throw new Error('Setup needs a K1 owner key on this account. Add one in Accounts.');
-
- if (isCurrent()) setProgress('Checking shared contracts');
- const deployment = await probeSingleton(client);
- if (!deployment) throw new Error('The shared VIBE/USDV market is not ready yet.');
- let contract = await probeConditionalWithdrawal(client, deployment.tokenA);
-
- let activeBalance = await client.getBalance({ address: acct.address });
- if (activeBalance < ACTIVE_ACCOUNT_FUNDING_FLOOR) {
- if (isCurrent()) setProgress('Waiting for account funding');
- activeBalance = await waitForBalance(client, acct.address, ACTIVE_ACCOUNT_FUNDING_FLOOR, 4_000);
- }
- if (activeBalance < ACTIVE_ACCOUNT_FUNDING_FLOOR) {
- if (isCurrent()) setProgress('Funding the active account');
- await currentEngine.requestFaucet();
- const fundedBalance = await waitForBalance(client, acct.address, ACTIVE_ACCOUNT_FUNDING_FLOOR, 8_000);
- if (fundedBalance < ACTIVE_ACCOUNT_FUNDING_FLOOR) {
- throw new Error('The active account needs at least 0.2 ETH of setup and gas headroom. Top it up and retry.');
- }
- }
-
- const owner = privateKeyToAccount(k1.privateKey);
- const ownerBalance = await client.getBalance({ address: owner.address });
- if (ownerBalance < OWNER_DEPLOY_GAS) {
- if (isCurrent()) setProgress('Funding the deploy key');
- await currentEngine.sendActiveCalls({
- calls: [{ to: owner.address, data: '0x', value: OWNER_DEPLOY_SEND }],
- metadata: 'Race the Agent bootstrap',
- });
- }
-
- const wallet = makeWalletClient(owner);
- const reportProgress = (label: string) => {
- if (isCurrent()) setProgress(label);
- };
- if (!isCurrent()) return;
- setVibe(deployment.tokenA);
-
- if (!contract) {
- reportProgress('Preparing conditional withdrawal');
- contract = await ensureConditionalWithdrawal({
- wallet,
- publicClient: client,
- account: owner,
- vibe: deployment.tokenA,
- onProgress: reportProgress,
- });
- }
- if (!isCurrent()) return;
- setWithdrawal(contract);
-
- const funding = await prepareConditionalWithdrawalFunding(client, {
- minter: deployment.minter,
- vibe: deployment.tokenA,
- withdrawal: contract,
- });
- if (funding) {
- setProgress('Funding conditional withdrawal');
- const hash = await wallet.sendTransaction({
- account: owner,
- chain: wallet.chain,
- to: funding.to,
- data: funding.data,
- });
- const receipt = await client.waitForTransactionReceipt({ hash, pollingInterval: RECEIPT_POLL_MS });
- if (receipt.status === 'reverted') throw new Error('Singleton funding reverted.');
- }
- if (!isCurrent()) return;
-
- let latestEngine = engineRef.current;
- let conditionAgent = latestEngine.accounts.find(
- (item) => item.parentId === parent.id && item.label === AGENT_LABEL,
- );
- if (!conditionAgent) {
- conditionAgent = latestEngine.doCreateSubAccount(AGENT_LABEL, {
- withSpareKey: true,
- parent,
- })?.account;
- }
- if (!conditionAgent) throw new Error('Could not create the condition agent subaccount.');
- setAgent(conditionAgent);
-
- const agentSignerIds = new Set(
- conditionAgent.owners.flatMap((ownerActor) => ownerActor.signerId ? [ownerActor.signerId] : []),
- );
- const signerDeadline = Date.now() + 2_000;
- while (
- isCurrent() &&
- !engineRef.current.signers.some((signer) => agentSignerIds.has(signer.id)) &&
- Date.now() < signerDeadline
- ) {
- await delay(50);
- }
- latestEngine = engineRef.current;
- if (!latestEngine.signers.some((signer) => agentSignerIds.has(signer.id))) {
- throw new Error('Could not load the condition agent signing key.');
- }
-
- let agentBalance = await client.getBalance({ address: conditionAgent.address });
- if (agentBalance < AGENT_GAS_FLOOR) {
- setProgress('Waiting for condition agent funding');
- agentBalance = await waitForBalance(client, conditionAgent.address, AGENT_GAS_FLOOR, 4_000);
- }
- if (agentBalance < AGENT_GAS_FLOOR) {
- setProgress('Funding the condition agent');
- latestEngine.autoFundNewAccount(conditionAgent.address);
- agentBalance = await waitForBalance(client, conditionAgent.address, AGENT_GAS_FLOOR, 8_000);
- }
- if (agentBalance < AGENT_GAS_FLOOR) {
- setProgress('Funding the condition agent from the active account');
- await latestEngine.sendActiveCalls({
- calls: [{ to: conditionAgent.address, data: '0x', value: AGENT_GAS_SEND }],
- metadata: 'Race the Agent funding fallback',
- });
- agentBalance = await waitForBalance(client, conditionAgent.address, AGENT_GAS_FLOOR, 8_000);
- }
- if (agentBalance < AGENT_GAS_FLOOR) {
- throw new Error('The condition agent needs ETH for deployment and gas.');
- }
-
- setProgress('Deploying condition agent and disabling condition');
- const disable = encodeSetConditionalWithdrawalEnabled(contract, false);
- await latestEngine.sendAccountCalls({
- account: conditionAgent,
- calls: [{ to: disable.to, data: disable.data, value: '0' }],
- metadata: 'Race the Agent setup',
- });
-
- await refreshPreparedState(client, deployment.tokenA, contract, applyObservation, setContractBalance);
- if (!isCurrent()) return;
- setupReadyKeyRef.current = setupKey;
- setupFailedKeyRef.current = null;
- setPrepared(true);
- } catch (err) {
- if (!isCurrent()) return;
- setupFailedKeyRef.current = setupKey;
- setSetupError(err instanceof Error ? err.message : 'Setup failed.');
- } finally {
- if (setupInFlightKeyRef.current === setupKey) setupInFlightKeyRef.current = null;
- if (isCurrent()) {
- setSetupRunning(false);
- setProgress(null);
- }
- }
- })();
- }, [acct, applyObservation, client, engine.hydrated, genesisHash, parent, setupRetry]);
-
- const retrySetup = () => {
- setupFailedKeyRef.current = null;
- setSetupError(null);
- setSetupRetry((attempt) => attempt + 1);
- };
-
const submitValidity = async () => {
if (
!acct ||
@@ -633,94 +410,6 @@ function RaceTheAgentDemoInner() {
}
};
- useEffect(() => {
- if (!shouldRunConditionAgent({
- prepared,
- hasAgent: Boolean(agent),
- hasClient: Boolean(client),
- hasContract: Boolean(withdrawal),
- }) || !agent || !client || !withdrawal || !vibe) return;
-
- const generation = generationRef.current + 1;
- generationRef.current = generation;
- setAgentRunning(true);
- setAgentPhase('Waiting');
- setAgentError(null);
- trackValidityRace('agent', 'started');
- const active = () => generationRef.current === generation;
-
- const ensureAgentFunding = async () => {
- const agentBalance = await client.getBalance({ address: agent.address });
- if (agentBalance >= AGENT_GAS_FLOOR) return;
- setAgentPhase('Retrying');
- engineRef.current.autoFundNewAccount(agent.address);
- const funded = await waitForBalance(client, agent.address, AGENT_GAS_FLOOR, 8_000);
- if (funded < AGENT_GAS_FLOOR) throw new Error('Condition agent needs ETH for gas.');
- };
-
- const run = async () => {
- while (active()) {
- try {
- await ensureAgentFunding();
- const block = await client.getBlockNumber({ cacheTime: 0 });
- if (!active()) break;
- const openBlock = scheduledAgentOpenBlock(block, randomAgentDwellMs());
- const validity = scheduledAgentPredicates(withdrawal, openBlock);
- const fields = noncelessFields(RACE_VALIDITY_SECONDS);
- const open = encodeSetConditionalWithdrawalEnabled(withdrawal, true);
- const close = encodeSetConditionalWithdrawalEnabled(withdrawal, false);
- const seqOpt = { ...fields, assumeDeployed: true };
-
- setAgentPhase('Scheduling');
- // The same signer cannot service two composition requests concurrently.
- // Sign sequentially, then submit both scheduled transactions together.
- const signedOpen = await engineRef.current.signAccountCalls({
- account: agent,
- calls: [{ to: open.to, data: open.data, value: '0' }],
- seqOpt,
- metadata: `${AGENT_LABEL} open`,
- });
- const signedClose = await engineRef.current.signAccountCalls({
- account: agent,
- calls: [{ to: close.to, data: close.data, value: '0' }],
- seqOpt,
- metadata: `${AGENT_LABEL} close`,
- });
- const closeSubmission = sendValidityTransaction(signedClose.serialized, validity.close);
- const openSubmission = sendValidityTransaction(signedOpen.serialized, validity.open);
- await Promise.all([closeSubmission, openSubmission]);
- setAgentError(null);
- setAgentPhase('Opening');
- await waitForScheduledClose(
- openBlock,
- active,
- () => observedRef.current,
- () => setAgentPhase('Closing'),
- );
- if (active()) setAgentPhase('Waiting');
- } catch (err) {
- if (!active()) break;
- setAgentPhase('Retrying');
- setAgentError(err instanceof Error ? err.message : 'Condition update failed.');
- await delay(AGENT_RETRY_MS);
- }
- }
- };
- void run().finally(() => {
- if (!shouldRestartConditionAgent(prepared, active())) return;
- setAgentRunning(false);
- setAgentPhase('Retrying');
- setAgentRestartToken((token) => token + 1);
- });
-
- return () => {
- if (generationRef.current === generation) generationRef.current += 1;
- setAgentRunning(false);
- setAgentPhase('Waiting');
- trackValidityRace('agent', 'stopped');
- };
- }, [agent, agentRestartToken, client, prepared, vibe, withdrawal]);
-
const withdrawNow = async () => {
if (!withdrawal || !client || !observed || !canSubmitManual({
status: manual.status,
@@ -839,21 +528,20 @@ function RaceTheAgentDemoInner() {
- {setupError ? (
-
- {setupError}
- Retry setup
-
- ) : (
-
- {progress ?? (agentRunning ? agentPhase : setupRunning ? 'Starting after setup' : 'Waiting for setup')}
-
- )}
+
+ {prepared
+ ? 'Connected to the shared agent switch'
+ : setupError
+ ? `Shared infrastructure is not ready; retrying automatically. ${setupError}`
+ : setupRunning
+ ? 'Waiting for shared Vibenet infrastructure'
+ : 'Discovering shared Vibenet infrastructure'}
+
- {(error || agentError) ? (
+ {error ? (
- {error ?
{error}
: null}
- {agentError ?
Agent retrying: {agentError}
: null}
+
{error}
) : null}
@@ -935,7 +622,7 @@ function RaceTheAgentDemoInner() {
{observations.length === 0 ? (
-
State observations appear after automatic setup.
+
State observations appear when the shared agent singleton is ready.
) : observations.map((item, index) => (
void,
- setBalance: (balance: bigint) => void,
-): Promise {
- const [state, block] = await Promise.all([
- readConditionalWithdrawalState(client, vibe),
- client.getBlockNumber({ cacheTime: 0 }),
- ]);
- setBalance(state.balance);
- applyObservation({ enabled: state.enabled, block, at: Date.now() });
-}
-
-async function waitForBalance(
- client: PublicClient,
- address: Address,
- minimum: bigint,
- timeoutMs = 5_000,
-): Promise {
- const deadline = Date.now() + timeoutMs;
- let balance = 0n;
- while (Date.now() < deadline) {
- balance = await client.getBalance({ address });
- if (balance >= minimum) return balance;
- await new Promise((resolve) => setTimeout(resolve, 400));
- }
- return balance;
-}
-
-async function waitForScheduledClose(
- openBlock: bigint,
- active: () => boolean,
- observation: () => Observation | null,
- onClosing: () => void,
-): Promise {
- let closing = false;
- const deadline = Date.now() + (RACE_VALIDITY_SECONDS + 2) * 1_000;
- while (active()) {
- const current = observation();
- if (current && current.block >= openBlock && !closing) {
- closing = true;
- onClosing();
- }
- if (current && current.block >= openBlock + 1n && !current.enabled) return;
- if (Date.now() >= deadline) throw new Error('Scheduled close was not observed before expiry.');
- await delay(100);
- }
-}
-
-function delay(ms: number): Promise {
- return new Promise((resolve) => window.setTimeout(resolve, ms));
-}
-
function extractHash(message: string): Hex | undefined {
return message.match(/0x[0-9a-fA-F]{64}/)?.[0] as Hex | undefined;
}
diff --git a/app/vibenet/demos/validity/race-the-agent/comparison.test.ts b/app/vibenet/demos/validity/race-the-agent/comparison.test.ts
index e007c698..0e500135 100644
--- a/app/vibenet/demos/validity/race-the-agent/comparison.test.ts
+++ b/app/vibenet/demos/validity/race-the-agent/comparison.test.ts
@@ -1,26 +1,15 @@
import { describe, expect, it } from 'vitest';
import {
- AGENT_DISABLED_DWELL_MAX_MS,
- AGENT_DISABLED_DWELL_MIN_MS,
attemptHistoryRows,
canSubmitManual,
canSubmitValidity,
isAttemptTerminal,
preserveCompletedAttempt,
- randomAgentDwellMs,
RACE_VALIDITY_SECONDS,
- scheduledAgentOpenBlock,
- scheduledAgentPredicates,
- shouldRunConditionAgent,
- shouldRestartConditionAgent,
type Attempt,
} from './comparison';
import { noncelessFields } from '../../../library/aa';
-import { conditionalWithdrawalEnabledPredicate } from '../lib/conditionalWithdrawal';
-import { blockNumberPredicate } from '../lib/predicates';
-
-const WITHDRAWAL = '0x1111111111111111111111111111111111111111';
describe('isAttemptTerminal', () => {
it('only stops on final receipt or expiry states', () => {
@@ -40,38 +29,6 @@ describe('race lifecycle predicates', () => {
expect(fields.validBefore).toBe(BigInt(now + 15_000));
});
- it('runs the condition agent only when automatic setup resources are ready', () => {
- expect(shouldRunConditionAgent({ prepared: true, hasAgent: true, hasClient: true, hasContract: true })).toBe(true);
- expect(shouldRunConditionAgent({ prepared: false, hasAgent: true, hasClient: true, hasContract: true })).toBe(false);
- expect(shouldRunConditionAgent({ prepared: true, hasAgent: false, hasClient: true, hasContract: true })).toBe(false);
- expect(shouldRestartConditionAgent(true, true)).toBe(true);
- expect(shouldRestartConditionAgent(false, true)).toBe(false);
- expect(shouldRestartConditionAgent(true, false)).toBe(false);
- });
-
- it('converts bounded disabled dwell times to 200ms Vibenet schedule blocks', () => {
- expect(AGENT_DISABLED_DWELL_MIN_MS).toBe(2_000);
- expect(AGENT_DISABLED_DWELL_MAX_MS).toBe(10_000);
- expect(randomAgentDwellMs(0)).toBe(AGENT_DISABLED_DWELL_MIN_MS);
- expect(randomAgentDwellMs(0.999999)).toBe(AGENT_DISABLED_DWELL_MAX_MS);
- expect(scheduledAgentOpenBlock(100n, 2_000)).toBe(110n);
- expect(scheduledAgentOpenBlock(100n, 10_000)).toBe(150n);
- expect(scheduledAgentOpenBlock(100n, 2_001)).toBe(111n);
- });
-
- it('opens only at the exact scheduled block and closes afterward when enabled', () => {
- const predicates = scheduledAgentPredicates(WITHDRAWAL, 110n);
- expect(predicates.open).toEqual([
- blockNumberPredicate('>=', 110n),
- blockNumberPredicate('<=', 110n),
- ]);
- expect(predicates.close).toEqual([
- blockNumberPredicate('>=', 111n),
- blockNumberPredicate('<=', 111n),
- conditionalWithdrawalEnabledPredicate(WITHDRAWAL),
- ]);
- });
-
it('allows retries after terminal attempts and preserves every completed attempt', () => {
expect(canSubmitValidity('pending')).toBe(false);
expect(canSubmitValidity('submitting')).toBe(false);
diff --git a/app/vibenet/demos/validity/race-the-agent/comparison.ts b/app/vibenet/demos/validity/race-the-agent/comparison.ts
index d2cd2b46..0c3ac67c 100644
--- a/app/vibenet/demos/validity/race-the-agent/comparison.ts
+++ b/app/vibenet/demos/validity/race-the-agent/comparison.ts
@@ -1,13 +1,6 @@
-import type { Address, Hex } from 'viem';
-
-import { CANDLE_SAMPLE_MS } from '../lib/constants';
-import { conditionalWithdrawalEnabledPredicate } from '../lib/conditionalWithdrawal';
-import { blockNumberPredicate } from '../lib/predicates';
-import type { ValidityPredicate } from '../lib/types';
+import type { Hex } from 'viem';
export const RACE_VALIDITY_SECONDS = 15;
-export const AGENT_DISABLED_DWELL_MIN_MS = 2_000;
-export const AGENT_DISABLED_DWELL_MAX_MS = 10_000;
export type AttemptStatus = 'idle' | 'submitting' | 'pending' | 'success' | 'reverted' | 'expired' | 'error';
@@ -76,49 +69,6 @@ export function attemptHistoryRows(current: Attempt, history: Attempt[]): Attemp
});
}
-function randomInteger(random: number, min: number, max: number): number {
- const bounded = Math.min(Math.max(random, 0), 0.999999999);
- return min + Math.floor(bounded * (max - min + 1));
-}
-
-export function randomAgentDwellMs(random = Math.random()): number {
- return randomInteger(random, AGENT_DISABLED_DWELL_MIN_MS, AGENT_DISABLED_DWELL_MAX_MS);
-}
-
-export function scheduledAgentOpenBlock(currentBlock: bigint, dwellMs: number): bigint {
- return currentBlock + BigInt(Math.ceil(dwellMs / CANDLE_SAMPLE_MS));
-}
-
-export function scheduledAgentPredicates(
- withdrawal: Address,
- openBlock: bigint,
-): { open: ValidityPredicate[]; close: ValidityPredicate[] } {
- return {
- open: [
- blockNumberPredicate('>=', openBlock),
- blockNumberPredicate('<=', openBlock),
- ],
- close: [
- blockNumberPredicate('>=', openBlock + 1n),
- blockNumberPredicate('<=', openBlock + 1n),
- conditionalWithdrawalEnabledPredicate(withdrawal),
- ],
- };
-}
-
-export function shouldRunConditionAgent(args: {
- prepared: boolean;
- hasAgent: boolean;
- hasClient: boolean;
- hasContract: boolean;
-}): boolean {
- return args.prepared && args.hasAgent && args.hasClient && args.hasContract;
-}
-
-export function shouldRestartConditionAgent(setupValid: boolean, generationActive: boolean): boolean {
- return setupValid && generationActive;
-}
-
export function shortHash(hash?: Hex): string {
return hash ? `${hash.slice(0, 8)}…${hash.slice(-6)}` : 'Not submitted';
}