diff --git a/app/demos/b20/B20Demo.tsx b/app/demos/b20/B20Demo.tsx
index bb77ae6..34d3d65 100644
--- a/app/demos/b20/B20Demo.tsx
+++ b/app/demos/b20/B20Demo.tsx
@@ -25,7 +25,7 @@ import { AnnouncementModule, SampleAnnouncementViewer } from './components/Annou
import { DeployModule } from './components/DeployModule';
import { MemoModule } from './components/MemoModule';
import { PolicyModule } from './components/PolicyModule';
-import { client, CHAIN_ID, MODULES, SAMPLE_TOKEN } from './lib/constants';
+import { client, CHAIN_ID, MODULES } from './lib/constants';
import {
b20Abi,
b20Variant,
@@ -40,6 +40,7 @@ import {
shortAddress,
} from './lib/protocol';
import { readRecent, readRecentPolicies, writeRecent, writeRecentPolicy } from './lib/recent';
+import { sampleTokenForAddress } from './lib/samples';
import type {
ActivityItem,
CreatedToken,
@@ -102,7 +103,7 @@ export function B20Demo() {
useEffect(() => {
let cancelled = false;
setIsOperator(false);
- if (!activeTokenAddress || !wallet || activeTokenAddress.toLowerCase() === SAMPLE_TOKEN.toLowerCase()) return;
+ if (!activeTokenAddress || !wallet || sampleTokenForAddress(activeTokenAddress)) return;
client
.readContract({
address: activeTokenAddress,
@@ -128,7 +129,7 @@ export function B20Demo() {
setTokenAdminCheckedFor(null);
if (!activeTokenAddress || !wallet) return;
const checkKey = `${activeTokenAddress.toLowerCase()}:${wallet.toLowerCase()}`;
- if (activeTokenAddress.toLowerCase() === SAMPLE_TOKEN.toLowerCase()) {
+ if (sampleTokenForAddress(activeTokenAddress)) {
setTokenAdminCheckedFor(checkKey);
return;
}
@@ -206,6 +207,14 @@ export function B20Demo() {
const inspect = useCallback(
async (candidate = tokenAddress) => {
+ const sampleToken = sampleTokenForAddress(candidate);
+ if (sampleToken) {
+ setInspectError('');
+ setChecks(null);
+ setToken(sampleToken);
+ setTokenAddress(sampleToken.address);
+ return;
+ }
if (!isAddress(candidate)) {
setInspectError('Paste a valid token address, then try again.');
return;
@@ -235,13 +244,12 @@ export function B20Demo() {
if (!isB20 || !initialized) throw new Error('This address is not a ready-to-use B20 token.');
const variant = b20Variant(address);
if (!variant) throw new Error('We could not identify this B20 token type.');
- const [name, symbol, decimals, supply, cap, contractURI, policyRows] = await Promise.all([
+ const [name, symbol, decimals, supply, cap, policyRows] = await Promise.all([
client.readContract({ address, abi: b20Abi, functionName: 'name' }),
client.readContract({ address, abi: b20Abi, functionName: 'symbol' }),
client.readContract({ address, abi: b20Abi, functionName: 'decimals' }),
client.readContract({ address, abi: b20Abi, functionName: 'totalSupply' }),
client.readContract({ address, abi: b20Abi, functionName: 'supplyCap' }),
- client.readContract({ address, abi: b20Abi, functionName: 'contractURI' }),
Promise.all(
POLICY_SCOPES.map(async ([scope, label]) => {
const id = await client.readContract({
@@ -268,7 +276,7 @@ export function B20Demo() {
}),
),
]);
- setToken({ address, name, symbol, decimals, variant, supply, cap, contractURI, policies: policyRows });
+ setToken({ address, name, symbol, decimals, variant, supply, cap, policies: policyRows });
setTokenAddress(address);
} catch (error) {
setToken(null);
@@ -367,7 +375,7 @@ export function B20Demo() {
trackB20ModuleSelect(next);
};
const tokenAccess: TokenAccess =
- token?.address.toLowerCase() === SAMPLE_TOKEN.toLowerCase()
+ sampleTokenForAddress(token?.address)
? 'sample'
: isOperator
? 'operator'
diff --git a/app/demos/b20/components/AnnouncementModule.tsx b/app/demos/b20/components/AnnouncementModule.tsx
index f730c78..d315ec7 100644
--- a/app/demos/b20/components/AnnouncementModule.tsx
+++ b/app/demos/b20/components/AnnouncementModule.tsx
@@ -210,86 +210,6 @@ export function AnnouncementModule({
setError(walletErrorMessage(error));
}
};
- if (token && tokenAccess === 'sample')
- return (
-
-
}
- />
-
-
-
-
-
- Scheduled update · Mock data
-
-
- 2027-Q1 forward split
-
-
- This notice explains a two-for-one split and when wallets will show the new balance.
-
-
-
2027-Q1-split
-
-
-
-
Token change
- Displayed balances double
-
-
-
Effective
- 15 Jan 2027, 09:00 UTC
-
-
-
Recorded together
- Notice and token change
-
-
-
-
-
-
-
- Information only · Mock data
-
-
- Quarterly reserve attestation
-
-
- This notice shares information without changing the token.
-
-
-
2026-Q4-reserves
-
-
-
-
Token changes
- None
-
-
-
Supporting link
- https://example.com/disclosures/reserves
-
-
-
-
-
-
- Ready to publish an announcement?
-
- Create an Asset token to write and publish your own announcements.
-
-
-
- Create your own token
-
-
-
- );
return (
- Sample transaction
+ Sample data · Read only
- Transfer with memo
+ Memo history
- A token transfer with a memo attached to help identify the transaction’s purpose.
+ Example token activity with short references attached to it. These records are local mock data.
-
- View transaction ↗
-
+
+ {SAMPLE_MEMOS.length} memo{SAMPLE_MEMOS.length === 1 ? '' : 's'}
+
+
+
+ {SAMPLE_MEMOS.map((row) => (
+
+
+
+
+ {row.operation} with memo
+
+
+
+
+
Memo caller
+ {shortAddress(row.caller)}
+
+
+
From
+ {shortAddress(row.from)}
+
+
+
To
+ {shortAddress(row.to)}
+
+
+
Amount
+ {formatAmount(row.value, token.decimals)} {token.symbol}
+
+
+
+ ))}
-
-
-
Operation
- Transfer with a note
-
-
-
Amount
- 0.001 {token.symbol}
-
-
-
Memo
- sending test
-
-
-
Encoding
- bytes32
-
-
Create a token to add memos to your own transactions.
@@ -104,7 +114,6 @@ export function MemoModule({
-
);
return (
diff --git a/app/demos/b20/components/PolicyModule.tsx b/app/demos/b20/components/PolicyModule.tsx
index 7e47f37..4125e8e 100644
--- a/app/demos/b20/components/PolicyModule.tsx
+++ b/app/demos/b20/components/PolicyModule.tsx
@@ -10,10 +10,10 @@ import { cn } from '../../../components/ui/cn';
import { InfoTooltip } from '../../../components/ui/InfoTooltip';
import { Text } from '../../../components/ui/Text';
import { VIBENET_EXPLORER_PATH } from '../../../vibenet/library/config';
-import { SAMPLE_TOKEN } from '../lib/constants';
import { B20_HELP, SCOPE_HELP } from '../lib/glossary';
import { formatAmount, MAX_SUPPLY_CAP, policyKindFromId, policyKindLabel, shortAddress } from '../lib/protocol';
import { READ_POLICY_PROMPT } from '../lib/prompts';
+import { SAMPLE_TOKEN } from '../lib/samples';
import type { CreatedPolicy, RecentPolicy, RecentToken, TokenAccess, TokenInfo } from '../lib/types';
import { AttachPolicy, type TokenAdminStatus } from './AttachPolicy';
import { CopyPromptButton } from './CopyPromptButton';
@@ -59,6 +59,7 @@ export function PolicyModule({
}) {
const [showCreator, setShowCreator] = useState(false);
const [suggestedPolicyId, setSuggestedPolicyId] = useState(null);
+ const isSample = tokenAccess === 'sample';
return (
@@ -71,11 +72,13 @@ export function PolicyModule({
- setShowCreator((shown) => !shown)}>
- {showCreator ? 'Close creator' : 'Create policy'}
-
+ {!isSample ? (
+ setShowCreator((shown) => !shown)}>
+ {showCreator ? 'Close creator' : 'Create policy'}
+
+ ) : null}
- {showCreator ? (
+ {showCreator && !isSample ? (
onInspect(SAMPLE_TOKEN)}
+ onClick={() => onInspect(SAMPLE_TOKEN.address)}
disabled={busy === 'inspect'}
>
{busy === 'inspect' ? 'Loading…' : 'Explore the sample'}
@@ -282,33 +285,37 @@ export function PolicyModule({
))}
-
-
-
- Check a wallet
- {B20_HELP.checkAddress}
-
-
- See what this wallet can do with the token you selected.
-
-
-
setCheckAddress(e.target.value)}
- placeholder="Paste a wallet address"
+ {!isSample ? (
+ <>
+
-
- Check
-
-
-
+
+
+ Check a wallet
+ {B20_HELP.checkAddress}
+
+
+ See what this wallet can do with the token you selected.
+
+
+ setCheckAddress(e.target.value)}
+ placeholder="Paste a wallet address"
+ />
+
+ Check
+
+
+
+ >
+ ) : null}
Token details
@@ -322,41 +329,47 @@ export function PolicyModule({
value={token.cap === MAX_SUPPLY_CAP ? 'Unlimited' : formatAmount(token.cap, token.decimals)}
/>
-
- View on Explorer ↗
-
+ {isSample ? (
+ Mock data · Not a deployed token
+ ) : (
+
+ View on Explorer ↗
+
+ )}
-
-
-
- Technical reference
-
- These are the contract checks behind this screen.
-
-
-
-
-
- {[
- 'factory.isB20(address)',
- 'token.policyId(scope)',
- 'registry.policyExists(id)',
- 'registry.policyAdmin(id)',
- 'registry.isAuthorized(id, account)',
- ].map((item) => (
-
-
{item}
-
Read
+ {!isSample ? (
+
+
+
+ Technical reference
+
+ These are the contract checks behind this screen.
+
- ))}
-
-
+
+
+
+ {[
+ 'factory.isB20(address)',
+ 'token.policyId(scope)',
+ 'registry.policyExists(id)',
+ 'registry.policyAdmin(id)',
+ 'registry.isAuthorized(id, account)',
+ ].map((item) => (
+
+ {item}
+ Read
+
+ ))}
+
+
+ ) : null}
>
) : null}
diff --git a/app/demos/b20/lib/constants.ts b/app/demos/b20/lib/constants.ts
index 56b56eb..ee1840e 100644
--- a/app/demos/b20/lib/constants.ts
+++ b/app/demos/b20/lib/constants.ts
@@ -1,4 +1,4 @@
-import { createPublicClient, http, isAddress, type Address } from 'viem';
+import { createPublicClient, http } from 'viem';
import { VIBENET_RPC_URL } from '../../../vibenet/library/config';
import type { Module } from './types';
@@ -10,14 +10,6 @@ export const client = createPublicClient({ transport: http(VIBENET_RPC_URL) });
export const STORAGE_KEY = 'vibenet.b20.recent.v1';
export const POLICY_STORAGE_KEY = 'vibenet.b20.recent-policies.v1';
-const configuredSampleToken = process.env.NEXT_PUBLIC_B20_SAMPLE_TOKEN;
-export const SAMPLE_TOKEN = (
- configuredSampleToken && isAddress(configuredSampleToken)
- ? configuredSampleToken
- : '0xb200000000000000000000D7E62F6c2E13Ea9dDb'
-) as Address;
-export const SAMPLE_MEMO_TX = '0x91e52e0c63d05116b9fda41d1168c2fe9b7b9fcadf9071494c16410c809d1b09';
-
export const INITIAL_ALLOCATION_MEMO = 'Initial deposit';
export const INITIAL_ALLOCATION_MAX = 100n;
diff --git a/app/demos/b20/lib/protocol.ts b/app/demos/b20/lib/protocol.ts
index 80f3e2a..f9dde53 100644
--- a/app/demos/b20/lib/protocol.ts
+++ b/app/demos/b20/lib/protocol.ts
@@ -154,7 +154,6 @@ export const b20Abi = [
{ type: 'function', name: 'decimals', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] },
{ type: 'function', name: 'totalSupply', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
{ type: 'function', name: 'supplyCap', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
- { type: 'function', name: 'contractURI', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
{
type: 'function',
name: 'balanceOf',
diff --git a/app/demos/b20/lib/samples.test.ts b/app/demos/b20/lib/samples.test.ts
new file mode 100644
index 0000000..4f6ea06
--- /dev/null
+++ b/app/demos/b20/lib/samples.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest';
+
+import { POLICY_SCOPES } from './protocol';
+import { SAMPLE_MEMOS, SAMPLE_TOKEN, sampleTokenForAddress } from './samples';
+
+describe('B20 sample data', () => {
+ it('resolves only the local sample token address, regardless of casing', () => {
+ expect(sampleTokenForAddress(SAMPLE_TOKEN.address)).toBe(SAMPLE_TOKEN);
+ expect(sampleTokenForAddress(SAMPLE_TOKEN.address.toUpperCase())).toBe(SAMPLE_TOKEN);
+ expect(sampleTokenForAddress('0x1111111111111111111111111111111111111111')).toBeNull();
+ expect(sampleTokenForAddress(undefined)).toBeNull();
+ });
+
+ it('provides a complete read-only policy and memo walkthrough', () => {
+ expect(SAMPLE_TOKEN.policies.map((policy) => policy.scope)).toEqual(POLICY_SCOPES.map(([scope]) => scope));
+ expect(SAMPLE_TOKEN.policies.filter((policy) => policy.id !== 0n)).toHaveLength(2);
+ expect(SAMPLE_TOKEN.policies.every((policy) => policy.exists)).toBe(true);
+ expect(SAMPLE_MEMOS).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ operation: 'transfer', memo: 'sending test', value: 1_000_000_000_000_000n }),
+ ]),
+ );
+ });
+});
diff --git a/app/demos/b20/lib/samples.ts b/app/demos/b20/lib/samples.ts
index 8d0b5b2..edfb364 100644
--- a/app/demos/b20/lib/samples.ts
+++ b/app/demos/b20/lib/samples.ts
@@ -2,6 +2,82 @@
// token. Kept out of the components so the copy is easy to review/revise in one
// place and the render functions stay presentational.
+import type { Address } from 'viem';
+
+import type { TokenInfo } from './types';
+
+const SAMPLE_POLICY_ADMIN = '0x1111111111111111111111111111111111111111' as Address;
+
+// This is intentionally a B20-shaped, synthetic address. It identifies the
+// local walkthrough only; it is not a deployed contract and must never be read
+// from the public RPC.
+export const SAMPLE_TOKEN: TokenInfo = {
+ address: '0xb200000000000000000000D7E62F6c2E13Ea9dDb' as Address,
+ name: 'Vibenet Reserve Asset',
+ symbol: 'VRA',
+ decimals: 18,
+ variant: 'asset',
+ supply: 2_500_000n * 10n ** 18n,
+ cap: 5_000_000n * 10n ** 18n,
+ policies: [
+ {
+ scope: 'TRANSFER_SENDER_POLICY',
+ label: 'Transfer sender',
+ id: (1n << 56n) + 42n,
+ exists: true,
+ admin: SAMPLE_POLICY_ADMIN,
+ },
+ {
+ scope: 'TRANSFER_RECEIVER_POLICY',
+ label: 'Transfer receiver',
+ id: 0n,
+ exists: true,
+ admin: SAMPLE_POLICY_ADMIN,
+ },
+ {
+ scope: 'TRANSFER_EXECUTOR_POLICY',
+ label: 'Transfer executor',
+ id: 0n,
+ exists: true,
+ admin: SAMPLE_POLICY_ADMIN,
+ },
+ {
+ scope: 'MINT_RECEIVER_POLICY',
+ label: 'Mint recipient',
+ id: 7n,
+ exists: true,
+ admin: SAMPLE_POLICY_ADMIN,
+ },
+ ],
+};
+
+export type SampleMemo = {
+ id: string;
+ operation: 'transfer' | 'mint';
+ memo: string;
+ caller: Address;
+ from: Address;
+ to: Address;
+ value: bigint;
+};
+
+export const SAMPLE_MEMOS: SampleMemo[] = [
+ {
+ id: 'reserve-transfer-001',
+ operation: 'transfer',
+ memo: 'sending test',
+ caller: SAMPLE_POLICY_ADMIN,
+ from: SAMPLE_POLICY_ADMIN,
+ to: '0x2222222222222222222222222222222222222222' as Address,
+ value: 1_000_000_000_000_000n,
+ },
+];
+
+/** Returns the local walkthrough token without initiating a network read. */
+export function sampleTokenForAddress(candidate: string | null | undefined): TokenInfo | null {
+ return candidate?.toLowerCase() === SAMPLE_TOKEN.address.toLowerCase() ? SAMPLE_TOKEN : null;
+}
+
export type SampleAnnouncement = {
id: string;
type: string;
@@ -13,20 +89,11 @@ export type SampleAnnouncement = {
};
export const SAMPLE_ANNOUNCEMENTS: SampleAnnouncement[] = [
- {
- id: '2027-Q1-split',
- type: 'Scheduled token update',
- title: '2027 Q1 Forward Split',
- description: 'A two-for-one split that doubles the tokens shown in wallets. It does not set the token price.',
- uri: 'https://example.com/disclosures/2027-q1-split',
- effective: '15 Jan 2027, 09:00 UTC',
- call: 'Displayed balances double',
- },
{
id: '2026-Q4-reserves',
type: 'Disclosure only',
title: 'Quarterly Reserve Attestation',
- description: 'The issuer shared its quarterly reserve report without changing the token.',
+ description: 'The issuer shared its quarterly reserve report.',
uri: 'https://example.com/disclosures/2026-q4-reserves',
effective: 'Published 20 Dec 2026',
call: 'No token change',
diff --git a/app/demos/b20/lib/types.ts b/app/demos/b20/lib/types.ts
index 62ebcc7..3e73a8a 100644
--- a/app/demos/b20/lib/types.ts
+++ b/app/demos/b20/lib/types.ts
@@ -17,7 +17,6 @@ export type RecentToken = {
export type TokenInfo = RecentToken & {
supply: bigint;
cap: bigint;
- contractURI: string;
policies: Array<{ scope: string; label: string; id: bigint; exists: boolean; admin: Address }>;
};