Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GUI] include unconfirmed coins from self in confirmed balance #1498

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions liana-gui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
282 changes: 240 additions & 42 deletions liana-gui/src/app/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,55 @@ 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.
///
/// The confirmed balance includes the values of any unconfirmed coins
/// from self.
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() {
// Include unconfirmed coins from self in confirmed balance.
if coin.block_height.is_some() || coin.is_from_self {
balance += coin.amount;
// Only consider confirmed coins for remaining seq
// (they would not be considered as expiring so we can also skip that part)
if coin.block_height.is_none() {
continue;
}
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,
Expand All @@ -87,27 +136,25 @@ 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, expiring_coins, remaining_seq) = coins_summary(
coins,
tip_height as u32,
wallet.main_descriptor.first_timelock_value(),
);

Self {
wallet,
sync_status,
balance,
unconfirmed_balance,
remaining_sequence: None,
expiring_coins: Vec::new(),
remaining_sequence: remaining_seq,
expiring_coins,
selected_event: None,
events: Vec::new(),
labels_edited: LabelsEdited::default(),
Expand Down Expand Up @@ -158,34 +205,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 {
Expand Down Expand Up @@ -360,3 +389,172 @@ 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 confirmed balance. Other values remain the same.
assert_eq!(
coins_summary(&coins, tip_height, timelock),
(
Amount::from_sat(111),
Amount::from_sat(109),
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(212),
Amount::from_sat(109),
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(212),
Amount::from_sat(109),
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(317),
Amount::from_sat(109),
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(425),
Amount::from_sat(109),
vec![OutPoint::new(dummy_txid, 3), OutPoint::new(dummy_txid, 5)],
Some(500)
)
);
}
}
1 change: 1 addition & 0 deletions liana-gui/src/lianalite/client/backend/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ pub struct Coin {
pub spend_info: Option<CoinSpendInfo>,
pub is_immature: bool,
pub is_change_address: bool,
pub is_from_self: bool,
}

#[derive(Clone, Deserialize)]
Expand Down
6 changes: 3 additions & 3 deletions liana-gui/src/lianalite/client/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ impl Daemon for BackendWalletClient {
txid: info.txid,
height: info.height,
}),
is_from_self: false, // FIXME: use value from backend
is_from_self: c.is_from_self,
})
.collect(),
})
Expand Down Expand Up @@ -1134,7 +1134,7 @@ fn history_tx_from_api(value: api::Transaction, network: Network) -> HistoryTran
txid: info.txid,
height: info.height,
}),
is_from_self: false, // FIXME: use value from backend
is_from_self: c.is_from_self,
});
}
}
Expand Down Expand Up @@ -1190,7 +1190,7 @@ fn spend_tx_from_api(
txid: info.txid,
height: info.height,
}),
is_from_self: false, // FIXME: use value from backend
is_from_self: c.is_from_self,
});
}
}
Expand Down
Loading