Skip to content

Commit 74def0a

Browse files
committed
Change get_balance to return in categories.
Add type balance with add, display traits. Change affected tests. Update `CHANGELOG.md`
1 parent dd832cb commit 74def0a

File tree

7 files changed

+207
-71
lines changed

7 files changed

+207
-71
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
- 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)
1010
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
1111
- Set coin type in BIP44, BIP49, and BIP84 templates
12+
- Return balance in separate categories, namely `available`, `trusted_pending`, `untrusted_pending` & `immature`.
1213

1314
## [v0.19.0] - [v0.18.0]
1415

src/blockchain/electrum.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ mod test {
378378
.sync_wallet(&wallet, None, Default::default())
379379
.unwrap();
380380

381-
assert_eq!(wallet.get_balance().unwrap(), 50_000);
381+
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
382382
}
383383

384384
#[test]

src/blockchain/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ This example shows how to sync multiple walles and return the sum of their balan
187187
# use bdk::database::*;
188188
# use bdk::wallet::*;
189189
# use bdk::*;
190-
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
190+
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<Balance, Error> {
191191
Ok(wallets
192192
.iter()
193193
.map(|w| -> Result<_, Error> {

src/testutils/blockchain_tests.rs

Lines changed: 71 additions & 53 deletions
Large diffs are not rendered by default.

src/testutils/configurable_blockchain_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ where
117117
// perform wallet sync
118118
wallet.sync(&blockchain, Default::default()).unwrap();
119119

120-
let wallet_balance = wallet.get_balance().unwrap();
120+
let wallet_balance = wallet.get_balance().unwrap().get_total();
121121

122122
let details = format!(
123123
"test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]",

src/types.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ pub struct TransactionDetails {
207207
/// Sent value (sats)
208208
/// Sum of owned inputs of this transaction.
209209
pub sent: u64,
210-
/// Fee value (sats) if available.
210+
/// Fee value (sats) if confirmed.
211211
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
212212
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
213213
/// funds while offline.
@@ -242,6 +242,65 @@ impl BlockTime {
242242
}
243243
}
244244

245+
/// Balance differentiated in various categories
246+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
247+
pub struct Balance {
248+
/// All coinbase outputs not yet matured
249+
pub immature: u64,
250+
/// Unconfirmed UTXOs generated by a wallet tx
251+
pub trusted_pending: u64,
252+
/// Unconfirmed UTXOs received from an external wallet
253+
pub untrusted_pending: u64,
254+
/// Confirmed and immediately spendable balance
255+
pub confirmed: u64,
256+
}
257+
258+
impl Balance {
259+
/// Get sum of trusted_pending and confirmed coins
260+
pub fn get_spendable(&self) -> u64 {
261+
self.confirmed + self.trusted_pending
262+
}
263+
264+
/// Get the whole balance visible to the wallet
265+
pub fn get_total(&self) -> u64 {
266+
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
267+
}
268+
}
269+
270+
impl std::fmt::Display for Balance {
271+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272+
write!(
273+
f,
274+
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
275+
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
276+
)
277+
}
278+
}
279+
280+
impl std::ops::Add for Balance {
281+
type Output = Self;
282+
283+
fn add(self, other: Self) -> Self {
284+
Self {
285+
immature: self.immature + other.immature,
286+
trusted_pending: self.trusted_pending + other.trusted_pending,
287+
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
288+
confirmed: self.confirmed + other.confirmed,
289+
}
290+
}
291+
}
292+
293+
impl std::iter::Sum for Balance {
294+
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
295+
iter.fold(
296+
Balance {
297+
..Default::default()
298+
},
299+
|a, b| a + b,
300+
)
301+
}
302+
}
303+
245304
#[cfg(test)]
246305
mod tests {
247306
use super::*;

src/wallet/mod.rs

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -446,15 +446,52 @@ where
446446
self.database.borrow().iter_txs(include_raw)
447447
}
448448

449-
/// Return the balance, meaning the sum of this wallet's unspent outputs' values
449+
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
450+
/// values.
450451
///
451452
/// Note that this methods only operate on the internal database, which first needs to be
452453
/// [`Wallet::sync`] manually.
453-
pub fn get_balance(&self) -> Result<u64, Error> {
454-
Ok(self
455-
.list_unspent()?
456-
.iter()
457-
.fold(0, |sum, i| sum + i.txout.value))
454+
pub fn get_balance(&self) -> Result<Balance, Error> {
455+
let mut immature = 0;
456+
let mut trusted_pending = 0;
457+
let mut untrusted_pending = 0;
458+
let mut available = 0;
459+
let utxos = self.list_unspent()?;
460+
let database = self.database.borrow();
461+
let last_sync_height = match database
462+
.get_sync_time()?
463+
.map(|sync_time| sync_time.block_time.height)
464+
{
465+
Some(height) => height,
466+
// None means database was never synced
467+
None => return Ok(Balance::default()),
468+
};
469+
for u in utxos {
470+
// Unwrap used since utxo set is created from database
471+
let tx = database
472+
.get_tx(&u.outpoint.txid, true)?
473+
.expect("Transaction not found in database");
474+
if let Some(tx_conf_time) = &tx.confirmation_time {
475+
if tx.transaction.expect("No transaction").is_coin_base()
476+
&& (last_sync_height - tx_conf_time.height) < COINBASE_MATURITY
477+
{
478+
immature += u.txout.value;
479+
} else {
480+
available += u.txout.value;
481+
}
482+
} else if u.keychain == KeychainKind::Internal {
483+
trusted_pending += u.txout.value;
484+
} else {
485+
untrusted_pending += u.txout.value;
486+
}
487+
}
488+
489+
Ok(Balance {
490+
immature,
491+
trusted_pending,
492+
untrusted_pending,
493+
confirmed: available,
494+
})
458495
}
459496

460497
/// Add an external signer
@@ -4729,23 +4766,30 @@ pub(crate) mod test {
47294766
Some(confirmation_time),
47304767
(@coinbase true)
47314768
);
4769+
let sync_time = SyncTime {
4770+
block_time: BlockTime {
4771+
height: confirmation_time,
4772+
timestamp: 0,
4773+
},
4774+
};
4775+
wallet
4776+
.database
4777+
.borrow_mut()
4778+
.set_sync_time(sync_time)
4779+
.unwrap();
47324780

47334781
let not_yet_mature_time = confirmation_time + COINBASE_MATURITY - 1;
47344782
let maturity_time = confirmation_time + COINBASE_MATURITY;
47354783

4736-
// The balance is nonzero, even if we can't spend anything
4737-
// FIXME: we should differentiate the balance between immature,
4738-
// trusted, untrusted_pending
4739-
// See https://github.com/bitcoindevkit/bdk/issues/238
47404784
let balance = wallet.get_balance().unwrap();
4741-
assert!(balance != 0);
4785+
assert!(balance.immature != 0 && balance.confirmed == 0);
47424786

47434787
// We try to create a transaction, only to notice that all
47444788
// our funds are unspendable
47454789
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
47464790
let mut builder = wallet.build_tx();
47474791
builder
4748-
.add_recipient(addr.script_pubkey(), balance / 2)
4792+
.add_recipient(addr.script_pubkey(), balance.immature / 2)
47494793
.set_current_height(confirmation_time);
47504794
assert!(matches!(
47514795
builder.finish().unwrap_err(),
@@ -4758,7 +4802,7 @@ pub(crate) mod test {
47584802
// Still unspendable...
47594803
let mut builder = wallet.build_tx();
47604804
builder
4761-
.add_recipient(addr.script_pubkey(), balance / 2)
4805+
.add_recipient(addr.script_pubkey(), balance.immature / 2)
47624806
.set_current_height(not_yet_mature_time);
47634807
assert!(matches!(
47644808
builder.finish().unwrap_err(),
@@ -4769,9 +4813,23 @@ pub(crate) mod test {
47694813
));
47704814

47714815
// ...Now the coinbase is mature :)
4816+
let sync_time = SyncTime {
4817+
block_time: BlockTime {
4818+
height: maturity_time,
4819+
timestamp: 0,
4820+
},
4821+
};
4822+
wallet
4823+
.database
4824+
.borrow_mut()
4825+
.set_sync_time(sync_time)
4826+
.unwrap();
4827+
4828+
let balance = wallet.get_balance().unwrap();
4829+
assert!(balance.immature == 0 && balance.confirmed != 0);
47724830
let mut builder = wallet.build_tx();
47734831
builder
4774-
.add_recipient(addr.script_pubkey(), balance / 2)
4832+
.add_recipient(addr.script_pubkey(), balance.confirmed / 2)
47754833
.set_current_height(maturity_time);
47764834
builder.finish().unwrap();
47774835
}

0 commit comments

Comments
 (0)