Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4538fc4
feat(platform-wallet): let a Core build fund from only the inputs it …
jeanpierreroma Aug 31, 2026
d207874
feat(platform-wallet): let a Core build fund from only the inputs it …
jeanpierreroma Sep 1, 2026
28044ee
feat(platform-wallet): report the balance a pooled build can actually…
jeanpierreroma Sep 1, 2026
96c8be1
refactor(platform-wallet): drive both funding paths from one account …
jeanpierreroma Sep 3, 2026
c002d33
Merge branch 'v4.2-dev' into feat/pooled-spendable-balance
jeanpierreroma Sep 3, 2026
bb3ea98
test(platform-wallet): pin pooled_spendable_balance to the funding set
jeanpierreroma Sep 3, 2026
dd92e1b
feat(platform-wallet): price the fee into a pooled send-max figure
jeanpierreroma Sep 3, 2026
6444e67
style: rustfmt the new pooled-max-sendable FFI entry point
jeanpierreroma Sep 3, 2026
f95e401
fix(platform-wallet): cap pooled_max_sendable at the standard input l…
jeanpierreroma Sep 3, 2026
2abc906
fix(platform-wallet-ffi): look the pooled-balance handle up in the co…
jeanpierreroma Sep 7, 2026
e9c69dd
fix(platform-wallet): make the pooled send-max figure safe to act on
jeanpierreroma Sep 9, 2026
8a857a8
Merge remote-tracking branch 'origin/v4.2-dev' into feat/pooled-spend…
jeanpierreroma Sep 9, 2026
6ca60d2
Merge branch 'v4.2-dev' into feat/pooled-spendable-balance
romchornyi Sep 10, 2026
61daa8d
fix(platform-wallet): price the pooled maximum the way coin selection…
jeanpierreroma Sep 10, 2026
8a3ecc7
Merge branch 'v4.2-dev' into feat/pooled-spendable-balance
romchornyi Sep 10, 2026
8b6131f
Merge remote-tracking branch 'origin/v4.2-dev' into feat/pooled-spend…
jeanpierreroma Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::core_wallet_types::OutPointFFI;
use crate::error::*;
use crate::handle::{Handle, CORE_SIGNED_TRANSACTION_STORAGE, PLATFORM_WALLET_STORAGE};
use crate::handle::{
Handle, CORE_SIGNED_TRANSACTION_STORAGE, CORE_WALLET_STORAGE, PLATFORM_WALLET_STORAGE,
};
use crate::runtime::runtime;
use crate::types::{FFINetwork, Network};
use crate::{check_ptr, unwrap_option_or_return, unwrap_result_or_return};
Expand Down Expand Up @@ -761,6 +763,79 @@ pub unsafe extern "C" fn core_wallet_tx_builder_use_only_added_inputs(
PlatformWalletFFIResult::ok()
}

/// The balance a build funded by `account_type` could actually select from — the
Comment thread
romchornyi marked this conversation as resolved.
/// same accounts `core_wallet_tx_builder_finalize` would fund from, counting
/// only UTXOs coin selection accepts.
///
/// Gate amount entry on this rather than on `core_wallet_get_balance`, which
/// sums every funding account the wallet has — CoinJoin included — and so
/// reports money a build then refuses.
///
/// Reservations are not subtracted; see `CoreWallet::pooled_spendable_balance`.
///
/// `core_wallet` is the handle `platform_wallet_get_core` returns — the same
/// one `core_wallet_get_balance` takes — NOT the platform-wallet handle the
/// `core_wallet_tx_builder_*` entry points above take. Handles are drawn from
/// one global counter, so a core handle looked up in the platform table (or
/// vice versa) is always `NotFound`, never a wrong wallet.
///
/// # Safety
/// `out_balance` must be a valid, writable pointer.
#[no_mangle]
pub unsafe extern "C" fn core_wallet_pooled_spendable_balance(
core_wallet: Handle,
account_type: CoreAccountTypeFFI,
account_index: u32,
out_balance: *mut u64,
) -> PlatformWalletFFIResult {
check_ptr!(out_balance);
*out_balance = 0;

let core = unwrap_option_or_return!(CORE_WALLET_STORAGE.with_item(core_wallet, |w| w.clone()));
let balance = unwrap_result_or_return!(runtime()
.block_on(core.pooled_spendable_balance(account_type.funding_sources(), account_index)));

*out_balance = balance;
PlatformWalletFFIResult::ok()
}

