Skip to content

Demo-UX: two-pane shell, privacy mask, agent visuals, shield flow + KAT-gated shielded-balance hero - #11

Merged
hellno merged 7 commits into
mainfrom
hellno/demo-ux-design-spec
Jun 9, 2026
Merged

Demo-UX: two-pane shell, privacy mask, agent visuals, shield flow + KAT-gated shielded-balance hero#11
hellno merged 7 commits into
mainfrom
hellno/demo-ux-design-spec

Conversation

@hellno

@hellno hellno commented Jun 9, 2026

Copy link
Copy Markdown
Owner

What this is

The demo-UX build for Deckard: the DESIGN.md two-pane shell, the agent/privacy visuals, the Railgun shield trigger flow, and the hero — a KAT-verified shielded-balance view. Brings the locked design language and the privacy hero into the app without overselling: every figure is honest, and a wrong key derivation can render nothing rather than a silent wrong number.

Branch: hellno/demo-ux-design-specmain. Built Wave-by-Wave; each commit is independently green (cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, the same with -p deckard-app --features tray, and cargo test --workspace).

What shipped

  • Wave 1 (T1–T3, b15edc0) — locked palette/theme.rs, mono-for-money, the hand-built two-pane shell (sidebar tree + breadcrumb + status strip), contextual wallet/project/agent homes.
  • T4 (039b7b6) — the agent breathing pulse (1.2s pulsating_between, only while acting, the one sanctioned ambient motion) + the full privacy mask: fixed-bullet mask_money threaded through every money surface, a Splits-style allocation bar that flattens when masked, triggers via ⌘⇧M / eye glyph / click-the-Total / Settings, and an opt-in macOS capture-block (reuses the tray objc2 dep — no manifest churn).
  • T5 (a119815, P1 fixes dbc9d55) — the shield trigger UI: compose → clear-signing review card (amount / recipient / 0.25% fee / net + the three honesty lines) → hand-built hold-to-confirm (amber fill-sweep) → broadcast, wired to the daemon's existing propose/execute. Daemon/core unchanged.
  • T8 (046011a)KAT-gated Railgun key derivation (deckard-core/src/railgun_keys.rs). The derivation is consensus-critical (a wrong one shows a silent $0 forever), so it is gated by a known-answer test against Railgun's own engine vectors. The gate caught a real bug: Railgun keys the master HMAC with the literal "babyjubjub seed", not the textbook "ed25519 seed". Fixed; the derived 0zk now matches the engine byte-for-byte on two chains.
  • T9/T10 (c6bfcf5, P1/P2 fixes 3995026) — the hero: a read-only sync actor (ShieldedHandle, mirrors EthProvider) + the merged Total / Private / Public split render + the ShieldStatus lifecycle in the status strip.

Trust & honesty properties (please sanity-check)

  • The spending key never leaves the daemon. The daemon (which already builds railgun via its shield feature) derives and exports only a RailgunViewGrant (0zk address + viewing key, redacted Debug), and only when known_answer_ok() passes. The app re-gates on the KAT independently.
  • Private balance is never faked. It's None (unknown) while the first sync runs — never a silent 0 — and is always tagged Unsynced (never Verified) because the sync rides the raw RPC/Subsquid, not the Helios-verified read path. The merged hero is labelled "Total" only when private is known, else "Public · private balance still syncing".
  • Lifecycle copy is honest. A broadcast sets ShieldStatus::Sending (not the "Confirmed" SyncingPrivate); the watcher settles to PrivateSpendable only on a real synced value, Failed on error, and stays in-flight on timeout — never "Spendable now · $0".

Reviews

Three read-only codex (GPT-5 xhigh) adversarial passes: the T5 flow review, a T9/T10 design consult before writing code, and a final Wave-2 review. All raised P1/P2s are fixed in-tree (see dbc9d55 and 3995026).

