Skip to content

fix(ui): keep Max transfer amount exact in fiat mode - #956

Merged
llbartekll merged 1 commit into
developfrom
codex/fix-fiat-max-rounding
Aug 10, 2026
Merged

fix(ui): keep Max transfer amount exact in fiat mode#956
llbartekll merged 1 commit into
developfrom
codex/fix-fiat-max-rounding

Conversation

@llbartekll

@llbartekll llbartekll commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • preserve the exact spendable amount in duffs after selecting Max
  • treat the rounded fiat amount as display-only
  • keep the exact Max value when switching units and clear it after manual edits

Verification

  • xcodebuild dashpay Debug arm64 simulator build succeeded
  • no automated tests added

Summary by CodeRabbit

  • Bug Fixes
    • Max amount selections now retain the exact payment amount when switching between DASH and fiat.
    • Prevented rounding in displayed values from changing the actual amount sent.
    • Editing the amount now correctly clears the previous Max selection.
    • Max selections are cleared when the amount is zero or fiat conversion is unavailable.
    • Improved amount parsing and display consistency across regular payments and internal transfers.

@llbartekll llbartekll changed the title fix(payments): keep Max transfer amount exact in fiat mode fix(ui): keep Max transfer amount exact in fiat mode Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Both payment view models preserve exact Max amounts in duffs while displaying DASH or fiat values. Unit changes reuse the stored amount, and invalid Max selections are cleared.

Changes

Max amount handling

Layer / File(s) Summary
Send amount state and rendering
DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift
SendViewModel stores exact Max duffs, prioritizes them during parsing, reapplies them after unit changes, and centralizes Max formatting.
Internal transfer amount state and rendering
DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift
InternalTransferViewModel stores exact Max duffs, prioritizes them during parsing, reapplies them after unit changes, and clears failed conversions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: jeanpierreroma, quantumexplorer, romchornyi

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preserving the exact Max amount when using fiat mode.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-fiat-max-rounding

Comment @coderabbitai help to get the list of available commands.

@llbartekll

Copy link
Copy Markdown
Contributor Author

@jeanpierreroma could you please review this fix? It keeps the exact Max amount in duffs when fiat display rounding would otherwise disable Continue.

@llbartekll
llbartekll requested a review from romchornyi August 10, 2026 12:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift (1)

670-700: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add regression tests for exact Max amount state.

The PR changes the executable amount independently from the visible fiat text. Add tests before merge.

  • DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift#L670-L700: test that fiat Max, unit changes, and manual edits retain or clear maxAmountDuffs as required.
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift#L836-L866: test the same Max-state contract for internal transfers.

Use a Max value whose fiat representation rounds before conversion. Assert that dashDuffsUnsigned remains the original duff amount until a manual edit occurs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift` around lines 670 -
700, Add regression tests covering the exact Max amount state in
SendViewModel.swift (lines 670-700) and InternalTransferViewModel.swift (lines
836-866): use a Max value whose fiat display rounds, verify fiat Max selection
and unit changes preserve the original maxDuffs and dashDuffsUnsigned, and
verify manual edits clear that state while retaining the edited amount behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift`:
- Around line 670-700: Add regression tests covering the exact Max amount state
in SendViewModel.swift (lines 670-700) and InternalTransferViewModel.swift
(lines 836-866): use a Max value whose fiat display rounds, verify fiat Max
selection and unit changes preserve the original maxDuffs and dashDuffsUnsigned,
and verify manual edits clear that state while retaining the edited amount
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f77f9c17-6f40-4525-b7d5-4f3dc76e5a1d

📥 Commits

Reviewing files that changed from the base of the PR and between 7e6b393 and 4bc548c.

📒 Files selected for processing (2)
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift
  • DashWallet/Sources/UI/Payments/Pay/SendViewModel.swift

@romchornyi romchornyi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving — the approach is right and the change is minimal.

I pulled the branch and read both view models end to end. The key invariants hold:

  • Manual edits clear Max. amountText.didSet calls clearMaxSelection(), and isApplyingMax is only ever true inside fillMaxFromWallet and the unit didSet, so the exact amount can't survive a user edit.
  • No stale Max across sources. source/destination changes route through routeDidChange()clearMaxSelection() (SendViewModel.swift:262, InternalTransferViewModel.swift:312), so a Max taken from Core can't leak into a Platform or Shielded send.
  • Balance gates are not bypassed. canContinue still checks dashDuffsUnsigned <= coreBalanceDuffs for coreToCore and the partial-withdrawal cap for platformToCore. Exact duffs actually help here: isFullPlatformWithdrawal and creditsPreview no longer drift because of two-decimal fiat rounding.
  • Wider coverage than before. The old preserveMax only applied when isFullShieldedSweep was set; exactness now holds for every route, which is the substance of the fix.
  • Both view models were changed symmetrically.

Two non-blocking notes:

  1. SendViewModel.swift:693 and InternalTransferViewModel.swift:859 — in the fiat-conversion-failure branch only maxAmountDuffs is reset, while isFullShieldedSweep and shieldedSweepAmountCredits stay set from the .ready path in fillMaxFromWallet. That leaves a "full sweep with amount 0" state. It isn't sendable (canContinue requires dashDuffsUnsigned > 0) and any manual edit clears it, so it's not a blocker — but the reset would be better made complete. Note that reusing clearMaxSelection() verbatim would also wipe the Max notice, so it likely needs a narrower reset.

  2. No regression tests. CodeRabbit's point stands: this PR deliberately decouples the displayed amount from the executable amount, and that's exactly the kind of invariant that breaks silently later. DashWalletTests already ships a rates.json fixture, so a "tap Max in fiat → toggle units → dashDuffsUnsigned unchanged" test is feasible, though the singletons (CurrencyExchanger.shared, SwiftDashSDKWalletState.shared) make it more than a one-liner. Not a condition of this approval.

One thing to be aware of: CI here only runs the PR-title check and CodeRabbit — there's no build job, so the xcodebuild result in the description isn't independently verified.

@llbartekll
llbartekll merged commit fb08d72 into develop Aug 10, 2026
3 of 4 checks passed
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