Skip to content

fix(wallet): hold back enough on Identity Max for the network to accept it - #1172

Open
romchornyi wants to merge 4 commits into
developfrom
fix/identity-withdraw-fee-reserve
Open

romchornyi wants to merge 4 commits into
developfrom
fix/identity-withdraw-fee-reserve

Conversation

@romchornyi

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

On Internal transfer, Max from Identity to Transparent is always refused by the network. Max held back 0.002 DASH, but Platform checks an IdentityCreditWithdrawal against amount + 0.004 DASH (STATE_TRANSITION_MIN_FEES_VERSION1.credit_withdrawal). The user confirms, then gets a banner with the raw protocol error: Insufficient identity <id> balance <n> required <n>. On testnet the refusal's required was the submitted amount + exactly 400,000,000 credits.

The 0.002 reserve was PlatformPaymentIdentityFundingPolicy.feeHeadroomCredits, which belongs to the opposite direction (funding an identity from addresses). There it is kept small on purpose, so raising it was not an option.

Stacked on #1140 — it uses that PR's IdentityWithdrawViewModel.minimumFeeCredits(target:) and its error-slot rules. Base is fix/max-notice-info-in-error-slot; this PR will be retargeted to develop once #1140 merges.

What was done?

  • IdentityWithdrawViewModel.feeReserveCredits(target:) replaces feeHeadroomCredits and is owned here:
    • .transparent: minimumFeeCredits(.transparent) + 0.001 margin = 0.005 DASH. EvonodeWithdrawalViewModel already holds back the same amount for the same transition.
    • .platform: 0.002 DASH, unchanged (its minimum is 0.000065).
  • spendableCredits(balanceCredits:target:). InternalTransferViewModel passes resolvedWithdrawalTarget to Max, the Continue gate, the inline validation and feeReserveCredits.
  • IdentityWithdrawViewModel.userFacingMessage(for:): PlatformWalletError.insufficientIdentityCredits becomes "Your Identity balance can't cover this amount plus the network fee. Enter a smaller amount and try again." After the reserve fix this can only happen if the balance changes between Continue and Confirm. Today the SDK reports this refusal as an untyped InvalidIdentityData; fix(platform-wallet): report an identity balance refusal on withdrawal as insufficient credits platform#5206 makes it typed. Until the SDK carries that change, the old text is shown in that rare case.

How Has This Been Tested?

  • IdentityBalanceRefreshTests.testIdentityMaxLeavesTheConsensusMinimumFeeForEachTarget: for both targets, Max leaves at least the consensus minimum fee. The suite passes 13/13 on an iOS 26.5 simulator, and scripts/test_identity_balance.py passes 9/9.
  • Testnet, simulator, SDK = dd461eb8bd + fix(platform-wallet): report an identity balance refusal on withdrawal as insufficient credits platform#5206:
    • Identity → Transparent, Max. 0.02744604 → 0.02244604. The confirm sheet shows a fee of ~0.004 DASH. The withdrawal was accepted, leaving 0.00285161 (the real fee was ≈ 0.00215).
    • Identity → Platform, Max. 0.02277031 → 0.02077031. The credits arrived on Platform.
    • Identity balance 0.0019, Transparent. Max fills 0 and shows "Your Identity balance is too low to cover the transfer fee." Continue is disabled.
    • Manual amount over balance − 0.005. Shows "Insufficient balance". Continue is disabled.

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

…pt it

Max from Identity to Transparent held back 0.002 DASH, a reserve borrowed
from identity funding (the opposite direction, where it must stay small).
Platform checks an IdentityCreditWithdrawal against amount + 0.004 DASH,
so every Max was refused at Confirm with the raw protocol error.

The reserve is now owned by the withdrawal and depends on the target:
- Transparent: the 0.004 DASH consensus minimum + 0.001 margin (0.005,
  the same the evonode withdrawal holds back).
- Platform: 0.002 DASH, unchanged — its minimum is 0.000065 DASH.

