Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ members = [
]

[workspace.dependencies]
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "dca5b05b83a9a2d6fd7b650e7cce4356d1c2ec29" }
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "944e53a503a88daa0d7706d7d7c5f5350213d295" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "944e53a503a88daa0d7706d7d7c5f5350213d295" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "944e53a503a88daa0d7706d7d7c5f5350213d295" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "944e53a503a88daa0d7706d7d7c5f5350213d295" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "944e53a503a88daa0d7706d7d7c5f5350213d295" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "944e53a503a88daa0d7706d7d7c5f5350213d295" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "944e53a503a88daa0d7706d7d7c5f5350213d295" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "944e53a503a88daa0d7706d7d7c5f5350213d295" }

tokio-metrics = "0.5"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ class CoreTransactionBuilder internal constructor(network: Network) : AutoClosea
BIP44(0),
BIP32(1),
COIN_JOIN(2),

/**
* Pool every spendable transparent source: BIP44 + BIP32 + all DashPay
* contact-receiving accounts. Change returns to BIP44 (the first
* pooled source). CoinJoin stays out (separate privacy domain), as do
* a contact's watch-only external coins. The default for a plain send.
*/
ALL_SPENDABLE(3),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,19 @@ class ManagedPlatformWallet internal constructor(
)
}

/** Standard account derivation shape for [sendToAddresses]. */
/** Funding-source selector for [sendToAddresses] / [buildSignedPayment]. */
enum class AccountType(val ffiValue: Int) {
BIP44(0),
BIP32(1),

/**
* Pool every spendable transparent source — BIP44 + BIP32 + all
* DashPay contact-receiving accounts — with change returning to
* BIP44. CoinJoin is excluded (separate privacy domain), as are a
* contact's watch-only coins. The default for plain sends, so funds
* a contact paid us are spendable without picking an account.
*/
ALL_SPENDABLE(3),
}

