From 0f03831274d3aa69da6e89729c65d66530bbd752 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 | 120 +++++++++++------- .../configurable_blockchain_tests.rs | 8 +- src/types.rs | 61 ++++++++- src/wallet/mod.rs | 102 +++++++++++++-- 7 files changed, 226 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fda3e060..2fd9022cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Consolidate params `fee_amount` and `amount_needed` in `target_amount` in `CoinSelectionAlgorithm::coin_select` signature. - Change the meaning of the `fee_amount` field inside `CoinSelectionResult`: from now on the `fee_amount` will represent only the fees asociated with the utxos in the `selected` field of `CoinSelectionResult`. - New `RpcBlockchain` implementation with various fixes. +- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`. ## [v0.20.0] - [v0.19.0] diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index faf7ea756..c1e1c66cf 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -385,7 +385,7 @@ 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); } #[test] diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index 1dc5c95a1..2502f61b0 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -194,7 +194,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 975c377dc..a3d7c2b17 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -454,7 +454,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]; @@ -477,7 +477,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"); } @@ -486,7 +486,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().get_total(), 0); test_client.receive(testutils! { @tx ( (@external descriptors, 0) => 50_000 ) @@ -494,8 +494,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().confirmed, 100_000, "incorrect balance"); + assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs"); } #[test] @@ -508,7 +516,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"); @@ -532,7 +540,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"); } @@ -546,14 +554,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] @@ -566,7 +574,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"); @@ -580,7 +588,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"); @@ -603,8 +611,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_spendable(), 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"); @@ -617,7 +624,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"); @@ -635,7 +642,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); @@ -646,7 +653,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().confirmed, 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"); @@ -720,7 +732,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_spendable(), 75_000, "incorrect balance"); let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address; let tx1 = { @@ -744,7 +756,7 @@ macro_rules! bdk_blockchain_tests { blockchain.broadcast(&tx1).expect("broadcasting first"); 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] @@ -770,7 +782,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_spendable(), 100_000); } #[test] @@ -784,7 +797,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(); @@ -808,7 +821,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); @@ -820,7 +833,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_spendable(), details.received, "incorrect balance after receive"); // empty wallet let wallet = get_wallet_from_descriptors(&descriptors); @@ -851,7 +864,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 { @@ -868,7 +881,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_spendable(), 50_000 - total_sent, "incorrect balance after chain"); // empty wallet @@ -878,7 +891,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_spendable(), 50_000 - total_sent, "incorrect balance empty wallet"); } @@ -892,7 +905,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_spendable(), 50_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf(); @@ -901,8 +914,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_spendable(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees"); + assert_eq!(wallet.get_balance().unwrap().get_spendable(), 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)); @@ -911,8 +924,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_spendable(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump"); + assert_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance from received after bump"); assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees"); } @@ -927,7 +940,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_spendable(), 50_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); @@ -936,8 +949,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_spendable(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send"); + assert_eq!(wallet.get_balance().unwrap().get_spendable(), 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)); @@ -946,7 +959,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_spendable(), 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"); @@ -962,7 +975,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_spendable(), 75_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); @@ -971,7 +984,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_spendable(), 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(); @@ -982,7 +995,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_spendable(), new_details.received, "incorrect balance after add input"); } #[test] @@ -995,7 +1008,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_spendable(), 75_000, "incorrect balance"); let mut builder = wallet.build_tx(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); @@ -1004,7 +1017,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_spendable(), 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(); @@ -1017,7 +1030,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_spendable(), 0, "incorrect balance after add input"); assert_eq!(new_details.received, 0, "incorrect received after add input"); } @@ -1031,7 +1044,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]; @@ -1046,7 +1059,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_spendable(), 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(); @@ -1060,12 +1073,21 @@ macro_rules! bdk_blockchain_tests { println!("wallet addr: {}", wallet_addr); 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)); wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase"); + + 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().confirmed > 0, "incorrect balance after maturing coinbase"); + } #[test] @@ -1142,7 +1164,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 @@ -1154,7 +1176,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_spendable(), 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); @@ -1265,7 +1287,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(); @@ -1288,7 +1310,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(); @@ -1309,7 +1331,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_spendable(), 50_000); let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap(); diff --git a/src/testutils/configurable_blockchain_tests.rs b/src/testutils/configurable_blockchain_tests.rs index b07fc9cda..8662844dd 100644 --- a/src/testutils/configurable_blockchain_tests.rs +++ b/src/testutils/configurable_blockchain_tests.rs @@ -124,7 +124,7 @@ where // perform wallet sync wallet.sync(&blockchain, Default::default()).unwrap(); - let wallet_balance = wallet.get_balance().unwrap(); + let wallet_balance = wallet.get_balance().unwrap().get_total(); println!( "max: {}, min: {}, actual: {}", max_balance, min_balance, wallet_balance @@ -193,7 +193,7 @@ where wallet.sync(&blockchain, Default::default()).unwrap(); println!("sync done!"); - let balance = wallet.get_balance().unwrap(); + let balance = wallet.get_balance().unwrap().get_total(); assert_eq!(balance, expected_balance); } @@ -245,13 +245,13 @@ where // actually test the wallet wallet.sync(&blockchain, Default::default()).unwrap(); - let balance = wallet.get_balance().unwrap(); + let balance = wallet.get_balance().unwrap().get_total(); assert_eq!(balance, expected_balance); // now try with a fresh wallet let fresh_wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap(); fresh_wallet.sync(&blockchain, Default::default()).unwrap(); - let fresh_balance = fresh_wallet.get_balance().unwrap(); + let fresh_balance = fresh_wallet.get_balance().unwrap().get_total(); assert_eq!(fresh_balance, expected_balance); } diff --git a/src/types.rs b/src/types.rs index edebd28da..5e54a3dcd 100644 --- a/src/types.rs +++ b/src/types.rs @@ -227,7 +227,7 @@ pub struct TransactionDetails { /// Sent value (sats) /// Sum of owned inputs of this transaction. pub sent: u64, - /// Fee value (sats) if available. + /// Fee value (sats) if confirmed. /// The availability of the fee depends on the backend. It's never `None` with an Electrum /// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive /// funds while offline. @@ -262,6 +262,65 @@ impl BlockTime { } } +/// Balance differentiated in various categories +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] +pub struct Balance { + /// All coinbase outputs not yet matured + pub immature: u64, + /// Unconfirmed UTXOs generated by a wallet tx + pub trusted_pending: u64, + /// Unconfirmed UTXOs received from an external wallet + pub untrusted_pending: u64, + /// Confirmed and immediately spendable balance + pub confirmed: u64, +} + +impl Balance { + /// Get sum of trusted_pending and confirmed coins + pub fn get_spendable(&self) -> u64 { + self.confirmed + self.trusted_pending + } + + /// Get the whole balance visible to the wallet + pub fn get_total(&self) -> u64 { + self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature + } +} + +impl std::fmt::Display for Balance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}", + self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed + ) + } +} + +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, + confirmed: self.confirmed + other.confirmed, + } + } +} + +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 dc1f3724a..aa7ef202e 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -460,15 +460,52 @@ 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 confirmed = 0; + let utxos = self.list_unspent()?; + let database = self.database.borrow(); + let last_sync_height = match database + .get_sync_time()? + .map(|sync_time| sync_time.block_time.height) + { + Some(height) => height, + // None means database was never synced + None => return Ok(Balance::default()), + }; + 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() + && (last_sync_height - tx_conf_time.height) < COINBASE_MATURITY + { + immature += u.txout.value; + } else { + confirmed += 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, + confirmed, + }) } /// Add an external signer @@ -5232,23 +5269,38 @@ 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) + .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_eq!( + balance, + Balance { + immature: 25_000, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 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) .current_height(confirmation_time); assert!(matches!( builder.finish().unwrap_err(), @@ -5261,7 +5313,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) .current_height(not_yet_mature_time); assert!(matches!( builder.finish().unwrap_err(), @@ -5272,9 +5324,31 @@ pub(crate) mod test { )); // ...Now the coinbase is mature :) + let sync_time = SyncTime { + block_time: BlockTime { + height: maturity_time, + timestamp: 0, + }, + }; + wallet + .database + .borrow_mut() + .set_sync_time(sync_time) + .unwrap(); + + let balance = wallet.get_balance().unwrap(); + assert_eq!( + balance, + Balance { + immature: 0, + trusted_pending: 0, + untrusted_pending: 0, + confirmed: 25_000 + } + ); let mut builder = wallet.build_tx(); builder - .add_recipient(addr.script_pubkey(), balance / 2) + .add_recipient(addr.script_pubkey(), balance.confirmed / 2) .current_height(maturity_time); builder.finish().unwrap(); }