Skip to content

Wallet balance refresh + swap UX fixes (one-hold, repeat-amount, hardening) - #81

Merged
hellno merged 7 commits into
mainfrom
hellno/wallet-balance-refresh
Jun 18, 2026
Merged

Wallet balance refresh + swap UX fixes (one-hold, repeat-amount, hardening)#81
hellno merged 7 commits into
mainfrom
hellno/wallet-balance-refresh

Conversation

@hellno

@hellno hellno commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

Keeps balances current and fixes the swap flow's real-world friction surfaced by dogfooding on Sepolia. Bundles the balance-refresh feature (auto-poll + ⌘K command) with three swap UX/correctness fixes and a trust-core hardening — all tested, with the signer-daemon changes codex-reviewed.

Balance refresh

  • Auto-poll the public balance (~20s) while the wallet home is focused; cancels on lock.
  • ⌘K Refresh command (+ the header refresh button) → full refresh (public + shielded).
  • Simplified + hardened (codex review): one in-flight flag (portfolio_loading); every async balance reply is fenced by auth_epoch + view_epoch so a stale fetch can't repopulate state after lock / retarget.

Swap UX + correctness

  • One hold, not two: the confirm now polls the relayer approval until it mines (~32s bound; instant on an auto-mining fork), then submits in the same hold — no more "approve, then hold again" on real networks.
  • Repeat the same amount any number of times: the signer's already_executed replay guard is now fund-moving-only. An ERC-20 approval moves no funds (idempotent), so a relayer approve starts a fresh cycle each swap (still gated by a matching pending order + the human hold). Sends/shields keep the strict anti-double-spend guard.
  • Honest deny copy: already_executed on a swap → "this swap was already submitted" (was the deposit-worded fallback).
  • Tighter admission: a shaped approve is admitted only by a live (Pending) order — a stale / resolved / expired order can't surface a spurious approval card.

Trust core + review

