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
9 changes: 5 additions & 4 deletions app/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Toaster } from 'sonner';
import { getActiveParent, isChildActive, isTopNavActive, navActiveParent, navHighlightPath, NAV_ITEMS, NavIcon, titleForPath } from '../navigation';
import { BLUE, BORDER, BRAND_BLUE, DISABLED, INK, MUTED, SELECTED } from '../theme';
import { getChangeBySlug } from '../upgrades/data/changes';
import { demoLabel } from '../vibenet/demos/catalogue';
import { demoBreadcrumb } from '../vibenet/demos/catalogue';
import { getUpgradeById } from '../upgrades/data/upgrades';

import { trackNavClick } from '../analytics/events';
Expand Down Expand Up @@ -802,15 +802,16 @@ export function AppShell({ children }: PropsWithChildren) {
let childLabel = title;
let middle: { label: string; href: string } | undefined;
const explorerDetailMatch = pathname.match(/^\/vibenet\/explorer\/(tx|block|address)\/(.+)$/);
const demoMatch = pathname.match(/^\/vibenet\/demos\/(.+)$/);
const demo = demoBreadcrumb(pathname);
if (explorerDetailMatch) {
middle = { label: 'Explorer', href: '/vibenet/explorer' };
const raw = explorerDetailMatch[2];
childLabel = raw.startsWith('0x') && raw.length > 12
? `${raw.slice(0, 6)}…${raw.slice(-4)}`
: raw;
} else if (demoMatch) {
childLabel = demoLabel(demoMatch[1].split('/')[0]);
} else if (demo) {
childLabel = demo.childLabel;
middle = demo.middle;
}
return (
<Breadcrumb
Expand Down
5 changes: 5 additions & 0 deletions app/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,9 @@ describe('titleForPath', () => {
expect(titleForPath('/vibenet/faucet')).toBe('Faucet');
expect(titleForPath('/vibenet')).toBe('Overview');
});

it('uses catalogue labels for grouped and nested demos', () => {
expect(titleForPath('/vibenet/demos/validity')).toBe('Validity Transactions');
expect(titleForPath('/vibenet/demos/validity/conditional-swaps')).toBe('Conditional Swaps');
});
});
3 changes: 3 additions & 0 deletions app/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BENCHMARK_ENABLED } from './benchmark/flag';
import { EXPLORER_ENABLED, EXPLORER_LABEL } from './internal-explorer/flag';
import { demoBreadcrumb } from './vibenet/demos/catalogue';

export type NavIcon = 'home' | 'snapshots' | 'upgrades' | 'changelog' | 'vibenet' | 'overview' | 'demos' | 'faucet' | 'explorer' | 'internal-explorer' | 'benchmark' | 'runs' | 'loadtest';

Expand Down Expand Up @@ -66,6 +67,8 @@ export function pathMatches(href: string, pathname: string, exact = false): bool

