Merge #1498: [GUI] include unconfirmed coins from self in confirmed balance
715aeb9298d1bac41ab3c904201696ddf9a794be gui(lianalite): use is_from_self from response (Michael Mallan) 8d99981c143b424d660b04b829da17afdfb1608a gui: include unconfirmed from self in confirmed balance (Michael Mallan) 31224ba44369e8eb76f004fefb0ddb4206458f4d gui: set missing fields for new home page (Michael Mallan) 0a4861f409867744c768228889b1107ea2ae8a9a refactor(gui): use common function for coins summary (Michael Mallan) Pull request description: This is to complete #1375, building on the changes from #1483. The Liana Connect API response has been updated to include `is_from_self` so this will work for both local and remote backends. ACKs for top commit: edouardparis: utACK 715aeb9298d1bac41ab3c904201696ddf9a794be Tree-SHA512: 905494ee29f19dd527b2b1173075e6f2c332030b00ec05aca680921fc50411fee0d65977d2ee29b82fcfb07472df7717d8844a60cd629ffd8850d77c29b4f71e
This commit is contained in:
commit
7e2c7ed60a
@ -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()),
|
||||
|
||||
@ -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,
|
||||
@ -87,18 +136,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, expiring_coins, remaining_seq) = coins_summary(
|
||||
coins,
|
||||
tip_height as u32,
|
||||
wallet.main_descriptor.first_timelock_value(),
|
||||
);
|
||||
|
||||
Self {
|
||||
@ -106,8 +153,8 @@ impl Home {
|
||||
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(),
|
||||
@ -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 {
|
||||
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user