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
11 changes: 3 additions & 8 deletions app/vibenet/components/useAccountNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { useEffect, useState } from 'react';

import { deserializeState } from '../demos/account/library/model';
import { deserializeState, normalizePersistedState } from '../demos/account/library/model';
import type { Persisted } from '../demos/account/shared';
import { ACCOUNTS_STORAGE_KEY } from '../demos/account/useAccounts';

Expand All @@ -15,14 +15,9 @@ export function readAccountNames(): Record<string, string> {
try {
const raw = localStorage.getItem(ACCOUNTS_STORAGE_KEY);
if (!raw) return {};
const state = deserializeState<Persisted>(raw);
const { accounts } = normalizePersistedState(deserializeState<Partial<Persisted>>(raw));
const map: Record<string, string> = {};
for (const account of state.accounts ?? []) {
map[account.address.toLowerCase()] = account.label;
for (const sub of account.subAccounts ?? []) {
map[sub.address.toLowerCase()] = sub.label;
}
}
for (const account of accounts) map[account.address.toLowerCase()] = account.label;
return map;
} catch {
return {};
Expand Down
17 changes: 5 additions & 12 deletions app/vibenet/data/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,11 @@ export const FEATURES: VibenetFeature[] = [
availabilityLabel: 'Base Denim',
availabilityHref: {label: 'Base Denim', href: 'https://docs.base.org/upgrades/denim', external: true},
highlights: [
{ title: 'Authorize How You Want', detail: 'Support for secp256k1, P-256, and WebAuthn passkeys.' },
{ title: 'Portable Everywhere', detail: 'Same account and address on any EVM chain.' },
{
title: 'Rotate Keys, Keep Your Address',
detail: 'Swap signers without ever migrating accounts.',
},
{
title: 'Session Keys & Sub-Accounts',
detail: 'Scoped, policy-gated or full account separation.',
},
{ title: 'Sponsored & ERC-20 Gas', detail: 'Native payer support via ERC-8168 contracts.' },
{ title: 'Batch Everything', detail: 'Atomic multicall with top-level call metadata.' },
{ title: 'Superpowered EOAs', detail: 'Any secp256k1 key sends native AA transactions — nothing to deploy.' },
{ title: 'Portable Everywhere', detail: 'Same key and address on any EVM chain.' },
{ title: 'Nonce-Free Sends', detail: 'Short-lived transactions with no nonce to track or collide on.' },
{ title: 'Sponsored & ERC-20 Gas', detail: 'Native payer support via the ERC-8168 payer service.' },
{ title: 'Batch Everything', detail: 'Atomic, phased multicall with per-call value and top-level metadata.' },
],
// `live` hides the "coming-soon" badge. "Tutorial" points at the
// EIP-8130 build guide; there's no "try it" CTA since this card already
Expand Down
18 changes: 5 additions & 13 deletions app/vibenet/demos/_shared/AccountSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { CHECK_MORPH_ICON, CLIPBOARD_MORPH_ICON } from '../../../components/ui/i
import { Text } from '../../../components/ui/Text';
import type { StoredAccount } from '../account/library/model';
import { ChevronIcon, CreateRowButton, DeleteConfirmButton } from './dropdown';
import { AccountAvatar, AccountIdentity, Badge } from './primitives';
import { AccountAvatar, AccountIdentity } from './primitives';

// Icon-only button that copies an account's address; swaps to a green check for
// 2s after copying. Matches the sibling Details button's styling.
Expand Down Expand Up @@ -89,19 +89,17 @@ export function AccountSwitcher({
const [open, setOpen] = useState(false);

const active = accounts.find((a) => a.id === activeAccountId) ?? null;
const topLevel = accounts.filter((a) => !a.parentId);

const close = () => setOpen(false);

const row = (a: StoredAccount, nested: boolean) => {
const row = (a: StoredAccount) => {
const isActive = a.id === activeAccountId;
return (
<div
key={a.id}
className={cn(
'flex items-center gap-2 rounded-lg px-2 py-1.5',
isActive ? 'bg-bds-gray-5 dark:bg-white/10' : 'hover:bg-bds-gray-5 dark:hover:bg-white/5',
nested && 'ml-3',
)}
>
<button
Expand All @@ -115,8 +113,7 @@ export function AccountSwitcher({
<AccountIdentity
label={a.label}
address={a.address}
variant={a.parentId ? 'spending' : 'default'}
badges={a.deployed ? <Badge tone="ok">Deployed</Badge> : null}
variant="default"
className="min-w-0 flex-1"
/>
</button>
Expand Down Expand Up @@ -165,7 +162,7 @@ export function AccountSwitcher({
>
{active ? (
<span className="flex min-w-0 items-center gap-2">
<AccountAvatar variant={active.parentId ? 'spending' : 'default'} size={22} />
<AccountAvatar variant="default" size={22} />
<span className="max-w-[180px] truncate text-[13px] font-medium">{active.label}</span>
</span>
) : (
Expand All @@ -188,12 +185,7 @@ export function AccountSwitcher({
className="flex max-h-[60vh] w-[max(340px,var(--anchor-width))] max-w-[calc(100vw-2rem)] origin-[var(--transform-origin)] flex-col gap-1 overflow-y-auto rounded-2xl border border-bds-gray-10 bg-background p-2 shadow-lg outline-none [transform:scale(1)] transition-[opacity,transform] duration-150 ease-out data-[ending-style]:opacity-0 data-[ending-style]:[transform:scale(0.97)] data-[starting-style]:opacity-0 data-[starting-style]:[transform:scale(0.97)] dark:border-white/10 dark:bg-[rgb(38,38,38)] motion-reduce:transition-none"
>
<Popover.Title className="sr-only">Accounts</Popover.Title>
{topLevel.map((parent) => (
<div key={parent.id} className="flex flex-col gap-1">
{row(parent, false)}
{accounts.filter((s) => s.parentId === parent.id).map((sub) => row(sub, true))}
</div>
))}
{accounts.map((a) => row(a))}
{onCreate ? (
<CreateRowButton
label="+ New Account"
Expand Down
4 changes: 1 addition & 3 deletions app/vibenet/demos/_shared/primitives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ export function Badge({ children, tone }: { children: ReactNode; tone?: 'ok' | '

const KIND_BADGE: Record<SignerKind, string> = {
k1: 'bg-bds-blue-0 text-bds-blue-70 dark:text-base-blue',
p256: 'bg-bds-purple-0 text-bds-purple-70',
passkey: 'bg-bds-teal-0 text-bds-teal-70',
};

export function AccountAvatar({
Expand Down Expand Up @@ -159,7 +157,7 @@ export function AccountIdentity({
);
}

// Signer-kind chip (K1 / P-256 / passkey).
// Signer-kind chip (K1).
export function KindBadge({ kind }: { kind: SignerKind }) {
return (
<span
Expand Down
183 changes: 11 additions & 172 deletions app/vibenet/demos/account/AccountDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
'use client';

// Account demo (EIP-8130): in-browser signer keys, portable account creation
// (smart + EOA), balances/assets, native transact, session keys, and the apps
// directory. Account management (owners / session keys / sub-accounts / balances)
// now lives on the explorer address page (/vibenet/explorer/address/<addr>) when
// the address is a local account; this demo links there.
// Account demo (EIP-8130): in-browser K1 keys, EOA accounts, balances/assets,
// native transact (batched calls, nonce-free sends, payer-sponsored or
// token-paid gas). Per-account detail (balances, activity) lives on the
// explorer address page
// (/vibenet/explorer/address/<addr>) when the address is a local account.
//
// The shared account engine + transact dialog are consumed from context, so this
// demo, B20, and the account page all behave identically.

import { trackAccountAction } from '../../../analytics/events';
import { useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { toast } from 'sonner';

import { Button } from '../../../components/ui/Button';
import { Modal } from '../../../components/ui/Modal';
Expand All @@ -21,12 +20,9 @@ import { AccountDemoShell } from '../_components/AccountDemoShell';
import { FeatureCard } from '../../components/FeatureCard';
import { FEATURES } from '../../data/features';
import { ActivityLog } from './components/ActivityLog';
import { AppCard, AppCardPlaceholder, AppsNetworkNotice } from './components/AppsView';
import { FeatureGridCard, FeatureGridPlaceholder } from '../_shared/FeatureGridCard';
import { TransactionModal, type ApplyTarget, type TransactPreset } from './components/TransactionModal';
import { DEMO_APPS, type DemoApp } from './library/apps';
import { TransactionModal, type TransactPreset } from './components/TransactionModal';
import { encodeUsdvTransfer, isAddressStr, newCallRow } from './library/calls';
import { EXPIRY_PRESETS, type AppSessionKey, type AppSubAccount } from './library/model';
import { AccountEngineProvider, useAccountEngine } from './useAccountEngine';
import { vibenetApi } from '../../library/client';
import type { Address } from '@aa';
Expand All @@ -41,49 +37,20 @@ export function AccountDemo() {

function AccountDemoInner() {
const engine = useAccountEngine();
const {
signers,
accounts,
activeAccountId,
activity,
networkShort,
setNetworkShort,
deleteAccount,
const { accounts, activeAccountId, activity, regenesisNotice, setRegenesisNotice, acct } = engine;

activeSigner,

chain,
regenesisNotice,
setRegenesisNotice,

acct,

deleteSigner,
revokeSessionKey,
undoStagedRevoke,
doAuthorizeSession,
doCreateSubAccount,
mintAppKey,
} = engine;

// Apps directory.
const [appBusy, setAppBusy] = useState<string | null>(null);
const [transactionRequest, setTransactionRequest] = useState<{
preset?: TransactPreset;
applyTarget?: ApplyTarget;
contentKey: string;
} | null>(null);

// Crossfade key for the dashboard (transact + apps). Capture it in the modal
// request so changing "From" doesn't remount the content behind an open dialog.
// Crossfade key for the dashboard. Capture it in the modal request so
// changing "From" doesn't remount the content behind an open dialog.
const activeAccountKey = activeAccountId ?? 'empty';
const contentKey = transactionRequest?.contentKey ?? activeAccountKey;
const openTransaction = (preset?: TransactPreset) => {
setTransactionRequest({ preset, contentKey: activeAccountKey });
};
const openApply = (applyTarget: ApplyTarget) => {
setTransactionRequest({ applyTarget, contentKey: activeAccountKey });
};

const resolveUsdvAddress = async (): Promise<Address | null> => {
const status = await vibenetApi.faucet.status().catch(() => null);
Expand Down Expand Up @@ -120,71 +87,6 @@ function AccountDemoInner() {
});
};

// Session-app config changes use the same transaction review popup as every
// other account-demo send. A never-landed authorization is only local, so
// revoking it simply discards it without opening a transaction.
const unsubscribeApp = async (sk: AppSessionKey) => {
const outcome = await revokeSessionKey(sk.id);
if (outcome === 'staged' || outcome === 'noop') openApply({ session: sk.id });
};

const sessionKeyFor = (name: string) => acct?.sessionKeys.find((sk) => sk.label === name);
const subAccountFor = (name: string) => acct?.subAccounts.find((sa) => sa.label === name);

// "Delete Account" on a connected Spending Account app card.
const deleteVault = (sub: AppSubAccount) => {
const rec = accounts.find((a) => a.address.toLowerCase() === sub.address.toLowerCase());
if (rec) deleteAccount(rec.id);
};

// Connect a session-key app: mint a dedicated key and stage its owner-signed
// authorization, then hand submission to the common transaction popup.
const connectSessionApp = async (app: DemoApp) => {
if (!acct || !activeSigner) return;
setAppBusy(app.id);
let mintedKeyId: string | null = null;
try {
const target = mintAppKey(app.name);
if (!target) {
toast.error("Couldn't mint an app key — try again.");
return;
}
mintedKeyId = target.id;
const expirySecs = EXPIRY_PRESETS.find((p) => p.id === app.expiryId)?.seconds ?? 0;
const sk = await doAuthorizeSession(target, {
expirySecs,
policyLabel: app.policyLabel ?? 'Policy',
spec: app.spec?.(acct.address) ?? {},
label: app.name,
chainShort: chain.shortName,
});
if (sk) {
mintedKeyId = null;
openApply({ session: sk.id });
}
} catch (err) {
const e = err as { message?: string; name?: string };
toast.error(e.name === 'NotAllowedError' ? 'Signature was dismissed.' : (e.message ?? String(err)));
} finally {
if (mintedKeyId) deleteSigner(mintedKeyId);
setAppBusy(null);
}
};

// Connect a sub-account app ("spending account"): derive a delegated account
// with a spare owner key you hold.
const connectVault = (app: DemoApp) => {
if (!acct) return;
setAppBusy(app.id);
try {
doCreateSubAccount(app.name, { withSpareKey: true });
} catch (err) {
toast.error((err as { message?: string }).message ?? String(err));
} finally {
setAppBusy(null);
}
};

return (
<>
<AccountDemoShell
Expand Down Expand Up @@ -212,9 +114,7 @@ function AccountDemoInner() {
{renderSponsorship()}
{renderBatchedCalls()}
{renderGasToken()}
{renderOwners()}
{renderTransact()}
{renderApps()}
</motion.div>
</AnimatePresence>
</AccountDemoShell>
Expand All @@ -224,7 +124,6 @@ function AccountDemoInner() {
key={activeAccountId ?? 'no-account'}
onClose={() => setTransactionRequest(null)}
preset={transactionRequest.preset}
applyTarget={transactionRequest.applyTarget}
/>
) : null}

Expand All @@ -239,45 +138,13 @@ function AccountDemoInner() {
}
>
<Text variant="body" tone="muted">
The vibenet devnet has been regenesised — its onchain state was wiped. Your accounts and keys are still here
and their addresses are unchanged; they&apos;ve been marked undeployed and will redeploy on their next
transaction.
The vibenet devnet has been regenesised — its onchain state was wiped. Your
accounts and keys are still here and their addresses are unchanged; top up to transact again.
</Text>
</Modal>
</>
);

function renderApps() {
if (!acct) {
return DEMO_APPS.map((app) => <AppCardPlaceholder key={app.id} app={app} />);
}
const native = chain.mode === 'eip8130-native';
return (
<>
{!native ? <AppsNetworkNotice onSwitchToVibenet={() => setNetworkShort('vibenet')} /> : null}
{DEMO_APPS.map((app) => (
<AppCard
key={app.id}
acct={acct}
native={native}
app={app}
appBusy={appBusy}
activeSigner={activeSigner}
signers={signers}
sessionKeyFor={sessionKeyFor}
subAccountFor={subAccountFor}
connectSessionApp={connectSessionApp}
connectVault={connectVault}
unsubscribeApp={unsubscribeApp}
reviewSessionApp={(sessionKey) => openApply({ session: sessionKey.id })}
undoSessionRevoke={undoStagedRevoke}
deleteVault={deleteVault}
/>
))}
</>
);
}

function renderTransact() {
if (!acct)
return (
Expand Down Expand Up @@ -346,34 +213,6 @@ function AccountDemoInner() {
);
}

function renderOwners() {
if (!acct) {
return (
<FeatureGridPlaceholder title="Modify Owners" message="Create and select an account to manage owners." />
);
}
return (
<FeatureGridCard
icon={
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="6" cy="6" r="4" stroke="currentColor" strokeWidth="1.6" />
<path d="M9 9L18 18M14 14L17 11M16 16L18.5 13.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
}
title="Modify Owners"
description="Add or revoke owner keys and rotate signers — swap keys anytime without ever migrating accounts."
>
<Button
size="sm"
href={`/vibenet/explorer/address/${acct.address}?section=owners`}
onClick={() => trackAccountAction('modify_owners')}
>
Manage Owners
</Button>
</FeatureGridCard>
);
}

function renderBatchedCalls() {
if (!acct) {
return (
Expand Down
Loading
Loading