diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fda3e060..87b119268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [v0.21.0] - [v0.20.0] + - Add `descriptor::checksum::get_checksum_bytes` method. - Add `Excess` enum to handle remaining amount after coin selection. - Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`. @@ -14,6 +17,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] @@ -44,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v0.18.0] - [v0.17.0] -- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, ie. for mobile platforms. +- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, i.e. for mobile platforms. - Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait. - Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database. - Add `keychain: KeychainKind` to `wallet::AddressInfo`. @@ -487,4 +491,5 @@ final transaction is created by calling `finish` on the builder. [v0.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0 [v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.0 [v0.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0 -[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...HEAD +[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0 +[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.21.0...HEAD diff --git a/Cargo.toml b/Cargo.toml index 9c9109295..d61e4f372 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk" -version = "0.20.1-dev" +version = "0.21.1-dev" edition = "2018" authors = ["Alekos Filini ", "Riccardo Casatta "] homepage = "https://bitcoindevkit.org" diff --git a/DEVELOPMENT_CYCLE.md b/DEVELOPMENT_CYCLE.md index c2392ddfa..bd6f8638f 100644 --- a/DEVELOPMENT_CYCLE.md +++ b/DEVELOPMENT_CYCLE.md @@ -39,7 +39,7 @@ Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordin 11. Publish **all** the updated crates to crates.io. 12. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev". 13. Merge the release branch back into `master`. -14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (ie. `bdk-macros = { path = "./macros"}`) +14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (i.e. `bdk-macros = { path = "./macros"}`) 15. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes. 16. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs. 17. Announce the release on Twitter, Discord and Telegram. 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/descriptor/dsl.rs b/src/descriptor/dsl.rs index ccbe2b2bb..2d0d9422d 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -839,7 +839,7 @@ mod test { } } - // - at least one of each "type" of operator; ie. one modifier, one leaf_opcode, one leaf_opcode_value, etc. + // - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc. // - mixing up key types that implement IntoDescriptorKey in multi() or thresh() // expected script for pk and bare manually created diff --git a/src/lib.rs b/src/lib.rs index 4c4bb3c20..b3195fbae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ //! interact with the bitcoin P2P network. //! //! ```toml -//! bdk = "0.20.0" +//! bdk = "0.21.0" //! ``` //! //! # Examples 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..f7f7dc526 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -325,7 +325,7 @@ where /// Return a derived address using the external descriptor, see [`AddressIndex`] for /// available address index selection strategies. If none of the keys in the descriptor are derivable - /// (ie. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. + /// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. pub fn get_address(&self, address_index: AddressIndex) -> Result { self._get_address(address_index, KeychainKind::External) } @@ -335,7 +335,7 @@ where /// If the wallet doesn't have an internal descriptor it will use the external descriptor. /// /// see [`AddressIndex`] for available address index selection strategies. If none of the keys - /// in the descriptor are derivable (ie. does not end with /*) then the same address will always + /// in the descriptor are derivable (i.e. does not end with /*) then the same address will always /// be returned for any [`AddressIndex`]. pub fn get_internal_address(&self, address_index: AddressIndex) -> Result { self._get_address(address_index, KeychainKind::Internal) @@ -454,21 +454,58 @@ where /// }); /// ``` /// - /// Note that this methods only operate on the internal database, which first needs to be + /// Note that this method only operates on the internal database, which first needs to be /// [`Wallet::sync`] manually. pub fn list_transactions(&self, include_raw: bool) -> Result, Error> { 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 + /// Note that this method only operates 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(); } diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 0fee8aa9e..9e93e551c 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -516,7 +516,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> } } - /// Finish the building the transaction. + /// Finish building the transaction. /// /// Returns the [`BIP174`] "PSBT" and summary details about the transaction. ///