From 6a85ffab5c2ad2d470b3c938131325768a4a3e4a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 6 Aug 2026 03:33:24 +0700 Subject: [PATCH 1/2] chore(platform-wallet): bump key-wallet pin and migrate to new AddressState/asset-lock API Repin all 8 rust-dashcore crates 70d4bf8e -> 8f78baa6 and migrate the baseline to the key-wallet API changes that rev carries: - #818: AddressInfo's flat used/generated_at/used_at fields became state: AddressState { Available | Reserved | Used }. Mechanical, behavior-preserving migration of all 15 sites. Reserved is unrepresentable in the persisted address schema; the emit path warns instead of silently flattening and a round-trip test pins the boundary. - #915: build_asset_lock_with_signer takes an explicit funding account and drain flag; mapped to Bip44 + non-drain, the only behavior-preserving choice (CoinJoin funding is drain-only upstream). - #916: build_unsigned -> build_unsigned_reserved; the ReservationToken (no Drop side effects) is discarded, preserving the unconditional release_reservation flow. Owner-guarded release lands with #4185. - Swift: AddressPool.swift read the removed generated_at from the regenerated cbindgen header; generatedAt is pinned to the epoch (the value it always evaluated to) so call sites keep compiling. Supersedes #4299 with a minimal Cargo.lock diff (pin entries only, no unrelated transitive downgrades) and constraint-only comments. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 24 ++--- Cargo.toml | 16 ++-- .../src/core_address_types.rs | 10 +- .../rs-platform-wallet-ffi/src/persistence.rs | 94 ++++++++++++++++--- .../src/changeset/changeset.rs | 6 +- .../src/changeset/core_bridge.rs | 8 +- .../src/manager/accessors.rs | 15 ++- .../src/wallet/asset_lock/build.rs | 28 +++++- .../src/wallet/core/transaction.rs | 13 ++- .../src/wallet/provider_key_at_index.rs | 6 +- .../SwiftDashSDK/KeyWallet/AddressPool.swift | 24 ++++- 11 files changed, 184 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ea5c271632..d3c69ffce11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1662,7 +1662,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "bincode", "bincode_derive", @@ -1673,7 +1673,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "dash-network", ] @@ -1750,7 +1750,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "async-trait", "chrono", @@ -1779,7 +1779,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "anyhow", "base64-compat", @@ -1805,12 +1805,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "dashcore-rpc-json", "hex", @@ -1823,7 +1823,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "bincode", "dashcore", @@ -1838,7 +1838,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "bincode", "dashcore-private", @@ -2904,7 +2904,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" [[package]] name = "glob" @@ -4095,7 +4095,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "aes", "async-trait", @@ -4124,7 +4124,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4140,7 +4140,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 86e5432b7ef..3c7a1ad0760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,14 +52,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } tokio-metrics = "0.5" diff --git a/packages/rs-platform-wallet-ffi/src/core_address_types.rs b/packages/rs-platform-wallet-ffi/src/core_address_types.rs index c82827b67d1..3aa930abf02 100644 --- a/packages/rs-platform-wallet-ffi/src/core_address_types.rs +++ b/packages/rs-platform-wallet-ffi/src/core_address_types.rs @@ -76,7 +76,15 @@ pub struct CoreAddressEntryFFI { pub pool_type_tag: u8, /// Derivation index within this pool. pub address_index: u32, - /// `AddressInfo.used` at emit time. + /// Whether funds had been seen at this address at emit time, i.e. + /// `AddressInfo.state == AddressState::Used`. + /// + /// This schema has no slot for a reservation, so an + /// `AddressState::Reserved` entry necessarily flattens to `false` + /// and reloads as `Available`. Nothing in platform reserves + /// addresses today, so that lossy case is unreachable; adding a + /// reserving caller requires a schema decision here first (see + /// `persistence::build_core_address_entry_ffi`). pub is_used: bool, /// Cached balance in duffs from `AddressInfo.balance`. pub balance: u64, diff --git a/packages/rs-platform-wallet-ffi/src/persistence.rs b/packages/rs-platform-wallet-ffi/src/persistence.rs index be30d937dd0..fc28dd2e5a9 100644 --- a/packages/rs-platform-wallet-ffi/src/persistence.rs +++ b/packages/rs-platform-wallet-ffi/src/persistence.rs @@ -12,7 +12,9 @@ use key_wallet::bip32::DerivationPath; use key_wallet::bip32::ExtendedPubKey; use key_wallet::derivation_bls_bip32::ExtendedBLSPubKey; use key_wallet::derivation_slip10::ExtendedEd25519PubKey; -use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType, PublicKeyType}; +use key_wallet::managed_account::address_pool::{ + AddressPool, AddressPoolType, AddressState, PublicKeyType, +}; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::wallet::Wallet; @@ -3125,7 +3127,28 @@ fn build_core_address_entry_ffi( key_type_tag, pool_type_tag, address_index: info.index, - is_used: info.used, + // `CoreAddressEntryFFI` carries a single `is_used` bool with no + // slot for a reservation, so `Reserved` can only flatten to + // `false` and would reload as `Available` — silently returning a + // handed-out address to the pool. Nothing in platform reserves + // addresses (no caller of `next_receive_address_and_reserve` / + // `next_unused_and_reserve` in the workspace), so that arm is + // unreachable today. It is spelled out rather than folded into a + // catch-all so the first reserving caller shows up as an explicit + // schema decision instead of losing state on the next reload. + is_used: match info.state { + AddressState::Used => true, + AddressState::Available => false, + AddressState::Reserved { .. } => { + tracing::warn!( + index = info.index, + "persist: address pool entry is reserved, but the persisted \ + address schema cannot represent a reservation; it will \ + reload as available and may be handed out again" + ); + false + } + }, balance: info.balance, address_base58: address_ptr, derivation_path: path_ptr, @@ -3221,9 +3244,14 @@ unsafe fn address_info_from_ffi( public_key, index: entry.address_index, path, - used: entry.is_used, - generated_at: 0, - used_at: if entry.is_used { Some(0) } else { None }, + // The persisted entry only carries `is_used`, so state round-trips + // to `Used`/`Available`; a `Reserved` entry is not representable — + // see `build_core_address_entry_ffi`. + state: if entry.is_used { + AddressState::Used + } else { + AddressState::Available + }, tx_count: 0, total_received: 0, total_sent: 0, @@ -3270,7 +3298,7 @@ fn restore_address_pool(pool: &mut AddressPool, infos: Vec) { pool.script_pubkey_index .insert(info.script_pubkey.clone(), idx); pool.highest_generated = Some(pool.highest_generated.map_or(idx, |h| h.max(idx))); - if info.used { + if matches!(info.state, AddressState::Used) { pool.used_indices.insert(idx); pool.highest_used = Some(pool.highest_used.map_or(idx, |h| h.max(idx))); } @@ -6248,9 +6276,7 @@ mod tests { index, path: DerivationPath::from_str(&format!("m/9'/1'/2'/{}", index)) .expect("static derivation path must parse"), - used: false, - generated_at: 0, - used_at: None, + state: AddressState::Available, tx_count: 0, total_received: 0, total_sent: 0, @@ -6370,6 +6396,50 @@ mod tests { } } + /// Pin what the persisted address row does to `AddressState`. + /// `CoreAddressEntryFFI` carries a single `is_used` bool, so only + /// `Used` and `Available` survive a save/load cycle intact; + /// `Reserved { .. }` has no representation and comes back + /// `Available`, i.e. a reload silently frees a handed-out address. + /// Nothing in platform reserves addresses today, so this records the + /// boundary rather than a live bug: if a reserving caller is ever + /// added, this test is where the schema decision (a dedicated + /// reservation field on the row) has to be made. + #[test] + fn address_state_round_trip_keeps_used_and_flattens_reserved() { + for (state, expected) in [ + (AddressState::Available, AddressState::Available), + (AddressState::Used, AddressState::Used), + ( + AddressState::Reserved { at: 1_700_000_000 }, + AddressState::Available, + ), + ] { + let mut info = typed_key_test_address_info(11, None); + info.state = state; + + let mut owned: Vec = Vec::new(); + let entry = build_core_address_entry_ffi( + &info, + AddressPoolTypeTagFFI::AbsentHardened as u8, + false, + &mut owned, + ) + .expect("build_core_address_entry_ffi must succeed"); + // SAFETY: the address / path c-strings live in `owned`, kept + // alive until after this decode. + let restored = unsafe { address_info_from_ffi(&entry, Network::Testnet) } + .expect("address_info_from_ffi must decode the row"); + drop(owned); + + assert_eq!( + restored.state, expected, + "{:?} must restore as {:?} through the persisted row", + state, expected + ); + } + } + /// A LEGACY row (persisted before the typed-key column: empty key, /// `public_key: None` after decode) must NOT strip the typed key the /// gap-limit prederivation put at the same index — pre-typed-key @@ -7072,9 +7142,7 @@ mod tests { public_key: Some(PublicKeyType::ECDSA(TEST_PUBKEY_G.to_vec())), index, path, - used: true, - generated_at: 0, - used_at: None, + state: AddressState::Used, tx_count: 0, total_received: 0, total_sent: 0, @@ -7124,7 +7192,7 @@ mod tests { entries .iter() .flat_map(|e| e.addresses.iter()) - .all(|a| a.used), + .all(|a| matches!(a.state, AddressState::Used)), "every emitted marked-used address must carry used == true" ); } diff --git a/packages/rs-platform-wallet/src/changeset/changeset.rs b/packages/rs-platform-wallet/src/changeset/changeset.rs index 1e302d2ea32..2b417e4c3be 100644 --- a/packages/rs-platform-wallet/src/changeset/changeset.rs +++ b/packages/rs-platform-wallet/src/changeset/changeset.rs @@ -2158,7 +2158,7 @@ mod tests { index: u32, ) -> key_wallet::transaction_checking::DerivedAddressInfo { use key_wallet::bip32::{ChildNumber, DerivationPath}; - use key_wallet::managed_account::address_pool::{AddressInfo, PublicKeyType}; + use key_wallet::managed_account::address_pool::{AddressInfo, AddressState, PublicKeyType}; let pubkey = dashcore::PublicKey::from_slice(&TEST_PUBKEY_G).expect("generator point is valid"); @@ -2177,9 +2177,7 @@ mod tests { public_key: Some(PublicKeyType::ECDSA(TEST_PUBKEY_G.to_vec())), index, path, - used: true, - generated_at: 0, - used_at: None, + state: AddressState::Used, tx_count: 0, total_received: 0, total_sent: 0, diff --git a/packages/rs-platform-wallet/src/changeset/core_bridge.rs b/packages/rs-platform-wallet/src/changeset/core_bridge.rs index b700e73dcaf..abafe2ef9b3 100644 --- a/packages/rs-platform-wallet/src/changeset/core_bridge.rs +++ b/packages/rs-platform-wallet/src/changeset/core_bridge.rs @@ -31,7 +31,7 @@ use std::sync::Arc; use dashcore::blockdata::transaction::{txout::TxOut, OutPoint}; use dashcore::ScriptBuf; use key_wallet::account::AccountType; -use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType}; +use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType, AddressState}; use key_wallet::managed_account::transaction_record::{OutputRole, TransactionRecord}; use key_wallet::transaction_checking::transaction_router::AccountTypeToCheck; use key_wallet::transaction_checking::{DerivedAddressInfo, TransactionContext}; @@ -565,7 +565,7 @@ fn collect_usage_deltas_from_accounts( touched.insert(*owner_type); if seen.insert((*owner_type, pool.pool_type, pool_info.index)) { let mut info = pool_info.clone(); - info.used = true; + info.state = AddressState::Used; marked_used.push(DerivedAddressInfo { account_type: *owner_type, pool_type: pool.pool_type, @@ -836,7 +836,7 @@ mod usage_delta_tests { assert_eq!(entry.account_type, bip44_account_0()); assert_eq!(entry.pool_type, AddressPoolType::External); assert_eq!(entry.info.index, 0); - assert!(entry.info.used); + assert!(matches!(entry.info.state, AddressState::Used)); let watermarks = highest .get(&bip44_account_0()) @@ -902,7 +902,7 @@ mod usage_delta_tests { .find(|d| d.info.address == receive_address) .expect("spent-input address must be in the marked-used delta"); assert_eq!(entry.pool_type, AddressPoolType::External); - assert!(entry.info.used); + assert!(matches!(entry.info.state, AddressState::Used)); // The foreign output must NOT resolve to any pool. assert!( marked.iter().all(|d| d.info.address != { diff --git a/packages/rs-platform-wallet/src/manager/accessors.rs b/packages/rs-platform-wallet/src/manager/accessors.rs index 7dfc444c833..ef0c780e3eb 100644 --- a/packages/rs-platform-wallet/src/manager/accessors.rs +++ b/packages/rs-platform-wallet/src/manager/accessors.rs @@ -5,7 +5,9 @@ use std::sync::Arc; use dashcore::{OutPoint, Txid}; use dpp::prelude::Identifier; use key_wallet::account::AccountType; -use key_wallet::managed_account::address_pool::{AddressInfo, AddressPool, AddressPoolType}; +use key_wallet::managed_account::address_pool::{ + AddressInfo, AddressPool, AddressPoolType, AddressState, +}; use key_wallet::managed_account::transaction_record::TransactionRecord; use key_wallet::utxo::Utxo; use key_wallet::WalletCoreBalance; @@ -400,8 +402,13 @@ impl PlatformWalletManager

