Skip to content

Commit 03d3c78

Browse files
committed
Merge #640: Get balance in categories
0f03831 Change get_balance to return in categories. (wszdexdrf) Pull request description: ### Description This changes `get_balance()` function so that it returns balance separated in 4 categories: - available - trusted-pending - untrusted-pending - immature Fixes #238 ### Notes to the reviewers Based on #614 ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've updated tests for the new feature * [x] I've added docs for the new feature * [x] I've updated `CHANGELOG.md` ACKs for top commit: afilini: ACK 0f03831 Tree-SHA512: 39f02c22c61b6c73dd8e6d27b1775a72e64ab773ee67c0ad00e817e555c52cdf648f482ca8be5fcc2f3d62134c35b720b1e61b311cb6debb3ad651e79c829b93
2 parents dc7adb7 + 0f03831 commit 03d3c78

File tree

7 files changed

+226
-70
lines changed

7 files changed

+226
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Consolidate params `fee_amount` and `amount_needed` in `target_amount` in `CoinSelectionAlgorithm::coin_select` signature.
1515
- 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`.
1616
- New `RpcBlockchain` implementation with various fixes.
17+
- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`.
1718

1819
## [v0.20.0] - [v0.19.0]
1920

src/blockchain/electrum.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ mod test {
385385
.sync_wallet(&wallet, None, Default::default())
386386
.unwrap();
387387

388-
assert_eq!(wallet.get_balance().unwrap(), 50_000);
388+
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
389389
}
390390

391391
#[test]

src/blockchain/mod.rs

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

src/testutils/blockchain_tests.rs

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

src/testutils/configurable_blockchain_tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ where
124124
// perform wallet sync
125125
wallet.sync(&blockchain, Default::default()).unwrap();
126126

127-
let wallet_balance = wallet.get_balance().unwrap();
127+
let wallet_balance = wallet.get_balance().unwrap().get_total();
128128
println!(
129129
"max: {}, min: {}, actual: {}",
130130
max_balance, min_balance, wallet_balance
@@ -193,7 +193,7 @@ where
193193
wallet.sync(&blockchain, Default::default()).unwrap();
194194
println!("sync done!");
195195

196-
let balance = wallet.get_balance().unwrap();
196+
let balance = wallet.get_balance().unwrap().get_total();
197197
assert_eq!(balance, expected_balance);
198198
}
199199

@@ -245,13 +245,13 @@ where
245245

246246
// actually test the wallet
247247
wallet.sync(&blockchain, Default::default()).unwrap();
248-
let balance = wallet.get_balance().unwrap();
248+
let balance = wallet.get_balance().unwrap().get_total();
249249
assert_eq!(balance, expected_balance);
250250

251251
// now try with a fresh wallet
252252
let fresh_wallet =
253253
Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
254254
fresh_wallet.sync(&blockchain, Default::default()).unwrap();
255-
let fresh_balance = fresh_wallet.get_balance().unwrap();
255+
let fresh_balance = fresh_wallet.get_balance().unwrap().get_total();
256256
assert_eq!(fresh_balance, expected_balance);
257257
}

src/types.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ pub struct TransactionDetails {
227227
/// Sent value (sats)
228228
/// Sum of owned inputs of this transaction.
229229
pub sent: u64,
230-
/// Fee value (sats) if available.
230+
/// Fee value (sats) if confirmed.
231231
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
232232
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
233233
/// funds while offline.
@@ -262,6 +262,65 @@ impl BlockTime {
262262
}
263263
}
264264

