Skip to content

feat(wallet): fund sends from every spendable account - #928

Merged
QuantumExplorer merged 3 commits into
developfrom
feat/pooled-send-funding
Aug 7, 2026
Merged

feat(wallet): fund sends from every spendable account#928
QuantumExplorer merged 3 commits into
developfrom
feat/pooled-send-funding

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 7, 2026

Copy link
Copy Markdown
Member

Adopts .allSpendable from dashpay/platform#4329 on both send paths, replacing the explicit .bip44 pin.

Why this is a bug fix, not just an opt-in

The home balance has always been wallet-wide: refreshBalanceBridge publishes coreWallet().balance() and the initial seed uses getWalletBalance(walletId:) — neither is per-account. But sends could only spend BIP44.

So a user holding funds on BIP32 or a DashPay contact-receiving account saw those funds in their total and got "insufficient funds" trying to spend them. maxSendable derives from the same wallet-wide balance, so it was over-optimistic for exactly the same reason; it becomes correct here.

CoinJoin is not pooled

.allSpendable resolves to SEND_FUNDING_SOURCES = BIP44 + BIP32 + AllDashpayReceivingFunds. CoinJoin is excluded by construction in the Rust const, not merely by convention — spending mixed outputs beside transparent ones links them and undoes the mixing. A contact's watch-only DashpayExternalAccount coins are excluded too: the selector takes only the receiving side the local seed can sign. Change returns to BIP44, the first pooled source.

The mixed balance stays reachable only through the paths that name it explicitly — the CoinJoin sweep and the CoinJoin-drain asset lock — which are the two deliberate "move the mixed coins" flows.

MAYA swap path — checked

The swap deposit takes the same pooled funding. Verified rather than assumed:

  • changeToFirstInput is unaffected. It routes change to selected_inputs.first() post-BIP-69-sort, whatever account that input came from, and the builder already reserves "the largest eligible routing script so every possible winner is covered" when sizing the change output. Account-agnostic by construction.
  • assertSwapDepositShape still holds, because every pooled source is P2PKH. Its comment claimed BIP44 was the only possible source; corrected.
  • One behavioural nuance, now recorded in that comment: VIN0 decides where MAYA sends a refund, so under pooling a refund can land on a DashPay receiving address rather than a BIP44 one. Still this wallet's own seed-signable address, still counted in its balance — but worth knowing.

If a non-P2PKH input ever reached VIN0 the assertion refuses the build: fail-closed, before broadcast.

Left pinned deliberately

Call site Kept as Why
CoinJoin sweep .coinJoin must drain exactly that account
CrowdNode selected-input send .bip44 + addInputs CrowdNode identifies the user by the originating address, so inputs and change must stay on it

Requires an SDK rebuild

Needs DashSDKFFI.xcframework rebuilt from platform v4.2-dev at or past #4329 (adds CORE_ACCOUNT_TYPE_FFI_ALL_SPENDABLE): cd ../platform/packages/swift-sdk && ./build_ios.sh --target ios --target sim.

Max-send reserve

maxSendFeeReserveDuffs() enumerated BIP44 account 0 only — exactly right for the old BIP44-pinned send, but an under-reserve once funding pools BIP32 and DashPay receiving inputs. Since feeAwareMaxSendable() subtracts that reserve from a wallet-wide balance, Max could have priced itself above what the builder can fund. Fixed in f8c03da: the reserve now enumerates the same set .allSpendable spends, still excluding CoinJoin and watch-only external coins.

Verification

