Skip to content

Commit

Permalink
Change get_balance to return in categories.
Browse files Browse the repository at this point in the history
Add type balance with add, display traits. Change affected tests.
Update `CHANGELOG.md`
  • Loading branch information
wszdexdrf committed Jul 6, 2022
1 parent ec22fa2 commit 4ec63e4
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 71 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New MSRV set to `1.56.1`
- 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)
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
- Return balance in separate categories, namely `available`, `trusted_pending`, `untrusted_pending` & `immature`.

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

Expand Down
2 changes: 1 addition & 1 deletion src/blockchain/electrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,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]
Expand Down
2 changes: 1 addition & 1 deletion src/blockchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<Balance, Error> {
Ok(wallets
.iter()
.map(|w| -> Result<_, Error> {
Expand Down
124 changes: 71 additions & 53 deletions src/testutils/blockchain_tests.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/testutils/configurable_blockchain_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,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();

let details = format!(
"test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]",
Expand Down
61 changes: 60 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,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.
Expand Down Expand Up @@ -242,6 +242,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<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(
Balance {
..Default::default()
},
|a, b| a + b,
)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
86 changes: 72 additions & 14 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,15 +446,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<u64, Error> {
Ok(self
.list_unspent()?
.iter()
.fold(0, |sum, i| sum + i.txout.value))
pub fn get_balance(&self) -> Result<Balance, Error> {
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();
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 {
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,
confirmed: available,
})
}

/// Add an external signer
Expand Down Expand Up @@ -4729,23 +4766,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)
.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.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)
.set_current_height(confirmation_time);
assert!(matches!(
builder.finish().unwrap_err(),
Expand All @@ -4758,7 +4802,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(),
Expand All @@ -4769,9 +4813,23 @@ 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!(balance.immature == 0 && balance.confirmed != 0);
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance / 2)
.add_recipient(addr.script_pubkey(), balance.confirmed / 2)
.set_current_height(maturity_time);
builder.finish().unwrap();
}
Expand Down

0 comments on commit 4ec63e4

Please sign in to comment.