265+
/// Balance differentiated in various categories
266+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
267+
pub struct Balance {
268+
/// All coinbase outputs not yet matured
269+
pub immature: u64,
270+
/// Unconfirmed UTXOs generated by a wallet tx
271+
pub trusted_pending: u64,
272+
/// Unconfirmed UTXOs received from an external wallet
273+
pub untrusted_pending: u64,
274+
/// Confirmed and immediately spendable balance
275+
pub confirmed: u64,
276+
}
277+
278+
impl Balance {
279+
/// Get sum of trusted_pending and confirmed coins
280+
pub fn get_spendable(&self) -> u64 {
281+
self.confirmed + self.trusted_pending
282+
}
283+
284+
/// Get the whole balance visible to the wallet
285+
pub fn get_total(&self) -> u64 {
286+
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
287+
}
288+
}
289+
290+
impl std::fmt::Display for Balance {
291+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292+
write!(
293+
f,
294+
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
295+
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
296+
)
297+
}
298+
}
299+
300+
impl std::ops::Add for Balance {
301+
type Output = Self;
302+
303+
fn add(self, other: Self) -> Self {
304+
Self {
305+
immature: self.immature + other.immature,
306+
trusted_pending: self.trusted_pending + other.trusted_pending,
307+
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
308+
confirmed: self.confirmed + other.confirmed,
309+
}
310+
}
311+
}
312+
313+
impl std::iter::Sum for Balance {
314+
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
315+
iter.fold(
316+
Balance {
317+
..Default::default()
318+
},
319+
|a, b| a + b,
320+
)
321+
}
322+
}
323+
265324
#[cfg(test)]
266325
mod tests {
267326
use super::*;

src/wallet/mod.rs

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -460,15 +460,52 @@ where
460460
self.database.borrow().iter_txs(include_raw)
461461
}
462462

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

474511
/// Add an external signer
@@ -5232,23 +5269,38 @@ pub(crate) mod test {
52325269
Some(confirmation_time),
52335270
(@coinbase true)
52345271
);
5272+
let sync_time = SyncTime {
5273+
block_time: BlockTime {
5274+
height: confirmation_time,
5275+
timestamp: 0,
5276+
},
5277+
};
5278+
wallet
5279+
.database
5280+
.borrow_mut()
5281+
.set_sync_time(sync_time)
5282+
.unwrap();
52355283

52365284
let not_yet_mature_time = confirmation_time + COINBASE_MATURITY - 1;
52375285
let maturity_time = confirmation_time + COINBASE_MATURITY;
52385286

5239-
// The balance is nonzero, even if we can't spend anything
5240-
// FIXME: we should differentiate the balance between immature,
5241-
// trusted, untrusted_pending
5242-
// See https://github.com/bitcoindevkit/bdk/issues/238
52435287
let balance = wallet.get_balance().unwrap();
5244-
assert!(balance != 0);
5288+
assert_eq!(
5289+
balance,
5290+
Balance {
5291+
immature: 25_000,
5292+
trusted_pending: 0,
5293+
untrusted_pending: 0,
5294+
confirmed: 0
5295+
}
5296+
);
52455297

52465298
// We try to create a transaction, only to notice that all
52475299
// our funds are unspendable
52485300
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
52495301
let mut builder = wallet.build_tx();
52505302
builder
5251-
.add_recipient(addr.script_pubkey(), balance / 2)
5303+
.add_recipient(addr.script_pubkey(), balance.immature / 2)
52525304
.current_height(confirmation_time);
52535305
assert!(matches!(
52545306
builder.finish().unwrap_err(),
@@ -5261,7 +5313,7 @@ pub(crate) mod test {
52615313
// Still unspendable...
52625314
let mut builder = wallet.build_tx();
52635315
builder
5264-
.add_recipient(addr.script_pubkey(), balance / 2)
5316+
.add_recipient(addr.script_pubkey(), balance.immature / 2)
52655317
.current_height(not_yet_mature_time);
52665318
assert!(matches!(
52675319
builder.finish().unwrap_err(),
@@ -5272,9 +5324,31 @@ pub(crate) mod test {
52725324
));
52735325

52745326
// ...Now the coinbase is mature :)
5327+
let sync_time = SyncTime {
5328+
block_time: BlockTime {
5329+
height: maturity_time,
5330+
timestamp: 0,
5331+
},
5332+
};
5333+
wallet
5334+
.database
5335+
.borrow_mut()
5336+
.set_sync_time(sync_time)
5337+
.unwrap();
5338+
5339+
let balance = wallet.get_balance().unwrap();
5340+
assert_eq!(
5341+
balance,
5342+
Balance {
5343+
immature: 0,
5344+
trusted_pending: 0,
5345+
untrusted_pending: 0,
5346+
confirmed: 25_000
5347+
}
5348+
);
52755349
let mut builder = wallet.build_tx();
52765350
builder
5277-
.add_recipient(addr.script_pubkey(), balance / 2)
5351+
.add_recipient(addr.script_pubkey(), balance.confirmed / 2)
52785352
.current_height(maturity_time);
52795353
builder.finish().unwrap();
52805354
}

0 commit comments

Comments
 (0)