Skip to content
Open
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +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 |

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`.
Expand Down
7 changes: 7 additions & 0 deletions app/analytics/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,10 @@ export function trackValidityOrder(
): void {
track('validity_order', { side, status });
}

export function trackValidityRace(
attempt: 'validity' | 'manual' | 'agent',
status: 'started' | 'submitted' | 'success' | 'reverted' | 'expired' | 'stopped' | 'error',
): void {
track('validity_race', { attempt, status });
}
1 change: 1 addition & 0 deletions app/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,6 @@ describe('titleForPath', () => {
it('uses catalogue labels for grouped and nested demos', () => {
expect(titleForPath('/vibenet/demos/validity')).toBe('Validity Transactions');
expect(titleForPath('/vibenet/demos/validity/limit-orders')).toBe('Limit Orders');
expect(titleForPath('/vibenet/demos/validity/race-the-agent')).toBe('Race the Agent');
});
});
3 changes: 2 additions & 1 deletion app/sitemap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { describe, expect, it } from 'vitest';
import sitemap from './sitemap';

describe('sitemap', () => {
it('indexes the Validity Transactions group and its Limit Orders demo', () => {
it('indexes the Validity Transactions group and both nested demos', () => {
const urls = sitemap().map((entry) => entry.url);

expect(urls).toContain('https://chain.base.org/vibenet/demos/validity');
expect(urls).toContain('https://chain.base.org/vibenet/demos/validity/limit-orders');
expect(urls).toContain('https://chain.base.org/vibenet/demos/validity/race-the-agent');
});
});
1 change: 1 addition & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ path: '/vibenet/demos/b20', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/validity', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/validity/limit-orders', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/validity/race-the-agent', priority: 0.5, changeFrequency: 'weekly' },
];

return routes.map(({ path, priority, changeFrequency }) => ({
Expand Down
14 changes: 14 additions & 0 deletions app/vibenet/demos/account/library/receipt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';

import { aaReceiptSucceeded } from './receipt';

describe('aaReceiptSucceeded', () => {
it('requires both the outer transaction and every AA phase to succeed', () => {
expect(aaReceiptSucceeded({ status: 'success', eip8130: { phaseStatuses: ['0x1'] } })).toBe(true);
expect(aaReceiptSucceeded({ status: 'success' })).toBe(true);
expect(aaReceiptSucceeded({ status: '0x1', eip8130: { phaseStatuses: ['0x1', '0x1'] } })).toBe(true);
expect(aaReceiptSucceeded({ status: 'reverted', eip8130: { phaseStatuses: ['0x1'] } })).toBe(false);
expect(aaReceiptSucceeded({ status: '0x0', eip8130: { phaseStatuses: ['0x1'] } })).toBe(false);
expect(aaReceiptSucceeded({ status: 'success', eip8130: { phaseStatuses: ['0x1', '0x0'] } })).toBe(false);
});
});
12 changes: 12 additions & 0 deletions app/vibenet/demos/account/library/receipt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { allPhasesSucceeded, type Hex } from '@aa';

export type AaReceiptLike = {
status?: 'success' | 'reverted' | Hex;
eip8130?: { phaseStatuses?: readonly Hex[] };
};

/** An EIP-8130 transaction succeeds only when its outer tx and every call phase succeed. */
export function aaReceiptSucceeded(receipt: AaReceiptLike): boolean {
if (receipt.status === 'reverted' || receipt.status === '0x0') return false;
return allPhasesSucceeded(receipt.eip8130 ?? {});
}
25 changes: 16 additions & 9 deletions app/vibenet/demos/account/useAccountEngine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { vibenetApi } from '../../library/client';
import { ACCOUNT_RPC_URL } from '../../library/config';
import { type DemoChain, deploymentFromContracts, estimateTxGas, getDemoChain } from './library/chains';
import { buildPhases, type CallRow, newCallRow, safeGasLimit, valueBearingCallCount } from './library/calls';
import { aaReceiptSucceeded } from './library/receipt';
import {
type AppPolicy,
type AppSessionKey,
Expand Down Expand Up @@ -803,10 +804,7 @@ function useAccountEngineCore() {
const awaitInclusion = async (txHash: Hex, timeout = 30_000): Promise<Hex> => {
try {
const receipt = await waitForTransactionReceipt(makeRpcClient() as never, { hash: txHash, timeout });
if (receipt.status === '0x0') throw new Error(`Transaction reverted onchain (${txHash}).`);
const phases = receipt.eip8130?.phaseStatuses ?? [];
const failedPhase = phases.findIndex((s: Hex) => s === '0x0');
if (failedPhase !== -1) throw new Error(`Phase ${failedPhase} reverted (tx ${txHash}).`);
if (!aaReceiptSucceeded(receipt)) throw new Error(`Transaction reverted onchain (${txHash}).`);
} catch (err) {
if ((err as Error)?.message?.includes('timed out')) throw new TxPendingError(txHash);
throw err;
Expand Down Expand Up @@ -1174,16 +1172,14 @@ function useAccountEngineCore() {
return signer;
};

const sendAccountCalls = async ({
const signAccountCalls = async ({
account,
calls,
wait = true,
seqOpt,
metadata,
}: {
account: StoredAccount;
calls: { to: Address; data: Hex; value?: string }[];
wait?: boolean;
seqOpt?: {
nonceSequence?: bigint;
nonceKey?: bigint;
Expand All @@ -1193,10 +1189,10 @@ function useAccountEngineCore() {
maxPriorityFeePerGas?: bigint;
};
metadata?: string;
}): Promise<{ hash: Hex; serialized: Hex; nextSeq: number }> => {
}): Promise<{ serialized: Hex; nextSeq: number }> => {
if (!calls.length) throw new Error('No calls to send.');
const signer = signerForAccount(account);
const { serialized, nextSeq } = await signComposed(
return signComposed(
account,
signer,
calls.map((call) => newCallRow({ to: call.to, data: call.data, value: call.value ?? '0' })),
Expand All @@ -1207,6 +1203,16 @@ function useAccountEngineCore() {
undefined,
seqOpt,
);
};

const sendAccountCalls = async ({
wait = true,
...signArgs
}: Parameters<typeof signAccountCalls>[0] & {
wait?: boolean;
}): Promise<{ hash: Hex; serialized: Hex; nextSeq: number }> => {
const { account } = signArgs;
const { serialized, nextSeq } = await signAccountCalls(signArgs);
if (wait) {
const hash = await broadcast8130(serialized);
applyLandedBundle(account, nextSeq, []);
Expand Down Expand Up @@ -2079,6 +2085,7 @@ function useAccountEngineCore() {
// Signing engine (also used by each surface's own Transact flow)
broadcast8130,
signComposed,
signAccountCalls,
sendActiveCalls,
sendAccountCalls,
sendActiveCallsBatches,
Expand Down
13 changes: 12 additions & 1 deletion app/vibenet/demos/catalogue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ describe('DEMOS', () => {
it('lists Validity Transactions as a top-level group', () => {
const validity = listedDemos().find((demo) => demo.href === '/vibenet/demos/validity');
expect(validity?.title).toBe('Validity Transactions');
expect(validity?.children?.map((demo) => demo.title)).toEqual(['Limit Orders']);
expect(validity?.children?.map((demo) => demo.title)).toEqual(['Limit Orders', 'Race the Agent']);
});
});

describe('demoForPath', () => {
it('finds nested demos without flattening them onto the Vibenet grid', () => {
expect(demoForPath('/vibenet/demos/validity/limit-orders')?.title).toBe('Limit Orders');
expect(demoForPath('/vibenet/demos/validity/race-the-agent')?.title).toBe('Race the Agent');
expect(listedDemos().some((demo) => demo.title === 'Limit Orders')).toBe(false);
});
});
Expand All @@ -75,6 +76,16 @@ describe('demoBreadcrumb', () => {
});
});

it('resolves the second nested validity demo', () => {
expect(demoBreadcrumb('/vibenet/demos/validity/race-the-agent')).toEqual({
middle: {
label: 'Validity Transactions',
href: '/vibenet/demos/validity',
},
childLabel: 'Race the Agent',
});
});

it('falls back to readable labels for unregistered nested routes', () => {
expect(demoBreadcrumb('/vibenet/demos/trading/stop-loss')).toEqual({
middle: { label: 'Trading', href: '/vibenet/demos/trading' },
Expand Down
12 changes: 12 additions & 0 deletions app/vibenet/demos/catalogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ export const DEMOS: DemoEntry[] = [
],
available: true,
},
{
href: '/vibenet/demos/validity/race-the-agent',
title: 'Race the Agent',
summary:
'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',
'Judge the result by inclusion blocks, not browser timing',
],
available: true,
},
],
},
];
Expand Down
17 changes: 16 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 { blockExpiryPredicate, priceValidity, storagePredicate } from './predicates';

const PAIR = '0x1111111111111111111111111111111111111111';

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

it('uses neutral labels for a full-mask non-AMM storage slot', () => {
const predicate = storagePredicate(PAIR, 123n, (1n << 256n) - 1n, '=', 1n);
const notes = annotatedValidity([predicate]).map((row) => row.note).filter(Boolean);
expect(notes).toContain('Storage condition');
expect(notes).toContain('Contract whose storage is read');
expect(notes).toContain('Keep the selected bits');
expect(notes.some((note) => /reserve/i.test(note ?? ''))).toBe(false);
expect(reviewClauses([predicate])).toEqual([
{
title: 'Storage condition',
detail: 'Include only if the selected value is exactly — 1',
},
]);
});
});

describe('reviewClauses', () => {
Expand Down
19 changes: 14 additions & 5 deletions app/vibenet/demos/validity/lib/annotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,20 @@ function storageNotes(predicate: StoragePredicate, vibeToken0: boolean): Record<
const mask = BigInt(predicate.params.mask);
const slot = BigInt(predicate.params.slot);
const value = BigInt(predicate.params.value);
const reserve = reserveFromMask(mask);
const symbol = reserve === null ? 'reserve' : tokenForReserve(reserve, vibeToken0);
const half = reserve === 0 ? 'low 112 bits' : reserve === 1 ? 'high 112 bits' : 'selected bits';
const amount =
reserve === null ? value.toString() : formatReserve(decodeReserve(value, mask), symbol);
const reserve = slot === PAIR_RESERVES_SLOT ? reserveFromMask(mask) : null;
if (reserve === null) {
return {
type: 'Storage condition',
address: 'Contract whose storage is read',
slot: `Storage slot ${slot.toString()}`,
mask: 'Keep the selected bits',
op: `Include only if the selected value is ${comparePhrase(predicate.params.op)}`,
value: value.toString(),
};
}
const symbol = tokenForReserve(reserve, vibeToken0);
const half = reserve === 0 ? 'low 112 bits' : 'high 112 bits';
const amount = formatReserve(decodeReserve(value, mask), symbol);
return {
type: `${boundWord(predicate.params.op)} on the ${symbol} reserve`,
address: 'The simulated VIBE/USDV pair',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compiler": {
"version": "0.8.24+commit.e11b9ed9"
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
}
},
"command": "npx --yes solc@0.8.24 --optimize --optimize-runs 200 --bin --abi -o <output-dir> app/vibenet/demos/validity/lib/contracts/ConditionalWithdrawal.sol",
"source": "../contracts/ConditionalWithdrawal.sol",
"abi": [
{"inputs":[{"internalType":"contract IERC20","name":"vibe","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},
{"inputs":[],"name":"ENABLED_SLOT","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"VIBE","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"enabled","outputs":[{"internalType":"bool","name":"value","type":"bool"}],"stateMutability":"view","type":"function"},
{"inputs":[],"name":"flip","outputs":[{"internalType":"bool","name":"value","type":"bool"}],"stateMutability":"nonpayable","type":"function"},
{"inputs":[{"internalType":"bool","name":"value","type":"bool"}],"name":"setEnabled","outputs":[],"stateMutability":"nonpayable","type":"function"},
{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}
],
"bytecode": "0x60a060405234801561000f575f80fd5b506040516103b03803806103b083398101604081905261002e9161003f565b6001600160a01b031660805261006c565b5f6020828403121561004f575f80fd5b81516001600160a01b0381168114610065575f80fd5b9392505050565b60805161032661008a5f395f8181608301526101d101526103265ff3fe608060405234801561000f575f80fd5b506004361061007a575f3560e01c8063328d8f7211610058578063328d8f72146101065780633ccfd60b14610127578063848606331461012f578063cde4efa91461013e575f80fd5b806304a3b7cd1461007e57806304c879d6146100c2578063238dafe0146100e4575b5f80fd5b6100a57f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b6100d65f805160206102d183398151915281565b6040519081526020016100b9565b5f805160206102d18339815191525415155b60405190151581526020016100b9565b610125610114366004610293565b5f805160206102d183398151915255565b005b610125610158565b6100d6670de0b6b3a764000081565b5f805160206102d1833981519152805415908190556100f6565b5f805160206102d1833981519152546101ae5760405162461bcd60e51b81526020600482015260136024820152721dda5d1a191c985dd85b08191a5cd8589b1959606a1b60448201526064015b60405180910390fd5b60405163a9059cbb60e01b8152336004820152670de0b6b3a764000060248201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303815f875af115801561021f573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061024391906102b5565b6102815760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016101a5565b565b8015158114610290575f80fd5b50565b5f602082840312156102a3575f80fd5b81356102ae81610283565b9392505050565b5f602082840312156102c5575f80fd5b81516102ae8161028356fea91a9aee734204743335c443df931dcb220441d8aa6c1355dc61503a4bec3129a264697066735822122086d03dd0ac93876dcc7d8450420e92071a584f9b6452b41d07282203bd45a11f64736f6c63430008180033"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
ensureCreate2Contract: vi.fn(),
ensureCreate2Deployer: vi.fn(),
hasCode: vi.fn(),
}));

vi.mock('./singleton', () => ({
create2Address: () => '0x2222222222222222222222222222222222222222',
ensureCreate2Contract: mocks.ensureCreate2Contract,
ensureCreate2Deployer: mocks.ensureCreate2Deployer,
hasCode: mocks.hasCode,
singletonSalt: () => `0x${'11'.repeat(32)}`,
}));

import { ensureConditionalWithdrawal } from './conditionalWithdrawal';

const VIBE = '0x1111111111111111111111111111111111111111';
const WITHDRAWAL = '0x2222222222222222222222222222222222222222';

describe('ensureConditionalWithdrawal', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.ensureCreate2Deployer.mockResolvedValue(undefined);
});

it('accepts a correctly configured deployment created concurrently by another visitor', async () => {
mocks.hasCode.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
mocks.ensureCreate2Contract.mockRejectedValue(new Error('CREATE2 duplicate'));
const publicClient = {
readContract: vi.fn().mockResolvedValue(VIBE),
};

await expect(ensureConditionalWithdrawal({
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',
}));
});
});
Loading
Loading