diff --git a/app/components/AppShell.tsx b/app/components/AppShell.tsx index 1113e76..b84468c 100644 --- a/app/components/AppShell.tsx +++ b/app/components/AppShell.tsx @@ -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'; @@ -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 ( { 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'); + }); }); diff --git a/app/navigation.ts b/app/navigation.ts index 9730402..d294733 100644 --- a/app/navigation.ts +++ b/app/navigation.ts @@ -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'; @@ -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) { diff --git a/app/sitemap.test.ts b/app/sitemap.test.ts new file mode 100644 index 0000000..c500afc --- /dev/null +++ b/app/sitemap.test.ts @@ -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'); + }); +}); diff --git a/app/sitemap.ts b/app/sitemap.ts index a87908a..d70a940 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -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 }) => ({ diff --git a/app/vibenet/demos/catalogue.test.ts b/app/vibenet/demos/catalogue.test.ts index 34ed354..dd72425 100644 --- a/app/vibenet/demos/catalogue.test.ts +++ b/app/vibenet/demos/catalogue.test.ts @@ -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', () => { @@ -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', + }); }); }); diff --git a/app/vibenet/demos/catalogue.ts b/app/vibenet/demos/catalogue.ts index 83e0052..f854d87 100644 --- a/app/vibenet/demos/catalogue.ts +++ b/app/vibenet/demos/catalogue.ts @@ -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. */ @@ -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, + 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, + }, + ], }, ]; @@ -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) ?? ''), + }; +} diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index 10ee954..e004ef0 100644 --- a/app/vibenet/demos/validity/ValidityDemo.tsx +++ b/app/vibenet/demos/validity/ValidityDemo.tsx @@ -1131,9 +1131,9 @@ function ValidityDemoInner() { ) : (
{makersDry ? ( @@ -1150,12 +1150,11 @@ function ValidityDemoInner() { {!deployed ? ( - Shared pool + Shared singleton pool - 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. {address ? (
@@ -1187,7 +1186,7 @@ function ValidityDemoInner() {
- Spot {spot === 0n ? '—' : `$${formatPrice(spot)}`} USDV · simulated flow moves the mid + Spot {spot === 0n ? '—' : `$${formatPrice(spot)}`} USDV · page-scoped traders move the shared mid
diff --git a/app/vibenet/demos/validity/conditional-swaps/layout.tsx b/app/vibenet/demos/validity/conditional-swaps/layout.tsx new file mode 100644 index 0000000..98306f2 --- /dev/null +++ b/app/vibenet/demos/validity/conditional-swaps/layout.tsx @@ -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}; +} diff --git a/app/vibenet/demos/validity/conditional-swaps/page.tsx b/app/vibenet/demos/validity/conditional-swaps/page.tsx new file mode 100644 index 0000000..8a98f18 --- /dev/null +++ b/app/vibenet/demos/validity/conditional-swaps/page.tsx @@ -0,0 +1,5 @@ +import { ValidityDemo } from '../ValidityDemo'; + +export default function ConditionalSwapsPage() { + return ; +} diff --git a/app/vibenet/demos/validity/layout.tsx b/app/vibenet/demos/validity/layout.tsx index 3ad36c7..a09c99f 100644 --- a/app/vibenet/demos/validity/layout.tsx +++ b/app/vibenet/demos/validity/layout.tsx @@ -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}; } diff --git a/app/vibenet/demos/validity/metadata.test.ts b/app/vibenet/demos/validity/metadata.test.ts new file mode 100644 index 0000000..bbebcc2 --- /dev/null +++ b/app/vibenet/demos/validity/metadata.test.ts @@ -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'); + }); +}); diff --git a/app/vibenet/demos/validity/page.tsx b/app/vibenet/demos/validity/page.tsx index 42a8089..35d50d4 100644 --- a/app/vibenet/demos/validity/page.tsx +++ b/app/vibenet/demos/validity/page.tsx @@ -1,5 +1,82 @@ -import { ValidityDemo } from './ValidityDemo'; +import { Button } from '../../../components/ui/Button'; +import { Text } from '../../../components/ui/Text'; +import { FeatureCard } from '../../components/FeatureCard'; +import type { VibenetFeature } from '../../library/types'; +import { FeatureGridCard } from '../_shared/FeatureGridCard'; +import { demoForPath } from '../catalogue'; -export default function ValidityDemoPage() { - return ; +const VALIDITY_PATH = '/vibenet/demos/validity'; + +const VALIDITY_FEATURE: VibenetFeature = { + id: 'validity-transactions', + tag: 'EIP-8130', + title: 'Validity Transactions', + summary: + 'Submit transactions with onchain conditions, then let the sequencer include them only while those conditions are valid.', + status: 'live', + availability: 'Coming soon in ', + availabilityLabel: 'Base Cobalt', + availabilityHref: '/upgrades/upgrade/cobalt', + highlights: [ + { + title: 'State-Aware Inclusion', + detail: 'The sequencer evaluates current onchain state immediately before including a transaction.', + }, + { + title: 'Submit Before It Is Valid', + detail: 'Sign and submit intent now, then let it wait until its execution conditions are satisfied.', + }, + { + title: 'Storage Conditions', + detail: 'Require contract storage values to match ranges or expected values at inclusion time.', + }, + { + title: 'Block Bounds & Expiry', + detail: 'Constrain execution to explicit block windows so stale transactions expire safely.', + }, + { + title: 'Concurrent Intents', + detail: 'Nonce-isolated transactions can wait independently without blocking other account activity.', + }, + { + title: 'No Keeper Required', + detail: 'Build intent-like flows from ordinary account transactions without a separate settlement contract.', + }, + ], +}; + +export default function ValidityTransactionsPage() { + const validity = demoForPath(VALIDITY_PATH); + if (!validity) return null; + + return ( +
+ + + + Demos + + +
+ {validity.children?.map((demo) => ( + + + + + + } + title={demo.title} + description={demo.summary} + > + + + ))} +
+
+ ); } diff --git a/public/AGENTS.md b/public/AGENTS.md index f007d1b..b86c490 100644 --- a/public/AGENTS.md +++ b/public/AGENTS.md @@ -27,7 +27,7 @@ Machine-readable entry point for agents working with Base Chain network state. | /upgrades/changelog | per release | re-fetch before stating an activation status | | /vibenet/faucet | monthly | stable within a session | | /api/snapshots | daily | re-fetch every session; never cache across sessions | -| /, /vibenet, /vibenet/demos/account, /vibenet/demos/b20, /vibenet/demos/validity | infrequent | stable within a session | +| /, /vibenet, /vibenet/demos/account, /vibenet/demos/b20, /vibenet/demos/validity, /vibenet/demos/validity/conditional-swaps | infrequent | stable within a session | ## Machine-readable endpoints @@ -88,7 +88,8 @@ Discovered from the Next.js app directory. - [/vibenet](https://chain.base.org/vibenet) — Explore Vibenet, the Base devnet for testing in-flight protocol features. - [/vibenet/demos/account](https://chain.base.org/vibenet/demos/account) — Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [/vibenet/demos/b20](https://chain.base.org/vibenet/demos/b20) — Explore, configure, and issue Base-native B20 tokens on Vibenet. -- [/vibenet/demos/validity](https://chain.base.org/vibenet/demos/validity) — Attach conditions to a transaction. A simulated pool shows how a swap waits, lands, or expires. +- [/vibenet/demos/validity](https://chain.base.org/vibenet/demos/validity) — Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. +- [/vibenet/demos/validity/conditional-swaps](https://chain.base.org/vibenet/demos/validity/conditional-swaps) — Place a validity-backed swap on Vibenet that waits for a target price, then fills or expires as the market moves. - [/vibenet/explorer](https://chain.base.org/vibenet/explorer) — Browse blocks, transactions, and addresses on the Vibenet devnet. - [/vibenet/faucet](https://chain.base.org/vibenet/faucet) — Request testnet tokens on Vibenet to fund accounts and try in-flight Base features. diff --git a/public/llms-full.txt b/public/llms-full.txt index 2567977..84e1cda 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -21,7 +21,8 @@ - [Vibenet · Base Chain](https://chain.base.org/vibenet): Explore Vibenet, the Base devnet for testing in-flight protocol features. - [Accounts · Vibenet](https://chain.base.org/vibenet/demos/account): Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [Tokens · Vibenet](https://chain.base.org/vibenet/demos/b20): Explore, configure, and issue Base-native B20 tokens on Vibenet. -- [Validity · Vibenet](https://chain.base.org/vibenet/demos/validity): Attach conditions to a transaction. A simulated pool shows how a swap waits, lands, or expires. +- [Validity Transactions · Vibenet](https://chain.base.org/vibenet/demos/validity): Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. +- [Conditional Swaps · Validity Transactions](https://chain.base.org/vibenet/demos/validity/conditional-swaps): Place a validity-backed swap on Vibenet that waits for a target price, then fills or expires as the market moves. - [Explorer · Vibenet](https://chain.base.org/vibenet/explorer): Browse blocks, transactions, and addresses on the Vibenet devnet. (changes daily; re-fetch before relying on it) - [Faucet · Vibenet](https://chain.base.org/vibenet/faucet): Request testnet tokens on Vibenet to fund accounts and try in-flight Base features. (changes monthly; re-fetch before relying on it) diff --git a/public/llms.txt b/public/llms.txt index 80acc38..1d93dc4 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -19,7 +19,8 @@ Freshness: /snapshots and /vibenet/explorer change daily. /vibenet/faucet change - [Vibenet · Base Chain](https://chain.base.org/vibenet): Explore Vibenet, the Base devnet for testing in-flight protocol features. - [Accounts · Vibenet](https://chain.base.org/vibenet/demos/account): Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [Tokens · Vibenet](https://chain.base.org/vibenet/demos/b20): Explore, configure, and issue Base-native B20 tokens on Vibenet. -- [Validity · Vibenet](https://chain.base.org/vibenet/demos/validity): Attach conditions to a transaction. A simulated pool shows how a swap waits, lands, or expires. +- [Validity Transactions · Vibenet](https://chain.base.org/vibenet/demos/validity): Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. +- [Conditional Swaps · Validity Transactions](https://chain.base.org/vibenet/demos/validity/conditional-swaps): Place a validity-backed swap on Vibenet that waits for a target price, then fills or expires as the market moves. - [Explorer · Vibenet](https://chain.base.org/vibenet/explorer): Browse blocks, transactions, and addresses on the Vibenet devnet. - [Faucet · Vibenet](https://chain.base.org/vibenet/faucet): Request testnet tokens on Vibenet to fund accounts and try in-flight Base features.