diff --git a/README.md b/README.md index e3fe4595..82fa3d95 100644 --- a/README.md +++ b/README.md @@ -105,15 +105,16 @@ npm run flowchain:demo npm run flowchain:export ``` -Run the private/local acceptance smoke path when Foundry, Python, Visual Studio -Build Tools C++ workload, dashboard dependencies, and crypto dependencies are -installed: +Run the private/local product testnet acceptance path when Foundry, Python, +Visual Studio Build Tools C++ workload, dashboard dependencies, and crypto +dependencies are installed: ```powershell npm install --prefix apps/dashboard npm install --prefix crypto npm run flowchain:smoke npm run flowchain:full-smoke +npm run flowchain:product-e2e ``` Run the existing dashboard as the local workbench: @@ -141,8 +142,8 @@ npm run read:base-sepolia -- --rpc-url --address { + return collectionFrom(devnetState, [ + "localTestUnitBalances", + "balances", + "accountBalances", + "balanceSheet", + "credits", + "creditBalances", + ]).map((balance, index) => { const id = text(balance.balanceId ?? balance.accountId ?? balance.agentId ?? balance.id, `balance:${index + 1}`); return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { id, - kind: "No-value balance", + kind: "Local test-unit balance", title: id, summary: text(balance.summary, "Local no-value balance or credit metadata exported by the private testnet API."), status: statusFrom(balance.status, "observed"), facts: [ { label: "account", value: text(balance.accountId ?? balance.agentId) }, - { label: "amount", value: text(balance.amount ?? balance.balance ?? balance.credits) }, - { label: "unit", value: text(balance.unit, "no-value local credit") }, - { label: "updated", value: text(balance.updatedAt ?? balance.blockNumber) }, + { label: "owner", value: text(balance.owner ?? balance.controller) }, + { label: "amount", value: text(balance.amount ?? balance.balance ?? balance.credits ?? balance.units) }, + { label: "unit", value: text(balance.unit, "no-value local test unit") }, + { label: "faucet total", value: text(balance.totalFaucetUnits) }, + { label: "updated", value: text(balance.updatedAt ?? balance.updatedAtBlock ?? balance.blockNumber) }, ], raw: balance, }); @@ -778,8 +867,8 @@ function buildBalanceRecords(devnetState: unknown): WorkbenchRecord[] { } function buildFaucetEventRecords(devnetState: unknown): WorkbenchRecord[] { - return collectionFrom(devnetState, ["faucetEvents", "faucetClaims", "faucetCredits", "faucet"]).map((event, index) => { - const id = text(event.eventId ?? event.faucetEventId ?? event.txId ?? event.id, `faucet-event:${index + 1}`); + return collectionFrom(devnetState, ["faucetRecords", "faucetEvents", "faucetClaims", "faucetCredits", "faucet"]).map((event, index) => { + const id = text(event.eventId ?? event.faucetRecordId ?? event.faucetEventId ?? event.txId ?? event.id, `faucet-event:${index + 1}`); return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { id, kind: "Faucet event", @@ -788,8 +877,9 @@ function buildFaucetEventRecords(devnetState: unknown): WorkbenchRecord[] { status: statusFrom(event.status, "observed"), facts: [ { label: "account", value: text(event.accountId ?? event.agentId ?? event.wallet) }, - { label: "amount", value: text(event.amount ?? event.credits) }, - { label: "block", value: text(event.blockNumber) }, + { label: "recipient", value: text(event.recipient) }, + { label: "amount", value: text(event.amount ?? event.amountUnits ?? event.credits) }, + { label: "block", value: text(event.blockNumber ?? event.creditedAtBlock) }, { label: "created", value: text(event.createdAt ?? event.timestamp) }, ], raw: event, @@ -798,11 +888,11 @@ function buildFaucetEventRecords(devnetState: unknown): WorkbenchRecord[] { } function buildWalletMetadataRecords(devnetState: unknown): WorkbenchRecord[] { - return collectionFrom(devnetState, ["wallets", "walletMetadata", "publicWallets", "operatorKeyReferences"]).map((wallet, index) => { + return collectionFrom(devnetState, ["wallets", "walletMetadata", "publicWallets", "publicAccounts", "operatorKeyReferences"]).map((wallet, index) => { const id = text(wallet.walletId ?? wallet.keyReferenceId ?? wallet.operatorId ?? wallet.id, `wallet:${index + 1}`); return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { id, - kind: "Public wallet metadata", + kind: "Public wallet/account metadata", title: id, summary: text( wallet.secretMaterialBoundary ?? wallet.summary, @@ -821,26 +911,167 @@ function buildWalletMetadataRecords(devnetState: unknown): WorkbenchRecord[] { }); } -function buildBridgeRecords(devnetState: unknown, kind: "deposits" | "credits" | "withdrawals"): WorkbenchRecord[] { +function buildTokenLaunchRecords(devnetState: unknown): WorkbenchRecord[] { + return collectionFrom(devnetState, ["tokenLaunches", "tokenDefinitions", "tokens", "localTokens", "launchedTokens"]).map((token, index) => { + const id = text(token.tokenId ?? token.launchId ?? token.id ?? token.symbol, `token-launch:${index + 1}`); + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "Token launch", + title: text(token.symbol ?? token.name ?? id), + summary: text( + token.summary, + "Local/testnet token definition exported for the Product Testnet V1 token-launch surface.", + ), + status: token.active === false ? "stale" : statusFrom(token.status, token.active === true ? "verified" : "observed"), + facts: [ + { label: "token id", value: id }, + { label: "name", value: text(token.name) }, + { label: "symbol", value: text(token.symbol) }, + { label: "issuer", value: text(token.issuer ?? token.owner ?? token.creator) }, + { label: "supply", value: text(token.initialSupply ?? token.supply ?? token.totalSupply) }, + { label: "block", value: text(token.blockNumber ?? token.launchedAtBlock) }, + ], + raw: token, + }); + }); +} + +function buildTokenBalanceRecords(devnetState: unknown): WorkbenchRecord[] { + return collectionFrom(devnetState, [ + "tokenBalances", + "tokenAccountBalances", + "accountTokenBalances", + "tokenLedger", + "tokenHoldings", + ]).map((balance, index) => { + const id = text( + balance.balanceId ?? balance.tokenBalanceId ?? balance.id ?? `${text(balance.accountId)}:${text(balance.tokenId ?? balance.symbol)}`, + `token-balance:${index + 1}`, + ); + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "Token balance", + title: id, + summary: text(balance.summary, "Local/testnet token balance exported by the runtime/control-plane."), + status: statusFrom(balance.status, "observed"), + facts: [ + { label: "account", value: text(balance.accountId ?? balance.owner ?? balance.wallet) }, + { label: "token", value: text(balance.tokenId ?? balance.symbol) }, + { label: "amount", value: text(balance.amount ?? balance.balance ?? balance.units) }, + { label: "locked", value: text(balance.locked ?? balance.reserved, "0") }, + { label: "updated", value: text(balance.updatedAt ?? balance.updatedAtBlock ?? balance.blockNumber) }, + ], + raw: balance, + }); + }); +} + +function buildDexPoolRecords(devnetState: unknown): WorkbenchRecord[] { + return collectionFrom(devnetState, ["dexPools", "pools", "ammPools", "liquidityPools"]).map((pool, index) => { + const id = text(pool.poolId ?? pool.id ?? `${text(pool.baseToken ?? pool.tokenA)}:${text(pool.quoteToken ?? pool.tokenB)}`, `pool:${index + 1}`); + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "DEX pool", + title: id, + summary: text(pool.summary, "Local/testnet DEX pool exported by the Product Testnet V1 runtime."), + status: statusFrom(pool.status, pool.active === false ? "stale" : "observed"), + facts: [ + { label: "base", value: text(pool.baseToken ?? pool.tokenA) }, + { label: "quote", value: text(pool.quoteToken ?? pool.tokenB) }, + { label: "reserve base", value: text(pool.reserveBase ?? pool.reserveA) }, + { label: "reserve quote", value: text(pool.reserveQuote ?? pool.reserveB) }, + { label: "lp supply", value: text(pool.lpSupply ?? pool.totalShares) }, + { label: "fee bps", value: text(pool.feeBps ?? pool.fee, "local default") }, + ], + raw: pool, + }); + }); +} + +function buildLiquidityPositionRecords(devnetState: unknown): WorkbenchRecord[] { + return collectionFrom(devnetState, [ + "liquidityPositions", + "lpPositions", + "positions", + "liquidityEvents", + "liquidityReceipts", + ]).map((position, index) => { + const id = text(position.positionId ?? position.lpPositionId ?? position.id, `liquidity:${index + 1}`); + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "Liquidity position", + title: id, + summary: text(position.summary, "Local/testnet liquidity position or liquidity receipt."), + status: statusFrom(position.status, "observed"), + facts: [ + { label: "owner", value: text(position.owner ?? position.accountId ?? position.wallet) }, + { label: "pool", value: text(position.poolId) }, + { label: "shares", value: text(position.shares ?? position.lpTokens) }, + { label: "amount base", value: text(position.amountBase ?? position.amountA) }, + { label: "amount quote", value: text(position.amountQuote ?? position.amountB) }, + { label: "block", value: text(position.blockNumber ?? position.updatedAtBlock) }, + ], + raw: position, + }); + }); +} + +function buildSwapRecords(devnetState: unknown): WorkbenchRecord[] { + return collectionFrom(devnetState, ["swaps", "swapReceipts", "swapEvents", "dexSwaps"]).map((swap, index) => { + const id = text(swap.swapId ?? swap.receiptId ?? swap.txId ?? swap.id, `swap:${index + 1}`); + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "Swap", + title: id, + summary: text(swap.summary, "Local/testnet swap receipt exported by the DEX runtime/control-plane."), + status: statusFrom(swap.status, "observed"), + facts: [ + { label: "trader", value: text(swap.trader ?? swap.accountId ?? swap.wallet) }, + { label: "pool", value: text(swap.poolId) }, + { label: "token in", value: text(swap.tokenIn) }, + { label: "amount in", value: text(swap.amountIn) }, + { label: "token out", value: text(swap.tokenOut) }, + { label: "amount out", value: text(swap.amountOut) }, + ], + raw: swap, + }); + }); +} + +function bridgeFixtureRecords(kind: "deposits" | "credits" | "withdrawals", bridgeTestDeposit: unknown | null): UnknownRecord[] { + if (kind !== "deposits" || !isRecord(bridgeTestDeposit)) { + return []; + } + + return [bridgeTestDeposit]; +} + +function buildBridgeRecords( + devnetState: unknown, + kind: "deposits" | "credits" | "withdrawals", + bridgeTestDeposit: unknown | null = null, +): WorkbenchRecord[] { const keyMap = { deposits: ["bridgeDeposits", "deposits"], credits: ["bridgeCredits", "bridgeCreditEvents"], withdrawals: ["bridgeWithdrawals", "withdrawals"], } satisfies Record; - return collectionFrom(devnetState, keyMap[kind]).map((event, index) => { + return [...collectionFrom(devnetState, keyMap[kind]), ...bridgeFixtureRecords(kind, bridgeTestDeposit)].map((event, index) => { const id = text(event.bridgeEventId ?? event.depositId ?? event.creditId ?? event.withdrawalId ?? event.id, `bridge-${kind}:${index + 1}`); - return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + const fixturePath = event === bridgeTestDeposit ? WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH : WORKBENCH_DEVNET_STATE_PATH; + return makeRecord("devnet", fixturePath, { id, kind: `Bridge ${kind.slice(0, -1)}`, title: id, - summary: text(event.summary, "Private/local bridge lifecycle object exported by the control-plane."), + summary: text(event.summary, "Private/local or Base Sepolia bridge lifecycle test object exported for workbench inspection."), status: statusFrom(event.status, "pending"), facts: [ - { label: "account", value: text(event.accountId ?? event.wallet ?? event.recipient) }, + { label: "account", value: text(event.accountId ?? event.wallet ?? event.recipient ?? event.flowchainRecipient) }, { label: "amount", value: text(event.amount) }, - { label: "source", value: text(event.sourceChain ?? event.fromChain) }, + { label: "source", value: text(event.sourceChain ?? event.sourceChainId ?? event.fromChain) }, { label: "destination", value: text(event.destinationChain ?? event.toChain) }, + { label: "tx hash", value: text(event.txHash) }, { label: "block", value: text(event.blockNumber) }, ], raw: event, @@ -951,6 +1182,29 @@ function buildTransactionRecords(data: DashboardData, devnetState: unknown): Wor ); } +function buildExplorerRecords(data: DashboardData, devnetState: unknown, bridgeTestDeposit: unknown | null): WorkbenchRecord[] { + const explorerRecords = [ + ...buildBlockRecords(data, devnetState).slice(0, 4), + ...buildTransactionRecords(data, devnetState).slice(0, 8), + ...buildReceiptRecords(data, devnetState).slice(0, 6), + ...buildTokenLaunchRecords(devnetState).slice(0, 4), + ...buildTokenBalanceRecords(devnetState).slice(0, 4), + ...buildDexPoolRecords(devnetState).slice(0, 4), + ...buildLiquidityPositionRecords(devnetState).slice(0, 4), + ...buildSwapRecords(devnetState).slice(0, 4), + ...buildBridgeRecords(devnetState, "deposits", bridgeTestDeposit).slice(0, 4), + ...buildBridgeRecords(devnetState, "credits", bridgeTestDeposit).slice(0, 4), + ...buildBridgeRecords(devnetState, "withdrawals", bridgeTestDeposit).slice(0, 4), + ]; + + return explorerRecords.map((record) => ({ + ...record, + id: `explorer:${record.kind}:${record.id}`, + kind: `Explorer ${record.kind}`, + summary: `Explorer index projection: ${record.summary}`, + })); +} + function buildRootfieldRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { const devnetRootfields = collectionFrom(devnetState, ["rootfields", "rootfieldState", "rootfieldsById"]).map((rootfield, index) => { const id = text(rootfield.rootfieldId ?? rootfield.id, `rootfield:${index + 1}`); @@ -1423,6 +1677,7 @@ function buildRawJsonRecords( controlPlane: ControlPlaneProbe, devnetState: unknown | null, devnetDashboardState: unknown | null, + bridgeTestDeposit: unknown | null, ): WorkbenchRecord[] { return [ makeRecord("indexer", data.metadata.fixturePath, { @@ -1464,6 +1719,20 @@ function buildRawJsonRecords( ], raw: devnetDashboardState, }), + makeRecord("devnet", WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH, { + id: "raw-bridge-test-deposit", + kind: "Raw JSON", + title: WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH, + summary: bridgeTestDeposit + ? "Bridge test-deposit fixture loaded for the local/testnet bridge record surface." + : "Bridge test-deposit fixture was not loaded.", + status: bridgeTestDeposit ? "verified" : "unresolved", + facts: [ + { label: "schema", value: isRecord(bridgeTestDeposit) ? text(bridgeTestDeposit.schema) : "missing" }, + { label: "keys", value: topLevelKeys(bridgeTestDeposit) }, + ], + raw: bridgeTestDeposit, + }), makeLocalRecord( "indexer", controlPlane.url, @@ -1498,6 +1767,7 @@ function buildProvenanceRecords( controlPlane: ControlPlaneProbe, devnetState: unknown | null, devnetDashboardState: unknown | null, + bridgeTestDeposit: unknown | null, ): WorkbenchRecord[] { return [ makeLocalRecord( @@ -1561,6 +1831,20 @@ function buildProvenanceRecords( ], raw: devnetDashboardState, }), + makeRecord("devnet", WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH, { + id: "bridge-test-deposit-fixture", + kind: "Bridge fixture", + title: WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH, + summary: bridgeTestDeposit + ? "Existing bridge test-deposit fixture is available for bridge/explorer views." + : "Bridge test-deposit fixture was not loaded.", + status: bridgeTestDeposit ? "verified" : "unresolved", + facts: [ + { label: "schema", value: isRecord(bridgeTestDeposit) ? text(bridgeTestDeposit.schema) : "missing" }, + { label: "source", value: "fixtures/bridge/test deposit runtime copy" }, + ], + raw: bridgeTestDeposit, + }), ]; } @@ -1636,6 +1920,7 @@ export function buildWorkbenchSnapshot( controlPlane?: ControlPlaneProbe; devnetState?: unknown | null; devnetDashboardState?: unknown | null; + bridgeTestDeposit?: unknown | null; loadIssues?: string[]; } = {}, ): WorkbenchSnapshot { @@ -1650,6 +1935,7 @@ export function buildWorkbenchSnapshot( } satisfies ControlPlaneProbe); const controlPlaneState = extractControlPlaneState(controlPlane.state); const activeDevnetState = controlPlaneState ?? options.devnetState ?? null; + const bridgeTestDeposit = options.bridgeTestDeposit ?? null; const source: WorkbenchSource = controlPlane.status === "available" && controlPlaneState ? "control-plane" : "fixture-fallback"; const sections: Record = { @@ -1662,6 +1948,12 @@ export function buildWorkbenchSnapshot( balances: buildBalanceRecords(activeDevnetState), faucetEvents: buildFaucetEventRecords(activeDevnetState), walletMetadata: buildWalletMetadataRecords(activeDevnetState), + tokenLaunches: buildTokenLaunchRecords(activeDevnetState), + tokenBalances: buildTokenBalanceRecords(activeDevnetState), + dexPools: buildDexPoolRecords(activeDevnetState), + liquidityPositions: buildLiquidityPositionRecords(activeDevnetState), + swaps: buildSwapRecords(activeDevnetState), + explorerRecords: buildExplorerRecords(data, activeDevnetState, bridgeTestDeposit), rootfields: buildRootfieldRecords(data, activeDevnetState), agents: buildAgentRecords(data, activeDevnetState), models: buildModelRecords(activeDevnetState), @@ -1672,16 +1964,28 @@ export function buildWorkbenchSnapshot( verifierReports: buildVerifierRecords(data, activeDevnetState), challenges: buildChallengeRecords(activeDevnetState), finality: buildFinalityRecords(data, activeDevnetState), - bridgeDeposits: buildBridgeRecords(activeDevnetState, "deposits"), - bridgeCredits: buildBridgeRecords(activeDevnetState, "credits"), - bridgeWithdrawals: buildBridgeRecords(activeDevnetState, "withdrawals"), + bridgeDeposits: buildBridgeRecords(activeDevnetState, "deposits", bridgeTestDeposit), + bridgeCredits: buildBridgeRecords(activeDevnetState, "credits", bridgeTestDeposit), + bridgeWithdrawals: buildBridgeRecords(activeDevnetState, "withdrawals", bridgeTestDeposit), provenance: [], hardwareSignals: buildHardwareSignalRecords(data, activeDevnetState), rawJson: [], }; - sections.provenance = buildProvenanceRecords(data, controlPlane, options.devnetState ?? null, options.devnetDashboardState ?? null); - sections.rawJson = buildRawJsonRecords(data, controlPlane, options.devnetState ?? null, options.devnetDashboardState ?? null); + sections.provenance = buildProvenanceRecords( + data, + controlPlane, + options.devnetState ?? null, + options.devnetDashboardState ?? null, + bridgeTestDeposit, + ); + sections.rawJson = buildRawJsonRecords( + data, + controlPlane, + options.devnetState ?? null, + options.devnetDashboardState ?? null, + bridgeTestDeposit, + ); const displayedSections = source === "control-plane" ? relabelDevnetRecordsAsControlPlane(sections, controlPlane) : sections; return { @@ -1697,6 +2001,7 @@ export function buildWorkbenchSnapshot( dashboard: data, devnetState: options.devnetState ?? null, devnetDashboardState: options.devnetDashboardState ?? null, + bridgeTestDeposit, controlPlaneHealth: controlPlane.health ?? null, controlPlaneState: controlPlane.state ?? null, }, @@ -1704,12 +2009,13 @@ export function buildWorkbenchSnapshot( } export async function fetchWorkbenchSnapshot(data: DashboardData): Promise { - const [controlPlane, devnetStateResult, devnetDashboardStateResult] = await Promise.all([ + const [controlPlane, devnetStateResult, devnetDashboardStateResult, bridgeTestDepositResult] = await Promise.all([ probeControlPlane(), fetchOptionalJson(WORKBENCH_DEVNET_STATE_PATH), fetchOptionalJson(WORKBENCH_DEVNET_DASHBOARD_STATE_PATH), + fetchOptionalJson(WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH), ]); - const loadIssues = [devnetStateResult.error, devnetDashboardStateResult.error].filter( + const loadIssues = [devnetStateResult.error, devnetDashboardStateResult.error, bridgeTestDepositResult.error].filter( (issue): issue is string => typeof issue === "string" && issue.length > 0, ); @@ -1717,6 +2023,7 @@ export async function fetchWorkbenchSnapshot(data: DashboardData): Promise { const workbench = buildWorkbenchSnapshot(data, { devnetState, devnetDashboardState, + bridgeTestDeposit, }); expect(workbench.source).toBe("fixture-fallback"); @@ -132,9 +135,16 @@ describe("dashboard fixture", () => { expect(workbench.sections.rawJson.map((record) => record.id)).toContain("raw-dashboard-fixture"); expect(workbench.sections.models.length).toBeGreaterThan(0); expect(workbench.sections.challenges.length).toBeGreaterThan(0); - expect(workbench.sections.bridgeDeposits).toHaveLength(0); + expect(workbench.sections.balances.length).toBeGreaterThan(0); + expect(workbench.sections.tokenLaunches).toHaveLength(0); + expect(workbench.sections.tokenBalances).toHaveLength(0); + expect(workbench.sections.dexPools).toHaveLength(0); + expect(workbench.sections.liquidityPositions).toHaveLength(0); + expect(workbench.sections.swaps).toHaveLength(0); + expect(workbench.sections.bridgeDeposits.length).toBeGreaterThan(0); expect(workbench.sections.bridgeCredits).toHaveLength(0); expect(workbench.sections.bridgeWithdrawals).toHaveLength(0); + expect(workbench.sections.explorerRecords.length).toBeGreaterThan(0); expect(workbench.node.status).toBe("offline"); expect(workbench.actions).toEqual([]); @@ -197,6 +207,9 @@ describe("dashboard fixture", () => { if (url === WORKBENCH_DEVNET_DASHBOARD_STATE_PATH) { return Response.json(devnetDashboardState); } + if (url === WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH) { + return Response.json(bridgeTestDeposit); + } return new Response("not found", { status: 404 }); }); @@ -208,15 +221,18 @@ describe("dashboard fixture", () => { expect(workbench.raw.controlPlaneHealth).toEqual({ status: "ok" }); expect(workbench.raw.controlPlaneState).toEqual({ state: devnetState }); expect(workbench.raw.devnetState).toEqual(devnetState); + expect(workbench.raw.bridgeTestDeposit).toEqual(bridgeTestDeposit); expect(workbench.loadIssues).toEqual([]); expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:8787/health", expect.any(Object)); expect(fetchMock).toHaveBeenCalledWith(WORKBENCH_DEVNET_STATE_PATH, expect.any(Object)); + expect(fetchMock).toHaveBeenCalledWith(WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH, expect.any(Object)); }); it("renders the critical workbench view labels from fixture fallback", () => { const workbench = buildWorkbenchSnapshot(data, { devnetState, devnetDashboardState, + bridgeTestDeposit, }); const html = renderToStaticMarkup(createElement(WorkbenchView, { data, workbench })); @@ -224,7 +240,14 @@ describe("dashboard fixture", () => { expect(html).toContain("Node and API status"); expect(html).toContain("Control-plane offline"); expect(html).toContain("Wallet Metadata"); + expect(html).toContain("Token Launch"); + expect(html).toContain("Token Balances"); + expect(html).toContain("DEX Pools"); + expect(html).toContain("Liquidity"); + expect(html).toContain("Swaps"); + expect(html).toContain("Explorer Records"); expect(html).toContain("Bridge Deposits"); + expect(html).toContain("private keys in browser localStorage"); expect(html).toContain("Rootfields"); expect(html).toContain("Verifier Modules"); expect(html).toContain("Hardware Signals"); diff --git a/apps/dashboard/src/views/WorkbenchView.tsx b/apps/dashboard/src/views/WorkbenchView.tsx index 4be659b4..8fdd4cc0 100644 --- a/apps/dashboard/src/views/WorkbenchView.tsx +++ b/apps/dashboard/src/views/WorkbenchView.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import { Activity, Database, Network, PlayCircle, RefreshCw, Search, Server, Terminal } from "lucide-react"; +import { Activity, Coins, Database, ListChecks, Network, PlayCircle, RefreshCw, Repeat2, Search, Server, ShieldAlert, Terminal, Wallet } from "lucide-react"; import { EmptyState } from "../components/EmptyState"; import { HashValue } from "../components/HashValue"; import { ProvenanceLine } from "../components/ProvenanceLine"; @@ -41,6 +41,10 @@ function missingStateDetail(activeDefinition: (typeof WORKBENCH_SECTIONS)[number return `${activeDefinition.missingService} did not provide records for ${activeDefinition.expectedEndpoint}. Run ${activeDefinition.missingCommand} locally, then refresh this dashboard.`; } +function statusForCount(count: number): DashboardStatus { + return count > 0 ? "verified" : "pending"; +} + export function WorkbenchView({ data, workbench, onRefresh }: WorkbenchViewProps) { const [activeSection, setActiveSection] = useState(DEFAULT_SECTION); const [query, setQuery] = useState(""); @@ -52,6 +56,65 @@ export function WorkbenchView({ data, workbench, onRefresh }: WorkbenchViewProps [activeRecords, query], ); const sourceStatus: DashboardStatus = workbench.source === "control-plane" ? "verified" : "stale"; + const bridgeRecordCount = + workbench.sections.bridgeDeposits.length + workbench.sections.bridgeCredits.length + workbench.sections.bridgeWithdrawals.length; + const productSurfaces: Array<{ + key: WorkbenchSectionKey; + label: string; + detail: string; + command: string; + count: number; + Icon: typeof Wallet; + }> = [ + { + key: "walletMetadata", + label: "Wallet public state", + detail: "Public account/key references only. Signing secrets stay outside browser storage.", + command: "npm run flowchain:init", + count: workbench.sections.walletMetadata.length + workbench.sections.accounts.length, + Icon: Wallet, + }, + { + key: "balances", + label: "Local balances", + detail: "No-value local test units from faucet or bridge-credit flows.", + command: "npm run flowchain:faucet", + count: workbench.sections.balances.length + workbench.sections.faucetEvents.length, + Icon: Coins, + }, + { + key: "tokenLaunches", + label: "Token launch", + detail: "Local/testnet token definitions and launch receipts.", + command: "npm run flowchain:product-e2e", + count: workbench.sections.tokenLaunches.length + workbench.sections.tokenBalances.length, + Icon: ListChecks, + }, + { + key: "dexPools", + label: "DEX pools", + detail: "Pool reserves, liquidity positions, and swap receipt visibility.", + command: "npm run flowchain:product-e2e", + count: workbench.sections.dexPools.length + workbench.sections.liquidityPositions.length + workbench.sections.swaps.length, + Icon: Repeat2, + }, + { + key: "explorerRecords", + label: "Explorer records", + detail: "Blocks, transactions, receipts, token, DEX, and bridge rollups.", + command: "npm run flowchain:product-e2e", + count: workbench.sections.explorerRecords.length, + Icon: Database, + }, + { + key: "bridgeDeposits", + label: "Bridge records", + detail: "Local/Anvil/Base Sepolia test records only; real-funds bridge remains blocked.", + command: "npm run bridge:local-credit:smoke", + count: bridgeRecordCount, + Icon: ShieldAlert, + }, + ]; const runLocalAction = async (endpoint: string, label: string) => { const [method, path] = endpoint.split(/\s+/, 2); @@ -73,7 +136,7 @@ export function WorkbenchView({ data, workbench, onRefresh }: WorkbenchViewProps