diff --git a/src/bridge.rs b/src/bridge.rs index f8cc574..140f3ac 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -1,4 +1,5 @@ use fedimint_core::Amount; +use fedimint_core::api::InviteCode; use fedimint_ln_common::lightning_invoice::Bolt11Invoice; use tokio::sync::mpsc; @@ -8,6 +9,7 @@ pub enum UICoreMsg { FakeSend(u64), Send(Bolt11Invoice), Receive(u64), + AddFederation(InviteCode), } #[derive(Debug, Clone)] @@ -20,6 +22,7 @@ pub enum CoreUIMsg { ReceiveSuccess, ReceiveFailed(String), BalanceUpdated(Amount), + AddFederationFailed(String), } #[derive(Debug)] diff --git a/src/core.rs b/src/core.rs index 4d6e480..4d4f2bd 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,9 +1,12 @@ use anyhow::anyhow; +use bip39::Mnemonic; use bitcoin::Network; use fedimint_core::api::InviteCode; +use fedimint_core::config::FederationId; use fedimint_core::Amount; use fedimint_ln_client::{LightningClientModule, PayType}; use fedimint_ln_common::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; +use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use std::sync::atomic::AtomicBool; @@ -14,6 +17,7 @@ use iced::{ subscription::{self, Subscription}, }; use log::error; +use tokio::sync::RwLock; use tokio::time::sleep; use crate::{ @@ -31,8 +35,11 @@ use crate::{ struct HarborCore { balance: Amount, + network: Network, + mnemonic: Mnemonic, tx: Sender, - client: FedimintClient, // todo multiple clients + clients: Arc>>, + stop: Arc, } impl HarborCore { @@ -77,14 +84,17 @@ impl HarborCore { self.msg(CoreUIMsg::BalanceUpdated(self.balance)).await; } - async fn send(&mut self, invoice: Bolt11Invoice) -> anyhow::Result<()> { - log::info!("Sending invoice: {invoice}"); - let lightning_module = self - .client - .fedimint_client - .get_first_module::(); + // todo for now just use the first client, but eventually we'll want to have a way to select a client + async fn get_client(&self) -> FedimintClient { + self.clients.read().await.values().next().unwrap().clone() + } + + async fn send(&self, invoice: Bolt11Invoice) -> anyhow::Result<()> { + // todo go through all clients and select the first one that has enough balance + let client = self.get_client().await.fedimint_client; + let lightning_module = client.get_first_module::(); - let gateway = select_gateway(&self.client.fedimint_client) + let gateway = select_gateway(&client) .await .ok_or(anyhow!("Internal error: No gateway found for federation"))?; @@ -95,21 +105,11 @@ impl HarborCore { match outgoing.payment_type { PayType::Internal(op_id) => { let sub = lightning_module.subscribe_internal_pay(op_id).await?; - spawn_internal_payment_subscription( - self.tx.clone(), - self.client.fedimint_client.clone(), - sub, - ) - .await; + spawn_internal_payment_subscription(self.tx.clone(), client.clone(), sub).await; } PayType::Lightning(op_id) => { let sub = lightning_module.subscribe_ln_pay(op_id).await?; - spawn_invoice_payment_subscription( - self.tx.clone(), - self.client.fedimint_client.clone(), - sub, - ) - .await; + spawn_invoice_payment_subscription(self.tx.clone(), client.clone(), sub).await; } } @@ -119,12 +119,10 @@ impl HarborCore { } async fn receive(&self, amount: u64) -> anyhow::Result { - let lightning_module = self - .client - .fedimint_client - .get_first_module::(); + let client = self.get_client().await.fedimint_client; + let lightning_module = client.get_first_module::(); - let gateway = select_gateway(&self.client.fedimint_client) + let gateway = select_gateway(&client) .await .ok_or(anyhow!("Internal error: No gateway found for federation"))?; @@ -143,18 +141,32 @@ impl HarborCore { // Create subscription to operation if it exists if let Ok(subscription) = lightning_module.subscribe_ln_receive(op_id).await { - spawn_invoice_receive_subscription( - self.tx.clone(), - self.client.fedimint_client.clone(), - subscription, - ) - .await; + spawn_invoice_receive_subscription(self.tx.clone(), client.clone(), subscription).await; } else { error!("Could not create subscription to lightning receive"); } Ok(invoice) } + + async fn add_federation(&self, invite_code: InviteCode) -> anyhow::Result<()> { + let id = invite_code.federation_id(); + + let mut clients = self.clients.write().await; + if clients.get(&id).is_some() { + return Err(anyhow!("Federation already added")); + } + + let client = + FedimintClient::new(invite_code, &self.mnemonic, self.network, self.stop.clone()) + .await?; + + clients.insert(client.fedimint_client.federation_id(), client); + + // todo add to database + + Ok(()) + } } pub fn run_core() -> Subscription { @@ -169,8 +181,7 @@ pub fn run_core() -> Subscription { } let mut state = State::NeedsInit; - let handles = bridge::create_handles(); - let (ui_handle, mut core_handle) = handles; + let (ui_handle, mut core_handle) = bridge::create_handles(); let arc_ui_handle = Arc::new(ui_handle); let network = Network::Signet; @@ -190,24 +201,33 @@ pub fn run_core() -> Subscription { let mnemonic = get_mnemonic(db).expect("should get seed"); + let stop = Arc::new(AtomicBool::new(false)); + // fixme, properly initialize this let client = FedimintClient::new( - "test".to_string(), InviteCode::from_str("fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er").unwrap(), &mnemonic, network, - Arc::new(AtomicBool::new(false)), + stop.clone(), ) .await .expect("Could not create fedimint client"); - let balance = client.fedimint_client.get_balance().await; + let mut clients = HashMap::new(); + clients.insert(client.fedimint_client.federation_id(), client); + + let mut balance = Amount::ZERO; + for client in clients.values() { + balance += client.fedimint_client.get_balance().await; + } let mut core = HarborCore { balance, tx, - // TODO: add a database handle that works across async stuff - client, + mnemonic, + network, + clients: Arc::new(RwLock::new(clients)), + stop, }; loop { @@ -255,6 +275,13 @@ pub fn run_core() -> Subscription { } } } + UICoreMsg::AddFederation(invite_code) => { + if let Err(e) = core.add_federation(invite_code).await { + error!("Error adding federation: {e}"); + core.msg(CoreUIMsg::AddFederationFailed(e.to_string())) + .await; + } + } } } } diff --git a/src/fedimint_client.rs b/src/fedimint_client.rs index 8ee0f9d..4bc8673 100644 --- a/src/fedimint_client.rs +++ b/src/fedimint_client.rs @@ -19,7 +19,7 @@ use fedimint_wallet_client::{WalletClientInit, WalletClientModule}; use iced::futures::channel::mpsc::Sender; use iced::futures::{SinkExt, StreamExt}; use log::{debug, error, info, trace}; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Instant; use tokio::spawn; @@ -27,24 +27,20 @@ use tokio::spawn; #[derive(Debug, Clone)] #[allow(unused)] // TODO: remove pub(crate) struct FedimintClient { - pub(crate) uuid: String, pub(crate) fedimint_client: ClientHandleArc, invite_code: InviteCode, stop: Arc, } impl FedimintClient { - #[allow(clippy::too_many_arguments)] pub(crate) async fn new( - uuid: String, - federation_code: InviteCode, + invite_code: InviteCode, mnemonic: &Mnemonic, network: Network, stop: Arc, ) -> anyhow::Result { - info!("initializing a new federation client: {uuid}"); - - let federation_id = federation_code.federation_id(); + let federation_id = invite_code.federation_id(); + info!("initializing a new federation client: {federation_id}"); trace!("Building fedimint client db"); // todo use a real db @@ -72,7 +68,7 @@ impl FedimintClient { })? } else { let download = Instant::now(); - let config = ClientConfig::download_from_invite_code(&federation_code) + let config = ClientConfig::download_from_invite_code(&invite_code) .await .map_err(|e| { error!("Could not download federation info: {e}"); @@ -109,6 +105,7 @@ impl FedimintClient { // Update gateway cache in background let client_clone = fedimint_client.clone(); + let stop_clone = stop.clone(); spawn(async move { let start = Instant::now(); let lightning_module = client_clone.get_first_module::(); @@ -123,17 +120,26 @@ impl FedimintClient { } trace!( - "Setting active gateway took: {}ms", + "Updating gateway cache took: {}ms", start.elapsed().as_millis() ); + + // continually update gateway cache + loop { + lightning_module + .update_gateway_cache_continuously(|g| async { g }) + .await; + if stop_clone.load(Ordering::Relaxed) { + break; + } + } }); debug!("Built fedimint client"); Ok(FedimintClient { - uuid, fedimint_client, - invite_code: federation_code, + invite_code, stop, }) } @@ -141,8 +147,9 @@ impl FedimintClient { pub(crate) async fn select_gateway(client: &ClientHandleArc) -> Option { let ln = client.get_first_module::(); - let mut selected_gateway = None; - for gateway in ln.list_gateways().await { + let gateways = ln.list_gateways().await; + let mut selected_gateway: Option = None; + for gateway in gateways.iter() { // first try to find a vetted gateway if gateway.vetted { // if we can select the gateway, return it @@ -155,14 +162,17 @@ pub(crate) async fn select_gateway(client: &ClientHandleArc) -> Option= 1_000 && fees.proportional_millionths >= 100 { if let Some(g) = ln.select_gateway(&gateway.info.gateway_id).await { - selected_gateway = Some(g); + // only select gateways that support private payments, unless we don't have a gateway + if g.supports_private_payments || selected_gateway.is_none() { + selected_gateway = Some(g); + } } } } // if no gateway found, just select the first one we can find if selected_gateway.is_none() { - for gateway in ln.list_gateways().await { + for gateway in gateways { if let Some(g) = ln.select_gateway(&gateway.info.gateway_id).await { selected_gateway = Some(g); break; diff --git a/src/main.rs b/src/main.rs index 4f2e01d..e488ab2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -253,6 +253,10 @@ impl HarborWallet { self.receive_invoice = Some(invoice); Command::none() } + CoreUIMsg::AddFederationFailed(_) => { + // todo show error + Command::none() + } }, } }