diff --git a/Cargo.lock b/Cargo.lock index 9ea5c271632..0bdbd033393 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,7 +1229,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -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", @@ -2474,7 +2474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2535,7 +2535,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -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" @@ -3588,7 +3588,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -3839,7 +3839,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -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", @@ -5709,7 +5709,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.6.4", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -5747,9 +5747,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6556,7 +6556,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6569,7 +6569,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6628,7 +6628,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7488,7 +7488,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8937,7 +8937,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] 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..67348971c7a 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,17 @@ 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`. + /// + /// key-wallet #818 turned `AddressInfo`'s flat `used` bool into the + /// `AddressState { Available | Reserved { at } | Used }` lifecycle. + /// This schema predates that and has no slot for a reservation, so + /// `Reserved` necessarily flattens to `false` and reloads as + /// `Available`. Nothing in platform reserves addresses today, so + /// that lossy case is currently 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..73bcfb16e46 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,31 @@ fn build_core_address_entry_ffi( key_type_tag, pool_type_tag, address_index: info.index, - is_used: info.used, + // key-wallet #818: `used` bool -> `state` enum (Used == funded). + // + // `CoreAddressEntryFFI` predates that enum and 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` + // anywhere 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 +3247,23 @@ 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 }, + // key-wallet #818: the flat `used`/`generated_at`/`used_at` fields + // became the `state` enum. The persisted entry only carries + // `is_used`, so it round-trips to `Used`/`Available`; see + // `build_core_address_entry_ffi` for why `Reserved` is unreachable + // here. + // + // Dropping `generated_at`/`used_at` is lossless because neither was + // ever persisted: `CoreAddressEntryFFI` has no field for them, so + // this constructor was the only thing that produced them, from the + // literals `0` / `Some(0)`. Upstream never had real values either — + // every key-wallet `AddressInfo` constructor set `generated_at: 0` + // ("Should use actual timestamp"). No stored information is lost. + state: if entry.is_used { + AddressState::Used + } else { + AddressState::Available + }, tx_count: 0, total_received: 0, total_sent: 0, @@ -3270,7 +3310,8 @@ 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 { + // key-wallet #818: `used` bool -> `state` enum (Used == funded). + 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 +6289,8 @@ 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, + // key-wallet #818: `used`/`generated_at`/`used_at` -> `state` enum. + state: AddressState::Available, tx_count: 0, total_received: 0, total_sent: 0, @@ -6370,6 +6410,50 @@ mod tests { } } + /// Pin what the persisted address row does to key-wallet #818's + /// `AddressState`. `CoreAddressEntryFFI` predates the enum and 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 +7156,8 @@ mod tests { public_key: Some(PublicKeyType::ECDSA(TEST_PUBKEY_G.to_vec())), index, path, - used: true, - generated_at: 0, - used_at: None, + // key-wallet #818: `used`/`generated_at`/`used_at` -> `state`. + state: AddressState::Used, tx_count: 0, total_received: 0, total_sent: 0, @@ -7124,7 +7207,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..3251220486a 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,13 @@ mod tests { public_key: Some(PublicKeyType::ECDSA(TEST_PUBKEY_G.to_vec())), index, path, - used: true, - generated_at: 0, - used_at: None, + // key-wallet #818 replaced the flat `used`/`generated_at`/ + // `used_at` fields with the `state` enum. This stub models a + // used address, so `used: true` maps to `AddressState::Used`. + // The new `Used` variant carries no timestamp, so the former + // `generated_at`/`used_at` values have no equivalent and are + // dropped (they were write-only in this test stub). + 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..522ff1c1d63 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,10 @@ 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; + // key-wallet #818: the flat `used` bool became the + // `state` enum. Marking the address used is now + // `AddressState::Used`. + info.state = AddressState::Used; marked_used.push(DerivedAddressInfo { account_type: *owner_type, pool_type: pool.pool_type, @@ -836,7 +839,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 +905,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..39057a919b0 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,15 @@ impl PlatformWalletManager

