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      Note: this PR is based on #1644 closes #1632 ACKs for top commit: edouardparis: ACK d86bd20a2fbcec0c3c37dd0cabb5ea02130a88f6 Tree-SHA512: 8bfde198fd1ff18fe893a64ffa31fd6c0fde2aa54214df7d1e0d1bb2b4ebfc67904160b5d7375e74c180d73ee2f2a754d32bb5be98e548bd7b54106597ce7b49
This commit is contained in:
commit
3837f35714
@ -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);
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -25,6 +25,8 @@ pub enum Message {
|
||||
ShowQrCode(usize),
|
||||
ImportExport(ImportExportMessage),
|
||||
HideRescanWarning,
|
||||
ExportPsbt,
|
||||
ImportPsbt,
|
||||
}
|
||||
|
||||
impl Close for Message {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user