Skip to content

fix: correctness pass — security + copy + UX#49

Merged
epicexcelsior merged 8 commits into
anonmesh:v3from
epicexcelsior:epic/correctness-pass
May 15, 2026
Merged

fix: correctness pass — security + copy + UX#49
epicexcelsior merged 8 commits into
anonmesh:v3from
epicexcelsior:epic/correctness-pass

Conversation

@epicexcelsior
Copy link
Copy Markdown
Collaborator

@epicexcelsior epicexcelsior commented May 15, 2026

Eight focused fixes. One real security gap, several copy/UX papercuts.

Note: commit 8 touches components/onboarding/CTAButtons.tsx.

+80 / -32 LOC.

  1. fix(wallet): disableDeviceFallback: true on all 3 LocalAuthentication.authenticateAsync sitessrc/infrastructure/wallet/LocalWallet.ts:55,107,144. Before: anyone with the device PIN/pattern could bypass biometric → extract seed in 4 taps. Now biometric fails closed. AUDIT T13 / THREAT_MODEL T-WALLET-01 / ROADMAP § 0.2.

  2. fix(wallet): drop dead error state in useWalletBalancesrc/hooks/useWalletBalance.tsx. The careful "Couldn't reach devnet" copy was set on a state field nothing read (grep). Removed dead state from interface + provider value + refetch reset. Rate-limit signal now flows entirely through activityError ("Devnet rate-limited") which RecentActivity already consumes. AUDIT T24.

  3. fix(send): drop hardcoded "devnet" from copycomponents/send/SuccessCard.tsx:122 "...on devnet" → "...on-chain"; ReviewCard.tsx:401 "Estimated from devnet RPC" → "...network RPC". Mainnet-ready copy without changing live network behavior. UX P1 V3 magic branch #16.

  4. fix(send): silent user-cancel on TransactionNotApprovedErrorcomponents/send/ReviewCard.tsx:300-320. MWA popup dismissal was painting "Approve the transaction in your wallet" as an inline error banner — reads like a failure, isn't. Branches before setError when isUserCancel; slider + phase still reset. Matches Solana Mobile guidance + LESSON 2026-05-13. ROADMAP § 2.3.

  5. fix(send): MAX chip reserves fee buffer on native SOLcomponents/send/AmountKeypad.tsx. MAX = full balance → tx fails at simulate-time on full-balance wallets. Now reserves 0.00005 SOL when native SOL (sig fee + priority headroom). SPL paths unchanged (fees in SOL). UX P1 Wallet truth pass #20.

  6. fix(settings): biometric re-auth on rotate identitycomponents/settings/RotateKeypairModal.tsx. Rotate is irreversible (old peers can't reach you again) but tappable from any unlocked session. Now prompts biometric before fire, gated by useBiometricEnabled + hasHardware + isEnrolled so users without bio don't get locked out. UX P1 feat(v3): mesh network adapter, storage refactor, BLE peers, notifications, wallet fixes #13.

  7. fix(send): cancel affordance in "Approve in wallet" footercomponents/send/ReviewCard.tsx. Once MWA was launched there was no way back to the review screen without dismissing the wallet popup. Small uppercase Cancel under the waiting footer resets isConfirming / txPhase / slider. Known limitation: doesn't actually abort an in-flight signed tx (Solana semantics — once signed + broadcast, no client-side recall). Behaves as "dismiss waiting UI"; tx may still land on chain if MWA returns successfully after Cancel tap. Follow-up tracked: thread AbortController through handleConfirm. UX P1 V3 magic branch #15.

  8. fix(onboarding): CTAButtons shows "CREATING…" on create pathcomponents/onboarding/CTAButtons.tsx:30. Loading label was "CONNECTING..." which is the CONNECT WALLET button's verb, not CREATE IDENTITY's. UX P1 V3 magic branch #17.

Test plan

  • `npx tsc --noEmit` clean
  • `npm run lint` clean
  • `node ./scripts/validate-tier0-config.mjs` clean
  • Pre-existing v3 `tier0:services` brand-validator drift; not introduced
  • AI-fingerprint scan clean
  • Device (Android stock w/ bio enrolled): unlock app → bio prompts → succeed; reveal seed → bio prompts → succeed; fresh wallet create → bio prompts → succeed
  • Device: deliberately fail bio (cover sensor / wrong finger 3×) → does NOT offer "Use PIN" / "Use Pattern" fallback → returns "Authentication cancelled" (this is the security fix)
  • Device: bio toggle OFF in Settings → unlock app → no prompt (skip path)
  • Device: send success card subtitle = "Settlement confirmed on-chain."
  • Device: send review fee row secondary = "Estimated from network RPC"
  • Device: hammer wallet refresh until devnet 429 → activity area shows "Devnet is rate-limiting us" + throttle subtitle
  • Device: send → MWA → dismiss popup → no inline banner; slider re-armed
  • Device: full-balance MAX → submit → succeeds (no over-balance fail)
  • Device: Settings → rotate identity → ROTATE → bio prompts → cancel = no rotate
  • Device: send review → slide to confirm → small Cancel under "Approve in wallet" → tap = back to review, slider re-armed
  • Device: onboarding CREATE IDENTITY button → label "CREATING…" during loading
  • iOS: same paths via FaceID — code path identical to Android; will validate on prod build (no local iOS test rig)

Notes

  • iOS local testing not available — will validate FaceID prompt on prod build. Code path identical to Android.

…alls

Anyone with the device unlock pattern could previously extract the seed in four taps — biometric fell through to the device PIN/pattern instead of failing closed.

Flips disableDeviceFallback to true on all three LocalAuthentication.authenticateAsync sites in LocalWallet (unlock, sign-time check via readAndDecrypt, create-wallet). Closes AUDIT T13 / THREAT_MODEL T-WALLET-01 / ROADMAP § 0.2.
RecentActivity has read the rate-limit signal off activityError === 'Devnet rate-limited' for a while now. The hook still carried a second error field that set 'Couldn't reach devnet' when every fetch failed — nothing in the app ever read it, so the careful copy was unreachable per AUDIT T24.

Removes the dead state from the interface + provider value + refetch reset. The 429 path now flows entirely through activityError so the consumer copy stays canonical.
Settlement subtitle and fee secondary line both said 'devnet' verbatim — fine today, wrong the moment mainnet ships. SuccessCard now reads 'Settlement confirmed on-chain.', fee row reads 'Estimated from network RPC'. Banner / explorer link still disclose the active network.
When the user dismissed the wallet popup we still painted an 'Approve the transaction in your wallet' banner — which reads like an error, not a cancel. Solana Mobile's own guidance says swallow those (LESSON 2026-05-13, ROADMAP § 2.3).

Branches before setError on isUserCancel and skips it; slider + phase still reset so the user can retry. Real failures still show the error card.
@epicexcelsior epicexcelsior marked this pull request as ready for review May 15, 2026 08:48
MAX previously paid the full balance, so the resulting tx instantly failed at simulate-time on full-balance wallets. Now reserves a 0.001 SOL buffer on native SOL only — covers signature fee (~5k lamports), priority spikes, recipient rent-exempt minimum (~890k lamports) when sending to a brand-new wallet, and any rounding between uiAmount display and underlying lamport count. SPL paths still send the full token balance since SPL fees come out of SOL.
Rotate identity is irreversible — old peers can't reach you again. Was triggerable from any unlocked session with one tap. Now requires a fresh biometric prompt before the rotate fires, matching the seed-export gate. Falls back to the existing confirm path if biometric is disabled / unavailable so users don't get locked out.
Once MWA was launched and the wallet popup hadn't returned, the user was stuck staring at 'Approve in wallet' with no way out. Adds a small Cancel under the waiting footer that flips isConfirming back, resets txPhase, and re-arms the slider. UX P1 anonmesh#15.
CREATE IDENTITY button rendered 'CONNECTING...' once tapped — copied straight from the CONNECT WALLET label. Shows 'CREATING…' instead so the affordance matches the action.

D-LANE NOTE: this file is Djason-owned. Branch stays local until Hunter sends the Djason memo at push-time.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Eight small correctness/UX fixes across wallet, send, settings, and onboarding. Notably, biometric authentication now fails closed (no device-PIN fallback) at all three LocalAuthentication.authenticateAsync call sites, MAX-amount sends reserve a fee buffer, identity rotation re-prompts for biometrics, and several copy strings are made network-agnostic.

Changes:

  • Security: disableDeviceFallback: true on all biometric prompts + re-auth gate before keypair rotate.
  • Send UX: silent user-cancel handling, cancel affordance in "Approve in wallet" footer, MAX reserves SOL fee buffer, neutral "on-chain"/"network RPC" copy.
  • Wallet/onboarding: drop unread error state in useWalletBalance; fix CTA loading label to "CREATING…" on create path.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
mobile_app/src/infrastructure/wallet/LocalWallet.ts Disable device-PIN/pattern fallback on all 3 biometric auth sites
mobile_app/src/hooks/useWalletBalance.tsx Remove unread error state; surface 429s via activityError
mobile_app/components/settings/RotateKeypairModal.tsx Require biometric re-auth before irreversible rotate (when enabled)
mobile_app/components/send/SuccessCard.tsx Replace "on devnet" copy with "on-chain"
mobile_app/components/send/ReviewCard.tsx Silent user-cancel, "network RPC" copy, Cancel affordance under waiting footer
mobile_app/components/send/AmountKeypad.tsx Reserve SOL fee buffer on MAX for native SOL sends
mobile_app/components/onboarding/CTAButtons.tsx Use "CREATING…" loading label on create path

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to +73
const SOL_FEE_BUFFER = 0.001;
const isNativeSol = !mint || mint === "";
const maxSendable = isNativeSol ? Math.max(0, balanceNum - SOL_FEE_BUFFER) : balanceNum;
@epicexcelsior epicexcelsior merged commit 2f6499c into anonmesh:v3 May 15, 2026
5 checks passed
@epicexcelsior epicexcelsior deleted the epic/correctness-pass branch May 15, 2026 09:12
Magicred-1 pushed a commit that referenced this pull request May 16, 2026
* fix(wallet): disable device-PIN fallback on all LocalAuthentication calls

Anyone with the device unlock pattern could previously extract the seed in four taps — biometric fell through to the device PIN/pattern instead of failing closed.

Flips disableDeviceFallback to true on all three LocalAuthentication.authenticateAsync sites in LocalWallet (unlock, sign-time check via readAndDecrypt, create-wallet). Closes AUDIT T13 / THREAT_MODEL T-WALLET-01 / ROADMAP § 0.2.

* fix(wallet): drop dead error state in useWalletBalance

RecentActivity has read the rate-limit signal off activityError === 'Devnet rate-limited' for a while now. The hook still carried a second error field that set 'Couldn't reach devnet' when every fetch failed — nothing in the app ever read it, so the careful copy was unreachable per AUDIT T24.

Removes the dead state from the interface + provider value + refetch reset. The 429 path now flows entirely through activityError so the consumer copy stays canonical.

* fix(send): drop hardcoded devnet from SuccessCard + ReviewCard

Settlement subtitle and fee secondary line both said 'devnet' verbatim — fine today, wrong the moment mainnet ships. SuccessCard now reads 'Settlement confirmed on-chain.', fee row reads 'Estimated from network RPC'. Banner / explorer link still disclose the active network.

* fix(send): silent user-cancel on TransactionNotApprovedError

When the user dismissed the wallet popup we still painted an 'Approve the transaction in your wallet' banner — which reads like an error, not a cancel. Solana Mobile's own guidance says swallow those (LESSON 2026-05-13, ROADMAP § 2.3).

Branches before setError on isUserCancel and skips it; slider + phase still reset so the user can retry. Real failures still show the error card.

* fix(send): MAX chip subtracts fee buffer on native SOL

MAX previously paid the full balance, so the resulting tx instantly failed at simulate-time on full-balance wallets. Now reserves a 0.001 SOL buffer on native SOL only — covers signature fee (~5k lamports), priority spikes, recipient rent-exempt minimum (~890k lamports) when sending to a brand-new wallet, and any rounding between uiAmount display and underlying lamport count. SPL paths still send the full token balance since SPL fees come out of SOL.

* fix(settings): biometric re-auth on rotate identity confirm

Rotate identity is irreversible — old peers can't reach you again. Was triggerable from any unlocked session with one tap. Now requires a fresh biometric prompt before the rotate fires, matching the seed-export gate. Falls back to the existing confirm path if biometric is disabled / unavailable so users don't get locked out.

* fix(send): cancel affordance in 'Approve in wallet' footer

Once MWA was launched and the wallet popup hadn't returned, the user was stuck staring at 'Approve in wallet' with no way out. Adds a small Cancel under the waiting footer that flips isConfirming back, resets txPhase, and re-arms the slider. UX P1 #15.

* fix(onboarding): create-button shows CREATING during create flow

CREATE IDENTITY button rendered 'CONNECTING...' once tapped — copied straight from the CONNECT WALLET label. Shows 'CREATING…' instead so the affordance matches the action.

D-LANE NOTE: this file is Djason-owned. Branch stays local until Hunter sends the Djason memo at push-time.
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.

2 participants