Skip to content

Commit

Permalink
Add verify address modal to receive panel
Browse files Browse the repository at this point in the history
bump async-hwi v0.0.13
  • Loading branch information
edouardparis committed Nov 20, 2023
1 parent fa992aa commit c7c465c
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 145 deletions.
256 changes: 144 additions & 112 deletions gui/Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ name = "liana-gui"
path = "src/main.rs"

[dependencies]
async-hwi = "0.0.12"
async-hwi = "0.0.13"
liana = { git = "https://github.com/wizardsardine/liana", branch = "master", default-features = false, features = ["nonblocking_shutdown"] }
liana_ui = { path = "ui" }
backtrace = "0.3"
Expand Down
9 changes: 7 additions & 2 deletions gui/src/app/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ use std::sync::Arc;

use liana::{
config::Config as DaemonConfig,
miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Address},
miniscript::bitcoin::{
bip32::{ChildNumber, Fingerprint},
psbt::Psbt,
Address,
},
};

use crate::{
Expand All @@ -21,7 +25,7 @@ pub enum Message {
LoadWallet,
WalletLoaded(Result<Arc<Wallet>, Error>),
Info(Result<GetInfoResult, Error>),
ReceiveAddress(Result<Address, Error>),
ReceiveAddress(Result<(Address, ChildNumber), Error>),
Coins(Result<Vec<Coin>, Error>),
Labels(Result<HashMap<String, String>, Error>),
SpendTxs(Result<Vec<SpendTx>, Error>),
Expand All @@ -31,6 +35,7 @@ pub enum Message {
WalletRegistered(Result<Fingerprint, Error>),
Updated(Result<(), Error>),
Saved(Result<(), Error>),
Verified(Fingerprint, Result<(), Error>),
StartRescan(Result<(), Error>),
HardwareWallets(HardwareWalletMessage),
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
Expand Down
4 changes: 3 additions & 1 deletion gui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ impl App {
self.cache.blockheight,
)
.into(),
menu::Menu::Receive => ReceivePanel::default().into(),
menu::Menu::Receive => {
ReceivePanel::new(self.data_dir.clone(), self.wallet.clone()).into()
}
menu::Menu::Transactions => TransactionsPanel::new().into(),
menu::Menu::PSBTs => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(),
menu::Menu::CreateSpendTx => CreateSpendPanel::new(
Expand Down
211 changes: 189 additions & 22 deletions gui/src/app/state/receive.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;

use iced::{widget::qr_code, Command, Subscription};
use liana::miniscript::bitcoin::Address;
use liana_ui::widget::*;

use crate::app::{
cache::Cache,
error::Error,
menu::Menu,
message::Message,
state::{label::LabelsEdited, State},
view,
wallet::Wallet,
use liana::miniscript::bitcoin::{
bip32::{ChildNumber, Fingerprint},
Address, Network,
};
use liana_ui::{component::modal, widget::*};

use crate::{
app::{
cache::Cache,
error::Error,
menu::Menu,
message::Message,
state::{label::LabelsEdited, State},
view,
wallet::Wallet,
},
hw::{HardwareWallet, HardwareWallets},
};

use crate::daemon::{
Expand All @@ -23,6 +30,7 @@ use crate::daemon::{
#[derive(Debug, Default)]
pub struct Addresses {
list: Vec<Address>,
derivation_indexes: Vec<ChildNumber>,
labels: HashMap<String, String>,
}

Expand All @@ -38,17 +46,33 @@ impl Labelled for Addresses {
}
}

#[derive(Default)]
pub struct ReceivePanel {
data_dir: PathBuf,
wallet: Arc<Wallet>,
addresses: Addresses,
labels_edited: LabelsEdited,
qr_code: Option<qr_code::State>,
modal: Option<VerifyAddressModal>,
warning: Option<Error>,
}

impl ReceivePanel {
pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>) -> Self {
Self {
data_dir,
wallet,
addresses: Addresses::default(),
labels_edited: LabelsEdited::default(),
qr_code: None,
modal: None,
warning: None,
}
}
}

impl State for ReceivePanel {
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
view::dashboard(
let content = view::dashboard(
&Menu::Receive,
cache,
self.warning.as_ref(),
Expand All @@ -58,12 +82,28 @@ impl State for ReceivePanel {
&self.addresses.labels,
self.labels_edited.cache(),
),
)
);
if let Some(m) = &self.modal {
modal::Modal::new(content, m.view())
.on_blur(Some(view::Message::Close))
.into()
} else {
content
}
}

fn subscription(&self) -> Subscription<Message> {
if let Some(modal) = &self.modal {
modal.subscription()
} else {
Subscription::none()
}
}

fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
cache: &Cache,
message: Message,
) -> Command<Message> {
match message {
Expand All @@ -82,17 +122,40 @@ impl State for ReceivePanel {
}
Message::ReceiveAddress(res) => {
match res {
Ok(address) => {
Ok((address, derivation_index)) => {
self.warning = None;
self.qr_code = Some(qr_code::State::new(address.to_qr_uri()).unwrap());
self.addresses.list.push(address);
self.addresses.derivation_indexes.push(derivation_index);
}
Err(e) => self.warning = Some(e),
}
Command::none()
}
Message::View(view::Message::Close) => {
self.modal = None;
Command::none()
}
Message::View(view::Message::Select(i)) => {
self.modal = Some(VerifyAddressModal::new(
self.data_dir.clone(),
self.wallet.clone(),
cache.network,
self.addresses.list.get(i).expect("Must be present").clone(),
*self
.addresses
.derivation_indexes
.get(i)
.expect("Must be present"),
));
Command::none()
}
Message::View(view::Message::Next) => self.load(daemon),
_ => Command::none(),
_ => self
.modal
.as_mut()
.map(|m| m.update(daemon, cache, message))
.unwrap_or_else(Command::none),
}
}

Expand All @@ -102,7 +165,7 @@ impl State for ReceivePanel {
async move {
daemon
.get_new_address()
.map(|res| res.address().clone())
.map(|res| (res.address, res.derivation_index))
.map_err(|e| e.into())
},
Message::ReceiveAddress,
Expand All @@ -116,6 +179,102 @@ impl From<ReceivePanel> for Box<dyn State> {
}
}

pub struct VerifyAddressModal {
warning: Option<Error>,
chosen_hws: HashSet<Fingerprint>,
hws: HardwareWallets,
address: Address,
derivation_index: ChildNumber,
}

impl VerifyAddressModal {
pub fn new(
data_dir: PathBuf,
wallet: Arc<Wallet>,
network: Network,
address: Address,
derivation_index: ChildNumber,
) -> Self {
Self {
warning: None,
chosen_hws: HashSet::new(),
hws: HardwareWallets::new(data_dir, network).with_wallet(wallet),
address,
derivation_index,
}
}
}

impl VerifyAddressModal {
fn view(&self) -> Element<view::Message> {
view::receive::verify_address_modal(
self.warning.as_ref(),
&self.hws.list,
&self.chosen_hws,
&self.address,
&self.derivation_index,
)
}

fn subscription(&self) -> Subscription<Message> {
self.hws.refresh().map(Message::HardwareWallets)
}

fn update(
&mut self,
_daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: Message,
) -> Command<Message> {
match message {
Message::HardwareWallets(msg) => match self.hws.update(msg) {
Ok(cmd) => cmd.map(Message::HardwareWallets),
Err(e) => {
self.warning = Some(e.into());
Command::none()
}
},
Message::Verified(fg, res) => {
self.chosen_hws.remove(&fg);
if let Err(e) = res {
self.warning = Some(e);
}
Command::none()
}
Message::View(view::Message::SelectHardwareWallet(i)) => {
if let Some(HardwareWallet::Supported {
device,
fingerprint,
..
}) = self.hws.list.get(i)
{
self.chosen_hws.insert(*fingerprint);
let fg = *fingerprint;
Command::perform(
verify_address(device.clone(), self.derivation_index),
move |res| Message::Verified(fg, res),
)
} else {
Command::none()
}
}
_ => Command::none(),
}
}
}

async fn verify_address(
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
index: ChildNumber,
) -> Result<(), Error> {
hw.display_address(&async_hwi::AddressScript::Miniscript {
change: false,
index: index.into(),
})
.await?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -128,10 +287,12 @@ mod tests {
utils::{mock::Daemon, sandbox::Sandbox},
};

use liana::miniscript::bitcoin::Address;
use liana::{descriptors::LianaDescriptor, miniscript::bitcoin::Address};
use serde_json::json;
use std::str::FromStr;

const DESC: &str = "wsh(or_d(multi(2,[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<0;1>/*,[de6eb005/48'/1'/0'/2']tpubDFGuYfS2JwiUSEXiQuNGdT3R7WTDhbaE6jbUhgYSSdhmfQcSx7ZntMPPv7nrkvAqjpj3jX9wbhSGMeKVao4qAzhbNyBi7iQmv5xxQk6H6jz/<0;1>/*),and_v(v:pkh([ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<2;3>/*),older(3))))#p9ax3xxp";

#[tokio::test]
async fn test_receive_panel() {
let addr =
Expand All @@ -140,10 +301,16 @@ mod tests {
.assume_checked();
let daemon = Daemon::new(vec![(
Some(json!({"method": "getnewaddress", "params": Option::<Request>::None})),
Ok(json!(GetAddressResult::new(addr.clone()))),
Ok(json!(GetAddressResult::new(
addr.clone(),
ChildNumber::from_normal_idx(0).unwrap()
))),
)]);

let sandbox: Sandbox<ReceivePanel> = Sandbox::new(ReceivePanel::default());
let sandbox: Sandbox<ReceivePanel> = Sandbox::new(ReceivePanel::new(
PathBuf::new(),
Arc::new(Wallet::new(LianaDescriptor::from_str(DESC).unwrap())),
));
let client = Arc::new(Lianad::new(daemon.run()));
let sandbox = sandbox.load(client, &Cache::default()).await;

Expand Down
37 changes: 37 additions & 0 deletions gui/src/app/view/hw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,40 @@ pub fn hw_list_view_for_registration(
.style(theme::Container::Card(theme::Card::Simple))
.into()
}

pub fn hw_list_view_verify_address(
i: usize,
hw: &HardwareWallet,
chosen: bool,
) -> Element<Message> {
let mut bttn = Button::new(match hw {
HardwareWallet::Supported {
kind,
version,
fingerprint,
alias,
..
} => {
if chosen {
hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
} else {
hw::supported_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
}
}
HardwareWallet::Unsupported { version, kind, .. } => {
hw::unsupported_hardware_wallet(&kind.to_string(), version.as_ref())
}
HardwareWallet::Locked {
kind, pairing_code, ..
} => hw::locked_hardware_wallet(kind, pairing_code.as_ref()),
})
.style(theme::Button::Border)
.width(Length::Fill);
if !chosen && hw.is_supported() {
bttn = bttn.on_press(Message::SelectHardwareWallet(i));
}
Container::new(bttn)
.width(Length::Fill)
.style(theme::Container::Card(theme::Card::Simple))
.into()
}
Loading

0 comments on commit c7c465c

Please sign in to comment.