dashpay scheme builds clean (arm64 simulator) against a freshly rebuilt xcframework, on a branch merged up to current develop (#925 and #926 are in).

Not exercised on-device: spending pooled funds needs a wallet holding balance on BIP32 or a DashPay receiving account, and the simulator is PIN-gated.

Summary by CodeRabbit

  • Improvements
    • Standard payments and swap deposits can now use all eligible spendable funds, rather than being limited to a single account.
    • Max-send fee estimates now account for the full set of eligible wallet funds, with a safe fallback when balances cannot be fully enumerated.
    • Swap validation supports a broader range of wallet inputs and change addresses, improving compatibility with pooled wallet funds and seed-signable addresses.

Adopts `.allSpendable` (platform #4329) on both send paths, replacing
the explicit `.bip44` pin.

The home balance has always been wallet-wide — `refreshBalanceBridge`
publishes `coreWallet().balance()` and the initial seed uses
`getWalletBalance(walletId:)`, neither of which is per-account — while
sends could only spend BIP44. A user holding funds on BIP32 or a DashPay
contact-receiving account saw them in the total and got "insufficient
funds" trying to spend them. `maxSendable`, derived from that same
balance, was over-optimistic for the same reason; it is now correct.

`.allSpendable` resolves to SEND_FUNDING_SOURCES — BIP44 + BIP32 +
AllDashpayReceivingFunds. CoinJoin is excluded by construction: spending
mixed outputs beside transparent ones links them and undoes the mixing.
A contact's watch-only external coins are excluded too — the selector
takes only the receiving side the local seed can sign. Change returns to
BIP44, the first pooled source.

The MAYA swap deposit takes the same funding. `changeToFirstInput` is
unaffected by pooling: it routes to whichever input BIP-69 puts at VIN0
regardless of source account, and the builder already sizes the change
output for the largest eligible routing script. Its shape assertion
still holds because every pooled source is P2PKH; the comment claiming
BIP44 is the only possible source is corrected, and now records that
VIN0 decides the MAYA refund address, which under pooling can be a
DashPay receiving address — still this wallet's own seed-signable
address, still counted in its balance.

Two call sites keep their explicit pins: the CoinJoin sweep must drain
exactly that account, and the CrowdNode selected-input send must keep
inputs and change on the address CrowdNode identifies the user by.

Requires a DashSDKFFI.xcframework rebuilt from platform v4.2-dev at or
past #4329, which adds CORE_ACCOUNT_TYPE_FFI_ALL_SPENDABLE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Standard payments and swap deposits now use .allSpendable funding across BIP44, BIP32, and DashPay receiving accounts. Max-send fee estimation counts pooled accounts, and swap validation accepts pooled VIN0 change destinations.

Changes

Transaction Funding

Layer / File(s) Summary
Standard payment funding and fee estimation
DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKTransactionSender.swift
Standard payments use pooled BIP44, BIP32, and DashPay receiving sources. Max-send fee estimation counts UTXOs from the same accounts and keeps the fixed reserve fallback when enumeration fails or returns no accounts.
Swap deposit funding and validation
DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKTransactionSender.swift
Swap deposits use .allSpendable funding. VIN0 change validation accepts seed-signable P2PKH addresses from BIP44, BIP32, and DashPay receiving accounts.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: llbartekll, jeanpierreroma

🚥 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: funding sends from all spendable wallet accounts.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pooled-send-funding

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

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKTransactionSender.swift`:
- Around line 110-117: Update maxSendFeeReserveDuffs() to estimate fees against
the same .allSpendable pool used by finalizeAtomic, including BIP44, BIP32, and
eligible DashPay receiving UTXOs while excluding CoinJoin and watch-only
outputs. Use the SDK’s pooled-selection fee estimate if available, otherwise
enumerate that exact pool before calculating the reserve so
feeAwareMaxSendable() cannot exceed what finalizeAtomic can build.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f369d2a5-0ce5-40d1-b7c8-118b87361c4e

📥 Commits

Reviewing files that changed from the base of the PR and between 642e114 and f305b98.

📒 Files selected for processing (1)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKTransactionSender.swift

QuantumExplorer and others added 2 commits August 7, 2026 23:51
Review follow-up, and a regression this PR introduced.

maxSendFeeReserveDuffs() enumerated BIP44 account 0 only. That matched
the old BIP44-pinned send exactly, but once finalizeAtomic pools
BIP44 + BIP32 + DashPay receiving, a real send can carry inputs the
reserve never counted — more inputs, larger transaction, higher fee than
reserved. feeAwareMaxSendable() subtracts that reserve from a
wallet-wide balance, so Max could price itself above what the builder
can fund and fail to build.

The +50 % margin absorbs jitter, not a systematic undercount: BIP44 with
2 UTXOs and a DashPay account with 30 reserves for 2 inputs and spends
32.

The reserve now enumerates the same set `.allSpendable` spends — BIP44
and BIP32 at the funding index plus every DashPay receiving account —
and still excludes CoinJoin and a contact's watch-only external coins
(tag 13), which the builder cannot sign. The flat-reserve fallback and
the safety margin are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit 6822412 into develop Aug 7, 2026
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.

1 participant