Demo-UX: two-pane shell, privacy mask, agent visuals, shield flow + KAT-gated shielded-balance hero - #11
Merged
Merged
Conversation
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
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
This was referenced Jun 14, 2026
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.
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-spec→main. 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, andcargo test --workspace).What shipped
b15edc0) — locked palette/theme.rs, mono-for-money, the hand-built two-pane shell (sidebar tree + breadcrumb + status strip), contextual wallet/project/agent homes.039b7b6) — the agent breathing pulse (1.2spulsating_between, only while acting, the one sanctioned ambient motion) + the full privacy mask: fixed-bulletmask_moneythreaded 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 trayobjc2dep — no manifest churn).a119815, P1 fixesdbc9d55) — 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.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.c6bfcf5, P1/P2 fixes3995026) — the hero: a read-only sync actor (ShieldedHandle, mirrorsEthProvider) + the merged Total / Private / Public split render + theShieldStatuslifecycle in the status strip.Trust & honesty properties (please sanity-check)
railgunvia itsshieldfeature) derives and exports only aRailgunViewGrant(0zk address + viewing key, redactedDebug), and only whenknown_answer_ok()passes. The app re-gates on the KAT independently.None(unknown) while the first sync runs — never a silent 0 — and is always taggedUnsynced(neverVerified) 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".ShieldStatus::Sending(not the "Confirmed"SyncingPrivate); the watcher settles toPrivateSpendableonly on a real synced value,Failedon 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
dbc9d55and3995026).Before merge / reviewer checklist
SHIELD_CHAIN_IDdefaults toDAEMON_CHAIN_ID(mainnet = 1). A shield on Sepolia needs the daemon + reader relaunched on Sepolia and the const flipped, orproposedenieschain_mismatch.TODO(fonts)inmain.rs); GPUI falls back to system fonts until a human drops them in.timefeature widening), end-to-end zeroizing of the viewing key, a committed SDK fixture for KAT provenance, and theViewOnlySignerdummy spending key (kohaku ships no view-only signer andRailgunSignerErrorhas no public constructor; it's module-private + verified-unreachable on the balance path).hmac+sha2added todeckard-corefor the SLIP-0010 derivation (approved; both already transitively inCargo.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(theanvil_e2eintegration tests need a local anvil and are run single-threaded). No new or changed dependencies beyond the approvedhmac/sha2.