Use spend state in recovery panel

This commit is contained in:
edouard 2023-01-20 12:41:23 +01:00
parent a2ac34e6b0
commit 9a1cda2f5e
4 changed files with 68 additions and 272 deletions

View File

@ -23,6 +23,7 @@ pub enum Message {
Coins(Result<Vec<Coin>, Error>),
SpendTxs(Result<Vec<SpendTx>, Error>),
Psbt(Result<Psbt, Error>),
Recovery(Result<SpendTx, Error>),
Signed(Result<(Psbt, Fingerprint), Error>),
Updated(Result<(), Error>),
Saved(Result<(), Error>),

View File

@ -10,19 +10,19 @@ use crate::{
error::Error,
menu::Menu,
message::Message,
state::spend::detail,
state::{redirect, State},
view,
wallet::Wallet,
},
daemon::{
model::{remaining_sequence, Coin},
model::{remaining_sequence, Coin, SpendTx},
Daemon,
},
hw::{list_hardware_wallets, HardwareWallet},
ui::component::form,
};
use liana::miniscript::bitcoin::{util::psbt::Psbt, Address, Amount, Network};
use liana::miniscript::bitcoin::{Address, Amount, Network};
pub struct RecoveryPanel {
wallet: Wallet,
@ -32,10 +32,7 @@ pub struct RecoveryPanel {
warning: Option<Error>,
feerate: form::Value<String>,
recipient: form::Value<String>,
generated: Option<Psbt>,
hws: Vec<HardwareWallet>,
selected_hw: Option<usize>,
signed: bool,
generated: Option<detail::SpendTxState>,
/// timelock value to pass for the heir to consume a coin.
timelock: u32,
}
@ -72,30 +69,27 @@ impl RecoveryPanel {
recipient: form::Value::default(),
generated: None,
timelock,
hws: Vec::new(),
selected_hw: None,
signed: false,
}
}
}
impl State for RecoveryPanel {
fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, view::Message> {
view::modal(
false,
self.warning.as_ref(),
view::recovery::recovery(
&self.locked_coins,
&self.recoverable_coins,
&self.feerate,
&self.recipient,
self.generated.as_ref(),
&self.hws,
self.selected_hw,
self.signed,
),
None::<Element<view::Message>>,
)
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
if let Some(generated) = &self.generated {
generated.view(cache)
} else {
view::modal(
false,
self.warning.as_ref(),
view::recovery::recovery(
&self.locked_coins,
&self.recoverable_coins,
&self.feerate,
&self.recipient,
),
None::<Element<view::Message>>,
)
}
}
fn update(
@ -127,27 +121,18 @@ impl State for RecoveryPanel {
}
}
},
// We add the new hws without dropping the reference of the previous ones.
Message::ConnectedHardwareWallets(hws) => {
for h in hws {
if !self.hws.iter().any(|hw| hw.fingerprint == h.fingerprint) {
self.hws.push(h);
}
Message::Recovery(res) => match res {
Ok(tx) => {
self.generated = Some(detail::SpendTxState::new(
self.wallet.clone(),
self.config.clone(),
tx,
false,
))
}
}
Message::Psbt(res) => match res {
Ok(psbt) => self.generated = Some(psbt),
Err(e) => self.warning = Some(e),
},
Message::Updated(res) => match res {
Err(e) => self.warning = Some(e),
Ok(()) => {
self.warning = None;
self.signed = true;
}
},
Message::View(msg) => match msg {
view::Message::Reload => return self.load(daemon),
view::Message::Close => return redirect(Menu::Settings),
view::Message::Previous => self.generated = None,
view::Message::CreateSpend(view::CreateSpendMessage::RecipientEdited(
@ -177,68 +162,52 @@ impl State for RecoveryPanel {
self.warning = None;
return Command::perform(
async move {
daemon
.create_recovery(address, feerate_vb)
.map_err(|e| e.into())
let psbt = daemon.create_recovery(address, feerate_vb)?;
let coins = daemon.list_coins().map(|res| res.coins)?;
let coins = coins
.iter()
.filter(|coin| {
psbt.unsigned_tx
.input
.iter()
.any(|input| input.previous_output == coin.outpoint)
})
.copied()
.collect();
Ok(SpendTx::new(psbt, coins))
},
Message::Psbt,
Message::Recovery,
);
}
view::Message::Spend(view::SpendTxMessage::SelectHardwareWallet(i)) => {
if let Some(hw) = self.hws.get(i) {
let device = hw.device.clone();
self.selected_hw = Some(i);
let psbt = self.generated.clone().unwrap();
return Command::perform(
send_funds(daemon, device, psbt),
Message::Updated,
);
_ => {
if let Some(generated) = &mut self.generated {
return generated.update(daemon, cache, Message::View(msg));
}
}
_ => {}
},
_ => {}
_ => {
if let Some(generated) = &mut self.generated {
return generated.update(daemon, cache, message);
}
}
};
Command::none()
}
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
let config = self.config.clone();
let desc = self.wallet.main_descriptor.to_string();
let daemon = daemon.clone();
Command::batch(vec![
Command::perform(
async move {
daemon
.list_coins()
.map(|res| res.coins)
.map_err(|e| e.into())
},
Message::Coins,
),
Command::perform(
list_hws(config, self.wallet.name.clone(), desc),
Message::ConnectedHardwareWallets,
),
])
Command::perform(
async move {
daemon
.list_coins()
.map(|res| res.coins)
.map_err(|e| e.into())
},
Message::Coins,
)
}
}
async fn list_hws(config: Config, wallet_name: String, descriptor: String) -> Vec<HardwareWallet> {
list_hardware_wallets(&config.hardware_wallets, Some((&wallet_name, &descriptor))).await
}
async fn send_funds(
daemon: Arc<dyn Daemon + Sync + Send>,
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
mut psbt: Psbt,
) -> Result<(), Error> {
hw.sign_tx(&mut psbt).await.map_err(Error::from)?;
daemon.update_spend_tx(&psbt)?;
daemon.broadcast_spend_tx(&psbt.unsigned_tx.txid())?;
Ok(())
}
impl From<RecoveryPanel> for Box<dyn State> {
fn from(s: RecoveryPanel) -> Box<dyn State> {
Box::new(s)

View File

@ -1,4 +1,4 @@
mod detail;
pub mod detail;
mod step;
use std::sync::Arc;

View File

@ -1,18 +1,14 @@
use iced::{
widget::{Button, Column, Container, Row, Space},
widget::{Column, Container, Row, Space},
Alignment, Element, Length,
};
use liana::miniscript::bitcoin::{util::psbt::Psbt, Amount};
use liana::miniscript::bitcoin::Amount;
use crate::{
app::view::{
hw::hw_list_view,
message::{CreateSpendMessage, Message},
},
hw::HardwareWallet,
app::view::message::{CreateSpendMessage, Message},
ui::{
component::{button, card, form, text::*},
component::{button, form, text::*},
icon,
util::Collection,
},
@ -24,10 +20,6 @@ pub fn recovery<'a>(
recoverable_coins: &(usize, Amount),
feerate: &form::Value<String>,
address: &'a form::Value<String>,
generated: Option<&Psbt>,
hws: &[HardwareWallet],
chosen_hw: Option<usize>,
done: bool,
) -> Element<'a, Message> {
Column::new()
.push(Space::with_height(Length::Units(100)))
@ -59,173 +51,7 @@ pub fn recovery<'a>(
None
})
.push(Space::with_height(Length::Units(20)))
.push(if let Some(psbt) = generated {
if done {
Column::new()
.spacing(20)
.align_items(Alignment::Center)
.push(text("Funds were sweeped"))
.push(card::simple(
Column::new()
.push(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(
text(format!(
"{}",
Amount::from_sat(psbt.unsigned_tx.output[0].value)
))
.small()
.bold(),
)
.push(text(" to ").small())
.push(text(&address.value).small().bold()),
)
.push(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(
text(format!("Txid: {}", psbt.unsigned_tx.txid())).small(),
)
.push(
Button::new(icon::clipboard_icon().small())
.on_press(Message::Clipboard(
psbt.unsigned_tx.txid().to_string(),
))
.style(button::Style::Border.into()),
),
)
.push_maybe(
if recoverable_coins.1.to_sat() > psbt.unsigned_tx.output[0].value {
Some(
Row::new().push(
text(format!(
"Fees: {}",
recoverable_coins.1
- Amount::from_sat(
psbt.unsigned_tx.output[0].value
)
))
.small(),
),
)
} else {
None
},
),
))
} else {
Column::new()
.spacing(20)
.align_items(Alignment::Center)
.push_maybe(if chosen_hw.is_none() {
Some(button::border(None, "< Previous").on_press(Message::Previous))
} else {
None
})
.push(text("Sign the transaction to sweep the funds").bold())
.push(card::simple(
Column::new()
.push(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(
text(format!(
"{}",
Amount::from_sat(psbt.unsigned_tx.output[0].value)
))
.small()
.bold(),
)
.push(text(" to ").small())
.push(text(&address.value).small().bold()),
)
.push(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(
text(format!("Txid: {}", psbt.unsigned_tx.txid())).small(),
)
.push(
Button::new(icon::clipboard_icon().small())
.on_press(Message::Clipboard(
psbt.unsigned_tx.txid().to_string(),
))
.style(button::Style::Border.into()),
),
)
.push_maybe(
if recoverable_coins.1.to_sat() > psbt.unsigned_tx.output[0].value {
Some(
Row::new().push(
text(format!(
"Fees: {}",
recoverable_coins.1
- Amount::from_sat(
psbt.unsigned_tx.output[0].value
)
))
.small(),
),
)
} else {
None
},
),
))
.push(if !hws.is_empty() {
Column::new()
.push(
Row::new()
.align_items(Alignment::Center)
.push(
text("Select hardware wallet to sign with:")
.bold()
.width(Length::Fill),
)
.push_maybe(if chosen_hw.is_none() {
Some(
button::border(None, "Refresh")
.on_press(Message::Reload),
)
} else {
None
}),
)
.spacing(10)
.push(hws.iter().enumerate().fold(
Column::new().spacing(10),
|col, (i, hw)| {
col.push(hw_list_view(
i,
hw,
Some(i) == chosen_hw,
chosen_hw.is_some(),
false,
))
},
))
.max_width(500)
} else {
Column::new()
.push(
Column::new()
.spacing(20)
.width(Length::Fill)
.push("Please connect a hardware wallet")
.push(
button::primary(None, "Refresh").on_press(Message::Reload),
)
.align_items(Alignment::Center),
)
.width(Length::Fill)
})
}
} else {
.push(
Column::new()
.push(text("Enter destination address and feerate:").bold())
.push(
@ -267,8 +93,8 @@ pub fn recovery<'a>(
},
)
.spacing(20)
.align_items(Alignment::Center)
})
.align_items(Alignment::Center),
)
.align_items(Alignment::Center)
.spacing(20)
.into()