diff --git a/dash-spv-ffi/tests/test_wallet_manager.rs b/dash-spv-ffi/tests/test_wallet_manager.rs index dde3af4d6..dcf8891b6 100644 --- a/dash-spv-ffi/tests/test_wallet_manager.rs +++ b/dash-spv-ffi/tests/test_wallet_manager.rs @@ -94,6 +94,7 @@ mod tests { wallet_manager_ptr, serialized_wallet.as_ptr(), serialized_wallet.len(), + 0, imported_wallet_id.as_mut_ptr(), &mut error as *mut FFIError, ); diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index 9fa14f312..c6a91b7b2 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -679,7 +679,7 @@ Get wallet IDs # Safety - `manager` must be a valid pointer to an FFIWalletMan #### `wallet_manager_import_wallet_from_bytes` ```c -wallet_manager_import_wallet_from_bytes(manager: *mut FFIWalletManager, wallet_bytes: *const u8, wallet_bytes_len: usize, wallet_id_out: *mut u8, error: *mut FFIError,) -> bool +wallet_manager_import_wallet_from_bytes(manager: *mut FFIWalletManager, wallet_bytes: *const u8, wallet_bytes_len: usize, birth_height: u32, wallet_id_out: *mut u8, error: *mut FFIError,) -> bool ``` **Module:** `wallet_manager` diff --git a/key-wallet-ffi/src/wallet_manager.rs b/key-wallet-ffi/src/wallet_manager.rs index 05f0ee786..6b246b9da 100644 --- a/key-wallet-ffi/src/wallet_manager.rs +++ b/key-wallet-ffi/src/wallet_manager.rs @@ -295,6 +295,8 @@ pub unsafe extern "C" fn wallet_manager_free_wallet_bytes(wallet_bytes: *mut u8, /// - `manager` must be a valid pointer to an FFIWalletManager instance /// - `wallet_bytes` must be a valid pointer to bincode-serialized wallet bytes /// - `wallet_bytes_len` must be the exact length of the wallet bytes +/// - `birth_height` is the birth height to seed the wallet's sync checkpoint +/// (0 to rescan from genesis) /// - `wallet_id_out` must be a valid pointer to a 32-byte array that will receive the wallet ID /// - `error` must be a valid pointer to an FFIError structure /// - The caller must ensure all pointers remain valid for the duration of this call @@ -304,6 +306,7 @@ pub unsafe extern "C" fn wallet_manager_import_wallet_from_bytes( manager: *mut FFIWalletManager, wallet_bytes: *const u8, wallet_bytes_len: usize, + birth_height: u32, wallet_id_out: *mut u8, error: *mut FFIError, ) -> bool { @@ -316,7 +319,7 @@ pub unsafe extern "C" fn wallet_manager_import_wallet_from_bytes( // Import the wallet using async runtime let result = manager_ref.runtime.block_on(async { let mut manager_guard = manager_ref.manager.write().await; - manager_guard.import_wallet_from_bytes(wallet_bytes_slice) + manager_guard.import_wallet_from_bytes(wallet_bytes_slice, birth_height) }); let wallet_id = unwrap_or_return!(result, error); diff --git a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs index 1e97d8095..c2f2aea8a 100644 --- a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs @@ -252,6 +252,7 @@ mod tests { manager2, wallet_bytes_out, wallet_bytes_len_out, + 0, imported_wallet_id.as_mut_ptr(), error, ) diff --git a/key-wallet-ffi/src/wallet_manager_tests.rs b/key-wallet-ffi/src/wallet_manager_tests.rs index 9cb6ed66f..4549006a2 100644 --- a/key-wallet-ffi/src/wallet_manager_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_tests.rs @@ -949,6 +949,7 @@ mod tests { manager3, wallet_bytes_slice.as_ptr(), wallet_bytes_slice.len(), + 0, import_wallet_id_out.as_mut_ptr(), error, ) @@ -1109,6 +1110,7 @@ mod tests { manager2, wallet_bytes_copy.as_ptr(), wallet_bytes_copy.len(), + 0, import_wallet_id_out.as_mut_ptr(), error, ) diff --git a/key-wallet-ffi/tests/test_import_wallet.rs b/key-wallet-ffi/tests/test_import_wallet.rs index b28cdfece..3abee8cee 100644 --- a/key-wallet-ffi/tests/test_import_wallet.rs +++ b/key-wallet-ffi/tests/test_import_wallet.rs @@ -60,6 +60,7 @@ mod tests { manager2, ptr::null(), 0, + 0, imported_wallet_id.as_mut_ptr(), &mut error, ); diff --git a/key-wallet-manager/src/lib.rs b/key-wallet-manager/src/lib.rs index 1f050e7c2..a7631230e 100644 --- a/key-wallet-manager/src/lib.rs +++ b/key-wallet-manager/src/lib.rs @@ -322,6 +322,7 @@ impl WalletManager { /// /// # Arguments /// * `xprv` - The extended private key string (base58check encoded) + /// * `birth_height` - Birth height for wallet scanning (0 to sync from genesis) /// * `account_creation_options` - Specifies which accounts to create during initialization /// /// # Returns @@ -330,6 +331,7 @@ impl WalletManager { pub fn import_wallet_from_extended_priv_key( &mut self, xprv: &str, + birth_height: CoreBlockHeight, account_creation_options: key_wallet::wallet::initialization::WalletAccountCreationOptions, ) -> Result { // Parse the extended private key @@ -349,7 +351,7 @@ impl WalletManager { } // Create managed wallet info - let mut managed_info = T::from_wallet(&wallet, self.last_processed_height()); + let mut managed_info = T::from_wallet(&wallet, birth_height); managed_info.set_first_loaded_at(current_timestamp()); self.wallets.insert(wallet_id, wallet); @@ -365,6 +367,7 @@ impl WalletManager { /// /// # Arguments /// * `xpub` - The extended public key string (base58check encoded) + /// * `birth_height` - Birth height for wallet scanning (0 to sync from genesis) /// * `can_sign_externally` - If true, creates an externally signable wallet (e.g., for hardware wallets). /// If false, creates a pure watch-only wallet. /// @@ -374,6 +377,7 @@ impl WalletManager { pub fn import_wallet_from_xpub( &mut self, xpub: &str, + birth_height: CoreBlockHeight, can_sign_externally: bool, ) -> Result { // Parse the extended public key @@ -396,7 +400,7 @@ impl WalletManager { } // Create managed wallet info - let mut managed_info = T::from_wallet(&wallet, self.last_processed_height()); + let mut managed_info = T::from_wallet(&wallet, birth_height); managed_info.set_first_loaded_at(current_timestamp()); self.wallets.insert(wallet_id, wallet); @@ -411,8 +415,14 @@ impl WalletManager { /// This is useful for restoring wallets from backups or transferring wallets /// between systems. /// + /// The serialized form contains only the immutable `Wallet`, not the mutable + /// `ManagedWalletInfo`, so the caller must supply a `birth_height` to seed the + /// new wallet's sync checkpoint. Pass 0 to rescan from genesis, or the chain + /// tip if no historical scan is desired. + /// /// # Arguments /// * `wallet_bytes` - The bincode-serialized wallet bytes + /// * `birth_height` - Birth height for wallet scanning (0 to sync from genesis) /// /// # Returns /// * `Ok(WalletId)` - The computed wallet ID of the imported wallet @@ -421,6 +431,7 @@ impl WalletManager { pub fn import_wallet_from_bytes( &mut self, wallet_bytes: &[u8], + birth_height: CoreBlockHeight, ) -> Result { // Deserialize the wallet from bincode let wallet: Wallet = bincode::decode_from_slice(wallet_bytes, bincode::config::standard()) @@ -437,10 +448,8 @@ impl WalletManager { return Err(WalletError::WalletExists(wallet_id)); } - // Create managed wallet info from the imported wallet, using the manager's - // current aggregated last-processed height as the fallback birth height - // since the serialized form does not preserve it. - let mut managed_info = T::from_wallet(&wallet, self.last_processed_height()); + // Create managed wallet info from the imported wallet + let mut managed_info = T::from_wallet(&wallet, birth_height); managed_info.set_first_loaded_at(current_timestamp()); self.wallets.insert(wallet_id, wallet); diff --git a/key-wallet-manager/tests/test_serialized_wallets.rs b/key-wallet-manager/tests/test_serialized_wallets.rs index b79d5d84b..7e5f33c2c 100644 --- a/key-wallet-manager/tests/test_serialized_wallets.rs +++ b/key-wallet-manager/tests/test_serialized_wallets.rs @@ -67,11 +67,16 @@ mod tests { assert_eq!(wallet_id, wallet_id3); println!("Externally signable wallet ID: {}", hex::encode(wallet_id3)); - // Test 4: Import the serialized wallet back + // Test 4: Import the serialized wallet back with a specific birth height let mut manager4 = WalletManager::::new(Network::Testnet); - let import_result = manager4.import_wallet_from_bytes(&bytes); + let import_result = manager4.import_wallet_from_bytes(&bytes, 50_000); assert!(import_result.is_ok()); - assert_eq!(import_result.unwrap(), wallet_id); + let imported_id = import_result.unwrap(); + assert_eq!(imported_id, wallet_id); + let imported = manager4.get_wallet_info(&imported_id).unwrap(); + assert_eq!(imported.birth_height(), 50_000); + assert_eq!(imported.synced_height(), 49_999); + assert_eq!(imported.last_processed_height(), 49_999); } #[test]