Skip to content

Commit 5148368

Browse files
docs(kotlin-sdk): a drain's scope is the account type, which defaults to pooled
`SelectionStrategy.ALL` was documented as "drains the account" -- written when a drain could only mean one account. Since #4329 the send APIs default `accountType` to `ALL_SPENDABLE`, so `ALL` on a call that does not name an account type drains BIP44 AND BIP32 AND every DashPay contact-receiving account in one transaction, change returning to BIP44. That is the intended shape rather than a hazard: "send everything" means everything the wallet can sign for, and achieving it before the pooled selector meant sweeping accounts together on-chain first. Document it on the strategy enum and again on `buildSignedPayment`, where the two parameters meet and where the scope is inherited rather than typed, so a reader can see what a defaulted drain reaches. Naming a single account type is for drains genuinely scoped to one family -- a CoinJoin sweep that must not leave its privacy domain. A test pins the sweep scope per selector, so anything later added to SEND_FUNDING_SOURCES -- which would enlarge every defaulted drain -- fails there first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent d571d3e commit 5148368

3 files changed

Lines changed: 71 additions & 2 deletions

File tree

packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/CoreTransactionBuilder.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,31 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea
4848

4949
/**
5050
* Coin-selection strategy — mirror of key-wallet's `SelectionStrategy`
51-
* (`CoreSelectionStrategyFFI`). [ALL] drains the account.
51+
* (`CoreSelectionStrategyFFI`).
52+
*
53+
* [ALL] drains — it selects every spendable UTXO the chosen funding
54+
* source offers, sets the single destination output to
55+
* `total inputs − fee`, and leaves no change. **Its scope is whatever
56+
* [AccountType] names, not "the wallet's main account":**
57+
*
58+
* - [AccountType.BIP44] / [AccountType.BIP32] / [AccountType.COIN_JOIN]
59+
* drain that one account family;
60+
* - [AccountType.ALL_SPENDABLE] — **the default** — drains BIP44 **and**
61+
* BIP32 **and** every DashPay contact-receiving account, in one
62+
* transaction.
63+
*
64+
* So `selectionStrategy = ALL` on a call that does not name an
65+
* [AccountType] sweeps the wallet's whole spendable balance, contact
66+
* receiving accounts included. That is the intended shape of a drain: a
67+
* host asking to send everything means everything it can sign for, and
68+
* before the pooled selector existed a host had to sweep accounts
69+
* together on-chain first to achieve it. Name a single [AccountType] only
70+
* when the drain is genuinely scoped to one family — a CoinJoin sweep,
71+
* say, which must stay in its own privacy domain.
72+
*
73+
* Read what a drain actually pays from
74+
* [SignedCoreTransaction.deliverableAmountDuffs]; the engine computes it,
75+
* and the caller's requested amount is discarded.
5276
*/
5377
enum class SelectionStrategy(val ffiValue: Int) {
5478
SMALLEST_FIRST(0),

packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/ManagedPlatformWallet.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,17 @@ class ManagedPlatformWallet internal constructor(
384384
* [SignedCoreTransaction.deliverableAmountDuffs] BEFORE broadcasting —
385385
* that is the only way to learn the engine-computed amount, and it is
386386
* what a swap quote must be taken from.
387+
*
388+
* **A drain's scope is [accountType], which defaults to
389+
* [CoreTransactionBuilder.AccountType.ALL_SPENDABLE].** Combined with
390+
* `ALL`, a call that does not name an account type sweeps BIP44, BIP32
391+
* AND every DashPay contact-receiving account into one transaction,
392+
* change returning to BIP44. That is the intended shape: "send
393+
* everything" means everything the wallet can sign for, which before
394+
* the pooled selector required sweeping accounts together on-chain
395+
* first. Name a single [CoreTransactionBuilder.AccountType] only for a
396+
* drain genuinely scoped to one family, such as a CoinJoin sweep that
397+
* must not leave its privacy domain.
387398
*/
388399
suspend fun buildSignedPayment(
389400
recipients: List<Pair<String, Long>>,

packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,9 +923,43 @@ pub unsafe extern "C" fn core_wallet_transaction_free(tx: *mut FFICoreTransactio
923923

924924
#[cfg(test)]
925925
mod tests {
926-
use super::sole_deliverable_value;
926+
use super::{sole_deliverable_value, CoreAccountTypeFFI};
927927
use dashcore::blockdata::script::ScriptBuf;
928928
use dashcore::TxOut;
929+
use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference;
930+
931+
/// What a DRAIN spans, per selector. `SelectionStrategy::All` takes every
932+
/// UTXO each named source offers, so this list IS the sweep scope — the
933+
/// claim the Kotlin `SelectionStrategy.ALL` doc makes to callers.
934+
///
935+
/// The pooled selector is the DEFAULT for a send, so a caller who asks for
936+
/// a drain without naming an account type sweeps all three families at
937+
/// once, contact-receiving funds included. Pinned here so that widening
938+
/// cannot happen silently: anything added to `SEND_FUNDING_SOURCES`
939+
/// enlarges every defaulted drain, and this test is where that shows up.
940+
#[test]
941+
fn a_drains_scope_is_whatever_the_selector_names() {
942+
assert_eq!(
943+
CoreAccountTypeFFI::BIP44.funding_sources(),
944+
&[AccountTypePreference::BIP44],
945+
"a single-family selector drains exactly one account"
946+
);
947+
assert_eq!(
948+
CoreAccountTypeFFI::CoinJoin.funding_sources(),
949+
&[AccountTypePreference::CoinJoin],
950+
"CoinJoin stays its own privacy domain, never pooled"
951+
);
952+
assert_eq!(
953+
CoreAccountTypeFFI::AllSpendable.funding_sources(),
954+
&[
955+
AccountTypePreference::BIP44,
956+
AccountTypePreference::BIP32,
957+
AccountTypePreference::AllDashpayReceivingFunds,
958+
],
959+
"the DEFAULT selector drains BIP44 + BIP32 + every DashPay \
960+
receiving account; BIP44 must stay first, as it supplies change"
961+
);
962+
}
929963

930964
/// A spendable output. The script only has to NOT be an OP_RETURN.
931965
fn destination(value: u64) -> TxOut {

0 commit comments

Comments
 (0)