gui: integrate import PSBT from file

This commit is contained in:
pythcoiner 2025-04-08 15:13:38 +02:00
parent 9658cfde3f
commit 71684e7039
No known key found for this signature in database
GPG Key ID: C1048AEEDF303B88
7 changed files with 77 additions and 215 deletions

View File

@ -155,12 +155,6 @@ impl ExportModal {
self.error = Some(e.clone());
}
Progress::None => {}
Progress::Psbt(_) => {
if matches!(self.import_export_type, ImportExportType::ImportPsbt(_)) {
self.state = ImportExportState::Ended;
}
// TODO: forward PSBT
}
Progress::Xpub(xpub_str) => {
if matches!(self.import_export_type, ImportExportType::ExportXpub(_)) {
self.state = ImportExportState::Ended;
@ -181,6 +175,7 @@ impl ExportModal {
});
}
Progress::WalletFromBackup(_) => {}
Progress::Psbt(_) => {}
},
ImportExportMessage::TimedOut => {
self.stop(ImportExportState::TimedOut);

View File

@ -1,6 +1,5 @@
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use iced::Subscription;
@ -13,13 +12,10 @@ use liana::{
use lianad::commands::CoinStatus;
use liana_ui::component::toast;
use liana_ui::{
component::{form, modal},
widget::Element,
};
use liana_ui::{component::modal, widget::Element};
use crate::daemon::model::LabelsLoader;
use crate::export::{ImportExportMessage, ImportExportType};
use crate::export::{ImportExportMessage, ImportExportType, Progress};
use crate::{
app::{
cache::Cache,
@ -59,7 +55,6 @@ pub trait Modal {
pub enum PsbtModal {
Save(SaveModal),
Sign(SignModal),
Update(UpdateModal),
Broadcast(BroadcastModal),
Delete(DeleteModal),
Export(ExportModal),
@ -70,7 +65,6 @@ impl<'a> AsRef<dyn Modal + 'a> for PsbtModal {
match &self {
Self::Save(a) => a,
Self::Sign(a) => a,
Self::Update(a) => a,
Self::Broadcast(a) => a,
Self::Delete(a) => a,
Self::Export(a) => a,
@ -83,7 +77,6 @@ impl<'a> AsMut<dyn Modal + 'a> for PsbtModal {
match self {
Self::Save(a) => a,
Self::Sign(a) => a,
Self::Update(a) => a,
Self::Broadcast(a) => a,
Self::Delete(a) => a,
Self::Export(a) => a,
@ -150,6 +143,17 @@ impl PsbtState {
return launch;
}
}
Message::View(view::Message::ImportPsbt) => {
if self.modal.is_none() {
let modal = ExportModal::new(
Some(daemon.clone()),
ImportExportType::ImportPsbt(Some(self.tx.psbt.unsigned_tx.compute_txid())),
);
let launch = modal.launch(false);
self.modal = Some(PsbtModal::Export(modal));
return launch;
}
}
Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => {
if matches!(self.modal, Some(PsbtModal::Export(_))) {
self.modal = None;
@ -188,12 +192,6 @@ impl PsbtState {
self.modal = Some(PsbtModal::Sign(modal));
return cmd;
}
Message::View(view::Message::Spend(view::SpendTxMessage::EditPsbt)) => {
let modal = UpdateModal::new(self.wallet.clone(), self.tx.psbt.to_string());
let cmd = modal.load(daemon);
self.modal = Some(PsbtModal::Update(modal));
return cmd;
}
Message::View(view::Message::Spend(view::SpendTxMessage::Broadcast)) => {
let outpoints: Vec<_> = self.tx.coins.keys().cloned().collect();
return Task::perform(
@ -246,6 +244,16 @@ impl PsbtState {
self.warning = Some(e);
}
},
Message::Export(ImportExportMessage::Progress(Progress::Psbt(psbt))) => {
merge_signatures(&mut self.tx.psbt, &psbt);
self.tx.sigs = self
.wallet
.main_descriptor
.partial_spend_info(&self.tx.psbt)
// FIXME: we should not unwrap here, it seems importing a PSBT w/ an
// non-owned input will trigger a panic
.unwrap();
}
_ => {
if let Some(modal) = self.modal.as_mut() {
return modal.as_mut().update(daemon.clone(), message, &mut self.tx);
@ -658,97 +666,6 @@ async fn sign_psbt(
Ok(psbt)
}
pub struct UpdateModal {
wallet: Arc<Wallet>,
psbt: String,
updated: form::Value<String>,
processing: bool,
error: Option<Error>,
success: bool,
}
impl UpdateModal {
pub fn new(wallet: Arc<Wallet>, psbt: String) -> Self {
Self {
wallet,
psbt,
updated: form::Value::default(),
processing: false,
error: None,
success: false,
}
}
}
impl Modal for UpdateModal {
fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message> {
modal::Modal::new(
content,
if self.success {
view::psbt::update_spend_success_view()
} else {
view::psbt::update_spend_view(
self.psbt.clone(),
&self.updated,
self.error.as_ref(),
self.processing,
)
},
)
.on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel)))
.into()
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
message: Message,
tx: &mut SpendTx,
) -> Task<Message> {
match message {
Message::Updated(res) => {
self.processing = false;
match res {
Ok(()) => {
self.success = true;
self.error = None;
let psbt = Psbt::from_str(&self.updated.value).expect("Already checked");
merge_signatures(&mut tx.psbt, &psbt);
tx.sigs = self
.wallet
.main_descriptor
.partial_spend_info(&tx.psbt)
.unwrap();
}
Err(e) => self.error = e.into(),
}
}
Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => {
self.updated.value = s;
if let Ok(psbt) = Psbt::from_str(&self.updated.value) {
self.updated.valid =
tx.psbt.unsigned_tx.compute_txid() == psbt.unsigned_tx.compute_txid();
} else {
self.updated.valid = false;
}
}
Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => {
self.processing = true;
self.error = None;
if let Ok(updated) = Psbt::from_str(&self.updated.value) {
return Task::perform(
async move { daemon.update_spend_tx(&updated).await.map_err(|e| e.into()) },
Message::Updated,
);
}
}
_ => {}
}
Task::none()
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,18 +1,14 @@
use std::str::FromStr;
use std::sync::Arc;
use iced::{Subscription, Task};
use liana::miniscript::bitcoin::psbt::Psbt;
use liana_ui::{
component::{form, modal},
widget::Element,
};
use liana_ui::widget::Element;
use super::{psbt, State};
use super::{export::ExportModal, psbt, State};
use crate::{
app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet},
daemon::{model::SpendTx, Daemon},
export::{ImportExportMessage, ImportExportType},
};
pub struct PsbtsPanel {
@ -20,7 +16,7 @@ pub struct PsbtsPanel {
selected_tx: Option<psbt::PsbtState>,
spend_txs: Vec<SpendTx>,
warning: Option<Error>,
import_tx: Option<ImportPsbtModal>,
modal: Option<ExportModal>,
}
impl PsbtsPanel {
@ -30,7 +26,7 @@ impl PsbtsPanel {
spend_txs: Vec::new(),
warning: None,
selected_tx: None,
import_tx: None,
modal: None,
}
}
@ -38,7 +34,7 @@ impl PsbtsPanel {
let psbt_state = psbt::PsbtState::new(self.wallet.clone(), spend_tx, true);
self.selected_tx = Some(psbt_state);
self.warning = None;
self.import_tx = None;
self.modal = None;
}
}
@ -53,14 +49,8 @@ impl State for PsbtsPanel {
self.warning.as_ref(),
view::psbts::psbts_view(&self.spend_txs),
);
if let Some(import_tx) = &self.import_tx {
modal::Modal::new(list_view, import_tx.view())
.on_blur(if import_tx.processing {
None
} else {
Some(view::Message::Close)
})
.into()
if let Some(modal) = &self.modal {
modal.view(list_view)
} else {
list_view
}
@ -99,9 +89,32 @@ impl State for PsbtsPanel {
}
}
},
Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Import)) => {
if self.import_tx.is_none() {
self.import_tx = Some(ImportPsbtModal::new());
Message::View(view::Message::ImportPsbt) => {
if let Some(tx) = &mut self.selected_tx {
return tx.update(daemon, cache, message);
} else if self.modal.is_none() {
let modal =
ExportModal::new(Some(daemon.clone()), ImportExportType::ImportPsbt(None));
let launch = modal.launch(false);
self.modal = Some(modal);
return launch;
}
}
Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => {
if let Some(tx) = &mut self.selected_tx {
return tx.update(daemon, cache, message);
} else if self.modal.is_some() {
self.modal = None;
return Task::perform(async {}, |_| Message::View(view::Message::Reload));
}
}
Message::View(view::Message::ImportExport(m)) => {
let m = m.clone();
if let Some(tx) = &mut self.selected_tx {
let message = Message::View(view::Message::ImportExport(m));
return tx.update(daemon, cache, message);
} else if let Some(modal) = self.modal.as_mut() {
return modal.update(m.clone());
}
}
Message::View(view::Message::Select(i)) => {
@ -116,10 +129,6 @@ impl State for PsbtsPanel {
if let Some(tx) = &mut self.selected_tx {
return tx.update(daemon, cache, message);
}
if let Some(import_tx) = &mut self.import_tx {
return import_tx.update(daemon, cache, message);
}
}
}
Task::none()
@ -128,6 +137,17 @@ impl State for PsbtsPanel {
fn subscription(&self) -> Subscription<Message> {
if let Some(psbt) = &self.selected_tx {
psbt.subscription()
} else if let Some(modal) = &self.modal {
modal
.subscription()
.map(|s| {
s.map(|m| {
Message::View(view::Message::ImportExport(ImportExportMessage::Progress(
m,
)))
})
})
.unwrap_or(Subscription::none())
} else {
Subscription::none()
}
@ -140,7 +160,7 @@ impl State for PsbtsPanel {
) -> Task<Message> {
self.wallet = wallet;
self.selected_tx = None;
self.import_tx = None;
self.modal = None;
let daemon = daemon.clone();
Task::perform(
async move {
@ -159,74 +179,3 @@ impl From<PsbtsPanel> for Box<dyn State> {
Box::new(s)
}
}
pub struct ImportPsbtModal {
imported: form::Value<String>,
processing: bool,
error: Option<Error>,
success: bool,
}
impl ImportPsbtModal {
pub fn new() -> Self {
Self {
imported: form::Value::default(),
processing: false,
error: None,
success: false,
}
}
}
impl ImportPsbtModal {
fn view<'a>(&self) -> Element<'a, view::Message> {
if self.success {
view::psbts::import_psbt_success_view()
} else {
view::psbts::import_psbt_view(&self.imported, self.error.as_ref(), self.processing)
}
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: Message,
) -> Task<Message> {
match message {
Message::Updated(res) => {
self.processing = false;
match res {
Ok(()) => {
self.success = true;
self.error = None;
}
Err(e) => self.error = e.into(),
}
}
Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => {
self.imported.value = s;
self.imported.valid = Psbt::from_str(&self.imported.value).ok().is_some();
}
Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => {
if self.imported.valid {
self.processing = true;
self.error = None;
let imported = Psbt::from_str(&self.imported.value).expect("Already checked");
return Task::perform(
async move {
daemon
.update_spend_tx(&imported)
.await
.map_err(|e| e.into())
},
Message::Updated,
);
}
}
_ => {}
}
Task::none()
}
}

View File

@ -26,6 +26,7 @@ pub enum Message {
ImportExport(ImportExportMessage),
HideRescanWarning,
ExportPsbt,
ImportPsbt,
}
impl Close for Message {

View File

@ -346,10 +346,10 @@ pub fn spend_overview_view<'a>(
)
.push(
button::secondary(
Some(icon::import_icon()),
"Update",
Some(icon::restore_icon()),
"Import",
)
.on_press(Message::Spend(SpendTxMessage::EditPsbt)),
.on_press(Message::ImportPsbt),
),
)
.align_y(Alignment::Center),

View File

@ -68,8 +68,8 @@ pub fn psbts_view(spend_txs: &[SpendTx]) -> Element<'_, Message> {
.spacing(10)
.push(Container::new(h3("PSBTs")).width(Length::Fill))
.push(
button::secondary(Some(icon::import_icon()), "Import")
.on_press(Message::ImportSpend(ImportSpendMessage::Import)),
button::secondary(Some(icon::restore_icon()), "Import")
.on_press(Message::ImportPsbt),
)
.push(
button::secondary(Some(icon::plus_icon()), "New")

View File

@ -605,9 +605,9 @@ pub async fn import_psbt(
}
daemon.update_spend_tx(&psbt).await?;
send_progress!(sender, Psbt(psbt));
send_progress!(sender, Progress(100.0));
send_progress!(sender, Psbt(psbt));
Ok(())
}