/**
Expand Down Expand Up @@ -142,7 +151,7 @@ class ManagedPlatformWallet internal constructor(
recipients: List<Pair<String, Long>>,
network: org.dashfoundation.dashsdk.Network,
coreSignerHandle: Long,
accountType: AccountType = AccountType.BIP44,
accountType: AccountType = AccountType.ALL_SPENDABLE,
accountIndex: Int = 0,
): String = gate.op {
require(accountIndex >= 0) { "accountIndex must be non-negative, got $accountIndex" }
Expand All @@ -153,6 +162,7 @@ class ManagedPlatformWallet internal constructor(
val builderAccountType = when (accountType) {
AccountType.BIP44 -> CoreTransactionBuilder.AccountType.BIP44
AccountType.BIP32 -> CoreTransactionBuilder.AccountType.BIP32
AccountType.ALL_SPENDABLE -> CoreTransactionBuilder.AccountType.ALL_SPENDABLE
}
mapNativeErrors {
val builder = CoreTransactionBuilder(network)
Expand Down Expand Up @@ -341,7 +351,7 @@ class ManagedPlatformWallet internal constructor(
recipients: List<Pair<String, Long>>,
network: org.dashfoundation.dashsdk.Network,
coreSignerHandle: Long,
accountType: AccountType = AccountType.BIP44,
accountType: AccountType = AccountType.ALL_SPENDABLE,
accountIndex: Int = 0,
opReturnData: ByteArray? = null,
preserveOutputOrder: Boolean = false,
Expand All @@ -365,6 +375,7 @@ class ManagedPlatformWallet internal constructor(
val builderAccountType = when (accountType) {
AccountType.BIP44 -> CoreTransactionBuilder.AccountType.BIP44
AccountType.BIP32 -> CoreTransactionBuilder.AccountType.BIP32
AccountType.ALL_SPENDABLE -> CoreTransactionBuilder.AccountType.ALL_SPENDABLE
}
mapNativeErrors {
// One atomic native operation: select + reserve + sign + register.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::error::*;
use crate::handle::*;
use crate::runtime::runtime;
use crate::{check_ptr, unwrap_option_or_return, unwrap_result_or_return};
use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference;
use std::ffi::CString;
use std::os::raw::c_char;

Expand Down Expand Up @@ -65,7 +64,12 @@ pub unsafe extern "C" fn core_wallet_set_gap_limit(
account_index: u32,
gap_limit: u32,
) -> PlatformWalletFFIResult {
let source: AccountTypePreference = account_type.into();
let Some(source) = account_type.single_preference() else {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"AllSpendable pools multiple accounts; set gap limits per account".to_string(),
);
};

let option = CORE_WALLET_STORAGE.with_item(handle, |wallet| {
runtime().block_on(wallet.set_gap_limit(source, account_index, gap_limit))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ mod tests {
&Address::dummy(Network::Testnet, usize::from(tag)),
1_000_000,
),
AccountTypePreference::BIP44,
&[AccountTypePreference::BIP44],
0,
signer,
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,38 @@ pub enum CoreAccountTypeFFI {
BIP44,
BIP32,
CoinJoin,
/// Pool every spendable transparent source: BIP44 + BIP32 + all DashPay
/// contact-receiving accounts (`platform_wallet::SEND_FUNDING_SOURCES`).
/// Change returns to BIP44 (the first pooled source). CoinJoin stays out
/// (separate privacy domain), as do a contact's watch-only external
/// coins. The default selector for a plain send.
AllSpendable,
}

impl From<CoreAccountTypeFFI> for AccountTypePreference {
fn from(value: CoreAccountTypeFFI) -> Self {
match value {
CoreAccountTypeFFI::BIP44 => AccountTypePreference::BIP44,
CoreAccountTypeFFI::BIP32 => AccountTypePreference::BIP32,
CoreAccountTypeFFI::CoinJoin => AccountTypePreference::CoinJoin,
impl CoreAccountTypeFFI {
/// The single account family this selector names, or `None` for the
/// pooled [`AllSpendable`](Self::AllSpendable) — used by APIs that address
/// exactly one account (gap limits, per-account UTXO listing), which must
/// reject the pooled selector with a typed parameter error.
pub(crate) fn single_preference(self) -> Option<AccountTypePreference> {
match self {
CoreAccountTypeFFI::BIP44 => Some(AccountTypePreference::BIP44),
CoreAccountTypeFFI::BIP32 => Some(AccountTypePreference::BIP32),
CoreAccountTypeFFI::CoinJoin => Some(AccountTypePreference::CoinJoin),
CoreAccountTypeFFI::AllSpendable => None,
}
}

/// The funding sources this selector pools, in funding order — handed to
/// [`CoreWallet::finalize_transaction`]'s multi-source API, whose first
/// source supplies the change address. A single-family selector yields a
/// one-element list, which keeps that API's strict one-account semantics.
pub(crate) fn funding_sources(self) -> &'static [AccountTypePreference] {
match self {
CoreAccountTypeFFI::BIP44 => &[AccountTypePreference::BIP44],
CoreAccountTypeFFI::BIP32 => &[AccountTypePreference::BIP32],
CoreAccountTypeFFI::CoinJoin => &[AccountTypePreference::CoinJoin],
CoreAccountTypeFFI::AllSpendable => &platform_wallet::SEND_FUNDING_SOURCES,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -120,7 +144,7 @@ pub unsafe extern "C" fn core_wallet_tx_builder_finalize(
MnemonicResolverCoreSigner::new(core_signer_handle, wallet.wallet_id(), wallet.network());
let finalized = runtime().block_on(wallet.core().finalize_transaction(
inner,
account_type.into(),
account_type.funding_sources(),
account_index,
&signer,
));
Expand Down Expand Up @@ -255,7 +279,7 @@ pub unsafe extern "C" fn core_wallet_signed_payment_finalize(
// Atomic select + reserve + sign in one wallet-manager critical section.
let finalized = runtime().block_on(wallet.core().finalize_transaction(
inner,
account_type.into(),
account_type.funding_sources(),
account_index,
&signer,
));
Expand Down Expand Up @@ -391,11 +415,9 @@ fn managed_account(
source: AccountTypePreference,
account_index: u32,
) -> Option<&ManagedCoreFundsAccount> {
match source {
AccountTypePreference::BIP44 => accounts.standard_bip44_accounts.get(&account_index),
AccountTypePreference::BIP32 => accounts.standard_bip32_accounts.get(&account_index),
AccountTypePreference::CoinJoin => accounts.coinjoin_accounts.get(&account_index),
}
source
.account_type(account_index)
.and_then(|at| accounts.funds_account(&at))
}

impl FFITransactionBuilder {
Expand Down Expand Up @@ -710,7 +732,12 @@ pub unsafe extern "C" fn core_wallet_tx_builder_add_inputs_from_outpoints(
}

let wallet_id = wallet.wallet_id();
let source: AccountTypePreference = account_type.into();
let Some(source) = account_type.single_preference() else {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"AllSpendable pools multiple accounts; this API addresses exactly one".to_string(),
);
};

let requested: Vec<OutPoint> = if outpoints_len == 0 {
Vec::new()
Expand Down
50 changes: 49 additions & 1 deletion packages/rs-platform-wallet-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,12 @@ impl From<PlatformWalletError> for PlatformWalletFFIResult {
PlatformWalletError::AddressNonceMismatch { .. } => {
PlatformWalletFFIResultCode::ErrorAddressNonceMismatch
}
PlatformWalletError::CoreInsufficientFunds { .. } => {
// Both shapes are "the wallet cannot cover this payment"; hosts
// classify and retry them identically, so the pooled variant rides
// the same code rather than forcing every host to learn a second
// insufficient-funds value.
PlatformWalletError::CoreInsufficientFunds { .. }
| PlatformWalletError::CorePooledInsufficientFunds { .. } => {
PlatformWalletFFIResultCode::ErrorCoreInsufficientFunds
}
PlatformWalletError::AssetLockNotTracked(..) => {
Expand Down Expand Up @@ -508,6 +513,13 @@ impl From<PlatformWalletError> for PlatformWalletFFIResult {
PlatformWalletError::MessageSigningMessageInvalid { .. } => {
PlatformWalletFFIResultCode::ErrorInvalidParameter
}
// A caller-argument rejection raised below the FFI boundary — the
// same class the boundary itself rejects with this code, so both
// sides agree instead of one reporting a not-found or an internal
// failure for a bad argument.
PlatformWalletError::InvalidParameter(..) => {
PlatformWalletFFIResultCode::ErrorInvalidParameter
}
// A second producer of code 31 (the arm above is the first),
// reached without any marker inspection at this layer: message
// signing found no signable account for the address, or the signer
Expand Down Expand Up @@ -853,6 +865,42 @@ mod tests {
}
}

/// A pooled shortfall is the same thing to a host as a single-account one —
/// "this wallet cannot cover the payment" — so it deliberately rides the
/// SAME code rather than making every host learn a second value. Pin that,
/// since splitting it later would silently reclassify the most common send
/// failure on the pooled (default) path.
#[test]
fn pooled_insufficient_funds_shares_the_single_account_code() {
let result: PlatformWalletFFIResult = PlatformWalletError::CorePooledInsufficientFunds {
sources: vec![
AccountTypePreference::BIP44,
AccountTypePreference::BIP32,
AccountTypePreference::AllDashpayReceivingFunds,
],
available: Some(1_000),
required: Some(2_000),
}
.into();
assert_eq!(
result.code,
PlatformWalletFFIResultCode::ErrorCoreInsufficientFunds
);
}

/// A caller-argument rejection raised BELOW the FFI boundary must reach the
/// host as the same parameter error the boundary itself returns — not as a
/// not-found, and not through the `ErrorUnknown` catch-all.
#[test]
fn invalid_parameter_maps_to_the_parameter_code() {
let result: PlatformWalletFFIResult =
PlatformWalletError::InvalidParameter("names a set of accounts".to_string()).into();
assert_eq!(
result.code,
PlatformWalletFFIResultCode::ErrorInvalidParameter
);
}

#[test]
fn asset_lock_recovery_failures_map_to_stable_codes() {
use dashcore::OutPoint;
Expand Down
Loading
Loading