From 917c54ab408f3b99e9e5f5b7e640fe370ba00551 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Thu, 25 Dec 2025 23:25:33 +0100 Subject: [PATCH 1/4] Fix `FFIArray` test cleanup memory leaks The tests were wrapping `FFIArray` in a `Box` before passing to `dash_spv_ffi_array_destroy`, but that function only frees the internal data buffer, not the struct itself. This caused the `Box` allocations to leak. --- dash-spv-ffi/tests/test_types.rs | 12 ++--- .../tests/unit/test_memory_management.rs | 52 +++++++++---------- .../tests/unit/test_type_conversions.rs | 20 +++---- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/dash-spv-ffi/tests/test_types.rs b/dash-spv-ffi/tests/test_types.rs index da43b6295..25e09feed 100644 --- a/dash-spv-ffi/tests/test_types.rs +++ b/dash-spv-ffi/tests/test_types.rs @@ -43,7 +43,7 @@ mod tests { fn test_ffi_array_new_and_destroy() { let test_data = vec![1u32, 2, 3, 4, 5]; let len = test_data.len(); - let array = FFIArray::new(test_data); + let mut array = FFIArray::new(test_data); assert!(!array.data.is_null()); assert_eq!(array.len, len); @@ -54,16 +54,14 @@ mod tests { assert_eq!(slice.len(), len); assert_eq!(slice, &[1, 2, 3, 4, 5]); - // Allocate on heap for proper FFI destroy - let array_ptr = Box::into_raw(Box::new(array)); - dash_spv_ffi_array_destroy(array_ptr); + dash_spv_ffi_array_destroy(&mut array as *mut FFIArray); } } #[test] fn test_ffi_array_empty() { let empty_vec: Vec = vec![]; - let array = FFIArray::new(empty_vec); + let mut array = FFIArray::new(empty_vec); assert_eq!(array.len, 0); @@ -71,9 +69,7 @@ mod tests { let slice = array.as_slice::(); assert_eq!(slice.len(), 0); - // Allocate on heap for proper FFI destroy - let array_ptr = Box::into_raw(Box::new(array)); - dash_spv_ffi_array_destroy(array_ptr); + dash_spv_ffi_array_destroy(&mut array as *mut FFIArray); } } diff --git a/dash-spv-ffi/tests/unit/test_memory_management.rs b/dash-spv-ffi/tests/unit/test_memory_management.rs index 0a29e7b18..4ceeba5da 100644 --- a/dash-spv-ffi/tests/unit/test_memory_management.rs +++ b/dash-spv-ffi/tests/unit/test_memory_management.rs @@ -45,25 +45,25 @@ mod tests { unsafe { // Test with different types and sizes let small_array: Vec = vec![1, 2, 3, 4, 5]; - let small_ffi = FFIArray::new(small_array); + let mut small_ffi = FFIArray::new(small_array); assert!(!small_ffi.data.is_null()); assert_eq!(small_ffi.len, 5); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(small_ffi))); + dash_spv_ffi_array_destroy(&mut small_ffi as *mut FFIArray); // Test with large array let large_array: Vec = (0..100_000).collect(); - let large_ffi = FFIArray::new(large_array); + let mut large_ffi = FFIArray::new(large_array); assert!(!large_ffi.data.is_null()); assert_eq!(large_ffi.len, 100_000); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(large_ffi))); + dash_spv_ffi_array_destroy(&mut large_ffi as *mut FFIArray); // Test with empty array let empty_array: Vec = vec![]; - let empty_ffi = FFIArray::new(empty_array); + let mut empty_ffi = FFIArray::new(empty_array); // Even empty arrays have valid pointers assert!(!empty_ffi.data.is_null()); assert_eq!(empty_ffi.len, 0); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(empty_ffi))); + dash_spv_ffi_array_destroy(&mut empty_ffi as *mut FFIArray); } } @@ -125,12 +125,12 @@ mod tests { // Each thread creates and destroys arrays for j in 0..50 { let array: Vec = (0..j * 10).collect(); - let ffi_array = FFIArray::new(array); + let mut ffi_array = FFIArray::new(array); // Simulate some work thread::sleep(Duration::from_micros(10)); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(ffi_array))); + dash_spv_ffi_array_destroy(&mut ffi_array as *mut FFIArray); } } }); @@ -163,11 +163,11 @@ mod tests { // Array allocation let large_array: Vec = vec![0xFF; size]; - let ffi_array = FFIArray::new(large_array); + let mut ffi_array = FFIArray::new(large_array); assert!(!ffi_array.data.is_null()); assert_eq!(ffi_array.len, size); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(ffi_array))); + dash_spv_ffi_array_destroy(&mut ffi_array as *mut FFIArray); } } } @@ -192,18 +192,18 @@ mod tests { dash_spv_ffi_string_destroy(null_string); // Test with array - let ffi_array = FFIArray::new(vec![1u32, 2, 3]); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(ffi_array))); + let mut ffi_array = FFIArray::new(vec![1u32, 2, 3]); + dash_spv_ffi_array_destroy(&mut ffi_array as *mut FFIArray); // Destroying with null should be safe - let null_array = FFIArray { + let mut null_array = FFIArray { data: std::ptr::null_mut(), len: 0, capacity: 0, elem_size: 0, elem_align: 1, }; - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(null_array))); + dash_spv_ffi_array_destroy(&mut null_array as *mut FFIArray); } } @@ -215,21 +215,21 @@ mod tests { // u8 - 1 byte alignment let u8_array = vec![1u8, 2, 3, 4]; - let u8_ffi = FFIArray::new(u8_array); + let mut u8_ffi = FFIArray::new(u8_array); assert_eq!(u8_ffi.data as usize % std::mem::align_of::(), 0); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(u8_ffi))); + dash_spv_ffi_array_destroy(&mut u8_ffi as *mut FFIArray); // u32 - 4 byte alignment let u32_array = vec![1u32, 2, 3, 4]; - let u32_ffi = FFIArray::new(u32_array); + let mut u32_ffi = FFIArray::new(u32_array); assert_eq!(u32_ffi.data as usize % std::mem::align_of::(), 0); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(u32_ffi))); + dash_spv_ffi_array_destroy(&mut u32_ffi as *mut FFIArray); // u64 - 8 byte alignment let u64_array = vec![1u64, 2, 3, 4]; - let u64_ffi = FFIArray::new(u64_array); + let mut u64_ffi = FFIArray::new(u64_array); assert_eq!(u64_ffi.data as usize % std::mem::align_of::(), 0); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(u64_ffi))); + dash_spv_ffi_array_destroy(&mut u64_ffi as *mut FFIArray); } } @@ -341,10 +341,10 @@ mod tests { // Empty array let empty_vec: Vec = vec![]; - let empty_array = FFIArray::new(empty_vec); + let mut empty_array = FFIArray::new(empty_vec); assert!(!empty_array.data.is_null()); assert_eq!(empty_array.len, 0); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(empty_array))); + dash_spv_ffi_array_destroy(&mut empty_array as *mut FFIArray); } } @@ -407,8 +407,8 @@ mod tests { dash_spv_ffi_string_destroy(s); } - for a in arrays { - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(a))); + for mut a in arrays { + dash_spv_ffi_array_destroy(&mut a as *mut FFIArray); } cycle += 1; @@ -424,7 +424,7 @@ mod tests { // Test that memory allocated in one thread can be safely used in another unsafe { let string = FFIString::new("Allocated in thread 1"); - let array = FFIArray::new(vec![1u32, 2, 3, 4, 5]); + let mut array = FFIArray::new(vec![1u32, 2, 3, 4, 5]); // Verify we can read the data let s = FFIString::from_ptr(string.ptr).unwrap(); @@ -435,7 +435,7 @@ mod tests { // Clean up dash_spv_ffi_string_destroy(string); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(array))); + dash_spv_ffi_array_destroy(&mut array as *mut FFIArray); } } } diff --git a/dash-spv-ffi/tests/unit/test_type_conversions.rs b/dash-spv-ffi/tests/unit/test_type_conversions.rs index 4f4c807f8..22a192cc6 100644 --- a/dash-spv-ffi/tests/unit/test_type_conversions.rs +++ b/dash-spv-ffi/tests/unit/test_type_conversions.rs @@ -61,29 +61,29 @@ mod tests { fn test_ffi_array_different_sizes() { // Test empty array let empty: Vec = vec![]; - let empty_array = FFIArray::new(empty); + let mut empty_array = FFIArray::new(empty); assert_eq!(empty_array.len, 0); assert!(!empty_array.data.is_null()); // Even empty vec has allocated pointer unsafe { let slice = empty_array.as_slice::(); assert_eq!(slice.len(), 0); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(empty_array))); + dash_spv_ffi_array_destroy(&mut empty_array as *mut FFIArray); } // Test single element let single = vec![42u32]; - let single_array = FFIArray::new(single); + let mut single_array = FFIArray::new(single); assert_eq!(single_array.len, 1); unsafe { let slice = single_array.as_slice::(); assert_eq!(slice.len(), 1); assert_eq!(slice[0], 42); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(single_array))); + dash_spv_ffi_array_destroy(&mut single_array as *mut FFIArray); } // Test large array let large: Vec = (0..10000).collect(); - let large_array = FFIArray::new(large.clone()); + let mut large_array = FFIArray::new(large.clone()); assert_eq!(large_array.len, 10000); unsafe { let slice = large_array.as_slice::(); @@ -91,7 +91,7 @@ mod tests { for (i, &val) in slice.iter().enumerate() { assert_eq!(val, i as u32); } - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(large_array))); + dash_spv_ffi_array_destroy(&mut large_array as *mut FFIArray); } } @@ -99,22 +99,22 @@ mod tests { fn test_ffi_array_memory_alignment() { // Test with u8 let bytes: Vec = vec![1, 2, 3, 4]; - let byte_array = FFIArray::new(bytes); + let mut byte_array = FFIArray::new(bytes); unsafe { let slice = byte_array.as_slice::(); assert_eq!(slice, &[1, 2, 3, 4]); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(byte_array))); + dash_spv_ffi_array_destroy(&mut byte_array as *mut FFIArray); } // Test with u64 (requires 8-byte alignment) let longs: Vec = vec![u64::MAX, 0, 42]; - let long_array = FFIArray::new(longs); + let mut long_array = FFIArray::new(longs); unsafe { let slice = long_array.as_slice::(); assert_eq!(slice[0], u64::MAX); assert_eq!(slice[1], 0); assert_eq!(slice[2], 42); - dash_spv_ffi_array_destroy(Box::into_raw(Box::new(long_array))); + dash_spv_ffi_array_destroy(&mut long_array as *mut FFIArray); } } From d17172dd9f4ee73abea17fa203a73f3ab3c29fdc Mon Sep 17 00:00:00 2001 From: xdustinface Date: Thu, 25 Dec 2025 23:37:43 +0100 Subject: [PATCH 2/4] Free sync progress and stats in `test_client_resource_cleanup` --- dash-spv-ffi/tests/unit/test_client_lifecycle.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dash-spv-ffi/tests/unit/test_client_lifecycle.rs b/dash-spv-ffi/tests/unit/test_client_lifecycle.rs index 067c1ba6d..752d8e356 100644 --- a/dash-spv-ffi/tests/unit/test_client_lifecycle.rs +++ b/dash-spv-ffi/tests/unit/test_client_lifecycle.rs @@ -142,8 +142,11 @@ mod tests { assert!(!client.is_null()); // Do some operations - let _ = dash_spv_ffi_client_get_sync_progress(client); - let _ = dash_spv_ffi_client_get_stats(client); + let progress = dash_spv_ffi_client_get_sync_progress(client); + let stats = dash_spv_ffi_client_get_stats(client); + + dash_spv_ffi_sync_progress_destroy(progress); + dash_spv_ffi_spv_stats_destroy(stats); dash_spv_ffi_client_destroy(client); dash_spv_ffi_config_destroy(config); From 54a8d6ef2d7370bfe9b0700e683194e4c54f168a Mon Sep 17 00:00:00 2001 From: xdustinface Date: Thu, 25 Dec 2025 23:54:58 +0100 Subject: [PATCH 3/4] Prevent wallet related memory leaks in tests - Properly keep track of all create wallets and wallet infos and clean them up at the end. - Head allocate wallet infos like it would happen in real FFI usage and free them properly at the end. --- key-wallet-ffi/src/managed_wallet.rs | 27 +++++++-------- key-wallet-ffi/src/utxo_tests.rs | 31 ++++++++++------- .../src/wallet_manager_serialization_tests.rs | 15 +++++---- key-wallet-ffi/src/wallet_manager_tests.rs | 33 ++++++++++--------- key-wallet-ffi/tests/test_import_wallet.rs | 3 ++ 5 files changed, 59 insertions(+), 50 deletions(-) diff --git a/key-wallet-ffi/src/managed_wallet.rs b/key-wallet-ffi/src/managed_wallet.rs index 33f1931f8..38472c074 100644 --- a/key-wallet-ffi/src/managed_wallet.rs +++ b/key-wallet-ffi/src/managed_wallet.rs @@ -748,14 +748,14 @@ mod tests { assert!(!wallet.is_null()); assert_eq!(error.code, FFIErrorCode::Success); - // Create managed wallet info from the wallet + // Create managed wallet info from the wallet heap-allocated like C would do let wallet_rust = unsafe { &(*wallet).wallet }; let managed_info = ManagedWalletInfo::from_wallet(wallet_rust); - let mut ffi_managed = FFIManagedWalletInfo::new(managed_info); + let ffi_managed = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); // Test get_next_receive_address with valid pointers let receive_addr = unsafe { - managed_wallet_get_next_bip44_receive_address(&mut ffi_managed, wallet, 0, &mut error) + managed_wallet_get_next_bip44_receive_address(ffi_managed, wallet, 0, &mut error) }; if !receive_addr.is_null() { @@ -776,7 +776,7 @@ mod tests { // Test get_next_change_address with valid pointers let change_addr = unsafe { - managed_wallet_get_next_bip44_change_address(&mut ffi_managed, wallet, 0, &mut error) + managed_wallet_get_next_bip44_change_address(ffi_managed, wallet, 0, &mut error) }; if !change_addr.is_null() { @@ -795,6 +795,7 @@ mod tests { // Clean up unsafe { + managed_wallet_free(ffi_managed); wallet::wallet_free(wallet); } } @@ -869,8 +870,8 @@ mod tests { // Insert the managed account directly into managed_info's accounts managed_info.accounts.insert(managed_account); - // Create wrapper for managed info - let mut ffi_managed = FFIManagedWalletInfo::new(managed_info); + // Create wrapper for managed info heap-allocated like C would do + let ffi_managed = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); // Use the existing wallet pointer let ffi_wallet_ptr = wallet_ptr; @@ -878,7 +879,7 @@ mod tests { // Test 1: Get next receive address let receive_addr = unsafe { managed_wallet_get_next_bip44_receive_address( - &mut ffi_managed, + ffi_managed, ffi_wallet_ptr, 0, &mut error, @@ -895,12 +896,7 @@ mod tests { // Test 2: Get next change address let change_addr = unsafe { - managed_wallet_get_next_bip44_change_address( - &mut ffi_managed, - ffi_wallet_ptr, - 0, - &mut error, - ) + managed_wallet_get_next_bip44_change_address(ffi_managed, ffi_wallet_ptr, 0, &mut error) }; assert!(!change_addr.is_null()); @@ -917,7 +913,7 @@ mod tests { let success = unsafe { managed_wallet_get_bip_44_external_address_range( - &mut ffi_managed, + ffi_managed, ffi_wallet_ptr, 0, 0, @@ -950,7 +946,7 @@ mod tests { let success = unsafe { managed_wallet_get_bip_44_internal_address_range( - &mut ffi_managed, + ffi_managed, ffi_wallet_ptr, 0, 0, @@ -980,6 +976,7 @@ mod tests { // Clean up unsafe { + managed_wallet_free(ffi_managed); wallet::wallet_free(wallet_ptr); } } diff --git a/key-wallet-ffi/src/utxo_tests.rs b/key-wallet-ffi/src/utxo_tests.rs index 0b5ede629..ce2c8fc26 100644 --- a/key-wallet-ffi/src/utxo_tests.rs +++ b/key-wallet-ffi/src/utxo_tests.rs @@ -113,18 +113,20 @@ mod utxo_tests { let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); let mut count_out: usize = 0; - // Create an empty managed wallet info + // Create an empty managed wallet info heap-allocated like C would do let managed_info = ManagedWalletInfo::new(Network::Testnet, [0u8; 32]); - let ffi_managed_info = FFIManagedWalletInfo::new(managed_info); + let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); let result = unsafe { - managed_wallet_get_utxos(&ffi_managed_info, &mut utxos_out, &mut count_out, error) + managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, &mut count_out, error) }; assert!(result); assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); assert_eq!(count_out, 0); assert!(utxos_out.is_null()); + + unsafe { crate::managed_wallet::managed_wallet_free(ffi_managed_info) }; } // Note: There's no individual utxo_free function, only utxo_array_free @@ -229,10 +231,10 @@ mod utxo_tests { managed_info.accounts.insert(bip44_account); - let ffi_managed_info = FFIManagedWalletInfo::new(managed_info); + let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); let result = unsafe { - managed_wallet_get_utxos(&ffi_managed_info, &mut utxos_out, &mut count_out, error) + managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, &mut count_out, error) }; assert!(result); @@ -267,6 +269,7 @@ mod utxo_tests { // Clean up unsafe { utxo_array_free(utxos_out, count_out); + crate::managed_wallet::managed_wallet_free(ffi_managed_info); } } @@ -393,10 +396,10 @@ mod utxo_tests { } managed_info.accounts.insert(coinjoin_account); - let ffi_managed_info = FFIManagedWalletInfo::new(managed_info); + let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); let result = unsafe { - managed_wallet_get_utxos(&ffi_managed_info, &mut utxos_out, &mut count_out, error) + managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, &mut count_out, error) }; assert!(result); @@ -406,6 +409,7 @@ mod utxo_tests { // Clean up unsafe { utxo_array_free(utxos_out, count_out); + crate::managed_wallet::managed_wallet_free(ffi_managed_info); } } @@ -464,16 +468,17 @@ mod utxo_tests { testnet_account.utxos.insert(outpoint, utxo); managed_info.accounts.insert(testnet_account); - let ffi_managed_info = FFIManagedWalletInfo::new(managed_info); + let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); // Get UTXOs let result = unsafe { - managed_wallet_get_utxos(&ffi_managed_info, &mut utxos_out, &mut count_out, error) + managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, &mut count_out, error) }; assert!(result); assert_eq!(count_out, 1); unsafe { utxo_array_free(utxos_out, count_out); + crate::managed_wallet::managed_wallet_free(ffi_managed_info); } } @@ -573,11 +578,11 @@ mod utxo_tests { let mut count_out: usize = 0; let managed_info = ManagedWalletInfo::new(Network::Testnet, [4u8; 32]); - let ffi_managed_info = FFIManagedWalletInfo::new(managed_info); + let ffi_managed_info = Box::into_raw(Box::new(FFIManagedWalletInfo::new(managed_info))); // Test with null utxos_out let result = unsafe { - managed_wallet_get_utxos(&ffi_managed_info, ptr::null_mut(), &mut count_out, error) + managed_wallet_get_utxos(&*ffi_managed_info, ptr::null_mut(), &mut count_out, error) }; assert!(!result); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); @@ -585,10 +590,12 @@ mod utxo_tests { // Test with null count_out let mut utxos_out: *mut FFIUTXO = ptr::null_mut(); let result = unsafe { - managed_wallet_get_utxos(&ffi_managed_info, &mut utxos_out, ptr::null_mut(), error) + managed_wallet_get_utxos(&*ffi_managed_info, &mut utxos_out, ptr::null_mut(), error) }; assert!(!result); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { crate::managed_wallet::managed_wallet_free(ffi_managed_info) }; } #[test] diff --git a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs index e6cc8a217..06b6ebe29 100644 --- a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs @@ -212,8 +212,8 @@ mod tests { let error = &mut error as *mut FFIError; // Create a wallet manager - let manager = wallet_manager::wallet_manager_create(FFINetwork::Testnet, error); - assert!(!manager.is_null()); + let manager1 = wallet_manager::wallet_manager_create(FFINetwork::Testnet, error); + assert!(!manager1.is_null()); let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); let passphrase = CString::new("").unwrap(); @@ -225,7 +225,7 @@ mod tests { // First create and serialize a wallet let success = unsafe { wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - manager, + manager1, mnemonic.as_ptr(), passphrase.as_ptr(), 0, @@ -243,13 +243,13 @@ mod tests { assert!(!wallet_bytes_out.is_null()); // Now import the wallet into a new manager - let manager = wallet_manager::wallet_manager_create(FFINetwork::Testnet, error); - assert!(!manager.is_null()); + let manager2 = wallet_manager::wallet_manager_create(FFINetwork::Testnet, error); + assert!(!manager2.is_null()); let mut imported_wallet_id = [0u8; 32]; let import_success = unsafe { wallet_manager::wallet_manager_import_wallet_from_bytes( - manager, + manager2, wallet_bytes_out, wallet_bytes_len_out, imported_wallet_id.as_mut_ptr(), @@ -269,7 +269,8 @@ mod tests { wallet_bytes_out, wallet_bytes_len_out, ); - wallet_manager::wallet_manager_free(manager); + wallet_manager::wallet_manager_free(manager1); + wallet_manager::wallet_manager_free(manager2); } } diff --git a/key-wallet-ffi/src/wallet_manager_tests.rs b/key-wallet-ffi/src/wallet_manager_tests.rs index 2ca247146..53c97423b 100644 --- a/key-wallet-ffi/src/wallet_manager_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_tests.rs @@ -812,65 +812,65 @@ mod tests { let wallet_id_slice = unsafe { slice::from_raw_parts(wallet_ids, 32) }; // Test getting the wallet - let wallet = unsafe { + let valid_wallet = unsafe { wallet_manager::wallet_manager_get_wallet(manager, wallet_id_slice.as_ptr(), error) }; - assert!(!wallet.is_null()); + assert!(!valid_wallet.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); // Test getting the managed wallet info - let wallet_info = unsafe { + let valid_wallet_info = unsafe { wallet_manager::wallet_manager_get_managed_wallet_info( manager, wallet_id_slice.as_ptr(), error, ) }; - assert!(!wallet_info.is_null()); + assert!(!valid_wallet_info.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); // Test with invalid wallet ID (all zeros) let invalid_wallet_id = [0u8; 32]; - let wallet = unsafe { + let invalid_wallet = unsafe { wallet_manager::wallet_manager_get_wallet(manager, invalid_wallet_id.as_ptr(), error) }; - assert!(wallet.is_null()); + assert!(invalid_wallet.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::NotFound); - let wallet_info = unsafe { + let invalid_wallet_info = unsafe { wallet_manager::wallet_manager_get_managed_wallet_info( manager, invalid_wallet_id.as_ptr(), error, ) }; - assert!(wallet_info.is_null()); + assert!(invalid_wallet_info.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::NotFound); // Test with null manager - let wallet = unsafe { + let null_wallet = unsafe { wallet_manager::wallet_manager_get_wallet(ptr::null(), wallet_id_slice.as_ptr(), error) }; - assert!(wallet.is_null()); + assert!(null_wallet.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - let wallet_info = unsafe { + let null_wallet_info = unsafe { wallet_manager::wallet_manager_get_managed_wallet_info( ptr::null(), wallet_id_slice.as_ptr(), error, ) }; - assert!(wallet_info.is_null()); + assert!(null_wallet_info.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); // Clean up unsafe { - // Free the wallet (cast from const to mut for free) - wallet::wallet_free(wallet as *mut _); - // Free the managed wallet info - crate::managed_wallet::managed_wallet_info_free(wallet_info); + // Free the valid wallet (cast from const to mut for free) + wallet::wallet_free(valid_wallet as *mut _); + // Free the valid managed wallet info + crate::managed_wallet::managed_wallet_info_free(valid_wallet_info); // Free the wallet IDs wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); // Free the manager @@ -1166,6 +1166,7 @@ mod tests { // Clean up unsafe { + wallet::wallet_free(wallet as *mut _); crate::wallet_manager::wallet_manager_free_wallet_bytes( wallet_bytes_out, wallet_bytes_len_out, diff --git a/key-wallet-ffi/tests/test_import_wallet.rs b/key-wallet-ffi/tests/test_import_wallet.rs index 8cb6491dc..ddf0f4042 100644 --- a/key-wallet-ffi/tests/test_import_wallet.rs +++ b/key-wallet-ffi/tests/test_import_wallet.rs @@ -4,6 +4,7 @@ #[cfg(test)] mod tests { use key_wallet_ffi::error::{FFIError, FFIErrorCode}; + use key_wallet_ffi::wallet::wallet_free_const; use key_wallet_ffi::wallet_manager::*; use key_wallet_ffi::FFINetwork; use std::os::raw::c_char; @@ -66,6 +67,8 @@ mod tests { assert_eq!(error.code, FFIErrorCode::InvalidInput); // Clean up + error.free_message(); + wallet_free_const(wallet_ptr); wallet_manager_free_wallet_ids(wallet_ids_ptr, count); wallet_manager_free(manager); wallet_manager_free(manager2); From e0053f1f3874f4678f6559e785de0009011c2f4f Mon Sep 17 00:00:00 2001 From: xdustinface Date: Fri, 26 Dec 2025 08:47:34 +0100 Subject: [PATCH 4/4] Prevent `FFIError` message leaks in tests - Modified `set_error`, `set_success`, and `ffi_error_set` macro to free previous error messages before setting new ones - Added `FFIError::free_message` helper for test cleanup - Added cleanup in all relevalt tests --- .../src/account_derivation_tests.rs | 5 ++ key-wallet-ffi/src/account_tests.rs | 4 + key-wallet-ffi/src/address_pool.rs | 2 + key-wallet-ffi/src/address_tests.rs | 20 +++++ key-wallet-ffi/src/derivation_tests.rs | 70 +++++++++++------- key-wallet-ffi/src/error.rs | 37 +++++++++- key-wallet-ffi/src/keys_tests.rs | 19 ++++- key-wallet-ffi/src/managed_wallet.rs | 13 ++++ key-wallet-ffi/src/managed_wallet_tests.rs | 33 +++++++-- key-wallet-ffi/src/mnemonic_tests.rs | 42 +++++++++++ key-wallet-ffi/src/transaction_tests.rs | 7 ++ key-wallet-ffi/src/utxo_tests.rs | 7 +- .../src/wallet_manager_serialization_tests.rs | 18 +++++ key-wallet-ffi/src/wallet_manager_tests.rs | 25 +++++-- key-wallet-ffi/src/wallet_tests.rs | 21 ++++++ key-wallet-ffi/tests/integration_test.rs | 2 + .../tests/test_error_conversions.rs | 73 +++++++------------ .../tests/test_managed_account_collection.rs | 1 + 18 files changed, 306 insertions(+), 93 deletions(-) diff --git a/key-wallet-ffi/src/account_derivation_tests.rs b/key-wallet-ffi/src/account_derivation_tests.rs index a322aadcf..0522ddd7b 100644 --- a/key-wallet-ffi/src/account_derivation_tests.rs +++ b/key-wallet-ffi/src/account_derivation_tests.rs @@ -59,6 +59,7 @@ mod tests { extended_private_key_free(master_xpriv); account_free(account); wallet::wallet_free(wallet); + error.free_message(); } } @@ -97,6 +98,8 @@ mod tests { ); assert_eq!(error.code, FFIErrorCode::InvalidInput); } + + unsafe { error.free_message() }; } #[test] @@ -134,6 +137,7 @@ mod tests { extended_private_key_free(master_xpriv); account_free(account); wallet::wallet_free(wallet); + error.free_message(); } } @@ -213,6 +217,7 @@ mod tests { unsafe { account_free(account); wallet::wallet_free(wallet); + error.free_message(); } } diff --git a/key-wallet-ffi/src/account_tests.rs b/key-wallet-ffi/src/account_tests.rs index ada8605cf..2ea6bb6be 100644 --- a/key-wallet-ffi/src/account_tests.rs +++ b/key-wallet-ffi/src/account_tests.rs @@ -77,6 +77,8 @@ mod tests { assert_eq!(count, 0); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -105,6 +107,7 @@ mod tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } @@ -190,6 +193,7 @@ mod tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index 33f7a3828..b255efac7 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -1135,6 +1135,7 @@ mod tests { managed_account_free(account); wallet_manager_free_wallet_ids(wallet_ids_out, count_out); wallet_manager_free(manager); + error.free_message(); } } @@ -1263,6 +1264,7 @@ mod tests { managed_account_free(account); wallet_manager_free_wallet_ids(wallet_ids_out, count_out); wallet_manager_free(manager); + error.free_message(); } } } diff --git a/key-wallet-ffi/src/address_tests.rs b/key-wallet-ffi/src/address_tests.rs index 6a343f672..0ec9b6777 100644 --- a/key-wallet-ffi/src/address_tests.rs +++ b/key-wallet-ffi/src/address_tests.rs @@ -29,6 +29,8 @@ mod address_tests { let is_valid = unsafe { address_validate(ptr::null(), FFINetwork::Testnet, error) }; assert!(!is_valid); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } #[test] @@ -43,6 +45,8 @@ mod address_tests { assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); // Returns 0 for P2PKH assert_eq!(addr_type, 0); + + unsafe { (*error).free_message() }; } #[test] @@ -55,6 +59,8 @@ mod address_tests { unsafe { address_validate(addr_str.as_ptr(), FFINetwork::Testnet, &mut error) }; assert!(is_valid); + + unsafe { error.free_message() }; } #[test] @@ -68,6 +74,8 @@ mod address_tests { assert!(!is_valid); assert_eq!(error.code, FFIErrorCode::InvalidAddress); + + unsafe { error.free_message() }; } #[test] @@ -78,6 +86,8 @@ mod address_tests { assert!(!is_valid); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -97,6 +107,8 @@ mod address_tests { assert!(addr_type <= 2); assert_eq!(error.code, FFIErrorCode::Success); } + + unsafe { error.free_message() }; } #[test] @@ -110,6 +122,8 @@ mod address_tests { // Should return 255 (u8::MAX) for invalid assert_eq!(addr_type, 255); assert_eq!(error.code, FFIErrorCode::InvalidAddress); + + unsafe { error.free_message() }; } #[test] @@ -121,6 +135,8 @@ mod address_tests { // Should return 255 (u8::MAX) for null input assert_eq!(addr_type, 255); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -177,6 +193,8 @@ mod address_tests { let is_valid = address_validate(addr_str.as_ptr(), FFINetwork::Testnet, &mut error); assert!(!is_valid); } + + error.free_message(); } } @@ -200,6 +218,8 @@ mod address_tests { // Should return a valid type (0, 1, 2) or 255 for error assert!(addr_type <= 2 || addr_type == 255); } + + error.free_message(); } } } diff --git a/key-wallet-ffi/src/derivation_tests.rs b/key-wallet-ffi/src/derivation_tests.rs index e21bafda7..a057e12a3 100644 --- a/key-wallet-ffi/src/derivation_tests.rs +++ b/key-wallet-ffi/src/derivation_tests.rs @@ -42,6 +42,7 @@ mod tests { // Clean up unsafe { derivation_xpriv_free(xprv); + error.free_message(); } } @@ -67,9 +68,8 @@ mod tests { // Clean up unsafe { derivation_xpub_free(xpub); - } - unsafe { derivation_xpriv_free(xprv); + error.free_message(); } } @@ -97,9 +97,8 @@ mod tests { // Clean up unsafe { derivation_string_free(xprv_str); - } - unsafe { derivation_xpriv_free(xprv); + error.free_message(); } } @@ -129,12 +128,9 @@ mod tests { // Clean up unsafe { derivation_string_free(xpub_str); - } - unsafe { derivation_xpub_free(xpub); - } - unsafe { derivation_xpriv_free(xprv); + error.free_message(); } } @@ -166,9 +162,8 @@ mod tests { // Clean up unsafe { derivation_xpub_free(xpub); - } - unsafe { derivation_xpriv_free(xprv); + error.free_message(); } } @@ -207,6 +202,8 @@ mod tests { let path_str = unsafe { CStr::from_ptr(payment_path.as_ptr() as *const c_char) }.to_str().unwrap(); assert_eq!(path_str, "m/44'/1'/0'/0/0"); + + unsafe { error.free_message() }; } #[test] @@ -258,6 +255,8 @@ mod tests { &mut error, ); assert!(success); + + unsafe { error.free_message() }; } #[test] @@ -289,6 +288,7 @@ mod tests { // Clean up unsafe { derivation_xpriv_free(xpriv); + error.free_message(); } } @@ -304,6 +304,8 @@ mod tests { // Note: The BIP32 implementation actually accepts seeds as small as 16 bytes // so we can't test for invalid seed length error here + + unsafe { error.free_message() }; } #[test] @@ -329,12 +331,9 @@ mod tests { // Clean up unsafe { derivation_string_free(xpub_string); - } - unsafe { derivation_xpub_free(xpub); - } - unsafe { derivation_xpriv_free(master_key); + error.free_message(); } } @@ -363,9 +362,8 @@ mod tests { // Clean up unsafe { derivation_string_free(xpriv_string); - } - unsafe { derivation_xpriv_free(master_key); + error.free_message(); } } @@ -396,9 +394,8 @@ mod tests { // Clean up unsafe { derivation_xpub_free(xpub); - } - unsafe { derivation_xpriv_free(master_key); + error.free_message(); } } @@ -452,6 +449,8 @@ mod tests { let path_str = unsafe { CStr::from_ptr(buffer.as_ptr() as *const c_char) }.to_str().unwrap(); assert!(path_str.contains("m/")); + + unsafe { error.free_message() }; } #[test] @@ -494,6 +493,8 @@ mod tests { derivation_xpriv_free(xprv); } } + + unsafe { error.free_message() }; } #[test] @@ -524,6 +525,8 @@ mod tests { derivation_coinjoin_path(FFINetwork::Testnet, 0, ptr::null_mut(), 256, &mut error); assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -554,6 +557,8 @@ mod tests { ); assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -591,15 +596,10 @@ mod tests { // Clean up unsafe { derivation_string_free(main_str); - } - unsafe { derivation_string_free(test_str); - } - unsafe { derivation_xpriv_free(xprv_main); - } - unsafe { derivation_xpriv_free(xprv_test); + error.free_message(); } } @@ -611,6 +611,8 @@ mod tests { assert!(xpub.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -621,6 +623,8 @@ mod tests { assert!(xprv_str.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -631,6 +635,8 @@ mod tests { assert!(xpub_str.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -664,9 +670,8 @@ mod tests { // Clean up unsafe { derivation_xpub_free(xpub); - } - unsafe { derivation_xpriv_free(xprv); + error.free_message(); } } @@ -701,6 +706,8 @@ mod tests { }; assert!(xpriv.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -729,6 +736,8 @@ mod tests { derivation_xpriv_free(xpriv); } } + + unsafe { error.free_message() }; } #[test] @@ -769,6 +778,8 @@ mod tests { ); assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -810,6 +821,8 @@ mod tests { ); assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -843,6 +856,8 @@ mod tests { assert_eq!(path1, "m/44'/1'/0'"); assert_eq!(path2, "m/44'/1'/5'"); assert_ne!(path1, path2); + + unsafe { error.free_message() }; } #[test] @@ -880,6 +895,8 @@ mod tests { let path_str = unsafe { CStr::from_ptr(buffer.as_ptr() as *const c_char) }.to_str().unwrap(); assert_eq!(path_str, "m/44'/1'/0'/1/3"); + + unsafe { error.free_message() }; } #[test] @@ -957,6 +974,7 @@ mod tests { derivation_xpriv_free(child_xprv); derivation_xpub_free(master_xpub); derivation_xpriv_free(master_xprv); + error.free_message(); } } } diff --git a/key-wallet-ffi/src/error.rs b/key-wallet-ffi/src/error.rs index c7c8b924f..a8ca15af8 100644 --- a/key-wallet-ffi/src/error.rs +++ b/key-wallet-ffi/src/error.rs @@ -48,25 +48,50 @@ impl FFIError { } } - /// Set error on a mutable pointer if it's not null + /// Set error on a mutable pointer if it's not null. + /// Frees any previous error message before setting the new one. #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn set_error(error_ptr: *mut FFIError, code: FFIErrorCode, msg: String) { if !error_ptr.is_null() { unsafe { + // Free previous message if present + let prev = &mut *error_ptr; + if !prev.message.is_null() { + let _ = CString::from_raw(prev.message); + } *error_ptr = Self::error(code, msg); } } } - /// Set success on a mutable pointer if it's not null + /// Set success on a mutable pointer if it's not null. + /// Frees any previous error message before setting success. #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn set_success(error_ptr: *mut FFIError) { if !error_ptr.is_null() { unsafe { + // Free previous message if present + let prev = &mut *error_ptr; + if !prev.message.is_null() { + let _ = CString::from_raw(prev.message); + } *error_ptr = Self::success(); } } } + + /// Free the error message if present. + /// Use this in tests to prevent memory leaks. + /// + /// # Safety + /// + /// The message pointer must have been allocated by this library. + pub unsafe fn free_message(&mut self) { + if !self.message.is_null() { + let _ = CString::from_raw(self.message); + self.message = ptr::null_mut(); + } + } } /// Free an error message @@ -83,13 +108,19 @@ pub unsafe extern "C" fn error_message_free(message: *mut c_char) { } } -/// Helper macro to convert any error that implements `Into` and set it on the error pointer +/// Helper macro to convert any error that implements `Into` and set it on the error pointer. +/// Frees any previous error message before setting the new one. #[macro_export] macro_rules! ffi_error_set { ($error_ptr:expr, $err:expr) => {{ let ffi_error: $crate::error::FFIError = $err.into(); if !$error_ptr.is_null() { unsafe { + // Free previous message if present + let prev = &mut *$error_ptr; + if !prev.message.is_null() { + let _ = std::ffi::CString::from_raw(prev.message); + } *$error_ptr = ffi_error; } } diff --git a/key-wallet-ffi/src/keys_tests.rs b/key-wallet-ffi/src/keys_tests.rs index 963327078..c21c1896b 100644 --- a/key-wallet-ffi/src/keys_tests.rs +++ b/key-wallet-ffi/src/keys_tests.rs @@ -94,6 +94,7 @@ mod tests { public_key_free(pub_key); extended_public_key_free(ext_pub); wallet::wallet_free(wallet); + error.free_message(); } } @@ -127,6 +128,7 @@ mod tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } @@ -159,8 +161,8 @@ mod tests { // Clean up unsafe { crate::utils::string_free(xpub_str); - wallet::wallet_free(wallet); + error.free_message(); } } @@ -207,9 +209,8 @@ mod tests { } unsafe { private_key_free(privkey_ptr); - } - unsafe { wallet::wallet_free(wallet); + error.free_message(); } } @@ -265,6 +266,7 @@ mod tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } @@ -299,6 +301,7 @@ mod tests { // Clean up crate::utils::string_free(hex_str); wallet::wallet_free(wallet); + error.free_message(); } } @@ -346,6 +349,7 @@ mod tests { // Clean up unsafe { derivation_path_free(indices_out, hardened_out, count_out); + error.free_message(); } } @@ -376,6 +380,7 @@ mod tests { // Clean up (should handle null pointers gracefully) unsafe { derivation_path_free(indices_out, hardened_out, count_out); + error.free_message(); } } @@ -405,6 +410,8 @@ mod tests { }; assert!(!success); + + unsafe { error.free_message() }; } #[test] @@ -441,6 +448,7 @@ mod tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } @@ -480,6 +488,8 @@ mod tests { assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -546,6 +556,7 @@ mod tests { // Clean up unsafe { derivation_path_free(indices_out, hardened_out, count_out); + error.free_message(); } } @@ -591,6 +602,7 @@ mod tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } @@ -649,6 +661,7 @@ mod tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } diff --git a/key-wallet-ffi/src/managed_wallet.rs b/key-wallet-ffi/src/managed_wallet.rs index 38472c074..bde2027dd 100644 --- a/key-wallet-ffi/src/managed_wallet.rs +++ b/key-wallet-ffi/src/managed_wallet.rs @@ -657,6 +657,8 @@ mod tests { assert!(address.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -675,6 +677,8 @@ mod tests { assert!(address.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -701,6 +705,8 @@ mod tests { assert_eq!(count_out, 0); assert!(addresses_out.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -727,6 +733,8 @@ mod tests { assert_eq!(count_out, 0); assert!(addresses_out.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -797,6 +805,7 @@ mod tests { unsafe { managed_wallet_free(ffi_managed); wallet::wallet_free(wallet); + error.free_message(); } } @@ -978,6 +987,7 @@ mod tests { unsafe { managed_wallet_free(ffi_managed); wallet::wallet_free(wallet_ptr); + error.free_message(); } } @@ -1073,6 +1083,7 @@ mod tests { unsafe { managed_wallet_free(ffi_managed_ptr); wallet::wallet_free(wallet_ptr); + error.free_message(); } } @@ -1114,5 +1125,7 @@ mod tests { assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } } diff --git a/key-wallet-ffi/src/managed_wallet_tests.rs b/key-wallet-ffi/src/managed_wallet_tests.rs index d027f15ac..0d44e8361 100644 --- a/key-wallet-ffi/src/managed_wallet_tests.rs +++ b/key-wallet-ffi/src/managed_wallet_tests.rs @@ -41,7 +41,8 @@ mod tests { // Clean up unsafe { managed_wallet_free(managed_wallet); - unsafe {wallet::wallet_free(wallet);} + wallet::wallet_free(wallet); + error.free_message(); } } @@ -55,6 +56,8 @@ mod tests { assert!(managed_wallet.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -101,7 +104,8 @@ mod tests { // Clean up unsafe { managed_wallet_free(managed_wallet); - unsafe {wallet::wallet_free(wallet);} + wallet::wallet_free(wallet); + error.free_message(); } } @@ -143,7 +147,8 @@ mod tests { // Clean up unsafe { managed_wallet_free(managed_wallet); - unsafe {wallet::wallet_free(wallet);} + wallet::wallet_free(wallet); + error.free_message(); } } @@ -162,6 +167,8 @@ mod tests { assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -180,6 +187,8 @@ mod tests { assert!(address.is_null()); assert_eq!(error.code, FFIErrorCode::WalletError); + + unsafe { error.free_message() }; } #[test] @@ -198,6 +207,8 @@ mod tests { assert!(address.is_null()); assert_eq!(error.code, FFIErrorCode::WalletError); + + unsafe { error.free_message() }; } #[test] @@ -221,6 +232,8 @@ mod tests { assert_eq!(count_out, 0); assert!(addresses_out.is_null()); assert_eq!(error.code, FFIErrorCode::Success); + + unsafe { error.free_message() }; } #[test] @@ -257,6 +270,8 @@ mod tests { assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -296,7 +311,8 @@ mod tests { // Clean up wallet unsafe { - unsafe {wallet::wallet_free(wallet);} + wallet::wallet_free(wallet); + error.free_message(); } } @@ -334,7 +350,8 @@ mod tests { // Clean up unsafe { managed_wallet_free(managed_wallet); - unsafe {wallet::wallet_free(wallet);} + wallet::wallet_free(wallet); + error.free_message(); } } @@ -376,7 +393,8 @@ mod tests { // Clean up unsafe { managed_wallet_free(managed_wallet); - unsafe {wallet::wallet_free(wallet);} + wallet::wallet_free(wallet); + error.free_message(); } } @@ -433,7 +451,8 @@ mod tests { // Clean up unsafe { managed_wallet_free(managed_wallet); - unsafe {wallet::wallet_free(wallet);} + wallet::wallet_free(wallet); + error.free_message(); } } } diff --git a/key-wallet-ffi/src/mnemonic_tests.rs b/key-wallet-ffi/src/mnemonic_tests.rs index 466d55cc8..4ea3e8638 100644 --- a/key-wallet-ffi/src/mnemonic_tests.rs +++ b/key-wallet-ffi/src/mnemonic_tests.rs @@ -38,6 +38,8 @@ mod tests { let is_valid = unsafe { mnemonic::mnemonic_validate(ptr::null(), error) }; assert!(!is_valid); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } #[test] @@ -78,6 +80,8 @@ mod tests { let invalid = mnemonic::mnemonic_generate(13, error); assert!(invalid.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } #[test] @@ -121,6 +125,8 @@ mod tests { assert!(success); assert_ne!(seed, seed_with_pass); // Different passphrase should produce different seed + + unsafe { (*error).free_message() }; } #[test] @@ -143,6 +149,8 @@ mod tests { mnemonic::mnemonic_free(mnemonic); } } + + unsafe { (*error).free_message() }; } #[test] @@ -158,6 +166,8 @@ mod tests { assert!(mnemonic.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); } + + unsafe { (*error).free_message() }; } #[test] @@ -195,6 +205,8 @@ mod tests { }; assert!(success); assert_eq!(seed_len, 64); + + unsafe { (*error).free_message() }; } #[test] @@ -230,6 +242,8 @@ mod tests { mnemonic::mnemonic_free(mnemonic_ptr); } } + + unsafe { error.free_message() }; } #[test] @@ -269,6 +283,8 @@ mod tests { mnemonic::mnemonic_free(portuguese_mnemonic); } + + unsafe { error.free_message() }; } #[test] @@ -291,6 +307,8 @@ mod tests { unsafe { mnemonic::mnemonic_free(mnemonic_ptr); } + + unsafe { error.free_message() }; } #[test] @@ -340,6 +358,8 @@ mod tests { // Seeds should be different assert_ne!(seed1, seed2); + + unsafe { error.free_message() }; } #[test] @@ -363,6 +383,8 @@ mod tests { assert_eq!(count, expected_count); assert_eq!(error.code, FFIErrorCode::Success); } + + error.free_message(); } } @@ -374,6 +396,8 @@ mod tests { assert_eq!(count, 0); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -391,6 +415,8 @@ mod tests { assert_eq!(count, 0); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -438,6 +464,8 @@ mod tests { }; assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -459,6 +487,8 @@ mod tests { assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidMnemonic); + + unsafe { error.free_message() }; } #[test] @@ -494,6 +524,8 @@ mod tests { }; assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -511,6 +543,8 @@ mod tests { assert!(!is_valid); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -526,6 +560,8 @@ mod tests { assert!(mnemonic.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -552,6 +588,8 @@ mod tests { mnemonic::mnemonic_free(mnemonic); } } + + unsafe { error.free_message() }; } #[test] @@ -589,6 +627,8 @@ mod tests { } } } + + unsafe { error.free_message() }; } #[test] @@ -634,6 +674,7 @@ mod tests { unsafe { mnemonic::mnemonic_free(mnemonic); + error.free_message(); } } @@ -679,6 +720,7 @@ mod tests { // Free unsafe { mnemonic::mnemonic_free(mnemonic); + error.free_message(); } } } diff --git a/key-wallet-ffi/src/transaction_tests.rs b/key-wallet-ffi/src/transaction_tests.rs index 7bc312894..f68628b51 100644 --- a/key-wallet-ffi/src/transaction_tests.rs +++ b/key-wallet-ffi/src/transaction_tests.rs @@ -40,6 +40,7 @@ mod transaction_tests { // Clean up unsafe { let _ = CString::from_raw(output.address as *mut c_char); + error.free_message(); } } @@ -82,6 +83,7 @@ mod transaction_tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } @@ -106,6 +108,8 @@ mod transaction_tests { assert!(!success); assert_eq!(error.code, FFIErrorCode::InvalidInput); + + unsafe { error.free_message() }; } #[test] @@ -145,6 +149,7 @@ mod transaction_tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } @@ -202,6 +207,7 @@ mod transaction_tests { unsafe { let _ = CString::from_raw(output.address as *mut c_char); wallet::wallet_free(wallet); + error.free_message(); } } @@ -244,6 +250,7 @@ mod transaction_tests { // Clean up unsafe { wallet::wallet_free(wallet); + error.free_message(); } } } diff --git a/key-wallet-ffi/src/utxo_tests.rs b/key-wallet-ffi/src/utxo_tests.rs index ce2c8fc26..66d9f5288 100644 --- a/key-wallet-ffi/src/utxo_tests.rs +++ b/key-wallet-ffi/src/utxo_tests.rs @@ -100,6 +100,8 @@ mod utxo_tests { unsafe { managed_wallet_get_utxos(ptr::null(), &mut utxos_out, &mut count_out, error) }; assert!(!result); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } #[test] @@ -595,7 +597,10 @@ mod utxo_tests { assert!(!result); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - unsafe { crate::managed_wallet::managed_wallet_free(ffi_managed_info) }; + unsafe { + crate::managed_wallet::managed_wallet_free(ffi_managed_info); + (*error).free_message(); + } } #[test] diff --git a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs index 06b6ebe29..624d21a79 100644 --- a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs @@ -59,6 +59,7 @@ mod tests { wallet_bytes_len_out, ); wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -107,6 +108,7 @@ mod tests { wallet_bytes_len_out, ); wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -155,6 +157,7 @@ mod tests { wallet_bytes_len_out, ); wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -203,6 +206,7 @@ mod tests { wallet_bytes_len_out, ); wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -271,6 +275,7 @@ mod tests { ); wallet_manager::wallet_manager_free(manager1); wallet_manager::wallet_manager_free(manager2); + (*error).free_message(); } } @@ -310,6 +315,12 @@ mod tests { assert_ne!(unsafe { (*error).code }, FFIErrorCode::Success); assert!(wallet_bytes_out.is_null()); assert_eq!(wallet_bytes_len_out, 0); + + // Clean up + unsafe { + wallet_manager::wallet_manager_free(manager); + (*error).free_message(); + } } #[test] @@ -343,6 +354,12 @@ mod tests { assert!(!success, "Should fail with null mnemonic"); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + // Clean up + unsafe { + wallet_manager::wallet_manager_free(manager); + (*error).free_message(); + } } #[test] @@ -408,6 +425,7 @@ mod tests { wallet_bytes_len_out, ); wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } } diff --git a/key-wallet-ffi/src/wallet_manager_tests.rs b/key-wallet-ffi/src/wallet_manager_tests.rs index 53c97423b..955ffd24b 100644 --- a/key-wallet-ffi/src/wallet_manager_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_tests.rs @@ -32,6 +32,7 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -66,6 +67,7 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -140,9 +142,8 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, count); - } - unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -201,9 +202,8 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, count); - } - unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -231,6 +231,7 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -265,6 +266,7 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -297,6 +299,7 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -348,9 +351,8 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); - } - unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -409,9 +411,8 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); - } - unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -454,6 +455,8 @@ mod tests { ) }; assert!(!success); + + unsafe { (*error).free_message() }; } #[test] @@ -489,6 +492,7 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -608,6 +612,7 @@ mod tests { unsafe { wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -767,6 +772,7 @@ mod tests { // Clean up unsafe { wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -875,6 +881,7 @@ mod tests { wallet_manager::wallet_manager_free_wallet_ids(wallet_ids, id_count); // Free the manager wallet_manager::wallet_manager_free(manager); + (*error).free_message(); } } @@ -1072,6 +1079,7 @@ mod tests { crate::wallet_manager::wallet_manager_free(manager); crate::wallet_manager::wallet_manager_free(manager4); crate::wallet_manager::wallet_manager_free(manager5); + (*error).free_message(); } } @@ -1172,6 +1180,7 @@ mod tests { wallet_bytes_len_out, ); crate::wallet_manager::wallet_manager_free(manager2); + (*error).free_message(); } } } diff --git a/key-wallet-ffi/src/wallet_tests.rs b/key-wallet-ffi/src/wallet_tests.rs index 4d8514288..3771b15fc 100644 --- a/key-wallet-ffi/src/wallet_tests.rs +++ b/key-wallet-ffi/src/wallet_tests.rs @@ -35,6 +35,7 @@ mod wallet_tests { // Clean up unsafe { wallet::wallet_free(wallet); + (*error).free_message(); } } @@ -55,6 +56,7 @@ mod wallet_tests { // Clean up unsafe { wallet::wallet_free(wallet); + (*error).free_message(); } } @@ -75,6 +77,7 @@ mod wallet_tests { // Clean up unsafe { wallet::wallet_free(random_wallet); + (*error).free_message(); } } @@ -101,6 +104,8 @@ mod wallet_tests { // Clean up wallet::wallet_free(wallet); } + + (*error).free_message(); } } @@ -127,6 +132,7 @@ mod wallet_tests { // Clean up unsafe { wallet::wallet_free(wallet); + (*error).free_message(); } } @@ -165,6 +171,8 @@ mod wallet_tests { unsafe { wallet::wallet_create_from_seed(ptr::null(), 64, FFINetwork::Testnet, error) }; assert!(wallet.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } #[test] @@ -191,6 +199,7 @@ mod wallet_tests { // Clean up unsafe { wallet::wallet_free(wallet); + (*error).free_message(); } } @@ -217,6 +226,7 @@ mod wallet_tests { // Clean up unsafe { wallet::wallet_free(wallet); + (*error).free_message(); } } @@ -231,6 +241,8 @@ mod wallet_tests { assert!(wallet.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } #[test] @@ -260,6 +272,7 @@ mod wallet_tests { // Clean up unsafe { wallet::wallet_free(wallet_with_mnemonic); + (*error).free_message(); } } @@ -272,6 +285,8 @@ mod wallet_tests { let has_mnemonic = unsafe { wallet::wallet_has_mnemonic(ptr::null(), error) }; assert!(!has_mnemonic); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } #[test] @@ -306,6 +321,7 @@ mod wallet_tests { // Clean up unsafe { wallet::wallet_free(wallet); + (*error).free_message(); } } @@ -362,6 +378,8 @@ mod wallet_tests { wallet::wallet_free(wallet); } } + + unsafe { (*error).free_message() }; } #[test] @@ -385,6 +403,7 @@ mod wallet_tests { unsafe { let _ = CString::from_raw(xpub); wallet::wallet_free(wallet); + (*error).free_message(); } } @@ -397,6 +416,8 @@ mod wallet_tests { let xpub = unsafe { wallet::wallet_get_xpub(ptr::null(), 0, error) }; assert!(xpub.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } #[test] diff --git a/key-wallet-ffi/tests/integration_test.rs b/key-wallet-ffi/tests/integration_test.rs index 8ac8b4a66..b284838a5 100644 --- a/key-wallet-ffi/tests/integration_test.rs +++ b/key-wallet-ffi/tests/integration_test.rs @@ -211,4 +211,6 @@ fn test_error_handling() { }; assert!(wallet.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + + unsafe { (*error).free_message() }; } diff --git a/key-wallet-ffi/tests/test_error_conversions.rs b/key-wallet-ffi/tests/test_error_conversions.rs index e5665d60f..7a2a832ae 100644 --- a/key-wallet-ffi/tests/test_error_conversions.rs +++ b/key-wallet-ffi/tests/test_error_conversions.rs @@ -2,59 +2,55 @@ use key_wallet_ffi::error::{FFIError, FFIErrorCode}; +/// Helper to test an FFIError conversion and clean up the message +fn assert_ffi_error_code(mut ffi_err: FFIError, expected: FFIErrorCode) { + assert_eq!(ffi_err.code, expected); + unsafe { ffi_err.free_message() }; +} + #[test] fn test_key_wallet_error_to_ffi_error() { use key_wallet::Error as KeyWalletError; // Test InvalidMnemonic conversion let err = KeyWalletError::InvalidMnemonic("bad mnemonic".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidMnemonic); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidMnemonic); // Test InvalidNetwork conversion let err = KeyWalletError::InvalidNetwork; - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidNetwork); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidNetwork); // Test InvalidAddress conversion let err = KeyWalletError::InvalidAddress("bad address".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidAddress); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidAddress); // Test InvalidDerivationPath conversion let err = KeyWalletError::InvalidDerivationPath("bad path".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidDerivationPath); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidDerivationPath); // Test InvalidParameter conversion let err = KeyWalletError::InvalidParameter("bad param".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidInput); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidInput); // Test Serialization conversion let err = KeyWalletError::Serialization("serialization failed".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::SerializationError); + assert_ffi_error_code(err.into(), FFIErrorCode::SerializationError); // Test WatchOnly conversion let err = KeyWalletError::WatchOnly; - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidState); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidState); // Test CoinJoinNotEnabled conversion let err = KeyWalletError::CoinJoinNotEnabled; - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidState); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidState); // Test KeyError conversion (should map to WalletError) let err = KeyWalletError::KeyError("key error".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::WalletError); + assert_ffi_error_code(err.into(), FFIErrorCode::WalletError); // Test Base58 conversion (should map to WalletError) let err = KeyWalletError::Base58; - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::WalletError); + assert_ffi_error_code(err.into(), FFIErrorCode::WalletError); } #[test] @@ -64,58 +60,47 @@ fn test_wallet_manager_error_to_ffi_error() { // Test WalletNotFound conversion let wallet_id = [0u8; 32]; let err = WalletError::WalletNotFound(wallet_id); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::NotFound); + assert_ffi_error_code(err.into(), FFIErrorCode::NotFound); // Test InvalidMnemonic conversion let err = WalletError::InvalidMnemonic("bad mnemonic".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidMnemonic); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidMnemonic); // Test InvalidNetwork conversion let err = WalletError::InvalidNetwork; - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidNetwork); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidNetwork); // Test AccountNotFound conversion let err = WalletError::AccountNotFound(0); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::NotFound); + assert_ffi_error_code(err.into(), FFIErrorCode::NotFound); // Test AddressGeneration conversion let err = WalletError::AddressGeneration("failed to generate".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidAddress); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidAddress); // Test InvalidParameter conversion let err = WalletError::InvalidParameter("bad param".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidInput); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidInput); // Test TransactionBuild conversion let err = WalletError::TransactionBuild("tx build failed".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidTransaction); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidTransaction); // Test InsufficientFunds conversion let err = WalletError::InsufficientFunds; - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidState); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidState); // Test WalletCreation conversion let err = WalletError::WalletCreation("creation failed".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::WalletError); + assert_ffi_error_code(err.into(), FFIErrorCode::WalletError); // Test WalletExists conversion let err = WalletError::WalletExists(wallet_id); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::InvalidState); + assert_ffi_error_code(err.into(), FFIErrorCode::InvalidState); // Test AccountCreation conversion let err = WalletError::AccountCreation("account creation failed".to_string()); - let ffi_err: FFIError = err.into(); - assert_eq!(ffi_err.code, FFIErrorCode::WalletError); + assert_ffi_error_code(err.into(), FFIErrorCode::WalletError); } #[test] @@ -201,9 +186,7 @@ fn test_error_message_consistency() { // Convert to FFIError let ffi_err: FFIError = key_err.into(); - // Note: We can't easily check the message in FFIError since it's a raw pointer - // but we know it should contain the original message - assert_eq!(ffi_err.code, FFIErrorCode::InvalidMnemonic); + assert_ffi_error_code(ffi_err, FFIErrorCode::InvalidMnemonic); } #[test] diff --git a/key-wallet-ffi/tests/test_managed_account_collection.rs b/key-wallet-ffi/tests/test_managed_account_collection.rs index 3cd4bc6b2..16a228f89 100644 --- a/key-wallet-ffi/tests/test_managed_account_collection.rs +++ b/key-wallet-ffi/tests/test_managed_account_collection.rs @@ -403,6 +403,7 @@ fn test_managed_account_collection_null_safety() { managed_wallet_get_account_collection(ptr::null(), ptr::null(), &mut error); assert!(collection.is_null()); assert_eq!(error.code, FFIErrorCode::InvalidInput); + error.free_message(); // Test with null collection for various functions assert_eq!(managed_account_collection_count(ptr::null()), 0);