export function titleForPath(pathname: string): string {
if (pathname === '/') return 'Home';
const demo = demoBreadcrumb(pathname);
if (demo) return demo.childLabel;
for (const item of NAV_ITEMS) {
if (item.children) {
for (const child of item.children) {
Expand Down
12 changes: 12 additions & 0 deletions app/sitemap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';

import sitemap from './sitemap';

describe('sitemap', () => {
it('indexes the Validity Transactions group and its Conditional Swaps demo', () => {
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/conditional-swaps');
});
});
1 change: 1 addition & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ path: '/vibenet/demos/account', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/b20', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/validity', priority: 0.5, changeFrequency: 'weekly' },
{ path: '/vibenet/demos/validity/conditional-swaps', priority: 0.5, changeFrequency: 'weekly' },
];

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

import { DEMOS, demoLabel, listedDemos } from './catalogue';
import { DEMOS, demoBreadcrumb, demoForPath, demoLabel, listedDemos } from './catalogue';

describe('demoLabel', () => {
it('prefers shortTitle for the validity demo', () => {
expect(demoLabel('validity')).toBe('Validity');
it('uses the group title for the validity demo', () => {
expect(demoLabel('validity')).toBe('Validity Transactions');
});

it('prefers shortTitle over title when both are set', () => {
Expand All @@ -31,19 +31,54 @@ describe('demoLabel', () => {
});

describe('DEMOS', () => {
const allDemos = DEMOS.flatMap((demo) => [demo, ...(demo.children ?? [])]);

it('gives every entry a /vibenet/demos/ href, so demoLabel can resolve it', () => {
for (const demo of DEMOS) {
for (const demo of allDemos) {
expect(demo.href.startsWith('/vibenet/demos/')).toBe(true);
}
});

it('has no duplicate hrefs', () => {
const hrefs = DEMOS.map((d) => d.href);
const hrefs = allDemos.map((demo) => demo.href);
expect(new Set(hrefs).size).toBe(hrefs.length);
});

it('keeps Validity off the Vibenet demos grid while the route still resolves', () => {
expect(listedDemos().some((demo) => demo.href === '/vibenet/demos/validity')).toBe(false);
expect(demoLabel('validity')).toBe('Validity');
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(['Conditional Swaps']);
});
});

describe('demoForPath', () => {
it('finds nested demos without flattening them onto the Vibenet grid', () => {
expect(demoForPath('/vibenet/demos/validity/conditional-swaps')?.title).toBe('Conditional Swaps');
expect(listedDemos().some((demo) => demo.title === 'Conditional Swaps')).toBe(false);
});
});

describe('demoBreadcrumb', () => {
it('resolves a top-level group breadcrumb', () => {
expect(demoBreadcrumb('/vibenet/demos/validity')).toEqual({
childLabel: 'Validity Transactions',
});
});

it('resolves a nested demo breadcrumb through its group', () => {
expect(demoBreadcrumb('/vibenet/demos/validity/conditional-swaps')).toEqual({
middle: {
label: 'Validity Transactions',
href: '/vibenet/demos/validity',
},
childLabel: 'Conditional Swaps',
});
});

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' },
childLabel: 'Stop Loss',
});
});
});
68 changes: 61 additions & 7 deletions app/vibenet/demos/catalogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type DemoEntry = {
available: boolean;
/** When false, the route stays live but is omitted from the Vibenet demos grid. */
listed?: boolean;
/** Nested demos shown from a group landing page. */
children?: DemoEntry[];
};

/** Demos shown on the Vibenet index. Unlisted entries stay reachable by URL. */
Expand Down Expand Up @@ -55,17 +57,30 @@ export const DEMOS: DemoEntry[] = [
},
{
href: '/vibenet/demos/validity',
title: 'Validity',
shortTitle: 'Validity',
title: 'Validity Transactions',
shortTitle: 'Validity Transactions',
summary:
'Attach conditions to a transaction so the sequencer includes it only while they hold. A simulated pool shows a swap waiting on price, then landing or expiring.',
'Explore transactions that remain pending until their onchain conditions are satisfied, then execute without a keeper or a custom settlement contract.',
points: [
'Add storage and block-number conditions to an ordinary swap',
'A simulated AMM makes those conditions visible on a moving mid',
'Stack several 8130 conditions at once, or replace the resting one',
'Attach storage and block-number conditions to signed transactions',
'Let the sequencer evaluate validity before inclusion',
'Build intent-like flows from ordinary account transactions',
],
available: true,
listed: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's keep listed:false for now

children: [
{
href: '/vibenet/demos/validity/conditional-swaps',
title: 'Conditional Swaps',
summary:
'Place a swap that waits for a target price, then lands or expires as a shared simulated market moves through its validity window.',
points: [
'Set a buy or sell price against a live VIBE/USDV pool',
'Inspect the EIP-8130 predicates attached to the swap',
'Watch pending orders fill, expire, or get replaced',
],
available: true,
},
],
},
];

Expand All @@ -87,3 +102,42 @@ export function demoLabel(slug: string): string {
const demo = DEMOS.find((entry) => entry.href === `/vibenet/demos/${slug}`);
return demo?.shortTitle ?? demo?.title ?? prettifySlug(slug);
}

function entryLabel(entry: DemoEntry | undefined, fallbackSlug: string): string {
return entry?.shortTitle ?? entry?.title ?? prettifySlug(fallbackSlug);
}

/** Finds a registered top-level or nested demo by its full route. */
export function demoForPath(pathname: string): DemoEntry | undefined {
for (const demo of DEMOS) {
if (demo.href === pathname) return demo;
const child = demo.children?.find((entry) => entry.href === pathname);
if (child) return child;
}
return undefined;
}

export type DemoBreadcrumb = {
childLabel: string;
middle?: { label: string; href: string };
};

/** Resolves catalogue-backed labels for any route below `/vibenet/demos`. */
export function demoBreadcrumb(pathname: string): DemoBreadcrumb | null {
const prefix = '/vibenet/demos/';
if (!pathname.startsWith(prefix)) return null;

const segments = pathname.slice(prefix.length).split('/').filter(Boolean);
if (segments.length === 0) return null;

const parentHref = `${prefix}${segments[0]}`;
const parent = DEMOS.find((entry) => entry.href === parentHref);
const parentLabel = entryLabel(parent, segments[0]);
if (segments.length === 1) return { childLabel: parentLabel };

const child = parent?.children?.find((entry) => entry.href === pathname);
return {
middle: { label: parentLabel, href: parentHref },
childLabel: entryLabel(child, segments.at(-1) ?? ''),
};
}
17 changes: 8 additions & 9 deletions app/vibenet/demos/validity/ValidityDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1131,9 +1131,9 @@ function ValidityDemoInner() {
) : (
<div className="flex min-w-0 flex-1 flex-col gap-10 pb-16 text-foreground">
<DemoHeader
eyebrow="Validity · experimental"
title="Send now. Land later."
description="A transaction can carry predicates the sequencer checks before inclusion. Everyone shares one VIBE/USDV pool — VIBE is a B20, USDV is the faucet stablecoin — so you can watch a swap wait for a price condition, then land or expire."
eyebrow="Validity Transactions · experimental"
title="Conditional Swaps"
description="A transaction can carry predicates the sequencer checks before inclusion. Everyone trades against one shared singleton VIBE/USDV pool — VIBE is a B20 and USDV is the faucet stablecoin — while page-scoped background traders move the mid so conditional swaps can land or expire."
/>

{makersDry ? (
Expand All @@ -1150,12 +1150,11 @@ function ValidityDemoInner() {

{!deployed ? (
<Card className="flex flex-col gap-4 bg-background p-6 dark:bg-white/5">
<Text variant="title3">Shared pool</Text>
<Text variant="title3">Shared singleton pool</Text>
<Text variant="label.regular" tone="muted">
Your Vibenet account signs the swaps. The first visitor publishes a
network-wide pair of VIBE (a B20) and the faucet USDV. Everyone
else attaches to the same factory. Makers mint a starter bag and
buy or sell against that pool.
Your Vibenet account signs the swaps. The first visitor publishes the
network-wide singleton pair of VIBE (a B20) and the faucet USDV.
Everyone else attaches to that same shared factory and pool.
</Text>
{address ? (
<div className="flex items-center justify-between gap-3">
Expand Down Expand Up @@ -1187,7 +1186,7 @@ function ValidityDemoInner() {
<div className="flex min-w-0 flex-col gap-3">
<PriceCandles samples={samples} levels={chartLevels} fills={fillMarks} />
<Text variant="footnote" tone="muted">
Spot {spot === 0n ? '—' : `$${formatPrice(spot)}`} USDV · simulated flow moves the mid
Spot {spot === 0n ? '—' : `$${formatPrice(spot)}`} USDV · page-scoped traders move the shared mid
</Text>
</div>
<div className="rounded-2xl border border-bds-gray-10 bg-background px-5 py-4 dark:border-white/10 dark:bg-white/5">
Expand Down
12 changes: 12 additions & 0 deletions app/vibenet/demos/validity/conditional-swaps/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';

export const metadata: Metadata = {
title: 'Conditional Swaps · Validity Transactions',
description:
'Place a validity-backed swap on Vibenet that waits for a target price, then fills or expires as the market moves.',
};

export default function ConditionalSwapsLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
5 changes: 5 additions & 0 deletions app/vibenet/demos/validity/conditional-swaps/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ValidityDemo } from '../ValidityDemo';

export default function ConditionalSwapsPage() {
return <ValidityDemo />;
}
6 changes: 3 additions & 3 deletions app/vibenet/demos/validity/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import type { Metadata } from 'next';
import type { ReactNode } from 'react';

export const metadata: Metadata = {
title: 'Validity · Vibenet',
title: 'Validity Transactions · Vibenet',
description:
'Attach conditions to a transaction. A simulated pool shows how a swap waits, lands, or expires.',
'Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold.',
};

export default function ValidityDemoLayout({ children }: { children: ReactNode }) {
export default function ValidityTransactionsLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
11 changes: 11 additions & 0 deletions app/vibenet/demos/validity/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';

import { metadata as groupMetadata } from './layout';
import { metadata as conditionalSwapsMetadata } from './conditional-swaps/layout';

describe('validity route metadata', () => {
it('names the group and nested demo independently', () => {
expect(groupMetadata.title).toBe('Validity Transactions · Vibenet');
expect(conditionalSwapsMetadata.title).toBe('Conditional Swaps · Validity Transactions');
});
});
Loading
Loading