From 7e7d25a4eedcfaebfd8aa68befdfa5c38fe01713 Mon Sep 17 00:00:00 2001 From: wszdexdrf Date: Wed, 22 Jun 2022 14:37:29 +0530 Subject: [PATCH] Change get_balance to return in categories. Add type balance with add, display traits. Change affected tests. Update `CHANGELOG.md` --- CHANGELOG.md | 1 + src/blockchain/electrum.rs | 2 +- src/blockchain/mod.rs | 2 +- src/testutils/blockchain_tests.rs | 124 +++++++++++++++++------------- src/types.rs | 54 +++++++++++++ src/wallet/mod.rs | 73 ++++++++++++++---- 6 files changed, 187 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3b92b46b7..7e46625faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced) +- Return balance in separate categories, namely `available`, `trusted_pending`, `untrusted_pending` & `immature`. ## [v0.19.0] - [v0.18.0] diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index f8ac758ce0..98c346a637 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -383,6 +383,6 @@ mod test { .sync_wallet(&wallet, None, Default::default()) .unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000); } } diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index cf593c3c83..0e0a3f9453 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -187,7 +187,7 @@ This example shows how to sync multiple walles and return the sum of their balan # use bdk::database::*; # use bdk::wallet::*; # use bdk::*; -fn sum_of_balances(blockchain_factory: B, wallets: &[Wallet]) -> Result { +fn sum_of_balances(blockchain_factory: B, wallets: &[Wallet]) -> Result { Ok(wallets .iter() .map(|w| -> Result<_, Error> { diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index a78e4a893b..7803d4fa17 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -453,7 +453,7 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated"); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance"); assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind"); let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; @@ -476,7 +476,7 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 100_000, "incorrect balance"); assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs"); } @@ -485,7 +485,7 @@ macro_rules! bdk_blockchain_tests { let (wallet, blockchain, descriptors, mut test_client) = init_single_sig(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 0); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 0); test_client.receive(testutils! { @tx ( (@external descriptors, 0) => 50_000 ) @@ -493,8 +493,16 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); - assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance"); + + test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) + }); + + wallet.sync(&blockchain, SyncOptions::default()).unwrap(); + + assert_eq!(wallet.get_balance().unwrap().available, 100_000, "incorrect balance"); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs"); } #[test] @@ -507,7 +515,7 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 105_000, "incorrect balance"); assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs"); assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents"); @@ -531,7 +539,7 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance"); assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs"); assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent"); } @@ -545,14 +553,14 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000); test_client.receive(testutils! { @tx ( (@external descriptors, 0) => 25_000 ) }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance"); } #[test] @@ -565,7 +573,7 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance"); assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs"); assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent"); @@ -579,7 +587,7 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after bump"); assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump"); assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump"); @@ -602,8 +610,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000, "incorrect balance"); assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs"); assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents"); @@ -616,7 +623,7 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after invalidate"); let list_tx_item = &wallet.list_transactions(false).unwrap()[0]; assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate"); @@ -634,7 +641,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey(), 25_000); @@ -645,7 +652,12 @@ macro_rules! bdk_blockchain_tests { println!("{}", bitcoin::consensus::encode::serialize_hex(&tx)); blockchain.broadcast(&tx).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send"); + assert_eq!(wallet.get_balance().unwrap().trusted_pending, details.received, "incorrect balance after send"); + + test_client.generate(1, Some(node_addr)); + wallet.sync(&blockchain, SyncOptions::default()).unwrap(); + + assert_eq!(wallet.get_balance().unwrap().available, details.received, "incorrect balance after send"); assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs"); assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents"); @@ -665,7 +677,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).expect("sync"); - assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 75_000, "incorrect balance"); let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address; let tx1 = { @@ -690,7 +702,7 @@ macro_rules! bdk_blockchain_tests { blockchain.broadcast(&tx2).expect("broadcasting replacement"); receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver"); - assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once"); + assert_eq!(receiver_wallet.get_balance().expect("balance").untrusted_pending, 49_000, "should have received coins once and only once"); } #[test] @@ -716,7 +728,8 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 100_000); + let balance = wallet.get_balance().unwrap(); + assert_eq!(balance.untrusted_pending + balance.get_trusted(), 100_000); } #[test] @@ -730,7 +743,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance"); let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::>(); let details = tx_map.get(&received_txid).unwrap(); @@ -754,7 +767,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey(), 25_000); @@ -766,7 +779,7 @@ macro_rules! bdk_blockchain_tests { blockchain.broadcast(&sent_tx).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), details.received, "incorrect balance after receive"); // empty wallet let wallet = get_wallet_from_descriptors(&descriptors); @@ -797,7 +810,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance"); let mut total_sent = 0; for _ in 0..5 { @@ -814,7 +827,7 @@ macro_rules! bdk_blockchain_tests { } wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000 - total_sent, "incorrect balance after chain"); // empty wallet @@ -824,7 +837,7 @@ macro_rules! bdk_blockchain_tests { test_client.generate(1, Some(node_addr)); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000 - total_sent, "incorrect balance empty wallet"); } @@ -838,7 +851,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf(); @@ -847,8 +860,8 @@ macro_rules! bdk_blockchain_tests { assert!(finalized, "Cannot finalize transaction"); blockchain.broadcast(&psbt.extract_tx()).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees"); - assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), details.received, "incorrect balance from received"); let mut builder = wallet.build_fee_bump(details.txid).unwrap(); builder.fee_rate(FeeRate::from_sat_per_vb(2.1)); @@ -857,8 +870,8 @@ macro_rules! bdk_blockchain_tests { assert!(finalized, "Cannot finalize transaction"); blockchain.broadcast(&new_psbt.extract_tx()).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump"); - assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), new_details.received, "incorrect balance from received after bump"); assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees"); } @@ -873,7 +886,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); @@ -882,8 +895,8 @@ macro_rules! bdk_blockchain_tests { assert!(finalized, "Cannot finalize transaction"); blockchain.broadcast(&psbt.extract_tx()).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send"); - assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), details.received, "incorrect received after send"); let mut builder = wallet.build_fee_bump(details.txid).unwrap(); builder.fee_rate(FeeRate::from_sat_per_vb(5.1)); @@ -892,7 +905,7 @@ macro_rules! bdk_blockchain_tests { assert!(finalized, "Cannot finalize transaction"); blockchain.broadcast(&new_psbt.extract_tx()).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 0, "incorrect balance after change removal"); assert_eq!(new_details.received, 0, "incorrect received after change removal"); assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees"); @@ -908,7 +921,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 75_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); @@ -917,7 +930,7 @@ macro_rules! bdk_blockchain_tests { assert!(finalized, "Cannot finalize transaction"); blockchain.broadcast(&psbt.extract_tx()).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send"); assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send"); let mut builder = wallet.build_fee_bump(details.txid).unwrap(); @@ -928,7 +941,7 @@ macro_rules! bdk_blockchain_tests { blockchain.broadcast(&new_psbt.extract_tx()).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); assert_eq!(new_details.sent, 75_000, "incorrect sent"); - assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), new_details.received, "incorrect balance after add input"); } #[test] @@ -941,7 +954,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 75_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); @@ -950,7 +963,7 @@ macro_rules! bdk_blockchain_tests { assert!(finalized, "Cannot finalize transaction"); blockchain.broadcast(&psbt.extract_tx()).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send"); assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send"); let mut builder = wallet.build_fee_bump(details.txid).unwrap(); @@ -963,7 +976,7 @@ macro_rules! bdk_blockchain_tests { blockchain.broadcast(&new_psbt.extract_tx()).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); assert_eq!(new_details.sent, 75_000, "incorrect sent"); - assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 0, "incorrect balance after add input"); assert_eq!(new_details.received, 0, "incorrect received after add input"); } @@ -977,7 +990,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance"); let mut builder = wallet.build_tx(); let data = [42u8;80]; @@ -992,7 +1005,7 @@ macro_rules! bdk_blockchain_tests { blockchain.broadcast(&tx).unwrap(); test_client.generate(1, Some(node_addr)); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send"); let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::>(); let _ = tx_map.get(&tx.txid()).unwrap(); @@ -1005,20 +1018,25 @@ macro_rules! bdk_blockchain_tests { let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address; wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().immature, 0, "incorrect balance"); test_client.generate(1, Some(wallet_addr)); - #[cfg(feature = "rpc")] + wallet.sync(&blockchain, SyncOptions::default()).unwrap(); + + #[cfg(not(feature="rpc"))] { // rpc consider coinbase only when mature (100 blocks) - let node_addr = test_client.get_node_address(None); - test_client.generate(100, Some(node_addr)); + assert!(wallet.get_balance().unwrap().immature > 0, "incorrect balance after receiving coinbase"); } - + // make coinbase mature (100 blocks) + let node_addr = test_client.get_node_address(None); + test_client.generate(100, Some(node_addr)); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase"); + + assert!(wallet.get_balance().unwrap().available > 0, "incorrect balance after maturing coinbase"); + } #[test] @@ -1095,7 +1113,7 @@ macro_rules! bdk_blockchain_tests { }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance"); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "wallet has incorrect balance"); // 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet @@ -1107,7 +1125,7 @@ macro_rules! bdk_blockchain_tests { let tx = psbt.extract_tx(); blockchain.broadcast(&tx).unwrap(); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send"); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), details.received, "wallet has incorrect balance after send"); assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs"); assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents"); test_client.generate(1, None); @@ -1218,7 +1236,7 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000); let tx = { let mut builder = wallet.build_tx(); @@ -1241,7 +1259,7 @@ macro_rules! bdk_blockchain_tests { @tx ( (@external descriptors, 0) => 50_000 ) }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000); let tx = { let mut builder = wallet.build_tx(); @@ -1262,7 +1280,7 @@ macro_rules! bdk_blockchain_tests { @tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 6 ) }); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert_eq!(wallet.get_balance().unwrap(), 50_000); + assert_eq!(wallet.get_balance().unwrap().get_trusted(), 50_000); let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap(); diff --git a/src/types.rs b/src/types.rs index fc81bc2789..77462ffda1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -240,6 +240,60 @@ impl BlockTime { } } +/// Balance differentiated in various categories +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)] +pub struct Balance { + /// All coinbase outputs not yet matured + pub immature: u64, + /// Unconfirmed UTXO generated by a wallet tx + pub trusted_pending: u64, + /// Unconfirmed UTXO received from an external wallet + pub untrusted_pending: u64, + /// All rest available balance + pub available: u64, +} + +impl Balance { + /// Get sum of trusted_pending and available coins + pub fn get_trusted(&self) -> u64 { + self.available + self.trusted_pending + } +} + +impl std::fmt::Display for Balance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, available: {} }}", + self.immature, self.trusted_pending, self.untrusted_pending, self.available + ) + } +} + +impl std::ops::Add for Balance { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { + immature: self.immature + other.immature, + trusted_pending: self.trusted_pending + other.trusted_pending, + untrusted_pending: self.untrusted_pending + other.untrusted_pending, + available: self.available + other.available, + } + } +} + +impl std::iter::Sum for Balance { + fn sum>(iter: I) -> Self { + iter.fold( + Balance { + ..Default::default() + }, + |a, b| a + b, + ) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 2e04c0c894..ea28cb124b 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -446,15 +446,53 @@ where self.database.borrow().iter_txs(include_raw) } - /// Return the balance, meaning the sum of this wallet's unspent outputs' values + /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature + /// values. /// /// Note that this methods only operate on the internal database, which first needs to be /// [`Wallet::sync`] manually. - pub fn get_balance(&self) -> Result { - Ok(self - .list_unspent()? - .iter() - .fold(0, |sum, i| sum + i.txout.value)) + pub fn get_balance(&self) -> Result { + let mut immature = 0; + let mut trusted_pending = 0; + let mut untrusted_pending = 0; + let mut available = 0; + let utxos = self.list_unspent()?; + let database = self.database.borrow(); + for u in utxos { + // Unwrap used since utxo set is created from database + let tx = database + .get_tx(&u.outpoint.txid, true)? + .expect("Transaction not found in database"); + if let Some(tx_conf_time) = &tx.confirmation_time { + if tx.transaction.expect("No transaction").is_coin_base() { + let last_sync_height = self + .database() + .get_sync_time()? + .map(|sync_time| sync_time.block_time.height); + + if let Some(last_sync_height) = last_sync_height { + if (last_sync_height - tx_conf_time.height) >= COINBASE_MATURITY { + available += u.txout.value; + } else { + immature += u.txout.value; + } + } + } else { + available += u.txout.value; + } + } else if u.keychain == KeychainKind::Internal { + trusted_pending += u.txout.value; + } else { + untrusted_pending += u.txout.value; + } + } + + Ok(Balance { + immature, + trusted_pending, + untrusted_pending, + available, + }) } /// Add an external signer @@ -4686,23 +4724,30 @@ pub(crate) mod test { Some(confirmation_time), (@coinbase true) ); + let sync_time = SyncTime { + block_time: BlockTime { + height: confirmation_time, + timestamp: 0, + }, + }; + wallet + .database + .borrow_mut() + .set_sync_time(sync_time.clone()) + .unwrap(); let not_yet_mature_time = confirmation_time + COINBASE_MATURITY - 1; let maturity_time = confirmation_time + COINBASE_MATURITY; - // The balance is nonzero, even if we can't spend anything - // FIXME: we should differentiate the balance between immature, - // trusted, untrusted_pending - // See https://github.com/bitcoindevkit/bdk/issues/238 let balance = wallet.get_balance().unwrap(); - assert!(balance != 0); + assert!(balance.immature != 0 && balance.available == 0); // We try to create a transaction, only to notice that all // our funds are unspendable let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let mut builder = wallet.build_tx(); builder - .add_recipient(addr.script_pubkey(), balance / 2) + .add_recipient(addr.script_pubkey(), balance.immature / 2) .set_current_height(confirmation_time); assert!(matches!( builder.finish().unwrap_err(), @@ -4715,7 +4760,7 @@ pub(crate) mod test { // Still unspendable... let mut builder = wallet.build_tx(); builder - .add_recipient(addr.script_pubkey(), balance / 2) + .add_recipient(addr.script_pubkey(), balance.immature / 2) .set_current_height(not_yet_mature_time); assert!(matches!( builder.finish().unwrap_err(), @@ -4728,7 +4773,7 @@ pub(crate) mod test { // ...Now the coinbase is mature :) let mut builder = wallet.build_tx(); builder - .add_recipient(addr.script_pubkey(), balance / 2) + .add_recipient(addr.script_pubkey(), balance.immature / 2) .set_current_height(maturity_time); builder.finish().unwrap(); }