Fetch and redeem provider keys using tokens
This commit is contained in:
parent
759411cfce
commit
52e552313f
@ -8,7 +8,7 @@ use std::path::PathBuf;
|
||||
use liana::miniscript::bitcoin::{bip32::Fingerprint, Network};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::hw::HardwareWalletConfig;
|
||||
use crate::{hw::HardwareWalletConfig, lianalite::client::backend, services};
|
||||
|
||||
pub const DEFAULT_FILE_NAME: &str = "settings.json";
|
||||
|
||||
@ -89,12 +89,66 @@ impl WalletSetting {
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
pub fn provider_keys(&self) -> HashMap<Fingerprint, ProviderKey> {
|
||||
let mut map = HashMap::new();
|
||||
for (fingerprint, provider_key) in self
|
||||
.keys
|
||||
.iter()
|
||||
.filter_map(|k| k.provider_key.as_ref().map(|pk| (k.master_fingerprint, pk)))
|
||||
{
|
||||
map.insert(fingerprint, provider_key.clone());
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
pub struct Provider {
|
||||
pub uuid: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<backend::api::Provider> for Provider {
|
||||
fn from(provider: backend::api::Provider) -> Self {
|
||||
Self {
|
||||
uuid: provider.uuid,
|
||||
name: provider.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<services::api::Provider> for Provider {
|
||||
fn from(provider: services::api::Provider) -> Self {
|
||||
Self {
|
||||
uuid: provider.uuid,
|
||||
name: provider.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
pub struct ProviderKey {
|
||||
pub uuid: String,
|
||||
pub token: String,
|
||||
pub provider: Provider,
|
||||
}
|
||||
|
||||
impl From<backend::api::ProviderKey> for ProviderKey {
|
||||
fn from(pk: backend::api::ProviderKey) -> Self {
|
||||
Self {
|
||||
uuid: pk.uuid.clone(),
|
||||
token: pk.token.clone(),
|
||||
provider: pk.provider.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct KeySetting {
|
||||
pub name: String,
|
||||
pub master_fingerprint: Fingerprint,
|
||||
pub provider_key: Option<ProviderKey>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
|
||||
@ -82,6 +82,7 @@ impl State for WalletSettingsState {
|
||||
self.warning.as_ref(),
|
||||
&self.descriptor,
|
||||
&self.keys_aliases,
|
||||
&self.wallet.provider_keys,
|
||||
self.processing,
|
||||
self.updated,
|
||||
);
|
||||
@ -389,6 +390,7 @@ async fn update_keys_aliases(
|
||||
.map(|(master_fingerprint, name)| settings::KeySetting {
|
||||
master_fingerprint: *master_fingerprint,
|
||||
name: name.clone(),
|
||||
provider_key: wallet.provider_keys.get(master_fingerprint).cloned(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
|
||||
use iced::{
|
||||
@ -27,6 +27,7 @@ use crate::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
menu::Menu,
|
||||
settings::ProviderKey,
|
||||
view::{hw, warning::warn},
|
||||
},
|
||||
hw::HardwareWallet,
|
||||
@ -849,6 +850,7 @@ pub fn wallet_settings<'a>(
|
||||
warning: Option<&Error>,
|
||||
descriptor: &'a LianaDescriptor,
|
||||
keys_aliases: &'a [(Fingerprint, form::Value<String>)],
|
||||
provider_keys: &'a HashMap<Fingerprint, ProviderKey>,
|
||||
processing: bool,
|
||||
updated: bool,
|
||||
) -> Element<'a, Message> {
|
||||
@ -943,16 +945,22 @@ pub fn wallet_settings<'a>(
|
||||
.push(header)
|
||||
.push(descr)
|
||||
.push(
|
||||
card::simple(display_policy(descriptor.policy(), keys_aliases)).width(Length::Fill),
|
||||
card::simple(display_policy(
|
||||
descriptor.policy(),
|
||||
keys_aliases,
|
||||
provider_keys,
|
||||
))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(aliases),
|
||||
)
|
||||
}
|
||||
|
||||
fn display_policy(
|
||||
fn display_policy<'a>(
|
||||
policy: LianaPolicy,
|
||||
keys_aliases: &[(Fingerprint, form::Value<String>)],
|
||||
) -> Element<'_, Message> {
|
||||
keys_aliases: &'a [(Fingerprint, form::Value<String>)],
|
||||
provider_keys: &'a HashMap<Fingerprint, ProviderKey>,
|
||||
) -> Element<'a, Message> {
|
||||
let (primary_threshold, primary_keys) = policy.primary_path().thresh_origins();
|
||||
let recovery_paths = policy.recovery_paths();
|
||||
|
||||
@ -1068,7 +1076,18 @@ fn display_policy(
|
||||
))
|
||||
.bold(),
|
||||
)
|
||||
.push(text(format!("(Recovery path #{})", i + 1))),
|
||||
.push(text(
|
||||
// If max timelock and all keys are from provider, then it's a safety net path.
|
||||
if *sequence == u16::MAX
|
||||
&& recovery_keys
|
||||
.iter()
|
||||
.all(|fg| provider_keys.contains_key(fg))
|
||||
{
|
||||
"(Safety Net path)".to_string()
|
||||
} else {
|
||||
format!("(Recovery path #{})", i + 1)
|
||||
},
|
||||
)),
|
||||
);
|
||||
}
|
||||
Column::new()
|
||||
|
||||
@ -31,7 +31,9 @@ pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String {
|
||||
pub struct Wallet {
|
||||
pub name: String,
|
||||
pub main_descriptor: LianaDescriptor,
|
||||
// TODO: We could replace these two fields with `keys: HashMap<Fingerprint, settings::KeySetting>`.
|
||||
pub keys_aliases: HashMap<Fingerprint, String>,
|
||||
pub provider_keys: HashMap<Fingerprint, settings::ProviderKey>,
|
||||
pub hardware_wallets: Vec<HardwareWalletConfig>,
|
||||
pub signer: Option<Arc<Signer>>,
|
||||
}
|
||||
@ -42,6 +44,7 @@ impl Wallet {
|
||||
name: wallet_name(&main_descriptor),
|
||||
main_descriptor,
|
||||
keys_aliases: HashMap::new(),
|
||||
provider_keys: HashMap::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
signer: None,
|
||||
}
|
||||
@ -57,6 +60,14 @@ impl Wallet {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_provider_keys(
|
||||
mut self,
|
||||
provider_keys: HashMap<Fingerprint, settings::ProviderKey>,
|
||||
) -> Self {
|
||||
self.provider_keys = provider_keys;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hardware_wallets(mut self, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
|
||||
self.hardware_wallets = hardware_wallets;
|
||||
self
|
||||
@ -101,6 +112,7 @@ impl Wallet {
|
||||
self.with_name(wallet_setting.name.clone())
|
||||
.with_hardware_wallets(wallet_setting.hardware_wallets.clone())
|
||||
.with_key_aliases(wallet_setting.keys_aliases())
|
||||
.with_provider_keys(wallet_setting.provider_keys())
|
||||
} else {
|
||||
self
|
||||
}
|
||||
@ -117,6 +129,7 @@ impl Wallet {
|
||||
.map(|(master_fingerprint, name)| settings::KeySetting {
|
||||
name,
|
||||
master_fingerprint,
|
||||
provider_key: self.provider_keys.get(&master_fingerprint).cloned(),
|
||||
})
|
||||
.collect(),
|
||||
descriptor_checksum: self.descriptor_checksum(),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@ -56,7 +57,7 @@ pub struct Context {
|
||||
pub bitcoin_backend: Option<BitcoinBackend>,
|
||||
pub descriptor_template: DescriptorTemplate,
|
||||
pub descriptor: Option<LianaDescriptor>,
|
||||
pub keys: Vec<KeySetting>,
|
||||
pub keys: HashMap<bitcoin::bip32::Fingerprint, KeySetting>,
|
||||
pub hws: Vec<(DeviceKind, bitcoin::bip32::Fingerprint, Option<[u8; 32]>)>,
|
||||
pub data_dir: PathBuf,
|
||||
pub network: bitcoin::Network,
|
||||
@ -83,7 +84,7 @@ impl Context {
|
||||
poll_interval_secs: Duration::from_secs(30),
|
||||
},
|
||||
hws: Vec::new(),
|
||||
keys: Vec::new(),
|
||||
keys: HashMap::new(),
|
||||
bitcoin_backend: None,
|
||||
descriptor: None,
|
||||
data_dir,
|
||||
|
||||
242
liana-gui/src/installer/descriptor.rs
Normal file
242
liana-gui/src/installer/descriptor.rs
Normal file
@ -0,0 +1,242 @@
|
||||
use async_hwi::{DeviceKind, Version};
|
||||
use liana::miniscript::{bitcoin::bip32::Fingerprint, descriptor::DescriptorPublicKey};
|
||||
|
||||
use crate::{
|
||||
app::settings::ProviderKey, hw::is_compatible_with_tapminiscript, services::api::KeyKind,
|
||||
};
|
||||
|
||||
/// Whether to enable cosigner keys on all paths (excluding safety net paths).
|
||||
const ENABLE_COSIGNER_KEYS: bool = true; // FIXME: Set to false after testing.
|
||||
|
||||
/// The source of a descriptor public key.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum KeySource {
|
||||
/// A hardware signing device with the given kind and version.
|
||||
Device(DeviceKind, Option<Version>),
|
||||
/// A hot signer on the user's computer.
|
||||
HotSigner,
|
||||
/// A manually inserted xpub.
|
||||
Manual,
|
||||
/// A token for a key with the given kind.
|
||||
Token(KeyKind, ProviderKey),
|
||||
}
|
||||
|
||||
impl KeySource {
|
||||
pub fn device_kind(&self) -> Option<&DeviceKind> {
|
||||
if let KeySource::Device(ref device_kind, _) = self {
|
||||
Some(device_kind)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn device_version(&self) -> Option<&Version> {
|
||||
if let KeySource::Device(_, ref version) = self {
|
||||
version.as_ref()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_compatible_taproot(&self) -> bool {
|
||||
if let KeySource::Device(ref device_kind, ref version) = self {
|
||||
is_compatible_with_tapminiscript(device_kind, version.as_ref())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_manual(&self) -> bool {
|
||||
matches!(self, KeySource::Manual)
|
||||
}
|
||||
|
||||
pub fn is_token(&self) -> bool {
|
||||
matches!(self, KeySource::Token(_, _))
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> KeySourceKind {
|
||||
match self {
|
||||
Self::Device(_, _) => KeySourceKind::Device,
|
||||
Self::HotSigner => KeySourceKind::HotSigner,
|
||||
Self::Manual => KeySourceKind::Manual,
|
||||
Self::Token(kind, _) => KeySourceKind::Token(*kind),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token(&self) -> Option<&String> {
|
||||
if let KeySource::Token(_, ProviderKey { token, .. }) = self {
|
||||
Some(token)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provider_key(&self) -> Option<ProviderKey> {
|
||||
if let KeySource::Token(_, provider_key) = self {
|
||||
Some(provider_key.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provider_key_kind(&self) -> Option<KeyKind> {
|
||||
if let KeySource::Token(key_kind, _) = self {
|
||||
Some(*key_kind)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of `KeySource`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
|
||||
pub enum KeySourceKind {
|
||||
/// A hardware signing device.
|
||||
Device,
|
||||
/// A hot signer.
|
||||
HotSigner,
|
||||
/// A manually inserted xpub.
|
||||
Manual,
|
||||
/// A token for a key with the given kind.
|
||||
Token(KeyKind),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Key {
|
||||
pub source: KeySource,
|
||||
pub name: String,
|
||||
pub fingerprint: Fingerprint,
|
||||
pub key: DescriptorPublicKey,
|
||||
}
|
||||
|
||||
pub struct Path {
|
||||
pub keys: Vec<Option<Key>>,
|
||||
pub threshold: usize,
|
||||
pub sequence: PathSequence,
|
||||
pub warning: Option<PathWarning>,
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn new(kind: PathKind) -> Self {
|
||||
let sequence = match kind {
|
||||
PathKind::Primary => PathSequence::Primary,
|
||||
PathKind::Recovery => PathSequence::Recovery(52_596), // displays "1y" in GUI
|
||||
PathKind::SafetyNet => PathSequence::SafetyNet,
|
||||
};
|
||||
Self {
|
||||
keys: vec![None],
|
||||
threshold: 1,
|
||||
sequence,
|
||||
warning: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_primary_path() -> Self {
|
||||
Self::new(PathKind::Primary)
|
||||
}
|
||||
|
||||
pub fn new_recovery_path() -> Self {
|
||||
Self::new(PathKind::Recovery)
|
||||
}
|
||||
|
||||
pub fn new_safety_net_path() -> Self {
|
||||
Self::new(PathKind::SafetyNet)
|
||||
}
|
||||
|
||||
pub fn with_n_keys(mut self, n: usize) -> Self {
|
||||
self.keys = Vec::new();
|
||||
for _i in 0..n {
|
||||
self.keys.push(None);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_threshold(mut self, t: usize) -> Self {
|
||||
self.threshold = if t > self.keys.len() {
|
||||
self.keys.len()
|
||||
} else {
|
||||
t
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> PathKind {
|
||||
self.sequence.path_kind()
|
||||
}
|
||||
|
||||
pub fn valid(&self) -> bool {
|
||||
!self.keys.is_empty() && !self.keys.iter().any(|k| k.is_none()) && self.warning.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of spending path.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PathKind {
|
||||
Primary,
|
||||
Recovery,
|
||||
SafetyNet,
|
||||
}
|
||||
|
||||
impl PathKind {
|
||||
/// Whether a key with the given `KeySourceKind` can be chosen for this `PathKind`.
|
||||
pub fn can_choose_key_source_kind(&self, source_kind: &KeySourceKind) -> bool {
|
||||
match (self, source_kind) {
|
||||
// Safety net path only allows safety net keys.
|
||||
(Self::SafetyNet, KeySourceKind::Token(KeyKind::SafetyNet)) => true,
|
||||
(Self::SafetyNet, _) => false,
|
||||
// Safety net keys cannot be used in any other path kind.
|
||||
(_, KeySourceKind::Token(KeyKind::SafetyNet)) => false,
|
||||
// Enable/disable cosigner keys.
|
||||
(_, KeySourceKind::Token(KeyKind::Cosigner)) => ENABLE_COSIGNER_KEYS,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The sequence of a spending path.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PathSequence {
|
||||
Primary,
|
||||
Recovery(u16), // this excludes zero, but we don't enforce it here.
|
||||
SafetyNet,
|
||||
}
|
||||
|
||||
impl PathSequence {
|
||||
pub fn as_u16(&self) -> u16 {
|
||||
match self {
|
||||
Self::Primary => 0,
|
||||
Self::Recovery(s) => *s,
|
||||
Self::SafetyNet => u16::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_kind(&self) -> PathKind {
|
||||
match self {
|
||||
Self::Primary => PathKind::Primary,
|
||||
Self::Recovery(_) => PathKind::Recovery,
|
||||
Self::SafetyNet => PathKind::SafetyNet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A path warning.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PathWarning {
|
||||
DuplicateSequence,
|
||||
OnlyCosignerKeys,
|
||||
KeySourceKindDisallowed,
|
||||
}
|
||||
|
||||
impl PathWarning {
|
||||
pub fn message(&self) -> &'static str {
|
||||
match self {
|
||||
Self::DuplicateSequence => {
|
||||
"No two recovery options may become available at the very same date."
|
||||
}
|
||||
Self::OnlyCosignerKeys => "A path cannot contain only cosigner keys.",
|
||||
Self::KeySourceKindDisallowed => {
|
||||
"Path contains a key that is disallowed for this kind of path."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,20 +6,22 @@ use std::path::PathBuf;
|
||||
|
||||
use super::{context, Error};
|
||||
use crate::{
|
||||
app::settings::ProviderKey,
|
||||
download::{DownloadError, Progress},
|
||||
hw::HardwareWalletMessage,
|
||||
installer::step::descriptor::editor::key::Key,
|
||||
installer::descriptor::{Key, PathKind},
|
||||
lianalite::client::{auth::AuthClient, backend::api},
|
||||
node::{
|
||||
bitcoind::{Bitcoind, ConfigField, RpcAuthType},
|
||||
electrum, NodeType,
|
||||
},
|
||||
services,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
UserActionDone(bool),
|
||||
Exit(PathBuf, Option<Bitcoind>),
|
||||
Exit(PathBuf, Option<Bitcoind>, /* remove log */ bool),
|
||||
Clibpboard(String),
|
||||
Next,
|
||||
Skip,
|
||||
@ -44,6 +46,9 @@ pub enum Message {
|
||||
WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>),
|
||||
MnemonicWord(usize, String),
|
||||
ImportMnemonic(bool),
|
||||
RedeemNextKey,
|
||||
KeyRedeemed(ProviderKey, Result<(), services::Error>),
|
||||
AllKeysRedeemed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -114,9 +119,10 @@ pub enum DefineDescriptor {
|
||||
ChangeTemplate(context::DescriptorTemplate),
|
||||
ImportDescriptor(String),
|
||||
KeysEdited(Vec<(usize, usize)>, Key),
|
||||
KeysEdit(Vec<(usize, usize)>),
|
||||
KeysEdit(PathKind, Vec<(usize, usize)>),
|
||||
Path(usize, DefinePath),
|
||||
AddRecoveryPath,
|
||||
AddSafetyNetPath,
|
||||
KeyModal(ImportKeyModal),
|
||||
ThresholdSequenceModal(ThresholdSequenceModal),
|
||||
}
|
||||
@ -147,6 +153,9 @@ pub enum ImportKeyModal {
|
||||
NameEdited(String),
|
||||
ManuallyImportXpub,
|
||||
ConfirmXpub,
|
||||
UseToken(services::api::KeyKind),
|
||||
TokenEdited(String),
|
||||
ConfirmToken,
|
||||
SelectKey(usize),
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
mod context;
|
||||
mod descriptor;
|
||||
mod message;
|
||||
mod prompt;
|
||||
mod step;
|
||||
@ -21,8 +22,8 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
config as gui_config, settings as gui_settings,
|
||||
settings::{AuthConfig, Settings, SettingsError, WalletSetting},
|
||||
config as gui_config,
|
||||
settings::{self as gui_settings, AuthConfig, Settings, SettingsError, WalletSetting},
|
||||
wallet::wallet_name,
|
||||
},
|
||||
daemon::DaemonError,
|
||||
@ -30,11 +31,16 @@ use crate::{
|
||||
hw::{HardwareWalletConfig, HardwareWallets},
|
||||
lianalite::client::{
|
||||
auth::AuthError,
|
||||
backend::{BackendClient, BackendWalletClient},
|
||||
backend::{
|
||||
api::payload::{Provider, ProviderKey},
|
||||
BackendClient, BackendWalletClient,
|
||||
},
|
||||
},
|
||||
services,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
pub use descriptor::{KeySource, KeySourceKind, PathKind, PathSequence};
|
||||
pub use message::Message;
|
||||
use step::{
|
||||
BackupDescriptor, BackupMnemonic, ChooseBackend, ChooseDescriptorTemplate, DefineDescriptor,
|
||||
@ -498,8 +504,23 @@ pub async fn create_remote_wallet(
|
||||
|
||||
info!("Gui configuration file created");
|
||||
|
||||
let pks: Vec<_> = ctx
|
||||
.keys
|
||||
.values()
|
||||
.filter_map(|key| {
|
||||
key.provider_key.as_ref().map(|pk| ProviderKey {
|
||||
fingerprint: key.master_fingerprint.to_string(),
|
||||
uuid: pk.uuid.clone(),
|
||||
token: pk.token.clone(),
|
||||
provider: Provider {
|
||||
uuid: pk.provider.uuid.clone(),
|
||||
name: pk.provider.name.clone(),
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let wallet = remote_backend
|
||||
.create_wallet(&wallet_name(descriptor), descriptor)
|
||||
.create_wallet(&wallet_name(descriptor), descriptor, &pks)
|
||||
.await
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
|
||||
@ -515,7 +536,7 @@ pub async fn create_remote_wallet(
|
||||
let descriptor_str = descriptor.to_string();
|
||||
let aliases = ctx
|
||||
.keys
|
||||
.iter()
|
||||
.values()
|
||||
.filter_map(|k| {
|
||||
if descriptor_str.contains(&k.master_fingerprint.to_string()) {
|
||||
Some((k.master_fingerprint, k.name.to_string()))
|
||||
@ -674,7 +695,7 @@ pub async fn extract_local_gui_settings(ctx: &Context) -> Settings {
|
||||
wallets: vec![WalletSetting {
|
||||
name: wallet_name(descriptor),
|
||||
descriptor_checksum,
|
||||
keys: ctx.keys.clone(),
|
||||
keys: ctx.keys.values().cloned().collect(),
|
||||
hardware_wallets,
|
||||
remote_backend_auth: None,
|
||||
}],
|
||||
@ -700,6 +721,7 @@ pub enum Error {
|
||||
// DaemonError does not implement Clone.
|
||||
// TODO: maybe Arc is overkill
|
||||
Backend(Arc<DaemonError>),
|
||||
Services(services::Error),
|
||||
Settings(SettingsError),
|
||||
Bitcoind(String),
|
||||
Electrum(String),
|
||||
@ -752,6 +774,7 @@ impl std::fmt::Display for Error {
|
||||
match self {
|
||||
Self::Auth(e) => write!(f, "Authentication error: {}", e),
|
||||
Self::Backend(e) => write!(f, "Remote backend error: {}", e),
|
||||
Self::Services(e) => write!(f, "Services error: {}", e),
|
||||
Self::Settings(e) => write!(f, "Settings file error: {}", e),
|
||||
Self::Bitcoind(e) => write!(f, "Failed to ping bitcoind: {}", e),
|
||||
Self::Electrum(e) => write!(f, "Failed to ping Electrum: {}", e),
|
||||
|
||||
@ -14,14 +14,15 @@ use liana::miniscript::{
|
||||
|
||||
use liana_ui::{component::form, widget::Element};
|
||||
|
||||
use async_hwi::{DeviceKind, Version};
|
||||
|
||||
use crate::{
|
||||
hw::{is_compatible_with_tapminiscript, HardwareWallet, HardwareWallets},
|
||||
app::settings::ProviderKey,
|
||||
hw::{HardwareWallet, HardwareWallets},
|
||||
installer::{
|
||||
descriptor::{Key, KeySource, KeySourceKind, PathKind},
|
||||
message::{self, Message},
|
||||
view, Error,
|
||||
},
|
||||
services,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
@ -41,17 +42,6 @@ pub fn new_multixkey_from_xpub(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Key {
|
||||
pub device_kind: Option<DeviceKind>,
|
||||
pub is_hot_signer: bool,
|
||||
pub device_version: Option<Version>,
|
||||
pub name: String,
|
||||
pub fingerprint: Fingerprint,
|
||||
pub key: DescriptorPublicKey,
|
||||
pub is_compatible_taproot: bool,
|
||||
}
|
||||
|
||||
pub fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool {
|
||||
match key {
|
||||
DescriptorPublicKey::XPub(key) => {
|
||||
@ -81,21 +71,27 @@ pub struct EditXpubModal {
|
||||
|
||||
form_name: form::Value<String>,
|
||||
form_xpub: form::Value<String>,
|
||||
manually_imported_xpub: bool,
|
||||
// TODO: Define new `form::Value` type with `Option<String>` instead of `bool` so that we can
|
||||
// store `form_token_warning` directly in `form_token`.
|
||||
form_token: form::Value<String>,
|
||||
form_token_warning: Option<String>,
|
||||
|
||||
other_path_keys: HashSet<Fingerprint>,
|
||||
duplicate_master_fg: bool,
|
||||
|
||||
path_kind: PathKind,
|
||||
keys: Vec<Key>,
|
||||
hot_signer: Arc<Mutex<Signer>>,
|
||||
hot_signer_fingerprint: Fingerprint,
|
||||
chosen_signer: Option<Key>,
|
||||
chosen_key_source_kind: Option<KeySourceKind>,
|
||||
}
|
||||
|
||||
impl EditXpubModal {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
device_must_support_tapminiscript: bool,
|
||||
path_kind: PathKind,
|
||||
other_path_keys: HashSet<Fingerprint>,
|
||||
key: Option<Key>,
|
||||
keys_coordinate: Vec<(usize, usize)>,
|
||||
@ -104,13 +100,9 @@ impl EditXpubModal {
|
||||
hot_signer_fingerprint: Fingerprint,
|
||||
keys: Vec<Key>,
|
||||
) -> Self {
|
||||
// The xpub is manually imported if the key is neither from a device or the hot signer.
|
||||
let manually_imported_xpub = key
|
||||
.as_ref()
|
||||
.map(|k| !k.is_hot_signer && k.device_kind.is_none())
|
||||
.unwrap_or(false);
|
||||
Self {
|
||||
device_must_support_tapminiscript,
|
||||
path_kind,
|
||||
other_path_keys,
|
||||
form_name: form::Value {
|
||||
valid: true,
|
||||
@ -118,18 +110,26 @@ impl EditXpubModal {
|
||||
},
|
||||
form_xpub: form::Value {
|
||||
valid: true,
|
||||
value: if manually_imported_xpub {
|
||||
key.as_ref().map(|k| k.key.to_string()).unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
value: key
|
||||
.as_ref()
|
||||
.filter(|k| k.source.is_manual())
|
||||
.map(|k| k.key.to_string())
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
manually_imported_xpub,
|
||||
form_token: form::Value {
|
||||
valid: true,
|
||||
value: key
|
||||
.as_ref()
|
||||
.and_then(|k| k.source.token().cloned())
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
form_token_warning: None,
|
||||
keys,
|
||||
keys_coordinate,
|
||||
processing: false,
|
||||
error: None,
|
||||
network,
|
||||
chosen_key_source_kind: key.as_ref().map(|k| k.source.kind()),
|
||||
chosen_signer: key,
|
||||
hot_signer_fingerprint,
|
||||
hot_signer,
|
||||
@ -163,7 +163,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
}) = hws.list.get(i)
|
||||
{
|
||||
self.processing = true;
|
||||
self.manually_imported_xpub = false;
|
||||
self.chosen_key_source_kind = Some(KeySourceKind::Device);
|
||||
let device_version = version.clone();
|
||||
let fingerprint = *fingerprint;
|
||||
let device_kind = *kind;
|
||||
@ -186,17 +186,13 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
Ok(key) => {
|
||||
if check_key_network(&key, network) {
|
||||
Ok(Key {
|
||||
is_hot_signer: false,
|
||||
source: KeySource::Device(
|
||||
device_kind,
|
||||
device_version,
|
||||
),
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key,
|
||||
is_compatible_taproot:
|
||||
is_compatible_with_tapminiscript(
|
||||
&device_kind,
|
||||
device_version.as_ref(),
|
||||
),
|
||||
device_kind: Some(device_kind),
|
||||
device_version,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Unexpected(
|
||||
@ -215,7 +211,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
return self.load();
|
||||
}
|
||||
Message::UseHotSigner => {
|
||||
self.manually_imported_xpub = false;
|
||||
self.chosen_key_source_kind = Some(KeySourceKind::HotSigner);
|
||||
let fingerprint = self.hot_signer.lock().unwrap().fingerprint();
|
||||
let derivation_path = default_derivation_path(self.network);
|
||||
let key_str = format!(
|
||||
@ -228,13 +224,10 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
.get_extended_pubkey(&derivation_path)
|
||||
);
|
||||
self.chosen_signer = Some(Key {
|
||||
is_hot_signer: true,
|
||||
source: KeySource::HotSigner,
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key: DescriptorPublicKey::from_str(&key_str).unwrap(),
|
||||
is_compatible_taproot: true,
|
||||
device_kind: None,
|
||||
device_version: None,
|
||||
});
|
||||
self.form_name.value = self
|
||||
.keys
|
||||
@ -254,9 +247,42 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok(key) => {
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value.clone_from(&key.name);
|
||||
self.chosen_signer = Some(key);
|
||||
// If it is a provider key that has just been fetched, do some additional sanity checks.
|
||||
if let Some(key_kind) = key.source.provider_key_kind() {
|
||||
// We don't need to check key's status as redeemed keys are not returned.
|
||||
self.form_token_warning = if self.chosen_key_source_kind
|
||||
!= Some(KeySourceKind::Token(key_kind))
|
||||
{
|
||||
Some("Wrong kind of token".to_string())
|
||||
} else if !check_key_network(&key.key, self.network) {
|
||||
Some(
|
||||
"Fetched key does not have the correct network".to_string(),
|
||||
)
|
||||
}
|
||||
// If two keys have the same fingerprint, they must both have the same provider key kind (which could be `None`).
|
||||
// Note that this checks all keys regardless of whether they are currently being used in a path.
|
||||
else if self.keys.iter().any(|existing| {
|
||||
existing.fingerprint == key.fingerprint
|
||||
&& existing.source.provider_key_kind()
|
||||
!= key.source.provider_key_kind()
|
||||
}) {
|
||||
Some("Two keys with the same fingerprint must have the same provider key kind.".to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.form_token.valid = self.form_token_warning.is_none();
|
||||
}
|
||||
// User can set name for key if it is not a provider key or is a valid provider key.
|
||||
if key.source.provider_key().is_none() || self.form_token.valid {
|
||||
self.form_name.valid = key.name.is_empty()
|
||||
|| !self.keys.iter().any(|k| {
|
||||
k.fingerprint != key.fingerprint && k.name == key.name
|
||||
});
|
||||
self.form_name.value.clone_from(&key.name);
|
||||
self.chosen_signer = Some(key);
|
||||
} else {
|
||||
self.chosen_signer = None;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.chosen_signer = None;
|
||||
@ -266,9 +292,14 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
}
|
||||
message::ImportKeyModal::ManuallyImportXpub => {
|
||||
self.chosen_signer = None;
|
||||
self.manually_imported_xpub = true;
|
||||
self.chosen_key_source_kind = Some(KeySourceKind::Manual);
|
||||
self.form_xpub = form::Value::default();
|
||||
}
|
||||
message::ImportKeyModal::UseToken(kind) => {
|
||||
self.chosen_signer = None;
|
||||
self.chosen_key_source_kind = Some(KeySourceKind::Token(kind));
|
||||
self.form_token = form::Value::default();
|
||||
}
|
||||
message::ImportKeyModal::NameEdited(name) => {
|
||||
self.form_name.valid = !self.keys.iter().any(|k| {
|
||||
Some(&k.fingerprint) != self.chosen_signer.as_ref().map(|s| &s.fingerprint)
|
||||
@ -276,6 +307,18 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
});
|
||||
self.form_name.value = name;
|
||||
}
|
||||
message::ImportKeyModal::TokenEdited(s) => {
|
||||
self.chosen_signer = None;
|
||||
// We check if the token has already been fetched and saved regardless of its kind.
|
||||
self.form_token_warning =
|
||||
if self.keys.iter().any(|k| k.source.token() == Some(&s)) {
|
||||
Some("Duplicate token".to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.form_token.valid = s.is_empty() || self.form_token_warning.is_none();
|
||||
self.form_token.value = s;
|
||||
}
|
||||
message::ImportKeyModal::XPubEdited(s) => {
|
||||
if let Ok(DescriptorPublicKey::XPub(key)) = DescriptorPublicKey::from_str(&s) {
|
||||
self.chosen_signer = None;
|
||||
@ -289,13 +332,10 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
};
|
||||
if self.form_xpub.valid {
|
||||
self.chosen_signer = Some(Key {
|
||||
is_hot_signer: false,
|
||||
source: KeySource::Manual,
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key: DescriptorPublicKey::XPub(key),
|
||||
is_compatible_taproot: true,
|
||||
device_kind: None,
|
||||
device_version: None,
|
||||
});
|
||||
self.form_name.value = "".to_string();
|
||||
self.form_name.valid = true;
|
||||
@ -325,9 +365,42 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
}
|
||||
}
|
||||
}
|
||||
message::ImportKeyModal::ConfirmToken => {
|
||||
// We have checked that the token has not already been fetched and saved.
|
||||
let token = self.form_token.value.clone();
|
||||
let client = services::Client::new();
|
||||
return Task::perform(
|
||||
async move { (token.clone(), client.get_key_by_token(token).await) },
|
||||
|(token, res)| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::FetchedKey(match res {
|
||||
Err(e) => Err(Error::Services(e)),
|
||||
Ok(ref key) => Ok(Key {
|
||||
source: KeySource::Token(
|
||||
key.kind,
|
||||
ProviderKey {
|
||||
uuid: key.uuid.clone(),
|
||||
token,
|
||||
provider: key.provider.clone().into(),
|
||||
},
|
||||
),
|
||||
fingerprint: key.xpub.master_fingerprint(),
|
||||
name: format!(
|
||||
"{} - {}",
|
||||
key.provider.name.clone(),
|
||||
key.kind
|
||||
),
|
||||
key: key.xpub.clone(),
|
||||
}),
|
||||
}),
|
||||
))
|
||||
},
|
||||
);
|
||||
}
|
||||
message::ImportKeyModal::SelectKey(i) => {
|
||||
if let Some(key) = self.keys.get(i) {
|
||||
self.chosen_signer = Some(key.clone());
|
||||
self.chosen_key_source_kind = Some(key.source.kind());
|
||||
self.form_name.value.clone_from(&key.name);
|
||||
self.form_name.valid = true;
|
||||
}
|
||||
@ -343,10 +416,26 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
}
|
||||
|
||||
fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> {
|
||||
// For provider keys, include the chosen signer in case this is a provider key
|
||||
// and has not yet been saved, i.e. if it's not in `self.keys`. An unsaved provider
|
||||
// key will be displayed in a similar way to saved ones.
|
||||
let provider_keys: Vec<_> = self
|
||||
.keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, k)| (Some(i), k))
|
||||
.chain((self.chosen_signer).iter().filter_map(|cs| {
|
||||
(!self.keys.iter().any(|k| k.fingerprint == cs.fingerprint)).then_some((None, cs))
|
||||
}))
|
||||
.filter(|(_, k)| {
|
||||
k.source.is_token() && self.path_kind.can_choose_key_source_kind(&k.source.kind())
|
||||
})
|
||||
.collect();
|
||||
let chosen_signer = self.chosen_signer.as_ref().map(|s| s.fingerprint);
|
||||
view::editor::edit_key_modal(
|
||||
"Set your key",
|
||||
self.network,
|
||||
self.path_kind,
|
||||
hws.list
|
||||
.iter()
|
||||
.enumerate()
|
||||
@ -355,6 +444,9 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
.keys
|
||||
.iter()
|
||||
.any(|k| Some(k.fingerprint) == hw.fingerprint())
|
||||
|| !self
|
||||
.path_kind
|
||||
.can_choose_key_source_kind(&KeySourceKind::Device)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
@ -373,23 +465,36 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, key)| {
|
||||
if key.fingerprint == self.hot_signer_fingerprint {
|
||||
// ignore hot signers and provider keys.
|
||||
if key.fingerprint == self.hot_signer_fingerprint
|
||||
|| key.source.is_token()
|
||||
|| !self
|
||||
.path_kind
|
||||
.can_choose_key_source_kind(&key.source.kind())
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(view::key_list_view(
|
||||
i,
|
||||
&key.name,
|
||||
&key.fingerprint,
|
||||
key.device_kind.as_ref(),
|
||||
key.device_version.as_ref(),
|
||||
key.source.device_kind(),
|
||||
key.source.device_version(),
|
||||
Some(key.fingerprint) == chosen_signer,
|
||||
self.device_must_support_tapminiscript,
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
provider_keys
|
||||
.iter()
|
||||
.map(|(i, pk)| {
|
||||
view::provider_key_list_view(*i, pk, Some(pk.fingerprint) == chosen_signer)
|
||||
})
|
||||
.collect(),
|
||||
self.error.as_ref(),
|
||||
self.chosen_signer.as_ref().map(|s| s.fingerprint),
|
||||
self.chosen_key_source_kind.as_ref(),
|
||||
&self.hot_signer_fingerprint,
|
||||
self.keys.iter().find_map(|k| {
|
||||
if k.fingerprint == self.hot_signer_fingerprint {
|
||||
@ -400,7 +505,8 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
}),
|
||||
&self.form_name,
|
||||
&self.form_xpub,
|
||||
self.manually_imported_xpub,
|
||||
&self.form_token,
|
||||
self.form_token_warning.as_ref(),
|
||||
self.duplicate_master_fg,
|
||||
)
|
||||
}
|
||||
|
||||
@ -20,19 +20,21 @@ use liana_ui::{
|
||||
widget::Element,
|
||||
};
|
||||
|
||||
use crate::installer::context::DescriptorTemplate;
|
||||
use crate::{
|
||||
app::settings::KeySetting,
|
||||
hw::HardwareWallets,
|
||||
installer::{
|
||||
context::DescriptorTemplate,
|
||||
descriptor::{Key, Path, PathKind, PathSequence, PathWarning},
|
||||
message::{self, Message},
|
||||
step::{Context, Step},
|
||||
view,
|
||||
},
|
||||
services::api::KeyKind,
|
||||
signer::Signer,
|
||||
};
|
||||
|
||||
use key::{new_multixkey_from_xpub, EditXpubModal, Key};
|
||||
use key::{new_multixkey_from_xpub, EditXpubModal};
|
||||
|
||||
pub trait DescriptorEditModal {
|
||||
fn processing(&self) -> bool {
|
||||
@ -47,55 +49,6 @@ pub trait DescriptorEditModal {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Path {
|
||||
keys: Vec<Option<Fingerprint>>,
|
||||
threshold: usize,
|
||||
// sequence is 0 if it is a primary path.
|
||||
sequence: u16,
|
||||
duplicate_sequence: bool,
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn new_primary_path() -> Self {
|
||||
Self {
|
||||
keys: vec![None],
|
||||
threshold: 1,
|
||||
sequence: 0,
|
||||
duplicate_sequence: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_recovery_path() -> Self {
|
||||
Self {
|
||||
keys: vec![None],
|
||||
threshold: 1,
|
||||
sequence: u16::MAX,
|
||||
duplicate_sequence: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_n_keys(mut self, n: usize) -> Self {
|
||||
self.keys = Vec::new();
|
||||
for _i in 0..n {
|
||||
self.keys.push(None);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_threshold(mut self, t: usize) -> Self {
|
||||
self.threshold = if t > self.keys.len() {
|
||||
self.keys.len()
|
||||
} else {
|
||||
t
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
fn valid(&self) -> bool {
|
||||
!self.keys.is_empty() && !self.keys.iter().any(|k| k.is_none()) && !self.duplicate_sequence
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefineDescriptor {
|
||||
network: Network,
|
||||
use_taproot: bool,
|
||||
@ -128,32 +81,35 @@ impl DefineDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
fn path_keys<'a>(&'a self, p: &Path) -> Vec<Option<&'a Key>> {
|
||||
p.keys
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if let Some(f) = f {
|
||||
self.keys.get(f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn check_for_duplicate(&mut self) {
|
||||
fn check_for_warning(&mut self) {
|
||||
let mut all_sequence = HashSet::new();
|
||||
let mut duplicate_sequences = HashSet::new();
|
||||
for path in &mut self.paths {
|
||||
if all_sequence.contains(&path.sequence) {
|
||||
duplicate_sequences.insert(path.sequence);
|
||||
if all_sequence.contains(&path.sequence.as_u16()) {
|
||||
duplicate_sequences.insert(path.sequence.as_u16());
|
||||
} else {
|
||||
all_sequence.insert(path.sequence);
|
||||
all_sequence.insert(path.sequence.as_u16());
|
||||
}
|
||||
}
|
||||
|
||||
for path in &mut self.paths {
|
||||
path.duplicate_sequence = duplicate_sequences.contains(&path.sequence);
|
||||
if duplicate_sequences.contains(&path.sequence.as_u16()) {
|
||||
path.warning = Some(PathWarning::DuplicateSequence);
|
||||
} else if path.keys.iter().all(|key| {
|
||||
// All keys must be Some for warning to apply.
|
||||
key.as_ref()
|
||||
.is_some_and(|k| k.source.provider_key_kind() == Some(KeyKind::Cosigner))
|
||||
}) {
|
||||
path.warning = Some(PathWarning::OnlyCosignerKeys);
|
||||
} else if path
|
||||
.keys
|
||||
.iter()
|
||||
.flatten() // can ignore None
|
||||
.any(|key| !path.kind().can_choose_key_source_kind(&key.source.kind()))
|
||||
{
|
||||
path.warning = Some(PathWarning::KeySourceKindDisallowed);
|
||||
} else {
|
||||
path.warning = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,18 +117,15 @@ impl DefineDescriptor {
|
||||
!self.paths.iter().any(|path| {
|
||||
!path.valid()
|
||||
|| (self.use_taproot
|
||||
&& path.keys.iter().any(|k| {
|
||||
if let Some(k) = k.and_then(|k| self.keys.get(&k)) {
|
||||
!k.is_compatible_taproot
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}))
|
||||
&& path
|
||||
.keys
|
||||
.iter()
|
||||
.any(|k| !k.as_ref().is_some_and(|k| k.source.is_compatible_taproot())))
|
||||
}) && self.paths.len() >= 2
|
||||
}
|
||||
|
||||
fn check_setup(&mut self) {
|
||||
self.check_for_duplicate();
|
||||
self.check_for_warning();
|
||||
}
|
||||
|
||||
fn load_template(&mut self, template: DescriptorTemplate) {
|
||||
@ -218,27 +171,33 @@ impl Step for DefineDescriptor {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::AddRecoveryPath) => {
|
||||
self.paths.push(Path::new_recovery_path());
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::AddSafetyNetPath) => {
|
||||
if !self.paths.iter().any(|p| p.kind() == PathKind::SafetyNet) {
|
||||
self.paths.push(Path::new_safety_net_path());
|
||||
}
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeysEdited(coordinate, key)) => {
|
||||
hws.set_alias(key.fingerprint, key.name.clone());
|
||||
for (i, j) in coordinate {
|
||||
self.paths[i].keys[j] = Some(key.fingerprint);
|
||||
self.paths[i].keys[j] = Some(key.clone());
|
||||
}
|
||||
self.keys.insert(key.fingerprint, key);
|
||||
self.modal = None;
|
||||
self.check_setup();
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(coordinate)) => {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(
|
||||
path_kind,
|
||||
coordinate,
|
||||
)) => {
|
||||
let use_taproot = self.use_taproot;
|
||||
let mut set = HashSet::<Fingerprint>::new();
|
||||
let key = coordinate
|
||||
.first()
|
||||
.and_then(|(i, j)| self.paths[*i].keys[*j])
|
||||
.and_then(|f| self.keys.get(&f))
|
||||
.cloned();
|
||||
.and_then(|(i, j)| self.paths[*i].keys[*j].clone());
|
||||
for (i, j) in &coordinate {
|
||||
set.extend(self.paths[*i].keys.iter().filter_map(|key| {
|
||||
if key.is_some() && key != &self.paths[*i].keys[*j] {
|
||||
*key
|
||||
set.extend(self.paths[*i].keys.iter().flatten().filter_map(|key| {
|
||||
if Some(key) != self.paths[*i].keys[*j].as_ref() {
|
||||
Some(key.fingerprint)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -246,6 +205,7 @@ impl Step for DefineDescriptor {
|
||||
}
|
||||
let modal = EditXpubModal::new(
|
||||
use_taproot,
|
||||
path_kind,
|
||||
set,
|
||||
key,
|
||||
coordinate,
|
||||
@ -258,89 +218,97 @@ impl Step for DefineDescriptor {
|
||||
self.modal = Some(Box::new(modal));
|
||||
return cmd;
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) => match msg {
|
||||
message::DefinePath::SequenceEdited(seq) => {
|
||||
self.modal = None;
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.sequence = seq;
|
||||
}
|
||||
self.check_for_duplicate();
|
||||
}
|
||||
message::DefinePath::ThresholdEdited(t) => {
|
||||
self.modal = None;
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.threshold = t;
|
||||
}
|
||||
}
|
||||
message::DefinePath::EditSequence => {
|
||||
if let Some(path) = self.paths.get(i) {
|
||||
self.modal = Some(Box::new(EditSequenceModal::new(i, path.sequence)));
|
||||
}
|
||||
}
|
||||
message::DefinePath::EditThreshold => {
|
||||
if let Some(path) = self.paths.get(i) {
|
||||
self.modal = Some(Box::new(EditThresholdModal::new(
|
||||
i,
|
||||
(path.threshold, path.keys.len()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
message::DefinePath::AddKey => {
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.keys.push(None);
|
||||
path.threshold += 1;
|
||||
}
|
||||
}
|
||||
message::DefinePath::Key(j, msg) => match msg {
|
||||
message::DefineKey::Clipboard(key) => {
|
||||
return Task::perform(async move { key }, Message::Clibpboard);
|
||||
}
|
||||
|
||||
message::DefineKey::Edit => {
|
||||
let use_taproot = self.use_taproot;
|
||||
let path = &self.paths[i];
|
||||
let modal = EditXpubModal::new(
|
||||
use_taproot,
|
||||
HashSet::from_iter(path.keys.iter().filter_map(|key| {
|
||||
if key.is_some() && key != &path.keys[j] {
|
||||
*key
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
path.keys[j].and_then(|f| self.keys.get(&f)).cloned(),
|
||||
vec![(i, j)],
|
||||
self.network,
|
||||
self.signer.clone(),
|
||||
self.signer_fingerprint,
|
||||
self.keys.values().cloned().collect(),
|
||||
);
|
||||
let cmd = modal.load();
|
||||
self.modal = Some(Box::new(modal));
|
||||
return cmd;
|
||||
}
|
||||
message::DefineKey::Delete => {
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.keys.remove(j);
|
||||
if path.threshold > path.keys.len() {
|
||||
path.threshold -= 1;
|
||||
}
|
||||
}
|
||||
// Only delete recovery paths.
|
||||
if i > 0
|
||||
&& self
|
||||
.paths
|
||||
.get(i)
|
||||
.map(|path| path.keys.is_empty())
|
||||
.unwrap_or(false)
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) => {
|
||||
match msg {
|
||||
message::DefinePath::SequenceEdited(seq) => {
|
||||
self.modal = None;
|
||||
if let Some(Path {
|
||||
sequence: PathSequence::Recovery(s),
|
||||
..
|
||||
}) = self.paths.get_mut(i)
|
||||
{
|
||||
self.paths.remove(i);
|
||||
*s = seq;
|
||||
}
|
||||
self.check_setup();
|
||||
self.check_for_warning();
|
||||
}
|
||||
},
|
||||
},
|
||||
message::DefinePath::ThresholdEdited(t) => {
|
||||
self.modal = None;
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.threshold = t;
|
||||
}
|
||||
}
|
||||
message::DefinePath::EditSequence => {
|
||||
if let Some(path) = self.paths.get(i) {
|
||||
self.modal = Some(Box::new(EditSequenceModal::new(i, path.sequence)));
|
||||
}
|
||||
}
|
||||
message::DefinePath::EditThreshold => {
|
||||
if let Some(path) = self.paths.get(i) {
|
||||
self.modal = Some(Box::new(EditThresholdModal::new(
|
||||
i,
|
||||
(path.threshold, path.keys.len()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
message::DefinePath::AddKey => {
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.keys.push(None);
|
||||
path.threshold += 1;
|
||||
self.check_for_warning();
|
||||
}
|
||||
}
|
||||
message::DefinePath::Key(j, msg) => match msg {
|
||||
message::DefineKey::Clipboard(key) => {
|
||||
return Task::perform(async move { key }, Message::Clibpboard);
|
||||
}
|
||||
|
||||
message::DefineKey::Edit => {
|
||||
let use_taproot = self.use_taproot;
|
||||
let path = &self.paths[i];
|
||||
let modal = EditXpubModal::new(
|
||||
use_taproot,
|
||||
path.kind(),
|
||||
HashSet::from_iter(path.keys.iter().flatten().filter_map(|key| {
|
||||
if Some(key) != path.keys[j].as_ref() {
|
||||
Some(key.fingerprint)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
path.keys[j].clone(),
|
||||
vec![(i, j)],
|
||||
self.network,
|
||||
self.signer.clone(),
|
||||
self.signer_fingerprint,
|
||||
self.keys.values().cloned().collect(),
|
||||
);
|
||||
let cmd = modal.load();
|
||||
self.modal = Some(Box::new(modal));
|
||||
return cmd;
|
||||
}
|
||||
message::DefineKey::Delete => {
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.keys.remove(j);
|
||||
if path.threshold > path.keys.len() {
|
||||
path.threshold -= 1;
|
||||
}
|
||||
}
|
||||
// Only delete non-primary paths.
|
||||
if i > 0 // we could alternatively check `path_kind != PathKind::Primary`
|
||||
&& self
|
||||
.paths
|
||||
.get(i)
|
||||
.map(|path| path.keys.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.paths.remove(i);
|
||||
}
|
||||
self.check_setup();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(modal) = &mut self.modal {
|
||||
return modal.update(hws, message);
|
||||
@ -364,23 +332,30 @@ impl Step for DefineDescriptor {
|
||||
}
|
||||
|
||||
ctx.bitcoin_config.network = self.network;
|
||||
ctx.keys = Vec::new();
|
||||
ctx.keys = HashMap::new();
|
||||
let mut hw_is_used = false;
|
||||
let mut spending_keys: Vec<DescriptorPublicKey> = Vec::new();
|
||||
let mut key_derivation_index = HashMap::<Fingerprint, usize>::new();
|
||||
for spending_key in self.paths[0].keys.iter().clone() {
|
||||
let fingerprint = spending_key.expect("Must be present at this step");
|
||||
let fingerprint = spending_key
|
||||
.as_ref()
|
||||
.expect("Must be present at this step")
|
||||
.fingerprint;
|
||||
let key = self
|
||||
.keys
|
||||
.get(&fingerprint)
|
||||
.expect("Must be present at this step");
|
||||
if let DescriptorPublicKey::XPub(xpub) = &key.key {
|
||||
if let Some((master_fingerprint, _)) = xpub.origin {
|
||||
ctx.keys.push(KeySetting {
|
||||
ctx.keys.insert(
|
||||
master_fingerprint,
|
||||
name: key.name.clone(),
|
||||
});
|
||||
if key.device_kind.is_some() {
|
||||
KeySetting {
|
||||
master_fingerprint,
|
||||
name: key.name.clone(),
|
||||
provider_key: key.source.provider_key(),
|
||||
},
|
||||
);
|
||||
if key.source.device_kind().is_some() {
|
||||
hw_is_used = true;
|
||||
}
|
||||
}
|
||||
@ -398,18 +373,25 @@ impl Step for DefineDescriptor {
|
||||
for path in &self.paths[1..] {
|
||||
let mut recovery_keys: Vec<DescriptorPublicKey> = Vec::new();
|
||||
for recovery_key in path.keys.iter().clone() {
|
||||
let fingerprint = recovery_key.expect("Must be present at this step");
|
||||
let fingerprint = recovery_key
|
||||
.as_ref()
|
||||
.expect("Must be present at this step")
|
||||
.fingerprint;
|
||||
let key = self
|
||||
.keys
|
||||
.get(&fingerprint)
|
||||
.expect("Must be present at this step");
|
||||
if let DescriptorPublicKey::XPub(xpub) = &key.key {
|
||||
if let Some((master_fingerprint, _)) = xpub.origin {
|
||||
ctx.keys.push(KeySetting {
|
||||
ctx.keys.insert(
|
||||
master_fingerprint,
|
||||
name: key.name.clone(),
|
||||
});
|
||||
if key.device_kind.is_some() {
|
||||
KeySetting {
|
||||
master_fingerprint,
|
||||
name: key.name.clone(),
|
||||
provider_key: key.source.provider_key(),
|
||||
},
|
||||
);
|
||||
if key.source.device_kind().is_some() {
|
||||
hw_is_used = true;
|
||||
}
|
||||
}
|
||||
@ -429,7 +411,7 @@ impl Step for DefineDescriptor {
|
||||
PathInfo::Multi(path.threshold, recovery_keys)
|
||||
};
|
||||
|
||||
recovery_paths.insert(path.sequence, recovery_keys);
|
||||
recovery_paths.insert(path.sequence.as_u16(), recovery_keys);
|
||||
}
|
||||
|
||||
if spending_keys.is_empty() {
|
||||
@ -470,13 +452,8 @@ impl Step for DefineDescriptor {
|
||||
view::editor::template::inheritance::inheritance_template(
|
||||
progress,
|
||||
self.use_taproot,
|
||||
self.paths[0].keys[0]
|
||||
.as_ref()
|
||||
.and_then(|f| self.keys.get(f)),
|
||||
self.paths[1].keys[0]
|
||||
.as_ref()
|
||||
.and_then(|f| self.keys.get(f)),
|
||||
self.paths[1].sequence,
|
||||
&self.paths[0],
|
||||
&self.paths[1],
|
||||
self.valid(),
|
||||
)
|
||||
}
|
||||
@ -484,31 +461,24 @@ impl Step for DefineDescriptor {
|
||||
view::editor::template::multisig_security_wallet::multisig_security_template(
|
||||
progress,
|
||||
self.use_taproot,
|
||||
self.path_keys(&self.paths[0]),
|
||||
self.path_keys(&self.paths[1]),
|
||||
self.paths[1].sequence,
|
||||
self.paths[1].threshold,
|
||||
&self.paths[0],
|
||||
&self.paths[1],
|
||||
self.valid(),
|
||||
)
|
||||
}
|
||||
DescriptorTemplate::Custom => view::editor::template::custom::custom_template(
|
||||
progress,
|
||||
self.use_taproot,
|
||||
view::editor::template::custom::Path {
|
||||
keys: self.path_keys(&self.paths[0]),
|
||||
sequence: self.paths[0].sequence,
|
||||
duplicate_sequence: self.paths[0].duplicate_sequence,
|
||||
threshold: self.paths[0].threshold,
|
||||
},
|
||||
&self.paths[0],
|
||||
&mut self.paths[1..]
|
||||
.iter()
|
||||
.map(|p| view::editor::template::custom::Path {
|
||||
sequence: p.sequence,
|
||||
duplicate_sequence: p.duplicate_sequence,
|
||||
threshold: p.threshold,
|
||||
keys: self.path_keys(p),
|
||||
}),
|
||||
self.paths.len().saturating_sub(1), // subtract 1 for primary path
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.kind() == PathKind::Recovery),
|
||||
self.paths[1..]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, p)| p.kind() == PathKind::SafetyNet),
|
||||
self.paths[1..].len(),
|
||||
self.valid(),
|
||||
),
|
||||
};
|
||||
@ -538,11 +508,11 @@ pub struct EditSequenceModal {
|
||||
}
|
||||
|
||||
impl EditSequenceModal {
|
||||
pub fn new(path_index: usize, sequence: u16) -> Self {
|
||||
pub fn new(path_index: usize, path_sequence: PathSequence) -> Self {
|
||||
Self {
|
||||
path_index,
|
||||
sequence: form::Value {
|
||||
value: sequence.to_string(),
|
||||
value: path_sequence.as_u16().to_string(),
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
@ -657,6 +627,8 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::installer::descriptor::KeySource;
|
||||
|
||||
pub struct Sandbox<S: Step> {
|
||||
step: Arc<Mutex<S>>,
|
||||
}
|
||||
@ -788,10 +760,7 @@ mod tests {
|
||||
name: "My Specter key".to_string(),
|
||||
fingerprint: key.master_fingerprint(),
|
||||
key,
|
||||
device_kind: Some(async_hwi::DeviceKind::Specter),
|
||||
device_version: None,
|
||||
is_compatible_taproot: false,
|
||||
is_hot_signer: false,
|
||||
source: KeySource::Device(async_hwi::DeviceKind::Specter, None),
|
||||
};
|
||||
|
||||
// Use Specter device for primary key
|
||||
|
||||
@ -16,7 +16,7 @@ use liana_ui::{component::form, widget::Element};
|
||||
use async_hwi::DeviceKind;
|
||||
|
||||
use crate::{
|
||||
app::wallet::wallet_name,
|
||||
app::{settings::KeySetting, wallet::wallet_name},
|
||||
hw::{HardwareWallet, HardwareWallets},
|
||||
installer::{
|
||||
message::{self, Message},
|
||||
@ -169,7 +169,7 @@ impl Step for RegisterDescriptor {
|
||||
}
|
||||
self.descriptor.clone_from(&ctx.descriptor);
|
||||
let mut map = HashMap::new();
|
||||
for key in ctx.keys.iter().filter(|k| !k.name.is_empty()) {
|
||||
for key in ctx.keys.values().filter(|k| !k.name.is_empty()) {
|
||||
map.insert(key.master_fingerprint, key.name.clone());
|
||||
}
|
||||
}
|
||||
@ -291,7 +291,7 @@ impl From<RegisterDescriptor> for Box<dyn Step> {
|
||||
pub struct BackupDescriptor {
|
||||
done: bool,
|
||||
descriptor: Option<LianaDescriptor>,
|
||||
key_aliases: HashMap<Fingerprint, String>,
|
||||
keys: HashMap<Fingerprint, KeySetting>,
|
||||
}
|
||||
|
||||
impl Step for BackupDescriptor {
|
||||
@ -306,12 +306,11 @@ impl Step for BackupDescriptor {
|
||||
self.descriptor.clone_from(&ctx.descriptor);
|
||||
self.done = false;
|
||||
}
|
||||
self.key_aliases = ctx
|
||||
self.keys = ctx
|
||||
.keys
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|k| (k.master_fingerprint, k.name))
|
||||
.collect()
|
||||
.values()
|
||||
.map(|k| (k.master_fingerprint, k.clone()))
|
||||
.collect();
|
||||
}
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
@ -323,7 +322,7 @@ impl Step for BackupDescriptor {
|
||||
progress,
|
||||
email,
|
||||
self.descriptor.as_ref().expect("Must be a descriptor"),
|
||||
&self.key_aliases,
|
||||
&self.keys,
|
||||
self.done,
|
||||
)
|
||||
}
|
||||
|
||||
@ -19,17 +19,20 @@ pub use descriptor::{
|
||||
pub use backend::{ChooseBackend, ImportRemoteWallet, RemoteBackendLogin};
|
||||
pub use mnemonic::{BackupMnemonic, RecoverMnemonic};
|
||||
pub use share_xpubs::ShareXpubs;
|
||||
use tracing::warn;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use iced::{Subscription, Task};
|
||||
|
||||
use liana_ui::widget::*;
|
||||
|
||||
use crate::{
|
||||
app::settings::ProviderKey,
|
||||
hw::HardwareWallets,
|
||||
installer::{context::Context, message::Message, view},
|
||||
node::bitcoind::Bitcoind,
|
||||
services,
|
||||
};
|
||||
|
||||
pub trait Step {
|
||||
@ -65,6 +68,7 @@ pub struct Final {
|
||||
internal_bitcoind: Option<Bitcoind>,
|
||||
warning: Option<String>,
|
||||
config_path: Option<PathBuf>,
|
||||
key_redemptions: HashMap<ProviderKey, Option<Result<(), services::Error>>>,
|
||||
}
|
||||
|
||||
impl Final {
|
||||
@ -74,6 +78,7 @@ impl Final {
|
||||
generating: false,
|
||||
warning: None,
|
||||
config_path: None,
|
||||
key_redemptions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -87,6 +92,11 @@ impl Default for Final {
|
||||
impl Step for Final {
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
self.internal_bitcoind.clone_from(&ctx.internal_bitcoind);
|
||||
self.key_redemptions = ctx
|
||||
.keys
|
||||
.values()
|
||||
.filter_map(|ks| ks.provider_key.as_ref().map(|pk| (pk.clone(), None)))
|
||||
.collect();
|
||||
}
|
||||
fn load(&self) -> Task<Message> {
|
||||
if !self.generating && self.config_path.is_none() {
|
||||
@ -97,24 +107,62 @@ impl Step for Final {
|
||||
}
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Installed(res) => {
|
||||
Message::RedeemNextKey => {
|
||||
if let Some((pk, _)) = self.key_redemptions.iter().find(|(_, v)| v.is_none()) {
|
||||
let client = services::Client::new();
|
||||
let pk = pk.clone();
|
||||
return Task::perform(
|
||||
async move { (pk.clone(), client.redeem_key(pk.uuid, pk.token).await) },
|
||||
|(pk, res)| Message::KeyRedeemed(pk, res.map(|_| ())),
|
||||
);
|
||||
}
|
||||
return Task::perform(async move {}, |_| Message::AllKeysRedeemed);
|
||||
}
|
||||
Message::KeyRedeemed(pk, res) => {
|
||||
if let Some(v) = self.key_redemptions.get_mut(&pk) {
|
||||
*v = Some(res);
|
||||
}
|
||||
return Task::perform(async move {}, |_| Message::RedeemNextKey);
|
||||
}
|
||||
Message::AllKeysRedeemed => {
|
||||
self.generating = false;
|
||||
match res {
|
||||
Err(e) => {
|
||||
self.config_path = None;
|
||||
self.warning = Some(e.to_string());
|
||||
}
|
||||
Ok(path) => {
|
||||
self.config_path = Some(path.clone());
|
||||
let internal_bitcoind = self.internal_bitcoind.clone();
|
||||
let path = path.clone();
|
||||
return Task::perform(
|
||||
async { (path, internal_bitcoind) },
|
||||
|(path, internal_bitcoind)| Message::Exit(path, internal_bitcoind),
|
||||
);
|
||||
// If any errors occurred redeeming tokens, add a warning to the log.
|
||||
let mut has_error = false;
|
||||
for (pk, res) in &self.key_redemptions {
|
||||
if let Some(res) = res {
|
||||
if let Err(e) = res {
|
||||
warn!("Error redeeming key for token '{}': '{}'.", pk.token, e);
|
||||
has_error = true;
|
||||
}
|
||||
} else {
|
||||
// We expect to have all redemption results by now.
|
||||
warn!("Missing redemption info for token '{}'.", pk.token);
|
||||
has_error = true;
|
||||
}
|
||||
}
|
||||
// Now exit the installer whether or not any redemption errors occurred.
|
||||
let internal_bitcoind = self.internal_bitcoind.clone();
|
||||
let path = self.config_path.clone().expect("config path already set");
|
||||
// If there were any errors, don't remove the installer log.
|
||||
return Task::perform(
|
||||
async move { (path, internal_bitcoind, has_error) },
|
||||
|(path, internal_bitcoind, has_error)| {
|
||||
Message::Exit(path, internal_bitcoind, !has_error)
|
||||
},
|
||||
);
|
||||
}
|
||||
Message::Installed(res) => match res {
|
||||
Err(e) => {
|
||||
self.generating = false;
|
||||
self.config_path = None;
|
||||
self.warning = Some(e.to_string());
|
||||
}
|
||||
Ok(path) => {
|
||||
self.config_path = Some(path.clone());
|
||||
// Now redeem any provider keys.
|
||||
return Task::perform(async move {}, |_| Message::RedeemNextKey);
|
||||
}
|
||||
},
|
||||
Message::Install => {
|
||||
self.generating = true;
|
||||
self.config_path = None;
|
||||
|
||||
@ -24,8 +24,9 @@ use liana_ui::{
|
||||
};
|
||||
|
||||
use crate::installer::{
|
||||
descriptor::{KeySourceKind, PathKind, PathSequence, PathWarning},
|
||||
message::{self, Message},
|
||||
prompt,
|
||||
prompt, services,
|
||||
view::defined_sequence,
|
||||
Error,
|
||||
};
|
||||
@ -89,8 +90,8 @@ pub fn define_descriptor_advanced_settings<'a>(use_taproot: bool) -> Element<'a,
|
||||
pub fn path(
|
||||
color: iced::Color,
|
||||
title: Option<String>,
|
||||
sequence: u16,
|
||||
duplicate_sequence: bool,
|
||||
sequence: PathSequence,
|
||||
warning: Option<PathWarning>,
|
||||
threshold: usize,
|
||||
keys: Vec<Element<message::DefinePath>>,
|
||||
fixed: bool,
|
||||
@ -100,7 +101,7 @@ pub fn path(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push_maybe(title.map(|t| Row::new().push(Space::with_width(10)).push(p1_bold(t))))
|
||||
.push(defined_sequence(sequence, duplicate_sequence))
|
||||
.push(defined_sequence(sequence, warning))
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
@ -119,8 +120,15 @@ pub fn path(
|
||||
.spacing(10)
|
||||
.push(defined_threshold(color, fixed, (threshold, keys_len)))
|
||||
.push(
|
||||
button::secondary(Some(icon::plus_icon()), "Add key")
|
||||
.on_press(message::DefinePath::AddKey),
|
||||
button::secondary(
|
||||
Some(icon::plus_icon()),
|
||||
if sequence.path_kind() == PathKind::SafetyNet {
|
||||
"Add Safety Net key"
|
||||
} else {
|
||||
"Add key"
|
||||
},
|
||||
)
|
||||
.on_press(message::DefinePath::AddKey),
|
||||
),
|
||||
)
|
||||
}),
|
||||
@ -251,19 +259,97 @@ pub fn undefined_key<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
fn maybe_key_from_token<'a>(
|
||||
path_kind: PathKind,
|
||||
chosen_key_source_kind: Option<&KeySourceKind>,
|
||||
has_chosen_signer: bool,
|
||||
form_token: &form::Value<String>,
|
||||
form_token_warning: Option<&'a String>,
|
||||
key_kind: services::api::KeyKind,
|
||||
) -> Option<Element<'a, Message>> {
|
||||
if !path_kind.can_choose_key_source_kind(&KeySourceKind::Token(key_kind)) {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
match (chosen_key_source_kind, has_chosen_signer) {
|
||||
(Some(KeySourceKind::Token(key_kind)), false) => card::simple(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
Row::new()
|
||||
.align_y(Alignment::Center)
|
||||
.push(
|
||||
p1_regular(format!("Enter a {key_kind} token:"))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(image::success_mark_icon().width(Length::Fixed(50.0))),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
form::Form::new_trimmed("", form_token, |msg| {
|
||||
Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::TokenEdited(msg),
|
||||
),
|
||||
)
|
||||
})
|
||||
.maybe_warning(form_token_warning.map(|w| w.as_str()))
|
||||
.size(text::P1_SIZE)
|
||||
.padding(10),
|
||||
)
|
||||
.push(button::primary(None, "Confirm").on_press_maybe(
|
||||
(!form_token.value.is_empty() && form_token.valid).then_some(
|
||||
Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::ConfirmToken,
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
.spacing(10),
|
||||
),
|
||||
),
|
||||
_ => Container::new(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(10)
|
||||
.push(icon::import_icon())
|
||||
.push(p1_regular(format!("Enter a {key_kind} token"))),
|
||||
)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(message::ImportKeyModal::UseToken(
|
||||
key_kind,
|
||||
)),
|
||||
))
|
||||
.style(theme::button::secondary),
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn edit_key_modal<'a>(
|
||||
title: &'a str,
|
||||
network: bitcoin::Network,
|
||||
path_kind: PathKind,
|
||||
hws: Vec<Element<'a, Message>>,
|
||||
keys: Vec<Element<'a, Message>>,
|
||||
provider_keys: Vec<Element<'a, Message>>,
|
||||
error: Option<&Error>,
|
||||
chosen_signer: Option<Fingerprint>,
|
||||
chosen_key_source_kind: Option<&KeySourceKind>,
|
||||
hot_signer_fingerprint: &Fingerprint,
|
||||
signer_alias: Option<&'a String>,
|
||||
form_name: &'a form::Value<String>,
|
||||
form_xpub: &form::Value<String>,
|
||||
manually_imported_xpub: bool,
|
||||
form_token: &form::Value<String>,
|
||||
form_token_warning: Option<&'a String>,
|
||||
duplicate_master_fg: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let content = Column::new()
|
||||
@ -280,26 +366,28 @@ pub fn edit_key_modal<'a>(
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.push(p1_regular("Select the signing device for your key"))
|
||||
.push(p1_regular("Select the source of your key"))
|
||||
.spacing(10)
|
||||
.push(
|
||||
Column::with_children(hws).spacing(10)
|
||||
)
|
||||
.push(
|
||||
Column::with_children(keys).spacing(10)
|
||||
)
|
||||
.push(
|
||||
Button::new(if Some(*hot_signer_fingerprint) == chosen_signer {
|
||||
.push(Column::with_children(hws).spacing(10))
|
||||
.push(Column::with_children(keys).spacing(10))
|
||||
.push(Column::with_children(provider_keys).spacing(10))
|
||||
.push_maybe(if !path_kind.can_choose_key_source_kind(&KeySourceKind::HotSigner) {
|
||||
None
|
||||
} else {
|
||||
Some(Button::new(if Some(*hot_signer_fingerprint) == chosen_signer {
|
||||
hw::selected_hot_signer(hot_signer_fingerprint, signer_alias)
|
||||
} else {
|
||||
hw::unselected_hot_signer(hot_signer_fingerprint, signer_alias)
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::UseHotSigner)
|
||||
.style(theme::button::secondary),
|
||||
.style(theme::button::secondary))
|
||||
}
|
||||
)
|
||||
.push(if manually_imported_xpub {
|
||||
card::simple(Column::new()
|
||||
.push_maybe(if !path_kind.can_choose_key_source_kind(&KeySourceKind::Manual) {
|
||||
None
|
||||
} else if chosen_key_source_kind == Some(&KeySourceKind::Manual) && chosen_signer.is_none() {
|
||||
Some(card::simple(Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
Row::new()
|
||||
@ -326,25 +414,27 @@ pub fn edit_key_modal<'a>(
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10)
|
||||
))
|
||||
} else {
|
||||
Container::new(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(10)
|
||||
.push(icon::import_icon())
|
||||
.push(p1_regular("Enter an extended public key"))
|
||||
)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(message::ImportKeyModal::ManuallyImportXpub)
|
||||
))
|
||||
.style(theme::button::secondary),
|
||||
)
|
||||
)))
|
||||
} else {
|
||||
Some(Container::new(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(10)
|
||||
.push(icon::import_icon())
|
||||
.push(p1_regular("Enter an extended public key"))
|
||||
)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(message::ImportKeyModal::ManuallyImportXpub)
|
||||
))
|
||||
.style(theme::button::secondary),
|
||||
))
|
||||
}
|
||||
)
|
||||
)
|
||||
.push_maybe(maybe_key_from_token(path_kind, chosen_key_source_kind, chosen_signer.is_some(), form_token, form_token_warning, services::api::KeyKind::SafetyNet))
|
||||
.push_maybe(maybe_key_from_token(path_kind, chosen_key_source_kind, chosen_signer.is_some(), form_token, form_token_warning, services::api::KeyKind::Cosigner))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push_maybe(
|
||||
@ -382,14 +472,16 @@ pub fn edit_key_modal<'a>(
|
||||
.push(
|
||||
button::primary(None, "Apply")
|
||||
.on_press_maybe(if !duplicate_master_fg
|
||||
&& (!manually_imported_xpub || form_xpub.valid)
|
||||
&& !form_name.value.is_empty() && form_name.valid {
|
||||
Some(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::ConfirmXpub,
|
||||
),
|
||||
))
|
||||
} else {None})
|
||||
&& !form_name.value.is_empty() && form_name.valid
|
||||
&& chosen_signer.is_some() {
|
||||
Some(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::ConfirmXpub,
|
||||
)
|
||||
))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.width(Length::Fixed(200.0))
|
||||
)
|
||||
.align_x(Alignment::Center),
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
use iced::{alignment, widget::Space, Alignment, Length};
|
||||
use iced::{
|
||||
alignment,
|
||||
widget::{tooltip, Container, Space},
|
||||
Alignment, Length,
|
||||
};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
@ -11,14 +15,16 @@ use liana_ui::{
|
||||
};
|
||||
|
||||
use crate::installer::{
|
||||
descriptor::Path,
|
||||
message::{self, Message},
|
||||
step::descriptor::editor::key::Key,
|
||||
view::{
|
||||
editor::{define_descriptor_advanced_settings, defined_key, path, undefined_key},
|
||||
layout,
|
||||
},
|
||||
};
|
||||
|
||||
const SAFETY_NET_DESCRIPTION: &str = "This adds a final recovery option containing keys from professional key agents.\n\nUse this option if you have been provided one or more Safety Net tokens.";
|
||||
|
||||
pub fn custom_template_description(progress: (usize, usize)) -> Element<'static, Message> {
|
||||
layout(
|
||||
progress,
|
||||
@ -47,19 +53,13 @@ pub fn custom_template_description(progress: (usize, usize)) -> Element<'static,
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Path<'a> {
|
||||
pub keys: Vec<Option<&'a Key>>,
|
||||
pub sequence: u16,
|
||||
pub duplicate_sequence: bool,
|
||||
pub threshold: usize,
|
||||
}
|
||||
|
||||
pub fn custom_template<'a>(
|
||||
progress: (usize, usize),
|
||||
use_taproot: bool,
|
||||
primary_path: Path<'a>,
|
||||
recovery_paths: &mut dyn Iterator<Item = Path<'a>>,
|
||||
num_recovery_paths: usize,
|
||||
primary_path: &'a Path,
|
||||
recovery_paths: &mut dyn Iterator<Item = (usize, &'a Path)>,
|
||||
safety_net_path: Option<(usize, &'a Path)>,
|
||||
num_non_primary_paths: usize,
|
||||
valid: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let prim_keys_fixed = primary_path.keys.len() < 2; // can only delete a primary key if there are 2 or more
|
||||
@ -98,7 +98,7 @@ pub fn custom_template<'a>(
|
||||
color::GREEN,
|
||||
Some("Primary spending option:".to_string()),
|
||||
primary_path.sequence,
|
||||
primary_path.duplicate_sequence,
|
||||
primary_path.warning,
|
||||
primary_path.threshold,
|
||||
primary_path
|
||||
.keys
|
||||
@ -110,7 +110,7 @@ pub fn custom_template<'a>(
|
||||
&key.name,
|
||||
color::GREEN,
|
||||
"Primary key",
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
if use_taproot && !key.source.is_compatible_taproot() {
|
||||
Some("This device does not support Taproot")
|
||||
} else {
|
||||
None
|
||||
@ -134,27 +134,28 @@ pub fn custom_template<'a>(
|
||||
)
|
||||
.push(recovery_paths.into_iter().enumerate().fold(
|
||||
Column::new().spacing(20),
|
||||
|col, (i, p)| {
|
||||
|col, (i, (p_idx, p))| {
|
||||
col.push(
|
||||
path(
|
||||
color::ORANGE,
|
||||
Some(format!("Recovery option #{}:", i + 1)),
|
||||
p.sequence,
|
||||
p.duplicate_sequence,
|
||||
p.warning,
|
||||
p.threshold,
|
||||
p.keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, recovery_key)| {
|
||||
// We cannot delete a key if doing so would remove all recovery paths,
|
||||
// i.e. if there is only 1 recovery path and it contains only 1 key.
|
||||
let fixed = num_recovery_paths < 2 && p.keys.len() < 2;
|
||||
// i.e. if there is only 1 recovery path and it contains only 1 key,
|
||||
// and there is no safety net path.
|
||||
let fixed = num_non_primary_paths < 2 && p.keys.len() < 2;
|
||||
if let Some(key) = recovery_key {
|
||||
defined_key(
|
||||
&key.name,
|
||||
color::ORANGE,
|
||||
"Recovery key",
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
if use_taproot && !key.source.is_compatible_taproot() {
|
||||
Some("This device does not support Taproot")
|
||||
} else {
|
||||
None
|
||||
@ -175,7 +176,10 @@ pub fn custom_template<'a>(
|
||||
false,
|
||||
)
|
||||
.map(move |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(i + 1, msg))
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(
|
||||
p_idx + 1, // add one to index to account for primary path.
|
||||
msg,
|
||||
))
|
||||
}),
|
||||
)
|
||||
},
|
||||
@ -189,12 +193,72 @@ pub fn custom_template<'a>(
|
||||
message::DefineDescriptor::AddRecoveryPath,
|
||||
)),
|
||||
)
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(
|
||||
button::primary(None, "Continue")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press_maybe(if valid { Some(Message::Next) } else { None }),
|
||||
),
|
||||
.push_maybe(
|
||||
safety_net_path.is_none().then_some(tooltip::Tooltip::new(
|
||||
button::secondary(Some(icon::plus_icon()), "Add Safety Net")
|
||||
.width(Length::Fixed(210.0))
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::AddSafetyNetPath,
|
||||
)),
|
||||
Container::new(text(SAFETY_NET_DESCRIPTION))
|
||||
.style(theme::card::simple)
|
||||
.padding(10),
|
||||
tooltip::Position::Bottom,
|
||||
)),
|
||||
)
|
||||
.spacing(10),
|
||||
)
|
||||
.push_maybe(safety_net_path.map(|(sn_index, sn_path)| {
|
||||
path(
|
||||
color::WHITE,
|
||||
Some("Safety Net:".to_string()),
|
||||
sn_path.sequence,
|
||||
sn_path.warning,
|
||||
sn_path.threshold,
|
||||
sn_path
|
||||
.keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, sn_key)| {
|
||||
// Cannot delete safety net key if doing so would remove the safety net path
|
||||
// and there are no other recovery paths.
|
||||
let fixed = num_non_primary_paths < 2 && sn_path.keys.len() < 2;
|
||||
if let Some(key) = sn_key {
|
||||
defined_key(
|
||||
&key.name,
|
||||
color::WHITE,
|
||||
"Safety Net key",
|
||||
if use_taproot && !key.source.is_compatible_taproot() {
|
||||
Some("This key source does not support Taproot")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
fixed,
|
||||
)
|
||||
} else {
|
||||
undefined_key(
|
||||
color::WHITE,
|
||||
"Safety Net key",
|
||||
!sn_path.keys[0..i].iter().any(|k| k.is_none()),
|
||||
fixed,
|
||||
)
|
||||
}
|
||||
.map(move |msg| message::DefinePath::Key(i, msg))
|
||||
})
|
||||
.collect(),
|
||||
false,
|
||||
)
|
||||
.map(move |msg| {
|
||||
// Add 1 to index to account for primary path.
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(sn_index + 1, msg))
|
||||
})
|
||||
}))
|
||||
.push(
|
||||
Row::new().push(Space::with_width(Length::Fill)).push(
|
||||
button::primary(None, "Continue")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press_maybe(if valid { Some(Message::Next) } else { None }),
|
||||
),
|
||||
)
|
||||
.push(Space::with_height(100.0))
|
||||
.spacing(20),
|
||||
|
||||
@ -12,8 +12,8 @@ use liana_ui::{
|
||||
|
||||
use crate::installer::{
|
||||
context,
|
||||
descriptor::{Path, PathSequence},
|
||||
message::{self, Message},
|
||||
step::descriptor::editor::key::Key,
|
||||
view::{
|
||||
editor::{define_descriptor_advanced_settings, defined_key, path, undefined_key},
|
||||
layout,
|
||||
@ -67,11 +67,15 @@ After a period of inactivity (but not before that) your Inheritance Key will bec
|
||||
pub fn inheritance_template<'a>(
|
||||
progress: (usize, usize),
|
||||
use_taproot: bool,
|
||||
primary_key: Option<&'a Key>,
|
||||
recovery_key: Option<&'a Key>,
|
||||
sequence: u16,
|
||||
primary_path: &'a Path,
|
||||
recovery_path: &'a Path,
|
||||
valid: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let primary_key = if let Some(first) = primary_path.keys.first() {
|
||||
first.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
@ -106,15 +110,15 @@ pub fn inheritance_template<'a>(
|
||||
path(
|
||||
color::GREEN,
|
||||
None,
|
||||
0,
|
||||
false,
|
||||
PathSequence::Primary,
|
||||
primary_path.warning,
|
||||
1,
|
||||
vec![if let Some(key) = primary_key {
|
||||
defined_key(
|
||||
&key.name,
|
||||
color::GREEN,
|
||||
"Primary key",
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
if use_taproot && !key.source.is_compatible_taproot() {
|
||||
Some("This device does not support Taproot")
|
||||
} else {
|
||||
None
|
||||
@ -133,15 +137,15 @@ pub fn inheritance_template<'a>(
|
||||
path(
|
||||
color::WHITE,
|
||||
None,
|
||||
sequence,
|
||||
false,
|
||||
recovery_path.sequence,
|
||||
recovery_path.warning,
|
||||
1,
|
||||
vec![if let Some(key) = recovery_key {
|
||||
vec![if let Some(Some(key)) = recovery_path.keys.first() {
|
||||
defined_key(
|
||||
&key.name,
|
||||
color::WHITE,
|
||||
"Inheritance key",
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
if use_taproot && !key.source.is_compatible_taproot() {
|
||||
Some("This device does not support Taproot")
|
||||
} else {
|
||||
None
|
||||
|
||||
@ -12,8 +12,8 @@ use liana_ui::{
|
||||
|
||||
use crate::installer::{
|
||||
context,
|
||||
descriptor::{Path, PathKind, PathSequence},
|
||||
message::{self, Message},
|
||||
step::descriptor::editor::key::Key,
|
||||
view::{
|
||||
editor::{
|
||||
define_descriptor_advanced_settings, defined_key, path, undefined_key,
|
||||
@ -77,10 +77,8 @@ pub fn multisig_security_template_description(
|
||||
pub fn multisig_security_template<'a>(
|
||||
progress: (usize, usize),
|
||||
use_taproot: bool,
|
||||
primary_keys: Vec<Option<&'a Key>>,
|
||||
recovery_keys: Vec<Option<&'a Key>>,
|
||||
sequence: u16,
|
||||
threshold: usize,
|
||||
primary_path: &'a Path,
|
||||
recovery_path: &'a Path,
|
||||
valid: bool,
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
@ -117,10 +115,11 @@ pub fn multisig_security_template<'a>(
|
||||
path(
|
||||
color::GREEN,
|
||||
None,
|
||||
0,
|
||||
false,
|
||||
primary_keys.len(),
|
||||
primary_keys
|
||||
PathSequence::Primary,
|
||||
primary_path.warning,
|
||||
primary_path.keys.len(),
|
||||
primary_path
|
||||
.keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, primary_key)| {
|
||||
@ -129,7 +128,7 @@ pub fn multisig_security_template<'a>(
|
||||
&key.name,
|
||||
color::GREEN,
|
||||
format!("Primary key #{}", i + 1),
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
if use_taproot && !key.source.is_compatible_taproot() {
|
||||
Some("This device does not support Taproot")
|
||||
} else {
|
||||
None
|
||||
@ -140,7 +139,7 @@ pub fn multisig_security_template<'a>(
|
||||
undefined_key(
|
||||
color::GREEN,
|
||||
format!("Primary key #{}", i + 1),
|
||||
!primary_keys[0..i].iter().any(|k| k.is_none()),
|
||||
!primary_path.keys[0..i].iter().any(|k| k.is_none()),
|
||||
true,
|
||||
)
|
||||
}
|
||||
@ -151,10 +150,10 @@ pub fn multisig_security_template<'a>(
|
||||
)
|
||||
.map(move |msg| {
|
||||
if let message::DefinePath::Key(i, message::DefineKey::Edit) = msg {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(vec![
|
||||
(0, i),
|
||||
(1, i),
|
||||
]))
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(
|
||||
PathKind::Primary,
|
||||
vec![(0, i), (1, i)],
|
||||
))
|
||||
} else {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(0, msg))
|
||||
}
|
||||
@ -164,10 +163,11 @@ pub fn multisig_security_template<'a>(
|
||||
path(
|
||||
color::ORANGE,
|
||||
None,
|
||||
sequence,
|
||||
false,
|
||||
threshold,
|
||||
recovery_keys
|
||||
recovery_path.sequence,
|
||||
recovery_path.warning,
|
||||
recovery_path.threshold,
|
||||
recovery_path
|
||||
.keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, recovery_key)| {
|
||||
@ -177,7 +177,7 @@ pub fn multisig_security_template<'a>(
|
||||
&key.name,
|
||||
color::GREEN,
|
||||
format!("Primary key #{}", j + 1),
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
if use_taproot && !key.source.is_compatible_taproot() {
|
||||
Some("This device does not support Taproot")
|
||||
} else {
|
||||
None
|
||||
@ -188,7 +188,7 @@ pub fn multisig_security_template<'a>(
|
||||
&key.name,
|
||||
color::ORANGE,
|
||||
"Recovery key".to_string(),
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
if use_taproot && !key.source.is_compatible_taproot() {
|
||||
Some("This device does not support Taproot")
|
||||
} else {
|
||||
None
|
||||
@ -204,8 +204,8 @@ pub fn multisig_security_template<'a>(
|
||||
} else {
|
||||
"Recovery key".to_string()
|
||||
},
|
||||
!(primary_keys.iter().any(|k| k.is_none())
|
||||
|| recovery_keys[0..j].iter().any(|k| k.is_none())),
|
||||
!(primary_path.keys.iter().any(|k| k.is_none())
|
||||
|| recovery_path.keys[0..j].iter().any(|k| k.is_none())),
|
||||
true,
|
||||
)
|
||||
}
|
||||
@ -216,12 +216,15 @@ pub fn multisig_security_template<'a>(
|
||||
)
|
||||
.map(move |msg| {
|
||||
if let message::DefinePath::Key(i, message::DefineKey::Edit) = msg {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(if i < 2 {
|
||||
vec![(0, i), (1, i)]
|
||||
let (path_kind, keys) = if i < 2 {
|
||||
(PathKind::Primary, vec![(0, i), (1, i)])
|
||||
} else {
|
||||
// recovery path is the path with three keys
|
||||
vec![(1, i)]
|
||||
}))
|
||||
(PathKind::Recovery, vec![(1, i)])
|
||||
};
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(
|
||||
path_kind, keys,
|
||||
))
|
||||
} else {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(1, msg))
|
||||
}
|
||||
|
||||
@ -29,8 +29,10 @@ use liana_ui::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::settings,
|
||||
hw::{is_compatible_with_tapminiscript, HardwareWallet, UnsupportedReason},
|
||||
installer::{
|
||||
descriptor::{Key, PathSequence, PathWarning},
|
||||
message::{self, DefineBitcoind, DefineNode, Message},
|
||||
prompt,
|
||||
step::{DownloadState, InstallState},
|
||||
@ -686,7 +688,7 @@ pub fn backup_descriptor<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
descriptor: &'a LianaDescriptor,
|
||||
keys_aliases: &'a HashMap<Fingerprint, String>,
|
||||
keys: &'a HashMap<Fingerprint, settings::KeySetting>,
|
||||
done: bool,
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
@ -749,7 +751,7 @@ pub fn backup_descriptor<'a>(
|
||||
.max_width(1500),
|
||||
)
|
||||
.push(
|
||||
card::simple(display_policy(descriptor.policy(), keys_aliases))
|
||||
card::simple(display_policy(descriptor.policy(), keys))
|
||||
.width(Length::Fill)
|
||||
.max_width(1500),
|
||||
)
|
||||
@ -772,7 +774,7 @@ pub fn backup_descriptor<'a>(
|
||||
|
||||
fn display_policy(
|
||||
policy: LianaPolicy,
|
||||
keys_aliases: &HashMap<Fingerprint, String>,
|
||||
keys: &HashMap<Fingerprint, settings::KeySetting>,
|
||||
) -> Element<'_, Message> {
|
||||
let (primary_threshold, primary_keys) = policy.primary_path().thresh_origins();
|
||||
// The iteration over an HashMap keys can have a different order at each refresh
|
||||
@ -800,10 +802,10 @@ fn display_policy(
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(Row::new().spacing(5), |row, (i, k)| {
|
||||
let content = if let Some(alias) = keys_aliases.get(k) {
|
||||
let content = if let Some(key) = keys.get(k) {
|
||||
Container::new(
|
||||
iced_tooltip::Tooltip::new(
|
||||
text(alias).bold(),
|
||||
text(key.name.clone()).bold(),
|
||||
text(k.to_string()),
|
||||
iced_tooltip::Position::Bottom,
|
||||
)
|
||||
@ -847,10 +849,10 @@ fn display_policy(
|
||||
.push(recovery_keys.iter().enumerate().fold(
|
||||
Row::new().spacing(5),
|
||||
|row, (i, k)| {
|
||||
let content = if let Some(alias) = keys_aliases.get(k) {
|
||||
let content = if let Some(key) = keys.get(k) {
|
||||
Container::new(
|
||||
iced_tooltip::Tooltip::new(
|
||||
text(alias).bold(),
|
||||
text(key.name.clone()).bold(),
|
||||
text(k.to_string()),
|
||||
iced_tooltip::Position::Bottom,
|
||||
)
|
||||
@ -877,7 +879,18 @@ fn display_policy(
|
||||
))
|
||||
.bold(),
|
||||
)
|
||||
.push(text(format!("(Recovery path #{})", i + 1))),
|
||||
.push(text(
|
||||
// If max timelock and all keys are from provider, then it's a safety net path.
|
||||
if *sequence == u16::MAX
|
||||
&& recovery_keys
|
||||
.iter()
|
||||
.all(|fg| keys.get(fg).is_some_and(|k| k.provider_key.is_some()))
|
||||
{
|
||||
"(Safety Net path)".to_string()
|
||||
} else {
|
||||
format!("(Recovery path #{})", i + 1)
|
||||
},
|
||||
)),
|
||||
);
|
||||
}
|
||||
Column::new()
|
||||
@ -1488,15 +1501,38 @@ pub fn defined_threshold<'a>(
|
||||
}
|
||||
|
||||
pub fn defined_sequence<'a>(
|
||||
sequence: u16,
|
||||
duplicate_sequence: bool,
|
||||
sequence: PathSequence,
|
||||
warning: Option<PathWarning>,
|
||||
) -> Element<'a, message::DefinePath> {
|
||||
let (n_years, n_months, n_days, n_hours, n_minutes) = duration_from_sequence(sequence);
|
||||
let (n_years, n_months, n_days, n_hours, n_minutes) = duration_from_sequence(sequence.as_u16());
|
||||
let duration_row = Row::new()
|
||||
.padding(5)
|
||||
.spacing(5)
|
||||
.align_y(Alignment::Center)
|
||||
.push(text(
|
||||
[
|
||||
(n_years, "y"),
|
||||
(n_months, "m"),
|
||||
(n_days, "d"),
|
||||
(n_hours, "h"),
|
||||
(n_minutes, "mn"),
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|(n, unit)| {
|
||||
if *n > 0 {
|
||||
Some(format!("{}{}", n, unit))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
));
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(if sequence != 0 {
|
||||
Row::new().align_y(Alignment::Center).push(
|
||||
.push(match sequence {
|
||||
PathSequence::Recovery(_) => Row::new().align_y(Alignment::Center).push(
|
||||
Container::new(
|
||||
Row::new()
|
||||
.align_y(Alignment::Center)
|
||||
@ -1506,57 +1542,38 @@ pub fn defined_sequence<'a>(
|
||||
.style(theme::text::secondary),
|
||||
)
|
||||
.push(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.padding(5)
|
||||
.spacing(5)
|
||||
.align_y(Alignment::Center)
|
||||
.push(text(
|
||||
[
|
||||
(n_years, "y"),
|
||||
(n_months, "m"),
|
||||
(n_days, "d"),
|
||||
(n_hours, "h"),
|
||||
(n_minutes, "mn"),
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|(n, unit)| {
|
||||
if *n > 0 {
|
||||
Some(format!("{}{}", n, unit))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
))
|
||||
.push(icon::pencil_icon()),
|
||||
)
|
||||
.style(theme::button::secondary)
|
||||
.on_press(message::DefinePath::EditSequence),
|
||||
Button::new(duration_row.push(icon::pencil_icon()))
|
||||
.style(theme::button::secondary)
|
||||
.on_press(message::DefinePath::EditSequence),
|
||||
),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(5)
|
||||
.align_y(alignment::Vertical::Center),
|
||||
)
|
||||
} else {
|
||||
Row::new()
|
||||
),
|
||||
PathSequence::Primary => Row::new()
|
||||
.push(
|
||||
p1_regular("Able to move the funds at any time.")
|
||||
.style(theme::text::secondary),
|
||||
)
|
||||
.padding(5),
|
||||
PathSequence::SafetyNet => Row::new().align_y(Alignment::Center).push(
|
||||
Container::new(
|
||||
Row::new()
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(5)
|
||||
.push(
|
||||
text::p1_regular("Available after inactivity of ~")
|
||||
.style(theme::text::secondary),
|
||||
)
|
||||
.push(duration_row),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(5)
|
||||
.align_y(alignment::Vertical::Center),
|
||||
),
|
||||
})
|
||||
.push_maybe(if duplicate_sequence {
|
||||
Some(
|
||||
text("No two recovery options may become available at the very same date.")
|
||||
.small()
|
||||
.style(theme::text::error),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push_maybe(warning.map(|w| text(w.message()).small().style(theme::text::error)))
|
||||
.spacing(15),
|
||||
)
|
||||
.padding(5)
|
||||
@ -1685,6 +1702,33 @@ pub fn key_list_view<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn provider_key_list_view(i: Option<usize>, key: &Key, chosen: bool) -> Element<'_, Message> {
|
||||
// If `i.is_some()`, it means this key is in our list of (saved) keys and can be selected.
|
||||
let key_kind = key
|
||||
.source
|
||||
.provider_key_kind()
|
||||
.expect("has kind")
|
||||
.to_string();
|
||||
let token = key.source.token().expect("has token");
|
||||
Button::new(if i.is_some() {
|
||||
if chosen {
|
||||
hw::selected_provider_key(key.fingerprint, key.name.clone(), key_kind, token)
|
||||
} else {
|
||||
hw::unselected_provider_key(key.fingerprint, key.name.clone(), key_kind, token)
|
||||
}
|
||||
} else {
|
||||
hw::unsaved_provider_key(key.fingerprint, key_kind, token)
|
||||
})
|
||||
.style(theme::button::secondary)
|
||||
.width(Length::Fill)
|
||||
.on_press_maybe(i.map(|i| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::SelectKey(i),
|
||||
))
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn backup_mnemonic<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
|
||||
@ -120,10 +120,26 @@ pub struct ListWallets {
|
||||
pub wallets: Vec<Wallet>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Provider {
|
||||
pub uuid: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ProviderKey {
|
||||
#[serde(deserialize_with = "deser_fromstr")]
|
||||
pub fingerprint: bip32::Fingerprint,
|
||||
pub uuid: String,
|
||||
pub token: String,
|
||||
pub provider: Provider,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct WalletMetadata {
|
||||
pub ledger_hmacs: Vec<LedgerHmac>,
|
||||
pub fingerprint_aliases: Vec<FingerprintAlias>,
|
||||
pub provider_keys: Vec<ProviderKey>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@ -335,11 +351,26 @@ pub mod payload {
|
||||
s.serialize_str(&field.to_string())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Provider {
|
||||
pub uuid: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProviderKey {
|
||||
pub fingerprint: String,
|
||||
pub uuid: String,
|
||||
pub token: String,
|
||||
pub provider: Provider,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CreateWallet<'a> {
|
||||
pub name: &'a str,
|
||||
#[serde(serialize_with = "ser_to_string")]
|
||||
pub descriptor: &'a LianaDescriptor,
|
||||
pub provider_keys: &'a Vec<ProviderKey>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@ -148,11 +148,16 @@ impl BackendClient {
|
||||
&self,
|
||||
name: &str,
|
||||
descriptor: &LianaDescriptor,
|
||||
provider_keys: &Vec<api::payload::ProviderKey>,
|
||||
) -> Result<api::Wallet, DaemonError> {
|
||||
let response = self
|
||||
.request(Method::POST, &format!("{}/v1/wallets", self.url))
|
||||
.await
|
||||
.json(&api::payload::CreateWallet { name, descriptor })
|
||||
.json(&api::payload::CreateWallet {
|
||||
name,
|
||||
descriptor,
|
||||
provider_keys,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
|
||||
@ -10,6 +10,7 @@ pub mod lianalite;
|
||||
pub mod loader;
|
||||
pub mod logger;
|
||||
pub mod node;
|
||||
pub mod services;
|
||||
pub mod signer;
|
||||
pub mod utils;
|
||||
|
||||
|
||||
@ -297,7 +297,7 @@ impl GUI {
|
||||
_ => l.update(*msg).map(|msg| Message::Login(Box::new(msg))),
|
||||
},
|
||||
(State::Installer(i), Message::Install(msg)) => {
|
||||
if let installer::Message::Exit(path, internal_bitcoind) = *msg {
|
||||
if let installer::Message::Exit(path, internal_bitcoind, remove_log) = *msg {
|
||||
let settings = app::settings::Settings::from_file(i.datadir.clone(), i.network)
|
||||
.expect("A settings file was created");
|
||||
if settings
|
||||
@ -326,7 +326,9 @@ impl GUI {
|
||||
self.log_level
|
||||
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
|
||||
);
|
||||
self.logger.remove_install_log_file(datadir_path.clone());
|
||||
if remove_log {
|
||||
self.logger.remove_install_log_file(datadir_path.clone());
|
||||
}
|
||||
let (loader, command) = Loader::new(
|
||||
datadir_path,
|
||||
cfg,
|
||||
@ -443,6 +445,12 @@ pub fn create_app_with_remote_backend(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let provider_keys: HashMap<_, _> = wallet
|
||||
.metadata
|
||||
.provider_keys
|
||||
.into_iter()
|
||||
.map(|pk| (pk.fingerprint, pk.into()))
|
||||
.collect();
|
||||
App::new(
|
||||
Cache {
|
||||
network,
|
||||
@ -459,6 +467,7 @@ pub fn create_app_with_remote_backend(
|
||||
Wallet::new(wallet.descriptor)
|
||||
.with_name(wallet.name)
|
||||
.with_key_aliases(aliases)
|
||||
.with_provider_keys(provider_keys)
|
||||
.with_hardware_wallets(hws)
|
||||
.load_hotsigners(&datadir, network)
|
||||
.expect("Datadir should be conform"),
|
||||
|
||||
81
liana-gui/src/services/api.rs
Normal file
81
liana-gui/src/services/api.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use serde::{de, Deserialize};
|
||||
|
||||
use liana::miniscript::descriptor::DescriptorPublicKey;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
|
||||
pub enum KeyKind {
|
||||
SafetyNet,
|
||||
Cosigner,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeyKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KeyKind::SafetyNet => write!(f, "Safety Net"),
|
||||
KeyKind::Cosigner => write!(f, "Cosigner"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyKind {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"safetynet" => Ok(KeyKind::SafetyNet),
|
||||
"cosigner" => Ok(KeyKind::Cosigner),
|
||||
s => Err(de::Error::custom(format!(
|
||||
"invalid value for KeyKind: '{}'",
|
||||
s
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum KeyStatus {
|
||||
NotFetched,
|
||||
Fetched,
|
||||
Redeemed,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyStatus {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"not-fetched" => Ok(KeyStatus::NotFetched),
|
||||
"fetched" => Ok(KeyStatus::Fetched),
|
||||
"redeemed" => Ok(KeyStatus::Redeemed),
|
||||
s => Err(de::Error::custom(format!(
|
||||
"invalid value for KeyStatus: '{}'",
|
||||
s
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Provider {
|
||||
pub uuid: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Key {
|
||||
pub provider: Provider,
|
||||
pub uuid: String,
|
||||
pub kind: KeyKind,
|
||||
pub status: KeyStatus,
|
||||
pub xpub: DescriptorPublicKey,
|
||||
}
|
||||
|
||||
impl Key {
|
||||
pub fn is_redeemed(&self) -> bool {
|
||||
matches!(self.status, KeyStatus::Redeemed)
|
||||
}
|
||||
}
|
||||
97
liana-gui/src/services/mod.rs
Normal file
97
liana-gui/src/services/mod.rs
Normal file
@ -0,0 +1,97 @@
|
||||
pub mod api;
|
||||
|
||||
use reqwest::{self, IntoUrl, Method, RequestBuilder};
|
||||
use serde_json::json;
|
||||
|
||||
const KEYS_API_URL: &str = "https://keys.wizardsardine.com";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
Http(Option<u16>, String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Http(kind, e) => write!(f, "Http error: [{:?}] {}", kind, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(error: reqwest::Error) -> Self {
|
||||
Self::Http(None, error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_response_status(response: reqwest::Response) -> Result<reqwest::Response, Error> {
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn request<U: reqwest::IntoUrl>(
|
||||
http: &reqwest::Client,
|
||||
method: reqwest::Method,
|
||||
url: U,
|
||||
) -> reqwest::RequestBuilder {
|
||||
let req = http
|
||||
.request(method, url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("API-Version", "0.1");
|
||||
tracing::debug!("Sending http request: {:?}", req);
|
||||
req
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client(reqwest::Client);
|
||||
|
||||
impl Default for Client {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
let http = reqwest::Client::new();
|
||||
Client(http)
|
||||
}
|
||||
|
||||
async fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
|
||||
request(&self.0, method, url)
|
||||
}
|
||||
|
||||
pub async fn get_key_by_token(&self, token: String) -> Result<api::Key, Error> {
|
||||
let response = self
|
||||
.request(Method::GET, &format!("{}/v1/keys", KEYS_API_URL))
|
||||
.await
|
||||
.query(&[("token", token)])
|
||||
.send()
|
||||
.await?;
|
||||
let response = check_response_status(response).await?;
|
||||
let key = response.json().await?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub async fn redeem_key(&self, uuid: String, token: String) -> Result<api::Key, Error> {
|
||||
let response = self
|
||||
.request(
|
||||
Method::POST,
|
||||
&format!("{}/v1/keys/{}/redeem", KEYS_API_URL, uuid),
|
||||
)
|
||||
.await
|
||||
.json(&json!({
|
||||
"token": token,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
let response = check_response_status(response).await?;
|
||||
let key = response.json().await?;
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
@ -93,6 +93,12 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Form`] with a warning message
|
||||
pub fn maybe_warning(mut self, warning: Option<&'a str>) -> Self {
|
||||
self.warning = warning;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the padding of the [`Form`].
|
||||
pub fn padding(mut self, units: u16) -> Self {
|
||||
self.input = self.input.padding(units);
|
||||
|
||||
@ -496,3 +496,82 @@ pub fn hot_signer<'a, T: 'a, F: Display>(
|
||||
)
|
||||
.padding(10)
|
||||
}
|
||||
|
||||
pub fn selected_provider_key<'a, T: 'a, F: Display>(
|
||||
fingerprint: F,
|
||||
alias: impl Into<Cow<'a, str>> + Display,
|
||||
key_kind: impl Into<Cow<'a, str>> + Display,
|
||||
token: impl Into<Cow<'a, str>> + Display,
|
||||
) -> Container<'a, T> {
|
||||
container(
|
||||
row(vec![
|
||||
column(vec![
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text::p1_bold(alias))
|
||||
.push(text::p1_regular(format!("#{}", fingerprint)))
|
||||
.into(),
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text::caption(format!("{key_kind} ({token})")))
|
||||
.into(),
|
||||
])
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
image::success_mark_icon().width(Length::Fixed(50.0)).into(),
|
||||
])
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.padding(10)
|
||||
}
|
||||
|
||||
pub fn unselected_provider_key<'a, T: 'a, F: Display>(
|
||||
fingerprint: F,
|
||||
alias: impl Into<Cow<'a, str>> + Display,
|
||||
key_kind: impl Into<Cow<'a, str>> + Display,
|
||||
token: impl Into<Cow<'a, str>> + Display,
|
||||
) -> Container<'a, T> {
|
||||
container(
|
||||
row(vec![column(vec![
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text::p1_bold(alias))
|
||||
.push(text::p1_regular(format!("#{}", fingerprint)))
|
||||
.into(),
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text::caption(format!("{key_kind} ({token})")))
|
||||
.into(),
|
||||
])
|
||||
.width(Length::Fill)
|
||||
.into()])
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.padding(10)
|
||||
}
|
||||
|
||||
pub fn unsaved_provider_key<'a, T: 'a, F: Display>(
|
||||
fingerprint: F,
|
||||
key_kind: impl Into<Cow<'a, str>> + Display,
|
||||
token: impl Into<Cow<'a, str>> + Display,
|
||||
) -> Container<'a, T> {
|
||||
container(
|
||||
row(vec![
|
||||
column(vec![
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text::p1_regular(format!("#{}", fingerprint)))
|
||||
.into(),
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text::caption(format!("{key_kind} ({token})")))
|
||||
.into(),
|
||||
])
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
image::success_mark_icon().width(Length::Fixed(50.0)).into(), // it must be selected if unsaved
|
||||
])
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.padding(10)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user