Skip to content
20 changes: 19 additions & 1 deletion liana-gui/src/app/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use liana::miniscript::bitcoin::{
address,
bip32::{ChildNumber, Fingerprint},
psbt::Psbt,
Address, Txid,
Address, Amount, Txid,
};
use lianad::config::Config as DaemonConfig;

Expand All @@ -16,6 +17,7 @@ use crate::{
wallet::Wallet,
},
daemon::model::*,
daemon::Daemon,
export::ImportExportMessage,
hw::HardwareWalletMessage,
services::fiat::{
Expand All @@ -28,12 +30,14 @@ use crate::{
pub enum Message {
Tick,
RedirectLianaConnectLogin,
RemoteBackendAliasUpdated(Result<(), String>),
UpdateDaemonCache(Result<DaemonCache, Error>),
CacheUpdated,
Fiat(FiatMessage),
UpdatePanelCache(/* is current panel */ bool),
View(view::Message),
LoadDaemonConfig(Box<DaemonConfig>),
DaemonConfigReloaded(Result<Arc<dyn Daemon + Sync + Send>, Error>),
DaemonConfigLoaded(Result<(), Error>),
LoadWallet(Wallet),
Info(Result<GetInfoResult, Error>),
Expand All @@ -49,6 +53,7 @@ pub enum Message {
Labels(Result<HashMap<String, String>, Error>),
SpendTxs(Result<Vec<SpendTx>, Error>),
Psbt(Result<(Psbt, Vec<String>), Error>),
RedraftSpend(u64, RedraftSpendResult),
RbfPsbt(Result<Txid, Error>),
Recovery(Result<SpendTx, Error>),
Signed(Fingerprint, Result<Psbt, Error>),
Expand All @@ -60,6 +65,8 @@ pub enum Message {
HardwareWallets(HardwareWalletMessage),
HistoryTransactionsExtension(Result<Vec<HistoryTransaction>, Error>),
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
PreselectedHistoryTransaction(Txid, Result<Option<HistoryTransaction>, Error>),
PreselectedSpendTransaction(Txid, Result<Option<SpendTx>, Error>),
Payments(Result<Vec<Payment>, Error>),
PaymentsExtension(Result<Vec<Payment>, Error>),
Payment(Result<(HistoryTransaction, usize), Error>),
Expand All @@ -69,6 +76,17 @@ pub enum Message {
Export(ImportExportMessage),
}

#[derive(Debug)]
pub struct RedraftSpendResult {
pub send_max_to_recipient: Option<usize>,
pub total_recipients: usize,
pub max_address: Address<address::NetworkUnchecked>,
pub is_user_coin_selection: bool,
pub is_recovery: bool,
pub result: Result<CreateSpendResult, Error>,
pub amount_left_to_select: Option<Amount>,
}

impl From<ImportExportMessage> for Message {
fn from(value: ImportExportMessage) -> Self {
Message::View(view::Message::ImportExport(value))
Expand Down
181 changes: 127 additions & 54 deletions liana-gui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use std::sync::Arc;
use std::time::Duration;

use iced::{clipboard, Subscription, Task};
use tokio::runtime::Handle;
use tracing::{error, info, warn};

pub use liana::miniscript::bitcoin;
Expand Down Expand Up @@ -247,31 +246,37 @@ impl<S: SettingsTrait> App<S> {

match &menu {
menu::Menu::TransactionPreSelected(txid) => {
if let Ok(Some(tx)) = Handle::current().block_on(async {
self.daemon
.get_history_txs(&[*txid])
.await
.map(|txs| txs.first().cloned())
}) {
self.panels.transactions.preselect(tx);
self.panels.current = menu;
return Task::none();
};
let txid = *txid;
let daemon = self.daemon.clone();
self.panels.current = menu;
return Task::perform(
async move {
daemon
.get_history_txs(&[txid])
.await
.map(|txs| txs.first().cloned())
.map_err(Error::from)
},
move |res| Message::PreselectedHistoryTransaction(txid, res),
);
}
menu::Menu::PsbtPreSelected(txid) => {
// Get preselected spend from DB in case it's not yet in the cache.
// We only need this single spend as we will go straight to its view and not show the PSBTs list.
// In case of any error loading the spend or if it doesn't exist, load PSBTs list in usual way.
if let Ok(Some(spend_tx)) = Handle::current().block_on(async {
self.daemon
.list_spend_transactions(Some(&[*txid]))
.await
.map(|txs| txs.first().cloned())
}) {
self.panels.psbts.preselect(spend_tx);
self.panels.current = menu;
return Task::none();
};
let txid = *txid;
let daemon = self.daemon.clone();
self.panels.current = menu;
return Task::perform(
async move {
daemon
.list_spend_transactions(Some(&[txid]))
.await
.map(|txs| txs.first().cloned())
.map_err(Error::from)
},
move |res| Message::PreselectedSpendTransaction(txid, res),
);
}
menu::Menu::SettingsPreSelected(setting) => {
self.panels.current = menu.clone();
Expand Down Expand Up @@ -324,14 +329,21 @@ impl<S: SettingsTrait> App<S> {
pub fn stop(&mut self) {
info!("Close requested");
if self.daemon.backend().is_embedded() {
if let Err(e) = Handle::current().block_on(async { self.daemon.stop().await }) {
error!("{}", e);
} else {
info!("Internal daemon stopped");
}
if let Some(bitcoind) = self.internal_bitcoind.take() {
bitcoind.stop();
}
let daemon = self.daemon.clone();
let bitcoind = self.internal_bitcoind.take();
tokio::spawn(async move {
if let Err(e) = daemon.stop().await {
error!("{}", e);
} else {
info!("Internal daemon stopped");
}

if let Some(bitcoind) = bitcoind {
if let Err(e) = tokio::task::spawn_blocking(move || bitcoind.stop()).await {
error!("Internal bitcoind shutdown task failed: {}", e);
}
}
});
}
}

Expand Down Expand Up @@ -479,6 +491,12 @@ impl<S: SettingsTrait> App<S> {

pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::RemoteBackendAliasUpdated(result) => {
if let Err(e) = result {
tracing::error!("Failed to update wallet settings with remote alias: {}", e);
}
Task::none()
}
Message::Fiat(FiatMessage::GetPriceResult(fiat_price)) => {
let relevant = self.wallet.fiat_price_is_relevant(&fiat_price);
tracing::trace!(
Expand Down Expand Up @@ -542,8 +560,27 @@ impl<S: SettingsTrait> App<S> {
Task::batch(commands)
}
Message::LoadDaemonConfig(cfg) => {
let res = self.load_daemon_config(self.cache.datadir_path.clone(), *cfg);
self.update(Message::DaemonConfigLoaded(res))
let daemon = self.daemon.clone();
let datadir_path = self.cache.datadir_path.clone();
let wallet_id = self.wallet.id();
Task::perform(
reload_daemon_config(datadir_path, wallet_id, daemon, *cfg),
Message::DaemonConfigReloaded,
)
}
Message::DaemonConfigReloaded(res) => {
let res = match res {
Ok(daemon) => {
self.daemon = daemon;
Ok(())
}
Err(e) => Err(e),
};
self.panels.current_mut().update(
self.daemon.clone(),
&self.cache,
Message::DaemonConfigLoaded(res),
)
}
Message::WalletUpdated(Ok(wallet)) => {
self.wallet = wallet.clone();
Expand All @@ -553,6 +590,36 @@ impl<S: SettingsTrait> App<S> {
Message::WalletUpdated(Ok(wallet)),
)
}
Message::PreselectedHistoryTransaction(txid, Ok(Some(tx)))
if self.panels.current == Menu::TransactionPreSelected(txid) =>
{
self.panels.transactions.preselect(tx);
Task::none()
}
Message::PreselectedHistoryTransaction(txid, _) => {
if self.panels.current == Menu::TransactionPreSelected(txid) {
return self
.panels
.current_mut()
.reload(self.daemon.clone(), self.wallet.clone());
}
Task::none()
}
Message::PreselectedSpendTransaction(txid, Ok(Some(spend_tx)))
if self.panels.current == Menu::PsbtPreSelected(txid) =>
{
self.panels.psbts.preselect(spend_tx);
Task::none()
}
Message::PreselectedSpendTransaction(txid, _) => {
if self.panels.current == Menu::PsbtPreSelected(txid) {
return self
.panels
.current_mut()
.reload(self.daemon.clone(), self.wallet.clone());
}
Task::none()
}
Message::View(view::Message::Menu(menu)) => self.set_current_panel(menu),
Message::View(view::Message::OpenUrl(url)) => {
if let Err(e) = open::that_detached(&url) {
Expand All @@ -568,24 +635,39 @@ impl<S: SettingsTrait> App<S> {
}
}

pub fn load_daemon_config(
&mut self,
datadir_path: LianaDirectory,
cfg: DaemonConfig,
) -> Result<(), Error> {
Handle::current().block_on(async { self.daemon.stop().await })?;
pub fn view(&self) -> Element<'_, Message> {
let content = self.panels.current().view(&self.cache).map(Message::View);
if self.cache.network != bitcoin::Network::Bitcoin {
Column::with_children(vec![network_banner(self.cache.network).into(), content]).into()
} else {
content
}
}

pub fn datadir_path(&self) -> &LianaDirectory {
&self.cache.datadir_path
}
}

async fn reload_daemon_config(
datadir_path: LianaDirectory,
wallet_id: WalletId,
old_daemon: Arc<dyn Daemon + Sync + Send>,
cfg: DaemonConfig,
) -> Result<Arc<dyn Daemon + Sync + Send>, Error> {
old_daemon.stop().await?;
tokio::task::spawn_blocking(move || {
let network = cfg.bitcoin_config.network;
let daemon = EmbeddedDaemon::start(cfg)?;
self.daemon = Arc::new(daemon);
let mut daemon_config_path = datadir_path
.network_directory(network)
.lianad_data_directory(&self.wallet.id())
.lianad_data_directory(&wallet_id)
.path()
.to_path_buf();
daemon_config_path.push("daemon.toml");

let content =
toml::to_string(&self.daemon.config()).map_err(|e| Error::Config(e.to_string()))?;
toml::to_string(&daemon.config()).map_err(|e| Error::Config(e.to_string()))?;

OpenOptions::new()
.write(true)
Expand All @@ -596,21 +678,12 @@ impl<S: SettingsTrait> App<S> {
.map_err(|e| {
warn!("failed to write to file: {:?}", e);
Error::Config(e.to_string())
})
}

pub fn view(&self) -> Element<'_, Message> {
let content = self.panels.current().view(&self.cache).map(Message::View);
if self.cache.network != bitcoin::Network::Bitcoin {
Column::with_children(vec![network_banner(self.cache.network).into(), content]).into()
} else {
content
}
}
})?;

pub fn datadir_path(&self) -> &LianaDirectory {
&self.cache.datadir_path
}
Ok(Arc::new(daemon) as Arc<dyn Daemon + Sync + Send>)
})
.await
.map_err(|e| Error::Unexpected(format!("Daemon config reload task failed: {e}")))?
}

fn new_recovery_panel(wallet: Arc<Wallet>, cache: &Cache) -> CreateSpendPanel {
Expand Down
Loading
Loading