The deckard-signerd changes touch replay/admission. Codex security review (gpt-5.5 xhigh): SAFE — no weakening of send/shield double-spend protection, and no malicious-client abuse path (Resolve is private-control-channel-only; the approve must be value-0 / GPv2 relayer / exact amount / Pending; it can't execute without a fresh hold).

Testing

  • cargo fmt --all --check clean · just check green (default + tray) · cargo test --workspace green.
  • New daemon regressions (verified to run against a live anvil): a repeat same-amount approve is admitted (and still requires a fresh hold; a repeat send is still already_executed); a non-Pending order no longer admits a shaped approve.

Screens

The swap/refresh-logic commits are behavioral — no view/layout changes; the user-facing surfaces (⌘K Refresh, header refresh, the one-hold swap) render unchanged structurally. Happy to attach live Sepolia captures on request (the app wasn't relaunched in this round).

Dev helper

crates/deckard-core/examples/dev-vault.rs — seals a fresh-random throwaway wallet for real-testnet QA (companion to qa-vault, which uses anvil's well-known account 0). examples/-only; never linked into the shipped binary.

hellno added 7 commits June 16, 2026 18:46
…-refresh

swap.rs: the confirm now polls the relayer allowance until the exact-gross approve mines (~32s bound; checks before each sleep so an auto-mining fork is instant), then submits in the SAME hold — one gesture instead of 'approve, then hold again' on real networks.

errors.rs: humanize_swap_deny maps already_executed to a swap-specific line ('this swap was already submitted — change the amount and try again') rather than the deposit-worded humanize_deny fallback.

shell.rs (codex review): drop portfolio_refresh_in_flight; portfolio_loading is now the single in-flight dedup flag; fence every async balance reply (kick_portfolio / kick_block_number) with auth_epoch + view_epoch so a stale fetch can't repopulate portfolio/read_status/synced_block after lock or retarget. lock() clears the read state too.

DoD: cargo fmt --check clean; just check (both configs) green; cargo test --workspace 250 passed.
A swap re-issues an identical exact-gross approve(relayer, amount) every time, which hashes to
the same request id. The daemon's replay guard treated the 2nd same-amount swap's approve as
ALREADY_EXECUTED and refused it, so you couldn't swap the same amount twice. An ERC-20 approve
moves no funds (idempotent) and is gated by a matching pending order + the human hold.

daemon.rs (finish_propose): the already_executed replay guard is now FUND-MOVING-ONLY. A shaped
relayer-approve (always_needs_card) whose record was already broadcast starts a FRESH approval
cycle (overwrites to Pending) instead of denying. Send/Shield keep the strict guard; execute()'s
own already_executed guard is unchanged.

errors.rs: already_executed swap copy -> plain "this swap was already submitted" (drop the
now-moot "change the amount").

anvil_e2e.rs: regression test - repeat same-amount approve re-proposes as NeedsApproval (not
already_executed), the re-proposed approve still requires a fresh hold (execute -> not_approved),
and a re-proposed send is still already_executed.

Codex security review (gpt-5.5 xhigh): SAFE - does not weaken send/shield double-spend
protection, no malicious-client abuse path (resolve is private-channel-only; approve is
value-0/exact-amount/Pending), overwrite is safe vs lock/stop/expire/status.

DoD: fmt clean; just check (both configs) green; cargo test --workspace 251 passed (carve-out
test verified to run against a live anvil).
Codex security-review nit (pre-existing, not exploitable): shaped_approve_admission matched ANY
stored order with the right sell_token + amount, ignoring its status — a Denied/Expired/already-
resolved order could admit a spurious approve card. Harmless alone (no execute without a fresh
hold), but looser than the stated "matching pending order" invariant, and more reachable now that
finish_propose lets a repeated approve start a fresh cycle.

daemon.rs (shaped_approve_admission): require matches!(req.status, ApprovalStatus::Pending) — only
a LIVE order admits its exact approve; a new swap brings its own pending order.

shaped_approve.rs: regression test — a Pending order admits the exact approve; once the order is
resolved (no longer Pending), the same approve is refused (approve_no_matching_order).

DoD: fmt clean; just check (both configs) green; cargo test --workspace green.
main merged the balance-refresh feature independently (#70, 7988d54). Resolved the shell.rs
conflict by keeping this branch's codex-hardened refresh (single portfolio_loading flag +
auth_epoch/view_epoch reply fencing), which supersedes #70's portfolio_refresh_in_flight marker.
dev-vault.rs and palette_commands.rs were byte-identical on both sides (auto-merged).
cargo-deny-advisories flagged bitcoin_hashes v0.14.100 as yanked from crates.io (pulled
transitively via bip39 2.2.2 in deckard-core's BIP-39 handling). cargo update -p bitcoin_hashes
pins the latest non-yanked compatible version (0.14.2, within bip39's ^0.14). Lockfile-only;
no Cargo.toml change. Unblocks CI / cargo-deny-advisories.
@hellno
hellno merged commit 878eaf5 into main Jun 18, 2026
5 checks passed
@hellno
hellno deleted the hellno/wallet-balance-refresh branch June 18, 2026 17:28
hellno added a commit that referenced this pull request Jun 21, 2026
…ated PRs (#82) (#122)

* ci(supply-chain): yanks warn, not block — stop dep drift gating unrelated PRs (#82)

cargo-deny's advisory check re-reads the live RustSec DB + crates.io yank status
on every run, so a yank or fresh CVE could turn an UNRELATED PR red with no change
on our side (PR #81 ate a bitcoin_hashes yank). Split the gate by determinism:
deterministic bans/licenses/sources still gate PRs; a bare crates.io yank is now
warn-level so outside-world drift can't block work that never touched that dep.
Real RUSTSEC advisories still block: on PRs, at release, and in the daily scan.

- deny.toml: yanked = "deny" -> "warn" (with rationale)
- audit.yml: open/refresh a tracking issue on a failed daily scan (gh CLI, no new action)
- ci.yml: advisories-job comment reflects the yank policy
- docs/AGENTIC-ENGINEERING.md section 4: document the PR-vs-release advisory policy

The daily watch + the release-boundary gate are the safety net, not the PR gate.
Mainstream Rust-OSS posture; never ship a vulnerable build.

* ci(supply-chain): block yanks at release, harden nightly alert (code-review on #82)

Cross-model code review (Codex + subagents) on PR #122 found that applying
yanked=warn uniformly let a release ship a yanked dependency, and the new
audit.yml alert step had shell/robustness gaps. Adopt variant D2 and harden it.

D2 (release boundary): deny.toml keeps `yanked = "deny"`; only the PR-time
cargo-deny-advisories job downgrades yanks to a warning (`--warn yanked`, gated
on pull_request). Releases, push-to-main, and the nightly audit still block
yanks. Real RUSTSEC advisories still block everywhere.

audit.yml alert hardening:
- gate on `steps.deny.outcome == 'failure'` so a checkout/infra failure no longer
  files a misleading "advisory landed" issue
- tolerate a transient `gh issue list` failure (|| true) so `set -e` can't
  swallow the alert
- exact-title de-dup (post-filter; gh search is fuzzy) + a concurrency group so
  overlapping runs can't open duplicate trackers
- pass `-R "$REPO"` explicitly to the gh calls

Also: fix a stale deny.toml header that called the supply-chain gate
"non-blocking" (it is required + blocking), annotate the ci.yml summary diagram,
and stop the docs claiming a security yank always carries a RUSTSEC advisory.
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