Release: develop -> main - #163
Merged
Merged
Conversation
The node now exposes a normalised `bitcoin_network` enum in `GET /api/info` (`'mainnet' | 'mutinynet'`, lower-case, derived server-side from `is_mainnet` — zk-coins/node#193), alongside the existing free-text `network` display string. The faucet guard previously branched on `network`, which the node ships as a capitalised display string (`"Mainnet"`). The original `network !== 'mainnet'` compare therefore always evaluated true and rendered the faucet on PRD; it had been patched defensively with a `.toLowerCase()` at the call site, but the underlying field is still free-text and casing-fragile. Branch the guard on the typed `bitcoin_network` enum instead — a closed set with stable casing, so there is no case-mismatch risk. The field is parsed as `.optional()` (fail-open) so an app talking to a pre-#193 node that ships only `network` still parses; in that case the guard falls back to the lower-cased free-text compare, keeping the faucet hidden on a legacy mainnet node and visible on testnet (the path DEV runs today). - schemas: add `bitcoin_network: z.enum(['mainnet','mutinynet']).optional()` - network store: add `bitcoinNetwork` + `setBitcoinNetwork` - WalletScreen: hydrate the field from `/api/info`, branch `showFaucet` on it with a free-text fallback for pre-#193 nodes - tests: schema parses with/without the field and rejects out-of-enum values; store setter; faucet gating across typed + fallback paths
* fix(network): consume typed bitcoin_network from /api/info The node now exposes a normalised `bitcoin_network` enum in `GET /api/info` (`'mainnet' | 'mutinynet'`, lower-case, derived server-side from `is_mainnet` — zk-coins/node#193), alongside the existing free-text `network` display string. The faucet guard previously branched on `network`, which the node ships as a capitalised display string (`"Mainnet"`). The original `network !== 'mainnet'` compare therefore always evaluated true and rendered the faucet on PRD; it had been patched defensively with a `.toLowerCase()` at the call site, but the underlying field is still free-text and casing-fragile. Branch the guard on the typed `bitcoin_network` enum instead — a closed set with stable casing, so there is no case-mismatch risk. The field is parsed as `.optional()` (fail-open) so an app talking to a pre-#193 node that ships only `network` still parses; in that case the guard falls back to the lower-cased free-text compare, keeping the faucet hidden on a legacy mainnet node and visible on testnet (the path DEV runs today). - schemas: add `bitcoin_network: z.enum(['mainnet','mutinynet']).optional()` - network store: add `bitcoinNetwork` + `setBitcoinNetwork` - WalletScreen: hydrate the field from `/api/info`, branch `showFaucet` on it with a free-text fallback for pre-#193 nodes - tests: schema parses with/without the field and rejects out-of-enum values; store setter; faucet gating across typed + fallback paths * feat(api): migrate client to async Jobs API (mint/send/commit) Replace the removed synchronous /api/{mint,send,commit} routes with the node's async Jobs API (zk-coins/node#161): - mint: POST /api/jobs/mint (mandatory Idempotency-Key) -> poll to completed. - send: balance hydrate (num_sends) -> sign -> POST /api/jobs/send -> poll to awaiting_signature -> read account_state_hash/output_coins_root from the JSON result (no binary CoinProof decode; hard-fail if absent) -> build the commitment via WASM -> POST /api/jobs/:id/commit -> poll to completed. - Add getJob/waitForJob/commitJob primitives, newIdempotencyKey (WebCrypto), and a typed JobFailedError. Retry-After backoff with a poll floor. - Add Job schemas (JobAccepted/JobStatus/JobResult/JobError) mirroring the SDK; keep bitcoin_network (#157), balance/info/username untouched; drop the dead sync Send/Mint/Commit schemas. Thin-client + no-fallback per CONTRIBUTING: every signed send re-fetches num_sends from the node before signing; a malformed awaiting_signature result fails hard rather than fabricating a commitment. * feat(send): drive the send page through the Jobs-API lifecycle Replace the sync api.send + api.commit pipeline with a single api.send call that runs the enqueue -> awaiting_signature -> commit -> completed lifecycle, surfacing the job's phase transitions as an inline progress label. Drop the localStorage in-flight-commit crash-recovery path: it replayed a bare commit payload against the removed synchronous /api/commit, and an async commit is keyed by a live awaiting_signature job id that cannot be reconstructed from a reload. Thin-client balance hydration stays inside api.send. * test(e2e): point the harness at the Jobs API - _helpers/api.ts mint() admits POST /api/jobs/mint (Idempotency-Key) and polls to completed so globalSetup can still seed Alice. - 13-send-server-errors mocks the /api/jobs/send admit route with the {error} envelope; it asserts the admit-time failure -> ApiError -> toast pipeline and so does not depend on the awaiting_signature proof path. - Drop 07-send recovering-banner (the in-flight-commit feature it covered was removed with the migration). - Update README endpoint references. * test(unit): cover the Jobs-API client + send lifecycle - client.test.ts: mint/send job lifecycles incl. awaiting_signature -> commit, Idempotency-Key, num_sends hydration / prev_commitment_pubkey, Retry-After backoff, and the no-fabrication hard-fail when ash/ocr are absent. - send-pipeline.test.tsx: rewritten over api.send (happy path, username resolution, error surfacing, defensive branches). - contract.live.test.ts: mint probe asserts job completion. - Remove client-signing.test.ts (covered the deleted api.sendSigned). Holds the strict src/lib/** 100% coverage gate. * test(e2e): exempt transient send-phase label from button audit The send page's inline send-job phase label (queued/proving/ awaiting_signature/broadcasting) renders only while a send is in flight and flips faster than Playwright can reliably catch — same class as the already-exempt seed-creating/import loading states. The phase transitions are covered by the client lifecycle unit tests' onPhase assertions. * build(deps): vendor @zkcoins/sdk 0.3.1 as a file: tarball The app now consumes @zkcoins/sdk for all node API traffic. The SDK is not published to npm, and the app CI checkout does not include the sdk sister repo, so the built tarball is vendored under vendor/ and referenced as a file: dependency. npm ci resolves it from the app repo alone — no sister directory required. * refactor(api): consume @zkcoins/sdk instead of direct API calls Replace the app-owned REST layer with the SDK: - src/lib/api/client.ts is now a thin adapter over the SDK's ZkCoinsClient. Every /api/* round-trip goes through the SDK; no direct fetch('/api...') remains in app code. The send/mint lifecycle composes the SDK's REST primitives with the existing WASM crypto for the local signing steps (Schnorr sign + commitment), and uses the SDK's buildSendMessage / buildClaimMessage for the signed-message byte layout. ApiError / JobFailedError are re-exported from the SDK. - Delete src/lib/api/schemas.ts — the wire schemas now come from the SDK (BalanceResponse, InfoResponse, JobStatus, Capabilities, ...). - capabilities store uses the SDK Capabilities type (adds multi_asset). - send page reads JobFailedError.serverError (SDK property name). Crypto choice: REST moves to the SDK, WASM stays for local signing — the SDK's @noble crypto is byte-equivalent to the WASM, but keeping the WASM path is the minimal-invasive change that preserves the exact send/commit correctness the browser bundle and E2E already exercise. * test(e2e): drive the harness through the SDK + add local-node proxy - e2e/_helpers/api.ts uses the SDK's ZkCoinsClient (info/balance/mint) so the test-side and app-side wire contracts cannot drift. - next.config.js gains an env-gated (LOCAL_NODE_PROXY_TARGET) same-origin rewrite to a local zkCoins node. Unset in CI and the Docker image, so production builds never proxy; it exists only for the local send-leg E2E run against http://127.0.0.1:4242. * test(unit): align fixtures with the SDK wire contract - Import wire schemas from @zkcoins/sdk (BalanceResponseSchema etc.). - num_sends is now required on BalanceResponse; capabilities carry multi_asset — update the affected mocks. - JobFailedError exposes serverError (not detail); ApiError.message is 'zkCoins API error ...' — update the assertions. - Add an api.sendJob direct test (the wrapper is no longer reached via api.send, which calls the SDK client directly) to keep src/lib at 100%. - Recalibrate the global statement floor to 84 after removing the 100%-covered app schemas.ts; src/lib/** and src/stores/** stay at a strict 100% on every axis. * test(e2e): dynamic E2E target — CI vs dev, local vs local node (#159) * test(e2e): add dynamic local target (local node) alongside dev Introduce an `E2E_TARGET` switch in playwright.config so the same spec files and the same `*-chromium-linux.png` baselines run in two modes: - dev (default, CI): unchanged — hosted DEV stack (dev.zkcoins.app / dev-api.zkcoins.app), no webServer. E2E_TARGET unset reproduces the historical behaviour byte-for-byte, so ci.yaml is untouched. - local: a locally-served standalone PR build + a local zkCoins node, driven by `npm run test:e2e:local` → scripts/e2e-local.sh. Local mode runs entirely inside a Linux Playwright container pinned to the exact @playwright/test version (the baselines are platform-specific *-chromium-linux.png). The script bakes the same-origin proxy config at build time (NEXT_PUBLIC_API_URL + LOCAL_NODE_PROXY_TARGET, which Next standalone applies only via `node server.js`), serves the standalone build, starts a test-only `/api/info` capability-normalisation proxy (scripts/e2e-info-proxy.mjs), then runs the two-leg suite (parallel bulk + send-success workers=1). The proxy rewrites only GET /api/info to the hosted-DEV surface (capabilities all false, username_domain=dev.zkcoins.app, no bitcoin_network) and passes everything else through 1:1 to the node. Without it the --all-features local node reports username_claim=true, which renders an extra "Claim a username" row (+~36px) and breaks ~16 baselines. Documents both modes (incl. the platform-baseline caveat and the cap-normalisation rationale) in e2e/README.md § 4.2; adds an explanatory comment to the unchanged ci.yaml E2E job; gitignores the local report dir. * ci(e2e): run the suite against the PR's own served build The E2E job used to point Playwright at https://dev.zkcoins.app — the DEPLOYED frontend (always develop, never the PR). During the Jobs-API migration that gap became load-bearing: the node dropped /api/mint and /api/send while the deployed frontend still called them, so every frontend-behaviour spec failed for reasons unrelated to the PR under test (deployment lag). The job now builds the PR's standalone bundle, fronts the hosted DEV node with the /api/info normalisation proxy (E2E_NODE_URL=dev-api), serves the bundle on the runner, and drives both legs with E2E_TARGET=local — the served-build machinery this branch already ships. A red E2E now means the PR is wrong, not that the deploy queue is behind. The hosted DEV node stays the upstream (real proof gen + broadcast), and ubuntu-latest keeps the *-chromium-linux.png baseline platform. e2e/README.md §4.2 updated accordingly. Also fix the seed-generating capture, which the served build exposed as timing luck: the route glob **/zkcoins_wasm_bg.wasm never matched the real client_bg.<hash>.wasm asset, and the PWA service worker bypassed page.route() anyway. Scope serviceWorkers:'block' to an inner describe (mirroring spec 13), match **/client_bg*.wasm, and hold the fetch for the duration of the snapshot. Validated end-to-end in the pinned Linux Playwright container against dev-api: leg 1 80/80, leg 2 (send-success, one real send) 1/1. * fix(e2e-proxy): drop forwarded accept-encoding so undici decompresses The /api/info normalisation proxy forwarded the caller's request headers verbatim, including accept-encoding. When that header is set explicitly, undici does NOT transparently decompress the response — so upstream.json() on the /api/info leg parsed raw gzip/br/zstd bytes (SyntaxError → 502) and the pass-through leg relayed a compressed body after copyHeaders had already stripped content-encoding. Browsers always send accept-encoding, so every browser-originated request through the proxy 502'd, while curl (which sends none) worked — and the pinned Linux Playwright container masked it because its node auto- decompressed. In served-local CI on ubuntu-latest the 502s starved the app of /api/info, so the username_domain never populated, the wallet address chip never rendered, and globalSetup's createSeedWallet timed out. Dropping accept-encoding (alongside host) lets undici negotiate and decompress transparently, which both response paths already assume. Reproduced natively (node 22) before the fix and confirmed globalSetup mints Alice+Bob after it.
…xt logging) (#161) CodeQL flagged two real issues on the test-only /api/info proxy that the App stack introduced: - js/stack-trace-exposure (medium): the catch handler returned `detail: String(err)` in the 502 body, exposing server internals (upstream URL, undici frames) to the HTTP caller. - js/clear-text-logging (HIGH): the startup log interpolated the process.env-derived upstream URL + username domain in clear text; an upstream URL can embed credentials (user:secret@host). Real fixes (no suppression): - The 502 response now carries only a generic `upstreamFailureBody()` ({error: '...'}); the cause is logged server-side via console.error, never returned to the caller. - `startupMessage(port)` contains no env-derived string; the standalone entry logs the socket-bound `server.address().port`, so no environment value reaches the log. To make the hardening test-covered, the module now exports its pure helpers + a `createProxyServer({nodeUrl, usernameDomain})` factory and only binds a port from a `c8 ignore`d main-guard (`node scripts/e2e-info-proxy.mjs` still starts it). A new node-environment vitest suite pins the security behaviours and the file is added to the coverage include with a strict 100% per-file gate. Verified: full served-local E2E green (80/80 + one real send) with the refactored proxy; proxy file at 100% lines/branches/functions/statements.
…#162) The send page mapped admit-time `ApiError`s through `userMessageFor` (German), but an async `JobFailedError` — thrown when a queued send job ends in failed/cancelled during proving — set the raw English node string (`err.serverError`) straight into the toast. Same node failure-contract string, two different user-facing results. Root fix: `userMessageFor` now accepts `ApiError | JobFailedError` (both expose the same `serverError`), handling the `undefined` serverError a job can carry, and the send page routes both error classes through it. Known node errors now show their German copy on the async leg too; unmapped strings get the German `Serverfehler <status>: <raw>` fallback instead of a bare English blob. The previously-unused `JobFailedError` import in client.test.ts is now used meaningfully: the throw assertions switched from `name`-string matching to `instanceof JobFailedError`, and error-mapping.test.ts + send-pipeline.test.tsx cover the async German mapping (mapped, family-pattern, unmapped fallback, and the no-string fallback). errorMessages.ts stays at 100% on the strict src/lib gate.
* docs(contributing): anchor trust model — node is trusted, wallet is thin (#155) * docs: reframe trust model as run-your-own-node (Bitcoin full-node model) (#160) * fix(network): consume typed bitcoin_network from /api/info (#157) The node now exposes a normalised `bitcoin_network` enum in `GET /api/info` (`'mainnet' | 'mutinynet'`, lower-case, derived server-side from `is_mainnet` — zk-coins/node#193), alongside the existing free-text `network` display string. The faucet guard previously branched on `network`, which the node ships as a capitalised display string (`"Mainnet"`). The original `network !== 'mainnet'` compare therefore always evaluated true and rendered the faucet on PRD; it had been patched defensively with a `.toLowerCase()` at the call site, but the underlying field is still free-text and casing-fragile. Branch the guard on the typed `bitcoin_network` enum instead — a closed set with stable casing, so there is no case-mismatch risk. The field is parsed as `.optional()` (fail-open) so an app talking to a pre-#193 node that ships only `network` still parses; in that case the guard falls back to the lower-cased free-text compare, keeping the faucet hidden on a legacy mainnet node and visible on testnet (the path DEV runs today). - schemas: add `bitcoin_network: z.enum(['mainnet','mutinynet']).optional()` - network store: add `bitcoinNetwork` + `setBitcoinNetwork` - WalletScreen: hydrate the field from `/api/info`, branch `showFaucet` on it with a free-text fallback for pre-#193 nodes - tests: schema parses with/without the field and rejects out-of-enum values; store setter; faucet gating across typed + fallback paths * feat(api): migrate frontend to async Jobs API (app#141) (#158) * fix(network): consume typed bitcoin_network from /api/info The node now exposes a normalised `bitcoin_network` enum in `GET /api/info` (`'mainnet' | 'mutinynet'`, lower-case, derived server-side from `is_mainnet` — zk-coins/node#193), alongside the existing free-text `network` display string. The faucet guard previously branched on `network`, which the node ships as a capitalised display string (`"Mainnet"`). The original `network !== 'mainnet'` compare therefore always evaluated true and rendered the faucet on PRD; it had been patched defensively with a `.toLowerCase()` at the call site, but the underlying field is still free-text and casing-fragile. Branch the guard on the typed `bitcoin_network` enum instead — a closed set with stable casing, so there is no case-mismatch risk. The field is parsed as `.optional()` (fail-open) so an app talking to a pre-#193 node that ships only `network` still parses; in that case the guard falls back to the lower-cased free-text compare, keeping the faucet hidden on a legacy mainnet node and visible on testnet (the path DEV runs today). - schemas: add `bitcoin_network: z.enum(['mainnet','mutinynet']).optional()` - network store: add `bitcoinNetwork` + `setBitcoinNetwork` - WalletScreen: hydrate the field from `/api/info`, branch `showFaucet` on it with a free-text fallback for pre-#193 nodes - tests: schema parses with/without the field and rejects out-of-enum values; store setter; faucet gating across typed + fallback paths * feat(api): migrate client to async Jobs API (mint/send/commit) Replace the removed synchronous /api/{mint,send,commit} routes with the node's async Jobs API (zk-coins/node#161): - mint: POST /api/jobs/mint (mandatory Idempotency-Key) -> poll to completed. - send: balance hydrate (num_sends) -> sign -> POST /api/jobs/send -> poll to awaiting_signature -> read account_state_hash/output_coins_root from the JSON result (no binary CoinProof decode; hard-fail if absent) -> build the commitment via WASM -> POST /api/jobs/:id/commit -> poll to completed. - Add getJob/waitForJob/commitJob primitives, newIdempotencyKey (WebCrypto), and a typed JobFailedError. Retry-After backoff with a poll floor. - Add Job schemas (JobAccepted/JobStatus/JobResult/JobError) mirroring the SDK; keep bitcoin_network (#157), balance/info/username untouched; drop the dead sync Send/Mint/Commit schemas. Thin-client + no-fallback per CONTRIBUTING: every signed send re-fetches num_sends from the node before signing; a malformed awaiting_signature result fails hard rather than fabricating a commitment. * feat(send): drive the send page through the Jobs-API lifecycle Replace the sync api.send + api.commit pipeline with a single api.send call that runs the enqueue -> awaiting_signature -> commit -> completed lifecycle, surfacing the job's phase transitions as an inline progress label. Drop the localStorage in-flight-commit crash-recovery path: it replayed a bare commit payload against the removed synchronous /api/commit, and an async commit is keyed by a live awaiting_signature job id that cannot be reconstructed from a reload. Thin-client balance hydration stays inside api.send. * test(e2e): point the harness at the Jobs API - _helpers/api.ts mint() admits POST /api/jobs/mint (Idempotency-Key) and polls to completed so globalSetup can still seed Alice. - 13-send-server-errors mocks the /api/jobs/send admit route with the {error} envelope; it asserts the admit-time failure -> ApiError -> toast pipeline and so does not depend on the awaiting_signature proof path. - Drop 07-send recovering-banner (the in-flight-commit feature it covered was removed with the migration). - Update README endpoint references. * test(unit): cover the Jobs-API client + send lifecycle - client.test.ts: mint/send job lifecycles incl. awaiting_signature -> commit, Idempotency-Key, num_sends hydration / prev_commitment_pubkey, Retry-After backoff, and the no-fabrication hard-fail when ash/ocr are absent. - send-pipeline.test.tsx: rewritten over api.send (happy path, username resolution, error surfacing, defensive branches). - contract.live.test.ts: mint probe asserts job completion. - Remove client-signing.test.ts (covered the deleted api.sendSigned). Holds the strict src/lib/** 100% coverage gate. * test(e2e): exempt transient send-phase label from button audit The send page's inline send-job phase label (queued/proving/ awaiting_signature/broadcasting) renders only while a send is in flight and flips faster than Playwright can reliably catch — same class as the already-exempt seed-creating/import loading states. The phase transitions are covered by the client lifecycle unit tests' onPhase assertions. * build(deps): vendor @zkcoins/sdk 0.3.1 as a file: tarball The app now consumes @zkcoins/sdk for all node API traffic. The SDK is not published to npm, and the app CI checkout does not include the sdk sister repo, so the built tarball is vendored under vendor/ and referenced as a file: dependency. npm ci resolves it from the app repo alone — no sister directory required. * refactor(api): consume @zkcoins/sdk instead of direct API calls Replace the app-owned REST layer with the SDK: - src/lib/api/client.ts is now a thin adapter over the SDK's ZkCoinsClient. Every /api/* round-trip goes through the SDK; no direct fetch('/api...') remains in app code. The send/mint lifecycle composes the SDK's REST primitives with the existing WASM crypto for the local signing steps (Schnorr sign + commitment), and uses the SDK's buildSendMessage / buildClaimMessage for the signed-message byte layout. ApiError / JobFailedError are re-exported from the SDK. - Delete src/lib/api/schemas.ts — the wire schemas now come from the SDK (BalanceResponse, InfoResponse, JobStatus, Capabilities, ...). - capabilities store uses the SDK Capabilities type (adds multi_asset). - send page reads JobFailedError.serverError (SDK property name). Crypto choice: REST moves to the SDK, WASM stays for local signing — the SDK's @noble crypto is byte-equivalent to the WASM, but keeping the WASM path is the minimal-invasive change that preserves the exact send/commit correctness the browser bundle and E2E already exercise. * test(e2e): drive the harness through the SDK + add local-node proxy - e2e/_helpers/api.ts uses the SDK's ZkCoinsClient (info/balance/mint) so the test-side and app-side wire contracts cannot drift. - next.config.js gains an env-gated (LOCAL_NODE_PROXY_TARGET) same-origin rewrite to a local zkCoins node. Unset in CI and the Docker image, so production builds never proxy; it exists only for the local send-leg E2E run against http://127.0.0.1:4242. * test(unit): align fixtures with the SDK wire contract - Import wire schemas from @zkcoins/sdk (BalanceResponseSchema etc.). - num_sends is now required on BalanceResponse; capabilities carry multi_asset — update the affected mocks. - JobFailedError exposes serverError (not detail); ApiError.message is 'zkCoins API error ...' — update the assertions. - Add an api.sendJob direct test (the wrapper is no longer reached via api.send, which calls the SDK client directly) to keep src/lib at 100%. - Recalibrate the global statement floor to 84 after removing the 100%-covered app schemas.ts; src/lib/** and src/stores/** stay at a strict 100% on every axis. * test(e2e): dynamic E2E target — CI vs dev, local vs local node (#159) * test(e2e): add dynamic local target (local node) alongside dev Introduce an `E2E_TARGET` switch in playwright.config so the same spec files and the same `*-chromium-linux.png` baselines run in two modes: - dev (default, CI): unchanged — hosted DEV stack (dev.zkcoins.app / dev-api.zkcoins.app), no webServer. E2E_TARGET unset reproduces the historical behaviour byte-for-byte, so ci.yaml is untouched. - local: a locally-served standalone PR build + a local zkCoins node, driven by `npm run test:e2e:local` → scripts/e2e-local.sh. Local mode runs entirely inside a Linux Playwright container pinned to the exact @playwright/test version (the baselines are platform-specific *-chromium-linux.png). The script bakes the same-origin proxy config at build time (NEXT_PUBLIC_API_URL + LOCAL_NODE_PROXY_TARGET, which Next standalone applies only via `node server.js`), serves the standalone build, starts a test-only `/api/info` capability-normalisation proxy (scripts/e2e-info-proxy.mjs), then runs the two-leg suite (parallel bulk + send-success workers=1). The proxy rewrites only GET /api/info to the hosted-DEV surface (capabilities all false, username_domain=dev.zkcoins.app, no bitcoin_network) and passes everything else through 1:1 to the node. Without it the --all-features local node reports username_claim=true, which renders an extra "Claim a username" row (+~36px) and breaks ~16 baselines. Documents both modes (incl. the platform-baseline caveat and the cap-normalisation rationale) in e2e/README.md § 4.2; adds an explanatory comment to the unchanged ci.yaml E2E job; gitignores the local report dir. * ci(e2e): run the suite against the PR's own served build The E2E job used to point Playwright at https://dev.zkcoins.app — the DEPLOYED frontend (always develop, never the PR). During the Jobs-API migration that gap became load-bearing: the node dropped /api/mint and /api/send while the deployed frontend still called them, so every frontend-behaviour spec failed for reasons unrelated to the PR under test (deployment lag). The job now builds the PR's standalone bundle, fronts the hosted DEV node with the /api/info normalisation proxy (E2E_NODE_URL=dev-api), serves the bundle on the runner, and drives both legs with E2E_TARGET=local — the served-build machinery this branch already ships. A red E2E now means the PR is wrong, not that the deploy queue is behind. The hosted DEV node stays the upstream (real proof gen + broadcast), and ubuntu-latest keeps the *-chromium-linux.png baseline platform. e2e/README.md §4.2 updated accordingly. Also fix the seed-generating capture, which the served build exposed as timing luck: the route glob **/zkcoins_wasm_bg.wasm never matched the real client_bg.<hash>.wasm asset, and the PWA service worker bypassed page.route() anyway. Scope serviceWorkers:'block' to an inner describe (mirroring spec 13), match **/client_bg*.wasm, and hold the fetch for the duration of the snapshot. Validated end-to-end in the pinned Linux Playwright container against dev-api: leg 1 80/80, leg 2 (send-success, one real send) 1/1. * fix(e2e-proxy): drop forwarded accept-encoding so undici decompresses The /api/info normalisation proxy forwarded the caller's request headers verbatim, including accept-encoding. When that header is set explicitly, undici does NOT transparently decompress the response — so upstream.json() on the /api/info leg parsed raw gzip/br/zstd bytes (SyntaxError → 502) and the pass-through leg relayed a compressed body after copyHeaders had already stripped content-encoding. Browsers always send accept-encoding, so every browser-originated request through the proxy 502'd, while curl (which sends none) worked — and the pinned Linux Playwright container masked it because its node auto- decompressed. In served-local CI on ubuntu-latest the 502s starved the app of /api/info, so the username_domain never populated, the wallet address chip never rendered, and globalSetup's createSeedWallet timed out. Dropping accept-encoding (alongside host) lets undici negotiate and decompress transparently, which both response paths already assume. Reproduced natively (node 22) before the fix and confirmed globalSetup mints Alice+Bob after it. * fix(security): harden e2e-info-proxy (stack-trace exposure + clear-text logging) (#161) CodeQL flagged two real issues on the test-only /api/info proxy that the App stack introduced: - js/stack-trace-exposure (medium): the catch handler returned `detail: String(err)` in the 502 body, exposing server internals (upstream URL, undici frames) to the HTTP caller. - js/clear-text-logging (HIGH): the startup log interpolated the process.env-derived upstream URL + username domain in clear text; an upstream URL can embed credentials (user:secret@host). Real fixes (no suppression): - The 502 response now carries only a generic `upstreamFailureBody()` ({error: '...'}); the cause is logged server-side via console.error, never returned to the caller. - `startupMessage(port)` contains no env-derived string; the standalone entry logs the socket-bound `server.address().port`, so no environment value reaches the log. To make the hardening test-covered, the module now exports its pure helpers + a `createProxyServer({nodeUrl, usernameDomain})` factory and only binds a port from a `c8 ignore`d main-guard (`node scripts/e2e-info-proxy.mjs` still starts it). A new node-environment vitest suite pins the security behaviours and the file is added to the coverage include with a strict 100% per-file gate. Verified: full served-local E2E green (80/80 + one real send) with the refactored proxy; proxy file at 100% lines/branches/functions/statements. * fix(send): translate async JobFailedError toast (issue #99 async leg) (#162) The send page mapped admit-time `ApiError`s through `userMessageFor` (German), but an async `JobFailedError` — thrown when a queued send job ends in failed/cancelled during proving — set the raw English node string (`err.serverError`) straight into the toast. Same node failure-contract string, two different user-facing results. Root fix: `userMessageFor` now accepts `ApiError | JobFailedError` (both expose the same `serverError`), handling the `undefined` serverError a job can carry, and the send page routes both error classes through it. Known node errors now show their German copy on the async leg too; unmapped strings get the German `Serverfehler <status>: <raw>` fallback instead of a bare English blob. The previously-unused `JobFailedError` import in client.test.ts is now used meaningfully: the throw assertions switched from `name`-string matching to `instanceof JobFailedError`, and error-mapping.test.ts + send-pipeline.test.tsx cover the async German mapping (mapped, family-pattern, unmapped fallback, and the no-string fallback). errorMessages.ts stays at 100% on the strict src/lib gate. --------- Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
fix(docker): copy vendored SDK tarball before npm ci
Promote: staging -> develop
Contributor
Author
[OK] Button-Inventory-Audit — all clearChecked 108 testid(s) in |
TaprootFreak
marked this pull request as ready for review
June 5, 2026 11:26
* test(e2e): add deterministic visual golden spec for /network (issue #166) The /network Network-activity page is the last always-on, ungated user-facing screen without a page-level golden (spec 09 only shoots the network badge on Settings). Add 14-network-activity.spec.ts with desktop + mobile shots, taking golden coverage of the live, ungated screens to 100%. Determinism: the page renders a live, self-advancing chart, so three variance sources are pinned: - page.clock.install + pauseAt(FIXED) BEFORE goto freeze Date.now() (deterministic buildHistory seed 1337 + stable x-axis labels) and halt the POLL_MS=8000 interval, so a live nextSample() tick (unseeded Math.random) can provably never fire. install alone is not enough - it resumes real time (verified against the Playwright 1.59 clock engine and empirically: 9 s after install-only the clock had drifted 9004 ms and the interval had fired once; with pauseAt drift is 0 and the rendered chart path is byte-identical). - timezoneId UTC + locale en-US pin the toLocaleTimeString ticks across runners. - the cross-origin explorer probe is route.abort()ed so the page always falls back to source 'simulated' regardless of the build's NEXT_PUBLIC_EXPLORER_URL (served-local CI bakes the not-yet-live PRD explorer URL); a visible-text guard on the explorer-preview note fails loudly if the simulated path ever stops rendering. Service workers are blocked so the PWA SW cannot bypass the route intercept. The spec is staged in playwright.config.ts::testIgnore until its linux baselines land via the regenerate-visual-baselines workflow (documented mechanism); a follow-up commit adds the PNGs and unstages it. No login fixture: /network has no route guard and reads no wallet state, so the baselines are immune to DEV wallet state. Verified locally (M5): linux (mcr playwright v1.59.1-noble image) 2/2 generate + 12/12 repeat-each stability against a served-local standalone build, plus 2/2 against hosted DEV proving DEV-rendered and served-local-rendered baselines are interchangeable for this page; darwin 10/10. e2e/README.md gains §8.14 (+ §8.15 totals incl. the previously undocumented spec 13 rows, file tree, PR-14 entry). * test(e2e): land /network visual baselines + unstage spec 14 Linux baselines for 14-network-activity (desktop + mobile) generated by the regenerate-visual-baselines workflow (run 27030754467). Remove the spec from playwright.config.ts::testIgnore in the same commit so regular CI now diffs it. Verified the workflow output is interchangeable with a served-local render: pixelmatch reports 0 diff pixels vs a served-local standalone build, and the served-local linux suite passes against these baselines (repeat-each, no E2E_REGENERATING) at the 0.01 ratio with full headroom. * docs(handbook): document spec 14 + close the network-activity triage gap The prebuild gate (scripts/sync-handbook-baselines.mjs) requires every e2e baseline to be referenced by the static handbook; the new 14-network-activity goldens tripped it. Add a spec-14 section (mobile/desktop viewport toggle) to both handbook languages. While here, bring the handbook self-consistent with its own sections: - drop the 'Network activity chart — no dedicated test' triage-gap entry (spec 14 covers it now, the clock-freeze removes the 'simulated data breaks pixel diff' blocker) - add the missing TOC entries for spec 13 (pre-existing omission) and spec 14 - update the global counters (11 specs / 70 tests / 67 baselines were the spec-01..11 sums; the displayed sections now total 13 specs / 76 tests / 72 baselines)
) * test(ci): add golden-coverage audit gate for live screens (issue #167) Mirrors the Button-Inventory-Audit at the screen level: fail CI when an active, ungated user-facing screen ships without a page-level visual golden (the #166 / /network failure mode). - e2e/_audit/gates.mjs — shared ENV_GATED_FEATURES (PASSKEY, APPS_DIRECTORY, DEV_ROUTES) source of truth, consumed by BOTH audits so they cannot drift on what 'env-gated' means. - e2e/_audit/screens.ts — declarative screen registry: each screen -> reach (route or /-state) + exact expected baseline name(s) + optional gate. Imported by the audit via Node's built-in TS type-stripping. - e2e/_audit/golden-coverage.mjs — auto-inventories src/app page.tsx routes, auto-detects env-gates from the '!FEATURES.<flag>) notFound()' guard (flags limited to ENV_GATED_FEATURES). Fails on (a) a non-gated route missing from the registry, (b) a registered baseline absent from e2e/*.spec.ts-snapshots, plus integrity checks (stale entry, bad gate, source/registry gate mismatch). exit 0/1, --markdown, --json, --report-only. - coverage.mjs — reuse ENV_GATED_FEATURES (tagged snippet exemptions + load-time consistency assertion); behavior unchanged. - .github/workflows/audit-golden-coverage.yml — same trigger matrix as audit-button-coverage.yml, sticky comment (header audit-golden-coverage). - e2e/README.md §15 documents the audit. Passes now that #166 landed the /network golden: 9 active screens covered, 3 env-gated routes exempt. Verified via fault injection that a new ungated route, a missing baseline, and a non-exempt-flag gate each fail the gate. * test(ci): harden golden-coverage gate detection (review fixes) Logic-review follow-ups on the golden-coverage audit: - HIGH: add the reverse gate-consistency check. A registry entry that declares `gate: <flag>` on a route whose source does NOT carry the matching `!FEATURES.<flag>) notFound()` guard previously skipped the baseline check and passed green — a mistyped gate could silently disable coverage for a live screen. Now flagged as a gate inconsistency (fails CI). - MEDIUM: strip block + line comments before gate detection so a commented-out guard can no longer mark a live route as exempt. The line-comment strip preserves `://` in URL string literals. Both re-verified via fault injection: mislabelling /send as gated, and a commented-out guard on an unregistered route, now each fail; a real guard sharing a file with a URL literal is still detected; clean tree passes.
…174) Add api.getHistory(address, { limit, offset }) to the SDK adapter, delegating to ZkCoinsClient.history so the no-direct-HTTP invariant is preserved (same pattern as balance/info). Re-export the SDK's canonical, cross-rust-verified history schemas under the issue's names (HistoryItemSchema, HistoryResponseSchema, HistoryErrorResponseSchema) plus the HistoryResponse/HistoryItem/HistoryErrorResponse types. The @zkcoins/sdk migration removed the app-owned src/lib/api/schemas.ts (wire schemas now live in the SDK), so the schemas issue #145 asks for are surfaced from the SDK rather than re-declared. Pagination is caller-driven; 422 (malformed) and 500 (DB) both surface as ApiError with the node's { error } string. pending is accepted as a steady state.
* feat(settings): show connected node, trim redundant address UI The wallet home duplicated the zk-address: the username slot fell back to the same value the copyable address button already shows. Drop the fallback so the address renders once; the username line only appears when a username is actually set. In Settings, replace the raw hex address with the connected node host (the configured apiUrl with the scheme stripped) — a more actionable detail than the local address. Also remove the redundant header network badge and subtitle (network already lives in About) and the Security info section. * test(e2e): retarget settings specs from network badge to node host The settings header network badge was removed; specs 05 and 09 anchored on its `settings-network-badge` testid (now a ghost selector flagged by the button-coverage audit) and lacked a reference to the new `settings-node-host` testid. Anchor settings navigation on the always-present node-host row instead, and convert the two badge display/loading baselines to cover the node-host row. Settings visual baselines still need regeneration (separate workflow); this only fixes the static testid-coverage gate. * test(e2e): regenerate visual baselines for the UI changes The wallet home is one line shorter (the duplicate zk-address was removed) and the Settings page lost its network badge, subtitle and Security section while gaining a node-host row. Regenerate the 24 affected *-chromium-linux.png baselines on amd64 (matching CI) and add the two new node-host baselines (09-network-info-node, 09-network-loading). * test(e2e): repoint handbook at renamed node baselines The network-badge tests became network-info-node / network-loading, so their baselines were renamed. Update both handbook pages (en + de) to reference the new screenshots and delete the now-orphaned 09-network-badge-* baselines. Fixes the prebuild handbook ↔ baseline coverage check.
… bullet (#172) * feat(welcome): remove footer links, drop Shielded CSV tagline, reword bullet - Remove the FooterLinks bar everywhere it rendered (AppShell, Onboarding welcome, Settings "Resources" section) and delete the component. Add a bottom spacer in AppShell so the floating BottomNav keeps its clearance. - Drop the "Shielded CSV · v{APP_VERSION}" tagline from the welcome screen. - Reword benefit heading "Just Bitcoin. No altcoin." -> "Just Bitcoin, not a new blockchain". - Update unit tests; remove the two spec-09 footerlinks visual tests and their baselines; prune stale spec-header / README / handbook references. Visual baselines for footer-affected screens still need regeneration on linux against the PR build. * docs(e2e): correct spec-09 counts after footerlinks removal Update §8.15 totals (spec 09 4/4, Σ 83/73) and drop the deleted FooterLinks.tsx from the §8 audited-component list. * test(e2e): refresh visual baselines after footer-link removal Removing the footer-link bar shortens every full-page screen and shifts the disconnect, settings and network-badge viewports, invalidating 23 chromium-linux baselines. Regenerate them to match the footer-less layout so the served-local E2E job matches the PR build. * test(e2e): regenerate visual baselines for the merged footer-less layout After merging staging (#173 settings node host, network test rename), 21 chromium-linux baselines still showed the footer-era full-page heights and the pre-merge settings/network viewports. Refresh them so the served-local E2E job matches the merged build.
* test(e2e): visual golden for /network (issue #166) (#168) * test(e2e): add deterministic visual golden spec for /network (issue #166) The /network Network-activity page is the last always-on, ungated user-facing screen without a page-level golden (spec 09 only shoots the network badge on Settings). Add 14-network-activity.spec.ts with desktop + mobile shots, taking golden coverage of the live, ungated screens to 100%. Determinism: the page renders a live, self-advancing chart, so three variance sources are pinned: - page.clock.install + pauseAt(FIXED) BEFORE goto freeze Date.now() (deterministic buildHistory seed 1337 + stable x-axis labels) and halt the POLL_MS=8000 interval, so a live nextSample() tick (unseeded Math.random) can provably never fire. install alone is not enough - it resumes real time (verified against the Playwright 1.59 clock engine and empirically: 9 s after install-only the clock had drifted 9004 ms and the interval had fired once; with pauseAt drift is 0 and the rendered chart path is byte-identical). - timezoneId UTC + locale en-US pin the toLocaleTimeString ticks across runners. - the cross-origin explorer probe is route.abort()ed so the page always falls back to source 'simulated' regardless of the build's NEXT_PUBLIC_EXPLORER_URL (served-local CI bakes the not-yet-live PRD explorer URL); a visible-text guard on the explorer-preview note fails loudly if the simulated path ever stops rendering. Service workers are blocked so the PWA SW cannot bypass the route intercept. The spec is staged in playwright.config.ts::testIgnore until its linux baselines land via the regenerate-visual-baselines workflow (documented mechanism); a follow-up commit adds the PNGs and unstages it. No login fixture: /network has no route guard and reads no wallet state, so the baselines are immune to DEV wallet state. Verified locally (M5): linux (mcr playwright v1.59.1-noble image) 2/2 generate + 12/12 repeat-each stability against a served-local standalone build, plus 2/2 against hosted DEV proving DEV-rendered and served-local-rendered baselines are interchangeable for this page; darwin 10/10. e2e/README.md gains §8.14 (+ §8.15 totals incl. the previously undocumented spec 13 rows, file tree, PR-14 entry). * test(e2e): land /network visual baselines + unstage spec 14 Linux baselines for 14-network-activity (desktop + mobile) generated by the regenerate-visual-baselines workflow (run 27030754467). Remove the spec from playwright.config.ts::testIgnore in the same commit so regular CI now diffs it. Verified the workflow output is interchangeable with a served-local render: pixelmatch reports 0 diff pixels vs a served-local standalone build, and the served-local linux suite passes against these baselines (repeat-each, no E2E_REGENERATING) at the 0.01 ratio with full headroom. * docs(handbook): document spec 14 + close the network-activity triage gap The prebuild gate (scripts/sync-handbook-baselines.mjs) requires every e2e baseline to be referenced by the static handbook; the new 14-network-activity goldens tripped it. Add a spec-14 section (mobile/desktop viewport toggle) to both handbook languages. While here, bring the handbook self-consistent with its own sections: - drop the 'Network activity chart — no dedicated test' triage-gap entry (spec 14 covers it now, the clock-freeze removes the 'simulated data breaks pixel diff' blocker) - add the missing TOC entries for spec 13 (pre-existing omission) and spec 14 - update the global counters (11 specs / 70 tests / 67 baselines were the spec-01..11 sums; the displayed sections now total 13 specs / 76 tests / 72 baselines) * test(ci): golden-coverage audit gate for live screens (issue #167) (#171) * test(ci): add golden-coverage audit gate for live screens (issue #167) Mirrors the Button-Inventory-Audit at the screen level: fail CI when an active, ungated user-facing screen ships without a page-level visual golden (the #166 / /network failure mode). - e2e/_audit/gates.mjs — shared ENV_GATED_FEATURES (PASSKEY, APPS_DIRECTORY, DEV_ROUTES) source of truth, consumed by BOTH audits so they cannot drift on what 'env-gated' means. - e2e/_audit/screens.ts — declarative screen registry: each screen -> reach (route or /-state) + exact expected baseline name(s) + optional gate. Imported by the audit via Node's built-in TS type-stripping. - e2e/_audit/golden-coverage.mjs — auto-inventories src/app page.tsx routes, auto-detects env-gates from the '!FEATURES.<flag>) notFound()' guard (flags limited to ENV_GATED_FEATURES). Fails on (a) a non-gated route missing from the registry, (b) a registered baseline absent from e2e/*.spec.ts-snapshots, plus integrity checks (stale entry, bad gate, source/registry gate mismatch). exit 0/1, --markdown, --json, --report-only. - coverage.mjs — reuse ENV_GATED_FEATURES (tagged snippet exemptions + load-time consistency assertion); behavior unchanged. - .github/workflows/audit-golden-coverage.yml — same trigger matrix as audit-button-coverage.yml, sticky comment (header audit-golden-coverage). - e2e/README.md §15 documents the audit. Passes now that #166 landed the /network golden: 9 active screens covered, 3 env-gated routes exempt. Verified via fault injection that a new ungated route, a missing baseline, and a non-exempt-flag gate each fail the gate. * test(ci): harden golden-coverage gate detection (review fixes) Logic-review follow-ups on the golden-coverage audit: - HIGH: add the reverse gate-consistency check. A registry entry that declares `gate: <flag>` on a route whose source does NOT carry the matching `!FEATURES.<flag>) notFound()` guard previously skipped the baseline check and passed green — a mistyped gate could silently disable coverage for a live screen. Now flagged as a gate inconsistency (fails CI). - MEDIUM: strip block + line comments before gate detection so a commented-out guard can no longer mark a live route as exempt. The line-comment strip preserves `://` in URL string literals. Both re-verified via fault injection: mislabelling /send as gated, and a commented-out guard on an unregistered route, now each fail; a real guard sharing a file with a URL literal is still detected; clean tree passes. * feat(api): wire GET /api/history into the typed client (issue #145) (#174) Add api.getHistory(address, { limit, offset }) to the SDK adapter, delegating to ZkCoinsClient.history so the no-direct-HTTP invariant is preserved (same pattern as balance/info). Re-export the SDK's canonical, cross-rust-verified history schemas under the issue's names (HistoryItemSchema, HistoryResponseSchema, HistoryErrorResponseSchema) plus the HistoryResponse/HistoryItem/HistoryErrorResponse types. The @zkcoins/sdk migration removed the app-owned src/lib/api/schemas.ts (wire schemas now live in the SDK), so the schemas issue #145 asks for are surfaced from the SDK rather than re-declared. Pagination is caller-driven; 422 (malformed) and 500 (DB) both surface as ApiError with the node's { error } string. pending is accepted as a steady state. * feat(settings): show connected node, trim redundant address UI (#173) * feat(settings): show connected node, trim redundant address UI The wallet home duplicated the zk-address: the username slot fell back to the same value the copyable address button already shows. Drop the fallback so the address renders once; the username line only appears when a username is actually set. In Settings, replace the raw hex address with the connected node host (the configured apiUrl with the scheme stripped) — a more actionable detail than the local address. Also remove the redundant header network badge and subtitle (network already lives in About) and the Security info section. * test(e2e): retarget settings specs from network badge to node host The settings header network badge was removed; specs 05 and 09 anchored on its `settings-network-badge` testid (now a ghost selector flagged by the button-coverage audit) and lacked a reference to the new `settings-node-host` testid. Anchor settings navigation on the always-present node-host row instead, and convert the two badge display/loading baselines to cover the node-host row. Settings visual baselines still need regeneration (separate workflow); this only fixes the static testid-coverage gate. * test(e2e): regenerate visual baselines for the UI changes The wallet home is one line shorter (the duplicate zk-address was removed) and the Settings page lost its network badge, subtitle and Security section while gaining a node-host row. Regenerate the 24 affected *-chromium-linux.png baselines on amd64 (matching CI) and add the two new node-host baselines (09-network-info-node, 09-network-loading). * test(e2e): repoint handbook at renamed node baselines The network-badge tests became network-info-node / network-loading, so their baselines were renamed. Update both handbook pages (en + de) to reference the new screenshots and delete the now-orphaned 09-network-badge-* baselines. Fixes the prebuild handbook ↔ baseline coverage check. * feat(welcome): remove footer links, drop Shielded CSV tagline, reword bullet (#172) * feat(welcome): remove footer links, drop Shielded CSV tagline, reword bullet - Remove the FooterLinks bar everywhere it rendered (AppShell, Onboarding welcome, Settings "Resources" section) and delete the component. Add a bottom spacer in AppShell so the floating BottomNav keeps its clearance. - Drop the "Shielded CSV · v{APP_VERSION}" tagline from the welcome screen. - Reword benefit heading "Just Bitcoin. No altcoin." -> "Just Bitcoin, not a new blockchain". - Update unit tests; remove the two spec-09 footerlinks visual tests and their baselines; prune stale spec-header / README / handbook references. Visual baselines for footer-affected screens still need regeneration on linux against the PR build. * docs(e2e): correct spec-09 counts after footerlinks removal Update §8.15 totals (spec 09 4/4, Σ 83/73) and drop the deleted FooterLinks.tsx from the §8 audited-component list. * test(e2e): refresh visual baselines after footer-link removal Removing the footer-link bar shortens every full-page screen and shifts the disconnect, settings and network-badge viewports, invalidating 23 chromium-linux baselines. Regenerate them to match the footer-less layout so the served-local E2E job matches the PR build. * test(e2e): regenerate visual baselines for the merged footer-less layout After merging staging (#173 settings node host, network test rename), 21 chromium-linux baselines still showed the footer-era full-page heights and the pre-merge settings/network viewports. Refresh them so the served-local E2E job matches the merged build. --------- Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Contributor
Author
[OK] Golden-Coverage-Audit — all clearEvery active, ungated screen has its golden: 10 active screen(s) across 8 route(s), 2 env-gated route(s) exempt.
|
…ocal tx store (issue #175) (#176) The wallet screen sourced its transaction list from a localStorage-backed zustand array that only ever recorded sends made by the current tab, so a funded wallet (faucet mint, restore, second device, cleared storage) always showed 'No transactions yet' — a thin-client violation: transaction-history truth belongs to the node. - Add useHistory(address): fetches GET /api/history via the typed SDK adapter (api.getHistory, #145/#174) on mount and re-polls on the same 5 s cadence as the balance tick; resets on account swap; never clears the last good list on a transient fetch error. - WalletScreen renders the list from the server HistoryItem rows (direction -> label/sign, wire timestamp is Unix seconds) and holds the empty state back until the first history response lands. - Remove the local transaction store: Transaction type, transactions field, addTransaction(), loadTransactions()/saveTransactions(), the zkcoins_transactions localStorage key, and the transactions slice of the encrypted wallet payload. clearLegacyStorage() sweeps the stale key. - Remove the /simulate dev route + src/lib/simulate.ts: their sole purpose was injecting fabricated rows into the now-retired local store. - Coverage: src/hooks/** added to the include list with the same strict 100% per-glob gate as lib/stores; useHistory is fully covered. - E2E: 06-balance funded specs now assert the tx list is rendered (not the empty state) before snapping; regenerate the 13 baselines that show the funded wallet's transaction region (fullPage height shrinks 818->812 as the placeholder gives way to the mint row). Closes #175
Promote: staging -> develop
* feat(send): scan recipient address from a QR code Add a 'Scan QR' affordance to the Send screen's recipient field. It opens a camera scanner (getUserMedia → canvas → jsQR) that decodes a zkCoins address QR and fills the recipient input, with an image-upload fallback for browsers without camera access or when permission is denied. Mirrors the Receive screen's QR (qrcode.react) the other way. - src/lib/qr.ts: pure decode + recipient parse/validation (100% unit) - src/components/QrScanModal.tsx: camera + file-upload overlay, permission states, lazy-loaded so jsQR ships in its own chunk - wire Scan button + scanned-confirmation flash into the Send page * test(e2e): scan-QR specs with fake-camera + upload fallback - 15-send-scan-qr: drives Chromium's fake video device with a single- frame Y4M QR clip, asserting the full getUserMedia → canvas → jsQR pipeline fills the recipient input - 16-send-scan-fallback: scanner modal UI golden + PNG-upload decode + close-keeps-recipient, on a fake camera with no QR in view - scripts/make-qr-y4m.mjs: pure-Node Y4M + PNG fixture generator (no ffmpeg) - linux baselines (15-scan-filled, 16-scan-modal-open) + handbook entries * refactor(send): split scan payload routing, plug upload URL leak - separate tryFinish() (resolve on a recipient) from flashInvalid() so an uploaded non-recipient QR shows only the upload error, not the camera flash too - revoke a still-decoding upload's object URL on unmount * test(e2e): refresh send-form baselines for the Scan QR button The recipient label row gained the Scan QR affordance, so the 07-send and 13-server-error send-form goldens are regenerated to depict it (the change sits within the 1% pixel tolerance, but the baselines + handbook should reflect the real UI). 07-send-success is unaffected (success screen has no form). Regenerated + verified in the pinned noble Playwright image against the DEV node. * chore: mark .y4m fixtures as binary in gitattributes The QR Y4M's leading white-pixel run (0xFF, no NUL) trips git's text sniffing; pin it binary so line-ending normalisation can never corrupt the fake-camera stream on checkout.
…ge (#178) Every wallet history row is now a link to a dedicated detail page that renders the full server-truth transaction detail (zk-coins/node GET /api/history/{id} -> TxDetail, via @zkcoins/sdk getTransaction). - src/hooks/useTransaction.ts: one-shot fetch of api.getTransaction(id, address); null id / missing address resolve to not_found without a request; 404 -> not_found, other failures -> error. No local tx store (issue #175), no polling — opened from the already-polling list. - src/app/tx/[id]/page.tsx: loading / not-found / error / body states. The body shows direction + signed amount + status, then Overview, Amounts, Proof & verification (circuit digest + verified marker), On-chain (txid w/ explorer link when NEXT_PUBLIC_EXPLORER_URL is set, block height, commit value), and Privacy sections. Unix-seconds timestamps, sign via direction. - WalletScreen: each tx row is a <Link href=/tx/{id}> (visually unchanged — verified the funded-wallet goldens are pixel-identical). - api client: getTransaction adapter + TxDetail re-export. - Vendor @zkcoins/sdk 0.4.0 (built from the sdk getTransaction branch; the SDK is unpublished, so the app vendors the tarball — same pattern as 0.3.1). i18n: zk-coins/app has no ARB/i18n layer (strings are inline English, testid-stable per CONTRIBUTING); new labels follow that convention. Tests: useTransaction hook (100% incl unmount guards), the page (states + every field + explorer link), the api adapter (request shape, parse, 404/422/500 envelopes). New 15-tx-detail E2E spec (desktop / mobile / back-to-wallet / not-found) + 3 linux goldens (per-run values masked + width-pinned). Registered the route in the golden-coverage registry and added the spec section to both handbooks (bijection gate). 100% coverage gate (lib/stores/hooks) holds; button + golden audits pass.
Promote: staging -> develop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automatic Release PR
Commits: 7 new commit(s)