Max, the Continue gate and the inline validation all use it. A balance
refusal that still reaches Confirm (the balance moved in between) is
shown as a sentence instead of the protocol dump, once the SDK reports
it typed (dashpay/platform#5206).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 36 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 75e34d5c-564b-418d-a6dd-d7bc000b629a

📥 Commits

Reviewing files that changed from the base of the PR and between 2156bb8 and 57f7631.

📒 Files selected for processing (47)
  • DashWallet/Sources/UI/Menu/Tools/Masternode Withdrawal/EvonodeWithdrawalViewModel.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/IdentityWithdrawViewModel.swift
  • DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferViewModel.swift
  • DashWallet/ar.lproj/Localizable.strings
  • DashWallet/bg.lproj/Localizable.strings
  • DashWallet/ca.lproj/Localizable.strings
  • DashWallet/cs.lproj/Localizable.strings
  • DashWallet/da.lproj/Localizable.strings
  • DashWallet/de.lproj/Localizable.strings
  • DashWallet/el.lproj/Localizable.strings
  • DashWallet/en.lproj/Localizable.strings
  • DashWallet/eo.lproj/Localizable.strings
  • DashWallet/es.lproj/Localizable.strings
  • DashWallet/et.lproj/Localizable.strings
  • DashWallet/fa.lproj/Localizable.strings
  • DashWallet/fi.lproj/Localizable.strings
  • DashWallet/fil.lproj/Localizable.strings
  • DashWallet/fr.lproj/Localizable.strings
  • DashWallet/hr.lproj/Localizable.strings
  • DashWallet/hu.lproj/Localizable.strings
  • DashWallet/id.lproj/Localizable.strings
  • DashWallet/it.lproj/Localizable.strings
  • DashWallet/ja.lproj/Localizable.strings
  • DashWallet/ko.lproj/Localizable.strings
  • DashWallet/mk.lproj/Localizable.strings
  • DashWallet/ms.lproj/Localizable.strings
  • DashWallet/nb.lproj/Localizable.strings
  • DashWallet/nl.lproj/Localizable.strings
  • DashWallet/pl.lproj/Localizable.strings
  • DashWallet/pt.lproj/Localizable.strings
  • DashWallet/ro.lproj/Localizable.strings
  • DashWallet/ru.lproj/Localizable.strings
  • DashWallet/sk.lproj/Localizable.strings
  • DashWallet/sl.lproj/Localizable.strings
  • DashWallet/sl_SI.lproj/Localizable.strings
  • DashWallet/sq.lproj/Localizable.strings
  • DashWallet/sr.lproj/Localizable.strings
  • DashWallet/sv.lproj/Localizable.strings
  • DashWallet/th.lproj/Localizable.strings
  • DashWallet/tr.lproj/Localizable.strings
  • DashWallet/uk.lproj/Localizable.strings
  • DashWallet/vi.lproj/Localizable.strings
  • DashWallet/zh-Hans.lproj/Localizable.strings
  • DashWallet/zh-Hant-TW.lproj/Localizable.strings
  • DashWallet/zh.lproj/Localizable.strings
  • DashWallet/zh_TW.lproj/Localizable.strings
  • DashWalletTests/IdentityBalanceRefreshTests.swift
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@thepastaclaw

thepastaclaw commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit 57f7631) · triage: low

@thepastaclaw thepastaclaw 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.

Final validation — Phase 1 + Phase 2

The PR correctly separates identity-withdrawal fee reserves by target and threads the target-specific reserve through Max, validation, and Continue gating. The consensus-floor arithmetic and error handling are otherwise sound, but two non-blocking issues remain: the new user-facing string is absent from the source localization catalog, and a doc comment describes a typed SDK error path that the current withdrawal/transfer implementation does not produce.

🟡 1 suggestion(s) | 💬 1 nitpick(s)

Review provenance

Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: glm-5.3-flash (agent: phase1-reviewer, role: security-auditor); reviewer 3: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: low by gpt-6-astra (effort low) — The diff is a small, contained adjustment to target-specific withdrawal fee reserves and their validation call sites, with straightforward error messaging and regression tests, rather than a large or intricate change to funds movement.
  • Phase 1 reviewers: glm-5.3-flash — general (completed, effort high); agent phase1-reviewer, glm-5.3-flash — security-auditor (completed, effort high); agent phase1-reviewer
  • Phase 1 model: glm-5.3-flash — zai quota: 5h 99% left, weekly 82% left; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 13% left, 5h 100% left)
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort medium); agent phase2-reviewer, gpt-6-astra — security-auditor (completed, effort medium); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `DashWallet/Sources/UI/Payments/InternalTransfer/IdentityWithdrawViewModel.swift`:
- [SUGGESTION] DashWallet/Sources/UI/Payments/InternalTransfer/IdentityWithdrawViewModel.swift:184-185: Add the new withdrawal error to the source localization catalog
  This new user-visible string is passed through NSLocalizedString but is absent from DashWallet/en.lproj/Localizable.strings. English currently falls back to the key, but the source catalog is what localization tooling and Transifex use to discover strings, so translators cannot receive this message. Add the key/value pair to the English source catalog and let the localization tooling propagate it to other locales.
