Merge #346: GUI: hot signer
26ad36f34e28e38a7c205343b6e5376a7f580e6f fix register descriptor prompt (edouard)
9fac6277f42b605200134e6a670cb3511edf80cc Sign with hot signer (edouard)
e76459d159630068f2c0bd9799ce908c30e42d6e Load hot signer in Wallet (edouard)
ecb3e114865b7e502b2a709f4bd2db4752de8976 installer: store mnemonic in datadir (edouard)
7a76754842b7eb688cbc6cd791c412f9006f407d Add title to installer final step (edouard)
c70ac114768a6410e01308bbc69ee4db853947e7 installer: add recover seed step (edouard)
037c4d025d651940ef42637ae5a6b8592dae56cb installer: add signer to participate wallet process (edouard)
233f9b1886078ad2a94704537df8dacfeb5fe06a installer: add backup mnemonic step (edouard)
f8dcd1116dfe820b6de18364b4d7ecaadbe0e7db Add hot signer to create descriptor step (edouard)
1f09653198aefd699afd0e2cc5d26235c3193fb8 Add signer module to gui (edouard)
f3f04a41b00aef014f7f45b3418f5913a4361b8e Use liana hot signer (edouard)
Pull request description:
ACKs for top commit:
darosior:
ACK 26ad36f34e28e38a7c205343b6e5376a7f580e6f -- tested the branch merged on master.
Tree-SHA512: 578be77aff99c40aa21049bfccfb8d310d04ca48213f7af62ec8d5444e044a8375966568e496ff5443a48d2aac648387c410747d12ad15fa21a1bff613d17673
This commit is contained in:
commit
04eb511366
86
gui/Cargo.lock
generated
86
gui/Cargo.lock
generated
@ -216,6 +216,18 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
|
||||
|
||||
[[package]]
|
||||
name = "bip39"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e89470017230c38e52b82b3ee3f530db1856ba1d434e3a67a3456a8a8dec5f"
|
||||
dependencies = [
|
||||
"bitcoin_hashes 0.9.7",
|
||||
"rand_core 0.4.2",
|
||||
"serde",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
@ -239,13 +251,19 @@ checksum = "9cb36de3b18ad25f396f9168302e36fb7e1e8923298ab3127da252d288d5af9d"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bech32",
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.11.0",
|
||||
"core2",
|
||||
"hashbrown 0.8.2",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.11.0"
|
||||
@ -1640,16 +1658,19 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "liana"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/wizardsardine/liana?branch=master#88e5977166373437169ea00e54f365085afd03c0"
|
||||
source = "git+https://github.com/wizardsardine/liana?branch=master#200d3777e0fe89d3b73d296eb31c83b6272dc490"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"base64",
|
||||
"bip39",
|
||||
"dirs",
|
||||
"fern",
|
||||
"getrandom",
|
||||
"jsonrpc",
|
||||
"libc",
|
||||
"log",
|
||||
"miniscript",
|
||||
"rdrand",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1840,6 +1861,12 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
@ -2291,7 +2318,7 @@ dependencies = [
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"smallvec 1.9.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@ -2304,7 +2331,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"smallvec 1.9.0",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
@ -2473,7 +2500,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2483,9 +2510,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
@ -2559,6 +2592,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae028b272a6e99d9f8260ceefa3caa09300a8d6c8d2b2001316474bc52122e9"
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e233b642160555c1aa1ff7a78443c6139342f411b6fa6602af2ebbfee9e166bb"
|
||||
dependencies = [
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.15"
|
||||
@ -2648,7 +2690,7 @@ dependencies = [
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
"smallvec 1.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2671,7 +2713,7 @@ checksum = "44561062e583c4873162861261f16fd1d85fe927c4904d71329a4fe43dc355ef"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytemuck",
|
||||
"smallvec",
|
||||
"smallvec 1.9.0",
|
||||
"ttf-parser 0.12.3",
|
||||
"unicode-bidi-mirroring",
|
||||
"unicode-ccc",
|
||||
@ -2724,7 +2766,7 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7649a0b3ffb32636e60c7ce0d70511eda9c52c658cd0634e194d5a19943aeff"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.11.0",
|
||||
"secp256k1-sys",
|
||||
"serde",
|
||||
]
|
||||
@ -2859,6 +2901,15 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
|
||||
dependencies = [
|
||||
"maybe-uninit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.9.0"
|
||||
@ -3180,6 +3231,15 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09c8070a9942f5e7cfccd93f490fdebd230ee3c3c9f107cb25bad5351ef671cf"
|
||||
dependencies = [
|
||||
"smallvec 0.6.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-script"
|
||||
version = "0.5.4"
|
||||
@ -3372,7 +3432,7 @@ checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e"
|
||||
dependencies = [
|
||||
"nix 0.22.3",
|
||||
"once_cell",
|
||||
"smallvec",
|
||||
"smallvec 1.9.0",
|
||||
"wayland-sys",
|
||||
]
|
||||
|
||||
@ -3453,7 +3513,7 @@ dependencies = [
|
||||
"naga",
|
||||
"parking_lot 0.12.1",
|
||||
"raw-window-handle 0.5.0",
|
||||
"smallvec",
|
||||
"smallvec 1.9.0",
|
||||
"static_assertions",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
@ -3480,7 +3540,7 @@ dependencies = [
|
||||
"parking_lot 0.12.1",
|
||||
"profiling",
|
||||
"raw-window-handle 0.5.0",
|
||||
"smallvec",
|
||||
"smallvec 1.9.0",
|
||||
"thiserror",
|
||||
"web-sys",
|
||||
"wgpu-hal",
|
||||
@ -3518,7 +3578,7 @@ dependencies = [
|
||||
"range-alloc",
|
||||
"raw-window-handle 0.5.0",
|
||||
"renderdoc-sys",
|
||||
"smallvec",
|
||||
"smallvec 1.9.0",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
|
||||
@ -9,6 +9,7 @@ pub enum Error {
|
||||
Daemon(DaemonError),
|
||||
Unexpected(String),
|
||||
HardwareWallet(async_hwi::Error),
|
||||
HotSigner(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
@ -40,6 +41,7 @@ impl std::fmt::Display for Error {
|
||||
},
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
Self::HardwareWallet(e) => write!(f, "{}", e),
|
||||
Self::HotSigner(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,12 +25,11 @@ trait Action {
|
||||
fn warning(&self) -> Option<&Error> {
|
||||
None
|
||||
}
|
||||
fn load(&self, _wallet: &Wallet, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
fn load(&self, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
Command::none()
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
_wallet: &Wallet,
|
||||
_daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
_message: Message,
|
||||
_tx: &mut SpendTx,
|
||||
@ -61,7 +60,7 @@ impl SpendTxState {
|
||||
|
||||
pub fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
if let Some(action) = &self.action {
|
||||
action.load(&self.wallet, daemon)
|
||||
action.load(daemon)
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
@ -82,14 +81,14 @@ impl SpendTxState {
|
||||
self.action = Some(Box::new(DeleteAction::default()));
|
||||
}
|
||||
view::SpendTxMessage::Sign => {
|
||||
let action = SignAction::new();
|
||||
let cmd = action.load(&self.wallet, daemon);
|
||||
let action = SignAction::new(self.wallet.clone());
|
||||
let cmd = action.load(daemon);
|
||||
self.action = Some(Box::new(action));
|
||||
return cmd;
|
||||
}
|
||||
view::SpendTxMessage::EditPsbt => {
|
||||
let action = UpdateAction::new(self.tx.psbt.to_string());
|
||||
let cmd = action.load(&self.wallet, daemon);
|
||||
let action = UpdateAction::new(self.wallet.clone(), self.tx.psbt.to_string());
|
||||
let cmd = action.load(daemon);
|
||||
self.action = Some(Box::new(action));
|
||||
return cmd;
|
||||
}
|
||||
@ -101,19 +100,19 @@ impl SpendTxState {
|
||||
}
|
||||
_ => {
|
||||
if let Some(action) = self.action.as_mut() {
|
||||
return action.update(&self.wallet, daemon.clone(), message, &mut self.tx);
|
||||
return action.update(daemon.clone(), message, &mut self.tx);
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::Updated(Ok(_)) => {
|
||||
self.saved = true;
|
||||
if let Some(action) = self.action.as_mut() {
|
||||
return action.update(&self.wallet, daemon.clone(), message, &mut self.tx);
|
||||
return action.update(daemon.clone(), message, &mut self.tx);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(action) = self.action.as_mut() {
|
||||
return action.update(&self.wallet, daemon.clone(), message, &mut self.tx);
|
||||
return action.update(daemon.clone(), message, &mut self.tx);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -147,7 +146,6 @@ pub struct SaveAction {
|
||||
impl Action for SaveAction {
|
||||
fn update(
|
||||
&mut self,
|
||||
_wallet: &Wallet,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
message: Message,
|
||||
tx: &mut SpendTx,
|
||||
@ -183,7 +181,6 @@ pub struct BroadcastAction {
|
||||
impl Action for BroadcastAction {
|
||||
fn update(
|
||||
&mut self,
|
||||
_wallet: &Wallet,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
message: Message,
|
||||
tx: &mut SpendTx,
|
||||
@ -227,7 +224,6 @@ pub struct DeleteAction {
|
||||
impl Action for DeleteAction {
|
||||
fn update(
|
||||
&mut self,
|
||||
_wallet: &Wallet,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
message: Message,
|
||||
tx: &mut SpendTx,
|
||||
@ -260,6 +256,7 @@ impl Action for DeleteAction {
|
||||
}
|
||||
|
||||
pub struct SignAction {
|
||||
wallet: Arc<Wallet>,
|
||||
chosen_hw: Option<usize>,
|
||||
processing: bool,
|
||||
hws: Vec<HardwareWallet>,
|
||||
@ -268,8 +265,9 @@ pub struct SignAction {
|
||||
}
|
||||
|
||||
impl SignAction {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(wallet: Arc<Wallet>) -> Self {
|
||||
Self {
|
||||
wallet,
|
||||
chosen_hw: None,
|
||||
processing: false,
|
||||
hws: Vec::new(),
|
||||
@ -284,13 +282,14 @@ impl Action for SignAction {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
fn load(&self, wallet: &Wallet, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
let wallet = wallet.clone();
|
||||
Command::perform(list_hws(wallet), Message::ConnectedHardwareWallets)
|
||||
fn load(&self, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
Command::perform(
|
||||
list_hws(self.wallet.clone()),
|
||||
Message::ConnectedHardwareWallets,
|
||||
)
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
wallet: &Wallet,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
message: Message,
|
||||
tx: &mut SpendTx,
|
||||
@ -312,6 +311,13 @@ impl Action for SignAction {
|
||||
);
|
||||
}
|
||||
}
|
||||
Message::View(view::Message::Spend(view::SpendTxMessage::SelectHotSigner)) => {
|
||||
self.processing = true;
|
||||
return Command::perform(
|
||||
sign_psbt_with_hot_signer(self.wallet.clone(), tx.psbt.clone()),
|
||||
Message::Signed,
|
||||
);
|
||||
}
|
||||
Message::Signed(res) => match res {
|
||||
Err(e) => self.error = Some(e),
|
||||
Ok((psbt, fingerprint)) => {
|
||||
@ -328,7 +334,11 @@ impl Action for SignAction {
|
||||
Message::Updated(res) => match res {
|
||||
Ok(()) => {
|
||||
self.processing = false;
|
||||
tx.sigs = wallet.main_descriptor.partial_spend_info(&tx.psbt).unwrap();
|
||||
tx.sigs = self
|
||||
.wallet
|
||||
.main_descriptor
|
||||
.partial_spend_info(&tx.psbt)
|
||||
.unwrap();
|
||||
}
|
||||
Err(e) => self.error = Some(e),
|
||||
},
|
||||
@ -348,7 +358,7 @@ impl Action for SignAction {
|
||||
self.hws = Vec::new();
|
||||
self.chosen_hw = None;
|
||||
self.error = None;
|
||||
return self.load(wallet, daemon);
|
||||
return self.load(daemon);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@ -358,6 +368,7 @@ impl Action for SignAction {
|
||||
view::spend::detail::sign_action(
|
||||
self.error.as_ref(),
|
||||
&self.hws,
|
||||
self.wallet.signer.as_ref().map(|s| s.fingerprint()),
|
||||
self.processing,
|
||||
self.chosen_hw,
|
||||
&self.signed,
|
||||
@ -365,7 +376,7 @@ impl Action for SignAction {
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_hws(wallet: Wallet) -> Vec<HardwareWallet> {
|
||||
async fn list_hws(wallet: Arc<Wallet>) -> Vec<HardwareWallet> {
|
||||
list_hardware_wallets(
|
||||
&wallet.hardware_wallets,
|
||||
Some((&wallet.name, &wallet.main_descriptor.to_string())),
|
||||
@ -373,6 +384,20 @@ async fn list_hws(wallet: Wallet) -> Vec<HardwareWallet> {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sign_psbt_with_hot_signer(
|
||||
wallet: Arc<Wallet>,
|
||||
psbt: Psbt,
|
||||
) -> Result<(Psbt, Fingerprint), Error> {
|
||||
if let Some(signer) = &wallet.signer {
|
||||
let psbt = signer
|
||||
.sign_psbt(psbt)
|
||||
.map_err(|e| Error::HotSigner(format!("Hot signer failed to sign psbt: {}", e)))?;
|
||||
Ok((psbt, signer.fingerprint()))
|
||||
} else {
|
||||
Err(Error::HotSigner("Hot signer not loaded".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn sign_psbt(
|
||||
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
|
||||
fingerprint: Fingerprint,
|
||||
@ -383,6 +408,7 @@ async fn sign_psbt(
|
||||
}
|
||||
|
||||
pub struct UpdateAction {
|
||||
wallet: Arc<Wallet>,
|
||||
psbt: String,
|
||||
updated: form::Value<String>,
|
||||
processing: bool,
|
||||
@ -391,8 +417,9 @@ pub struct UpdateAction {
|
||||
}
|
||||
|
||||
impl UpdateAction {
|
||||
pub fn new(psbt: String) -> Self {
|
||||
pub fn new(wallet: Arc<Wallet>, psbt: String) -> Self {
|
||||
Self {
|
||||
wallet,
|
||||
psbt,
|
||||
updated: form::Value::default(),
|
||||
processing: false,
|
||||
@ -418,7 +445,6 @@ impl Action for UpdateAction {
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
wallet: &Wallet,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
message: Message,
|
||||
tx: &mut SpendTx,
|
||||
@ -455,7 +481,11 @@ impl Action for UpdateAction {
|
||||
.extend(updated_input.partial_sigs.clone().into_iter());
|
||||
}
|
||||
}
|
||||
tx.sigs = wallet.main_descriptor.partial_spend_info(&tx.psbt).unwrap();
|
||||
tx.sigs = self
|
||||
.wallet
|
||||
.main_descriptor
|
||||
.partial_spend_info(&tx.psbt)
|
||||
.unwrap();
|
||||
}
|
||||
Err(e) => self.error = e.into(),
|
||||
}
|
||||
|
||||
@ -74,12 +74,11 @@ pub fn hw_list_view(
|
||||
})
|
||||
.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)),
|
||||
),
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(5)
|
||||
.push(icon::circle_check_icon().style(color::SUCCESS))
|
||||
.push(text("Signed").style(color::SUCCESS)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
|
||||
@ -40,6 +40,7 @@ pub enum SpendTxMessage {
|
||||
Save,
|
||||
Confirm,
|
||||
Cancel,
|
||||
SelectHotSigner,
|
||||
SelectHardwareWallet(usize),
|
||||
EditPsbt,
|
||||
PsbtEdited(String),
|
||||
|
||||
@ -707,6 +707,7 @@ pub fn inputs_and_outputs_view<'a>(
|
||||
pub fn sign_action<'a>(
|
||||
warning: Option<&Error>,
|
||||
hws: &'a [HardwareWallet],
|
||||
signer: Option<Fingerprint>,
|
||||
processing: bool,
|
||||
chosen_hw: Option<usize>,
|
||||
signed: &[Fingerprint],
|
||||
@ -715,12 +716,12 @@ pub fn sign_action<'a>(
|
||||
.push_maybe(warning.map(|w| warn(Some(w))))
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.push(if !hws.is_empty() {
|
||||
.push(
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
text("Select hardware wallet to sign with:")
|
||||
text("Select signing device to sign with:")
|
||||
.bold()
|
||||
.width(Length::Fill),
|
||||
)
|
||||
@ -742,19 +743,42 @@ pub fn sign_action<'a>(
|
||||
))
|
||||
},
|
||||
))
|
||||
.width(Length::Fill)
|
||||
} else {
|
||||
Column::new()
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(15)
|
||||
.width(Length::Fill)
|
||||
.push("Please connect a hardware wallet")
|
||||
.push(button::border(None, "Refresh").on_press(Message::Reload))
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
})
|
||||
.push_maybe(signer.map(|fingerprint| {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Column::new()
|
||||
.width(Length::Fill)
|
||||
.push(text("This computer").bold())
|
||||
.push(
|
||||
text(format!("fingerprint: {}", fingerprint))
|
||||
.small(),
|
||||
)
|
||||
.spacing(5)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push_maybe(if signed.contains(&fingerprint) {
|
||||
Some(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(5)
|
||||
.push(
|
||||
icon::circle_check_icon().style(color::SUCCESS),
|
||||
)
|
||||
.push(text("Signed").style(color::SUCCESS)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
.on_press(Message::Spend(SpendTxMessage::SelectHotSigner))
|
||||
.padding(10)
|
||||
.style(button::Style::Border.into())
|
||||
.width(Length::Fill)
|
||||
}))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.spacing(20)
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center),
|
||||
|
||||
@ -37,6 +37,7 @@ impl From<&Error> for WarningMessage {
|
||||
},
|
||||
Error::Unexpected(_) => WarningMessage("Unknown error".to_string()),
|
||||
Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()),
|
||||
Error::HotSigner(_) => WarningMessage("Hot signer error".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::hw::HardwareWalletConfig;
|
||||
use crate::{hw::HardwareWalletConfig, signer::Signer};
|
||||
|
||||
use liana::descriptors::MultipathDescriptor;
|
||||
use liana::miniscript::bitcoin::util::bip32::Fingerprint;
|
||||
|
||||
pub const DEFAULT_WALLET_NAME: &str = "Liana";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub name: String,
|
||||
pub main_descriptor: MultipathDescriptor,
|
||||
pub keys_aliases: HashMap<Fingerprint, String>,
|
||||
pub hardware_wallets: Vec<HardwareWalletConfig>,
|
||||
pub signer: Option<Signer>,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
@ -22,6 +23,7 @@ impl Wallet {
|
||||
main_descriptor,
|
||||
keys_aliases: HashMap::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
signer: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +33,7 @@ impl Wallet {
|
||||
main_descriptor,
|
||||
keys_aliases: HashMap::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
signer: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,8 +42,25 @@ impl Wallet {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_harware_wallets(mut self, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
|
||||
pub fn with_hardware_wallets(mut self, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
|
||||
self.hardware_wallets = hardware_wallets;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_signer(mut self, signer: Signer) -> Self {
|
||||
self.signer = Some(signer);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn descriptor_keys(&self) -> HashSet<Fingerprint> {
|
||||
let info = self.main_descriptor.info();
|
||||
let mut descriptor_keys = HashSet::new();
|
||||
for (fingerprint, _) in info.primary_path().thresh_origins().1.iter() {
|
||||
descriptor_keys.insert(*fingerprint);
|
||||
}
|
||||
for (fingerprint, _) in info.recovery_path().1.thresh_origins().1.iter() {
|
||||
descriptor_keys.insert(*fingerprint);
|
||||
}
|
||||
descriptor_keys
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
@ -7,6 +8,7 @@ use crate::{
|
||||
wallet::DEFAULT_WALLET_NAME,
|
||||
},
|
||||
hw::HardwareWalletConfig,
|
||||
signer::Signer,
|
||||
};
|
||||
use async_hwi::DeviceKind;
|
||||
use liana::{
|
||||
@ -28,6 +30,7 @@ pub struct Context {
|
||||
Option<[u8; 32]>,
|
||||
)>,
|
||||
pub data_dir: PathBuf,
|
||||
pub signer: Option<Arc<Signer>>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@ -42,6 +45,7 @@ impl Context {
|
||||
bitcoind_config: None,
|
||||
descriptor: None,
|
||||
data_dir,
|
||||
signer: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,11 +16,13 @@ pub enum Message {
|
||||
Exit(PathBuf),
|
||||
Clibpboard(String),
|
||||
Next,
|
||||
Skip,
|
||||
Previous,
|
||||
Install,
|
||||
Close,
|
||||
Reload,
|
||||
Select(usize),
|
||||
UseHotSigner,
|
||||
Installed(Result<PathBuf, Error>),
|
||||
Network(Network),
|
||||
DefineBitcoind(DefineBitcoind),
|
||||
@ -28,6 +30,8 @@ pub enum Message {
|
||||
ImportXpub(usize, Result<DescriptorPublicKey, Error>),
|
||||
ConnectedHardwareWallets(Vec<HardwareWallet>),
|
||||
WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>),
|
||||
MnemonicWord(usize, String),
|
||||
ImportMnemonic(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@ -15,8 +15,8 @@ use crate::app::{config as gui_config, settings as gui_settings};
|
||||
|
||||
pub use message::Message;
|
||||
use step::{
|
||||
BackupDescriptor, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, ParticipateXpub,
|
||||
RegisterDescriptor, Step, Welcome,
|
||||
BackupDescriptor, BackupMnemonic, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor,
|
||||
ParticipateXpub, RecoverMnemonic, RegisterDescriptor, Step, Welcome,
|
||||
};
|
||||
|
||||
pub struct Installer {
|
||||
@ -91,6 +91,7 @@ impl Installer {
|
||||
self.steps = vec![
|
||||
Welcome::default().into(),
|
||||
DefineDescriptor::new().into(),
|
||||
BackupMnemonic::default().into(),
|
||||
BackupDescriptor::default().into(),
|
||||
RegisterDescriptor::default().into(),
|
||||
DefineBitcoind::new().into(),
|
||||
@ -103,6 +104,7 @@ impl Installer {
|
||||
Welcome::default().into(),
|
||||
ParticipateXpub::new().into(),
|
||||
ImportDescriptor::new(false).into(),
|
||||
BackupMnemonic::default().into(),
|
||||
BackupDescriptor::default().into(),
|
||||
RegisterDescriptor::default().into(),
|
||||
DefineBitcoind::new().into(),
|
||||
@ -114,6 +116,7 @@ impl Installer {
|
||||
self.steps = vec![
|
||||
Welcome::default().into(),
|
||||
ImportDescriptor::new(true).into(),
|
||||
RecoverMnemonic::default().into(),
|
||||
RegisterDescriptor::default().into(),
|
||||
DefineBitcoind::new().into(),
|
||||
Final::new().into(),
|
||||
@ -165,11 +168,28 @@ impl Installer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Some steps are skipped because of contextual choice of the user, this
|
||||
/// code is giving a correct progress summary to the user.
|
||||
fn progress(&self) -> (usize, usize) {
|
||||
let mut current = self.current;
|
||||
let mut total = 0;
|
||||
for (i, step) in self.steps.iter().enumerate() {
|
||||
if step.skip(&self.context) {
|
||||
if i < self.current {
|
||||
current -= 1;
|
||||
}
|
||||
} else {
|
||||
total += 1
|
||||
}
|
||||
}
|
||||
(current, total - 1)
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
self.steps
|
||||
.get(self.current)
|
||||
.expect("There is always a step")
|
||||
.view((self.current, self.steps.len() - 1))
|
||||
.view(self.progress())
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,7 +232,18 @@ pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
|
||||
daemon_config.to_string().as_bytes(),
|
||||
)?;
|
||||
|
||||
log::info!("Daemon config file created");
|
||||
log::info!("Daemon configuration file created");
|
||||
|
||||
if let Some(signer) = &ctx.signer {
|
||||
signer
|
||||
.store(
|
||||
&cfg.data_dir().expect("Already checked"),
|
||||
cfg.bitcoin_config.network,
|
||||
)
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?;
|
||||
|
||||
log::info!("Hot signer mnemonic stored");
|
||||
}
|
||||
|
||||
// create liana GUI configuration file
|
||||
let gui_config_path = create_and_write_file(
|
||||
@ -227,7 +258,7 @@ pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
log::info!("Gui config file created");
|
||||
log::info!("Gui configuration file created");
|
||||
|
||||
// create liana GUI settings file
|
||||
let settings: gui_settings::Settings = ctx.extract_gui_settings();
|
||||
|
||||
@ -6,4 +6,6 @@ pub const DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP: &str =
|
||||
"Number of blocks after a coin is received \nfor which the recovery path is not available";
|
||||
pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str =
|
||||
"The alias is applied on all the keys derived from the same seed";
|
||||
pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a device needs the descriptor. Registration on a device is not a substitute for backing up the descriptor.";
|
||||
pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a device needs the descriptor. If the descriptor contains one or more keys imported from an external signing device, the descriptor must be registered on it. Registration on a device is not a substitute for backing up the descriptor.";
|
||||
pub const MNEMONIC_HELP: &str = "A hot key generated on this computer was used for creating this wallet. It needs to be backed up. \n Keep it in a safe place. Never share it with anyone.";
|
||||
pub const RECOVER_MNEMONIC_HELP: &str = "If you were using a hot key (a key stored on the computer) in your wallet, you will need to recover it from mnemonics to be able to sign transactions again. Otherwise you can directly go the next step.";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use iced::{Command, Element};
|
||||
use liana::{
|
||||
@ -24,6 +25,7 @@ use crate::{
|
||||
step::{Context, Step},
|
||||
view, Error,
|
||||
},
|
||||
signer::Signer,
|
||||
ui::component::{form, modal::Modal},
|
||||
};
|
||||
|
||||
@ -47,6 +49,7 @@ pub struct DefineDescriptor {
|
||||
recovery_threshold: usize,
|
||||
sequence: form::Value<String>,
|
||||
modal: Option<Box<dyn DescriptorKeyModal>>,
|
||||
signer: Arc<Signer>,
|
||||
|
||||
error: Option<String>,
|
||||
}
|
||||
@ -63,6 +66,7 @@ impl DefineDescriptor {
|
||||
recovery_threshold: 1,
|
||||
sequence: form::Value::default(),
|
||||
modal: None,
|
||||
signer: Arc::new(Signer::generate(Network::Bitcoin).unwrap()),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
@ -75,6 +79,23 @@ impl DefineDescriptor {
|
||||
&& !self.spending_keys.iter().any(|k| k.key.is_none())
|
||||
}
|
||||
|
||||
fn set_network(&mut self, network: Network) {
|
||||
self.network = network;
|
||||
if let Some(signer) = Arc::get_mut(&mut self.signer) {
|
||||
signer.set_network(network);
|
||||
}
|
||||
if let Some(mut network_datadir) = self.data_dir.clone() {
|
||||
network_datadir.push(self.network.to_string());
|
||||
self.network_valid = !network_datadir.exists();
|
||||
}
|
||||
for key in self.spending_keys.iter_mut() {
|
||||
key.check_network(self.network);
|
||||
}
|
||||
for key in self.recovery_keys.iter_mut() {
|
||||
key.check_network(self.network);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Improve algo
|
||||
// Mark as duplicate every defined key that have the same name but not the same fingerprint.
|
||||
// And every undefined_key that have a same name than an other key.
|
||||
@ -201,18 +222,7 @@ impl Step for DefineDescriptor {
|
||||
Message::Close => {
|
||||
self.modal = None;
|
||||
}
|
||||
Message::Network(network) => {
|
||||
self.network = network;
|
||||
let mut network_datadir = self.data_dir.clone().unwrap();
|
||||
network_datadir.push(self.network.to_string());
|
||||
self.network_valid = !network_datadir.exists();
|
||||
for key in self.spending_keys.iter_mut() {
|
||||
key.check_network(self.network);
|
||||
}
|
||||
for key in self.recovery_keys.iter_mut() {
|
||||
key.check_network(self.network);
|
||||
}
|
||||
}
|
||||
Message::Network(network) => self.set_network(network),
|
||||
Message::DefineDescriptor(msg) => {
|
||||
match msg {
|
||||
message::DefineDescriptor::ThresholdEdited(is_recovery, value) => {
|
||||
@ -263,42 +273,30 @@ impl Step for DefineDescriptor {
|
||||
message::DefineKey::Edit => {
|
||||
if is_recovery {
|
||||
if let Some(recovery_key) = self.recovery_keys.get(i) {
|
||||
let name = recovery_key.name.clone();
|
||||
let key = recovery_key
|
||||
.key
|
||||
.as_ref()
|
||||
.map(|k| {
|
||||
k.to_string().trim_end_matches("/<0;1>/*").to_string()
|
||||
})
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let modal = EditXpubModal::new(
|
||||
name,
|
||||
key,
|
||||
recovery_key.name.clone(),
|
||||
recovery_key.key.as_ref(),
|
||||
i,
|
||||
is_recovery,
|
||||
self.network,
|
||||
self.fingerprint_account_index_mappping(),
|
||||
self.keys_aliases(),
|
||||
self.signer.clone(),
|
||||
);
|
||||
let cmd = modal.load();
|
||||
self.modal = Some(Box::new(modal));
|
||||
return cmd;
|
||||
}
|
||||
} else if let Some(spending_key) = self.spending_keys.get(i) {
|
||||
let name = spending_key.name.clone();
|
||||
let key = spending_key
|
||||
.key
|
||||
.as_ref()
|
||||
.map(|k| k.to_string().trim_end_matches("/<0;1>/*").to_string())
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let modal = EditXpubModal::new(
|
||||
name,
|
||||
key,
|
||||
spending_key.name.clone(),
|
||||
spending_key.key.as_ref(),
|
||||
i,
|
||||
is_recovery,
|
||||
self.network,
|
||||
self.fingerprint_account_index_mappping(),
|
||||
self.keys_aliases(),
|
||||
self.signer.clone(),
|
||||
);
|
||||
let cmd = modal.load();
|
||||
self.modal = Some(Box::new(modal));
|
||||
@ -337,16 +335,14 @@ impl Step for DefineDescriptor {
|
||||
}
|
||||
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
self.network = ctx.bitcoin_config.network;
|
||||
self.data_dir = Some(ctx.data_dir.clone());
|
||||
let mut network_datadir = ctx.data_dir.clone();
|
||||
network_datadir.push(self.network.to_string());
|
||||
self.network_valid = !network_datadir.exists();
|
||||
self.set_network(ctx.bitcoin_config.network)
|
||||
}
|
||||
|
||||
fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
ctx.bitcoin_config.network = self.network;
|
||||
ctx.keys = Vec::new();
|
||||
let mut signer_is_used = false;
|
||||
let mut spending_keys: Vec<DescriptorPublicKey> = Vec::new();
|
||||
for spending_key in self.spending_keys.iter().clone() {
|
||||
if let Some(key) = spending_key.key.as_ref() {
|
||||
@ -371,6 +367,9 @@ impl Step for DefineDescriptor {
|
||||
master_fingerprint,
|
||||
name: recovery_key.name.clone(),
|
||||
});
|
||||
if master_fingerprint == self.signer.fingerprint() {
|
||||
signer_is_used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
recovery_keys.push(key.clone());
|
||||
@ -421,6 +420,9 @@ impl Step for DefineDescriptor {
|
||||
};
|
||||
|
||||
ctx.descriptor = Some(desc);
|
||||
if signer_is_used {
|
||||
ctx.signer = Some(self.signer.clone());
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@ -555,17 +557,22 @@ pub struct EditXpubModal {
|
||||
|
||||
chosen_hw: Option<usize>,
|
||||
hws: Vec<HardwareWallet>,
|
||||
|
||||
signer: Arc<Signer>,
|
||||
chosen_signer: bool,
|
||||
}
|
||||
|
||||
impl EditXpubModal {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
name: String,
|
||||
key: String,
|
||||
key: Option<&DescriptorPublicKey>,
|
||||
key_index: usize,
|
||||
is_recovery: bool,
|
||||
network: Network,
|
||||
account_indexes: HashMap<Fingerprint, ChildNumber>,
|
||||
keys_aliases: HashMap<Fingerprint, String>,
|
||||
signer: Arc<Signer>,
|
||||
) -> Self {
|
||||
Self {
|
||||
form_name: form::Value {
|
||||
@ -574,7 +581,9 @@ impl EditXpubModal {
|
||||
},
|
||||
form_xpub: form::Value {
|
||||
valid: true,
|
||||
value: key,
|
||||
value: key
|
||||
.map(|k| k.to_string().trim_end_matches("/<0;1>/*").to_string())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
},
|
||||
keys_aliases,
|
||||
account_indexes,
|
||||
@ -586,6 +595,8 @@ impl EditXpubModal {
|
||||
error: None,
|
||||
network,
|
||||
edit_name: false,
|
||||
chosen_signer: Some(signer.fingerprint()) == key.map(|k| k.master_fingerprint()),
|
||||
signer,
|
||||
}
|
||||
}
|
||||
fn load(&self) -> Command<Message> {
|
||||
@ -622,8 +633,7 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
get_extended_pubkey(
|
||||
device.clone(),
|
||||
*fingerprint,
|
||||
self.network,
|
||||
account_index,
|
||||
generate_derivation_path(self.network, account_index),
|
||||
),
|
||||
|res| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported(
|
||||
@ -635,11 +645,45 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
}
|
||||
Message::ConnectedHardwareWallets(hws) => {
|
||||
self.hws = hws;
|
||||
if let Ok(key) =
|
||||
DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", self.form_xpub.value))
|
||||
{
|
||||
self.chosen_hw = self
|
||||
.hws
|
||||
.iter()
|
||||
.position(|hw| hw.fingerprint() == Some(key.master_fingerprint()));
|
||||
}
|
||||
}
|
||||
Message::Reload => {
|
||||
self.hws = Vec::new();
|
||||
return self.load();
|
||||
}
|
||||
Message::UseHotSigner => {
|
||||
self.chosen_hw = None;
|
||||
self.chosen_signer = true;
|
||||
self.form_xpub.valid = true;
|
||||
let fingerprint = self.signer.fingerprint();
|
||||
if let Some(alias) = self.keys_aliases.get(&fingerprint) {
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value = alias.clone();
|
||||
self.edit_name = false;
|
||||
} else {
|
||||
self.edit_name = true;
|
||||
self.form_name.value = String::new();
|
||||
}
|
||||
let account_index = self
|
||||
.account_indexes
|
||||
.get(&fingerprint)
|
||||
.map(|account_index| account_index.increment().unwrap())
|
||||
.unwrap_or_else(|| ChildNumber::from_hardened_idx(0).unwrap());
|
||||
let derivation_path = generate_derivation_path(self.network, account_index);
|
||||
self.form_xpub.value = format!(
|
||||
"[{}{}]{}",
|
||||
fingerprint,
|
||||
derivation_path.to_string().trim_start_matches('m'),
|
||||
self.signer.get_extended_pubkey(&derivation_path)
|
||||
);
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported(res)) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
@ -651,11 +695,13 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
} else {
|
||||
self.edit_name = true;
|
||||
}
|
||||
self.chosen_signer = false;
|
||||
self.form_xpub.valid = true;
|
||||
self.form_xpub.value =
|
||||
key.to_string().trim_end_matches("/<0;1>/*").to_string();
|
||||
}
|
||||
Err(e) => {
|
||||
self.chosen_hw = None;
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
@ -719,6 +765,7 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
self.error.as_ref(),
|
||||
self.processing,
|
||||
self.chosen_hw,
|
||||
self.chosen_signer,
|
||||
&self.form_xpub,
|
||||
&self.form_name,
|
||||
self.edit_name,
|
||||
@ -726,22 +773,24 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
}
|
||||
}
|
||||
|
||||
/// LIANA_STANDARD_PATH: m/48'/0'/0'/2';
|
||||
/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2';
|
||||
async fn get_extended_pubkey(
|
||||
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
|
||||
fingerprint: Fingerprint,
|
||||
network: Network,
|
||||
account_index: ChildNumber,
|
||||
) -> Result<DescriptorPublicKey, Error> {
|
||||
let derivation_path = DerivationPath::from_str(&{
|
||||
fn generate_derivation_path(network: Network, account_index: ChildNumber) -> DerivationPath {
|
||||
DerivationPath::from_str(&{
|
||||
if network == Network::Bitcoin {
|
||||
format!("m/48'/0'/{}/2'", account_index)
|
||||
} else {
|
||||
format!("m/48'/1'/{}/2'", account_index)
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// LIANA_STANDARD_PATH: m/48'/0'/0'/2';
|
||||
/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2';
|
||||
async fn get_extended_pubkey(
|
||||
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
|
||||
fingerprint: Fingerprint,
|
||||
derivation_path: DerivationPath,
|
||||
) -> Result<DescriptorPublicKey, Error> {
|
||||
let xkey = hw
|
||||
.get_extended_pubkey(&derivation_path, false)
|
||||
.await
|
||||
@ -792,6 +841,12 @@ impl HardwareWalletXpubs {
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.error = None;
|
||||
self.next_account = ChildNumber::from_hardened_idx(0).unwrap();
|
||||
self.xpubs = Vec::new();
|
||||
}
|
||||
|
||||
fn select(&mut self, i: usize, network: Network) -> Command<Message> {
|
||||
if let HardwareWallet::Supported {
|
||||
device,
|
||||
@ -803,12 +858,12 @@ impl HardwareWalletXpubs {
|
||||
let fingerprint = *fingerprint;
|
||||
self.processing = true;
|
||||
self.error = None;
|
||||
let next_account = self.next_account;
|
||||
let derivation_path = generate_derivation_path(network, self.next_account);
|
||||
Command::perform(
|
||||
async move {
|
||||
(
|
||||
i,
|
||||
get_extended_pubkey(device, fingerprint, network, next_account).await,
|
||||
get_extended_pubkey(device, fingerprint, derivation_path).await,
|
||||
)
|
||||
},
|
||||
|(i, res)| Message::ImportXpub(i, res),
|
||||
@ -829,6 +884,42 @@ impl HardwareWalletXpubs {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SignerXpubs {
|
||||
signer: Arc<Signer>,
|
||||
xpubs: Vec<String>,
|
||||
next_account: ChildNumber,
|
||||
}
|
||||
|
||||
impl SignerXpubs {
|
||||
fn new(signer: Arc<Signer>) -> Self {
|
||||
Self {
|
||||
signer,
|
||||
xpubs: Vec::new(),
|
||||
next_account: ChildNumber::from_hardened_idx(0).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.xpubs = Vec::new();
|
||||
self.next_account = ChildNumber::from_hardened_idx(0).unwrap();
|
||||
}
|
||||
|
||||
fn select(&mut self, network: Network) {
|
||||
let derivation_path = generate_derivation_path(network, self.next_account);
|
||||
self.next_account = self.next_account.increment().unwrap();
|
||||
self.xpubs.push(format!(
|
||||
"[{}{}]{}/<0;1>/*",
|
||||
self.signer.fingerprint(),
|
||||
derivation_path.to_string().trim_start_matches('m'),
|
||||
self.signer.get_extended_pubkey(&derivation_path)
|
||||
));
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
view::signer_xpubs(&self.xpubs)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ParticipateXpub {
|
||||
network: Network,
|
||||
network_valid: bool,
|
||||
@ -837,6 +928,7 @@ pub struct ParticipateXpub {
|
||||
shared: bool,
|
||||
|
||||
xpubs_hw: Vec<HardwareWalletXpubs>,
|
||||
xpubs_signer: SignerXpubs,
|
||||
}
|
||||
|
||||
impl ParticipateXpub {
|
||||
@ -847,8 +939,30 @@ impl ParticipateXpub {
|
||||
data_dir: None,
|
||||
xpubs_hw: Vec::new(),
|
||||
shared: false,
|
||||
xpubs_signer: SignerXpubs::new(Arc::new(Signer::generate(Network::Bitcoin).unwrap())),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_network(&mut self, network: Network) {
|
||||
if network != self.network {
|
||||
self.xpubs_hw.iter_mut().for_each(|hw| hw.reset());
|
||||
self.xpubs_signer.reset();
|
||||
}
|
||||
self.network = network;
|
||||
if let Some(signer) = Arc::get_mut(&mut self.xpubs_signer.signer) {
|
||||
signer.set_network(network);
|
||||
}
|
||||
if let Some(mut network_datadir) = self.data_dir.clone() {
|
||||
network_datadir.push(self.network.to_string());
|
||||
self.network_valid = !network_datadir.exists();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ParticipateXpub {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for ParticipateXpub {
|
||||
@ -857,10 +971,7 @@ impl Step for ParticipateXpub {
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Network(network) => {
|
||||
self.network = network;
|
||||
let mut network_datadir = self.data_dir.clone().unwrap();
|
||||
network_datadir.push(self.network.to_string());
|
||||
self.network_valid = !network_datadir.exists();
|
||||
self.set_network(network);
|
||||
}
|
||||
Message::UserActionDone(shared) => self.shared = shared,
|
||||
Message::ImportXpub(i, res) => {
|
||||
@ -868,6 +979,9 @@ impl Step for ParticipateXpub {
|
||||
hw.update(res);
|
||||
}
|
||||
}
|
||||
Message::UseHotSigner => {
|
||||
self.xpubs_signer.select(self.network);
|
||||
}
|
||||
Message::Select(i) => {
|
||||
if let Some(hw) = self.xpubs_hw.get_mut(i) {
|
||||
return hw.select(i, self.network);
|
||||
@ -894,11 +1008,8 @@ impl Step for ParticipateXpub {
|
||||
}
|
||||
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
self.network = ctx.bitcoin_config.network;
|
||||
self.data_dir = Some(ctx.data_dir.clone());
|
||||
let mut network_datadir = ctx.data_dir.clone();
|
||||
network_datadir.push(self.network.to_string());
|
||||
self.network_valid = !network_datadir.exists();
|
||||
self.set_network(ctx.bitcoin_config.network);
|
||||
}
|
||||
|
||||
fn load(&self) -> Command<Message> {
|
||||
@ -912,6 +1023,11 @@ impl Step for ParticipateXpub {
|
||||
ctx.bitcoin_config.network = self.network;
|
||||
// Drop connections to hardware wallets.
|
||||
self.xpubs_hw = Vec::new();
|
||||
if !self.xpubs_signer.xpubs.is_empty() {
|
||||
ctx.signer = Some(self.xpubs_signer.signer.clone());
|
||||
} else {
|
||||
ctx.signer = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@ -925,17 +1041,12 @@ impl Step for ParticipateXpub {
|
||||
.enumerate()
|
||||
.map(|(i, hw)| hw.view(i))
|
||||
.collect(),
|
||||
self.xpubs_signer.view(),
|
||||
self.shared,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ParticipateXpub {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParticipateXpub> for Box<dyn Step> {
|
||||
fn from(s: ParticipateXpub) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
|
||||
161
gui/src/installer/step/mnemonic.rs
Normal file
161
gui/src/installer/step/mnemonic.rs
Normal file
@ -0,0 +1,161 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use iced::{Command, Element};
|
||||
use liana::{bip39, signer::HotSigner};
|
||||
|
||||
use crate::{
|
||||
installer::{context::Context, message::Message, step::Step, view},
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BackupMnemonic {
|
||||
words: [&'static str; 12],
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl From<BackupMnemonic> for Box<dyn Step> {
|
||||
fn from(s: BackupMnemonic) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for BackupMnemonic {
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
if let Some(signer) = &ctx.signer {
|
||||
self.words = signer.mnemonic();
|
||||
}
|
||||
}
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
if let Message::UserActionDone(done) = message {
|
||||
self.done = done;
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
ctx.signer.is_none()
|
||||
}
|
||||
fn view(&self, progress: (usize, usize)) -> Element<Message> {
|
||||
view::backup_mnemonic(progress, &self.words, self.done)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecoverMnemonic {
|
||||
language: bip39::Language,
|
||||
words: [(String, bool); 12],
|
||||
current: usize,
|
||||
suggestions: Vec<String>,
|
||||
error: Option<String>,
|
||||
skip: bool,
|
||||
recover: bool,
|
||||
}
|
||||
|
||||
impl Default for RecoverMnemonic {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: bip39::Language::English,
|
||||
words: Default::default(),
|
||||
current: 0,
|
||||
suggestions: Vec::new(),
|
||||
error: None,
|
||||
skip: false,
|
||||
recover: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecoverMnemonic> for Box<dyn Step> {
|
||||
fn from(s: RecoverMnemonic) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for RecoverMnemonic {
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::MnemonicWord(index, value) => {
|
||||
if let Some((word, valid)) = self.words.get_mut(index) {
|
||||
if value.len() >= 3 {
|
||||
let suggestions = self.language.words_by_prefix(&value);
|
||||
if suggestions.contains(&value.as_ref()) {
|
||||
*valid = true;
|
||||
self.suggestions = Vec::new();
|
||||
} else {
|
||||
self.suggestions = suggestions.iter().map(|s| s.to_string()).collect();
|
||||
*valid = false;
|
||||
}
|
||||
} else {
|
||||
self.suggestions = Vec::new();
|
||||
*valid = false;
|
||||
}
|
||||
self.current = index;
|
||||
*word = value;
|
||||
}
|
||||
}
|
||||
Message::ImportMnemonic(recover) => self.recover = recover,
|
||||
Message::Skip => {
|
||||
self.skip = true;
|
||||
return Command::perform(async {}, |_| Message::Next);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
if self.skip {
|
||||
// If the user click previous, we dont want the skip to be set to true.
|
||||
self.skip = false;
|
||||
ctx.signer = None;
|
||||
return true;
|
||||
}
|
||||
|
||||
let words: Vec<String> = self
|
||||
.words
|
||||
.iter()
|
||||
.filter_map(|(s, valid)| if *valid { Some(s.clone()) } else { None })
|
||||
.collect();
|
||||
|
||||
let seed = match HotSigner::from_str(ctx.bitcoin_config.network, &words.join(" ")) {
|
||||
Ok(seed) => seed,
|
||||
Err(e) => {
|
||||
self.error = Some(e.to_string());
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let signer = Signer::new(seed);
|
||||
let fingerprint = signer.fingerprint();
|
||||
|
||||
if let Some(descriptor) = &ctx.descriptor {
|
||||
let info = descriptor.info();
|
||||
let mut descriptor_keys = HashSet::new();
|
||||
for (fingerprint, _) in info.primary_path().thresh_origins().1.iter() {
|
||||
descriptor_keys.insert(*fingerprint);
|
||||
}
|
||||
for (fingerprint, _) in info.recovery_path().1.thresh_origins().1.iter() {
|
||||
descriptor_keys.insert(*fingerprint);
|
||||
}
|
||||
if !descriptor_keys.contains(&fingerprint) {
|
||||
self.error =
|
||||
Some("The descriptor does not use a key derived from this seed".to_string());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.signer = Some(Arc::new(signer));
|
||||
|
||||
true
|
||||
}
|
||||
fn view(&self, progress: (usize, usize)) -> Element<Message> {
|
||||
view::recover_mnemonic(
|
||||
progress,
|
||||
&self.words,
|
||||
self.current,
|
||||
&self.suggestions,
|
||||
self.recover,
|
||||
self.error.as_ref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,12 @@
|
||||
mod descriptor;
|
||||
mod mnemonic;
|
||||
|
||||
pub use descriptor::{
|
||||
BackupDescriptor, DefineDescriptor, ImportDescriptor, ParticipateXpub, RegisterDescriptor,
|
||||
};
|
||||
|
||||
pub use mnemonic::{BackupMnemonic, RecoverMnemonic};
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use iced::widget::{
|
||||
scrollable::Properties, Button, Checkbox, Column, Container, PickList, Row, Scrollable, Space,
|
||||
TextInput,
|
||||
};
|
||||
use iced::{alignment, Alignment, Element, Length};
|
||||
|
||||
@ -411,6 +412,76 @@ pub fn import_descriptor<'a>(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn signer_xpubs(xpubs: &Vec<String>) -> Element<Message> {
|
||||
Container::new(
|
||||
Column::new()
|
||||
.push(
|
||||
Button::new(
|
||||
Row::new().align_items(Alignment::Center).push(
|
||||
Column::new()
|
||||
.push(text("This computer").bold())
|
||||
.push(
|
||||
text("Derive a key from a mnemonic stored on this computer")
|
||||
.small(),
|
||||
)
|
||||
.spacing(5)
|
||||
.width(Length::Fill),
|
||||
),
|
||||
)
|
||||
.on_press(Message::UseHotSigner)
|
||||
.padding(10)
|
||||
.style(button::Style::TransparentBorder.into())
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push_maybe(if xpubs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(separation().width(Length::Fill))
|
||||
})
|
||||
.push_maybe(if xpubs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(xpubs.iter().fold(Column::new().padding(15), |col, xpub| {
|
||||
col.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Container::new(
|
||||
Scrollable::new(Container::new(text(xpub).small()).padding(10))
|
||||
.horizontal_scroll(
|
||||
Properties::new().width(2).scroller_width(2),
|
||||
),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
Container::new(
|
||||
button::border(Some(icon::clipboard_icon()), "Copy")
|
||||
.on_press(Message::Clibpboard(xpub.clone()))
|
||||
.width(Length::Shrink),
|
||||
)
|
||||
.padding(10),
|
||||
),
|
||||
)
|
||||
}))
|
||||
})
|
||||
.push_maybe(if !xpubs.is_empty() {
|
||||
Some(
|
||||
Container::new(
|
||||
button::border(Some(icon::plus_icon()), "New public key")
|
||||
.on_press(Message::UseHotSigner),
|
||||
)
|
||||
.padding(10),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
.style(card::SimpleCardStyle)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn hardware_wallet_xpubs<'a>(
|
||||
i: usize,
|
||||
xpubs: &'a Vec<String>,
|
||||
@ -531,13 +602,14 @@ pub fn hardware_wallet_xpubs<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn participate_xpub(
|
||||
pub fn participate_xpub<'a>(
|
||||
progress: (usize, usize),
|
||||
network: bitcoin::Network,
|
||||
network_valid: bool,
|
||||
hws: Vec<Element<Message>>,
|
||||
hws: Vec<Element<'a, Message>>,
|
||||
signer: Element<'a, Message>,
|
||||
shared: bool,
|
||||
) -> Element<Message> {
|
||||
) -> Element<'a, Message> {
|
||||
let row_network = Row::new()
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
@ -583,6 +655,7 @@ pub fn participate_xpub(
|
||||
)
|
||||
.spacing(10)
|
||||
.push(Column::with_children(hws).spacing(10))
|
||||
.push(signer)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(Checkbox::new(
|
||||
@ -829,115 +902,142 @@ pub fn install<'a>(
|
||||
config_path: Option<&std::path::PathBuf>,
|
||||
warning: Option<&'a String>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut col = Column::new()
|
||||
.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("Descriptor:").small().bold())
|
||||
.push(text(descriptor).small()),
|
||||
layout(
|
||||
progress,
|
||||
Column::new()
|
||||
.push(Space::with_height(Length::Units(50)))
|
||||
.push(text("Final step").bold().size(50))
|
||||
.push(Space::with_height(Length::Units(50)))
|
||||
.push(text(
|
||||
"Check your information before finalizing the install process:",
|
||||
))
|
||||
.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("Descriptor:").small().bold())
|
||||
.push(text(descriptor).small()),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("Hardware devices:").small().bold())
|
||||
.push(context.hws.iter().fold(Column::new(), |acc, hw| {
|
||||
acc.push(
|
||||
.push_maybe(if context.hws.is_empty() && context.signer.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("Registered signing devices:").small().bold())
|
||||
.push_maybe(if context.hws.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(context.hws.iter().fold(
|
||||
Column::new(),
|
||||
|acc, hw| {
|
||||
acc.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text(hw.0.to_string()).small())
|
||||
.push(
|
||||
text(format!(
|
||||
"(fingerprint: {})",
|
||||
hw.1
|
||||
))
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
},
|
||||
))
|
||||
})
|
||||
.push_maybe(context.signer.as_ref().map(|signer| {
|
||||
Row::new().push(text("This computer").small()).push(
|
||||
text(format!(
|
||||
"(fingerprint: {})",
|
||||
signer.fingerprint()
|
||||
))
|
||||
.small(),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
})
|
||||
.push(
|
||||
card::simple(
|
||||
Column::new()
|
||||
.push(text("Bitcoind:").small().bold())
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text(hw.0.to_string()).small())
|
||||
.push(text(format!("(fingerprint: {})", hw.1)).small()),
|
||||
.align_items(Alignment::Center)
|
||||
.push(text("Cookie path:").small())
|
||||
.push(
|
||||
text(format!(
|
||||
"{}",
|
||||
context
|
||||
.bitcoind_config
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.cookie_path
|
||||
.to_string_lossy()
|
||||
))
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
card::simple(
|
||||
Column::new()
|
||||
.push(text("Bitcoind:").small().bold())
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(text("Cookie path:").small())
|
||||
.push(
|
||||
text(format!(
|
||||
"{}",
|
||||
context
|
||||
.bitcoind_config
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.cookie_path
|
||||
.to_string_lossy()
|
||||
))
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(text("Address:").small())
|
||||
.push(
|
||||
text(format!(
|
||||
"{}",
|
||||
context.bitcoind_config.as_ref().unwrap().addr
|
||||
))
|
||||
.small(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
),
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(text("Address:").small())
|
||||
.push(
|
||||
text(format!(
|
||||
"{}",
|
||||
context.bitcoind_config.as_ref().unwrap().addr
|
||||
))
|
||||
.small(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
),
|
||||
)
|
||||
.max_width(1000),
|
||||
)
|
||||
.padding(50)
|
||||
.max_width(1000),
|
||||
)
|
||||
.spacing(50)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
if let Some(error) = warning {
|
||||
col = col.push(text(error));
|
||||
}
|
||||
|
||||
if generating {
|
||||
col = col.push(button::primary(None, "Installing ...").width(Length::Units(200)))
|
||||
} else if let Some(path) = config_path {
|
||||
col = col.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.push(Container::new(text("Installed !")))
|
||||
.push(Container::new(
|
||||
button::primary(None, "Start")
|
||||
.on_press(Message::Exit(path.clone()))
|
||||
.width(Length::Units(200)),
|
||||
))
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20),
|
||||
)
|
||||
.padding(50)
|
||||
.push(Space::with_height(Length::Units(50)))
|
||||
.push_maybe(warning.map(|e| card::invalid(text(e))))
|
||||
.push(if generating {
|
||||
Container::new(button::primary(None, "Installing ...").width(Length::Units(200)))
|
||||
} else if let Some(path) = config_path {
|
||||
Container::new(
|
||||
Column::new()
|
||||
.push(Container::new(text("Installed !")))
|
||||
.push(Container::new(
|
||||
button::primary(None, "Start")
|
||||
.on_press(Message::Exit(path.clone()))
|
||||
.width(Length::Units(200)),
|
||||
))
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20),
|
||||
)
|
||||
.padding(50)
|
||||
.width(Length::Fill)
|
||||
.center_x()
|
||||
} else {
|
||||
Container::new(
|
||||
button::primary(None, "Finalize installation")
|
||||
.on_press(Message::Install)
|
||||
.width(Length::Units(200)),
|
||||
)
|
||||
})
|
||||
.spacing(10)
|
||||
.width(Length::Fill)
|
||||
.center_x(),
|
||||
);
|
||||
} else {
|
||||
col = col.push(
|
||||
button::primary(None, "Finalize installation")
|
||||
.on_press(Message::Install)
|
||||
.width(Length::Units(200)),
|
||||
);
|
||||
}
|
||||
|
||||
layout(progress, col)
|
||||
.height(Length::Fill)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
|
||||
@ -1086,6 +1186,7 @@ pub fn edit_key_modal<'a>(
|
||||
error: Option<&Error>,
|
||||
processing: bool,
|
||||
chosen_hw: Option<usize>,
|
||||
chosen_signer: bool,
|
||||
form_xpub: &form::Value<String>,
|
||||
form_name: &'a form::Value<String>,
|
||||
edit_name: bool,
|
||||
@ -1095,14 +1196,14 @@ pub fn edit_key_modal<'a>(
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.spacing(25)
|
||||
.push(if !hws.is_empty() {
|
||||
.push(
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Container::new(text("Select a hardware wallet:").bold())
|
||||
Container::new(text("Select a signing device:").bold())
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
@ -1126,19 +1227,34 @@ pub fn edit_key_modal<'a>(
|
||||
))
|
||||
},
|
||||
))
|
||||
.width(Length::Fill)
|
||||
} else {
|
||||
Column::new()
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(15)
|
||||
.width(Length::Fill)
|
||||
.push(text("Connect a hardware wallet").bold().width(Length::Fill))
|
||||
.push(button::border(None, "Refresh").on_press(Message::Reload))
|
||||
.align_items(Alignment::Center),
|
||||
Button::new(
|
||||
Row::new()
|
||||
.padding(5)
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("This computer").bold())
|
||||
.push(
|
||||
text("Derive a key from a mnemonic stored on this computer").small(),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push_maybe(if chosen_signer {
|
||||
Some(icon::circle_check_icon().style(color::SUCCESS))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.spacing(10),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::UseHotSigner)
|
||||
.style(button::Style::Border.into()),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
})
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
@ -1301,6 +1417,157 @@ fn hw_list_view(
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn backup_mnemonic<'a>(
|
||||
progress: (usize, usize),
|
||||
words: &'a [&'static str; 12],
|
||||
done: bool,
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
progress,
|
||||
Column::new()
|
||||
.push(text("Backup your mnemonic").bold().size(50))
|
||||
.push(text(prompt::MNEMONIC_HELP))
|
||||
.push(
|
||||
words
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(Column::new().spacing(5), |acc, (i, w)| {
|
||||
acc.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::End)
|
||||
.push(
|
||||
Container::new(text(format!("#{}", i + 1)).small())
|
||||
.width(Length::Units(50)),
|
||||
)
|
||||
.push(text(*w).bold()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.push(Checkbox::new(
|
||||
"I have backed up my mnemonic",
|
||||
done,
|
||||
Message::UserActionDone,
|
||||
))
|
||||
.push(if done {
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::Next)
|
||||
.width(Length::Units(200))
|
||||
} else {
|
||||
button::primary(None, "Next").width(Length::Units(200))
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(100)
|
||||
.spacing(50)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn recover_mnemonic<'a>(
|
||||
progress: (usize, usize),
|
||||
words: &'a [(String, bool); 12],
|
||||
current: usize,
|
||||
suggestions: &'a Vec<String>,
|
||||
recover: bool,
|
||||
error: Option<&'a String>,
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
progress,
|
||||
Column::new()
|
||||
.push(text("Mnemonics import").bold().size(50))
|
||||
.push(text(prompt::RECOVER_MNEMONIC_HELP))
|
||||
.push_maybe(if recover {
|
||||
Some(
|
||||
Column::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Container::new(if !suggestions.is_empty() {
|
||||
suggestions.iter().fold(Row::new().spacing(5), |row, sugg| {
|
||||
row.push(
|
||||
Button::new(text(sugg))
|
||||
.style(button::Style::Border.into())
|
||||
.on_press(Message::MnemonicWord(
|
||||
current,
|
||||
sugg.to_string(),
|
||||
)),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Row::new()
|
||||
})
|
||||
// Fixed height in order to not move words list
|
||||
.height(Length::Units(50)),
|
||||
)
|
||||
.push(words.iter().enumerate().fold(
|
||||
Column::new().spacing(5),
|
||||
|acc, (i, (word, valid))| {
|
||||
acc.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Container::new(text(format!("#{}", i + 1)).small())
|
||||
.width(Length::Units(50)),
|
||||
)
|
||||
.push(
|
||||
Container::new(TextInput::new("", word, move |msg| {
|
||||
Message::MnemonicWord(i, msg)
|
||||
}))
|
||||
.width(Length::Units(100)),
|
||||
)
|
||||
.push_maybe(if *valid {
|
||||
Some(icon::circle_check_icon().style(color::SUCCESS))
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
},
|
||||
))
|
||||
.push(Space::with_height(Length::Units(50)))
|
||||
.push_maybe(error.map(|e| card::invalid(text(e).style(color::ALERT)))),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(if !recover {
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::border(None, "Import mnemonic")
|
||||
.on_press(Message::ImportMnemonic(true))
|
||||
.width(Length::Units(200)),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Skip")
|
||||
.on_press(Message::Skip)
|
||||
.width(Length::Units(200)),
|
||||
)
|
||||
} else {
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::border(None, "Cancel")
|
||||
.on_press(Message::ImportMnemonic(false))
|
||||
.width(Length::Units(200)),
|
||||
)
|
||||
.push(
|
||||
if words.iter().any(|(_, valid)| !valid) || error.is_some() {
|
||||
button::primary(None, "Next").width(Length::Units(200))
|
||||
} else {
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::Next)
|
||||
.width(Length::Units(200))
|
||||
},
|
||||
)
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(100)
|
||||
.spacing(50)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
}
|
||||
|
||||
fn layout<'a>(
|
||||
progress: (usize, usize),
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
|
||||
@ -4,6 +4,7 @@ pub mod hw;
|
||||
pub mod installer;
|
||||
pub mod launcher;
|
||||
pub mod loader;
|
||||
pub mod signer;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ use log::{debug, info};
|
||||
use liana::{
|
||||
config::{Config, ConfigError},
|
||||
miniscript::bitcoin,
|
||||
signer::HotSigner,
|
||||
StartupError,
|
||||
};
|
||||
|
||||
@ -24,6 +25,7 @@ use crate::{
|
||||
wallet::Wallet,
|
||||
},
|
||||
daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError},
|
||||
signer::Signer,
|
||||
ui::{
|
||||
component::{button, notification, text::*},
|
||||
icon,
|
||||
@ -143,47 +145,14 @@ impl Loader {
|
||||
match res {
|
||||
Ok(info) => {
|
||||
if (info.sync - 1.0_f64).abs() < f64::EPSILON {
|
||||
let daemon = daemon.clone();
|
||||
let settings_path =
|
||||
settings_path(&self.datadir_path, self.network).unwrap();
|
||||
let gui_config_hws = self
|
||||
.gui_config
|
||||
.hardware_wallets
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
return Command::perform(
|
||||
async move {
|
||||
let coins = daemon.list_coins().map(|res| res.coins)?;
|
||||
let spend_txs = daemon.list_spend_transactions()?;
|
||||
let cache = Cache {
|
||||
network: info.network,
|
||||
blockheight: info.block_height,
|
||||
coins,
|
||||
spend_txs,
|
||||
..Default::default()
|
||||
};
|
||||
let wallet = match Settings::from_file(&settings_path) {
|
||||
Ok(settings) => {
|
||||
if let Some(wallet_setting) = settings.wallets.first() {
|
||||
Wallet::legacy(info.descriptors.main)
|
||||
.with_harware_wallets(
|
||||
wallet_setting.hardware_wallets.clone(),
|
||||
)
|
||||
.with_key_aliases(wallet_setting.keys_aliases())
|
||||
} else {
|
||||
Wallet::legacy(info.descriptors.main)
|
||||
.with_harware_wallets(gui_config_hws)
|
||||
}
|
||||
}
|
||||
Err(settings::SettingsError::NotFound) => {
|
||||
Wallet::legacy(info.descriptors.main)
|
||||
.with_harware_wallets(gui_config_hws)
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
Ok((Arc::new(wallet), cache, daemon))
|
||||
},
|
||||
load_application(
|
||||
daemon.clone(),
|
||||
info,
|
||||
self.gui_config.clone(),
|
||||
self.datadir_path.clone(),
|
||||
self.network,
|
||||
),
|
||||
Message::Synced,
|
||||
);
|
||||
} else {
|
||||
@ -229,6 +198,10 @@ impl Loader {
|
||||
Message::Started(res) => self.on_start(res),
|
||||
Message::Loaded(res) => self.on_load(res),
|
||||
Message::Syncing(res) => self.on_sync(res),
|
||||
Message::Synced(Err(e)) => {
|
||||
self.step = Step::Error(Box::new(e));
|
||||
Command::none()
|
||||
}
|
||||
Message::Failure(_) => {
|
||||
self.daemon_started = false;
|
||||
Command::none()
|
||||
@ -246,6 +219,71 @@ impl Loader {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_application(
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
info: GetInfoResult,
|
||||
gui_config: GUIConfig,
|
||||
datadir_path: Option<PathBuf>,
|
||||
network: bitcoin::Network,
|
||||
) -> Result<(Arc<Wallet>, Cache, Arc<dyn Daemon + Sync + Send>), Error> {
|
||||
let coins = daemon.list_coins().map(|res| res.coins)?;
|
||||
let spend_txs = daemon.list_spend_transactions()?;
|
||||
let cache = Cache {
|
||||
network: info.network,
|
||||
blockheight: info.block_height,
|
||||
coins,
|
||||
spend_txs,
|
||||
..Default::default()
|
||||
};
|
||||
let settings_path = settings_path(&datadir_path, network).unwrap();
|
||||
let gui_config_hws = gui_config
|
||||
.hardware_wallets
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut wallet = match Settings::from_file(&settings_path) {
|
||||
Ok(settings) => {
|
||||
if let Some(wallet_setting) = settings.wallets.first() {
|
||||
Wallet::new(wallet_setting.name.clone(), info.descriptors.main)
|
||||
.with_hardware_wallets(wallet_setting.hardware_wallets.clone())
|
||||
.with_key_aliases(wallet_setting.keys_aliases())
|
||||
} else {
|
||||
Wallet::legacy(info.descriptors.main).with_hardware_wallets(gui_config_hws)
|
||||
}
|
||||
}
|
||||
Err(settings::SettingsError::NotFound) => {
|
||||
Wallet::legacy(info.descriptors.main).with_hardware_wallets(gui_config_hws)
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
let hot_signers = match HotSigner::from_datadir(&get_datadir_path(&datadir_path)?, network) {
|
||||
Ok(signers) => signers,
|
||||
Err(e) => match e {
|
||||
liana::signer::SignerError::MnemonicStorage(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
Vec::new()
|
||||
} else {
|
||||
return Err(Error::HotSigner(e.to_string()));
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::HotSigner(e.to_string())),
|
||||
},
|
||||
};
|
||||
|
||||
let curve = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let keys = wallet.descriptor_keys();
|
||||
if let Some(hot_signer) = hot_signers
|
||||
.into_iter()
|
||||
.find(|s| keys.contains(&s.fingerprint(&curve)))
|
||||
{
|
||||
wallet = wallet.with_signer(Signer::new(hot_signer));
|
||||
}
|
||||
|
||||
Ok((Arc::new(wallet), cache, daemon))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ViewMessage {
|
||||
Retry,
|
||||
@ -368,6 +406,7 @@ pub enum Error {
|
||||
Settings(settings::SettingsError),
|
||||
Config(ConfigError),
|
||||
Daemon(DaemonError),
|
||||
HotSigner(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
@ -376,6 +415,7 @@ impl std::fmt::Display for Error {
|
||||
Self::Settings(e) => write!(f, "Settings error: {}", e),
|
||||
Self::Config(e) => write!(f, "Config error: {}", e),
|
||||
Self::Daemon(e) => write!(f, "Liana daemon error: {}", e),
|
||||
Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -398,16 +438,20 @@ impl From<DaemonError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_datadir_path(datadir_path: &Option<PathBuf>) -> Result<PathBuf, ConfigError> {
|
||||
if let Some(ref datadir) = datadir_path {
|
||||
Ok(datadir.clone())
|
||||
} else {
|
||||
default_datadir().map_err(|_| ConfigError::DatadirNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
/// default lianad socket path is .liana/bitcoin/lianad_rpc
|
||||
fn socket_path(
|
||||
datadir: &Option<PathBuf>,
|
||||
network: bitcoin::Network,
|
||||
) -> Result<PathBuf, ConfigError> {
|
||||
let mut path = if let Some(ref datadir) = datadir {
|
||||
datadir.clone()
|
||||
} else {
|
||||
default_datadir().map_err(|_| ConfigError::DatadirNotFound)?
|
||||
};
|
||||
let mut path = get_datadir_path(datadir)?;
|
||||
path.push(network.to_string());
|
||||
path.push("lianad_rpc");
|
||||
Ok(path)
|
||||
@ -418,11 +462,7 @@ fn settings_path(
|
||||
datadir: &Option<PathBuf>,
|
||||
network: bitcoin::Network,
|
||||
) -> Result<PathBuf, ConfigError> {
|
||||
let mut path = if let Some(ref datadir) = datadir {
|
||||
datadir.clone()
|
||||
} else {
|
||||
default_datadir().map_err(|_| ConfigError::DatadirNotFound)?
|
||||
};
|
||||
let mut path = get_datadir_path(datadir)?;
|
||||
path.push(network.to_string());
|
||||
path.push(settings::DEFAULT_FILE_NAME);
|
||||
Ok(path)
|
||||
|
||||
69
gui/src/signer.rs
Normal file
69
gui/src/signer.rs
Normal file
@ -0,0 +1,69 @@
|
||||
pub use liana::signer::SignerError;
|
||||
|
||||
use liana::{
|
||||
miniscript::bitcoin::{
|
||||
secp256k1,
|
||||
util::{
|
||||
bip32::{DerivationPath, ExtendedPubKey, Fingerprint},
|
||||
psbt::Psbt,
|
||||
},
|
||||
Network,
|
||||
},
|
||||
signer::HotSigner,
|
||||
};
|
||||
|
||||
pub struct Signer {
|
||||
curve: secp256k1::Secp256k1<secp256k1::SignOnly>,
|
||||
key: HotSigner,
|
||||
fingerprint: Fingerprint,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Signer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Signer").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer {
|
||||
pub fn new(key: HotSigner) -> Self {
|
||||
let curve = secp256k1::Secp256k1::signing_only();
|
||||
let fingerprint = key.fingerprint(&curve);
|
||||
Self {
|
||||
key,
|
||||
curve,
|
||||
fingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_network(&mut self, network: Network) {
|
||||
self.key.set_network(network)
|
||||
}
|
||||
|
||||
pub fn mnemonic(&self) -> [&'static str; 12] {
|
||||
self.key.words()
|
||||
}
|
||||
|
||||
pub fn generate(network: Network) -> Result<Self, SignerError> {
|
||||
Ok(Self::new(HotSigner::generate(network)?))
|
||||
}
|
||||
|
||||
pub fn fingerprint(&self) -> Fingerprint {
|
||||
self.fingerprint
|
||||
}
|
||||
|
||||
pub fn get_extended_pubkey(&self, path: &DerivationPath) -> ExtendedPubKey {
|
||||
self.key.xpub_at(path, &self.curve)
|
||||
}
|
||||
|
||||
pub fn sign_psbt(&self, psbt: Psbt) -> Result<Psbt, SignerError> {
|
||||
self.key.sign_psbt(psbt, &self.curve)
|
||||
}
|
||||
|
||||
pub fn store(
|
||||
&self,
|
||||
datadir_root: &std::path::Path,
|
||||
network: Network,
|
||||
) -> Result<(), SignerError> {
|
||||
self.key.store(datadir_root, network, &self.curve)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user