Merge #1645: Import/Export PSBT from/to file

d86bd20a2fbcec0c3c37dd0cabb5ea02130a88f6 export: better wording when user try to import psbt w/ non owned coins (pythcoiner)
a1918f5eb6009c5e4526edcc57f82f5a1ccec37f export: do not panic if import an insane PSBT (pythcoiner)
3d9440d3ac70451a85b96e780b71d540ec811415 export:cleanup (pythcoiner)
71684e7039252384d18146c06b79d6f7deb9e0e4 gui: integrate import PSBT from file (pythcoiner)
9658cfde3fdbdd5823c3fd9014253bbf2901368a export: finalyze PSBT import (pythcoiner)
0796efae19e2bce521cd55c3f7bbc9f731bbad0e gui: integrate PSBT export instead copy to clipboard (pythcoiner)
64fcf3415481d516d516148aebf57a2a0ff40e42 gui: rename Action => Modal for consistency (pythcoiner)

Pull request description:

  - [x] gui: integrate PSBT export to file
  - [x] export: finalyze import PSBT from file
  - [x] gui integrate PSBT import from file

  ![image](https://github.com/user-attachments/assets/f9eac738-2014-4901-9df3-9cce862ea510)

  ![image](https://github.com/user-attachments/assets/647cd1d1-0f33-43f3-b543-8f64ecd92a31)

  ![image](https://github.com/user-attachments/assets/b2c157d7-ac3a-4944-ac57-99a15088c1c7)

  ![image](https://github.com/user-attachments/assets/287fc5aa-bd36-4431-bedc-1ff8b1b57e83)

  ![image](https://github.com/user-attachments/assets/1b393295-03f3-4c32-9670-0226dfd23dcb)

  Note: this PR is based on #1644

  closes #1632

ACKs for top commit:
  edouardparis:
    ACK d86bd20a2fbcec0c3c37dd0cabb5ea02130a88f6

Tree-SHA512: 8bfde198fd1ff18fe893a64ffa31fd6c0fde2aa54214df7d1e0d1bb2b4ebfc67904160b5d7375e74c180d73ee2f2a754d32bb5be98e548bd7b54106597ce7b49
This commit is contained in:
edouardparis 2025-04-21 10:45:58 +02:00
commit 3837f35714
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
8 changed files with 224 additions and 272 deletions

View File

@ -8,7 +8,10 @@ use liana_ui::{component::modal::Modal, widget::Element};
use tokio::task::JoinHandle;
use crate::{
app::view::{export::export_modal, Close},
app::{
self,
view::{export::export_modal, Close},
},
daemon::Daemon,
export::{self, get_path, ImportExportMessage, ImportExportState, ImportExportType, Progress},
};
@ -23,6 +26,34 @@ pub struct ExportModal {
import_export_type: ImportExportType,
}
impl app::state::psbt::Modal for ExportModal {
fn subscription(&self) -> Subscription<app::Message> {
self.subscription()
.map(|s| s.map(|m| app::Message::Export(ImportExportMessage::Progress(m))))
.unwrap_or(Subscription::none())
}
fn update(
&mut self,
_daemon: Arc<dyn Daemon + Sync + Send>,
message: app::Message,
_tx: &mut crate::daemon::model::SpendTx,
) -> Task<app::Message> {
if let app::Message::Export(m) = message {
self.update(m)
} else {
Task::none()
}
}
fn view<'a>(
&'a self,
content: Element<'a, app::view::Message>,
) -> Element<'a, app::view::Message> {
self.view(content)
}
}
impl ExportModal {
#[allow(clippy::new_without_default)]
pub fn new(
@ -50,7 +81,7 @@ impl ExportModal {
ImportExportType::ExportProcessBackup(..) | ImportExportType::ExportLabels => {
"Export Labels"
}
ImportExportType::ImportPsbt => "Import PSBT",
ImportExportType::ImportPsbt(_) => "Import PSBT",
ImportExportType::ImportDescriptor => "Import Descriptor",
ImportExportType::ImportBackup(..) => "Restore Backup",
ImportExportType::WalletFromBackup => "Import existing wallet from backup",
@ -74,7 +105,7 @@ impl ExportModal {
.to_string();
format!("liana-{}.txt", checksum)
}
ImportExportType::ImportPsbt => "psbt.psbt".into(),
ImportExportType::ImportPsbt(_) => "psbt.psbt".into(),
ImportExportType::ImportDescriptor => "descriptor.txt".into(),
ImportExportType::ExportLabels => format!("liana-labels-{date}.jsonl"),
ImportExportType::ExportBackup(_) | ImportExportType::ExportProcessBackup(..) => {
@ -124,12 +155,6 @@ impl ExportModal {
self.error = Some(e.clone());
}
Progress::None => {}
Progress::Psbt(_) => {
if 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;
@ -142,7 +167,6 @@ impl ExportModal {
if self.import_export_type == ImportExportType::ImportDescriptor {
self.state = ImportExportState::Ended;
}
// TODO: forward Descriptor
}
Progress::UpdateAliases(map) => {
return Task::perform(async {}, move |_| {
@ -150,6 +174,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,12 +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, Progress};
use crate::{
app::{
cache::Cache,
@ -35,7 +32,9 @@ use crate::{
hw::{HardwareWallet, HardwareWallets},
};
pub trait Action {
use super::export::ExportModal;
pub trait Modal {
fn load(&self, _daemon: Arc<dyn Daemon + Sync + Send>) -> Task<Message> {
Task::none()
}
@ -53,34 +52,34 @@ pub trait Action {
fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message>;
}
pub enum PsbtAction {
Save(SaveAction),
Sign(SignAction),
Update(UpdateAction),
Broadcast(BroadcastAction),
Delete(DeleteAction),
pub enum PsbtModal {
Save(SaveModal),
Sign(SignModal),
Broadcast(BroadcastModal),
Delete(DeleteModal),
Export(ExportModal),
}
impl<'a> AsRef<dyn Action + 'a> for PsbtAction {
fn as_ref(&self) -> &(dyn Action + 'a) {
impl<'a> AsRef<dyn Modal + 'a> for PsbtModal {
fn as_ref(&self) -> &(dyn Modal + 'a) {
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,
}
}
}
impl<'a> AsMut<dyn Action + 'a> for PsbtAction {
fn as_mut(&mut self) -> &mut (dyn Action + 'a) {
impl<'a> AsMut<dyn Modal + 'a> for PsbtModal {
fn as_mut(&mut self) -> &mut (dyn Modal + 'a) {
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,
}
}
}
@ -92,7 +91,7 @@ pub struct PsbtState {
pub saved: bool,
pub warning: Option<Error>,
pub labels_edited: LabelsEdited,
pub action: Option<PsbtAction>,
pub modal: Option<PsbtModal>,
}
impl PsbtState {
@ -102,27 +101,27 @@ impl PsbtState {
wallet,
labels_edited: LabelsEdited::default(),
warning: None,
action: None,
modal: None,
tx,
saved,
}
}
pub fn interrupt(&mut self) {
self.action = None;
self.modal = None;
}
pub fn subscription(&self) -> Subscription<Message> {
if let Some(action) = &self.action {
action.as_ref().subscription()
if let Some(modal) = &self.modal {
modal.as_ref().subscription()
} else {
Subscription::none()
}
}
pub fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Task<Message> {
if let Some(action) = &self.action {
action.as_ref().load(daemon)
if let Some(modal) = &self.modal {
modal.as_ref().load(daemon)
} else {
Task::none()
}
@ -135,38 +134,62 @@ impl PsbtState {
message: Message,
) -> Task<Message> {
match message {
Message::View(view::Message::ExportPsbt) => {
if self.modal.is_none() {
let psbt_str = self.tx.psbt.to_string();
let modal = ExportModal::new(None, ImportExportType::ExportPsbt(psbt_str));
let launch = modal.launch(true);
self.modal = Some(PsbtModal::Export(modal));
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;
}
}
Message::View(view::Message::ImportExport(m)) => {
if let Some(PsbtModal::Export(modal)) = self.modal.as_mut() {
return modal.update(m);
}
}
Message::View(view::Message::Spend(view::SpendTxMessage::Cancel)) => {
if let Some(PsbtAction::Sign(SignAction { display_modal, .. })) = &mut self.action {
if let Some(PsbtModal::Sign(SignModal { display_modal, .. })) = &mut self.modal {
*display_modal = false;
return Task::none();
}
self.action = None;
self.modal = None;
}
Message::View(view::Message::Spend(view::SpendTxMessage::Delete)) => {
self.action = Some(PsbtAction::Delete(DeleteAction::default()));
self.modal = Some(PsbtModal::Delete(DeleteModal::default()));
}
Message::View(view::Message::Spend(view::SpendTxMessage::Sign)) => {
if let Some(PsbtAction::Sign(SignAction { display_modal, .. })) = &mut self.action {
if let Some(PsbtModal::Sign(SignModal { display_modal, .. })) = &mut self.modal {
*display_modal = true;
return Task::none();
}
let action = SignAction::new(
let modal = SignModal::new(
self.tx.signers(),
self.wallet.clone(),
cache.datadir_path.clone(),
cache.network,
self.saved,
);
let cmd = action.load(daemon);
self.action = Some(PsbtAction::Sign(action));
return cmd;
}
Message::View(view::Message::Spend(view::SpendTxMessage::EditPsbt)) => {
let action = UpdateAction::new(self.wallet.clone(), self.tx.psbt.to_string());
let cmd = action.load(daemon);
self.action = Some(PsbtAction::Update(action));
let cmd = modal.load(daemon);
self.modal = Some(PsbtModal::Sign(modal));
return cmd;
}
Message::View(view::Message::Spend(view::SpendTxMessage::Broadcast)) => {
@ -188,7 +211,7 @@ impl PsbtState {
);
}
Message::View(view::Message::Spend(view::SpendTxMessage::Save)) => {
self.action = Some(PsbtAction::Save(SaveAction::default()));
self.modal = Some(PsbtModal::Save(SaveModal::default()));
}
Message::View(view::Message::Label(_, _)) | Message::LabelsUpdated(_) => {
match self.labels_edited.update(
@ -206,15 +229,13 @@ impl PsbtState {
}
Message::Updated(Ok(_)) => {
self.saved = true;
if let Some(action) = self.action.as_mut() {
return action
.as_mut()
.update(daemon.clone(), message, &mut self.tx);
if let Some(modal) = self.modal.as_mut() {
return modal.as_mut().update(daemon.clone(), message, &mut self.tx);
}
}
Message::BroadcastModal(res) => match res {
Ok(conflicting_txids) => {
self.action = Some(PsbtAction::Broadcast(BroadcastAction {
self.modal = Some(PsbtModal::Broadcast(BroadcastModal {
conflicting_txids,
..Default::default()
}));
@ -223,11 +244,17 @@ 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)
.expect("already check in psbt import logic");
}
_ => {
if let Some(action) = self.action.as_mut() {
return action
.as_mut()
.update(daemon.clone(), message, &mut self.tx);
if let Some(modal) = self.modal.as_mut() {
return modal.as_mut().update(daemon.clone(), message, &mut self.tx);
}
}
};
@ -245,8 +272,8 @@ impl PsbtState {
cache.network,
self.warning.as_ref(),
);
if let Some(action) = &self.action {
action.as_ref().view(content)
if let Some(modal) = &self.modal {
modal.as_ref().view(content)
} else {
content
}
@ -254,12 +281,12 @@ impl PsbtState {
}
#[derive(Default)]
pub struct SaveAction {
pub struct SaveModal {
saved: bool,
error: Option<Error>,
}
impl Action for SaveAction {
impl Modal for SaveModal {
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
@ -303,14 +330,14 @@ impl Action for SaveAction {
}
#[derive(Default)]
pub struct BroadcastAction {
pub struct BroadcastModal {
broadcast: bool,
error: Option<Error>,
/// IDs of any directly conflicting transactions.
conflicting_txids: HashSet<Txid>,
}
impl Action for BroadcastAction {
impl Modal for BroadcastModal {
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
@ -358,12 +385,12 @@ impl Action for BroadcastAction {
}
#[derive(Default)]
pub struct DeleteAction {
pub struct DeleteModal {
deleted: bool,
error: Option<Error>,
}
impl Action for DeleteAction {
impl Modal for DeleteModal {
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
@ -403,7 +430,7 @@ impl Action for DeleteAction {
}
}
pub struct SignAction {
pub struct SignModal {
wallet: Arc<Wallet>,
hws: HardwareWallets,
error: Option<Error>,
@ -413,7 +440,7 @@ pub struct SignAction {
display_modal: bool,
}
impl SignAction {
impl SignModal {
pub fn new(
signed: HashSet<Fingerprint>,
wallet: Arc<Wallet>,
@ -433,7 +460,7 @@ impl SignAction {
}
}
impl Action for SignAction {
impl Modal for SignModal {
fn subscription(&self) -> Subscription<Message> {
self.hws.refresh().map(Message::HardwareWallets)
}
@ -637,97 +664,6 @@ async fn sign_psbt(
Ok(psbt)
}
pub struct UpdateAction {
wallet: Arc<Wallet>,
psbt: String,
updated: form::Value<String>,
processing: bool,
error: Option<Error>,
success: bool,
}
impl UpdateAction {
pub fn new(wallet: Arc<Wallet>, psbt: String) -> Self {
Self {
wallet,
psbt,
updated: form::Value::default(),
processing: false,
error: None,
success: false,
}
}
}
impl Action for UpdateAction {
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

@ -825,8 +825,8 @@ impl Step for SaveSpend {
cache.network,
psbt_state.warning.as_ref(),
);
if let Some(action) = &psbt_state.action {
action.as_ref().view(content)
if let Some(modal) = &psbt_state.modal {
modal.as_ref().view(content)
} else {
content
}

View File

@ -25,6 +25,8 @@ pub enum Message {
ShowQrCode(usize),
ImportExport(ImportExportMessage),
HideRescanWarning,
ExportPsbt,
ImportPsbt,
}
impl Close for Message {

View File

@ -339,17 +339,17 @@ pub fn spend_overview_view<'a>(
.spacing(5)
.push(
button::secondary(
Some(icon::clipboard_icon()),
"Copy",
Some(icon::backup_icon()),
"Export",
)
.on_press(Message::Clipboard(tx.psbt.to_string())),
.on_press(Message::ExportPsbt),
)
.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

@ -123,6 +123,9 @@ pub enum Error {
Backup(backup::Error),
ParseXpub,
XpubNetwork,
TxidNotMatch,
InsanePsbt,
OutpointNotOwned,
}
impl Display for Error {
@ -144,6 +147,12 @@ impl Display for Error {
Error::Backup(e) => write!(f, "Backup: {e}"),
Error::ParseXpub => write!(f, "Failed to parse Xpub from file"),
Error::XpubNetwork => write!(f, "Xpub is for another network"),
Error::TxidNotMatch => write!(f, "The imported PSBT txid doesn't match this PSBT"),
Error::InsanePsbt => write!(f, "The Psbt is not sane"),
Error::OutpointNotOwned => write!(
f,
"Import failed. The PSBT either doesn't belong to the wallet or has already been spent."
),
}
}
}
@ -162,7 +171,7 @@ pub enum ImportExportType {
WalletFromBackup,
Descriptor(LianaDescriptor),
ExportLabels,
ImportPsbt,
ImportPsbt(Option<Txid>),
ImportXpub(Network),
ImportDescriptor,
}
@ -178,7 +187,7 @@ impl ImportExportType {
| ImportExportType::ExportXpub(_)
| ImportExportType::ExportLabels => "Export successful!",
ImportExportType::ImportBackup(_, _)
| ImportExportType::ImportPsbt
| ImportExportType::ImportPsbt(_)
| ImportExportType::ImportXpub(_)
| ImportExportType::WalletFromBackup
| ImportExportType::ImportDescriptor => "Import successful",
@ -294,7 +303,7 @@ impl Export {
export_descriptor(&sender, path, descriptor).await
}
ImportExportType::ExportLabels => export_labels(&sender, daemon, path).await,
ImportExportType::ImportPsbt => import_psbt(&sender, path).await,
ImportExportType::ImportPsbt(txid) => import_psbt(daemon, &sender, path, txid).await,
ImportExportType::ImportXpub(network) => import_xpub(&sender, path, network).await,
ImportExportType::ImportDescriptor => import_descriptor(&sender, path).await,
ImportExportType::ExportBackup(str) => export_string(&sender, path, str).await,
@ -580,17 +589,48 @@ pub async fn export_string(
Ok(())
}
pub async fn import_psbt(sender: &UnboundedSender<Progress>, path: PathBuf) -> Result<(), Error> {
pub async fn import_psbt(
daemon: Option<Arc<dyn Daemon + Sync + Send>>,
sender: &UnboundedSender<Progress>,
path: PathBuf,
txid: Option<Txid>,
) -> Result<(), Error> {
let mut file = File::open(&path)?;
let daemon = daemon.ok_or(Error::DaemonMissing)?;
let descr = daemon.get_info().await?.descriptors.main;
let mut psbt_str = String::new();
file.read_to_string(&mut psbt_str)?;
psbt_str = psbt_str.trim().to_string();
let psbt = Psbt::from_str(&psbt_str).map_err(|_| Error::ParsePsbt)?;
send_progress!(sender, Progress(50.0));
descr
.partial_spend_info(&psbt)
.map_err(|_| Error::InsanePsbt)?;
if let Some(txid) = &txid {
if psbt.unsigned_tx.compute_txid() != *txid {
return Err(Error::TxidNotMatch);
}
}
let e = daemon.update_spend_tx(&psbt).await;
if let (None, Err(error)) = (txid, &e) {
if let DaemonError::Unexpected(e) = error {
if e.contains("Unknown outpoint") {
return Err(Error::OutpointNotOwned);
} else {
return Err(Error::Daemon(error.to_string()));
}
}
} else {
e?;
}
send_progress!(sender, Psbt(psbt));
send_progress!(sender, Progress(100.0));
send_progress!(sender, Psbt(psbt));
Ok(())
}