- [NITPICK] DashWallet/Sources/UI/Payments/InternalTransfer/IdentityWithdrawViewModel.swift:176-180: Document that the typed balance-refusal branch is not yet reachable for withdrawals
  The comment says this branch is reachable when the balance changes between Continue and Confirm. In the current SDK route used here, ManagedPlatformWallet.withdrawCredits and transferCreditsToAddresses call FFI operations whose Rust wrappers stringify operation failures into InvalidIdentityData; the typed InsufficientIdentityCredits promotion is used by the DPNS document-trade path instead. Consequently, a stale-balance withdrawal currently keeps the SDK's raw description and does not enter this branch. Rewrite the comment as an explicit TODO describing the SDK prerequisite, while retaining the branch for the typed error once the withdrawal/transfer mapping is shipped.

Comment on lines +184 to +185
"Your Identity balance can't cover this amount plus the network fee. Enter a smaller amount and try again.",
comment: "Identity withdrawal — Platform refused the amount as more than the balance can cover")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Suggestion: Add the new withdrawal error to the source localization catalog

This new user-visible string is passed through NSLocalizedString but is absent from DashWallet/en.lproj/Localizable.strings. English currently falls back to the key, but the source catalog is what localization tooling and Transifex use to discover strings, so translators cannot receive this message. Add the key/value pair to the English source catalog and let the localization tooling propagate it to other locales.

source: glm-5.3-flash (phase1-reviewer: general, security-auditor)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9d858d0 — the key is now in all 43 Localizable.strings catalogs (English everywhere until Transifex supplies translations), placed where the catalog tooling sorts it; bartycrouch lint reports only the two pre-existing duplicates.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resolved (re-reviewed at 9d858d0b): Your new error key is present in the English source catalog and all 42 other app catalogs, with English values pending translation. I verified that each catalog contains exactly the intended three-line addition, remains UTF-8 without a BOM, and passes plist syntax validation.

Comment on lines +176 to +180
/// Platform's balance refusal (`IdentityInsufficientBalanceError`) reads
/// as a protocol dump with the identity id and two raw credit figures;
/// say what it means instead. Reachable when the balance moved between
/// Continue and Confirm — the reserve above keeps Max itself clear of it.
/// Everything else keeps the SDK's own description.

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: Document that the typed balance-refusal branch is not yet reachable for withdrawals

The comment says this branch is reachable when the balance changes between Continue and Confirm. In the current SDK route used here, ManagedPlatformWallet.withdrawCredits and transferCreditsToAddresses call FFI operations whose Rust wrappers stringify operation failures into InvalidIdentityData; the typed InsufficientIdentityCredits promotion is used by the DPNS document-trade path instead. Consequently, a stale-balance withdrawal currently keeps the SDK's raw description and does not enter this branch. Rewrite the comment as an explicit TODO describing the SDK prerequisite, while retaining the branch for the typed error once the withdrawal/transfer mapping is shipped.

source: glm-5.3-flash (phase1-reviewer: general, security-auditor)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9d858d0 — the doc comment now carries a TODO(SwiftDashSDK) saying the .insufficientIdentityCredits branch is not reached until the SDK ships dashpay/platform#5206; the branch itself stays.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resolved (re-reviewed at 9d858d0b): Your new TODO explicitly identifies dashpay/platform#5206 as the prerequisite for reaching the typed balance-refusal branch, while preserving the future handling. I traced both current SDK operations and confirmed that their balance refusals still become untyped InvalidIdentityData, matching the documentation.

… dependency

- Add the new refusal string to every Localizable.strings (English in all
  locales until Transifex supplies translations), at the position the
  catalog tooling sorts it to.
- Say in the doc comment that the typed branch is not reached until the
  SDK ships dashpay/platform#5206, as a TODO rather than a present-tense
  claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@thepastaclaw thepastaclaw 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.

Re-review — Final validation — Phase 1 + Phase 2

At exact head 9d858d0, the complete PR diff consistently applies target-specific reserves to Identity Max, inline validation, and Continue; both prior findings are fixed, and no new in-scope defects were identified. Independent validation passed the standalone identity suite (9/9), extracted production reserve/error-mapping checks (3/3), and content, UTF-8 encoding, and plist validation for all 43 localization catalogs. Full app tests and runtime smoke remain unverified because the pinned DashUIKit requires Swift tools 6.3 while the installed toolchain provides 6.1; SwiftFormat lint also reports formatting violations.

