Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
aaf8be7
fix(rs-platform-wallet): auto_select_inputs honors Σ inputs == Σ outputs
lklimek Apr 28, 2026
9ea9e70
fix(rs-platform-wallet): reserve fee headroom at DeductFromInput(0) t…
lklimek Apr 28, 2026
687b1f8
test(rs-platform-wallet): protocol-level reproduction of CodeRabbit f…
lklimek Apr 28, 2026
60f7850
refactor(rs-platform-wallet): sort auto-select candidates by balance …
lklimek Apr 28, 2026
9ff937f
fix(rs-platform-wallet): enforce min_input_amount, restrict fee_strat…
lklimek Apr 28, 2026
79c2b28
ci(rs-packages-filter): trigger Rust workspace tests on rs-platform-w…
lklimek Apr 28, 2026
d610502
Merge remote-tracking branch 'origin/v3.1-dev' into fix/rs-platform-w…
lklimek Apr 28, 2026
3c4f919
fix(rs-platform-wallet): clippy-clean for Rust 1.92 (workspace tests …
lklimek Apr 28, 2026
d0c772f
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek Apr 29, 2026
45779e3
docs(rs-platform-wallet): trim verbose comments in auto_select_inputs…
lklimek Apr 29, 2026
74b81d1
feat(rs-platform-wallet): support ReduceOutput(0) fee strategy in aut…
lklimek Apr 29, 2026
e16a819
Merge remote-tracking branch 'origin/v3.1-dev' into fix/rs-platform-w…
lklimek Apr 30, 2026
357ddb8
docs(rs-platform-wallet): note platform #3040 fee-estimation bug in R…
lklimek Apr 30, 2026
545fa99
Merge remote-tracking branch 'origin/v3.1-dev' into fix/rs-platform-w…
lklimek Apr 30, 2026
ea7810f
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 4, 2026
5e50a0b
fix(rs-platform-wallet): exclude output addresses from auto_select_in…
lklimek May 4, 2026
923992a
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 4, 2026
b6e7d05
chore(rs-platform-wallet): fix macOS clippy lints in manager/accessor…
lklimek May 4, 2026
ef4345a
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 5, 2026
376ef56
chore(rs-platform-wallet): remove stale QA-001-followup TODO
lklimek May 5, 2026
dac5a7a
feat(rs-platform-wallet): add address_derivation_info and fee_paid ac…
lklimek Apr 27, 2026
468e774
revert(rs-platform-wallet): drop test-only production additions; abso…
lklimek Apr 27, 2026
bdd46f3
fix(rs-platform-wallet): defensive checked arithmetic on Credits in t…
lklimek May 4, 2026
47997a2
refactor(rs-platform-wallet): collapse estimate_fee_for_inputs_pub wr…
lklimek May 4, 2026
f81cdf2
test(rs-platform-wallet): tighten non_fee_target_below_min_input_redi…
lklimek May 4, 2026
196f6c5
feat(rs-platform-wallet): typed OnlyOutputAddressesFunded error [CMT-…
lklimek May 4, 2026
b326a18
chore(rs-platform-wallet): drop useless vec! in detect_only_output_ad…
lklimek May 4, 2026
92bc962
chore(rs-platform-wallet): drop stray ManagedIdentitySigner re-export
lklimek May 5, 2026
6b530e5
ci: clean stale mac runner artifacts before rust tests
vivekgsharma May 5, 2026
bf99f0b
fix(rs-platform-wallet): saturating arithmetic on Credits in selector…
lklimek May 5, 2026
0cec379
fix(rs-platform-wallet): explicit runtime invariant checks in selecto…
lklimek May 5, 2026
61c6ab8
fix(rs-platform-wallet): rename OnlyOutputAddressesFunded to NoSelect…
lklimek May 5, 2026
d67fd1d
feat(rs-platform-wallet): balance-descending donor in ReduceOutput Ph…
lklimek May 5, 2026
07d046b
feat(rs-platform-wallet): borderline warn-log for ReduceOutput(0) cha…
lklimek May 5, 2026
7b45af2
refactor(rs-platform-wallet): extract select_inputs shared helpers + …
lklimek May 5, 2026
4853cf9
fix(rs-platform-wallet): split NoSelectableInputs into discrete varia…
lklimek May 5, 2026
fa3e676
fix(rs-platform-wallet): use saturating arithmetic for outputs aggreg…
lklimek May 5, 2026
00ac7f2
chore(rs-platform-wallet): document accepted-risk on selector heurist…
lklimek May 5, 2026
2dec6a6
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 6, 2026
6574b84
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 7, 2026
5ef1b44
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 8, 2026
e46d12a
refactor(platform-wallet/auto_select_inputs): inline helpers, collaps…
lklimek May 8, 2026
e986342
fix(rs-platform-wallet): pin unreachable selector branches with debug…
lklimek May 8, 2026
9e9d289
refactor(rs-platform-wallet): tighten transfer.rs narration, TODO lin…
lklimek May 8, 2026
eb68dba
fix(rs-platform-wallet): NoSelectableInputs Display omits zero-valued…
lklimek May 8, 2026
35f0e98
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 8, 2026
776cbd7
feat(rs-platform-wallet): output_change_address override on platform-…
lklimek May 8, 2026
22149b0
Merge remote-tracking branch 'origin/v3.1-dev' into fix/rs-platform-w…
lklimek May 8, 2026
0a5aa2e
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 11, 2026
25bbb3f
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 12, 2026
b66e7dd
chore: fmt
lklimek May 13, 2026
ea8dc63
test(rs-platform-wallet): cover reduce_output Phase 1/3 + wrapper Aut…
lklimek May 13, 2026
9a4fc30
docs(rs-platform-wallet): document DeductFromInput contract + trim ve…
lklimek May 13, 2026
047ffca
docs(rs-platform-wallet): clarify donor selection + sub-minimum guard
lklimek May 13, 2026
79c83c6
refactor(rs-platform-wallet): extract saturating_sum + change-addr co…
lklimek May 13, 2026
9ee4e3c
refactor(rs-platform-wallet): remove format_address (duplicates Display)
lklimek May 13, 2026
aba7ca3
refactor(rs-platform-wallet): split NoSelectableInputs into typed var…
lklimek May 13, 2026
4526836
ci: revert tests-rs-workspace.yml disk-cleanup step
lklimek May 13, 2026
72fee50
fix(rs-platform-wallet-ffi): drop stale ptr arg from integration_tests
lklimek May 13, 2026
edd3ea5
chore: cargo fmt
lklimek May 13, 2026
c5511a7
chore(rs-platform-wallet-ffi): use Result::is_err in group_info tests
lklimek May 13, 2026
30e612b
Merge remote-tracking branch 'origin/v3.1-dev' into fix/rs-platform-w…
lklimek May 14, 2026
436d38b
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 19, 2026
d5d2b3d
Merge remote-tracking branch 'origin/v3.1-dev' into fix/rs-platform-w…
lklimek May 20, 2026
098484f
fix(platform-wallet): reject sub-minimum change residual pre-broadcast
lklimek May 21, 2026
8ee78a4
docs(platform-wallet): rewrite InputSelection::Auto contract
lklimek May 21, 2026
3aeed87
fix(platform-wallet): include addresses_derived in is_empty_no_records
lklimek May 21, 2026
6c3fb02
fix(platform-wallet): defensively sort selector inputs balance-descen…
lklimek May 21, 2026
e2140bd
fix(platform-wallet): recompute fee against selected count post-Phase-4
lklimek May 21, 2026
b85b6a9
fix(platform-wallet): preserve dust info in OnlyOutputAddressesFunded
lklimek May 21, 2026
cfe3c33
fix(platform-wallet): checked overflow on caller-supplied input sums
lklimek May 21, 2026
9854e38
refactor(platform-wallet)!: take outputs as IndexMap and guard change…
lklimek May 21, 2026
9c136cd
feat(platform-wallet-ffi,swift): typed FFI codes for selector errors
lklimek May 21, 2026
7c7a2ec
docs(platform-wallet): mark QA-007/CMT-004 follow-up TODOs
lklimek May 21, 2026
2957623
style(platform-wallet): cargo fmt after auto-select fixes
lklimek May 21, 2026
94bbb64
Merge branch 'v3.1-dev' into fix/rs-platform-wallet-auto-select-inputs
lklimek May 21, 2026
ceb4fc8
Merge remote-tracking branch 'origin/v3.1-dev' into mb-pr3554
lklimek May 22, 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
2 changes: 2 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions packages/rs-platform-wallet-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ bincode = { version = "=2.0.1" }
# Hex used for error diagnostics that include a wallet_id.
hex = "0.4"

# `IndexMap` mirrors `platform-wallet`'s insertion-ordered outputs API.
indexmap = "2.7"

# Persistence loader emits structured warnings for skipped /
# corrupt rows so operators can detect snapshot drift without a
# native debugger attached.
Expand Down
53 changes: 52 additions & 1 deletion packages/rs-platform-wallet-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ pub enum PlatformWalletFFIResultCode {
ErrorInvalidIdentifier = 10,
ErrorMemoryAllocation = 11,
ErrorUtf8Conversion = 12,
/// `PlatformWalletError::OnlyOutputAddressesFunded`: auto-selection
/// found that every funded address is also a destination output.
ErrorOnlyOutputAddressesFunded = 13,
/// `PlatformWalletError::OnlyDustInputs`: auto-selection found that
/// every funded address is below `min_input_amount`.
ErrorOnlyDustInputs = 14,

NotFound = 98, // Used exclusively for all the Option that are retuned as errors
ErrorUnknown = 99,
Expand Down Expand Up @@ -156,7 +162,16 @@ impl<T> From<Option<T>> for PlatformWalletFFIResult {

impl From<PlatformWalletError> for PlatformWalletFFIResult {
fn from(error: PlatformWalletError) -> Self {
PlatformWalletFFIResult::err(PlatformWalletFFIResultCode::ErrorUnknown, error.to_string())
let code = match &error {
PlatformWalletError::OnlyOutputAddressesFunded { .. } => {
PlatformWalletFFIResultCode::ErrorOnlyOutputAddressesFunded
}
PlatformWalletError::OnlyDustInputs { .. } => {
PlatformWalletFFIResultCode::ErrorOnlyDustInputs
}
_ => PlatformWalletFFIResultCode::ErrorUnknown,
};
PlatformWalletFFIResult::err(code, error.to_string())
}
}

Expand Down Expand Up @@ -368,6 +383,42 @@ mod tests {
assert!(r.message.is_null());
}

/// CMT-003: typed `PlatformWalletError` variants route to the
/// dedicated FFI codes, not the catch-all `ErrorUnknown`.
#[test]
fn typed_errors_route_to_dedicated_codes() {
use dpp::address_funds::PlatformAddress;
let cases: Vec<(PlatformWalletError, PlatformWalletFFIResultCode)> = vec![
(
PlatformWalletError::OnlyOutputAddressesFunded {
funded_outputs: vec![PlatformAddress::P2pkh([0u8; 20])],
sub_min_count: 0,
sub_min_aggregate: 0,
min_input_amount: 100_000,
},
PlatformWalletFFIResultCode::ErrorOnlyOutputAddressesFunded,
),
(
PlatformWalletError::OnlyDustInputs {
sub_min_count: 2,
sub_min_aggregate: 12_345,
min_input_amount: 100_000,
},
PlatformWalletFFIResultCode::ErrorOnlyDustInputs,
),
(
PlatformWalletError::AddressOperation("plain string".to_string()),
PlatformWalletFFIResultCode::ErrorUnknown,
),
];

for (err, expected) in cases {
let result: PlatformWalletFFIResult = err.into();
assert_eq!(result.code, expected);
assert!(!result.message.is_null());
}
}

#[test]
fn nul_in_message_is_replaced() {
let r = PlatformWalletFFIResult::err(
Expand Down
11 changes: 8 additions & 3 deletions packages/rs-platform-wallet-ffi/src/platform_address_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,23 @@ pub struct AddressBalanceEntryFFI {
pub address_index: u32,
}

/// Parse output entries into a BTreeMap.
/// Parse output entries into an insertion-ordered `IndexMap`.
///
/// Mirrors `platform-wallet`'s public-API output ordering (QA-002): the
/// wallet preserves the caller's order for UI/debug while DPP still
/// keys the transition by lex-smallest address. Use `IndexMap` here so
/// the caller's array order survives the FFI boundary.
///
/// # Safety
/// `ptr` must point to `count` valid elements.
pub unsafe fn parse_outputs(
ptr: *const AddressBalanceEntryFFI,
count: usize,
) -> Result<BTreeMap<PlatformAddress, Credits>, &'static str> {
) -> Result<indexmap::IndexMap<PlatformAddress, Credits>, &'static str> {
if ptr.is_null() && count > 0 {
return Err("Null output pointer with non-zero count");
}
let mut map = BTreeMap::new();
let mut map = indexmap::IndexMap::new();
if count > 0 {
for entry in std::slice::from_raw_parts(ptr, count) {
let addr = PlatformAddress::try_from(entry.address)?;
Expand Down
5 changes: 5 additions & 0 deletions packages/rs-platform-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ arc-swap = "1"

# Collections
bimap = "0.6"
# `IndexMap` powers the insertion-ordered public outputs map on
# `PlatformAddressWallet::transfer` / `transfer_with_change_address`.
# Same crate that dpp and rs-sdk already vendor; pin a workspace-aligned
# minor that satisfies all in-tree requirements.
indexmap = "2.7"

# Async runtime
tokio = { version = "1", features = ["sync", "rt", "time", "macros"] }
Expand Down
5 changes: 3 additions & 2 deletions packages/rs-platform-wallet/src/changeset/core_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ async fn build_core_changeset(
addresses_derived,
..
} => {
// Derive UTXO deltas BEFORE moving the record into `records`
// so we still have the per-record borrows.
// Derive UTXO deltas before moving the record into `records`
// so the per-record borrows are still live.
CoreChangeSet {
new_utxos: derive_new_utxos(record),
spent_utxos: derive_spent_utxos(record),
Expand Down Expand Up @@ -354,5 +354,6 @@ impl CoreChangeSet {
&& self.last_processed_height.is_none()
&& self.synced_height.is_none()
&& self.last_applied_chain_lock.is_none()
&& self.addresses_derived.is_empty()
}
}
54 changes: 54 additions & 0 deletions packages/rs-platform-wallet/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use dpp::address_funds::PlatformAddress;
use dpp::fee::Credits;
use dpp::identifier::Identifier;
use key_wallet::Network;

Expand Down Expand Up @@ -72,6 +74,58 @@ pub enum PlatformWalletError {
#[error("Address operation failed: {0}")]
AddressOperation(String),

#[error(
"no selectable inputs: only funded addresses appear as destinations \
(funded_outputs={funded_outputs:?}, sub_min_count={sub_min_count}, \
sub_min_aggregate={sub_min_aggregate}, min_input_amount={min_input_amount}); \
rotate to a fresh receive address, consolidate funds, or use \
InputSelection::Explicit"
)]
OnlyOutputAddressesFunded {
/// Funded addresses dropped by the input-equals-output filter.
funded_outputs: Vec<PlatformAddress>,
/// Number of additional addresses with a positive balance below
/// `min_input_amount`. Preserved even though the output-collision
/// signal is the typically-actionable fix, so a UI rotating to a
/// fresh receive address has the dust breadcrumb on the next try.
sub_min_count: usize,
/// Aggregate of the sub-minimum balances counted in `sub_min_count`.
sub_min_aggregate: Credits,
/// Per-input minimum from the active platform version.
min_input_amount: Credits,
},

#[error(
"no selectable inputs: every funded address is below the per-input \
minimum (sub_min_count={sub_min_count}, sub_min_aggregate={sub_min_aggregate} \
credits, min_input_amount={min_input_amount}); consolidate funds or use \
InputSelection::Explicit"
)]
OnlyDustInputs {
/// Number of addresses with a positive balance below `min_input_amount`.
sub_min_count: usize,
/// Aggregate of those sub-minimum balances.
sub_min_aggregate: Credits,
/// Per-input minimum from the active platform version.
min_input_amount: Credits,
},

#[error(
"change output amount {change_amount} is below the protocol per-output \
minimum {min_output_amount}; raise the input sum or drop the change \
address so the residual would exceed the minimum"
)]
ChangeBelowMinimumOutput {
/// `Σ inputs − Σ user_outputs` — the residual that would have been
/// routed to the change output.
change_amount: Credits,
/// Per-output minimum from the active platform version.
min_output_amount: Credits,
},

#[error("input sum overflow: caller-supplied input balances exceed u64::MAX")]
InputSumOverflow,

#[error("Platform address not found in wallet: {0}")]
AddressNotFound(String),

Expand Down
2 changes: 0 additions & 2 deletions packages/rs-platform-wallet/src/wallet/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,6 @@ impl PlatformWalletInfo {
drop(token_balances);

// 7. Recompute cached UI balance from the now-restored UTXO set.
// `update_balance` returns its own changeset internally; we
// discard it (apply does not re-emit).
use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface;
self.core_wallet.update_balance();
// Mirror the recomputed balance into the lock-free Arc that the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,42 @@ use dpp::address_funds::PlatformAddress;
use dpp::fee::Credits;
pub use dpp::prelude::AddressNonce;

#[cfg(doc)]
use crate::PlatformWalletError;

mod fund_from_asset_lock;
pub(crate) mod provider;
mod sync;
mod transfer;
mod wallet;
mod withdrawal;

/// Saturating sum over `Credits` (== `u64`) — total credit supply is far
/// below `u64::MAX`, so saturation is unreachable in practice but the policy
/// keeps debug-build panics off the table. Use this only for sums over
/// wallet-derived balances; for caller-supplied input maps prefer
/// [`checked_sum_credits`] so a bogus FFI input is reported as
/// [`crate::PlatformWalletError::InputSumOverflow`] rather than silently
/// saturating to `u64::MAX`.
pub(crate) fn saturating_sum_credits<I>(iter: I) -> Credits
where
I: IntoIterator<Item = Credits>,
{
iter.into_iter().fold(0u64, Credits::saturating_add)
}

/// Checked sum over `Credits` for caller-supplied input maps. Returns
/// [`crate::PlatformWalletError::InputSumOverflow`] on overflow so a
/// bogus FFI caller cannot trigger a silent saturation downstream.
pub(crate) fn checked_sum_credits<I>(iter: I) -> Result<Credits, crate::PlatformWalletError>
where
I: IntoIterator<Item = Credits>,
{
iter.into_iter()
.try_fold(0u64, |acc, c| acc.checked_add(c))
.ok_or(crate::PlatformWalletError::InputSumOverflow)
}

pub use provider::{
PerAccountPlatformAddressState, PerWalletPlatformAddressState, PlatformAddressTag,
};
Expand All @@ -24,8 +53,23 @@ pub enum InputSelection {
Explicit(BTreeMap<PlatformAddress, Credits>),
/// Explicit inputs with known nonces and balances.
ExplicitWithNonces(BTreeMap<PlatformAddress, (AddressNonce, Credits)>),
/// Automatically select inputs from the account, consuming addresses
/// from lowest derivation index to highest until the required amount
/// plus estimated fees is covered.
/// Automatically select inputs from the account.
///
/// Candidates are ordered balance-descending, filtered to balances
/// `≥ min_input_amount`, and addresses that also appear as outputs
/// are excluded (DPP rejects same-address input+output). Supported
/// fee strategies: `[DeductFromInput(0)]` (fee comes out of the
/// lex-smallest input's remaining balance) and `[ReduceOutput(0)]`
/// (fee absorbed at chain time from the lex-smallest output);
/// other shapes must use [`Self::Explicit`].
///
/// # Errors
///
/// Typed variants surface diagnosable failure shapes:
/// [`PlatformWalletError::OnlyOutputAddressesFunded`] when every
/// funded address is also a destination,
/// [`PlatformWalletError::OnlyDustInputs`] when every funded balance
/// is below `min_input_amount`, and the generic
/// [`PlatformWalletError::AddressOperation`] otherwise.
Auto,
}
Loading
Loading