refactor(gui): use common function for coins summary

This commit is contained in:
Michael Mallan 2024-12-11 12:06:17 +00:00
parent bbeca4d416
commit 0a4861f409
No known key found for this signature in database
GPG Key ID: 5177CDCEDB0EABEB
2 changed files with 225 additions and 40 deletions

View File

@ -75,6 +75,7 @@ impl Panels {
cache.last_poll_timestamp,
cache.last_poll_at_startup,
),
cache.blockheight,
),
coins: CoinsPanel::new(&cache.coins, wallet.main_descriptor.first_timelock_value()),
transactions: TransactionsPanel::new(wallet.clone()),

View File

@ -71,6 +71,46 @@ pub fn redirect(menu: Menu) -> Command<Message> {
})
}
/// Returns the confirmed and unconfirmed balances from `coins`, as well
/// as:
/// - the `OutPoint`s of those coins, if any, for which the current
/// `tip_height` is within 10% of the `timelock` expiring.
/// - the smallest number of blocks until the expiry of `timelock` among
/// all confirmed coins, if any.
fn coins_summary(
coins: &[Coin],
tip_height: u32,
timelock: u16,
) -> (Amount, Amount, Vec<OutPoint>, Option<u32>) {
let mut balance = Amount::from_sat(0);
let mut unconfirmed_balance = Amount::from_sat(0);
let mut expiring_coins = Vec::new();
let mut remaining_seq = None;
for coin in coins {
if coin.spend_info.is_none() {
if coin.block_height.is_some() {
balance += coin.amount;
let seq = remaining_sequence(coin, tip_height, timelock);
// Warn user for coins that are expiring in less than 10 percent of
// the timelock.
if seq <= timelock as u32 * 10 / 100 {
expiring_coins.push(coin.outpoint);
}
if let Some(last) = &mut remaining_seq {
if seq < *last {
*last = seq
}
} else {
remaining_seq = Some(seq);
}
} else {
unconfirmed_balance += coin.amount;
}
}
}
(balance, unconfirmed_balance, expiring_coins, remaining_seq)
}
pub struct Home {
wallet: Arc<Wallet>,
sync_status: SyncStatus,
@ -87,18 +127,16 @@ pub struct Home {
}
impl Home {
pub fn new(wallet: Arc<Wallet>, coins: &[Coin], sync_status: SyncStatus) -> Self {
let (balance, unconfirmed_balance) = coins.iter().fold(
(Amount::from_sat(0), Amount::from_sat(0)),
|(balance, unconfirmed_balance), coin| {
if coin.spend_info.is_some() {
(balance, unconfirmed_balance)
} else if coin.block_height.is_some() {
(balance + coin.amount, unconfirmed_balance)
} else {
(balance, unconfirmed_balance + coin.amount)
}
},
pub fn new(
wallet: Arc<Wallet>,
coins: &[Coin],
sync_status: SyncStatus,
tip_height: i32,
) -> Self {
let (balance, unconfirmed_balance, _, _) = coins_summary(
coins,
tip_height as u32,
wallet.main_descriptor.first_timelock_value(),
);
Self {
@ -158,34 +196,16 @@ impl State for Home {
Err(e) => self.warning = Some(e),
Ok(coins) => {
self.warning = None;
self.balance = Amount::from_sat(0);
self.unconfirmed_balance = Amount::from_sat(0);
self.remaining_sequence = None;
self.expiring_coins = Vec::new();
for coin in coins {
if coin.spend_info.is_none() {
if coin.block_height.is_some() {
self.balance += coin.amount;
let timelock = self.wallet.main_descriptor.first_timelock_value();
let seq =
remaining_sequence(&coin, cache.blockheight as u32, timelock);
// Warn user for coins that are expiring in less than 10 percent of
// the timelock.
if seq <= timelock as u32 * 10 / 100 {
self.expiring_coins.push(coin.outpoint);
}
if let Some(last) = &mut self.remaining_sequence {
if seq < *last {
*last = seq
}
} else {
self.remaining_sequence = Some(seq);
}
} else {
self.unconfirmed_balance += coin.amount;
}
}
}
(
self.balance,
self.unconfirmed_balance,
self.expiring_coins,
self.remaining_sequence,
) = coins_summary(
&coins,
cache.blockheight as u32,
self.wallet.main_descriptor.first_timelock_value(),
);
}
},
Message::HistoryTransactions(res) => match res {
@ -360,3 +380,167 @@ impl From<Home> for Box<dyn State> {
Box::new(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::daemon::model::Coin;
use liana::miniscript::bitcoin;
use lianad::commands::LCSpendInfo;
use std::str::FromStr;
#[tokio::test]
async fn test_coins_summary() {
// Will use the same address for all coins.
let dummy_address =
bitcoin::Address::from_str("bc1qvrl2849aggm6qry9ea7xqp2kk39j8vaa8r3cwg")
.unwrap()
.assume_checked();
// Will use the same txid for all outpoints and spend info.
let dummy_txid = bitcoin::Txid::from_str(
"f7bd1b2a995b689d326e51eb742eb1088c4a8f110d9cb56128fd553acc9f88e5",
)
.unwrap();
let tip_height = 800_000;
let timelock = 10_000;
let mut coins = Vec::new();
// Without coins, all values are 0 / empty / None:
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(Amount::from_sat(0), Amount::from_sat(0), Vec::new(), None)
);
// Add a spending coin.
coins.push(Coin {
outpoint: OutPoint::new(dummy_txid, 0),
amount: Amount::from_sat(100),
address: dummy_address.clone(),
derivation_index: bitcoin::bip32::ChildNumber::Normal { index: 0 },
block_height: Some(1),
is_immature: false,
is_change: false,
is_from_self: false,
spend_info: Some(LCSpendInfo {
txid: dummy_txid,
height: None,
}),
});
// Spending coin is ignored.
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(Amount::from_sat(0), Amount::from_sat(0), Vec::new(), None)
);
// Add unconfirmed change coin not from self.
coins.push(Coin {
outpoint: OutPoint::new(dummy_txid, 1),
amount: Amount::from_sat(109),
address: dummy_address.clone(),
derivation_index: bitcoin::bip32::ChildNumber::Normal { index: 1 },
block_height: None,
is_immature: false,
is_change: true,
is_from_self: false,
spend_info: None,
});
// Included in unconfirmed balance. Other values remain the same.
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(Amount::from_sat(0), Amount::from_sat(109), Vec::new(), None)
);
// Add unconfirmed coin from self.
coins.push(Coin {
outpoint: OutPoint::new(dummy_txid, 2),
amount: Amount::from_sat(111),
address: dummy_address.clone(),
derivation_index: bitcoin::bip32::ChildNumber::Normal { index: 2 },
block_height: None,
is_immature: false,
is_change: false,
is_from_self: true,
spend_info: None,
});
// Included in unconfirmed balance. Other values remain the same.
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(Amount::from_sat(0), Amount::from_sat(220), Vec::new(), None)
);
// Add a confirmed coin 1 more than 10% from expiry:
coins.push(Coin {
outpoint: OutPoint::new(dummy_txid, 3),
amount: Amount::from_sat(101),
address: dummy_address.clone(),
derivation_index: bitcoin::bip32::ChildNumber::Normal { index: 3 },
block_height: Some(791_001), // 791_001 + timelock - tip_height = 1_001 > 1_000 = (timelock / 10)
is_immature: false,
is_change: false,
is_from_self: false,
spend_info: None,
});
// Coin is added to confirmed balance. Not expiring, but remaining seq is set.
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(
Amount::from_sat(101),
Amount::from_sat(220),
Vec::new(),
Some(1_001)
)
);
// Now decrease the last coin's confirmation height by 1 so that
// it is within 10% of expiry:
coins.last_mut().unwrap().block_height = Some(791_000);
// Its outpoint has been added to expiring coins and remaining seq is lower.
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(
Amount::from_sat(101),
Amount::from_sat(220),
vec![OutPoint::new(dummy_txid, 3)],
Some(1_000)
)
);
// Now add a confirmed coin that is not yet expiring.
coins.push(Coin {
outpoint: OutPoint::new(dummy_txid, 4),
amount: Amount::from_sat(105),
address: dummy_address.clone(),
derivation_index: bitcoin::bip32::ChildNumber::Normal { index: 4 },
block_height: Some(792_000),
is_immature: false,
is_change: false,
is_from_self: false,
spend_info: None,
});
// Only confirmed balance has changed.
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(
Amount::from_sat(206),
Amount::from_sat(220),
vec![OutPoint::new(dummy_txid, 3)],
Some(1_000)
)
);
// Now add another confirmed coin that is expiring.
coins.push(Coin {
outpoint: OutPoint::new(dummy_txid, 5),
amount: Amount::from_sat(108),
address: dummy_address.clone(),
derivation_index: bitcoin::bip32::ChildNumber::Normal { index: 5 },
block_height: Some(790_500),
is_immature: false,
is_change: false,
is_from_self: false,
spend_info: None,
});
// Confirmed balance updated, as well as expiring coins and the remaining seq.
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(
Amount::from_sat(314),
Amount::from_sat(220),
vec![OutPoint::new(dummy_txid, 3), OutPoint::new(dummy_txid, 5)],
Some(500)
)
);
}
}