Skip to content

Release: develop -> main - #163

Merged
TaprootFreak merged 20 commits into
mainfrom
develop
Jun 9, 2026
Merged

Release: develop -> main#163
TaprootFreak merged 20 commits into
mainfrom
develop

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Automatic Release PR

Commits: 7 new commit(s)

  • Review all changes
  • Verify CI passes
  • Merge when ready for production

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
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

[OK] Button-Inventory-Audit — all clear

Checked 108 testid(s) in src/ against 95 reference(s) in e2e/. Nothing to do.

@TaprootFreak
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>
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

[OK] Golden-Coverage-Audit — all clear

Every active, ungated screen has its golden: 10 active screen(s) across 8 route(s), 2 env-gated route(s) exempt.

Screen Reach Golden(s) Status
onboarding-welcome / · welcome (no stored wallet) 01-welcome-desktop, 01-welcome-mobile
onboarding-create-seed / · create-seed reveal 02-seed-reveal-shown
onboarding-restore-seed / · restore-seed entry 03-restore-entry-empty
unlock / · hasStoredWallet && isLocked 04-unlock-empty
wallet / · account && !isLocked 06-balance-funded-desktop, 06-balance-funded-mobile
send /send 07-send-default
tx-detail /tx/[id] 17-tx-detail-desktop, 17-tx-detail-mobile
receive /receive 08-receive-default-desktop, 08-receive-default-mobile
settings /settings 05-settings-desktop, 05-settings-mobile
network /network 14-network-activity-desktop, 14-network-activity-mobile
(gated) /apps ⚪ exempt (APPS_DIRECTORY)
(gated) /reset ⚪ exempt (DEV_ROUTES)

…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
@TaprootFreak
TaprootFreak merged commit c131368 into main Jun 9, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant