From db227753638132d480ee1637ee3ea711ea78ab94 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Fri, 17 May 2024 16:16:37 -0500 Subject: [PATCH] Welcome route --- src/bridge.rs | 20 ++++++++++ src/conf.rs | 18 ++++++++- src/core.rs | 89 ++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 65 ++++++++++++++++++++++++++++++- src/routes/mod.rs | 4 ++ src/routes/welcome.rs | 61 +++++++++++++++++++++++++++++ 6 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 src/routes/welcome.rs diff --git a/src/bridge.rs b/src/bridge.rs index febb155..5388468 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -25,6 +25,10 @@ pub enum UICoreMsg { GetFederationInfo(InviteCode), AddFederation(InviteCode), Unlock(String), + Init { + password: String, + seed: Option, + }, GetSeedWords, } @@ -63,6 +67,11 @@ pub enum CoreUIMsg { FederationInfo(ClientConfig), AddFederationSuccess, FederationListUpdated(Vec), + NeedsInit, + Initing, + InitSuccess, + InitFailed(String), + Locked, Unlocking, UnlockSuccess, UnlockFailed(String), @@ -128,6 +137,17 @@ impl UIHandle { .await; } + pub async fn init(&self, id: Uuid, password: String) { + self.msg_send(UICoreMsgPacket { + msg: UICoreMsg::Init { + password, + seed: None, + }, + id, + }) + .await; + } + pub async fn add_federation(&self, id: Uuid, invite: InviteCode) { self.msg_send(UICoreMsgPacket { msg: UICoreMsg::AddFederation(invite), diff --git a/src/conf.rs b/src/conf.rs index 0898158..4a32c5b 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -21,7 +21,6 @@ pub fn data_dir(network: Network) -> PathBuf { } } -// todo store in encrypted database pub fn get_mnemonic(db: Arc) -> anyhow::Result { match db.get_seed()? { Some(m) => { @@ -41,3 +40,20 @@ pub fn get_mnemonic(db: Arc) -> anyhow::Result, + words: Option, +) -> anyhow::Result { + let mnemonic_words = words.unwrap_or(Mnemonic::generate_in(Language::English, 12)?.to_string()); + + let new_profile = NewProfile { + id: uuid::Uuid::new_v4().to_string(), + seed_words: mnemonic_words, + }; + + let p = db.insert_new_profile(new_profile)?; + + info!("creating new seed"); + Ok(Mnemonic::from_str(&p.seed_words)?) +} diff --git a/src/core.rs b/src/core.rs index a8a301f..4246a4f 100644 --- a/src/core.rs +++ b/src/core.rs @@ -24,7 +24,6 @@ use tokio::sync::RwLock; use tokio::task::spawn_blocking; use uuid::Uuid; -use crate::components::FederationItem; use crate::db::check_password; use crate::fedimint_client::{ spawn_onchain_payment_subscription, spawn_onchain_receive_subscription, FederationInviteOrId, @@ -35,6 +34,7 @@ use crate::{ db::DBConnection, Message, }; +use crate::{components::FederationItem, conf::generate_mnemonic}; use crate::{ db::setup_db, fedimint_client::{ @@ -44,6 +44,7 @@ use crate::{ }; const PEG_IN_TIMEOUT_YEAR: Duration = Duration::from_secs(86400 * 365); +const HARBOR_FILE_NAME: &str = "harbor.sqlite"; #[derive(Clone)] struct HarborCore { @@ -377,11 +378,25 @@ pub fn run_core() -> Subscription { std::fs::create_dir_all(path.clone()).expect("Could not create datadir"); log::info!("Using datadir: {path:?}"); + tokio::time::sleep(Duration::from_secs(1)).await; + + // Check if the database file exists already, if so tell UI to unlock + if std::fs::metadata(path.join(HARBOR_FILE_NAME)).is_ok() { + tx.send(Message::core_msg(None, CoreUIMsg::Locked)) + .await + .expect("should send"); + } else { + tx.send(Message::core_msg(None, CoreUIMsg::NeedsInit)) + .await + .expect("should send"); + } + loop { let msg = core_handle.recv().await; let id = msg.as_ref().map(|m| m.id); + // Watch for either Unlock or Init, ignore everything else until started match msg.map(|m| m.msg) { Some(UICoreMsg::Unlock(password)) => { log::info!("Sending unlock message"); @@ -390,7 +405,7 @@ pub fn run_core() -> Subscription { .expect("should send"); // attempting to unlock - let db_path = path.join("harbor.sqlite"); + let db_path = path.join(HARBOR_FILE_NAME); let db_path = db_path.to_str().unwrap().to_string(); @@ -431,6 +446,7 @@ pub fn run_core() -> Subscription { } let db = db.expect("no error"); + // TODO do not automatically generate words anymore let mnemonic = get_mnemonic(db.clone()).expect("should get seed"); let stop = Arc::new(AtomicBool::new(false)); @@ -471,6 +487,72 @@ pub fn run_core() -> Subscription { process_core(&mut core_handle, &core).await; } + Some(UICoreMsg::Init { password, seed }) => { + // TODO refactor and make this about init + log::info!("Sending init message"); + tx.send(Message::core_msg(id, CoreUIMsg::Initing)) + .await + .expect("should send"); + + // attempting to unlock + let db_path = path.join(HARBOR_FILE_NAME); + let db = + spawn_blocking(move || setup_db(db_path.to_str().unwrap(), password)) + .await + .expect("Could not create join handle"); + + if let Err(e) = db { + // probably invalid password + error!("error setting password: {e}"); + + tx.send(Message::core_msg(id, CoreUIMsg::InitFailed(e.to_string()))) + .await + .expect("should send"); + continue; + } + let db = db.expect("no error"); + + let mnemonic = + generate_mnemonic(db.clone(), seed).expect("should generate words"); + let stop = Arc::new(AtomicBool::new(false)); + + // check db for fedimints + let mut clients = HashMap::new(); + let federation_ids = db + .list_federations() + .expect("should load initial fedimints"); + for f in federation_ids { + let client = FedimintClient::new( + db.clone(), + FederationInviteOrId::Id( + FederationId::from_str(&f).expect("should parse federation id"), + ), + &mnemonic, + network, + stop.clone(), + ) + .await + .expect("Could not create fedimint client"); + + clients.insert(client.fedimint_client.federation_id(), client); + } + + let core = HarborCore { + storage: db.clone(), + tx: tx.clone(), + mnemonic, + network, + clients: Arc::new(RwLock::new(clients)), + stop, + }; + + tx.send(Message::core_msg(id, CoreUIMsg::InitSuccess)) + .await + .expect("should send"); + + process_core(&mut core_handle, &core).await; + } + _ => { warn!("Ignoring unrelated message to locked core") } @@ -573,6 +655,9 @@ async fn process_core(core_handle: &mut bridge::CoreHandle, core: &HarborCore) { UICoreMsg::Unlock(_password) => { unreachable!("should already be unlocked") } + UICoreMsg::Init { .. } => { + unreachable!("should already be inited") + } UICoreMsg::GetSeedWords => { let seed_words = core.get_seed_words().await; core.msg(Some(msg.id), CoreUIMsg::SeedWords(seed_words)) diff --git a/src/main.rs b/src/main.rs index 42225f4..3c5b23e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,6 @@ pub mod routes; pub fn main() -> iced::Result { pretty_env_logger::init(); program("Harbor", HarborWallet::update, HarborWallet::view) - // .load(HarborWallet::load) .font(include_bytes!("../assets/fonts/Inter-Regular.ttf").as_slice()) .font(include_bytes!("../assets/fonts/Inter-Bold.ttf").as_slice()) .theme(HarborWallet::theme) @@ -62,6 +61,15 @@ enum ReceiveStatus { WaitingToReceive, } +#[derive(Default, Debug, Clone, PartialEq)] +enum WelcomeStatus { + #[default] + Loading, + NeedsInit, + Inited, + Initing, +} + #[derive(Default, Debug, Clone, PartialEq)] enum UnlockStatus { #[default] @@ -90,6 +98,7 @@ pub enum Message { SetIsMax(bool), SendStateReset, PasswordInputChanged(String), + SeedInputChanged(String), MintInviteCodeInputChanged(String), DonateAmountChanged(String), CopyToClipboard(String), @@ -103,6 +112,7 @@ pub enum Message { GenerateInvoice, GenerateAddress, Unlock(String), + Init(String), // TODO add seed option AddFederation(String), PeekFederation(String), Donate, @@ -127,6 +137,10 @@ pub struct HarborWallet { balance_sats: u64, transaction_history: Vec, federation_list: Vec, + // Welcome screen + init_status: WelcomeStatus, + seed_input_str: String, + init_failure_reason: Option, // Lock screen password_input_str: String, unlock_status: UnlockStatus, @@ -216,6 +230,14 @@ impl HarborWallet { } } + async fn async_init(ui_handle: Option>, id: Uuid, password: String) { + if let Some(ui_handle) = ui_handle { + ui_handle.clone().init(id, password).await; + } else { + panic!("UI handle is None"); + } + } + async fn async_add_federation( ui_handle: Option>, id: Uuid, @@ -255,6 +277,7 @@ impl HarborWallet { self.ui_handle = Some(ui_handle); println!("Core loaded"); + // TODO I don't think this is the best place for it focus_input_id("password_unlock_input") // Command::none() @@ -292,6 +315,10 @@ impl HarborWallet { self.password_input_str = input; Command::none() } + Message::SeedInputChanged(input) => { + self.seed_input_str = input; + Command::none() + } Message::MintInviteCodeInputChanged(input) => { self.mint_invite_code_str = input; Command::none() @@ -423,6 +450,17 @@ impl HarborWallet { ) } }, + Message::Init(password) => match self.unlock_status { + UnlockStatus::Unlocking => Command::none(), + _ => { + self.unlock_failure_reason = None; + let id = Uuid::new_v4(); // todo use this id somewhere + Command::perform( + Self::async_init(self.ui_handle.clone(), id, password), + |_| Message::Noop, + ) + } + }, Message::AddFederation(invite_code) => { let invite = InviteCode::from_str(&invite_code); if let Ok(invite) = invite { @@ -577,6 +615,30 @@ impl HarborWallet { self.receive_address = Some(address); Command::none() } + CoreUIMsg::Locked => { + info!("Got locked message"); + self.active_route = Route::Unlock; + Command::none() + } + CoreUIMsg::NeedsInit => { + info!("Got init message"); + self.init_status = WelcomeStatus::NeedsInit; + Command::none() + } + CoreUIMsg::Initing => { + self.init_status = WelcomeStatus::Initing; + Command::none() + } + CoreUIMsg::InitSuccess => { + self.init_status = WelcomeStatus::Inited; + self.active_route = Route::Home; + Command::none() + } + CoreUIMsg::InitFailed(reason) => { + self.init_status = WelcomeStatus::NeedsInit; + self.init_failure_reason = Some(reason); + Command::none() + } CoreUIMsg::Unlocking => { info!("Got unlocking message"); self.unlock_status = UnlockStatus::Unlocking; @@ -614,6 +676,7 @@ impl HarborWallet { Route::History => row![sidebar, crate::routes::history(self)].into(), Route::Transfer => row![sidebar, crate::routes::transfer(self)].into(), Route::Settings => row![sidebar, crate::routes::settings(self)].into(), + Route::Welcome => crate::routes::welcome(self), }; ToastManager::new(active_route, &self.toasts, Message::CloseToast).into() diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 54ccc31..6841639 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -25,9 +25,13 @@ pub use history::*; pub mod settings; pub use settings::*; +pub mod welcome; +pub use welcome::*; + #[derive(Default, PartialEq, Debug, Clone, Copy)] pub enum Route { #[default] + Welcome, Unlock, Home, Mints, diff --git a/src/routes/welcome.rs b/src/routes/welcome.rs new file mode 100644 index 0000000..0bbce52 --- /dev/null +++ b/src/routes/welcome.rs @@ -0,0 +1,61 @@ +use crate::{ + components::{h_button, h_input, harbor_logo, the_spinner, SvgIcon}, + UnlockStatus, WelcomeStatus, +}; +use iced::{ + widget::{center, column, container, text}, + Theme, +}; +use iced::{Alignment, Element, Length}; + +use crate::{HarborWallet, Message}; + +pub fn welcome(harbor: &HarborWallet) -> Element { + let column = match harbor.init_status { + WelcomeStatus::Loading | WelcomeStatus::Inited | WelcomeStatus::Initing => { + let welcome_message = text("Welcome, we're glad you are here.").size(24); + + let spinner: Element<'static, Message, Theme> = the_spinner(); + + column![harbor_logo(), welcome_message, spinner] + .spacing(32) + .align_items(Alignment::Center) + .width(Length::Fixed(350.)) + } + WelcomeStatus::NeedsInit => { + let action = if harbor.unlock_status == UnlockStatus::Unlocking { + None + } else { + Some(Message::Init(harbor.password_input_str.clone())) + }; + + let new_wallet = h_button( + "Create New Wallet", + SvgIcon::Plus, + harbor.unlock_status == UnlockStatus::Unlocking, + ) + .on_press_maybe(action.clone()) + .width(Length::Fill); + + let password_input = h_input( + "Password", + "", + &harbor.password_input_str, + Message::PasswordInputChanged, + action.clone(), + true, + Some("password_unlock_input"), + None, + ); + + let welcome_message = text("Welcome, we're glad you are here.").size(24); + + column![harbor_logo(), welcome_message, password_input, new_wallet] + .spacing(32) + .align_items(Alignment::Center) + .width(Length::Fixed(350.)) + } + }; + + container(center(column)).into() +}