Merge #205: gui: add recovery feature
f43d99f0fbd851d9817b3668b26953f81c77c1f9 gui: add panel recovery (edouard) f2facdf1641eaa0241201dee23e13dec63e534cc gui: add create_recovery command to client (edouard) f4afed4d6a14ad9ea515a69ebf0c584106423075 bump liana master (edouard) Pull request description: After #182, this fixes #180. ACKs for top commit: edouardparis: Self-ACK f43d99f0fbd851d9817b3668b26953f81c77c1f9 Tree-SHA512: 49fa2582b5ef405b3ac79ce621792918afe00b0f6fd64bce626f84c11fc97c2b96459f3daf424e625eda31842f85982b0bf76da71afd36bd848e9a4d8fe96c27
This commit is contained in:
commit
a4299877eb
2
gui/Cargo.lock
generated
2
gui/Cargo.lock
generated
@ -1640,7 +1640,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "liana"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/revault/liana?branch=master#d385e99726733851da9101557cf2dfcbe4c2a2a0"
|
||||
source = "git+https://github.com/revault/liana?branch=master#ad1f0e20b1a4c46007839cc20200e348eb041275"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"base64",
|
||||
|
||||
@ -6,4 +6,5 @@ pub enum Menu {
|
||||
Settings,
|
||||
Coins,
|
||||
CreateSpendTx,
|
||||
Recovery,
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ pub use liana::config::Config as DaemonConfig;
|
||||
pub use config::Config;
|
||||
pub use message::Message;
|
||||
|
||||
use state::{CoinsPanel, CreateSpendPanel, Home, ReceivePanel, SpendPanel, State};
|
||||
use state::{CoinsPanel, CreateSpendPanel, Home, ReceivePanel, RecoveryPanel, SpendPanel, State};
|
||||
|
||||
use crate::{
|
||||
app::{cache::Cache, error::Error, menu::Menu},
|
||||
@ -69,6 +69,13 @@ impl App {
|
||||
self.daemon.config().main_descriptor.timelock_value(),
|
||||
)
|
||||
.into(),
|
||||
menu::Menu::Recovery => RecoveryPanel::new(
|
||||
self.config.clone(),
|
||||
&self.cache.coins,
|
||||
self.daemon.config().main_descriptor.timelock_value(),
|
||||
self.cache.blockheight as u32,
|
||||
)
|
||||
.into(),
|
||||
menu::Menu::Receive => ReceivePanel::default().into(),
|
||||
menu::Menu::Spend => SpendPanel::new(self.config.clone(), &self.cache.spend_txs).into(),
|
||||
menu::Menu::CreateSpendTx => CreateSpendPanel::new(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
mod coins;
|
||||
mod recovery;
|
||||
mod settings;
|
||||
mod spend;
|
||||
|
||||
@ -17,6 +18,7 @@ use crate::daemon::{
|
||||
Daemon,
|
||||
};
|
||||
pub use coins::CoinsPanel;
|
||||
pub use recovery::RecoveryPanel;
|
||||
pub use settings::SettingsState;
|
||||
pub use spend::{CreateSpendPanel, SpendPanel};
|
||||
|
||||
|
||||
230
gui/src/app/state/recovery.rs
Normal file
230
gui/src/app/state/recovery.rs
Normal file
@ -0,0 +1,230 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use iced::{Command, Element};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
cache::Cache,
|
||||
config::Config,
|
||||
error::Error,
|
||||
menu::Menu,
|
||||
message::Message,
|
||||
state::{redirect, State},
|
||||
view,
|
||||
},
|
||||
daemon::{
|
||||
model::{remaining_sequence, Coin},
|
||||
Daemon,
|
||||
},
|
||||
hw::{list_hardware_wallets, HardwareWallet},
|
||||
ui::component::form,
|
||||
};
|
||||
|
||||
use liana::miniscript::bitcoin::{util::psbt::Psbt, Address, Amount, Network};
|
||||
|
||||
pub struct RecoveryPanel {
|
||||
config: Config,
|
||||
locked_coins: (usize, Amount),
|
||||
recoverable_coins: (usize, Amount),
|
||||
warning: Option<Error>,
|
||||
feerate: form::Value<String>,
|
||||
recipient: form::Value<String>,
|
||||
generated: Option<Psbt>,
|
||||
hws: Vec<HardwareWallet>,
|
||||
selected_hw: Option<usize>,
|
||||
signed: bool,
|
||||
/// timelock value to pass for the heir to consume a coin.
|
||||
timelock: u32,
|
||||
}
|
||||
|
||||
impl RecoveryPanel {
|
||||
pub fn new(config: Config, coins: &[Coin], timelock: u32, blockheight: u32) -> Self {
|
||||
let mut locked_coins = (0, Amount::from_sat(0));
|
||||
let mut recoverable_coins = (0, Amount::from_sat(0));
|
||||
for coin in coins {
|
||||
if coin.spend_info.is_none() {
|
||||
if remaining_sequence(coin, blockheight, timelock) != 0 {
|
||||
locked_coins.0 += 1;
|
||||
locked_coins.1 += coin.amount;
|
||||
} else {
|
||||
recoverable_coins.0 += 1;
|
||||
recoverable_coins.1 += coin.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
config,
|
||||
locked_coins,
|
||||
recoverable_coins,
|
||||
warning: None,
|
||||
feerate: form::Value::default(),
|
||||
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 update(
|
||||
&mut self,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
cache: &Cache,
|
||||
message: Message,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Coins(res) => match res {
|
||||
Err(e) => self.warning = Some(e),
|
||||
Ok(coins) => {
|
||||
self.warning = None;
|
||||
self.locked_coins = (0, Amount::from_sat(0));
|
||||
self.recoverable_coins = (0, Amount::from_sat(0));
|
||||
for coin in coins {
|
||||
if coin.spend_info.is_none() {
|
||||
if remaining_sequence(&coin, cache.blockheight as u32, self.timelock)
|
||||
!= 0
|
||||
{
|
||||
self.locked_coins.0 += 1;
|
||||
self.locked_coins.1 += coin.amount;
|
||||
} else {
|
||||
self.recoverable_coins.0 += 1;
|
||||
self.recoverable_coins.1 += coin.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::ConnectedHardwareWallets(hws) => {
|
||||
self.hws = hws;
|
||||
}
|
||||
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(
|
||||
_,
|
||||
"address",
|
||||
address,
|
||||
)) => {
|
||||
self.recipient.value = address;
|
||||
if let Ok(address) = Address::from_str(&self.recipient.value) {
|
||||
if cache.network == Network::Bitcoin {
|
||||
self.recipient.valid = address.network == Network::Bitcoin;
|
||||
} else {
|
||||
self.recipient.valid = address.network == Network::Testnet;
|
||||
}
|
||||
} else {
|
||||
self.recipient.valid = false;
|
||||
}
|
||||
}
|
||||
view::Message::CreateSpend(view::CreateSpendMessage::FeerateEdited(feerate)) => {
|
||||
self.feerate.value = feerate;
|
||||
self.feerate.valid =
|
||||
self.feerate.value.parse::<u64>().is_ok() && self.feerate.value != "0";
|
||||
}
|
||||
view::Message::Next => {
|
||||
let address = Address::from_str(&self.recipient.value).expect("Checked before");
|
||||
let feerate_vb = self.feerate.value.parse::<u64>().expect("Checked before");
|
||||
self.warning = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
daemon
|
||||
.create_recovery(address, feerate_vb)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
Message::Psbt,
|
||||
);
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
let config = self.config.clone();
|
||||
let desc = daemon.config().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, "Liana".to_string(), desc),
|
||||
Message::ConnectedHardwareWallets,
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
70
gui/src/app/view/hw.rs
Normal file
70
gui/src/app/view/hw.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use iced::{
|
||||
widget::{Button, Column, Container, Row},
|
||||
Alignment, Element, Length,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::view::message::*,
|
||||
hw::HardwareWallet,
|
||||
ui::{
|
||||
color,
|
||||
component::{
|
||||
button, card,
|
||||
text::{text, Text},
|
||||
},
|
||||
icon,
|
||||
util::Collection,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn hw_list_view<'a>(
|
||||
i: usize,
|
||||
hw: &HardwareWallet,
|
||||
chosen: bool,
|
||||
processing: bool,
|
||||
signed: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let mut bttn = Button::new(
|
||||
Row::new()
|
||||
.push(
|
||||
Column::new()
|
||||
.push(text(format!("{}", hw.kind)).bold())
|
||||
.push(text(format!("fingerprint: {}", hw.fingerprint)).small())
|
||||
.spacing(5)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push_maybe(if chosen && processing {
|
||||
Some(
|
||||
Column::new()
|
||||
.push(text("Processing..."))
|
||||
.push(text("Please check your device").small()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push_maybe(if signed {
|
||||
Some(
|
||||
Column::new().push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(icon::circle_check_icon().style(color::SUCCESS))
|
||||
.push(text("Signed").style(color::SUCCESS)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.padding(10)
|
||||
.style(button::Style::Border.into())
|
||||
.width(Length::Fill);
|
||||
if !processing {
|
||||
bttn = bttn.on_press(Message::Spend(SpendTxMessage::SelectHardwareWallet(i)));
|
||||
}
|
||||
Container::new(bttn)
|
||||
.width(Length::Fill)
|
||||
.style(card::SimpleCardStyle)
|
||||
.into()
|
||||
}
|
||||
@ -3,7 +3,9 @@ mod warning;
|
||||
|
||||
pub mod coins;
|
||||
pub mod home;
|
||||
pub mod hw;
|
||||
pub mod receive;
|
||||
pub mod recovery;
|
||||
pub mod settings;
|
||||
pub mod spend;
|
||||
|
||||
|
||||
202
gui/src/app/view/recovery.rs
Normal file
202
gui/src/app/view/recovery.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use iced::{
|
||||
widget::{Button, Column, Container, Row, Space},
|
||||
Alignment, Element, Length,
|
||||
};
|
||||
|
||||
use liana::miniscript::bitcoin::{util::psbt::Psbt, Amount};
|
||||
|
||||
use crate::{
|
||||
app::view::{
|
||||
hw::hw_list_view,
|
||||
message::{CreateSpendMessage, Message},
|
||||
},
|
||||
hw::HardwareWallet,
|
||||
ui::{
|
||||
component::{button, card, form, text::*},
|
||||
icon,
|
||||
util::Collection,
|
||||
},
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn recovery<'a>(
|
||||
locked_coins: &(usize, Amount),
|
||||
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)))
|
||||
.push(
|
||||
Container::new(icon::recovery_icon().width(Length::Units(100)).size(50))
|
||||
.width(Length::Fill)
|
||||
.center_x(),
|
||||
)
|
||||
.push(text("Recover the funds").size(50).bold())
|
||||
.push(
|
||||
Container::new(Row::new().push(text(format!(
|
||||
"{} ({} coins) are recoverable at the current blockheight",
|
||||
recoverable_coins.1, recoverable_coins.0
|
||||
))))
|
||||
.center_x(),
|
||||
)
|
||||
.push_maybe(if *locked_coins != (0, Amount::from_sat(0)) {
|
||||
Some(
|
||||
Container::new(Row::new().push(text(format!(
|
||||
"{} ({} coins) have their recovery path not available at the current blockheight",
|
||||
locked_coins.1, locked_coins.0
|
||||
))))
|
||||
.center_x(),
|
||||
)
|
||||
} else {
|
||||
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(Row::new().push(text(format!("Fees: {}", recoverable_coins.1 - Amount::from_sat(psbt.unsigned_tx.output[0].value))).small()))
|
||||
))
|
||||
} 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("2/2").bold())
|
||||
.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(Row::new().push(text(format!("Fees: {}", recoverable_coins.1 - Amount::from_sat(psbt.unsigned_tx.output[0].value))).small()))
|
||||
)
|
||||
)
|
||||
.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 {
|
||||
Column::new()
|
||||
.push(text("1/2").bold())
|
||||
.push(text("Enter destination address and feerate:").bold())
|
||||
.push(
|
||||
Container::new(
|
||||
form::Form::new("Feerate (sat/vbyte)", feerate, move |msg| {
|
||||
Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg))
|
||||
})
|
||||
.warning("Please enter correct feerate (sat/vbyte)")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.width(Length::Units(250)),
|
||||
)
|
||||
.push(
|
||||
Container::new(
|
||||
form::Form::new("Address", address, move |msg| {
|
||||
Message::CreateSpend(CreateSpendMessage::RecipientEdited(0, "address", msg))
|
||||
})
|
||||
.warning("Please enter correct bitcoin address")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.width(Length::Units(250)),
|
||||
)
|
||||
.push(if feerate.valid && !feerate.value.is_empty() && address.valid && !address.value.is_empty() {
|
||||
button::primary(None, "Next").on_press(Message::Next).width(Length::Units(200))
|
||||
} else {
|
||||
button::primary(None, "Next")
|
||||
.width(Length::Units(200))
|
||||
})
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
}
|
||||
)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20)
|
||||
.into()
|
||||
}
|
||||
@ -2,7 +2,7 @@ use std::str::FromStr;
|
||||
|
||||
use iced::{
|
||||
alignment,
|
||||
widget::{self, Column, Container, ProgressBar, Row},
|
||||
widget::{self, Column, Container, ProgressBar, Row, Space},
|
||||
Alignment, Element, Length,
|
||||
};
|
||||
|
||||
@ -32,7 +32,29 @@ pub fn list<'a>(
|
||||
&Menu::Settings,
|
||||
cache,
|
||||
warning,
|
||||
widget::Column::with_children(settings).spacing(20),
|
||||
widget::Column::with_children(settings)
|
||||
.spacing(20)
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(badge::Badge::new(icon::recovery_icon()))
|
||||
.push(text("Recovery").bold())
|
||||
.padding(10)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(separation().width(Length::Fill))
|
||||
.push(Space::with_height(Length::Units(10)))
|
||||
.push(text("In case of loss of the main key, the recovery key can move the funds after a certain time."))
|
||||
.push(Space::with_height(Length::Units(10)))
|
||||
.push(
|
||||
Row::new()
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(button::primary(None, "Recover funds").on_press(Message::Menu(Menu::Recovery))),
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@ -114,7 +136,7 @@ pub fn bitcoind_edit<'a>(
|
||||
.push(
|
||||
Row::new()
|
||||
.push(badge::Badge::new(icon::bitcoin_icon()))
|
||||
.push(text("Bitcoind"))
|
||||
.push(text("Bitcoind").bold())
|
||||
.padding(10)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
@ -201,7 +223,7 @@ pub fn bitcoind<'a>(
|
||||
.push(
|
||||
Row::new()
|
||||
.push(badge::Badge::new(icon::bitcoin_icon()))
|
||||
.push(text("Bitcoind"))
|
||||
.push(text("Bitcoind").bold())
|
||||
.push(is_running_label(is_running))
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
@ -261,7 +283,7 @@ pub fn rescan<'a>(
|
||||
.push(
|
||||
Row::new()
|
||||
.push(badge::Badge::new(icon::block_icon()))
|
||||
.push(text("Rescan blockchain").width(Length::Fill))
|
||||
.push(text("Rescan blockchain").bold().width(Length::Fill))
|
||||
.push_maybe(if success {
|
||||
Some(text("Rescan was successful").style(color::SUCCESS))
|
||||
} else {
|
||||
|
||||
@ -8,12 +8,11 @@ use liana::miniscript::bitcoin::{util::bip32::Fingerprint, Address, Amount, Netw
|
||||
use crate::{
|
||||
app::{
|
||||
error::Error,
|
||||
view::{message::*, warning::warn},
|
||||
view::{hw::hw_list_view, message::*, warning::warn},
|
||||
},
|
||||
daemon::model::{Coin, SpendStatus, SpendTx},
|
||||
hw::HardwareWallet,
|
||||
ui::{
|
||||
color,
|
||||
component::{
|
||||
badge, button, card,
|
||||
collapse::Collapse,
|
||||
@ -515,55 +514,3 @@ pub fn sign_action<'a>(
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn hw_list_view<'a>(
|
||||
i: usize,
|
||||
hw: &HardwareWallet,
|
||||
chosen: bool,
|
||||
processing: bool,
|
||||
signed: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let mut bttn = Button::new(
|
||||
Row::new()
|
||||
.push(
|
||||
Column::new()
|
||||
.push(text(format!("{}", hw.kind)).bold())
|
||||
.push(text(format!("fingerprint: {}", hw.fingerprint)).small())
|
||||
.spacing(5)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push_maybe(if chosen && processing {
|
||||
Some(
|
||||
Column::new()
|
||||
.push(text("Processing..."))
|
||||
.push(text("Please check your device").small()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push_maybe(if signed {
|
||||
Some(
|
||||
Column::new().push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(icon::circle_check_icon().style(color::SUCCESS))
|
||||
.push(text("Signed").style(color::SUCCESS)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.padding(10)
|
||||
.style(button::Style::Border.into())
|
||||
.width(Length::Fill);
|
||||
if !processing {
|
||||
bttn = bttn.on_press(Message::Spend(SpendTxMessage::SelectHardwareWallet(i)));
|
||||
}
|
||||
Container::new(bttn)
|
||||
.width(Length::Fill)
|
||||
.style(card::SimpleCardStyle)
|
||||
.into()
|
||||
}
|
||||
|
||||
@ -134,6 +134,14 @@ impl<C: Client + Debug> Daemon for Lianad<C> {
|
||||
fn list_txs(&self, txids: &[Txid]) -> Result<ListTransactionsResult, DaemonError> {
|
||||
self.call("listtransactions", Some(vec![txids]))
|
||||
}
|
||||
|
||||
fn create_recovery(&self, address: Address, feerate_vb: u64) -> Result<Psbt, DaemonError> {
|
||||
let res: CreateSpendResult = self.call(
|
||||
"createrecovery",
|
||||
Some(vec![json!(address), json!(feerate_vb)]),
|
||||
)?;
|
||||
Ok(res.psbt)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
|
||||
@ -193,4 +193,16 @@ impl Daemon for EmbeddedDaemon {
|
||||
.start_rescan(t)
|
||||
.map_err(|e| DaemonError::Unexpected(e.to_string()))
|
||||
}
|
||||
|
||||
fn create_recovery(&self, address: Address, feerate_vb: u64) -> Result<Psbt, DaemonError> {
|
||||
self.handle
|
||||
.as_ref()
|
||||
.ok_or(DaemonError::NoAnswer)?
|
||||
.read()
|
||||
.unwrap()
|
||||
.control
|
||||
.create_recovery(address, feerate_vb)
|
||||
.map_err(|e| DaemonError::Unexpected(e.to_string()))
|
||||
.map(|res| res.psbt)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,6 +68,7 @@ pub trait Daemon: Debug {
|
||||
_end: u32,
|
||||
_limit: u64,
|
||||
) -> Result<model::ListTransactionsResult, DaemonError>;
|
||||
fn create_recovery(&self, address: Address, feerate_vb: u64) -> Result<Psbt, DaemonError>;
|
||||
fn list_txs(&self, txid: &[Txid]) -> Result<model::ListTransactionsResult, DaemonError>;
|
||||
|
||||
fn list_spend_transactions(&self) -> Result<Vec<model::SpendTx>, DaemonError> {
|
||||
|
||||
@ -13,6 +13,10 @@ fn icon(unicode: char) -> Text<'static> {
|
||||
.size(20)
|
||||
}
|
||||
|
||||
pub fn recovery_icon() -> Text<'static> {
|
||||
icon('\u{F467}')
|
||||
}
|
||||
|
||||
pub fn plug_icon() -> Text<'static> {
|
||||
icon('\u{F4F6}')
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user