/// The largest amount a build funded by `account_type` could actually pay out,
/// net of the fee spending it costs — what a "send max" control must use.
///
/// `core_wallet_pooled_spendable_balance` is the gross figure: entering it
/// verbatim as an amount fails, because a build needs `amount + fee`. This
/// prices the fee off the inputs that spending everything would take, at
/// `fee_rate_sat_per_kb` — pass 0 for the same default `TransactionBuilder`
/// starts from, or the rate the host sets on its builders.
///
/// `core_wallet` is the core-wallet handle, as for
/// `core_wallet_pooled_spendable_balance`.
///
/// # Safety
/// `out_max_sendable` must be a valid, writable pointer.
#[no_mangle]
pub unsafe extern "C" fn core_wallet_pooled_max_sendable(
core_wallet: Handle,
account_type: CoreAccountTypeFFI,
account_index: u32,
fee_rate_sat_per_kb: u64,
out_max_sendable: *mut u64,
) -> PlatformWalletFFIResult {
check_ptr!(out_max_sendable);
*out_max_sendable = 0;

let fee_rate = (fee_rate_sat_per_kb != 0).then(|| FeeRate::new(fee_rate_sat_per_kb));
let core = unwrap_option_or_return!(CORE_WALLET_STORAGE.with_item(core_wallet, |w| w.clone()));
let max_sendable = unwrap_result_or_return!(runtime().block_on(core.pooled_max_sendable(
account_type.funding_sources(),
account_index,
fee_rate
)));

*out_max_sendable = max_sendable;
PlatformWalletFFIResult::ok()
}

/// # Safety
/// `builder` must be a valid, non-destroyed pointer.
#[no_mangle]
Expand Down Expand Up @@ -966,6 +1041,93 @@ pub unsafe extern "C" fn core_wallet_transaction_free(tx: *mut FFICoreTransactio
tx.tx_len = 0;
}

#[cfg(test)]
mod pooled_balance_handle_tests {
//! The two pooled-balance entry points are exposed on the *core* wallet
//! (`ManagedCoreWallet` in the Swift SDK), so they must resolve the handle
//! `platform_wallet_get_core` hands out — the `CORE_WALLET_STORAGE` one —
//! not the platform-wallet handle their `core_wallet_tx_builder_*`
//! neighbours take. Looking a core handle up in the platform table failed
//! every call with `ErrorInvalidHandle`; the Swift caller swallowed it and
//! published a permanent 0, which zeroed Max and blocked every send on the
//! 2026-09-03 QA build (dashwallet-ios#1107 / platform#4582).

use key_wallet::account::account_type::StandardAccountType;
use platform_wallet::test_support::funded_spv_core_wallet;
use platform_wallet::SEND_FUNDING_SOURCES;

use super::*;

#[test]
fn pooled_spendable_balance_resolves_the_core_wallet_handle() {
let (core, _signer) =
runtime().block_on(funded_spv_core_wallet(StandardAccountType::BIP44Account));
let expected = runtime()
.block_on(core.pooled_spendable_balance(&SEND_FUNDING_SOURCES, 0))
.expect("direct pooled balance");
assert!(
expected > 0,
"the helper funds BIP44, so the pool is non-empty"
);

let core_handle = CORE_WALLET_STORAGE.insert(core.clone());
let mut out: u64 = 0;
let result = unsafe {
core_wallet_pooled_spendable_balance(
core_handle,
CoreAccountTypeFFI::AllSpendable,
0,
&mut out,
)
};
assert_eq!(result.code, PlatformWalletFFIResultCode::Success);
assert_eq!(out, expected);
CORE_WALLET_STORAGE.remove(core_handle);
}

#[test]
fn pooled_max_sendable_resolves_the_core_wallet_handle() {
let (core, _signer) =
runtime().block_on(funded_spv_core_wallet(StandardAccountType::BIP44Account));
let expected = runtime()
.block_on(core.pooled_max_sendable(&SEND_FUNDING_SOURCES, 0, None))
.expect("direct pooled max");
assert!(expected > 0);

let core_handle = CORE_WALLET_STORAGE.insert(core.clone());
let mut out: u64 = 0;
let result = unsafe {
core_wallet_pooled_max_sendable(
core_handle,
CoreAccountTypeFFI::AllSpendable,
0,
0,
&mut out,
)
};
assert_eq!(result.code, PlatformWalletFFIResultCode::Success);
assert_eq!(out, expected);
CORE_WALLET_STORAGE.remove(core_handle);
}

/// A handle that was never issued for a core wallet is refused, and the
/// out-parameter is left at 0 rather than at a stale value.
#[test]
fn unknown_handle_is_refused_with_zero_out() {
let mut out: u64 = 7;
let result = unsafe {
core_wallet_pooled_spendable_balance(
Handle::MAX,
CoreAccountTypeFFI::AllSpendable,
0,
&mut out,
)
};
assert_ne!(result.code, PlatformWalletFFIResultCode::Success);
assert_eq!(out, 0);
}
}

#[cfg(test)]
mod tests {
use super::{sole_deliverable_value, CoreAccountTypeFFI};
Expand Down
Loading
Loading