Before merge / reviewer checklist

  • Measure the mainnet Railgun cold-sync wall-clock (the handoff's MUST-MEASURE). The UI degrades gracefully ("private balance still syncing" stays honest however long it takes), but the latency is unmeasured — let it finish syncing before a recorded demo.
  • Chain reconcile for a live shield. SHIELD_CHAIN_ID defaults to DAEMON_CHAIN_ID (mainnet = 1). A shield on Sepolia needs the daemon + reader relaunched on Sepolia and the const flipped, or propose denies chain_mismatch.
  • Fonts. General Sans + JetBrains Mono are configured but the licensed files aren't bundled yet (TODO(fonts) in main.rs); GPUI falls back to system fonts until a human drops them in.
  • Deferred hardening (behavior is already honest — these are robustness, not correctness): a sync timeout/retry-backoff (needs a tokio time feature widening), end-to-end zeroizing of the viewing key, a committed SDK fixture for KAT provenance, and the ViewOnlySigner dummy spending key (kohaku ships no view-only signer and RailgunSignerError has no public constructor; it's module-private + verified-unreachable on the balance path).
  • Deps. hmac + sha2 added to deckard-core for the SLIP-0010 derivation (approved; both already transitively in Cargo.lock — 2 new edges, no new crates).

Verification

All commits green on: cargo fmt --all --check · cargo clippy --locked --workspace --all-targets -- -D warnings · the same with -p deckard-app --features tray · cargo test --workspace (the anvil_e2e integration tests need a local anvil and are run single-threaded). No new or changed dependencies beyond the approved hmac/sha2.

hellno added 7 commits June 9, 2026 11:52
theme: locked DESIGN palette + agent/cyan + amber Hsla tokens; drop the
  6-accent picker; neutral primary buttons + amber focus ring; font-family
  wiring (licensed font files deferred to a human drop-in + README)
money: mono-for-money helper (dim decimals/ticker by color only)
contract: ShieldStatus lifecycle enum (UI-free, serde, glyph hook)
signerd: blocking propose/execute/resolve/status client wrappers
shell: Route -> Selection/Surface routing + hand-built two-pane chrome
  (248px sidebar tree / 44px breadcrumb / 25px status strip), wallet/
  project/agent home, receive amber-keyline network warning, palette repoint

codex-review fixes: neutral caution surface (not a filled tint), weights <=600,
  token squares (not rounded-full), hairline holdings rows, stale comment removed

DoD green: cargo fmt + clippy -D warnings (default + --features tray) +
  cargo test --workspace; no new dependencies.

Static agent squircle landed; breathing pulse + full privacy mask + allocation
bar (T4) + shield UI (T5) + the railgun hero (Wave 2) are follow-ups.
Agent visuals:
- shared agent_squircle() with the one sanctioned ambient motion: a 1.2s
  pulsating_between(0.35,1.0) breathing of the cyan keyline, only while acting
- Shell.agent_acting demo toggle (palette row + agent-home button); sidebar
  status dot goes cyan while acting

Full privacy mask (persisted-once-on, inverse of seed reveal):
- money.rs: MASK_BULLETS (fixed 6, magnitude-safe) + mask_money(); masked
  param threaded through money()/usd() and every money surface (wallet/project
  hero, holdings, sidebar balance)
- Splits-style allocation_bar() (tonal, >=3px segments, legend) that flattens
  to one neutral bar when masked; single Public segment in v1
- triggers: click-the-Total, eye/eye-off breadcrumb glyph, palette row,
  ToggleMask action + secondary-shift-m, Settings Privacy toggle
- Settings.mask_balances + capture_block (serde default false)

macOS capture-block (opt-in, default OFF):
- capture.rs apply_capture_block() via NSApplication.windows + setSharingType,
  gated #[cfg(all(target_os="macos", feature="tray"))] to reuse the tray objc2
  dep (no manifest churn); inert no-op twin elsewhere; re-applied from render
  keyed off capture_block && mask

DoD green: fmt --check, clippy (default + --features tray), cargo test
--workspace all pass; no new deps.
New Surface::Shield flow, wired to the daemon's existing propose/execute:
- signer.rs: build_shield_intent(chain, 0zk recipient, value_wei) wrapping
  deckard_core::build_shield_native_intent (parses RailgunAddress, clear error
  on a bad 0zk); parse_eth_to_wei() — total, magnitude-safe ETH→wei (+ tests)
- shell.rs: shield state + review_shield()/confirm_shield() mirroring do_unlock
  (build+propose / execute off-thread, fold on the UI thread); epoch-guarded
  hold timer; humanize_deny() for friendly deny reasons; SHIELD_CHAIN_ID config
  seam (D1, defaults to DAEMON_CHAIN_ID) + SHIELD_HOLD
- shield_view.rs: compose (amount + free-text 0zk) → clear-signing review card
  (amount/recipient/0.25% fee/net + the 3 honesty lines in a neutral+amber-keyline
  caution frame) → hand-built hold-to-confirm with an amber theme::amber_tint
  fill-sweep over SHIELD_HOLD → broadcast-done card
- theme::shield() neutral low-chroma shield tone (off the actor axis, not cyan/amber)
- entry points: primary Shield CTA on wallet-home + a ⌘K "Shield to private" row

Daemon/core/contract unchanged. DoD green: fmt, clippy (default + tray), tests;
no new deps.
Cross-model (codex GPT-5 xhigh) read-only review vs DESIGN.md + the handoff:
- P1 clear-signing integrity: the review card rendered the LIVE recipient input,
  which the user could edit during the propose round-trip → could show recipient
  B while signing intent A. Now ShieldProposal carries a recipient snapshot and
  the card renders that; a shield_review_epoch drops a stale propose reply
  (guard-first so it can't clobber a newer review's busy flag).
- P1 hold-after-leave: a held confirm could fire after ⌘[/palette navigation
  (mouse-up never landed). Timer now requires surface==Shield && proposal.is_some,
  and open() cancels any in-progress hold when leaving Shield.
- P1 double-broadcast on timeout: confirm_shield now invalidates the proposal on
  EVERY execute attempt (not just success) so a second hold can't re-broadcast an
  ambiguous timeout; humanize_deny covers broadcast_timeout/_failed/already_executed.
- P1 shield on watched account: Shield is disabled while viewing_watch (it signs
  from your own wallet); open_shield no-ops there too.
- P2 single-cyan-surface: the acting status dot is now a neutral brightness lift
  (the breathing squircle stays the sole cyan).
- P2 disabled-invalid state: "Review deposit" is disabled until amount>0 and
  recipient are present (live via input subscriptions).

DoD green: fmt, clippy (default + tray), tests; no new deps.
The consensus-critical hero foundation: derive the Railgun spending (babyjubjub)
+ viewing (ed25519) keys from the wallet's BIP-39 entropy, GATED on a known-answer
test against Railgun's OWN engine vectors. A wrong derivation shows a silent $0
shielded balance forever — the worst bug for a trust wallet — so nothing downstream
may render a shielded balance unless known_answer_ok() passes.

- deckard-core/src/railgun_keys.rs (new, behind `shield`): SLIP-0010-style hardened
  HMAC-SHA512 derivation along m/44'/1984'/0'/0'/i' (spend) + m/420'/1984'/0'/0'/i'
  (view). The one non-textbook detail Railgun uses — and the bug the KAT caught —
  is the master HMAC key "babyjubjub seed", NOT "ed25519 seed". chainKey bytes feed
  HexKey::from_hex (ByteKey::from_bytes is crate-private). Empty-passphrase seed via
  to_seed_normalized. No unwrap/expect/indexing (split_at + try_into), propagated.
- KAT: mnemonic "test…junk" → getAddress({EVM,id:1/2}) asserted against the engine's
  published literals (two chains pin both the keys AND the chain-id encoding);
  known_answer_ok() is the same check at runtime.
- UnlockedVault::railgun_address(chain_id, index) (keystore.rs) mirrors account_signer;
  the seed/entropy never leaves core.
- Cargo: hmac + sha2 added to the `shield` feature (approved — already transitively
  in Cargo.lock, so +2 edges, no new crates).

DoD: fmt, clippy (default + tray), KAT + workspace tests green (anvil_e2e is a
parallel-load flake — passes reliably single-threaded). T9 (in-app sync actor)
paused pending the ZK-tree feature-widening decision.
Turns the KAT-verified derivation (T8) into a visible private balance, with
codex's honesty corrections folded in.

Boundary (decision D2, tightened):
- deckard-contract: RailgunViewGrant { address, viewing_key } wire type with a
  REDACTED Debug (the viewing key reveals private history). SignerRequest::
  RailgunViewGrant + SignerResponse::RailgunView.
- daemon derives + exports ONLY the view grant (viewing key + 0zk address, never
  the spending key), gated on known_answer_ok() — no grant from an unverified
  derivation. railgun is already in the daemon's build graph (its shield feature).
- core: railgun_view_grant_from_entropy + UnlockedVault::railgun_view_grant.

Sync actor (deckard-core/src/shielded.rs, behind shield):
- ShieldedHandle mirrors EthProvider: a background thread owns the RailgunProvider
  (raw RPC + Subsquid, NOT Helios) and updates a cached snapshot the GUI reads
  instantly — balance() never blocks on sync. Private balance is UNKNOWN while
  syncing (never a fake 0) and always Unsynced (never Verified) in v1.
- ViewOnlySigner (module-private): real viewing key + precomputed address + an
  inert dummy spending key; address() overridden, provider never exposed.

Render + lifecycle (app):
- welcome.rs render_shielded_hero: merged Total (public + private, summed only when
  both known), Private/Public allocation bar (Private first, neutral shield tone),
  composition lines, "WETH-equivalent net of fee" footnote. Integer (bps) ratios.
- shell.rs: fetch the grant post-unlock → spawn the actor → watch the sync (capped
  poll); after a shield broadcast → ShieldStatus::SyncingPrivate + resync →
  PrivateSpendable; auto-fill the recipient with the user's own 0zk; tear down on lock.
- shell_chrome.rs: ShieldStatus reassurance line + glyph in the status strip.

DoD green: fmt, clippy (default + tray), cargo test --workspace; no new deps.
Final adversarial codex pass on T8/T9/T10:
- [P1] app now independently gates shielded UI on known_answer_ok() before
  spawning the sync actor (defense-in-depth alongside the daemon gate).
- [P1] grant fetch carries a session/auth epoch (bumped on lock+unlock); a slow
  reply from a prior session can't re-install a stale handle/address after lock.
- [P1] shield lifecycle is honest: broadcast sets ShieldStatus::Sending (not the
  "Confirmed" SyncingPrivate), and the watcher settles to PrivateSpendable ONLY on
  a real synced value, Failed on error, and stays in-flight on timeout — never
  "Spendable now · $0" off a failed/hung sync.
- [P1] hero is labelled "Total" only when private is known; otherwise "Public ·
  private balance still syncing" (never reads public as public+0).
- [P2] private balance footnote now states it's synced over raw RPC and not
  independently verified.
- [P2] RPC change re-points the shielded worker too (drops stale private state).
- [P2] allocation bar omits a zero-value segment (no phantom 3px Private sliver).
- [P2] lock clears the shield inputs on the next render (no prior wallet's 0zk
  lingering); reset_shield on lock.
- [P3] status strip uses circular CircleCheck/CircleX glyphs; contract gains a
  RailgunViewGrant roundtrip + Debug-redaction test.

Acknowledged/deferred (honest behavior already holds): the ViewOnlySigner dummy
spending key (kohaku has no view-only signer + RailgunSignerError has no public
ctor; module-private + verified-unreachable), and a sync timeout/backoff (needs a
tokio `time` feature widening) — noted as follow-ups.

DoD green: fmt, clippy (default + tray), cargo test --workspace; no new deps.
@hellno
hellno merged commit b174fb7 into main Jun 9, 2026
3 of 4 checks passed
hellno pushed a commit that referenced this pull request Jun 14, 2026
Second decision round (2026-06-14):
- Ambition: harvest aligned attributes, do not climb the Stage ladder
- Hardware-wallet signing (#4): out of scope (deliberate non-goal)
- Browser integration (#25): deferred; MCP agent is the sole connection
  surface; app isolation (#11) reframed as per-agent/session isolation
- Account abstraction (#26): EIP-7702 research spike (on-chain agent
  policy), batching (#27) contingent on it
- Multi-identity (#8/#9): a post-v0 goal with non-correlation UX

Rewrites the Stages section as reference-only.
hellno pushed a commit that referenced this pull request Jun 14, 2026
Second decision round (2026-06-14):
- Ambition: harvest aligned attributes, do not climb the Stage ladder
- Hardware-wallet signing (#4): out of scope (deliberate non-goal)
- Browser integration (#25): deferred; MCP agent is the sole connection
  surface; app isolation (#11) reframed as per-agent/session isolation
- Account abstraction (#26): EIP-7702 research spike (on-chain agent
  policy), batching (#27) contingent on it
- Multi-identity (#8/#9): a post-v0 goal with non-correlation UX

Rewrites the Stages section as reference-only.
hellno pushed a commit that referenced this pull request Jun 14, 2026
PR #52 resolved the dapp-connectivity exploration and created canonical
issues (epic #44, PRD-01..05 #45-50) plus an issues-not-files convention.
Reconcile this doc to a rubric + decision record, not a parallel tracker:

- Replace the work-item sequencing backlog and the EIP-7702 spike brief
  (duplicated existing issue #33) with a Tracking & cross-references block
- Fix the now-outdated connection-surface framing: #25 browser integration
  is planned/phased via the owned EIP-6963 bridge (ADR 0001), not 'no dapps';
  #11 isolation maps to per-origin permissions (PRD-05 #48), not a wire token
- Point #3 clear-signing at PRD-02 #46; note PRD-01 #45 resolver-auth
- Update Stages + status notes to match
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