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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions app/analytics/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
2 changes: 1 addition & 1 deletion app/vibenet/demos/catalogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

This file was deleted.

72 changes: 22 additions & 50 deletions app/vibenet/demos/validity/lib/conditionalWithdrawal.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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',
Expand All @@ -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();
});
});
100 changes: 1 addition & 99 deletions app/vibenet/demos/validity/lib/conditionalWithdrawal.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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<Address> {
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,
Expand All @@ -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,
Expand Down
Loading
Loading