🔴 0 blocking | 🟡 0 suggestion(s) | 💬 0 nitpick(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — The diff changes IdentityWithdrawViewModel and InternalTransferViewModel logic governing spendable balances and fee reserves for identity-to-transparent/platform withdrawals, directly affecting funds movement and transaction amount selection.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 13% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify the current code and confirm that no unresolved issues remain.

No unresolved findings remain from the prior review on this head.

@llbartekll llbartekll 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.

LGTM — approving.

Checked:

  • Root cause is right. The old feeHeadroomCredits (0.002 DASH) was below the 0.004 DASH credit_withdrawal minimum that consensus adds to the amount. So every Identity → Transparent Max was refused. A target-specific reserve is the right fix. Leaving PlatformPaymentIdentityFundingPolicy alone is also right, since its reserve covers the funding direction.
  • Call sites. Max, inline validation, the Continue gate and the VM-level feeReserveCredits all take resolvedWithdrawalTarget. No caller of the old feeHeadroomCredits is left. The confirm sheet still shows minimumFeeCredits, a lower bound, which is fine.
  • Target flip after Max. routeDidChange() → clearMaxSelection() drops the Max flag and keeps the typed amount. Platform-Max followed by a switch to Transparent then shows "Insufficient balance" with Continue disabled, rather than sending something consensus would refuse. That matches how the other routes behave.
  • Arithmetic in the new test. For both targets, balance ≥ max + minimumFee. A 400M-credit balance on Transparent gives 0. /1000 flooring to duffs only lowers the amount.
  • Error mapping. InternalTransferRunner goes through withdrawExecutor.withdraw, so userFacingMessage(for:) is on the path that showed the raw protocol text. The TODO about the typed error waiting on dashpay/platform#5206 is accurate.
  • Localization. All 43 catalogs get the key once, plutil -lint passes on all of them, and the encoding (UTF-8) matches the base branch.

One non-blocking nit inline.

/// exposed for IdentityCreditWithdrawal / identity credit transfer.
static func feeReserveCredits(target: IdentityWithdrawalTarget) -> UInt64 {
switch target {
case .transparent: return minimumFeeCredits(target: .transparent) + 100_000_000

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.

nit (non-blocking): this is now the second literal for the same 0.005 DASH IdentityCreditWithdrawal reserve. EvonodeWithdrawalViewModel.feeReserveCredits is 500_000_000, and the doc comment above says the two are "the same". If either one is retuned later, that claim stops being true without anything flagging it. Consider having one derive from the other, e.g. EvonodeWithdrawalViewModel.feeReserveCredits = IdentityWithdrawViewModel.feeReserveCredits(target: .transparent), or a tiny assertion test. Fine as a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 57f7631: EvonodeWithdrawalViewModel.feeReserveCredits and estimatedFeeCredits now read IdentityWithdrawViewModel.feeReserveCredits(target: .transparent) / minimumFeeCredits(target: .transparent), so there is one source for both. Values are unchanged.

Base automatically changed from fix/max-notice-info-in-error-slot to develop October 2, 2026 10:25
jeanpierreroma and others added 2 commits October 2, 2026 13:28
…dentity one

Both run IdentityCreditWithdrawal, so the masternode reserve and fee figure
now read IdentityWithdrawViewModel's values instead of repeating the
literals. Values are unchanged: 500,000,000 and 400,000,000 credits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@thepastaclaw thepastaclaw 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.

Re-review — Final validation — Phase 1 + Phase 2

Reviewed the complete base-to-head diff at 57f7631 and the relevant validation, Max, confirmation, and withdrawal callers; no in-scope actionable issues were found. Target-specific reserves are applied consistently, masternode withdrawals retain their existing values through the shared source, and both prior findings are fixed. Verification was static only: the supplied exact-head CI snapshot contains successful accessibility, PR-title, and CodeRabbit checks, but no build or test results; runtime smoke results remain author-reported evidence.

🔴 0 blocking | 🟡 0 suggestion(s) | 💬 0 nitpick(s)

Review provenance

Source: reviewer 1: gemini-3.8-flash-high (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: low by gpt-6.1-sol (effort low) — The diff makes a small, contained change to target-specific withdrawal reserves and their validation call sites, with error messaging and regression coverage, rather than large or intricate changes to funds movement or consensus rules.
  • Phase 1 reviewers: gemini-3.8-flash-high — general (completed, effort high); agent phase1-reviewer
  • Phase 1 model: gemini-3.8-flash-high — antigravity quota: weekly 49% left, 5h 73% left
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort medium); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify the current code and confirm that no unresolved issues remain.

No unresolved findings remain from the prior review on this head.

This branch has not been deployed

No deployments
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.

4 participants