diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 097b077fa..a2aa1717b 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -18,7 +18,7 @@ use std::time::Duration; use iced::{clipboard, time, Command, Subscription}; use tracing::{error, info, warn}; -pub use liana::{config::Config as DaemonConfig, miniscript::bitcoin}; +pub use liana::{commands::CoinStatus, config::Config as DaemonConfig, miniscript::bitcoin}; use liana_ui::widget::Element; pub use config::Config; @@ -239,8 +239,8 @@ impl App { daemon.is_alive()?; let info = daemon.get_info()?; - // todo: filter coins to only have current coins. - let coins = daemon.list_coins()?; + let coins = daemon + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[])?; Ok(Cache { datadir_path, coins: coins.coins, diff --git a/gui/src/app/state/coins.rs b/gui/src/app/state/coins.rs index e5f2c1b8d..999e667e1 100644 --- a/gui/src/app/state/coins.rs +++ b/gui/src/app/state/coins.rs @@ -4,6 +4,7 @@ use std::{cmp::Ordering, collections::HashSet}; use iced::Command; +use liana::commands::CoinStatus; use liana_ui::widget::Element; use crate::{ @@ -162,7 +163,7 @@ impl State for CoinsPanel { Command::perform( async move { daemon1 - .list_coins() + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]) .map(|res| res.coins) .map_err(|e| e.into()) }, @@ -171,7 +172,7 @@ impl State for CoinsPanel { Command::perform( async move { let coins = daemon2 - .list_coins() + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]) .map(|res| res.coins) .map_err(Error::from)?; let mut targets = HashSet::::new(); diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index 2daba35e2..2a32f9d9f 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -13,7 +13,10 @@ use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use iced::{Command, Subscription}; -use liana::miniscript::bitcoin::{Amount, OutPoint}; +use liana::{ + commands::CoinStatus, + miniscript::bitcoin::{Amount, OutPoint}, +}; use liana_ui::widget::*; use super::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet}; @@ -295,7 +298,7 @@ impl State for Home { Command::perform( async move { daemon2 - .list_coins() + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]) .map(|res| res.coins) .map_err(|e| e.into()) }, diff --git a/gui/src/app/state/psbt.rs b/gui/src/app/state/psbt.rs index bfbf0b894..4bc53d48b 100644 --- a/gui/src/app/state/psbt.rs +++ b/gui/src/app/state/psbt.rs @@ -7,6 +7,7 @@ use iced::Subscription; use iced::Command; use liana::{ + commands::CoinStatus, descriptors::LianaPolicy, miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network, Txid}, }; @@ -167,23 +168,15 @@ impl PsbtState { return cmd; } Message::View(view::Message::Spend(view::SpendTxMessage::Broadcast)) => { - let outpoints: HashSet<_> = self.tx.coins.keys().cloned().collect(); + let outpoints: Vec<_> = self.tx.coins.keys().cloned().collect(); return Command::perform( async move { daemon - // TODO: filter for the outpoints in `tx.coins` when this is possible: - // https://github.com/wizardsardine/liana/issues/677 - .list_coins() + .list_coins(&[CoinStatus::Spending], &outpoints) .map(|res| { res.coins .iter() - .filter_map(|c| { - if outpoints.contains(&c.outpoint) { - c.spend_info.map(|info| info.txid) - } else { - None - } - }) + .filter_map(|c| c.spend_info.map(|info| info.txid)) .collect() }) .map_err(|e| e.into()) diff --git a/gui/src/app/state/recovery.rs b/gui/src/app/state/recovery.rs index bd8325cc6..392ac180e 100644 --- a/gui/src/app/state/recovery.rs +++ b/gui/src/app/state/recovery.rs @@ -4,9 +4,12 @@ use std::sync::Arc; use iced::Command; -use liana::miniscript::bitcoin::{ - bip32::{DerivationPath, Fingerprint}, - secp256k1, +use liana::{ + commands::CoinStatus, + miniscript::bitcoin::{ + bip32::{DerivationPath, Fingerprint}, + secp256k1, + }, }; use liana_ui::{component::form, widget::Element}; @@ -155,16 +158,13 @@ impl State for RecoveryPanel { return Command::perform( async move { let psbt = daemon.create_recovery(address, feerate_vb, sequence)?; - let coins = daemon.list_coins().map(|res| res.coins)?; - let coins = coins - .into_iter() - .filter(|coin| { - psbt.unsigned_tx - .input - .iter() - .any(|input| input.previous_output == coin.outpoint) - }) + let outpoints: Vec<_> = psbt + .unsigned_tx + .input + .iter() + .map(|txin| txin.previous_output) .collect(); + let coins = daemon.list_coins(&[], &outpoints).map(|res| res.coins)?; Ok(SpendTx::new( None, psbt, @@ -207,7 +207,7 @@ impl State for RecoveryPanel { Command::perform( async move { daemon - .list_coins() + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]) .map(|res| res.coins) .map_err(|e| e.into()) }, diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index 953d684bb..60e8d933f 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -5,7 +5,10 @@ use std::sync::Arc; use iced::Command; -use liana::miniscript::bitcoin::{Network, OutPoint}; +use liana::{ + commands::CoinStatus, + miniscript::bitcoin::{Network, OutPoint}, +}; use liana_ui::widget::Element; use super::{redirect, State}; @@ -123,7 +126,7 @@ impl State for CreateSpendPanel { Command::perform( async move { daemon1 - .list_coins() + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]) .map(|res| res.coins) .map_err(|e| e.into()) }, @@ -132,7 +135,7 @@ impl State for CreateSpendPanel { Command::perform( async move { let coins = daemon - .list_coins() + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]) .map(|res| res.coins) .map_err(Error::from)?; let mut targets = HashSet::::new(); diff --git a/gui/src/app/state/transactions.rs b/gui/src/app/state/transactions.rs index a5198a1da..d88d53382 100644 --- a/gui/src/app/state/transactions.rs +++ b/gui/src/app/state/transactions.rs @@ -7,6 +7,7 @@ use std::{ use iced::Command; use liana::{ + commands::CoinStatus, miniscript::bitcoin::{OutPoint, Txid}, spend::{SpendCreationError, MAX_FEERATE}, }; @@ -146,23 +147,23 @@ impl State for TransactionsPanel { if let Some(tx) = &self.selected_tx { if tx.fee_amount.is_some() { let tx = tx.clone(); - let txid = tx.tx.txid(); + let outpoints: Vec<_> = (0..tx.tx.output.len()) + .map(|vout| { + OutPoint::new( + tx.tx.txid(), + vout.try_into() + .expect("number of transaction outputs must fit in u32"), + ) + }) + .collect(); return Command::perform( async move { daemon - // TODO: filter for spending coins when this is possible: - // https://github.com/wizardsardine/liana/issues/677 - .list_coins() + .list_coins(&[CoinStatus::Spending], &outpoints) .map(|res| { res.coins .iter() - .filter_map(|c| { - if c.outpoint.txid == txid { - c.spend_info.map(|info| info.txid) - } else { - None - } - }) + .filter_map(|c| c.spend_info.map(|info| info.txid)) .collect() }) .map_err(|e| e.into()) @@ -248,7 +249,6 @@ impl State for TransactionsPanel { self.selected_tx = None; let daemon1 = daemon.clone(); let daemon2 = daemon.clone(); - let daemon3 = daemon.clone(); let now: u32 = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() @@ -257,7 +257,7 @@ impl State for TransactionsPanel { .unwrap(); Command::batch(vec![ Command::perform( - async move { daemon3.list_pending_txs().map_err(|e| e.into()) }, + async move { daemon2.list_pending_txs().map_err(|e| e.into()) }, Message::PendingTransactions, ), Command::perform( @@ -268,15 +268,6 @@ impl State for TransactionsPanel { }, Message::HistoryTransactions, ), - Command::perform( - async move { - daemon2 - .list_coins() - .map(|res| res.coins) - .map_err(|e| e.into()) - }, - Message::Coins, - ), ]) } } diff --git a/gui/src/daemon/client/mod.rs b/gui/src/daemon/client/mod.rs index eed290714..7f354a286 100644 --- a/gui/src/daemon/client/mod.rs +++ b/gui/src/daemon/client/mod.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::iter::FromIterator; -use liana::commands::CreateRecoveryResult; +use liana::commands::{CoinStatus, CreateRecoveryResult}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -77,8 +77,18 @@ impl Daemon for Lianad { self.call("getnewaddress", Option::::None) } - fn list_coins(&self) -> Result { - self.call("listcoins", Option::::None) + fn list_coins( + &self, + statuses: &[CoinStatus], + outpoints: &[OutPoint], + ) -> Result { + self.call( + "listcoins", + Some(vec![ + json!(statuses.iter().map(|s| s.to_arg()).collect::>()), + json!(outpoints), + ]), + ) } fn list_spend_txs(&self) -> Result { diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs index 0ccf16b10..ec4942f7e 100644 --- a/gui/src/daemon/embedded.rs +++ b/gui/src/daemon/embedded.rs @@ -3,7 +3,7 @@ use std::sync::Mutex; use super::{model::*, Daemon, DaemonError}; use liana::{ - commands::LabelItem, + commands::{CoinStatus, LabelItem}, config::Config, miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid}, DaemonControl, DaemonHandle, @@ -87,8 +87,12 @@ impl Daemon for EmbeddedDaemon { self.command(|daemon| Ok(daemon.get_new_address())) } - fn list_coins(&self) -> Result { - self.command(|daemon| Ok(daemon.list_coins(&[], &[]))) + fn list_coins( + &self, + statuses: &[CoinStatus], + outpoints: &[OutPoint], + ) -> Result { + self.command(|daemon| Ok(daemon.list_coins(statuses, outpoints))) } fn list_spend_txs(&self) -> Result { diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs index 5edbc9b09..477e68b3d 100644 --- a/gui/src/daemon/mod.rs +++ b/gui/src/daemon/mod.rs @@ -3,12 +3,13 @@ pub mod embedded; pub mod model; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::fmt::Debug; use std::io::ErrorKind; use std::iter::FromIterator; use liana::{ - commands::LabelItem, + commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, miniscript::bitcoin::{address, psbt::Psbt, secp256k1, Address, OutPoint, Txid}, StartupError, @@ -56,7 +57,11 @@ pub trait Daemon: Debug { fn stop(&self) -> Result<(), DaemonError>; fn get_info(&self) -> Result; fn get_new_address(&self) -> Result; - fn list_coins(&self) -> Result; + fn list_coins( + &self, + statuses: &[CoinStatus], + outpoints: &[OutPoint], + ) -> Result; fn list_spend_txs(&self) -> Result; fn create_spend_tx( &self, @@ -102,15 +107,26 @@ pub trait Daemon: Debug { txids: Option<&[Txid]>, ) -> Result, DaemonError> { let info = self.get_info()?; - let coins = self.list_coins()?.coins; let mut spend_txs = Vec::new(); let curve = secp256k1::Secp256k1::verification_only(); - for tx in self.list_spend_txs()?.spend_txs { - if let Some(txids) = txids { - if !txids.contains(&tx.psbt.unsigned_tx.txid()) { - continue; - } - } + // TODO: Use filters in `list_spend_txs` command. + let mut txs = self.list_spend_txs()?.spend_txs; + if let Some(txids) = txids { + txs.retain(|tx| txids.contains(&tx.psbt.unsigned_tx.txid())); + } + let outpoints: Vec<_> = txs + .iter() + .flat_map(|tx| { + tx.psbt + .unsigned_tx + .input + .iter() + .map(|txin| txin.previous_output) + .collect::>() + }) + .collect(); + let coins = self.list_coins(&[], &outpoints)?.coins; + for tx in txs { let coins = coins .iter() .filter(|coin| { @@ -145,15 +161,30 @@ pub trait Daemon: Debug { Ok(spend_txs) } - fn list_history_txs( + fn txs_to_historytxs( &self, - start: u32, - end: u32, - limit: u64, + txs: Vec, ) -> Result, DaemonError> { let info = self.get_info()?; - let coins = self.list_coins()?.coins; - let txs = self.list_confirmed_txs(start, end, limit)?.transactions; + let outpoints: Vec<_> = txs + .iter() + .flat_map(|tx| { + (0..tx.tx.output.len()) + .map(|vout| { + OutPoint::new( + tx.tx.txid(), + vout.try_into() + .expect("number of transaction outputs must fit in u32"), + ) + }) + .chain(tx.tx.input.iter().map(|txin| txin.previous_output)) + .collect::>() + }) + .collect::>() // remove duplicates + .iter() + .cloned() + .collect(); + let coins = self.list_coins(&[], &outpoints)?.coins; let mut txs = txs .into_iter() .map(|tx| { @@ -185,47 +216,31 @@ pub trait Daemon: Debug { Ok(txs) } + fn list_history_txs( + &self, + start: u32, + end: u32, + limit: u64, + ) -> Result, DaemonError> { + let txs = self.list_confirmed_txs(start, end, limit)?.transactions; + self.txs_to_historytxs(txs) + } + fn get_history_txs( &self, txids: &[Txid], ) -> Result, DaemonError> { - let info = self.get_info()?; - let coins = self.list_coins()?.coins; let txs = self.list_txs(txids)?.transactions; - let mut txs = txs - .into_iter() - .map(|tx| { - let mut tx_coins = Vec::new(); - let mut change_indexes = Vec::new(); - for coin in &coins { - if coin.outpoint.txid == tx.tx.txid() { - change_indexes.push(coin.outpoint.vout as usize) - } else if tx - .tx - .input - .iter() - .any(|input| input.previous_output == coin.outpoint) - { - tx_coins.push(coin.clone()); - } - } - model::HistoryTransaction::new( - tx.tx, - tx.height, - tx.time, - tx_coins, - change_indexes, - info.network, - ) - }) - .collect(); - load_labels(self, &mut txs)?; - Ok(txs) + self.txs_to_historytxs(txs) } fn list_pending_txs(&self) -> Result, DaemonError> { let info = self.get_info()?; - let coins = self.list_coins()?.coins; + // We want coins that are inputs to and/or outputs of a pending tx, + // which can only be unconfirmed and spending coins. + let coins = self + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Spending], &[])? + .coins; let mut txids: Vec = Vec::new(); for coin in &coins { if coin.block_height.is_none() && !txids.contains(&coin.outpoint.txid) { @@ -233,7 +248,7 @@ pub trait Daemon: Debug { } if let Some(spend) = coin.spend_info { - if spend.height.is_none() && !txids.contains(&spend.txid) { + if !txids.contains(&spend.txid) { txids.push(spend.txid); } } diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 0716485f8..84cd19eec 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -9,6 +9,7 @@ use iced::{Alignment, Command, Length, Subscription}; use tracing::{debug, info, warn}; use liana::{ + commands::CoinStatus, config::{Config, ConfigError}, miniscript::bitcoin, StartupError, @@ -370,7 +371,9 @@ pub async fn load_application( let wallet = Wallet::new(info.descriptors.main).load_settings(&gui_config, &datadir_path, network)?; - let coins = daemon.list_coins().map(|res| res.coins)?; + let coins = daemon + .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]) + .map(|res| res.coins)?; let cache = Cache { datadir_path,