diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts
index e59c68bfa..cb077c13c 100644
--- a/desktop/playwright.config.ts
+++ b/desktop/playwright.config.ts
@@ -120,6 +120,7 @@ export default defineConfig({
"**/inbox-live-update.spec.ts",
"**/mesh-compute.spec.ts",
"**/observer-archive-policy.spec.ts",
+ "**/wallet-screenshots.spec.ts",
],
use: {
...devices["Desktop Chrome"],
diff --git a/desktop/src/app/AppTopChrome.tsx b/desktop/src/app/AppTopChrome.tsx
index 845c403b8..4b7ec69f9 100644
--- a/desktop/src/app/AppTopChrome.tsx
+++ b/desktop/src/app/AppTopChrome.tsx
@@ -11,6 +11,8 @@ import { useIsFullscreen } from "@/shared/lib/useIsFullscreen";
import { Button } from "@/shared/ui/button";
import { cn } from "@/shared/lib/cn";
import { topChromeBackdrop } from "@/shared/layout/chromeLayout";
+import { FeatureGate } from "@/shared/features";
+import { WalletBalanceChip } from "@/features/wallet/ui/WalletBalanceChip";
import { useOptionalSidebar } from "@/shared/ui/sidebar";
type AppTopChromeProps = {
@@ -131,6 +133,11 @@ export function AppTopChrome({
+
+
+
+
+
);
}
diff --git a/desktop/src/features/wallet/lib/mockWallet.ts b/desktop/src/features/wallet/lib/mockWallet.ts
new file mode 100644
index 000000000..15f13fbc4
--- /dev/null
+++ b/desktop/src/features/wallet/lib/mockWallet.ts
@@ -0,0 +1,111 @@
+// Mock wallet-provider data backing the Lightning wallet UI prototype.
+//
+// The PRD (see #buzz-lightning) defines a wallet-provider abstraction that
+// any conforming Lightning SDK can plug into (Lexe is the v1 target). This
+// module stands in for that provider so the UX can be designed and reviewed
+// before any SDK is integrated. Everything here is static, deterministic
+// fixture data — no keys, no network, no money.
+
+export type WalletTransactionKind = "zap" | "kudos" | "send" | "fund";
+
+export interface WalletTransaction {
+ id: string;
+ direction: "receive" | "send";
+ amountSats: number;
+ counterparty: string;
+ memo: string;
+ /** Pre-rendered relative time label; static so screenshots stay stable. */
+ timeLabel: string;
+ kind: WalletTransactionKind;
+}
+
+export interface WalletSnapshot {
+ providerName: string;
+ balanceSats: number;
+ /** BOLT12 offer — the core send/receive primitive per the PRD. */
+ bolt12Offer: string;
+ /**
+ * BIP-321 funding URI carrying the amountless BOLT12 offer plus a BOLT11
+ * fallback so external wallets without BOLT12 support can still fund.
+ */
+ bip321Uri: string;
+ /** 12-word BIP-39 seed phrase (fixture — never a real key). */
+ seedPhrase: string;
+ transactions: WalletTransaction[];
+}
+
+/** Format sats with the ₿ symbol, per the PRD ("use ₿ instead of sats"). */
+export function formatBtc(sats: number): string {
+ return `₿${sats.toLocaleString("en-US")}`;
+}
+
+const MOCK_BOLT12_OFFER =
+ "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqgn3qzsyvfkx26qkyypvr5hfx4gdhjy7uq9jjjrrjkflcgwzycqqqqqqqqqqqqqqqqqqqq";
+
+const MOCK_BOLT11_FALLBACK =
+ "lnbc1pn9qw2app5x7rlq3mmkwvpksct5v4kxz6ryqdq5g9kxy7fqd9h8vmmfvdjsqqqqqqqqqqqq";
+
+export const MOCK_WALLET: WalletSnapshot = {
+ providerName: "Lexe",
+ balanceSats: 21_450,
+ bolt12Offer: MOCK_BOLT12_OFFER,
+ bip321Uri: `bitcoin:?lno=${MOCK_BOLT12_OFFER}&lightning=${MOCK_BOLT11_FALLBACK}`,
+ seedPhrase:
+ "narrow ensure hollow tribe fiscal vault lyrics aunt shrug wagon rocket kite",
+ transactions: [
+ {
+ id: "tx-01",
+ direction: "receive",
+ amountSats: 100,
+ counterparty: "Eva",
+ memo: "Zap · research synthesis",
+ timeLabel: "12m ago",
+ kind: "zap",
+ },
+ {
+ id: "tx-02",
+ direction: "receive",
+ amountSats: 500,
+ counterparty: "Tyler",
+ memo: "Kudos · shipping the harness fix",
+ timeLabel: "2h ago",
+ kind: "kudos",
+ },
+ {
+ id: "tx-03",
+ direction: "send",
+ amountSats: 250,
+ counterparty: "Dawn",
+ memo: "Shared agent invocation",
+ timeLabel: "5h ago",
+ kind: "send",
+ },
+ {
+ id: "tx-04",
+ direction: "receive",
+ amountSats: 1_000,
+ counterparty: "Mat",
+ memo: "Gifting channel · welcome to #spiral",
+ timeLabel: "1d ago",
+ kind: "fund",
+ },
+ {
+ id: "tx-05",
+ direction: "send",
+ amountSats: 100,
+ counterparty: "Wren",
+ memo: "Zap · rug-test writeup",
+ timeLabel: "2d ago",
+ kind: "zap",
+ },
+ {
+ id: "tx-06",
+ direction: "receive",
+ amountSats: 20_200,
+ counterparty: "External wallet",
+ memo: "Wallet funded via BIP-321",
+ timeLabel: "3d ago",
+ kind: "fund",
+ },
+ ],
+};
diff --git a/desktop/src/features/wallet/ui/WalletBalanceChip.tsx b/desktop/src/features/wallet/ui/WalletBalanceChip.tsx
new file mode 100644
index 000000000..ea5577eeb
--- /dev/null
+++ b/desktop/src/features/wallet/ui/WalletBalanceChip.tsx
@@ -0,0 +1,33 @@
+import { Zap } from "lucide-react";
+
+import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
+import { formatBtc, MOCK_WALLET } from "../lib/mockWallet";
+import { WalletPanel } from "./WalletPanel";
+
+// Top-right wallet affordance: the user's global bitcoin balance, always
+// visible in the app chrome ("running score" per the PRD's top-level balance
+// discussion). Clicking it opens the wallet panel in a popover.
+//
+// Fixed height on purpose: this chip lives in the top chrome beside the
+// px-pinned nav buttons (see `AppTopChrome`), so it follows the same
+// deliberate exception to the rem-first rule.
+export function WalletBalanceChip() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/desktop/src/features/wallet/ui/WalletPanel.tsx b/desktop/src/features/wallet/ui/WalletPanel.tsx
new file mode 100644
index 000000000..dd98c8973
--- /dev/null
+++ b/desktop/src/features/wallet/ui/WalletPanel.tsx
@@ -0,0 +1,306 @@
+import * as React from "react";
+import {
+ ArrowDownLeft,
+ ArrowUpRight,
+ ChevronLeft,
+ Copy,
+ Eye,
+ EyeOff,
+ QrCode,
+ RefreshCw,
+ Send,
+} from "lucide-react";
+
+import { cn } from "@/shared/lib/cn";
+import { Badge } from "@/shared/ui/badge";
+import { Button } from "@/shared/ui/button";
+import { Input } from "@/shared/ui/input";
+import { Separator } from "@/shared/ui/separator";
+import { StyledQrCode } from "@/shared/ui/styled-qr-code";
+import {
+ formatBtc,
+ MOCK_WALLET,
+ type WalletTransaction,
+} from "../lib/mockWallet";
+
+// Wallet panel prototype per the Bitcoin-in-Buzz PRD MVP list: show the
+// provider, show the balance prominently, fund via a BIP-321 QR, send to a
+// BOLT12/BOLT11/offer string, transaction history, and a hidden-by-default
+// seed-phrase reveal. Backed entirely by mock provider data — no funds move.
+
+type WalletView = "overview" | "fund" | "send";
+
+function TransactionRow({ tx }: { tx: WalletTransaction }) {
+ const isReceive = tx.direction === "receive";
+ return (
+
+
+ {isReceive ? (
+
+ ) : (
+
+ )}
+
+
+
{tx.counterparty}
+
{tx.memo}
+
+
+
+ {isReceive ? "+" : "−"}
+ {formatBtc(tx.amountSats)}
+
+
{tx.timeLabel}
+
+
+ );
+}
+
+function PanelHeader({
+ onBack,
+ title,
+}: {
+ onBack?: () => void;
+ title: string;
+}) {
+ return (
+
+ {onBack ? (
+
+ ) : null}
+
{title}
+
+ );
+}
+
+function OverviewView({
+ onFund,
+ onSend,
+}: {
+ onFund: () => void;
+ onSend: () => void;
+}) {
+ const [seedRevealed, setSeedRevealed] = React.useState(false);
+
+ return (
+
+
+
+
+ {MOCK_WALLET.providerName}
+
+
+
+
+
+ {formatBtc(MOCK_WALLET.balanceSats)}
+
+
+ Self-custodial · spans all your communities
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Recent activity
+
+
+
+ {MOCK_WALLET.transactions.map((tx) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ Recovery phrase
+
+
+
+ {seedRevealed ? (
+
+ {MOCK_WALLET.seedPhrase}
+
+ ) : (
+
+ Hidden. Anyone with these 12 words can spend your bitcoin.
+
+ )}
+
+
+ );
+}
+
+function FundView({ onBack }: { onBack: () => void }) {
+ return (
+
+
+
+
+
+
+ Scan with any Lightning wallet. Encodes a reusable BOLT12 offer with a
+ BOLT11 fallback (BIP-321).
+
+
+
+
+
+
+ );
+}
+
+function SendView({ onBack }: { onBack: () => void }) {
+ const [destination, setDestination] = React.useState("");
+ const [amount, setAmount] = React.useState("");
+ const canSend = destination.trim().length > 0 && amount.trim().length > 0;
+
+ return (
+
+
+
+
+ setDestination(event.target.value)}
+ placeholder="lno1… or lnbc1…"
+ value={destination}
+ />
+
+
+
+ setAmount(event.target.value)}
+ placeholder="100"
+ value={amount}
+ />
+
+
+
+ Prototype — no funds move.
+
+
+ );
+}
+
+export function WalletPanel() {
+ const [view, setView] = React.useState("overview");
+ const backToOverview = React.useCallback(() => setView("overview"), []);
+
+ return (
+
+ {view === "overview" ? (
+ setView("fund")}
+ onSend={() => setView("send")}
+ />
+ ) : view === "fund" ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/desktop/tests/e2e/wallet-screenshots.spec.ts b/desktop/tests/e2e/wallet-screenshots.spec.ts
new file mode 100644
index 000000000..18d6bc54b
--- /dev/null
+++ b/desktop/tests/e2e/wallet-screenshots.spec.ts
@@ -0,0 +1,87 @@
+import { expect, test } from "@playwright/test";
+
+import { installMockBridge } from "../helpers/bridge";
+import { waitForAnimations } from "../helpers/animations";
+
+const SHOTS = "test-results/wallet";
+
+// Screenshot evidence for the Lightning wallet prototype: the top-chrome
+// balance chip and each wallet panel view (overview, fund QR, send form).
+// The `wallet` preview feature is seeded on by the mock bridge.
+
+test.describe("wallet prototype screenshots", () => {
+ test.use({ viewport: { width: 1280, height: 900 } });
+
+ async function openApp(page: import("@playwright/test").Page) {
+ await installMockBridge(page);
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ await page.getByTestId("channel-general").click();
+ await expect(page.getByTestId("chat-title")).toHaveText("general");
+ }
+
+ test("01 balance chip in top-right chrome", async ({ page }) => {
+ await openApp(page);
+
+ const chip = page.getByTestId("wallet-balance-chip");
+ await expect(chip).toBeVisible();
+ await expect(chip).toContainText("₿21,450");
+
+ await waitForAnimations(page);
+ await page.screenshot({ path: `${SHOTS}/01-balance-chip.png` });
+ });
+
+ test("02 wallet overview panel", async ({ page }) => {
+ await openApp(page);
+
+ await page.getByTestId("wallet-balance-chip").click();
+ const panel = page.getByTestId("wallet-panel");
+ await expect(panel).toBeVisible();
+ await expect(page.getByTestId("wallet-panel-balance")).toHaveText(
+ "₿21,450",
+ );
+ await expect(page.getByTestId("wallet-provider-badge")).toHaveText("Lexe");
+ await expect(page.getByTestId("wallet-tx-tx-01")).toBeVisible();
+
+ await waitForAnimations(page);
+ await page.screenshot({ path: `${SHOTS}/02-wallet-overview.png` });
+ });
+
+ test("03 seed phrase reveal", async ({ page }) => {
+ await openApp(page);
+
+ await page.getByTestId("wallet-balance-chip").click();
+ await expect(page.getByTestId("wallet-seed-phrase")).toHaveCount(0);
+ await page.getByTestId("wallet-seed-toggle").click();
+ await expect(page.getByTestId("wallet-seed-phrase")).toBeVisible();
+
+ await waitForAnimations(page);
+ await page.screenshot({ path: `${SHOTS}/03-seed-reveal.png` });
+ });
+
+ test("04 fund wallet QR", async ({ page }) => {
+ await openApp(page);
+
+ await page.getByTestId("wallet-balance-chip").click();
+ await page.getByTestId("wallet-fund-button").click();
+ await expect(page.getByTestId("wallet-fund-qr")).toBeVisible();
+
+ await waitForAnimations(page);
+ await page.screenshot({ path: `${SHOTS}/04-fund-qr.png` });
+ });
+
+ test("05 send bitcoin form", async ({ page }) => {
+ await openApp(page);
+
+ await page.getByTestId("wallet-balance-chip").click();
+ await page.getByTestId("wallet-send-button").click();
+
+ const confirm = page.getByTestId("wallet-send-confirm");
+ await expect(confirm).toBeDisabled();
+ await page.getByTestId("wallet-send-destination").fill("lno1qgsq…example");
+ await page.getByTestId("wallet-send-amount").fill("500");
+ await expect(confirm).toBeEnabled();
+
+ await waitForAnimations(page);
+ await page.screenshot({ path: `${SHOTS}/05-send-form.png` });
+ });
+});
diff --git a/preview-features.json b/preview-features.json
index ad8090d5a..87a3aa766 100644
--- a/preview-features.json
+++ b/preview-features.json
@@ -40,6 +40,14 @@
"platforms": [
"desktop"
]
+ },
+ {
+ "id": "wallet",
+ "name": "Bitcoin Wallet",
+ "description": "Self-custodial Lightning wallet prototype: balance in the top chrome, fund, send, and transaction history",
+ "platforms": [
+ "desktop"
+ ]
}
]
}