Skip to content

Commit 7b515cd

Browse files
committed
fix: unify immature balance tracking with remaining balances
We currently keep track of immature balance separately from other balances with the `immature_balance` function. This PR drops the extra function, adds `WalletBalance.immature` and changes the balance tracking so that we have all balances in one place. It also fixes a mix-up between locked and immature balance in FFI.
1 parent 3814161 commit 7b515cd

File tree

14 files changed

+306
-51
lines changed

14 files changed

+306
-51
lines changed

key-wallet-ffi/FFI_API.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -915,14 +915,14 @@ Get address pool information for an account # Safety - `managed_wallet` must b
915915
#### `managed_wallet_get_balance`
916916

917917
```c
918-
managed_wallet_get_balance(managed_wallet: *const FFIManagedWalletInfo, confirmed_out: *mut u64, unconfirmed_out: *mut u64, locked_out: *mut u64, total_out: *mut u64, error: *mut FFIError,) -> bool
918+
managed_wallet_get_balance(managed_wallet: *const FFIManagedWalletInfo, confirmed_out: *mut u64, unconfirmed_out: *mut u64, immature_out: *mut u64, locked_out: *mut u64, total_out: *mut u64, error: *mut FFIError,) -> bool
919919
```
920920

921921
**Description:**
922-
Get wallet balance from managed wallet info Returns the balance breakdown including confirmed, unconfirmed, locked, and total amounts. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `confirmed_out` must be a valid pointer to store the confirmed balance - `unconfirmed_out` must be a valid pointer to store the unconfirmed balance - `locked_out` must be a valid pointer to store the locked balance - `total_out` must be a valid pointer to store the total balance - `error` must be a valid pointer to an FFIError
922+
Get wallet balance from managed wallet info Returns the balance breakdown including confirmed, unconfirmed, immature, locked, and total amounts. # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `confirmed_out` must be a valid pointer to store the confirmed balance - `unconfirmed_out` must be a valid pointer to store the unconfirmed balance - `immature_out` must be a valid pointer to store the immature balance - `locked_out` must be a valid pointer to store the locked balance - `total_out` must be a valid pointer to store the total balance - `error` must be a valid pointer to an FFIError
923923

924924
**Safety:**
925-
- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `confirmed_out` must be a valid pointer to store the confirmed balance - `unconfirmed_out` must be a valid pointer to store the unconfirmed balance - `locked_out` must be a valid pointer to store the locked balance - `total_out` must be a valid pointer to store the total balance - `error` must be a valid pointer to an FFIError
925+
- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `confirmed_out` must be a valid pointer to store the confirmed balance - `unconfirmed_out` must be a valid pointer to store the unconfirmed balance - `immature_out` must be a valid pointer to store the immature balance - `locked_out` must be a valid pointer to store the locked balance - `total_out` must be a valid pointer to store the total balance - `error` must be a valid pointer to an FFIError
926926

927927
**Module:** `managed_wallet`
928928

