Wallet balance refresh + swap UX fixes (one-hold, repeat-amount, hardening) - #81
Merged
Conversation
…-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.
This was referenced Jun 18, 2026
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.
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.
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
portfolio_loading); every async balance reply is fenced byauth_epoch+view_epochso a stale fetch can't repopulate state after lock / retarget.Swap UX + correctness
already_executedreplay 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.already_executedon a swap → "this swap was already submitted" (was the deposit-worded fallback).Pending) order — a stale / resolved / expired order can't surface a spurious approval card.Trust core + review
The
deckard-signerdchanges 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 (Resolveis 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 --checkclean ·just checkgreen (default +tray) ·cargo test --workspacegreen.already_executed); a non-Pendingorder 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 toqa-vault, which uses anvil's well-known account 0).examples/-only; never linked into the shipped binary.