liana/gui/src/app/mod.rs
edouardparis 2995df870f fix psbts panel: keep list of psbts in background
fix this comment:
https://github.com/wizardsardine/liana/pull/959#issuecomment-1964811858

new_preselected is changed for a preselect method that keeps the
current state of the psbts list panel and open the modal with the
selected psbt.
2024-03-06 15:32:06 +01:00

301 lines
9.6 KiB
Rust

pub mod cache;
pub mod config;
pub mod menu;
pub mod message;
pub mod settings;
pub mod state;
pub mod view;
pub mod wallet;
mod error;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use iced::{clipboard, time, Command, Subscription};
use tracing::{info, warn};
pub use liana::{config::Config as DaemonConfig, miniscript::bitcoin};
use liana_ui::widget::Element;
pub use config::Config;
pub use message::Message;
use state::{
CoinsPanel, CreateSpendPanel, Home, PsbtsPanel, ReceivePanel, RecoveryPanel, State,
TransactionsPanel,
};
use crate::{
app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet},
bitcoind::Bitcoind,
daemon::{embedded::EmbeddedDaemon, Daemon},
};
use self::state::SettingsState;
struct Panels {
current: Menu,
home: Home,
coins: CoinsPanel,
transactions: TransactionsPanel,
psbts: PsbtsPanel,
recovery: RecoveryPanel,
receive: ReceivePanel,
create_spend: CreateSpendPanel,
settings: SettingsState,
}
impl Panels {
fn new(
cache: &Cache,
wallet: Arc<Wallet>,
data_dir: PathBuf,
internal_bitcoind: Option<&Bitcoind>,
) -> Panels {
Self {
current: Menu::Home,
home: Home::new(wallet.clone(), &cache.coins),
coins: CoinsPanel::new(&cache.coins, wallet.main_descriptor.first_timelock_value()),
transactions: TransactionsPanel::new(),
psbts: PsbtsPanel::new(wallet.clone()),
recovery: RecoveryPanel::new(wallet.clone(), &cache.coins, cache.blockheight),
receive: ReceivePanel::new(data_dir.clone(), wallet.clone()),
create_spend: CreateSpendPanel::new(
wallet.clone(),
&cache.coins,
cache.blockheight as u32,
cache.network,
),
settings: state::SettingsState::new(
data_dir,
wallet.clone(),
internal_bitcoind.is_some(),
),
}
}
fn current(&self) -> &dyn State {
match self.current {
Menu::Home => &self.home,
Menu::Receive => &self.receive,
Menu::PSBTs => &self.psbts,
Menu::Transactions => &self.transactions,
Menu::Settings => &self.settings,
Menu::Coins => &self.coins,
Menu::CreateSpendTx => &self.create_spend,
Menu::Recovery => &self.recovery,
Menu::RefreshCoins(_) => &self.create_spend,
Menu::PsbtPreSelected(_) => &self.psbts,
}
}
fn current_mut(&mut self) -> &mut dyn State {
match self.current {
Menu::Home => &mut self.home,
Menu::Receive => &mut self.receive,
Menu::PSBTs => &mut self.psbts,
Menu::Transactions => &mut self.transactions,
Menu::Settings => &mut self.settings,
Menu::Coins => &mut self.coins,
Menu::CreateSpendTx => &mut self.create_spend,
Menu::Recovery => &mut self.recovery,
Menu::RefreshCoins(_) => &mut self.create_spend,
Menu::PsbtPreSelected(_) => &mut self.psbts,
}
}
}
pub struct App {
data_dir: PathBuf,
cache: Cache,
config: Config,
wallet: Arc<Wallet>,
daemon: Arc<dyn Daemon + Sync + Send>,
internal_bitcoind: Option<Bitcoind>,
panels: Panels,
}
impl App {
pub fn new(
cache: Cache,
wallet: Arc<Wallet>,
config: Config,
daemon: Arc<dyn Daemon + Sync + Send>,
data_dir: PathBuf,
internal_bitcoind: Option<Bitcoind>,
) -> (App, Command<Message>) {
let mut panels = Panels::new(
&cache,
wallet.clone(),
data_dir.clone(),
internal_bitcoind.as_ref(),
);
let cmd = panels.home.reload(daemon.clone());
(
Self {
panels,
data_dir,
cache,
config,
daemon,
wallet,
internal_bitcoind,
},
cmd,
)
}
fn set_current_panel(&mut self, menu: Menu) -> Command<Message> {
match &menu {
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)) = self
.daemon
.list_spend_transactions(Some(&[*txid]))
.map(|txs| txs.first().cloned())
{
self.panels.psbts.preselect(spend_tx);
self.panels.current = menu;
return Command::none();
};
}
menu::Menu::RefreshCoins(preselected) => {
self.panels.create_spend = CreateSpendPanel::new_self_send(
self.wallet.clone(),
&self.cache.coins,
self.cache.blockheight as u32,
preselected,
self.cache.network,
);
}
menu::Menu::CreateSpendTx => {
// redo the process of spending only if user want to start a new one.
if !self.panels.create_spend.is_first_step() {
self.panels.create_spend = CreateSpendPanel::new(
self.wallet.clone(),
&self.cache.coins,
self.cache.blockheight as u32,
self.cache.network,
);
}
}
_ => {}
};
self.panels.current = menu;
self.panels.current_mut().reload(self.daemon.clone())
}
pub fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
time::every(Duration::from_secs(10)).map(|_| Message::Tick),
self.panels.current().subscription(),
])
}
pub fn stop(&mut self) {
info!("Close requested");
if !self.daemon.is_external() {
self.daemon.stop();
info!("Internal daemon stopped");
if let Some(bitcoind) = &self.internal_bitcoind {
bitcoind.stop();
}
}
}
pub fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Tick => {
let daemon = self.daemon.clone();
let datadir_path = self.cache.datadir_path.clone();
Command::perform(
async move {
let info = daemon.get_info()?;
// todo: filter coins to only have current coins.
let coins = daemon.list_coins()?;
Ok(Cache {
datadir_path,
coins: coins.coins,
network: info.network,
blockheight: info.block_height,
rescan_progress: info.rescan_progress,
})
},
Message::UpdateCache,
)
}
Message::UpdateCache(res) => {
match res {
Ok(cache) => self.cache = cache,
Err(e) => tracing::error!("Failed to update cache: {}", e),
}
Command::none()
}
Message::LoadDaemonConfig(cfg) => {
let path = self.config.daemon_config_path.clone().expect(
"Application config must have a daemon configuration file path at this point.",
);
let res = self.load_daemon_config(&path, *cfg);
self.update(Message::DaemonConfigLoaded(res))
}
Message::LoadWallet => {
let res = self.load_wallet();
self.update(Message::WalletLoaded(res))
}
Message::View(view::Message::Menu(menu)) => self.set_current_panel(menu),
Message::View(view::Message::Clipboard(text)) => clipboard::write(text),
_ => self
.panels
.current_mut()
.update(self.daemon.clone(), &self.cache, message),
}
}
pub fn load_daemon_config(
&mut self,
daemon_config_path: &PathBuf,
cfg: DaemonConfig,
) -> Result<(), Error> {
self.daemon.stop();
let daemon = EmbeddedDaemon::start(cfg)?;
self.daemon = Arc::new(daemon);
let content =
toml::to_string(&self.daemon.config()).map_err(|e| Error::Config(e.to_string()))?;
OpenOptions::new()
.write(true)
.truncate(true)
.open(daemon_config_path)
.map_err(|e| Error::Config(e.to_string()))?
.write_all(content.as_bytes())
.map_err(|e| {
warn!("failed to write to file: {:?}", e);
Error::Config(e.to_string())
})
}
pub fn load_wallet(&mut self) -> Result<Arc<Wallet>, Error> {
let wallet = Wallet::new(self.wallet.main_descriptor.clone()).load_settings(
&self.config,
&self.data_dir,
self.cache.network,
)?;
self.wallet = Arc::new(wallet);
Ok(self.wallet.clone())
}
pub fn view(&self) -> Element<Message> {
self.panels.current().view(&self.cache).map(Message::View)
}
}