{ .address_pools() .iter() .fold((0u32, 0u32), |(used, total), pool| { - let pool_used = - pool.addresses.values().filter(|info| info.used).count() as u32; + // "used" counts only funded addresses; a `Reserved` + // address is handed out but not yet used. + let pool_used = pool + .addresses + .values() + .filter(|info| matches!(info.state, AddressState::Used)) + .count() as u32; let pool_total = pool.addresses.len() as u32; (used + pool_used, total + pool_total) }); @@ -1110,7 +1117,7 @@ fn addr_info_snapshot(info: &AddressInfo) -> AccountAddressInfoSnapshot { AccountAddressInfoSnapshot { pubkey_hash, address_index: info.index, - is_used: info.used, + is_used: matches!(info.state, AddressState::Used), address, public_key_bytes, } diff --git a/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs b/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs index 3fbe004aa1f..108172420fb 100644 --- a/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs +++ b/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs @@ -13,7 +13,7 @@ use key_wallet::bip32::DerivationPath; use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::signer::ExtendedPubKeySigner; use key_wallet::wallet::managed_wallet_info::asset_lock_builder::{ - AssetLockFundingType, CreditOutputFunding, + AssetLockFundingAccount, AssetLockFundingType, CreditOutputFunding, }; use key_wallet::wallet::managed_wallet_info::managed_account_operations::ManagedAccountOperations; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; @@ -106,14 +106,18 @@ impl AssetLockManager { identity_index, }; - // 3. Delegate to the key-wallet signer-driven builder. + // 3. Delegate to the key-wallet signer-driven builder. Platform + // asset locks fund from the standard BIP44 account and never + // drain; upstream only supports non-drain funding for BIP44 + // (CoinJoin funding is drain-only). let result = info .core_wallet .build_asset_lock_with_signer( wallet, - account_index, + AssetLockFundingAccount::Bip44 { account_index }, vec![funding], DEFAULT_FEE_PER_KB, + false, signer, ) .await @@ -937,7 +941,12 @@ mod tests { let persisted_invitation_used = stored.iter().any(|cs| { cs.account_address_pools.iter().any(|entry| { matches!(entry.account_type, AccountType::IdentityInvitation) - && entry.addresses.iter().any(|a| a.used) + && entry.addresses.iter().any(|a| { + matches!( + a.state, + key_wallet::managed_account::address_pool::AddressState::Used + ) + }) }) }); assert!( @@ -1343,7 +1352,16 @@ mod tests { .iter() .filter(|e| matches!(e.account_type, AccountType::IdentityInvitation)) { - let used = entry.addresses.iter().filter(|a| a.used).count(); + let used = entry + .addresses + .iter() + .filter(|a| { + matches!( + a.state, + key_wallet::managed_account::address_pool::AddressState::Used + ) + }) + .count(); assert!( used >= last_used, "invitation pool snapshot rolled back: {used} used after {last_used}" diff --git a/packages/rs-platform-wallet/src/wallet/core/transaction.rs b/packages/rs-platform-wallet/src/wallet/core/transaction.rs index 81b2e8a8249..a42b3de7310 100644 --- a/packages/rs-platform-wallet/src/wallet/core/transaction.rs +++ b/packages/rs-platform-wallet/src/wallet/core/transaction.rs @@ -147,13 +147,18 @@ impl CoreWallet { )) })?; - // `set_funding` observes ReservationSet and `build_unsigned` - // records its selection. There is no await between them and the + // `set_funding` observes ReservationSet and the build records + // its selection. There is no await between them and the // manager write guard prevents another finalizer interleaving. - let (unsigned, fee) = builder + // + // The `ReservationToken` is intentionally discarded: this path + // releases abandoned reservations via the unconditional + // `release_reservation(&unsigned)` below. Owner-guarded release + // via the token is threaded separately (dashpay/platform#4185). + let (unsigned, fee, _reservation) = builder .set_current_height(height) .set_funding(managed, &account) - .build_unsigned() + .build_unsigned_reserved() .map_err(|error| map_builder_error(error, account_type, account_index))?; let selected: Vec = match unsigned diff --git a/packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs b/packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs index 1584f7eb73e..54f579e3247 100644 --- a/packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs +++ b/packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs @@ -204,7 +204,7 @@ pub fn populate_platform_node_pool( network: key_wallet::Network, ) -> Result<(), PlatformWalletError> { use dashcore::hashes::Hash; - use key_wallet::managed_account::address_pool::{AddressPoolType, PublicKeyType}; + use key_wallet::managed_account::address_pool::{AddressPoolType, AddressState, PublicKeyType}; use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::AddressInfo; @@ -247,9 +247,7 @@ pub fn populate_platform_node_pool( public_key: Some(PublicKeyType::EdDSA(key.public_key.to_vec())), index: key.index, path, - used: false, - generated_at: 0, - used_at: None, + state: AddressState::Available, tx_count: 0, total_received: 0, total_sent: 0, diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift index 23378ea6dc6..f6b03675c7f 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift @@ -79,7 +79,27 @@ public struct AddressInfo { public let publicKey: Data? public let index: UInt32 public let path: String + + /// Whether funds have been seen at this address. + /// + /// Upstream, addresses have a three-state + /// `AddressState { Available | Reserved | Used }` lifecycle, and + /// `FFIAddressInfo.used` is derived as `state == .used` — so an + /// address that has been *reserved* (handed out but not yet funded) + /// still reports `used == false`. The C surface carries no + /// reservation flag, so reservation state is not observable from + /// Swift; if that is ever needed, `FFIAddressInfo` has to gain a + /// field upstream in key-wallet-ffi first. public let used: Bool + + /// Always the Unix epoch (1970-01-01) — do not read this as a real + /// generation time. + /// + /// key-wallet no longer exposes a generation timestamp, and the value + /// it used to expose was always the literal placeholder `0`, so this + /// property has always evaluated to the epoch. It is retained only so + /// existing call sites keep compiling and observe the same value as + /// before. public let generatedAt: Date init(ffiInfo: FFIAddressInfo) { @@ -114,6 +134,8 @@ public struct AddressInfo { } self.used = ffiInfo.used - self.generatedAt = Date(timeIntervalSince1970: TimeInterval(ffiInfo.generated_at)) + // The C struct no longer carries `generated_at`; see the property + // doc for why the epoch reproduces the previous value exactly. + self.generatedAt = Date(timeIntervalSince1970: 0) } } From bdb3ab1996f01cce6c93369565a1546408fcad9e Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 6 Aug 2026 06:13:12 +0700 Subject: [PATCH 2/2] fix(sdk): point parity manifest at the funding-type gate test renamed by #4015 The Kotlin SDK CI job validates docs/sdk/sdk-parity-manifest.json against the Swift test sources. Commit 1de13e9685 (#4015) replaced testInvitationVouchersAreExcludedFromResumableList with the broader testFundingTypeGateAcceptsOnlyGenericIdentityResumeTypes in CreateIdentityResumableTests.swift without updating the manifest, so the parity check fails for every branch based on the current v4.2-dev tip. Same capability (invitation-funded locks excluded from resume), new name. Co-Authored-By: Claude Fable 5 --- docs/sdk/sdk-parity-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdk/sdk-parity-manifest.json b/docs/sdk/sdk-parity-manifest.json index 8d7e023a197..26101839912 100644 --- a/docs/sdk/sdk-parity-manifest.json +++ b/docs/sdk/sdk-parity-manifest.json @@ -630,7 +630,7 @@ "host": "swift", "kind": "unit", "file": "packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/CreateIdentityResumableTests.swift", - "id": "testInvitationVouchersAreExcludedFromResumableList", + "id": "testFundingTypeGateAcceptsOnlyGenericIdentityResumeTypes", "command": "xcodebuild test -project packages/swift-sdk/SwiftExampleApp/SwiftExampleApp.xcodeproj -scheme SwiftExampleApp", "covers_restart": false },