B20 Playground demo (/demos/b20) + explorer announcement/memo decoding - #50
B20 Playground demo (/demos/b20) + explorer announcement/memo decoding#50montycheese wants to merge 64 commits into
Conversation
Migrated the app out of the internal Nx / @cbhq template into a standalone Next.js (App Router) project that builds and deploys with the standard toolchain on Vercel. - App code (snapshots, upgrades, vibenet, shared shell/UI) carried over unchanged; it had no internal @cbhq imports. - Replaced internal tooling with Vercel-native config: standard package.json scripts (dev/build/start/lint/typecheck/test), next.config.mjs, Next tsconfig, eslint-config-next, Tailwind + PostCSS, Vitest. - Dropped Nx, Helm charts, terraform, pipelines, Dockerfile, and the @cbhq registry/config. - Base Sans fonts via next/font/local; env documented in .env.example. Verified: next build (13 routes), lint, typecheck, vitest (10 tests), and a local dev run of /, /snapshots, /upgrades, /vibenet.
Brings the canonical /api/snapshots implementation from the v2-snapshots-ui repo into omni: - R2RequestError with status, and getLatestSnapshotManifest that walks newest-first and skips 404s, so an in-progress latest snapshot (manifest not yet uploaded) falls back to the previous completed one. - Returns archiveUrl / archiveFile / metadataUrl (added as optional fields on the Snapshot type). Kept omni's two prior review fixes rather than regress them: fail closed (502) when any configured network errors instead of a partial 206, and no SUPPORTED_COMPONENTS allowlist filter so new manifest components are counted. Verified: next build (13 routes), lint, types, and vitest (10 tests).
Ports v2-snapshots-ui@ba0c382 into the snapshots UI so the two stop drifting: - Group account_changesets + storage_changesets into a single "State History" row (combined size); toggling it toggles both. - Gate dependent components: transaction_senders requires transactions; rocksdb_indices requires transactions + receipts + state history. Unmet dependents are disabled and auto-deselected on toggle. - Update the Full preset to state, headers, transactions, receipts, and state history (+ description). - Rewrite the download command to `base-reth-node download --chain <chain>` with --with-txs/--with-senders/--with-receipts/--with-state-history flags, --<preset> --resumable for presets, --archive --without-rocksdb, and map mainnet to `base`. - Component count reflects the grouped State History row.
feat: upgrade runtime to Node.js 24
Co-authored-by: Montana Wong <montanawong@gmail.com>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com> Co-authored-by: Montana Wong <montanawong@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
The success screen rendered inline on the Native Deployment tab, which read as "still deploying," and its state was lost the moment you followed a link and came back. A modal is the right fit for an ephemeral confirmation: it overlays the tab, is expected to be transient, and dismisses cleanly. - CreatedView is now the body of a Modal opened when a token is created; the deploy form stays put behind it. Next-step actions navigate and close the modal; Done / Escape / backdrop dismiss it. - Explorer and creation-transaction links open in a new tab, so following one no longer tears down the modal and the demo context. Modal a11y, while here (addresses the standing gaps, benefits all consumers): - aria-labelledby wires the title to the dialog, so it's announced by name. - Focus moves into the panel on open and is restored to the trigger on close. Focus-trap intentionally left out for now — the account demo nests Radix poppers in modals and a naive trap can fight their focus management; wants its own change.
…tent Walk back the modal. The Native Deployment tab is where the token is created, so an inline result there is the natural home — the modal was over-correcting for problems that are better solved directly: - Lift `created` from DeployModule to the parent B20Demo, so the success view survives switching tabs and returning to Native Deployment (verified). It resets only via "Create another token". - Keep the fix that actually mattered: Explorer / creation-transaction links open in a new tab, so following one no longer unmounts the demo and loses the result. - Full-width success layout (banner + two-column identity / what-it-did + the three next-step cards) instead of the cramped modal body. The Modal a11y improvements from the previous commit (aria-labelledby, initial focus, focus restore) are intentionally kept — the account demo's modals use them.
|
Deployment failed for project omni-ui with the following error: View Documentation: https://vercel.com/docs/accounts/team-members-and-roles |
Publishing an announcement only cleared the form and logged a row in the Activity panel, so operators had no clear signal the action landed. Capture the tx hash from onSend and render a persistent success banner (role=status) at the top of the composer: the announcement ID, a disclosure/multiplier summary, a new-tab transaction link, and a dismiss control — matching the token-creation confirmation. Co-Authored-By: Claude <noreply@anthropic.com>
…oser The announcement confirmation appeared to flash and vanish. Root cause was not the banner itself: send() re-inspects the token after every tx, yielding a fresh token object with the same address, and the operator-role effect was keyed on the token object — so it re-ran, resetting isOperator to false and refetching on every action. That flipped tokenAccess operator→external→operator for ~100ms, glitching the whole operator UI (and the banner's subtree) after each publish. - Key the operator-role effect on the token address, not the object identity, so a same-address re-inspect no longer re-runs it (no flicker). Genuine token switches still reset and refetch. - Render the publish confirmation at the top of the module, outside the token/operator-gated Card, with a subtle animate-in entrance so it can never be hidden by transient inspect state. Co-Authored-By: Claude <noreply@anthropic.com>
Audit fixes: - Add rel="noreferrer" to the two docs links that open new tabs (reverse tabnabbing; matches every other _blank link in the repo). - Remove the Vercel-preview no-op comment from demos/b20/page.tsx. - Add type="button" to the recent-token, memo-kind, and variant selectors. - Clear CopyPromptButton's reset timer on unmount. Test coverage (pure decode/format logic that was untested): - explorer.ts: executeBatch offset decoding, all four memo-call shapes, UIMultiplierUpdated/EndAnnouncement events, EIP-8130 scope bitmask, and the number/type formatters. Fixtures are built with viem encoders so they match on-chain layout by construction. - protocol.ts: amount() validation, featureId per-variant, shortAddress, and the memo length boundary / empty-memo cases. 71 tests pass (was 53). typecheck, lint, build clean. Co-Authored-By: Claude <noreply@anthropic.com>
Inline errors: - Add an ErrorNote banner (role="alert", matching the inspectError style) and route the Memo, Announcement, and Deploy submit failures to it instead of the browser alert() — consistent, non-blocking feedback in a public demo. Dedupe: - Move formatAmount and a new bytes32ToMemo (the inverse of memoToBytes32) into protocol.ts, and consume them from B20Demo.tsx and MemoHistory.tsx. Removes three copies of amount formatting / memo decoding and makes both unit-testable. - Cover formatAmount and bytes32ToMemo (incl. the memoToBytes32 round-trip) in protocol.test.ts. 73 tests pass. typecheck, lint, build clean; inline error verified in-browser. Co-Authored-By: Claude <noreply@anthropic.com>
| useEffect(() => { | ||
| if (typeof window === 'undefined') return; | ||
| window.requestAnimationFrame(() => window.scrollTo({ top: 0, left: 0, behavior: 'auto' })); | ||
| }, [created?.address, module]); | ||
|
|
||
| const selectModule = (next: Module) => { setModule(next); trackB20ModuleSelect(next); }; | ||
| const tokenAccess: TokenAccess = token?.address.toLowerCase() === SAMPLE_TOKEN.toLowerCase() ? 'sample' : isOperator ? 'operator' : wallet ? 'external' : 'disconnected'; | ||
| return ( | ||
| <div className="animate-in -mb-20 flex min-h-[calc(100vh-116px)] flex-col gap-5 pb-6 text-black [&_.text-3xl]:hidden [&_.tracking-tight]:capitalize dark:text-white"> | ||
| <header className="flex flex-wrap items-center justify-end gap-3 border-b border-bds-gray-10 pb-4 dark:border-white/10"> | ||
| <div className={cn('flex flex-wrap items-center gap-2', textVariantClasses.label)}> | ||
| <span className="rounded-full border border-bds-gray-10 px-3 py-2 dark:border-white/10"><span className="mr-2 text-bds-green-50">●</span>Vibenet</span> | ||
| <Link href="/vibenet/faucet" className="rounded-full border border-bds-gray-10 px-3 py-2 hover:border-base-blue dark:border-white/10">Faucet</Link> | ||
| {wallet ? <div className="inline-flex items-center gap-2 rounded-full border border-bds-gray-10 px-3 py-1.5 dark:border-white/10"><CopyableValue value={wallet} display={shortAddress(wallet)} /><span aria-hidden="true">·</span><span>{walletBalance === null ? '…' : `${Number(formatEther(walletBalance)).toFixed(3)} ETH`}</span><button type="button" onClick={disconnect} className="rounded-full px-2 py-1 text-[11px] text-bds-gray-60 transition-colors hover:bg-bds-gray-5 hover:text-black dark:text-bds-gray-30 dark:hover:bg-white/10 dark:hover:text-white">Disconnect</button></div> : <Button size="sm" onClick={() => void connect()}>Connect wallet</Button>} | ||
| </div> | ||
| </header> | ||
| <div className="flex min-h-0 flex-1 flex-col gap-5"> | ||
| <div className="overflow-x-auto"> | ||
| <Tabs items={MODULES} value={module} onChange={(value) => selectModule(value as Module)} ariaLabel="B20 modules" /> | ||
| </div> | ||
| <main className="min-w-0 flex-1">{module === 'policy' ? <PolicyModule token={token} tokenAccess={tokenAccess} address={tokenAddress} setAddress={setTokenAddress} recent={recent} onInspect={inspect} onDeploy={() => selectModule('deploy')} busy={busy} checkAddress={checkAddress} setCheckAddress={setCheckAddress} checks={checks} onCheck={checkPolicies} /> : null}{module === 'memos' ? <MemoModule token={token} tokenAccess={tokenAccess} onDeploy={() => selectModule('deploy')} onSend={send} busy={busy} /> : null}{module === 'announcements' ? token && tokenAccess === 'sample' ? <SampleAnnouncementViewer onDeploy={() => selectModule('deploy')} /> : <AnnouncementModule token={token} tokenAccess={tokenAccess} wallet={wallet} onDeploy={() => selectModule('deploy')} onSend={send} busy={busy} /> : null}{module === 'deploy' ? <DeployModule wallet={wallet} onSend={send} created={created} onCreated={async (next) => { if (wallet) setRecent(writeRecent(wallet, next)); setTokenAddress(next.address); setCreated(next); await inspect(next.address); }} onReset={() => setCreated(null)} onNavigate={selectModule} busy={busy} /> : null}</main> | ||
| </div> | ||
| {inspectError ? <div role="alert" className="rounded-xl border border-bds-red-20 bg-bds-red-0 px-4 py-3 text-[13px] text-bds-red-70 dark:border-bds-red-80 dark:bg-bds-red-100/40 dark:text-bds-red-20">{inspectError}</div> : null} | ||
| <Activity rows={activity} /> | ||
| </div> |
There was a problem hiding this comment.
nestling all of this HTML into a use effect seems like an anti pattern, either change it or make it into a component for cleaner reading
There was a problem hiding this comment.
Done in ad5a00c — reformatted the dense single-line JSX returns into readable multi-line JSX (ran prettier over the file). Kept it as formatting rather than a full component split to keep the diff behavior-preserving; happy to extract sub-components if you want it broken down further.
| } | ||
|
|
||
| function PolicyModule({ token, tokenAccess, address, setAddress, recent, onInspect, onDeploy, busy, checkAddress, setCheckAddress, checks, onCheck }: { token: TokenInfo | null; tokenAccess: TokenAccess; address: string; setAddress: (v: string) => void; recent: RecentToken[]; onInspect: (v?: string) => void; onDeploy: () => void; busy: string | null; checkAddress: string; setCheckAddress: (v: string) => void; checks: Record<string, boolean> | null; onCheck: () => void }) { | ||
| return <div className="flex flex-col gap-5"><section><div className="flex items-center gap-3"><span className="text-3xl">♢</span><div><Text variant="title2">Policy Viewer</Text><Text variant="body" tone="muted">Inspect any B20 token’s policies and check address authorization.</Text></div></div></section>{!token && !address ? <div className="grid gap-4 md:grid-cols-2"><Card className="flex flex-col bg-white p-5 dark:bg-white/5"><span className="mb-3 w-fit rounded-full bg-bds-blue-0 px-2 py-1 text-[11px] text-base-blue dark:bg-bds-blue-100/40 dark:text-bds-blue-20">No wallet required</span><Text variant="headline">Explore sample token</Text><Text variant="footnote" tone="muted">Inspect a predeployed Asset B20 and learn how its policy scopes are configured.</Text><Button className="mt-5 self-start" variant="outline" onClick={() => onInspect(SAMPLE_TOKEN)} disabled={busy === 'inspect'}>{busy === 'inspect' ? 'Loading…' : 'Explore sample'}</Button></Card><Card className="flex flex-col bg-white p-5 dark:bg-white/5"><span className="mb-3 w-fit rounded-full bg-bds-green-0 px-2 py-1 text-[11px] text-bds-green-70 dark:bg-bds-green-100/40 dark:text-bds-green-20">Interactive</span><Text variant="headline">Create your own token</Text><Text variant="footnote" tone="muted">Deploy an Asset B20, receive its issuer roles, and sign announcements with your wallet.</Text><Button className="mt-5 self-start" onClick={onDeploy}>Create token</Button></Card></div> : null}<Card className="grid overflow-hidden bg-white md:grid-cols-[minmax(0,1fr)_250px] dark:bg-white/5"><div className="p-5"><Field label="Token"><div className="flex gap-2"><Input value={address} onChange={(e) => setAddress(e.target.value)} placeholder="Paste B20 token address" /><Button size="sm" variant="outline" onClick={() => onInspect()} disabled={busy === 'inspect'}>{busy === 'inspect' ? 'Checking…' : 'Check'}</Button></div></Field>{recent.length ? <><p className="mt-4 text-[12px] text-bds-gray-50">or select a recent deployment</p><div className="mt-3 flex flex-wrap gap-2">{recent.map((entry) => <button key={entry.address} type="button" onClick={() => onInspect(entry.address)} className="rounded-lg border border-bds-gray-10 px-3 py-2 text-left text-[12px] hover:border-base-blue dark:border-white/10"><strong className="block text-base-blue">{entry.symbol}</strong>{entry.variant}</button>)}</div></> : <p className="mt-4 text-[12px] text-bds-gray-50">Recent B20 deployments from this wallet appear here.</p>}</div><div className="border-t border-bds-gray-10 bg-bds-gray-5 p-5 md:border-l md:border-t-0 dark:border-white/10 dark:bg-white/[0.03]"><div className="flex items-center gap-1.5"><Text variant="label">Variant</Text><InfoTooltip label="About B20 variants">{B20_HELP.variant}</InfoTooltip></div>{token ? <><p className="mt-6 text-[18px] font-medium capitalize">{token.variant}</p><span className={cn('mt-3 inline-block rounded-full px-2 py-1 text-[11px]', tokenAccess === 'operator' ? 'bg-bds-green-0 text-bds-green-70 dark:bg-bds-green-100/40 dark:text-bds-green-20' : 'bg-bds-gray-10 text-bds-gray-60 dark:bg-white/10 dark:text-bds-gray-30')}>{tokenAccess === 'sample' ? 'Sample token · Read only' : tokenAccess === 'operator' ? 'Your token · OPERATOR_ROLE' : tokenAccess === 'external' ? 'External token · No operator access' : 'Connect wallet to check access'}</span><Link href="https://github.com/base/base-std/tree/main/docs/B20" target="_blank" rel="noreferrer" className="mt-5 block text-[12px] text-base-blue hover:underline">How variants work ↗</Link></> : <p className="mt-3 text-[12px] text-bds-gray-50">Load a token to inspect its variant.</p>}</div></Card>{token ? <><section className="rounded-2xl border border-bds-gray-10 bg-white p-5 dark:border-white/10 dark:bg-white/5"><div className="flex items-center justify-between gap-3"><div><div className="flex items-center gap-1.5"><Text variant="headline">Policy scopes</Text><InfoTooltip label="About policy scopes">{B20_HELP.policyScopes}</InfoTooltip></div><Text variant="footnote" tone="muted">Each scope maps to a Policy Registry entry. Burn is role-gated, not policy-gated.</Text></div><a href="https://github.com/base/base-std/tree/main/docs/PolicyRegistry" target="_blank" rel="noreferrer" className="text-[12px] text-base-blue hover:underline">Learn about scopes ↗</a></div><div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">{token.policies.map((policy) => <div key={policy.scope} className="rounded-xl border border-bds-gray-10 p-4 dark:border-white/10"><div className="flex items-center gap-1.5"><strong className="text-[13px]">{policy.label}</strong>{SCOPE_HELP[policy.scope] ? <InfoTooltip label={`About ${policy.label}`}>{SCOPE_HELP[policy.scope]}</InfoTooltip> : null}</div><div className="mt-4 flex items-center gap-1.5"><p className="text-[11px] text-bds-gray-50">Policy ID</p><InfoTooltip label="About policy ID">{B20_HELP.policyId}</InfoTooltip></div><p className="text-[16px]">{policy.id.toString()}</p><span className={cn('mt-2 inline-flex items-center gap-1 rounded px-2 py-1 text-[11px]', policy.id === 0n ? 'bg-bds-orange-0 text-bds-orange-70 dark:bg-bds-orange-100/40' : policy.exists ? 'bg-bds-green-0 text-bds-green-70 dark:bg-bds-green-100/40' : 'bg-bds-red-0 text-bds-red-70 dark:bg-bds-red-100/40')}>{policy.id === 0n ? 'Wide open' : policy.exists ? 'Configured' : 'Missing policy'}<InfoTooltip label="What this status means">{policy.id === 0n ? B20_HELP.statusWideOpen : policy.exists ? B20_HELP.statusConfigured : B20_HELP.statusMissing}</InfoTooltip></span><div className="mt-3 flex items-center gap-1.5"><p className="text-[11px] text-bds-gray-50">Admin</p><InfoTooltip label="About the policy admin">{B20_HELP.policyAdmin}</InfoTooltip></div><p className="font-mono text-[12px]">{shortAddress(policy.admin)}</p>{checks ? <p className={cn('mt-3 text-[12px]', checks[policy.scope] ? 'text-bds-green-60' : 'text-bds-red-60')}>{checks[policy.scope] ? '◉ Authorized' : '⊗ Blocked'}</p> : null}</div>)}</div></section><Card className="bg-white p-5 dark:bg-white/5"><div className="flex items-center gap-1.5"><Text variant="headline">Check an address</Text><InfoTooltip label="How the check works">{B20_HELP.checkAddress}</InfoTooltip></div><Text variant="footnote" tone="muted">Check the selected address against every displayed Policy Registry entry.</Text><div className="mt-4 flex gap-2"><Input value={checkAddress} onChange={(e) => setCheckAddress(e.target.value)} placeholder="Enter address (0x…)" /><Button size="sm" variant="outline" onClick={onCheck}>Check</Button></div></Card><div className="grid gap-5 lg:grid-cols-2"><Card className="bg-white p-5 dark:bg-white/5"><Text variant="headline">Token details</Text><dl className="mt-4 space-y-3 text-[13px]"><Row label="Address" value={shortAddress(token.address)} /><Row label="Variant" value={token.variant} /><Row label="Decimals" value={String(token.decimals)} /><Row label="Total supply" value={formatAmount(token.supply, token.decimals)} /><Row label="Supply cap" value={token.cap === MAX_SUPPLY_CAP ? 'Unlimited' : formatAmount(token.cap, token.decimals)} /></dl><Link href={`${VIBENET_EXPLORER_PATH}/address/${token.address}`} className="mt-5 inline-block text-[12px] text-base-blue hover:underline">View on Explorer ↗</Link></Card><Card className="bg-white p-5 dark:bg-white/5"><div className="flex items-start justify-between gap-3"><div><Text variant="headline">Read from contract</Text><Text variant="footnote" tone="muted">Raw reads used by this viewer.</Text></div><CopyPromptButton prompt={READ_POLICY_PROMPT} module="policy" /></div><div className="mt-4 space-y-2 font-mono text-[12px] text-bds-gray-60 dark:text-bds-gray-40">{['factory.isB20(address)', 'token.policyId(scope)', 'registry.policyExists(id)', 'registry.policyAdmin(id)', 'registry.isAuthorized(id, account)'].map((item) => <div key={item} className="flex items-center justify-between rounded-lg border border-bds-gray-10 px-3 py-2 dark:border-white/10"><span>{item}</span><span className="text-bds-green-60">Read</span></div>)}</div></Card></div></> : null}</div>; |
There was a problem hiding this comment.
same problem here
There was a problem hiding this comment.
Fixed in ad5a00c — this PolicyModule return is now multi-line/readable.
| } catch (error) { setError(walletErrorMessage(error)); } | ||
| }; | ||
|
|
||
| if (token && tokenAccess === 'sample') return <div className="flex flex-col gap-5"><ModuleHeading icon="▤" title="Memos" description="View bytes32 memos attached to B20 token operations." action={<CopyPromptButton prompt={READ_MEMO_PROMPT} module="memos" />} /><Card className="bg-white p-5 dark:bg-white/5"><div className="flex flex-wrap items-start justify-between gap-3"><div><span className="rounded-full bg-bds-blue-0 px-2 py-1 text-[11px] text-base-blue dark:bg-bds-blue-100/40 dark:text-bds-blue-20">Sample transaction</span><Text className="mt-3" variant="headline">Transfer with memo</Text><Text variant="footnote" tone="muted">A real transaction on the sample token, decoded by the Vibenet explorer.</Text></div><Link href={`${VIBENET_EXPLORER_PATH}/tx/${SAMPLE_MEMO_TX}`} className="text-[12px] text-base-blue hover:underline">View transaction ↗</Link></div><dl className="mt-5 grid gap-3 rounded-xl border border-bds-gray-10 p-4 text-[13px] sm:grid-cols-2 dark:border-white/10"><div><dt className="text-[11px] text-bds-gray-50">Operation</dt><dd className="mt-1 font-mono">transferWithMemo</dd></div><div><dt className="text-[11px] text-bds-gray-50">Amount</dt><dd className="mt-1">0.001 {token.symbol}</dd></div><div><dt className="text-[11px] text-bds-gray-50">Memo</dt><dd className="mt-1 font-medium">sending test</dd></div><div><dt className="text-[11px] text-bds-gray-50">Encoding</dt><dd className="mt-1 font-mono text-[11px]">bytes32</dd></div></dl><div className="mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-bds-gray-10 pt-4 dark:border-white/10"><p className="text-[12px] text-bds-gray-50">Deploy your own token to create memo transactions.</p><Button size="sm" onClick={onDeploy}>Create your own token</Button></div></Card><MemoHistory address={token.address} decimals={token.decimals} symbol={token.symbol} /></div>; |
There was a problem hiding this comment.
Fixed in ad5a00c — MemoModule returns are now multi-line.
| } catch (error) { setError(walletErrorMessage(error)); } | ||
| }; | ||
|
|
||
| if (token && tokenAccess === 'sample') return <div className="flex flex-col gap-5"><ModuleHeading icon="▤" title="Memos" description="View bytes32 memos attached to B20 token operations." action={<CopyPromptButton prompt={READ_MEMO_PROMPT} module="memos" />} /><Card className="bg-white p-5 dark:bg-white/5"><div className="flex flex-wrap items-start justify-between gap-3"><div><span className="rounded-full bg-bds-blue-0 px-2 py-1 text-[11px] text-base-blue dark:bg-bds-blue-100/40 dark:text-bds-blue-20">Sample transaction</span><Text className="mt-3" variant="headline">Transfer with memo</Text><Text variant="footnote" tone="muted">A real transaction on the sample token, decoded by the Vibenet explorer.</Text></div><Link href={`${VIBENET_EXPLORER_PATH}/tx/${SAMPLE_MEMO_TX}`} className="text-[12px] text-base-blue hover:underline">View transaction ↗</Link></div><dl className="mt-5 grid gap-3 rounded-xl border border-bds-gray-10 p-4 text-[13px] sm:grid-cols-2 dark:border-white/10"><div><dt className="text-[11px] text-bds-gray-50">Operation</dt><dd className="mt-1 font-mono">transferWithMemo</dd></div><div><dt className="text-[11px] text-bds-gray-50">Amount</dt><dd className="mt-1">0.001 {token.symbol}</dd></div><div><dt className="text-[11px] text-bds-gray-50">Memo</dt><dd className="mt-1 font-medium">sending test</dd></div><div><dt className="text-[11px] text-bds-gray-50">Encoding</dt><dd className="mt-1 font-mono text-[11px]">bytes32</dd></div></dl><div className="mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-bds-gray-10 pt-4 dark:border-white/10"><p className="text-[12px] text-bds-gray-50">Deploy your own token to create memo transactions.</p><Button size="sm" onClick={onDeploy}>Create your own token</Button></div></Card><MemoHistory address={token.address} decimals={token.decimals} symbol={token.symbol} /></div>; |
There was a problem hiding this comment.
Fixed in ad5a00c — same reformat applied here.
| return <div className="flex flex-col gap-5"><ModuleHeading icon="▤" title="Memos" description="Attach an indexed bytes32 memo to B20 transfers, mints, and burns." action={<CopyPromptButton prompt={READ_MEMO_PROMPT} module="memos" />} /><Card className="bg-white p-5 dark:bg-white/5">{!token ? <EmptyToken /> : <><div className="flex flex-wrap gap-2">{(['transfer', 'transferFrom', 'mint', 'burn'] as const).map((item) => <button key={item} type="button" onClick={() => setKind(item)} className={cn('rounded-full px-3 py-1.5 text-[12px]', kind === item ? 'bg-base-blue text-white' : 'bg-bds-gray-5 text-bds-gray-60 dark:bg-white/10 dark:text-bds-gray-30')}>{item}</button>)}</div><div className="mt-5 grid gap-4 md:grid-cols-2">{kind === 'transferFrom' ? <Field label="From"><Input value={from} onChange={(e) => setFrom(e.target.value)} placeholder="0x…" /></Field> : null}{kind !== 'burn' ? <Field label={kind === 'mint' ? 'Recipient' : 'To'}><Input value={to} onChange={(e) => setTo(e.target.value)} placeholder="0x…" /></Field> : null}<Field label={`Amount (${token.symbol})`}><Input value={value} onChange={(e) => setValue(e.target.value)} placeholder="0.00" inputMode="decimal" /></Field><Field label="Memo" help={B20_HELP.memo}><Input value={memo} onChange={(e) => setMemo(e.target.value)} placeholder="Text up to 32 bytes, or 0x bytes32" /></Field></div><p className="mt-3 font-mono text-[11px] text-bds-gray-50">{memo ? (() => { try { return memoToBytes32(memo); } catch { return 'Memo is too long'; } })() : 'Memo preview appears here'}</p><ErrorNote message={error} /><Button className="mt-5" onClick={() => void submit()} disabled={!!busy}>{busy ? 'Waiting for wallet…' : `Submit ${kind} with memo`}</Button></>}</Card>{token ? <MemoHistory address={token.address} decimals={token.decimals} symbol={token.symbol} /> : null}</div>; | ||
| } | ||
|
|
||
| function SampleAnnouncementViewer({ onDeploy }: { onDeploy: () => void }) { |
There was a problem hiding this comment.
move this to a helper file or config file
There was a problem hiding this comment.
Done in ad5a00c — moved the sample announcement mock data to app/demos/b20/lib/samples.ts (SAMPLE_ANNOUNCEMENTS) and import it here.
| // confirmation — the Activity log alone was too easy to miss. | ||
| const [published, setPublished] = useState<{ id: string; summary: string; hash: Hex } | null>(null); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const loadTemplate = useCallback(() => { setAnnouncementType('multiplier'); setId(`demo-split-${Date.now().toString(36)}`); setDescription('2:1 forward split demonstration'); setUri('https://example.com/disclosures/demo-split'); setMultiplier('2'); setEffectiveAt(futureDatetimeLocal()); }, []); |
There was a problem hiding this comment.
fix the syntax here, there should be a new line after a semi colon.
There was a problem hiding this comment.
Fixed in ad5a00c — chained declarations and multi-statement bodies are now one statement per line throughout the file.
- Break the dense single-line JSX returns and multi-statement lines into readable multi-line form (ran prettier over the file; one statement per line). - Move the sample announcement mock data out of B20Demo.tsx into lib/samples.ts. Formatting-only for the component logic; typecheck, lint, build, and the b20 tests pass, and Policy/Deploy render unchanged (verified in-browser). Co-Authored-By: Claude <noreply@anthropic.com>
The demo was one 2k-line file with every module and primitive inline. Extract them so each concern lives in its own file and B20Demo.tsx is just the orchestrator (state + wallet/inspect/send wiring + layout), 434 lines down from ~2050. New files: - lib/types.ts — Module, TokenAccess, RecentToken, TokenInfo, ActivityItem, CreatedToken - lib/constants.ts — shared viem client, chain id, sample/config constants, MODULES - lib/recent.ts — readRecent/writeRecent localStorage helpers - components/primitives.tsx — Input, Field, ModuleHeading, EmptyToken, ErrorNote, Row - components/Activity.tsx — the decoded-events log - components/PolicyModule.tsx - components/MemoModule.tsx - components/AnnouncementModule.tsx (+ SampleAnnouncementViewer) - components/DeployModule.tsx (+ CreatedView, confetti) MemoHistory now reuses the shared lib/constants client instead of creating its own. Behavior unchanged — formatting-only moves; typecheck, lint, build, and 73 tests pass, and the Policy/Memos/Announcements/Deploy tabs render identically (verified in-browser). Co-Authored-By: Claude <noreply@anthropic.com>
Register base-0005 (B20) in vibenetAvailability with demo: '/demos/b20' so the changelog detail page surfaces the "Test Now" card, mirroring how eip-8130 links to /demos/account. Left unfeatured — the link belongs on the changelog entry, not the featured-changes rail. Typed vibenetAvailability as VibenetAvailability[] (dropping `as const`) so entries can omit optional fields. Extended the upgrades test for the new demo link. Co-Authored-By: Claude <noreply@anthropic.com>
Empty commit to nudge a fresh Vercel preview build for b20-docs-vercel-preview. No source changes. Co-Authored-By: Claude <noreply@anthropic.com>
The Advanced policy settings inputs (Transfer sender, etc.) accept a Policy Registry rule ID (uint64) that updatePolicy references — but the labels + numeric placeholders read like they want a wallet address. Relabel each field "… rule", attach the policyId info tooltip, and spell out in the section intro that these take a rule ID number from the Policy Registry (which holds the allowed addresses), not an address. No behavior change. Co-Authored-By: Claude <noreply@anthropic.com>
Summary
Adds the B20 Playground interactive demo at
/demos/b20— a hands-on tour of Base-native B20 tokens on Vibenet — plus the explorer decoding needed to surface B20 events, and small shared UI primitives.What's included
B20 Playground demo (
app/demos/b20/) — four modules behind a tab bar:bytes32memo to transfers/mints/burns, with an on-chain memo history for the selected token.Structured as a thin
B20Demoorchestrator (state + wallet/inspect/send wiring) that dispatches to per-module components, with sharedlib/(types, constants, viem client, protocol ABIs/helpers, glossary, prompts, samples) and reusablecomponents/primitives.Explorer support (
app/vibenet/)library/explorer.ts— pure decoders for the B20 announcement bracket (Announcement/UIMultiplierUpdated/EndAnnouncement),bytes32memo calldata/events,executeBatch, ERC-20 transfers, and EIP-8130 scope/type formatting.explorer/tx/[hash]/page.tsx— renders those decoded B20 events in the transaction viewer.Shared UI
components/ui/InfoTooltip.tsx— accessible tooltip used for inline concept help across the demo.components/ui/icons.tsx— added icons.components/ui/Modal.tsx— accessibility improvements (labelled dialog, focus move-in/restore).Wiring & docs — demo catalogue entry, sitemap,
globals.cssanimations, and regeneratedllms.txt/AGENTS.md.Testing
app/demos/b20/lib/protocol.test.ts— variant/address parsing, memobytes32round-trip + boundaries, deployment-param ABI encoding,amountvalidation,featureId,formatAmount.app/demos/b20/lib/prompts.test.ts— prompt catalogue integrity.app/vibenet/library/explorer.test.ts— all memo-call shapes, the announcement-bracket events,executeBatchoffset decoding, the EIP-8130 scope bitmask, and number/type formatters (fixtures built with viem encoders so they match on-chain layout).npm run typecheck,npm run lint,npm run build, andnpm test(73 tests) all pass. The four modules were verified in-browser.Notes for reviewers