Add invitation process to add wallet flow
This commit is contained in:
parent
f7dde3d6ec
commit
5e4c04fa80
@ -119,6 +119,9 @@ pub trait Daemon: Debug {
|
||||
&self,
|
||||
labels: &HashMap<LabelItem, Option<String>>,
|
||||
) -> Result<(), DaemonError>;
|
||||
async fn send_wallet_invitation(&self, _email: &str) -> Result<(), DaemonError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// List spend transactions, optionally filtered to the specified `txids`.
|
||||
// Set `txids` to `None` for no filter (passing an empty slice returns no transactions).
|
||||
|
||||
@ -30,6 +30,7 @@ pub enum Message {
|
||||
Installed(Result<PathBuf, Error>),
|
||||
CreateTaprootDescriptor(bool),
|
||||
SelectBackend(SelectBackend),
|
||||
ImportRemoteWallet(ImportRemoteWallet),
|
||||
SelectBitcoindType(SelectBitcoindTypeMsg),
|
||||
InternalBitcoind(InternalBitcoindMsg),
|
||||
DefineBitcoind(DefineBitcoind),
|
||||
@ -53,7 +54,19 @@ pub enum SelectBackend {
|
||||
// Commands messages
|
||||
OTPRequested(Result<(AuthClient, String), Error>),
|
||||
OTPResent(Result<(), Error>),
|
||||
Connected(Result<(context::RemoteBackend, Option<api::Wallet>), Error>),
|
||||
Connected(Result<context::RemoteBackend, Error>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ImportRemoteWallet {
|
||||
RemoteWallets(Result<Vec<api::Wallet>, Error>),
|
||||
ImportDescriptor(String),
|
||||
ConfirmDescriptor,
|
||||
ImportInvitationToken(String),
|
||||
FetchInvitation,
|
||||
InvitationFetched(Result<api::WalletInvitation, Error>),
|
||||
AcceptInvitation,
|
||||
InvitationAccepted(Result<api::Wallet, Error>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@ -40,8 +40,8 @@ use crate::{
|
||||
pub use message::Message;
|
||||
use step::{
|
||||
BackupDescriptor, BackupMnemonic, ChooseBackend, DefineBitcoind, DefineDescriptor, Final,
|
||||
ImportDescriptor, InternalBitcoindStep, RecoverMnemonic, RegisterDescriptor,
|
||||
SelectBitcoindTypeStep, ShareXpubs, Step, Welcome,
|
||||
ImportDescriptor, ImportRemoteWallet, InternalBitcoindStep, RecoverMnemonic,
|
||||
RegisterDescriptor, SelectBitcoindTypeStep, ShareXpubs, Step, Welcome,
|
||||
};
|
||||
|
||||
pub struct Installer {
|
||||
@ -188,6 +188,7 @@ impl Installer {
|
||||
self.steps = vec![
|
||||
Welcome::default().into(),
|
||||
ChooseBackend::new(self.network).into(),
|
||||
ImportRemoteWallet::new(self.network).into(),
|
||||
ImportDescriptor::new(self.network).into(),
|
||||
RecoverMnemonic::default().into(),
|
||||
RegisterDescriptor::new_import_wallet().into(),
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use iced::Command;
|
||||
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use liana::{descriptors::LianaDescriptor, miniscript::bitcoin::Network};
|
||||
use liana_ui::{component::form, widget::Element};
|
||||
|
||||
use crate::{
|
||||
daemon::DaemonError,
|
||||
hw::HardwareWallets,
|
||||
installer::{
|
||||
context::{self, Context, RemoteBackend},
|
||||
@ -31,7 +34,6 @@ pub enum ConnectionStep {
|
||||
Connected {
|
||||
email: String,
|
||||
remote_backend: context::RemoteBackend,
|
||||
wallet: Option<api::Wallet>,
|
||||
remote_backend_is_selected: bool,
|
||||
},
|
||||
}
|
||||
@ -65,6 +67,9 @@ impl From<ChooseBackend> for Box<dyn Step> {
|
||||
}
|
||||
|
||||
impl Step for ChooseBackend {
|
||||
fn skip(&self, _ctx: &Context) -> bool {
|
||||
self.network != Network::Bitcoin && self.network != Network::Signet
|
||||
}
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
if matches!(
|
||||
message,
|
||||
@ -196,18 +201,17 @@ impl Step for ChooseBackend {
|
||||
Message::SelectBackend(message::SelectBackend::Connected(res)) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok((remote_backend, wallet)) => {
|
||||
Ok(remote_backend) => {
|
||||
self.step = ConnectionStep::Connected {
|
||||
email: email.clone(),
|
||||
remote_backend,
|
||||
wallet,
|
||||
remote_backend_is_selected: false,
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
if let Error::Auth(AuthError { http_status, .. }) = e {
|
||||
if http_status == Some(403) {
|
||||
self.auth_error = Some("Token is expired or is invalid")
|
||||
self.auth_error = Some("Token has expired or is invalid")
|
||||
} else {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
@ -278,10 +282,9 @@ impl Step for ChooseBackend {
|
||||
self.connection_error.as_ref(),
|
||||
self.auth_error,
|
||||
),
|
||||
ConnectionStep::Connected { email, wallet, .. } => view::connection_step_connected(
|
||||
ConnectionStep::Connected { email, .. } => view::connection_step_connected(
|
||||
email,
|
||||
self.processing,
|
||||
wallet.as_ref().map(|w| w.name.as_str()),
|
||||
self.connection_error.as_ref(),
|
||||
self.auth_error,
|
||||
),
|
||||
@ -294,14 +297,250 @@ pub async fn connect(
|
||||
auth: AuthClient,
|
||||
token: String,
|
||||
backend_api_url: String,
|
||||
) -> Result<(context::RemoteBackend, Option<api::Wallet>), Error> {
|
||||
) -> Result<context::RemoteBackend, Error> {
|
||||
let access = auth.verify_otp(token.trim_end()).await?;
|
||||
let client = BackendClient::connect(auth, backend_api_url, access.clone()).await?;
|
||||
Ok(RemoteBackend::WithoutWallet(client))
|
||||
}
|
||||
|
||||
if !client.list_wallets().await?.is_empty() {
|
||||
let (wallet_client, wallet) = client.connect_first().await?;
|
||||
Ok((RemoteBackend::WithWallet(wallet_client), Some(wallet)))
|
||||
} else {
|
||||
Ok((RemoteBackend::WithoutWallet(client), None))
|
||||
pub struct ImportRemoteWallet {
|
||||
network: Network,
|
||||
invitation_token: form::Value<String>,
|
||||
invitation: Option<api::WalletInvitation>,
|
||||
imported_descriptor: form::Value<String>,
|
||||
descriptor: Option<LianaDescriptor>,
|
||||
error: Option<String>,
|
||||
backend: Option<context::RemoteBackend>,
|
||||
wallets: Vec<api::Wallet>,
|
||||
}
|
||||
|
||||
impl ImportRemoteWallet {
|
||||
pub fn new(network: Network) -> Self {
|
||||
Self {
|
||||
network,
|
||||
invitation_token: form::Value::default(),
|
||||
invitation: None,
|
||||
imported_descriptor: form::Value::default(),
|
||||
descriptor: None,
|
||||
error: None,
|
||||
backend: None,
|
||||
wallets: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for ImportRemoteWallet {
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
ctx.remote_backend.is_none()
|
||||
}
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
self.backend.clone_from(&ctx.remote_backend);
|
||||
}
|
||||
fn load(&self) -> Command<Message> {
|
||||
let backend = self
|
||||
.backend
|
||||
.clone()
|
||||
.expect("Must be one otherwise the step is skipped");
|
||||
Command::perform(
|
||||
async move {
|
||||
let wallets = match backend {
|
||||
context::RemoteBackend::WithoutWallet(backend) => {
|
||||
backend.list_wallets().await?
|
||||
}
|
||||
context::RemoteBackend::WithWallet(backend) => {
|
||||
backend.inner_client().list_wallets().await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(wallets)
|
||||
},
|
||||
|res| Message::ImportRemoteWallet(message::ImportRemoteWallet::RemoteWallets(res)),
|
||||
)
|
||||
}
|
||||
// form value is set as valid each time it is edited.
|
||||
// Verification of the values is happening when the user click on Next button.
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::ImportDescriptor(desc)) => {
|
||||
self.imported_descriptor.value = desc;
|
||||
if !self.imported_descriptor.value.is_empty() {
|
||||
if let Ok(desc) = LianaDescriptor::from_str(&self.imported_descriptor.value) {
|
||||
if self.network == Network::Bitcoin {
|
||||
self.imported_descriptor.valid = desc.all_xpubs_net_is(self.network);
|
||||
} else {
|
||||
self.imported_descriptor.valid =
|
||||
desc.all_xpubs_net_is(Network::Testnet);
|
||||
}
|
||||
} else {
|
||||
self.imported_descriptor.valid = false;
|
||||
}
|
||||
} else {
|
||||
self.imported_descriptor.valid = false;
|
||||
}
|
||||
}
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::ConfirmDescriptor) => {
|
||||
if let Ok(desc) = LianaDescriptor::from_str(&self.imported_descriptor.value) {
|
||||
if self.network == Network::Bitcoin {
|
||||
self.imported_descriptor.valid = desc.all_xpubs_net_is(self.network);
|
||||
} else {
|
||||
self.imported_descriptor.valid = desc.all_xpubs_net_is(Network::Testnet);
|
||||
}
|
||||
if self.imported_descriptor.valid {
|
||||
let backend = self.backend.take();
|
||||
if let Some(context::RemoteBackend::WithWallet(backend)) = backend {
|
||||
self.backend =
|
||||
Some(context::RemoteBackend::WithoutWallet(backend.into_inner()));
|
||||
} else {
|
||||
self.backend = backend;
|
||||
}
|
||||
self.descriptor = Some(desc);
|
||||
return Command::perform(async {}, |_| Message::Next);
|
||||
}
|
||||
} else {
|
||||
self.imported_descriptor.valid = false;
|
||||
}
|
||||
}
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::RemoteWallets(res)) => {
|
||||
match res {
|
||||
Ok(wallets) => self.wallets = wallets,
|
||||
Err(e) => self.error = Some(e.to_string()),
|
||||
}
|
||||
}
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::ImportInvitationToken(
|
||||
token,
|
||||
)) => {
|
||||
self.invitation_token.value = token;
|
||||
}
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::FetchInvitation) => {
|
||||
let backend = self
|
||||
.backend
|
||||
.clone()
|
||||
.map(|b| match b {
|
||||
context::RemoteBackend::WithoutWallet(b) => b,
|
||||
context::RemoteBackend::WithWallet(b) => b.into_inner(),
|
||||
})
|
||||
.expect("Must be a remote backend at this point");
|
||||
let token = self.invitation_token.value.clone();
|
||||
self.error = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
let invitation = backend.get_wallet_invitation(&token).await?;
|
||||
Ok(invitation)
|
||||
},
|
||||
|res| {
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::InvitationFetched(
|
||||
res,
|
||||
))
|
||||
},
|
||||
);
|
||||
}
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::InvitationFetched(res)) => {
|
||||
match res {
|
||||
Err(_) => self.invitation_token.valid = false,
|
||||
Ok(invitation) => self.invitation = Some(invitation),
|
||||
}
|
||||
}
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::AcceptInvitation) => {
|
||||
let backend = self
|
||||
.backend
|
||||
.clone()
|
||||
.map(|b| match b {
|
||||
context::RemoteBackend::WithoutWallet(b) => b,
|
||||
context::RemoteBackend::WithWallet(b) => b.into_inner(),
|
||||
})
|
||||
.expect("Must be a remote backend at this point");
|
||||
let invitation = self.invitation.clone().expect("Invitation was fetched");
|
||||
self.error = None;
|
||||
return Command::perform(
|
||||
async move {
|
||||
backend.accept_wallet_invitation(&invitation.id).await?;
|
||||
let wallets = backend.list_wallets().await?;
|
||||
wallets
|
||||
.into_iter()
|
||||
.find(|w| w.id == invitation.wallet_id)
|
||||
.ok_or(
|
||||
DaemonError::Unexpected(
|
||||
"Wallet of accepted invitation not found".to_string(),
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
},
|
||||
|res| {
|
||||
Message::ImportRemoteWallet(
|
||||
message::ImportRemoteWallet::InvitationAccepted(res),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
Message::ImportRemoteWallet(message::ImportRemoteWallet::InvitationAccepted(res)) => {
|
||||
match res {
|
||||
Err(e) => self.error = Some(e.to_string()),
|
||||
Ok(wallet) => {
|
||||
self.invitation = None;
|
||||
self.invitation_token = form::Value::default();
|
||||
self.wallets.push(wallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Select(i) => {
|
||||
if let Some(wallet) = self.wallets.get(i).cloned() {
|
||||
if let Some(backend) = self.backend.take() {
|
||||
self.backend = Some(match backend {
|
||||
context::RemoteBackend::WithoutWallet(backend) => {
|
||||
context::RemoteBackend::WithWallet(
|
||||
backend.connect_wallet(wallet.clone()).0,
|
||||
)
|
||||
}
|
||||
context::RemoteBackend::WithWallet(backend) => {
|
||||
context::RemoteBackend::WithWallet(
|
||||
backend.into_inner().connect_wallet(wallet.clone()).0,
|
||||
)
|
||||
}
|
||||
});
|
||||
// ensure that no descriptor is imported.
|
||||
self.imported_descriptor = form::Value::default();
|
||||
self.descriptor = Some(wallet.descriptor);
|
||||
return Command::perform(async {}, |_| Message::Next);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
// Set to true in order to force the registration process to be shown to user.
|
||||
ctx.hw_is_used = true;
|
||||
ctx.descriptor.clone_from(&self.descriptor);
|
||||
ctx.remote_backend.clone_from(&self.backend);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::import_wallet_or_descriptor(
|
||||
progress,
|
||||
email,
|
||||
&self.invitation_token,
|
||||
self.invitation
|
||||
.as_ref()
|
||||
.map(|invit| invit.wallet_name.as_str()),
|
||||
&self.imported_descriptor,
|
||||
self.error.as_ref(),
|
||||
self.wallets.iter().map(|w| &w.name).collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ImportRemoteWallet> for Box<dyn Step> {
|
||||
fn from(s: ImportRemoteWallet) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1143,6 +1143,10 @@ impl ImportDescriptor {
|
||||
}
|
||||
|
||||
impl Step for ImportDescriptor {
|
||||
// ImportRemoteWallet is used instead
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
ctx.remote_backend.is_some()
|
||||
}
|
||||
// form value is set as valid each time it is edited.
|
||||
// Verification of the values is happening when the user click on Next button.
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
|
||||
@ -10,7 +10,7 @@ pub use bitcoind::{
|
||||
|
||||
pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor};
|
||||
|
||||
pub use backend::ChooseBackend;
|
||||
pub use backend::{ChooseBackend, ImportRemoteWallet};
|
||||
pub use mnemonic::{BackupMnemonic, RecoverMnemonic};
|
||||
pub use share_xpubs::ShareXpubs;
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
button, card, collapse, form, hw, separation,
|
||||
text::{h3, p1_regular, text, Text},
|
||||
text::{h3, h4_bold, h5_regular, p1_regular, text, Text},
|
||||
tooltip,
|
||||
},
|
||||
icon, image, theme,
|
||||
@ -376,6 +376,220 @@ pub fn recovery_path_view(
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn import_wallet_or_descriptor<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
invitation: &'a form::Value<String>,
|
||||
invitation_wallet: Option<&'a str>,
|
||||
imported_descriptor: &'a form::Value<String>,
|
||||
error: Option<&'a String>,
|
||||
wallets: Vec<&'a String>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut col_wallets = Column::new()
|
||||
.spacing(20)
|
||||
.push(h4_bold("Choose the wallet to import"));
|
||||
let no_wallets = wallets.is_empty();
|
||||
for (i, wallet) in wallets.into_iter().enumerate() {
|
||||
col_wallets = col_wallets.push(
|
||||
Button::new(h5_regular(wallet).width(Length::Fill))
|
||||
.padding(10)
|
||||
.on_press(Message::Select(i)),
|
||||
);
|
||||
}
|
||||
let card_wallets: Element<'a, Message> = if no_wallets {
|
||||
h4_bold("You have no current wallets").into()
|
||||
} else {
|
||||
card::simple(col_wallets).into()
|
||||
};
|
||||
|
||||
let col_invitation_token = collapse::Collapse::new(
|
||||
|| {
|
||||
Button::new(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(h4_bold("Join a shared wallet").style(color::WHITE))
|
||||
.push(
|
||||
text("If you received an invitation to join a shared wallet")
|
||||
.style(color::GREY_3),
|
||||
),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
|| {
|
||||
Button::new(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(h4_bold("Join a shared wallet").style(color::WHITE))
|
||||
.push(
|
||||
text("If you received an invitation to join a shared wallet")
|
||||
.style(color::GREY_3),
|
||||
),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
if let Some(wallet) = invitation_wallet {
|
||||
Element::<'a, Message>::from(
|
||||
Column::new()
|
||||
.push(Space::with_height(0))
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("Accept invitation for wallet:"))
|
||||
.push(text(wallet).bold()),
|
||||
)
|
||||
.push(
|
||||
Row::new().push(Space::with_width(Length::Fill)).push(
|
||||
button::primary(None, "Accept")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(Message::ImportRemoteWallet(
|
||||
message::ImportRemoteWallet::AcceptInvitation,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.spacing(20),
|
||||
)
|
||||
} else {
|
||||
Element::<'a, Message>::from(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.push(Space::with_height(0))
|
||||
.push(
|
||||
Column::new()
|
||||
.push(text("Paste invitation:").bold())
|
||||
.push(
|
||||
form::Form::new_trimmed("Invitation", invitation, |msg| {
|
||||
Message::ImportRemoteWallet(
|
||||
message::ImportRemoteWallet::ImportInvitationToken(
|
||||
msg,
|
||||
),
|
||||
)
|
||||
})
|
||||
.warning("Invitation token is invalid or expired")
|
||||
.size(text::P1_SIZE)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10),
|
||||
)
|
||||
.push(
|
||||
Row::new().push(Space::with_width(Length::Fill)).push(
|
||||
button::primary(None, "Next")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press_maybe(if !invitation.value.is_empty() {
|
||||
Some(Message::ImportRemoteWallet(
|
||||
message::ImportRemoteWallet::FetchInvitation,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
),
|
||||
)
|
||||
.spacing(20),
|
||||
)
|
||||
.padding(15),
|
||||
)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let col_descriptor = collapse::Collapse::new(
|
||||
|| {
|
||||
Button::new(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(h4_bold("Import a wallet from descriptor").style(color::WHITE))
|
||||
.push(
|
||||
text("The remote backend will rescan the blockchain to find your coins")
|
||||
.style(color::GREY_3),
|
||||
),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
|| {
|
||||
Button::new(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(h4_bold("Import a wallet from descriptor").style(color::WHITE))
|
||||
.push(
|
||||
text("The remote backend will rescan the blockchain to find your coins")
|
||||
.style(color::GREY_3),
|
||||
),
|
||||
)
|
||||
.padding(15)
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::TransparentBorder)
|
||||
},
|
||||
move || {
|
||||
Element::<'a, Message>::from(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.push(Space::with_height(0))
|
||||
.push(
|
||||
Column::new()
|
||||
.push(text("Descriptor:").bold())
|
||||
.push(
|
||||
form::Form::new_trimmed(
|
||||
"Descriptor",
|
||||
imported_descriptor,
|
||||
|msg| {
|
||||
Message::ImportRemoteWallet(
|
||||
message::ImportRemoteWallet::ImportDescriptor(msg),
|
||||
)
|
||||
},
|
||||
)
|
||||
.warning(
|
||||
"Either descriptor is invalid or incompatible with network",
|
||||
)
|
||||
.size(text::P1_SIZE)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10),
|
||||
)
|
||||
.push(
|
||||
Row::new().push(Space::with_width(Length::Fill)).push(
|
||||
button::primary(None, "Next")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press_maybe(
|
||||
if imported_descriptor.value.is_empty()
|
||||
|| !imported_descriptor.valid
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(Message::ImportRemoteWallet(
|
||||
message::ImportRemoteWallet::ConfirmDescriptor,
|
||||
))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.spacing(20),
|
||||
)
|
||||
.padding(15),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
layout(
|
||||
progress,
|
||||
email,
|
||||
"Add wallet",
|
||||
Column::new()
|
||||
.spacing(50)
|
||||
.push_maybe(error.map(|e| card::error("Something wrong happened", e.to_string())))
|
||||
.push(card_wallets)
|
||||
.push(card::simple(col_invitation_token).padding(0))
|
||||
.push(card::simple(col_descriptor).padding(0)),
|
||||
true,
|
||||
Some(Message::Previous),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn import_descriptor<'a>(
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
@ -2052,7 +2266,7 @@ pub fn connection_step_enter_otp<'a>(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(text(email).style(color::GREEN))
|
||||
.push(text("An authentication was send to you mail"))
|
||||
.push(text("An authentication token has been emailed to you"))
|
||||
.push_maybe(connection_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push_maybe(auth_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push(
|
||||
@ -2084,7 +2298,6 @@ pub fn connection_step_enter_otp<'a>(
|
||||
pub fn connection_step_connected<'a>(
|
||||
email: &'a str,
|
||||
processing: bool,
|
||||
wallet_name: Option<&str>,
|
||||
connection_error: Option<&Error>,
|
||||
auth_error: Option<&'static str>,
|
||||
) -> Element<'a, Message> {
|
||||
@ -2093,51 +2306,23 @@ pub fn connection_step_connected<'a>(
|
||||
.push(text(email).style(color::GREEN))
|
||||
.push_maybe(connection_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push_maybe(auth_error.map(|e| text(e.to_string()).style(color::ORANGE)))
|
||||
.push(if let Some(name) = wallet_name {
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(text(format!("Wallet {} already exists", name)))
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::primary(Some(icon::previous_icon()), "Change Email")
|
||||
.on_press(Message::SelectBackend(
|
||||
message::SelectBackend::EditEmail,
|
||||
)),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Continue with existing wallet")
|
||||
.on_press_maybe(if processing {
|
||||
None
|
||||
} else {
|
||||
Some(Message::SelectBackend(
|
||||
message::SelectBackend::ContinueWithRemoteBackend,
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::primary(Some(icon::previous_icon()), "Change Email")
|
||||
.on_press(Message::SelectBackend(message::SelectBackend::EditEmail)),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Continue").on_press_maybe(if processing {
|
||||
None
|
||||
} else {
|
||||
Some(Message::SelectBackend(
|
||||
message::SelectBackend::ContinueWithRemoteBackend,
|
||||
))
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.push(Container::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::primary(Some(icon::previous_icon()), "Change Email")
|
||||
.on_press(Message::SelectBackend(message::SelectBackend::EditEmail)),
|
||||
)
|
||||
.push(
|
||||
button::primary(None, "Continue").on_press_maybe(if processing {
|
||||
None
|
||||
} else {
|
||||
Some(Message::SelectBackend(
|
||||
message::SelectBackend::ContinueWithRemoteBackend,
|
||||
))
|
||||
}),
|
||||
),
|
||||
))
|
||||
.into()
|
||||
}
|
||||
|
||||
|
||||
@ -142,6 +142,21 @@ pub struct FingerprintAlias {
|
||||
pub alias: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum WalletInvitationStatus {
|
||||
Pending,
|
||||
Accepted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct WalletInvitation {
|
||||
pub id: String,
|
||||
pub wallet_name: String,
|
||||
pub wallet_id: String,
|
||||
pub status: WalletInvitationStatus,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct WalletLabels {
|
||||
pub labels: HashMap<String, String>,
|
||||
@ -320,6 +335,11 @@ pub mod payload {
|
||||
pub descriptor: &'a LianaDescriptor,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CreateWalletInvitation<'a> {
|
||||
pub email: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ImportPsbt {
|
||||
pub psbt: String,
|
||||
|
||||
@ -103,15 +103,19 @@ impl BackendClient {
|
||||
pub async fn connect_first(self) -> Result<(BackendWalletClient, api::Wallet), DaemonError> {
|
||||
let wallets = self.list_wallets().await?;
|
||||
let first = wallets.first().cloned().ok_or(DaemonError::NoAnswer)?;
|
||||
Ok((
|
||||
Ok(self.connect_wallet(first))
|
||||
}
|
||||
|
||||
pub fn connect_wallet(self, wallet: api::Wallet) -> (BackendWalletClient, api::Wallet) {
|
||||
(
|
||||
BackendWalletClient {
|
||||
inner: self,
|
||||
curve: secp256k1::Secp256k1::verification_only(),
|
||||
wallet_uuid: first.id.clone(),
|
||||
wallet_desc: first.descriptor.to_owned(),
|
||||
wallet_uuid: wallet.id.clone(),
|
||||
wallet_desc: wallet.descriptor.to_owned(),
|
||||
},
|
||||
first,
|
||||
))
|
||||
wallet,
|
||||
)
|
||||
}
|
||||
|
||||
async fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
|
||||
@ -170,7 +174,7 @@ impl BackendClient {
|
||||
.find(|w| w.id == wallet_uuid)
|
||||
.ok_or(DaemonError::Http(
|
||||
Some(404),
|
||||
"No wallet exists for this uui".to_string(),
|
||||
"No wallet exists for this uuid".to_string(),
|
||||
))?;
|
||||
let ledger_kinds = [
|
||||
async_hwi::DeviceKind::Ledger.to_string(),
|
||||
@ -248,6 +252,47 @@ impl BackendClient {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_wallet_invitation(
|
||||
&self,
|
||||
invitation_id: &str,
|
||||
) -> Result<api::WalletInvitation, DaemonError> {
|
||||
let response = self
|
||||
.request(
|
||||
Method::GET,
|
||||
&format!("{}/v1/invitations/{}", self.url, invitation_id),
|
||||
)
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(DaemonError::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
pub async fn accept_wallet_invitation(&self, invitation_id: &str) -> Result<(), DaemonError> {
|
||||
let response = self
|
||||
.request(
|
||||
Method::POST,
|
||||
&format!("{}/v1/invitations/{}/accept", self.url, invitation_id),
|
||||
)
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(DaemonError::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -263,6 +308,10 @@ impl BackendWalletClient {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> BackendClient {
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> &str {
|
||||
&self.inner.user_id
|
||||
}
|
||||
@ -1024,6 +1073,30 @@ impl Daemon for BackendWalletClient {
|
||||
.update_wallet_metadata(&self.wallet_uuid, fingerprint_aliases, hws)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_wallet_invitation(&self, email: &str) -> Result<(), DaemonError> {
|
||||
let response = self
|
||||
.inner
|
||||
.request(
|
||||
Method::POST,
|
||||
&format!(
|
||||
"{}/v1/wallets/{}/invitations",
|
||||
self.inner.url, self.wallet_uuid
|
||||
),
|
||||
)
|
||||
.await
|
||||
.json(&api::payload::CreateWalletInvitation { email })
|
||||
.send()
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(DaemonError::Http(
|
||||
Some(response.status().into()),
|
||||
response.text().await?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn history_tx_from_api(value: api::Transaction, network: Network) -> HistoryTransaction {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user