{ .address_pools() .iter() .fold((0u32, 0u32), |(used, total), pool| { - let pool_used = - pool.addresses.values().filter(|info| info.used).count() as u32; + // key-wallet #818: `used` bool -> `state` enum. + // "used" counts only funded addresses, so match on + // `AddressState::Used` (a `Reserved` address is not + // used); this preserves the pre-#818 count exactly. + 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 +1119,8 @@ fn addr_info_snapshot(info: &AddressInfo) -> AccountAddressInfoSnapshot { AccountAddressInfoSnapshot { pubkey_hash, address_index: info.index, - is_used: info.used, + // key-wallet #818: `used` bool -> `state` enum (Used == funded). + 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..34e65693d16 100644 --- a/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs +++ b/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs @@ -107,13 +107,25 @@ impl AssetLockManager { }; // 3. Delegate to the key-wallet signer-driven builder. + // + // key-wallet #915 changed the builder signature: the funding + // source is now an explicit `AssetLockFundingAccount` (BIP44 vs + // CoinJoin) and a `drain` flag was added. This call site has + // always funded from the standard BIP44 account (`account_index` + // is documented as a BIP44 account index) and never drained, so + // `Bip44 { account_index }` + `drain = false` reproduces the + // pre-#915 behavior exactly. + // TODO(baseline-bump): confirm `drain` intent with QE — defaulted + // to `false` to preserve pre-#915 behavior (non-drain BIP44 funding). + use key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockFundingAccount; 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 +949,13 @@ 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| { + // key-wallet #818: `used` bool -> `state` enum. + matches!( + a.state, + key_wallet::managed_account::address_pool::AddressState::Used + ) + }) }) }); assert!( @@ -1343,7 +1361,17 @@ mod tests { .iter() .filter(|e| matches!(e.account_type, AccountType::IdentityInvitation)) { - let used = entry.addresses.iter().filter(|a| a.used).count(); + // key-wallet #818: `used` bool -> `state` enum. + 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..e164b30b924 100644 --- a/packages/rs-platform-wallet/src/wallet/core/transaction.rs +++ b/packages/rs-platform-wallet/src/wallet/core/transaction.rs @@ -150,10 +150,21 @@ impl CoreWallet { // `set_funding` observes ReservationSet and `build_unsigned` // records its selection. There is no await between them and the // manager write guard prevents another finalizer interleaving. - let (unsigned, fee) = builder + // + // key-wallet #916 renamed `build_unsigned` to + // `build_unsigned_reserved`, which additionally surfaces the + // `ReservationToken` stamped onto the reserved inputs. Its body is + // otherwise identical to the old `build_unsigned` (the reservation + // was always recorded inside `assemble_unsigned`). This baseline + // path releases abandoned reservations via the unconditional + // `release_reservation(&unsigned)` below, exactly as before, so the + // token is intentionally discarded here to preserve pre-#916 + // behavior. Owner-guarded release via the token is the TOCTOU fix + // threaded by dashpay/platform#4185, not baseline maintenance. + 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..24e00f05617 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,12 @@ 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, + // key-wallet #818: the flat `used`/`generated_at`/`used_at` + // fields became the `state` enum. A freshly derived, unfunded + // address is `AddressState::Available` (the former + // `used: false`); the removed timestamps have no equivalent on + // the new `Available` variant. + 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..2102c6a397c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift @@ -79,7 +79,30 @@ public struct AddressInfo { public let publicKey: Data? public let index: UInt32 public let path: String + + /// Whether funds have been seen at this address. + /// + /// key-wallet #818 replaced `AddressInfo`'s flat `used` flag with a + /// three-state `AddressState { Available | Reserved { at } | Used }` + /// lifecycle. `FFIAddressInfo.used` is now 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). + /// + /// key-wallet #818 removed `generated_at` from `FFIAddressInfo`, and + /// none of the new `AddressState` variants carries a generation + /// timestamp, so there is nothing left to read. This is *not* a + /// substituted value: every `AddressInfo` upstream ever produced was + /// constructed with the literal placeholder `generated_at: 0` + /// (annotated "Should use actual timestamp" in key-wallet), 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; do not read it as a real generation time. public let generatedAt: Date init(ffiInfo: FFIAddressInfo) { @@ -114,6 +137,10 @@ public struct AddressInfo { } self.used = ffiInfo.used - self.generatedAt = Date(timeIntervalSince1970: TimeInterval(ffiInfo.generated_at)) + // key-wallet #818 dropped `generated_at` from the C struct. See the + // property doc: the field it used to read was always the literal 0, + // so pinning the epoch here reproduces the previous value exactly + // rather than inventing one. + self.generatedAt = Date(timeIntervalSince1970: 0) } }