key-wallet-ffi/include/key_wallet_ffi.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,11 +516,15 @@ typedef struct {
516516
*/
517517
uint64_t unconfirmed;
518518
/*
519-
Immature balance in duffs (e.g., mining rewards)
519+
Immature balance in duffs (e.g., mining rewards not yet mature)
520520
*/
521521
uint64_t immature;
522522
/*
523-
Total balance (confirmed + unconfirmed) in duffs
523+
Locked balance in duffs (e.g., CoinJoin reserves)
524+
*/
525+
uint64_t locked;
526+
/*
527+
Total balance in duffs
524528
*/
525529
uint64_t total;
526530
} FFIBalance;
@@ -3069,13 +3073,14 @@ bool managed_wallet_get_bip_44_internal_address_range(FFIManagedWalletInfo *mana
30693073
/*
30703074
Get wallet balance from managed wallet info
30713075
3072-
Returns the balance breakdown including confirmed, unconfirmed, locked, and total amounts.
3076+
Returns the balance breakdown including confirmed, unconfirmed, immature, locked, and total amounts.
30733077
30743078
# Safety
30753079
30763080
- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo
30773081
- `confirmed_out` must be a valid pointer to store the confirmed balance
30783082
- `unconfirmed_out` must be a valid pointer to store the unconfirmed balance
3083+
- `immature_out` must be a valid pointer to store the immature balance
30793084
- `locked_out` must be a valid pointer to store the locked balance
30803085
- `total_out` must be a valid pointer to store the total balance
30813086
- `error` must be a valid pointer to an FFIError
@@ -3084,6 +3089,7 @@ bool managed_wallet_get_bip_44_internal_address_range(FFIManagedWalletInfo *mana
30843089
bool managed_wallet_get_balance(const FFIManagedWalletInfo *managed_wallet,
30853090
uint64_t *confirmed_out,
30863091
uint64_t *unconfirmed_out,
3092+
uint64_t *immature_out,
30873093
uint64_t *locked_out,
30883094
uint64_t *total_out,
30893095
FFIError *error)

key-wallet-ffi/src/managed_account.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,8 @@ pub unsafe extern "C" fn managed_account_get_balance(
515515
*balance_out = crate::types::FFIBalance {
516516
confirmed: balance.spendable(),
517517
unconfirmed: balance.unconfirmed(),
518-
immature: 0, // WalletBalance doesn't have immature field
518+
immature: balance.immature(),
519+
locked: balance.locked(),
519520
total: balance.total(),
520521
};
521522

@@ -1236,6 +1237,7 @@ mod tests {
12361237
confirmed: 999,
12371238
unconfirmed: 999,
12381239
immature: 999,
1240+
locked: 999,
12391241
total: 999,
12401242
};
12411243
let success = managed_account_get_balance(account, &mut balance_out);
@@ -1244,6 +1246,7 @@ mod tests {
12441246
assert_eq!(balance_out.confirmed, 0);
12451247
assert_eq!(balance_out.unconfirmed, 0);
12461248
assert_eq!(balance_out.immature, 0);
1249+
assert_eq!(balance_out.locked, 0);
12471250
assert_eq!(balance_out.total, 0);
12481251

12491252
// Test get_transaction_count

key-wallet-ffi/src/managed_wallet.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -531,13 +531,14 @@ pub unsafe extern "C" fn managed_wallet_get_bip_44_internal_address_range(
531531

532532
/// Get wallet balance from managed wallet info
533533
///
534-
/// Returns the balance breakdown including confirmed, unconfirmed, locked, and total amounts.
534+
/// Returns the balance breakdown including confirmed, unconfirmed, immature, locked, and total amounts.
535535
///
536536
/// # Safety
537537
///
538538
/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo
539539
/// - `confirmed_out` must be a valid pointer to store the confirmed balance
540540
/// - `unconfirmed_out` must be a valid pointer to store the unconfirmed balance
541+
/// - `immature_out` must be a valid pointer to store the immature balance
541542
/// - `locked_out` must be a valid pointer to store the locked balance
542543
/// - `total_out` must be a valid pointer to store the total balance
543544
/// - `error` must be a valid pointer to an FFIError
@@ -546,6 +547,7 @@ pub unsafe extern "C" fn managed_wallet_get_balance(
546547
managed_wallet: *const FFIManagedWalletInfo,
547548
confirmed_out: *mut u64,
548549
unconfirmed_out: *mut u64,
550+
immature_out: *mut u64,
549551
locked_out: *mut u64,
550552
total_out: *mut u64,
551553
error: *mut FFIError,
@@ -561,6 +563,7 @@ pub unsafe extern "C" fn managed_wallet_get_balance(
561563

562564
if confirmed_out.is_null()
563565
|| unconfirmed_out.is_null()
566+
|| immature_out.is_null()
564567
|| locked_out.is_null()
565568
|| total_out.is_null()
566569
{
@@ -578,6 +581,7 @@ pub unsafe extern "C" fn managed_wallet_get_balance(
578581
unsafe {
579582
*confirmed_out = balance.spendable();
580583
*unconfirmed_out = balance.unconfirmed();
584+
*immature_out = balance.immature();
581585
*locked_out = balance.locked();
582586
*total_out = balance.total();
583587
}
@@ -1041,15 +1045,16 @@ mod tests {
10411045
let wallet_arc = unsafe { &(*wallet_ptr).wallet };
10421046
let mut managed_info = ManagedWalletInfo::from_wallet(wallet_arc);
10431047

1044-
// Set some test balance values
1045-
managed_info.balance = WalletBalance::new(1000000, 50000, 25000);
1048+
// Set some test balance values (confirmed, unconfirmed, immature, locked)
1049+
managed_info.balance = WalletBalance::new(1000000, 50000, 10000, 25000);
10461050

10471051
let ffi_managed = FFIManagedWalletInfo::new(managed_info);
10481052
let ffi_managed_ptr = Box::into_raw(Box::new(ffi_managed));
10491053

10501054
// Test getting balance
10511055
let mut confirmed: u64 = 0;
10521056
let mut unconfirmed: u64 = 0;
1057+
let mut immature: u64 = 0;
10531058
let mut locked: u64 = 0;
10541059
let mut total: u64 = 0;
10551060

@@ -1058,6 +1063,7 @@ mod tests {
10581063
ffi_managed_ptr,
10591064
&mut confirmed,
10601065
&mut unconfirmed,
1066+
&mut immature,
10611067
&mut locked,
10621068
&mut total,
10631069
&mut error,
@@ -1067,15 +1073,17 @@ mod tests {
10671073
assert!(success);
10681074
assert_eq!(confirmed, 1000000);
10691075
assert_eq!(unconfirmed, 50000);
1076+
assert_eq!(immature, 10000);
10701077
assert_eq!(locked, 25000);
1071-
assert_eq!(total, 1075000);
1078+
assert_eq!(total, 1085000);
10721079

10731080
// Test with null managed wallet
10741081
let success = unsafe {
10751082
managed_wallet_get_balance(
10761083
ptr::null(),
10771084
&mut confirmed,
10781085
&mut unconfirmed,
1086+
&mut immature,
10791087
&mut locked,
10801088
&mut total,
10811089
&mut error,
@@ -1091,6 +1099,7 @@ mod tests {
10911099
ffi_managed_ptr,
10921100
ptr::null_mut(),
10931101
&mut unconfirmed,
1102+
&mut immature,
10941103
&mut locked,
10951104
&mut total,
10961105
&mut error,

key-wallet-ffi/src/types.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ pub struct FFIBalance {
5555
pub confirmed: u64,
5656
/// Unconfirmed balance in duffs
5757
pub unconfirmed: u64,
58-
/// Immature balance in duffs (e.g., mining rewards)
58+
/// Immature balance in duffs (e.g., mining rewards not yet mature)
5959
pub immature: u64,
60-
/// Total balance (confirmed + unconfirmed) in duffs
60+
/// Locked balance in duffs (e.g., CoinJoin reserves)
61+
pub locked: u64,
62+
/// Total balance in duffs
6163
pub total: u64,
6264
}
6365

@@ -66,7 +68,8 @@ impl From<key_wallet::WalletBalance> for FFIBalance {
6668
FFIBalance {
6769
confirmed: balance.spendable(),
6870
unconfirmed: balance.unconfirmed(),
69-
immature: balance.locked(), // Map locked to immature for now
71+
immature: balance.immature(),
72+
locked: balance.locked(),
7073
total: balance.total(),
7174
}
7275
}

key-wallet-manager/tests/spv_integration_tests.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,29 @@ fn assert_wallet_heights(manager: &WalletManager<ManagedWalletInfo>, expected_he
162162
}
163163
}
164164

165+
/// Create a coinbase transaction paying to the given script
166+
/// TODO: Unify with other `create_coinbase_transaction` helpers into `dashcore` crate.
167+
fn create_coinbase_transaction(script_pubkey: ScriptBuf, value: u64) -> Transaction {
168+
Transaction {
169+
version: 2,
170+
lock_time: 0,
171+
input: vec![TxIn {
172+
previous_output: OutPoint {
173+
txid: Txid::all_zeros(),
174+
vout: 0xffffffff,
175+
},
176+
script_sig: ScriptBuf::new(),
177+
sequence: 0xffffffff,
178+
witness: dashcore::Witness::default(),
179+
}],
180+
output: vec![TxOut {
181+
value,
182+
script_pubkey,
183+
}],
184+
special_transaction_payload: None,
185+
}
186+
}
187+
165188
/// Test that the wallet heights are updated after block processing.
166189
#[tokio::test]
167190
async fn test_height_updated_after_block_processing() {
@@ -181,3 +204,81 @@ async fn test_height_updated_after_block_processing() {
181204
assert_wallet_heights(&manager, height);
182205
}
183206
}
207+
208+
#[tokio::test]
209+
async fn test_immature_balance_matures_during_block_processing() {
210+
let mut manager = WalletManager::<ManagedWalletInfo>::new(Network::Testnet);
211+
212+
// Create a wallet and get an address to receive the coinbase
213+
let wallet_id = manager
214+
.create_wallet_with_random_mnemonic(WalletAccountCreationOptions::Default)
215+
.expect("Failed to create wallet");
216+
217+
let account_xpub = {
218+
let wallet = manager.get_wallet(&wallet_id).expect("Wallet should exist");
219+
wallet.accounts.standard_bip44_accounts.get(&0).expect("Should have account").account_xpub
220+
};
221+
222+
// Get the first receive address from the wallet
223+
let receive_address = {
224+
let wallet_info =
225+
manager.get_wallet_info_mut(&wallet_id).expect("Wallet info should exist");
226+
wallet_info
227+
.first_bip44_managed_account_mut()
228+
.expect("Should have managed account")
229+
.next_receive_address(Some(&account_xpub), true)
230+
.expect("Should get address")
231+
};
232+
233+
// Create a coinbase transaction paying to our wallet
234+
let coinbase_value = 100;
235+
let coinbase_tx = create_coinbase_transaction(receive_address.script_pubkey(), coinbase_value);
236+
237+
// Process the coinbase at height 1000
238+
let coinbase_height = 1000;
239+
let coinbase_block = create_test_block(coinbase_height, vec![coinbase_tx.clone()]);
240+
manager.process_block(&coinbase_block, coinbase_height).await;
241+
242+
// Verify the coinbase is detected and stored as immature
243+
let wallet_info = manager.get_wallet_info(&wallet_id).expect("Wallet info should exist");
244+
assert!(
245+
wallet_info.immature_transactions().contains(&coinbase_tx),
246+
"Coinbase should be in immature transactions"
247+
);
248+
assert_eq!(
249+
wallet_info.balance().immature(),
250+
coinbase_value,
251+
"Immature balance should reflect coinbase"
252+
);
253+
254+
// Process 99 more blocks up to just before maturity
255+
let maturity_height = coinbase_height + 100;
256+
for height in (coinbase_height + 1)..maturity_height {
257+
let block = create_test_block(height, vec![create_test_transaction(1000)]);
258+
manager.process_block(&block, height).await;
259+
}
260+
261+
// Verify still immature just before maturity
262+
let wallet_info = manager.get_wallet_info(&wallet_id).expect("Wallet info should exist");
263+
assert!(
264+
wallet_info.immature_transactions().contains(&coinbase_tx),
265+
"Coinbase should still be immature at height {}",
266+
maturity_height - 1
267+
);
268+
269+
// Process the maturity block
270+
let maturity_block = create_test_block(maturity_height, vec![create_test_transaction(1000)]);
271+
manager.process_block(&maturity_block, maturity_height).await;
272+
273+
// Verify the coinbase has matured
274+
let wallet_info = manager.get_wallet_info(&wallet_id).expect("Wallet info should exist");
275+
assert!(
276+
!wallet_info.immature_transactions().contains(&coinbase_tx),
277+
"Coinbase should no longer be immature after maturity height"
278+
);
279+
assert_eq!(
280+
wallet_info.balance().immature(),
281+
0,
282+
"Immature balance should be zero after maturity"
283+
);
284+
}

key-wallet/src/managed_account/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,18 +268,21 @@ impl ManagedAccount {
268268
pub fn update_balance(&mut self, synced_height: u32) {
269269
let mut spendable = 0;
270270
let mut unconfirmed = 0;
271+
let mut immature = 0;
271272
let mut locked = 0;
272273
for utxo in self.utxos.values() {
273274
let value = utxo.txout.value;
274275
if utxo.is_locked {
275276
locked += value;
277+
} else if !utxo.is_mature(synced_height) {
278+
immature += value;
276279
} else if utxo.is_spendable(synced_height) {
277280
spendable += value;
278281
} else {
279282
unconfirmed += value;
280283
}
281284
}
282-
self.balance = WalletBalance::new(spendable, unconfirmed, locked);
285+
self.balance = WalletBalance::new(spendable, unconfirmed, immature, locked);
283286
self.metadata.last_used = Some(Self::current_timestamp());
284287
}
285288

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use crate::account::StandardAccountType;
2+
use crate::managed_account::address_pool::{AddressPool, AddressPoolType, KeySource};
3+
use crate::managed_account::managed_account_type::ManagedAccountType;
4+
use crate::managed_account::ManagedAccount;
5+
use crate::{DerivationPath, Network};
6+
7+
impl ManagedAccount {
8+
/// Create a test managed account with a standard BIP44 type and empty address pools
9+
pub fn new_test_bip44(network: Network) -> Self {
10+
let base_path = DerivationPath::master();
11+
12+
let external_pool = AddressPool::new(
13+
base_path.clone(),
14+
AddressPoolType::External,
15+
20,
16+
network,
17+
&KeySource::NoKeySource,
18+
)
19+
.expect("Failed to create external address pool");
20+
21+
let internal_pool = AddressPool::new(
22+
base_path,
23+
AddressPoolType::Internal,
24+
20,
25+
network,
26+
&KeySource::NoKeySource,
27+
)
28+
.expect("Failed to create internal address pool");
29+
30+
let account_type = ManagedAccountType::Standard {
31+
index: 0,
32+
standard_account_type: StandardAccountType::BIP44Account,
33+
external_addresses: external_pool,
34+
internal_addresses: internal_pool,
35+
};
36+
37+
ManagedAccount::new(account_type, network, false)
38+
}
39+
}

key-wallet/src/test_utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod account;
12
mod address;
23
mod utxo;
34

0 commit comments

Comments
 (0)