diff --git a/examples/node/cli.rs b/examples/node/cli.rs index 929a87e7..0580d305 100644 --- a/examples/node/cli.rs +++ b/examples/node/cli.rs @@ -323,10 +323,10 @@ pub(crate) fn poll_for_user_input(node: &LightningNode, log_file_path: &str) { Err(message) => println!("{}", format!("{message:#}").red()), }, "foreground" => { - node.foreground(); + node.config().foreground(); } "background" => { - node.background(); + node.config().background(); } "closechannels" => { if let Err(message) = node.close_all_channels_with_current_lsp() { @@ -694,7 +694,7 @@ fn get_exchange_rate(node: &LightningNode) { } fn list_currency_codes(node: &LightningNode) { - let codes = node.list_currency_codes(); + let codes = node.config().list_currencies(); println!("Supported currencies: {codes:?}"); } @@ -702,7 +702,8 @@ fn change_currency(node: &LightningNode, words: &mut dyn Iterator) let fiat_currency = words .next() .ok_or(anyhow!("Fiat currency code is required"))?; - node.change_fiat_currency(String::from(fiat_currency))?; + node.config() + .set_fiat_currency(String::from(fiat_currency))?; Ok(()) } @@ -724,7 +725,7 @@ fn change_timezone(node: &LightningNode, words: &mut dyn Iterator) tz_config.timezone_utc_offset_secs ); println!(" Timezone id: {}", tz_config.timezone_id); - node.change_timezone_config(tz_config); + node.config().set_timezone_config(tz_config); Ok(()) } @@ -1656,7 +1657,9 @@ fn set_feature_flag(node: &LightningNode, words: &mut dyn Iterator) .next() .ok_or(anyhow!(" is required"))? .parse()?; - node.set_feature_flag(feature, enabled).map_err(Into::into) + node.config() + .set_feature_flag(feature, enabled) + .map_err(Into::into) } fn hide_failed_swap(node: &LightningNode, words: &mut dyn Iterator) -> Result<()> { diff --git a/examples/node/main.rs b/examples/node/main.rs index ba06e7c2..e651c328 100644 --- a/examples/node/main.rs +++ b/examples/node/main.rs @@ -11,7 +11,7 @@ use uniffi_lipalightninglib::{ mnemonic_to_secret, recover_lightning_node, BreezSdkConfig, LightningNode, MaxRoutingFeeConfig, ReceiveLimitsConfig, RemoteServicesConfig, }; -use uniffi_lipalightninglib::{Config, TzConfig}; +use uniffi_lipalightninglib::{LightningNodeConfig, TzConfig}; use crate::environment::{Environment, EnvironmentCode}; use log::Level; @@ -53,7 +53,7 @@ fn main() { .unwrap(); } - let config = Config { + let config = LightningNodeConfig { seed, default_fiat_currency: "EUR".to_string(), local_persistence_path: base_dir.clone(), diff --git a/examples/notification_handler/main.rs b/examples/notification_handler/main.rs index 2a0c0016..c73b427c 100644 --- a/examples/notification_handler/main.rs +++ b/examples/notification_handler/main.rs @@ -15,8 +15,8 @@ use std::collections::HashSet; use std::env; use std::time::Duration; use uniffi_lipalightninglib::{ - handle_notification, mnemonic_to_secret, BreezSdkConfig, Config, MaxRoutingFeeConfig, - NotificationToggles, ReceiveLimitsConfig, RemoteServicesConfig, TzConfig, + handle_notification, mnemonic_to_secret, BreezSdkConfig, LightningNodeConfig, + MaxRoutingFeeConfig, NotificationToggles, ReceiveLimitsConfig, RemoteServicesConfig, TzConfig, }; static BASE_DIR: &str = ".3l_node"; @@ -112,7 +112,7 @@ fn map_environment_code(code: &str) -> EnvironmentCode { } } -fn get_config() -> Config { +fn get_config() -> LightningNodeConfig { let base_dir = format!("{BASE_DIR}_{}", ENVIRONMENT.as_str()); let environment_code = map_environment_code(ENVIRONMENT.as_str()); @@ -120,7 +120,7 @@ fn get_config() -> Config { let seed = read_seed_from_env(); - Config { + LightningNodeConfig { seed, default_fiat_currency: "EUR".to_string(), local_persistence_path: base_dir.clone(), diff --git a/src/activities.rs b/src/activities.rs index ea30143d..ef306918 100644 --- a/src/activities.rs +++ b/src/activities.rs @@ -1,53 +1,33 @@ use crate::amount::{AsSats, ToAmount}; -use crate::async_runtime::Handle; -use crate::config::WithTimezone; -use crate::data_store::{CreatedInvoice, DataStore}; +use crate::data_store::CreatedInvoice; use crate::errors::Result; use crate::locker::Locker; +use crate::node_config::WithTimezone; use crate::support::Support; use crate::util::unix_timestamp_to_system_time; use crate::{ fill_payout_fee, filter_out_and_log_corrupted_activities, - filter_out_and_log_corrupted_payments, Activity, ChannelCloseInfo, ChannelCloseState, Config, + filter_out_and_log_corrupted_payments, Activity, ChannelCloseInfo, ChannelCloseState, IncomingPaymentInfo, InvoiceDetails, ListActivitiesResponse, OutgoingPaymentInfo, PaymentInfo, - PaymentState, ReverseSwapInfo, RuntimeErrorCode, SwapInfo, UserPreferences, + PaymentState, ReverseSwapInfo, RuntimeErrorCode, SwapInfo, }; use breez_sdk_core::{ - parse_invoice, BreezServices, ClosedChannelPaymentDetails, ListPaymentsRequest, PaymentDetails, - PaymentStatus, PaymentTypeFilter, + parse_invoice, ClosedChannelPaymentDetails, ListPaymentsRequest, PaymentDetails, PaymentStatus, + PaymentTypeFilter, }; use perro::{invalid_input, permanent_failure, MapToError, OptionToError}; use std::cmp::{min, Reverse}; use std::collections::HashSet; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::SystemTime; pub struct Activities { - rt_handle: Handle, - sdk: Arc, - data_store: Arc>, - user_preferences: Arc>, - config: Config, support: Arc, } impl Activities { - pub(crate) fn new( - rt_handle: Handle, - sdk: Arc, - data_store: Arc>, - user_preferences: Arc>, - config: Config, - support: Arc, - ) -> Self { - Self { - rt_handle, - sdk, - data_store, - user_preferences, - config, - support, - } + pub(crate) fn new(support: Arc) -> Self { + Self { support } } /// List the latest activities @@ -72,8 +52,10 @@ impl Activities { offset: None, }; let breez_activities = self - .rt_handle - .block_on(self.sdk.list_payments(list_payments_request)) + .support + .rt + .handle() + .block_on(self.support.sdk.list_payments(list_payments_request)) .map_to_runtime_error(RuntimeErrorCode::NodeUnavailable, "Failed to list payments")? .into_iter() .map(|p| self.activity_from_breez_payment(p)) @@ -82,6 +64,7 @@ impl Activities { // Query created invoices, filter out ones which are in the breez db. let created_invoices = self + .support .data_store .lock_unwrap() .retrieve_created_invoices(number_of_completed_activities)?; @@ -103,15 +86,23 @@ impl Activities { completed_activities.truncate(number_of_completed_activities as usize); if let Some(in_progress_swap) = self - .rt_handle - .block_on(self.sdk.in_progress_swap()) + .support + .rt + .handle() + .block_on(self.support.sdk.in_progress_swap()) .map_to_runtime_error( RuntimeErrorCode::NodeUnavailable, "Failed to get in-progress swap", )? { let created_at = unix_timestamp_to_system_time(in_progress_swap.created_at as u64) - .with_timezone(self.user_preferences.lock_unwrap().clone().timezone_config); + .with_timezone( + self.support + .user_preferences + .lock_unwrap() + .clone() + .timezone_config, + ); pending_activities.push(Activity::Swap { incoming_payment_info: None, @@ -145,8 +136,10 @@ impl Activities { /// Requires network: **no** pub fn get(&self, hash: String) -> Result { if let Some(activity) = self - .rt_handle - .block_on(self.sdk.payment_by_hash(hash.clone())) + .support + .rt + .handle() + .block_on(self.support.sdk.payment_by_hash(hash.clone())) .map_to_runtime_error( RuntimeErrorCode::NodeUnavailable, "Failed to get payment by hash", @@ -155,6 +148,7 @@ impl Activities { { Ok(activity?) } else if let Some(incoming_payment_info) = self + .support .data_store .lock_unwrap() .retrieve_created_invoice_by_hash(&hash)? @@ -194,8 +188,10 @@ impl Activities { } false }; - self.rt_handle - .block_on(self.sdk.list_payments(list_payments_request)) + self.support + .rt + .handle() + .block_on(self.support.sdk.list_payments(list_payments_request)) .map_to_runtime_error(RuntimeErrorCode::NodeUnavailable, "Failed to list payments")? .into_iter() .find(is_swap_with_id) @@ -262,7 +258,8 @@ impl Activities { pub fn set_personal_note(&self, payment_hash: String, note: String) -> Result<()> { let note = Some(note.trim().to_string()).filter(|s| !s.is_empty()); - self.data_store + self.support + .data_store .lock_unwrap() .update_personal_note(&payment_hash, note.as_deref()) } @@ -316,6 +313,7 @@ impl Activities { } }; let local_payment_data = self + .support .data_store .lock_unwrap() .retrieve_payment_info(&payment_details.payment_hash)?; @@ -331,7 +329,11 @@ impl Activities { ), None => ( self.support.get_exchange_rate(), - self.user_preferences.lock_unwrap().timezone_config.clone(), + self.support + .user_preferences + .lock_unwrap() + .timezone_config + .clone(), None, None, None, @@ -347,7 +349,11 @@ impl Activities { personal_note, received_on, received_lnurl_comment, - &self.config.remote_services_config.lipa_lightning_domain, + &self + .support + .node_config + .remote_services_config + .lipa_lightning_domain, )?; let offer_kind = fill_payout_fee( offer, @@ -373,7 +379,11 @@ impl Activities { personal_note, received_on, received_lnurl_comment, - &self.config.remote_services_config.lipa_lightning_domain, + &self + .support + .node_config + .remote_services_config + .lipa_lightning_domain, )?; Ok(Activity::Swap { incoming_payment_info: Some(incoming_payment_info), @@ -394,7 +404,11 @@ impl Activities { &exchange_rate, tz_config, personal_note, - &self.config.remote_services_config.lipa_lightning_domain, + &self + .support + .node_config + .remote_services_config + .lipa_lightning_domain, )?; Ok(Activity::ReverseSwap { outgoing_payment_info, @@ -408,7 +422,11 @@ impl Activities { personal_note, received_on, received_lnurl_comment, - &self.config.remote_services_config.lipa_lightning_domain, + &self + .support + .node_config + .remote_services_config + .lipa_lightning_domain, )?; Ok(Activity::IncomingPayment { incoming_payment_info, @@ -419,7 +437,11 @@ impl Activities { &exchange_rate, tz_config, personal_note, - &self.config.remote_services_config.lipa_lightning_domain, + &self + .support + .node_config + .remote_services_config + .lipa_lightning_domain, )?; Ok(Activity::OutgoingPayment { outgoing_payment_info, @@ -439,7 +461,7 @@ impl Activities { .as_msats() .to_amount_up(&self.support.get_exchange_rate()); - let user_preferences = self.user_preferences.lock_unwrap(); + let user_preferences = self.support.user_preferences.lock_unwrap(); let time = unix_timestamp_to_system_time(breez_payment.payment_time as u64) .with_timezone(user_preferences.timezone_config.clone()); @@ -483,6 +505,7 @@ impl Activities { }; let local_payment_data = self + .support .data_store .lock_unwrap() .retrieve_payment_info(&invoice_details.payment_hash)? diff --git a/src/config.rs b/src/config.rs index 2337a128..83d32d09 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,98 +1,187 @@ -use std::time::SystemTime; +use crate::errors::Result; +use crate::locker::Locker; +use crate::support::Support; +use crate::{with_status, AnalyticsConfig, EnableStatus, FeatureFlag, RuntimeErrorCode, TzConfig}; +use crow::{CountryCode, LanguageCode}; +use log::info; +use perro::{MapToError, ResultTrait}; +use std::str::FromStr; +use std::sync::Arc; -/// An object that holds all configuration needed to start a LightningNode instance. -#[derive(Debug, Clone)] pub struct Config { - /// The seed derived from the mnemonic optionally including a pass phrase - pub seed: Vec, - /// ISO 4217 currency code. The backend does not support all of them, but supports at least USD - /// and EUR, so it is safe to default to one of them. Providing an invalid code will result in - /// missing fiat values for payments. - /// - /// The provided value is used as a default. After the first time the node is started, - /// this config starts being ignored. Changing the fiat currency can be done using - /// [`crate::LightningNode::change_fiat_currency`]. - pub default_fiat_currency: String, - /// A path on the local filesystem where this library will directly persist data. Only the - /// current instance of the app should have access to the provided directory. On app - /// uninstall/deletion, the directory should be purged. - pub local_persistence_path: String, - /// A timezone configuration object. - pub timezone_config: TzConfig, - /// If a value is provided, logs using the provided level will be created in the provided - /// `local_persistence_path`. - pub file_logging_level: Option, - /// The list of allowed countries for the use of phone numbers as identifiers. - pub phone_number_allowed_countries_iso_3166_1_alpha_2: Vec, - pub remote_services_config: RemoteServicesConfig, - pub breez_sdk_config: BreezSdkConfig, - pub max_routing_fee_config: MaxRoutingFeeConfig, - pub receive_limits_config: ReceiveLimitsConfig, + support: Arc, } -#[derive(Debug, Clone)] -pub struct RemoteServicesConfig { - /// lipa's backend URL. - pub backend_url: String, - /// Pocket's backend URL. - pub pocket_url: String, - /// Base URL used to construct the webhook URL used for notifications. - pub notification_webhook_base_url: String, - /// Secret used to encrypt the wallet's ID before being added to the webhook URL. - pub notification_webhook_secret_hex: String, - /// The domain used in lipa Lightning Addresses. - pub lipa_lightning_domain: String, -} +impl Config { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new(support: Arc) -> Self { + Self { support } + } -#[derive(Debug, Clone)] -pub struct MaxRoutingFeeConfig { - /// Routing fees will be limited to relative per myriad provided here. - pub max_routing_fee_permyriad: u16, - /// When the fee is lower or equal to this value, the relative limit is ignored. - pub max_routing_fee_exempt_fee_sats: u64, -} + /// Set the fiat currency (ISO 4217 currency code) - not all are supported + /// The method [`Config::list_currencies`] can used to list supported codes. + /// + /// Requires network: **no** + pub fn set_fiat_currency(&self, fiat_currency: String) -> Result<()> { + self.support + .data_store + .lock_unwrap() + .store_selected_fiat_currency(&fiat_currency)?; + self.support.user_preferences.lock_unwrap().fiat_currency = fiat_currency; + Ok(()) + } -#[derive(Debug, Clone)] -pub struct BreezSdkConfig { - pub breez_sdk_api_key: String, - pub breez_sdk_partner_certificate: String, - pub breez_sdk_partner_key: String, -} + /// Set the timezone config. + /// + /// Parameters: + /// * `timezone_config` - the user's current timezone + /// + /// Requires network: **no** + pub fn set_timezone_config(&self, timezone_config: TzConfig) { + self.support.user_preferences.lock_unwrap().timezone_config = timezone_config; + } -#[derive(Debug, Clone)] -pub struct ReceiveLimitsConfig { - pub max_receive_amount_sat: u64, - pub min_receive_channel_open_fee_multiplier: f64, -} + /// Set the analytics configuration. + /// + /// This can be used to completely prevent any analytics data from being reported. + /// + /// Requires network: **no** + pub fn set_analytics_config(&self, config: AnalyticsConfig) -> Result<()> { + *self.support.analytics_interceptor.config.lock_unwrap() = config.clone(); + self.support + .data_store + .lock_unwrap() + .append_analytics_config(config) + } -/// An object that holds timezone configuration values necessary for 3L to do timestamp annotation. These values get tied -/// together with every timestamp persisted in the local payment database. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct TzConfig { - /// String identifier whose format is completely arbitrary and can be chosen by the user - pub timezone_id: String, - /// Offset from the UTC timezone in seconds - pub timezone_utc_offset_secs: i32, -} + /// Get the currently configured analytics configuration. + /// + /// Requires network: **no** + pub fn get_analytics_config(&self) -> Result { + self.support + .data_store + .lock_unwrap() + .retrieve_analytics_config() + } -/// A UTC timestamp accompanied by the ID of the timezone on which it was recorded and the respective UTC offset. -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct TzTime { - pub time: SystemTime, - pub timezone_id: String, - pub timezone_utc_offset_secs: i32, -} + /// Registers a new notification token. If a token has already been registered, it will be updated. + /// + /// Requires network: **yes** + pub fn register_notification_token( + &self, + notification_token: String, + language_iso_639_1: String, + country_iso_3166_1_alpha_2: String, + ) -> Result<()> { + let language = LanguageCode::from_str(&language_iso_639_1.to_lowercase()) + .map_to_invalid_input("Invalid language code")?; + let country = CountryCode::for_alpha2(&country_iso_3166_1_alpha_2.to_uppercase()) + .map_to_invalid_input("Invalid country code")?; -pub(crate) trait WithTimezone { - fn with_timezone(self, tz_config: TzConfig) -> TzTime; -} + self.support + .offer_manager + .register_notification_token(notification_token, language, country) + .map_runtime_error_to(RuntimeErrorCode::OfferServiceUnavailable) + } + + /// Set value of a feature flag. + /// The method will report the change to the backend and update the local database. + /// + /// Parameters: + /// * `feature` - feature flag to be set. + /// * `enable` - enable or disable the feature. + /// + /// Requires network: **yes** + pub fn set_feature_flag(&self, feature: FeatureFlag, flag_enabled: bool) -> Result<()> { + let kind_of_address = match feature { + FeatureFlag::LightningAddress => |a: &String| !a.starts_with('-'), + FeatureFlag::PhoneNumber => |a: &String| a.starts_with('-'), + }; + let (from_status, to_status) = match flag_enabled { + true => (EnableStatus::FeatureDisabled, EnableStatus::Enabled), + false => (EnableStatus::Enabled, EnableStatus::FeatureDisabled), + }; -impl WithTimezone for SystemTime { - fn with_timezone(self, tz_config: TzConfig) -> TzTime { - TzTime { - time: self, - timezone_id: tz_config.timezone_id, - timezone_utc_offset_secs: tz_config.timezone_utc_offset_secs, + let addresses = self + .support + .data_store + .lock_unwrap() + .retrieve_lightning_addresses()? + .into_iter() + .filter_map(with_status(from_status)) + .filter(kind_of_address) + .collect::>(); + + if addresses.is_empty() { + info!("No lightning addresses to change the status"); + return Ok(()); } + + let doing = match flag_enabled { + true => "Enabling", + false => "Disabling", + }; + info!("{doing} {addresses:?} on the backend"); + + self.support + .rt + .handle() + .block_on(async { + if flag_enabled { + pigeon::enable_lightning_addresses( + &self.support.node_config.remote_services_config.backend_url, + &self.support.async_auth, + addresses.clone(), + ) + .await + } else { + pigeon::disable_lightning_addresses( + &self.support.node_config.remote_services_config.backend_url, + &self.support.async_auth, + addresses.clone(), + ) + .await + } + }) + .map_to_runtime_error( + RuntimeErrorCode::AuthServiceUnavailable, + "Failed to enable/disable a lightning address", + )?; + let mut data_store = self.support.data_store.lock_unwrap(); + addresses + .into_iter() + .try_for_each(|a| data_store.update_lightning_address(&a, to_status)) + } + + /// List codes of supported fiat currencies. + /// Please keep in mind that this method doesn't make any network calls. It simply retrieves + /// previously fetched values that are frequently updated by a background task. + /// + /// The fetched list will be persisted across restarts to alleviate the consequences of a + /// slow or unresponsive exchange rate service. + /// The method will return an empty list if there is nothing persisted yet and + /// the values are not yet fetched from the service. + /// + /// Requires network: **no** + pub fn list_currencies(&self) -> Vec { + let rates = self.support.task_manager.lock_unwrap().get_exchange_rates(); + rates.iter().map(|r| r.currency_code.clone()).collect() + } + + /// Call the method when the app goes to foreground, such that the user can interact with it. + /// The library starts running the background tasks more frequently to improve user experience. + /// + /// Requires network: **no** + pub fn foreground(&self) { + self.support.task_manager.lock_unwrap().foreground(); + } + + /// Call the method when the app goes to background, such that the user can not interact with it. + /// The library stops running some unnecessary tasks and runs necessary tasks less frequently. + /// It should save battery and internet traffic. + /// + /// Requires network: **no** + pub fn background(&self) { + self.support.task_manager.lock_unwrap().background(); } } diff --git a/src/data_store.rs b/src/data_store.rs index b581360c..d12bb8c8 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -700,8 +700,8 @@ fn fiat_topup_info_from_row(row: &Row) -> rusqlite::Result #[cfg(test)] mod tests { - use crate::config::TzConfig; use crate::data_store::{CreatedInvoice, DataStore}; + use crate::node_config::TzConfig; use crate::{EnableStatus, ExchangeRate, OfferKind, PocketOfferError, UserPreferences}; use crate::analytics::AnalyticsConfig; diff --git a/src/lib.rs b/src/lib.rs index 1378cf56..9b66a217 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ mod limits; mod locker; mod logger; mod migrations; +mod node_config; mod notification_handling; mod offer; mod payment; @@ -53,10 +54,6 @@ use crate::async_runtime::AsyncRuntime; use crate::auth::{build_async_auth, build_auth}; use crate::backup::BackupManager; pub use crate::callbacks::EventsCallback; -pub use crate::config::{ - BreezSdkConfig, Config, MaxRoutingFeeConfig, ReceiveLimitsConfig, RemoteServicesConfig, - TzConfig, TzTime, -}; pub use crate::errors::{ DecodeDataError, Error as LnError, LnUrlPayError, LnUrlPayErrorCode, LnUrlPayResult, MnemonicError, NotificationHandlingError, NotificationHandlingErrorCode, ParseError, @@ -74,6 +71,10 @@ pub use crate::lightning::lnurl::{LnUrlPayDetails, LnUrlWithdrawDetails, Lnurl}; pub use crate::lightning::receive_limits::{LiquidityLimit, ReceiveAmountLimits}; pub use crate::limits::PaymentAmountLimits; use crate::locker::Locker; +pub use crate::node_config::{ + BreezSdkConfig, LightningNodeConfig, MaxRoutingFeeConfig, ReceiveLimitsConfig, + RemoteServicesConfig, TzConfig, TzTime, +}; pub use crate::notification_handling::{handle_notification, Notification, NotificationToggles}; pub use crate::offer::{OfferInfo, OfferKind, OfferStatus}; pub use crate::payment::{ @@ -103,6 +104,7 @@ pub use crate::pocketclient::FiatTopupInfo; use crate::pocketclient::PocketClient; pub use crate::activities::Activities; +use crate::config::Config; pub use crate::lightning::{Lightning, PaymentAffordability}; use crate::support::Support; pub use breez_sdk_core::error::ReceiveOnchainError as SwapError; @@ -119,7 +121,7 @@ use breez_sdk_core::{ PrepareRefundRequest, ReceiveOnchainRequest, RedeemOnchainFundsRequest, RefundRequest, SignMessageRequest, UnspentTransactionOutput, }; -use crow::{CountryCode, LanguageCode, OfferManager, TopupError, TopupInfo}; +use crow::{OfferManager, TopupError, TopupInfo}; pub use crow::{PermanentFailureCode, TemporaryFailureCode}; use data_store::DataStore; use email_address::EmailAddress; @@ -308,19 +310,19 @@ pub struct LightningNode { sdk: Arc, auth: Arc, async_auth: Arc, - fiat_topup_client: PocketClient, - offer_manager: OfferManager, - rt: AsyncRuntime, + fiat_topup_client: Arc, + offer_manager: Arc, + rt: Arc, data_store: Arc>, task_manager: Arc>, - analytics_interceptor: Arc, allowed_countries_country_iso_3166_1_alpha_2: Vec, phone_number_prefix_parser: PhoneNumberPrefixParser, persistence_encryption_key: [u8; 32], - config: Config, + node_config: LightningNodeConfig, activities: Arc, lightning: Arc, support: Arc, + config: Arc, } /// Contains the fee information for the options to resolve funds that have moved on-chain. @@ -370,39 +372,44 @@ impl LightningNode { /// of certain events. /// /// Requires network: **yes** - pub fn new(config: Config, events_callback: Box) -> Result { + pub fn new( + node_config: LightningNodeConfig, + events_callback: Box, + ) -> Result { enable_backtrace(); - fs::create_dir_all(&config.local_persistence_path).map_to_permanent_failure(format!( - "Failed to create directory: {}", - &config.local_persistence_path, - ))?; - if let Some(level) = config.file_logging_level { + fs::create_dir_all(&node_config.local_persistence_path).map_to_permanent_failure( + format!( + "Failed to create directory: {}", + &node_config.local_persistence_path, + ), + )?; + if let Some(level) = node_config.file_logging_level { init_logger_once( level, - &Path::new(&config.local_persistence_path).join(LOGS_DIR), + &Path::new(&node_config.local_persistence_path).join(LOGS_DIR), )?; } info!("3L version: {}", env!("GITHUB_REF")); - let rt = AsyncRuntime::new()?; + let rt = Arc::new(AsyncRuntime::new()?); - let strong_typed_seed = sanitize_input::strong_type_seed(&config.seed)?; + let strong_typed_seed = sanitize_input::strong_type_seed(&node_config.seed)?; let auth = Arc::new(build_auth( &strong_typed_seed, - &config.remote_services_config.backend_url, + &node_config.remote_services_config.backend_url, )?); let async_auth = Arc::new(build_async_auth( &strong_typed_seed, - &config.remote_services_config.backend_url, + &node_config.remote_services_config.backend_url, )?); - let db_path = format!("{}/{DB_FILENAME}", config.local_persistence_path); + let db_path = format!("{}/{DB_FILENAME}", node_config.local_persistence_path); let mut data_store = DataStore::new(&db_path)?; let fiat_currency = match data_store.retrieve_last_set_fiat_currency()? { None => { - data_store.store_selected_fiat_currency(&config.default_fiat_currency)?; - config.default_fiat_currency.clone() + data_store.store_selected_fiat_currency(&node_config.default_fiat_currency)?; + node_config.default_fiat_currency.clone() } Some(c) => c, }; @@ -411,11 +418,11 @@ impl LightningNode { let user_preferences = Arc::new(Mutex::new(UserPreferences { fiat_currency, - timezone_config: config.timezone_config.clone(), + timezone_config: node_config.timezone_config.clone(), })); let analytics_client = AnalyticsClient::new( - config.remote_services_config.backend_url.clone(), + node_config.remote_services_config.backend_url.clone(), derive_analytics_keys(&strong_typed_seed)?, Arc::clone(&async_auth), ); @@ -435,7 +442,7 @@ impl LightningNode { )); let sdk = rt.handle().block_on(async { - let sdk = start_sdk(&config, event_listener).await?; + let sdk = start_sdk(&node_config, event_listener).await?; if sdk .lsp_id() .await @@ -462,24 +469,26 @@ impl LightningNode { })?; let exchange_rate_provider = Box::new(ExchangeRateProviderImpl::new( - config.remote_services_config.backend_url.clone(), + node_config.remote_services_config.backend_url.clone(), Arc::clone(&auth), )); - let offer_manager = OfferManager::new( - config.remote_services_config.backend_url.clone(), + let offer_manager = Arc::new(OfferManager::new( + node_config.remote_services_config.backend_url.clone(), Arc::clone(&auth), - ); + )); - let fiat_topup_client = PocketClient::new(config.remote_services_config.pocket_url.clone()) - .map_to_runtime_error( - RuntimeErrorCode::OfferServiceUnavailable, - "Couldn't create a fiat topup client", - )?; + let fiat_topup_client = Arc::new( + PocketClient::new(node_config.remote_services_config.pocket_url.clone()) + .map_to_runtime_error( + RuntimeErrorCode::OfferServiceUnavailable, + "Couldn't create a fiat topup client", + )?, + ); let persistence_encryption_key = derive_persistence_encryption_key(&strong_typed_seed)?; let backup_client = RemoteBackupClient::new( - config.remote_services_config.backend_url.clone(), + node_config.remote_services_config.backend_url.clone(), Arc::clone(&async_auth), ); let backup_manager = BackupManager::new(backup_client, db_path, persistence_encryption_key); @@ -491,40 +500,40 @@ impl LightningNode { Arc::clone(&sdk), backup_manager, events_callback, - config.breez_sdk_config.breez_sdk_api_key.clone(), + node_config.breez_sdk_config.breez_sdk_api_key.clone(), )?)); task_manager.lock_unwrap().foreground(); - register_webhook_url(&rt, &sdk, &auth, &config)?; + register_webhook_url(&rt, &sdk, &auth, &node_config)?; - let phone_number_prefix_parser = - PhoneNumberPrefixParser::new(&config.phone_number_allowed_countries_iso_3166_1_alpha_2); + let phone_number_prefix_parser = PhoneNumberPrefixParser::new( + &node_config.phone_number_allowed_countries_iso_3166_1_alpha_2, + ); - let support = Arc::new(Support::new( - Arc::clone(&user_preferences), - Arc::clone(&task_manager), - Arc::clone(&sdk), - )); + let support = Arc::new(Support { + user_preferences: Arc::clone(&user_preferences), + sdk: Arc::clone(&sdk), + auth: Arc::clone(&auth), + async_auth: Arc::clone(&async_auth), + fiat_topup_client: Arc::clone(&fiat_topup_client), + offer_manager: Arc::clone(&offer_manager), + rt: Arc::clone(&rt), + data_store: Arc::clone(&data_store), + task_manager: Arc::clone(&task_manager), + allowed_countries_country_iso_3166_1_alpha_2: node_config + .phone_number_allowed_countries_iso_3166_1_alpha_2 + .clone(), + phone_number_prefix_parser: phone_number_prefix_parser.clone(), + persistence_encryption_key, + node_config: node_config.clone(), + analytics_interceptor, + }); - let activities = Arc::new(Activities::new( - rt.handle(), - Arc::clone(&sdk), - Arc::clone(&data_store), - Arc::clone(&user_preferences), - config.clone(), - Arc::clone(&support), - )); + let activities = Arc::new(Activities::new(Arc::clone(&support))); - let lightning = Arc::new(Lightning::new( - rt.handle(), - Arc::clone(&sdk), - Arc::clone(&data_store), - Arc::clone(&analytics_interceptor), - Arc::clone(&user_preferences), - Arc::clone(&support), - config.clone(), - Arc::clone(&task_manager), - )); + let lightning = Arc::new(Lightning::new(Arc::clone(&support))); + + let config = Arc::new(Config::new(Arc::clone(&support))); Ok(LightningNode { user_preferences, @@ -536,16 +545,16 @@ impl LightningNode { rt, data_store, task_manager, - analytics_interceptor, - allowed_countries_country_iso_3166_1_alpha_2: config + allowed_countries_country_iso_3166_1_alpha_2: node_config .phone_number_allowed_countries_iso_3166_1_alpha_2 .clone(), phone_number_prefix_parser, persistence_encryption_key, - config, + node_config, activities, lightning, support, + config, }) } @@ -557,6 +566,10 @@ impl LightningNode { Arc::clone(&self.lightning) } + pub fn config(&self) -> Arc { + Arc::clone(&self.config) + } + /// Request some basic info about the node /// /// Requires network: **no** @@ -631,7 +644,7 @@ impl LightningNode { } /// Parse a phone number prefix, check against the list of allowed countries - /// (set in [`Config::phone_number_allowed_countries_iso_3166_1_alpha_2`]). + /// (set in [`LightningNodeConfig::phone_number_allowed_countries_iso_3166_1_alpha_2`]). /// The parser is not strict, it parses some invalid prefixes as valid. /// /// Requires network: **no** @@ -643,7 +656,7 @@ impl LightningNode { } /// Parse a phone number, check against the list of allowed countries - /// (set in [`Config::phone_number_allowed_countries_iso_3166_1_alpha_2`]). + /// (set in [`LightningNodeConfig::phone_number_allowed_countries_iso_3166_1_alpha_2`]). /// /// Returns a possible lightning address, which can be checked for existence /// with [`LightningNode::decode_data`]. @@ -654,8 +667,12 @@ impl LightningNode { phone_number: String, ) -> std::result::Result { let phone_number = self.parse_phone_number(phone_number)?; - Ok(phone_number - .to_lightning_address(&self.config.remote_services_config.lipa_lightning_domain)) + Ok(phone_number.to_lightning_address( + &self + .node_config + .remote_services_config + .lipa_lightning_domain, + )) } fn parse_phone_number( @@ -846,7 +863,10 @@ impl LightningNode { .map(|p| { Recipient::from_lightning_address( &p.0, - &self.config.remote_services_config.lipa_lightning_domain, + &self + .node_config + .remote_services_config + .lipa_lightning_domain, ) }) .collect(); @@ -947,8 +967,9 @@ impl LightningNode { /// The library starts running the background tasks more frequently to improve user experience. /// /// Requires network: **no** + #[deprecated = "config().foreground() should be used instead"] pub fn foreground(&self) { - self.task_manager.lock_unwrap().foreground(); + self.config.foreground() } /// Call the method when the app goes to background, such that the user can not interact with it. @@ -956,8 +977,9 @@ impl LightningNode { /// It should save battery and internet traffic. /// /// Requires network: **no** + #[deprecated = "config().background() should be used instead"] pub fn background(&self) { - self.task_manager.lock_unwrap().background(); + self.config.background() } /// List codes of supported fiat currencies. @@ -970,9 +992,9 @@ impl LightningNode { /// the values are not yet fetched from the service. /// /// Requires network: **no** + #[deprecated = "config().list_currencies() should be used instead"] pub fn list_currency_codes(&self) -> Vec { - let rates = self.task_manager.lock_unwrap().get_exchange_rates(); - rates.iter().map(|r| r.currency_code.clone()).collect() + self.config.list_currencies() } /// Get exchange rate on the BTC/default currency pair @@ -999,12 +1021,9 @@ impl LightningNode { /// The method [`LightningNode::list_currency_codes`] can used to list supported codes. /// /// Requires network: **no** + #[deprecated = "config().set_fiat_currency() should be used instead"] pub fn change_fiat_currency(&self, fiat_currency: String) -> Result<()> { - self.data_store - .lock_unwrap() - .store_selected_fiat_currency(&fiat_currency)?; - self.user_preferences.lock_unwrap().fiat_currency = fiat_currency; - Ok(()) + self.config.set_fiat_currency(fiat_currency) } /// Change the timezone config. @@ -1013,8 +1032,9 @@ impl LightningNode { /// * `timezone_config` - the user's current timezone /// /// Requires network: **no** + #[deprecated = "config().set_timezone_config() should be used instead"] pub fn change_timezone_config(&self, timezone_config: TzConfig) { - self.user_preferences.lock_unwrap().timezone_config = timezone_config; + self.config.set_timezone_config(timezone_config) } /// Accepts Pocket's T&C. @@ -1393,20 +1413,18 @@ impl LightningNode { /// Registers a new notification token. If a token has already been registered, it will be updated. /// /// Requires network: **yes** + #[deprecated = "config().register_notification_token() should be used instead"] pub fn register_notification_token( &self, notification_token: String, language_iso_639_1: String, country_iso_3166_1_alpha_2: String, ) -> Result<()> { - let language = LanguageCode::from_str(&language_iso_639_1.to_lowercase()) - .map_to_invalid_input("Invalid language code")?; - let country = CountryCode::for_alpha2(&country_iso_3166_1_alpha_2.to_uppercase()) - .map_to_invalid_input("Invalid country code")?; - - self.offer_manager - .register_notification_token(notification_token, language, country) - .map_runtime_error_to(RuntimeErrorCode::OfferServiceUnavailable) + self.config.register_notification_token( + notification_token, + language_iso_639_1, + country_iso_3166_1_alpha_2, + ) } /// Get the wallet UUID v5 from the wallet pubkey @@ -2104,7 +2122,7 @@ impl LightningNode { .rt .handle() .block_on(BreezServices::service_health_check( - self.config.breez_sdk_config.breez_sdk_api_key.clone(), + self.node_config.breez_sdk_config.breez_sdk_api_key.clone(), )) .map_to_runtime_error( RuntimeErrorCode::NodeUnavailable, @@ -2141,9 +2159,13 @@ impl LightningNode { let exchange_rate = self.get_exchange_rate(); // Accomodating lightning network routing fees. - let routing_fee = Permyriad(self.config.max_routing_fee_config.max_routing_fee_permyriad) - .of(&limits.min_sat.as_sats()) - .sats_round_up(); + let routing_fee = Permyriad( + self.node_config + .max_routing_fee_config + .max_routing_fee_permyriad, + ) + .of(&limits.min_sat.as_sats()) + .sats_round_up(); let min = limits.min_sat + routing_fee.sats; let range_hit = match balance_sat { balance_sat if balance_sat < min => RangeHit::Below { @@ -2241,18 +2263,17 @@ impl LightningNode { /// This can be used to completely prevent any analytics data from being reported. /// /// Requires network: **no** + #[deprecated = "config().set_analytics_config() should be used instead"] pub fn set_analytics_config(&self, config: AnalyticsConfig) -> Result<()> { - *self.analytics_interceptor.config.lock_unwrap() = config.clone(); - self.data_store - .lock_unwrap() - .append_analytics_config(config) + self.config.set_analytics_config(config) } /// Get the currently configured analytics configuration. /// /// Requires network: **no** + #[deprecated = "config().get_analytics_config() should be used instead"] pub fn get_analytics_config(&self) -> Result { - self.data_store.lock_unwrap().retrieve_analytics_config() + self.config.get_analytics_config() } /// Register a human-readable lightning address or return the previously @@ -2264,7 +2285,7 @@ impl LightningNode { .rt .handle() .block_on(pigeon::assign_lightning_address( - &self.config.remote_services_config.backend_url, + &self.node_config.remote_services_config.backend_url, &self.async_auth, )) .map_to_runtime_error( @@ -2304,7 +2325,10 @@ impl LightningNode { .and_then(|a| { lightning_address_to_phone_number( &a, - &self.config.remote_services_config.lipa_lightning_domain, + &self + .node_config + .remote_services_config + .lipa_lightning_domain, ) })) } @@ -2332,7 +2356,7 @@ impl LightningNode { self.rt .handle() .block_on(pigeon::request_phone_number_verification( - &self.config.remote_services_config.backend_url, + &self.node_config.remote_services_config.backend_url, &self.async_auth, phone_number.e164, encrypted_number, @@ -2358,7 +2382,7 @@ impl LightningNode { self.rt .handle() .block_on(pigeon::verify_phone_number( - &self.config.remote_services_config.backend_url, + &self.node_config.remote_services_config.backend_url, &self.async_auth, phone_number.e164.clone(), otp, @@ -2367,8 +2391,12 @@ impl LightningNode { RuntimeErrorCode::AuthServiceUnavailable, "Failed to submit phone number registration otp", )?; - let address = phone_number - .to_lightning_address(&self.config.remote_services_config.lipa_lightning_domain); + let address = phone_number.to_lightning_address( + &self + .node_config + .remote_services_config + .lipa_lightning_domain, + ); self.data_store .lock_unwrap() .store_lightning_address(&address) @@ -2382,63 +2410,9 @@ impl LightningNode { /// * `enable` - enable or disable the feature. /// /// Requires network: **yes** + #[deprecated = "config().set_feature_flag() should be used instead"] pub fn set_feature_flag(&self, feature: FeatureFlag, flag_enabled: bool) -> Result<()> { - let kind_of_address = match feature { - FeatureFlag::LightningAddress => |a: &String| !a.starts_with('-'), - FeatureFlag::PhoneNumber => |a: &String| a.starts_with('-'), - }; - let (from_status, to_status) = match flag_enabled { - true => (EnableStatus::FeatureDisabled, EnableStatus::Enabled), - false => (EnableStatus::Enabled, EnableStatus::FeatureDisabled), - }; - - let addresses = self - .data_store - .lock_unwrap() - .retrieve_lightning_addresses()? - .into_iter() - .filter_map(with_status(from_status)) - .filter(kind_of_address) - .collect::>(); - - if addresses.is_empty() { - info!("No lightning addresses to change the status"); - return Ok(()); - } - - let doing = match flag_enabled { - true => "Enabling", - false => "Disabling", - }; - info!("{doing} {addresses:?} on the backend"); - - self.rt - .handle() - .block_on(async { - if flag_enabled { - pigeon::enable_lightning_addresses( - &self.config.remote_services_config.backend_url, - &self.async_auth, - addresses.clone(), - ) - .await - } else { - pigeon::disable_lightning_addresses( - &self.config.remote_services_config.backend_url, - &self.async_auth, - addresses.clone(), - ) - .await - } - }) - .map_to_runtime_error( - RuntimeErrorCode::AuthServiceUnavailable, - "Failed to enable/disable a lightning address", - )?; - let mut data_store = self.data_store.lock_unwrap(); - addresses - .into_iter() - .try_for_each(|a| data_store.update_lightning_address(&a, to_status)) + self.config.set_feature_flag(feature, flag_enabled) } fn get_node_utxos(&self) -> Result> { @@ -2465,7 +2439,7 @@ impl LightningNode { } pub(crate) async fn start_sdk( - config: &Config, + config: &LightningNodeConfig, event_listener: Box, ) -> Result> { let developer_cert = config @@ -2672,7 +2646,7 @@ pub(crate) fn register_webhook_url( rt: &AsyncRuntime, sdk: &BreezServices, auth: &Auth, - config: &Config, + config: &LightningNodeConfig, ) -> Result<()> { let id = auth.get_wallet_pubkey_id().map_to_runtime_error( RuntimeErrorCode::AuthServiceUnavailable, @@ -2712,7 +2686,7 @@ include!(concat!(env!("OUT_DIR"), "/lipalightninglib.uniffi.rs")); #[cfg(test)] mod tests { use super::*; - use crate::config::WithTimezone; + use crate::node_config::WithTimezone; use crow::TopupStatus; use perro::Error; use std::time::SystemTime; diff --git a/src/lightning/bolt11.rs b/src/lightning/bolt11.rs index 65f2cd0f..f51e1aab 100644 --- a/src/lightning/bolt11.rs +++ b/src/lightning/bolt11.rs @@ -1,48 +1,24 @@ use crate::amount::AsSats; -use crate::analytics::AnalyticsInterceptor; -use crate::async_runtime::Handle; -use crate::data_store::DataStore; use crate::errors::map_send_payment_error; -use crate::lightning::Payments; +use crate::lightning::{report_send_payment_issue, store_payment_info}; use crate::locker::Locker; use crate::support::Support; -use crate::util::LogIgnoreError; use crate::{ - InvoiceCreationMetadata, InvoiceDetails, OfferKind, PayErrorCode, PayResult, PaymentMetadata, - RuntimeErrorCode, UserPreferences, + InvoiceCreationMetadata, InvoiceDetails, PayErrorCode, PayResult, PaymentMetadata, + RuntimeErrorCode, }; use breez_sdk_core::error::SendPaymentError; -use breez_sdk_core::{BreezServices, OpeningFeeParams, SendPaymentRequest}; -use log::Level; +use breez_sdk_core::{OpeningFeeParams, SendPaymentRequest}; use perro::{ensure, runtime_error, MapToError}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; pub struct Bolt11 { - rt_handle: Handle, - sdk: Arc, - data_store: Arc>, - analytics_interceptor: Arc, - user_preferences: Arc>, support: Arc, } impl Bolt11 { - pub(crate) fn new( - rt_handle: Handle, - sdk: Arc, - data_store: Arc>, - analytics_interceptor: Arc, - user_preferences: Arc>, - support: Arc, - ) -> Self { - Self { - rt_handle, - sdk, - data_store, - analytics_interceptor, - user_preferences, - support, - } + pub(crate) fn new(support: Arc) -> Self { + Self { support } } /// Create a bolt11 invoice to receive a payment with. @@ -66,9 +42,12 @@ impl Bolt11 { metadata: InvoiceCreationMetadata, ) -> crate::Result { let response = self - .rt_handle + .support + .rt + .handle() .block_on( - self.sdk + self.support + .sdk .receive_payment(breez_sdk_core::ReceivePaymentRequest { amount_msat: amount_sat.as_sats().msats, description, @@ -84,8 +63,9 @@ impl Bolt11 { "Failed to create an invoice", )?; - self.store_payment_info(&response.ln_invoice.payment_hash, None); - self.data_store + store_payment_info(&self.support, &response.ln_invoice.payment_hash, None); + self.support + .data_store .lock_unwrap() .store_created_invoice( &response.ln_invoice.payment_hash, @@ -95,7 +75,7 @@ impl Bolt11 { ) .map_to_permanent_failure("Failed to persist created invoice")?; - self.analytics_interceptor.request_initiated( + self.support.analytics_interceptor.request_initiated( response.clone(), self.support.get_exchange_rate(), metadata, @@ -140,8 +120,9 @@ impl Bolt11 { } else { Some(amount_sat.as_sats().msats) }; - self.store_payment_info(&invoice_details.payment_hash, None); + store_payment_info(&self.support, &invoice_details.payment_hash, None); let node_state = self + .support .sdk .node_info() .map_to_runtime_error(PayErrorCode::NodeUnavailable, "Failed to read node info")?; @@ -153,7 +134,7 @@ impl Bolt11 { ) ); - self.analytics_interceptor.pay_initiated( + self.support.analytics_interceptor.pay_initiated( invoice_details.clone(), metadata, amount_msat, @@ -161,8 +142,10 @@ impl Bolt11 { ); let result = self - .rt_handle - .block_on(self.sdk.send_payment(SendPaymentRequest { + .support + .rt + .handle() + .block_on(self.support.sdk.send_payment(SendPaymentRequest { bolt11: invoice_details.invoice, use_trampoline: true, amount_msat, @@ -178,41 +161,10 @@ impl Bolt11 { | SendPaymentError::RouteTooExpensive { .. } | SendPaymentError::ServiceConnectivity { .. }) ) { - self.report_send_payment_issue(invoice_details.payment_hash); + report_send_payment_issue(&self.support, invoice_details.payment_hash); } result.map_err(map_send_payment_error)?; Ok(()) } - - fn store_payment_info(&self, hash: &str, offer: Option) { - let user_preferences = self.user_preferences.lock_unwrap().clone(); - let exchange_rates = self.support.get_exchange_rates(); - self.data_store - .lock_unwrap() - .store_payment_info(hash, user_preferences, exchange_rates, offer, None, None) - .log_ignore_error(Level::Error, "Failed to persist payment info") - } -} - -impl Payments for Bolt11 { - fn handle(&self) -> &Handle { - &self.rt_handle - } - - fn sdk(&self) -> &BreezServices { - &self.sdk - } - - fn user_preferences(&self) -> &Mutex { - &self.user_preferences - } - - fn data_store(&self) -> &Mutex { - &self.data_store - } - - fn support(&self) -> &Support { - &self.support - } } diff --git a/src/lightning/lnurl.rs b/src/lightning/lnurl.rs index 7f664aa4..b00c4e6d 100644 --- a/src/lightning/lnurl.rs +++ b/src/lightning/lnurl.rs @@ -1,45 +1,25 @@ use crate::amount::{AsSats, ToAmount}; -use crate::async_runtime::Handle; -use crate::data_store::DataStore; use crate::errors::{ map_lnurl_pay_error, map_lnurl_withdraw_error, LnUrlWithdrawErrorCode, LnUrlWithdrawResult, }; -use crate::lightning::Payments; +use crate::lightning::{report_send_payment_issue, store_payment_info}; use crate::support::Support; -use crate::{ - Amount, DecodeDataError, ExchangeRate, LnUrlPayErrorCode, LnUrlPayResult, UserPreferences, -}; +use crate::{Amount, DecodeDataError, ExchangeRate, LnUrlPayErrorCode, LnUrlPayResult}; use breez_sdk_core::{ - BreezServices, LnUrlPayRequest, LnUrlPayRequestData, LnUrlWithdrawRequest, - LnUrlWithdrawRequestData, MetadataItem, + LnUrlPayRequest, LnUrlPayRequestData, LnUrlWithdrawRequest, LnUrlWithdrawRequestData, + MetadataItem, }; use log::warn; use perro::{ensure, invalid_input, runtime_error}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; pub struct Lnurl { - rt_handle: Handle, - sdk: Arc, - data_store: Arc>, - user_preferences: Arc>, support: Arc, } impl Lnurl { - pub(crate) fn new( - rt_handle: Handle, - sdk: Arc, - data_store: Arc>, - user_preferences: Arc>, - support: Arc, - ) -> Self { - Self { - rt_handle, - sdk, - data_store, - user_preferences, - support, - } + pub(crate) fn new(support: Arc) -> Self { + Self { support } } /// Pay an LNURL-pay the provided amount. @@ -69,8 +49,10 @@ impl Lnurl { ); let payment_hash = match self - .rt_handle - .block_on(self.sdk.lnurl_pay(LnUrlPayRequest { + .support + .rt + .handle() + .block_on(self.support.sdk.lnurl_pay(LnUrlPayRequest { data: lnurl_pay_request_data, amount_msat: amount_sat.as_sats().msats, use_trampoline: true, @@ -89,7 +71,7 @@ impl Lnurl { data.reason ), breez_sdk_core::lnurl::pay::LnUrlPayResult::PayError { data } => { - self.report_send_payment_issue(data.payment_hash); + report_send_payment_issue(&self.support, data.payment_hash); runtime_error!( LnUrlPayErrorCode::PaymentFailed, "Paying invoice for LNURL pay failed: {}", @@ -97,7 +79,7 @@ impl Lnurl { ) } }?; - self.store_payment_info(&payment_hash, None); + store_payment_info(&self.support, &payment_hash, None); Ok(payment_hash) } @@ -120,8 +102,10 @@ impl Lnurl { amount_sat: u64, ) -> LnUrlWithdrawResult { let payment_hash = match self - .rt_handle - .block_on(self.sdk.lnurl_withdraw(LnUrlWithdrawRequest { + .support + .rt + .handle() + .block_on(self.support.sdk.lnurl_withdraw(LnUrlWithdrawRequest { data: lnurl_withdraw_request_data, amount_msat: amount_sat.as_sats().msats, description: None, @@ -139,33 +123,11 @@ impl Lnurl { data.reason ), }?; - self.store_payment_info(&payment_hash, None); + store_payment_info(&self.support, &payment_hash, None); Ok(payment_hash) } } -impl Payments for Lnurl { - fn handle(&self) -> &Handle { - &self.rt_handle - } - - fn sdk(&self) -> &BreezServices { - &self.sdk - } - - fn user_preferences(&self) -> &Mutex { - &self.user_preferences - } - - fn data_store(&self) -> &Mutex { - &self.data_store - } - - fn support(&self) -> &Support { - &self.support - } -} - /// Information about an LNURL-pay. pub struct LnUrlPayDetails { /// The domain of the LNURL-pay service, to be shown to the user when asking for diff --git a/src/lightning/mod.rs b/src/lightning/mod.rs index b1118b5f..2bd6fb24 100644 --- a/src/lightning/mod.rs +++ b/src/lightning/mod.rs @@ -3,27 +3,21 @@ pub mod lnurl; pub mod receive_limits; use crate::amount::{AsSats, Permyriad, ToAmount}; -use crate::analytics::AnalyticsInterceptor; -use crate::async_runtime::Handle; -use crate::data_store::DataStore; use crate::errors::Result; use crate::lightning::bolt11::Bolt11; use crate::lightning::lnurl::Lnurl; use crate::lightning::receive_limits::ReceiveAmountLimits; use crate::locker::Locker; use crate::support::Support; -use crate::task_manager::TaskManager; use crate::util::LogIgnoreError; use crate::{ - CalculateLspFeeResponse, Config, ExchangeRate, LspFee, MaxRoutingFeeConfig, MaxRoutingFeeMode, - OfferKind, RuntimeErrorCode, UserPreferences, -}; -use breez_sdk_core::{ - BreezServices, OpenChannelFeeRequest, ReportIssueRequest, ReportPaymentFailureDetails, + CalculateLspFeeResponse, ExchangeRate, LspFee, MaxRoutingFeeConfig, MaxRoutingFeeMode, + OfferKind, RuntimeErrorCode, }; +use breez_sdk_core::{OpenChannelFeeRequest, ReportIssueRequest, ReportPaymentFailureDetails}; use log::{debug, Level}; use perro::{MapToError, OptionToError}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; /// Payment affordability returned by [`Lightning::determine_payment_affordability`]. #[derive(Debug)] @@ -38,50 +32,20 @@ pub enum PaymentAffordability { } pub struct Lightning { - rt_handle: Handle, - sdk: Arc, bolt11: Arc, lnurl: Arc, - config: Config, support: Arc, - task_manager: Arc>, } impl Lightning { #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - rt_handle: Handle, - sdk: Arc, - data_store: Arc>, - analytics_interceptor: Arc, - user_preferences: Arc>, - support: Arc, - config: Config, - task_manager: Arc>, - ) -> Self { - let bolt11 = Arc::new(Bolt11::new( - rt_handle.clone(), - Arc::clone(&sdk), - Arc::clone(&data_store), - analytics_interceptor, - Arc::clone(&user_preferences), - Arc::clone(&support), - )); - let lnurl = Arc::new(Lnurl::new( - rt_handle.clone(), - Arc::clone(&sdk), - data_store, - user_preferences, - Arc::clone(&support), - )); + pub(crate) fn new(support: Arc) -> Self { + let bolt11 = Arc::new(Bolt11::new(Arc::clone(&support))); + let lnurl = Arc::new(Lnurl::new(Arc::clone(&support))); Self { - rt_handle, - sdk, bolt11, lnurl, - config, support, - task_manager, } } @@ -99,7 +63,7 @@ impl Lightning { /// Requires network: **no** pub fn determine_max_routing_fee_mode(&self, amount_sat: u64) -> MaxRoutingFeeMode { get_payment_max_routing_fee_mode( - &self.config.max_routing_fee_config, + &self.support.node_config.max_routing_fee_config, amount_sat, &self.support.get_exchange_rate(), ) @@ -126,7 +90,7 @@ impl Lightning { MaxRoutingFeeMode::Absolute { max_fee_amount } => max_fee_amount.sats.as_sats().msats, }; - let node_state = self.sdk.node_info().map_to_runtime_error( + let node_state = self.support.sdk.node_info().map_to_runtime_error( RuntimeErrorCode::NodeUnavailable, "Failed to read node info", )?; @@ -162,7 +126,7 @@ impl Lightning { max_inbound_amount.sats, lsp_min_fee_amount.sats, &self.support.get_exchange_rate(), - &self.config.receive_limits_config, + &self.support.node_config.receive_limits_config, )) } @@ -182,8 +146,10 @@ impl Lightning { expiry: None, }; let res = self - .rt_handle - .block_on(self.sdk.open_channel_fee(req)) + .support + .rt + .handle() + .block_on(self.support.sdk.open_channel_fee(req)) .map_to_runtime_error( RuntimeErrorCode::NodeUnavailable, "Failed to compute opening channel fee", @@ -205,7 +171,7 @@ impl Lightning { /// Requires network: **no** pub fn get_lsp_fee(&self) -> Result { let exchange_rate = self.support.get_exchange_rate(); - let lsp_fee = self.task_manager.lock_unwrap().get_lsp_fee()?; + let lsp_fee = self.support.task_manager.lock_unwrap().get_lsp_fee()?; Ok(LspFee { channel_minimum_fee: lsp_fee.min_msat.as_msats().to_amount_up(&exchange_rate), channel_fee_permyriad: lsp_fee.proportional as u64 / 100, @@ -234,33 +200,28 @@ fn get_payment_max_routing_fee_mode( } } -trait Payments { - fn handle(&self) -> &Handle; - fn sdk(&self) -> &BreezServices; - fn user_preferences(&self) -> &Mutex; - fn data_store(&self) -> &Mutex; - fn support(&self) -> &Support; - - fn report_send_payment_issue(&self, payment_hash: String) { - debug!("Reporting failure of payment: {payment_hash}"); - let data = ReportPaymentFailureDetails { - payment_hash, - comment: None, - }; - let request = ReportIssueRequest::PaymentFailure { data }; - self.handle() - .block_on(self.sdk().report_issue(request)) - .log_ignore_error(Level::Warn, "Failed to report issue"); - } +fn report_send_payment_issue(support: &Support, payment_hash: String) { + debug!("Reporting failure of payment: {payment_hash}"); + let data = ReportPaymentFailureDetails { + payment_hash, + comment: None, + }; + let request = ReportIssueRequest::PaymentFailure { data }; + support + .rt + .handle() + .block_on(support.sdk.report_issue(request)) + .log_ignore_error(Level::Warn, "Failed to report issue"); +} - fn store_payment_info(&self, hash: &str, offer: Option) { - let user_preferences = self.user_preferences().lock_unwrap().clone(); - let exchange_rates = self.support().get_exchange_rates(); - self.data_store() - .lock_unwrap() - .store_payment_info(hash, user_preferences, exchange_rates, offer, None, None) - .log_ignore_error(Level::Error, "Failed to persist payment info") - } +fn store_payment_info(support: &Support, hash: &str, offer: Option) { + let user_preferences = support.user_preferences.lock_unwrap().clone(); + let exchange_rates = support.get_exchange_rates(); + support + .data_store + .lock_unwrap() + .store_payment_info(hash, user_preferences, exchange_rates, offer, None, None) + .log_ignore_error(Level::Error, "Failed to persist payment info") } #[cfg(test)] diff --git a/src/lightning/receive_limits.rs b/src/lightning/receive_limits.rs index ca5f7d8d..84904600 100644 --- a/src/lightning/receive_limits.rs +++ b/src/lightning/receive_limits.rs @@ -1,5 +1,5 @@ use crate::amount::{AsSats, ToAmount}; -use crate::config::ReceiveLimitsConfig; +use crate::node_config::ReceiveLimitsConfig; use crate::{Amount, ExchangeRate}; /// Information on the limits imposed on the next receiving payment diff --git a/src/limits.rs b/src/limits.rs index 5746843c..533d94a4 100644 --- a/src/limits.rs +++ b/src/limits.rs @@ -1,5 +1,5 @@ -use crate::config::ReceiveLimitsConfig; use crate::lightning::receive_limits::{LiquidityLimit, ReceiveAmountLimits}; +use crate::node_config::ReceiveLimitsConfig; use crate::{Amount, ExchangeRate}; /// Information on the limits imposed on the next receiving payment diff --git a/src/lipalightninglib.udl b/src/lipalightninglib.udl index 05b0af96..07f54063 100644 --- a/src/lipalightninglib.udl +++ b/src/lipalightninglib.udl @@ -4,7 +4,7 @@ interface LightningNode { [Throws=LnError] - constructor(Config config, EventsCallback events_callback); + constructor(LightningNodeConfig config, EventsCallback events_callback); [Throws=LnError] NodeInfo get_node_info(); @@ -198,6 +198,33 @@ interface LightningNode { Activities activities(); Lightning lightning(); + + Config config(); +}; + +interface Config { + [Throws=LnError] + void set_fiat_currency(string fiat_currency); + + void set_timezone_config(TzConfig timezone_config); + + [Throws=LnError] + void set_analytics_config(AnalyticsConfig config); + + [Throws=LnError] + AnalyticsConfig get_analytics_config(); + + [Throws=LnError] + void register_notification_token(string notification_token, string language_iso_639_1, string country_iso_3166_1_alpha_2); + + [Throws=LnError] + void set_feature_flag(FeatureFlag feature, boolean flag_enabled); + + sequence list_currencies(); + + void foreground(); + + void background(); }; interface Lightning { @@ -258,7 +285,7 @@ interface Activities { void set_personal_note(string payment_hash, string note); }; -dictionary Config { +dictionary LightningNodeConfig { bytes seed; string default_fiat_currency; string local_persistence_path; @@ -794,7 +821,7 @@ namespace lipalightninglib { void parse_lightning_address([ByRef] string address); [Throws=NotificationHandlingError] - Notification handle_notification(Config config, string notification_payload, NotificationToggles notification_toggles, duration timeout); + Notification handle_notification(LightningNodeConfig config, string notification_payload, NotificationToggles notification_toggles, duration timeout); }; dictionary Secret { diff --git a/src/node_config.rs b/src/node_config.rs new file mode 100644 index 00000000..66cabe21 --- /dev/null +++ b/src/node_config.rs @@ -0,0 +1,98 @@ +use std::time::SystemTime; + +/// An object that holds all configuration needed to start a LightningNode instance. +#[derive(Debug, Clone)] +pub struct LightningNodeConfig { + /// The seed derived from the mnemonic optionally including a pass phrase + pub seed: Vec, + /// ISO 4217 currency code. The backend does not support all of them, but supports at least USD + /// and EUR, so it is safe to default to one of them. Providing an invalid code will result in + /// missing fiat values for payments. + /// + /// The provided value is used as a default. After the first time the node is started, + /// this config starts being ignored. Changing the fiat currency can be done using + /// [`crate::LightningNode::change_fiat_currency`]. + pub default_fiat_currency: String, + /// A path on the local filesystem where this library will directly persist data. Only the + /// current instance of the app should have access to the provided directory. On app + /// uninstall/deletion, the directory should be purged. + pub local_persistence_path: String, + /// A timezone configuration object. + pub timezone_config: TzConfig, + /// If a value is provided, logs using the provided level will be created in the provided + /// `local_persistence_path`. + pub file_logging_level: Option, + /// The list of allowed countries for the use of phone numbers as identifiers. + pub phone_number_allowed_countries_iso_3166_1_alpha_2: Vec, + pub remote_services_config: RemoteServicesConfig, + pub breez_sdk_config: BreezSdkConfig, + pub max_routing_fee_config: MaxRoutingFeeConfig, + pub receive_limits_config: ReceiveLimitsConfig, +} + +#[derive(Debug, Clone)] +pub struct RemoteServicesConfig { + /// lipa's backend URL. + pub backend_url: String, + /// Pocket's backend URL. + pub pocket_url: String, + /// Base URL used to construct the webhook URL used for notifications. + pub notification_webhook_base_url: String, + /// Secret used to encrypt the wallet's ID before being added to the webhook URL. + pub notification_webhook_secret_hex: String, + /// The domain used in lipa Lightning Addresses. + pub lipa_lightning_domain: String, +} + +#[derive(Debug, Clone)] +pub struct MaxRoutingFeeConfig { + /// Routing fees will be limited to relative per myriad provided here. + pub max_routing_fee_permyriad: u16, + /// When the fee is lower or equal to this value, the relative limit is ignored. + pub max_routing_fee_exempt_fee_sats: u64, +} + +#[derive(Debug, Clone)] +pub struct BreezSdkConfig { + pub breez_sdk_api_key: String, + pub breez_sdk_partner_certificate: String, + pub breez_sdk_partner_key: String, +} + +#[derive(Debug, Clone)] +pub struct ReceiveLimitsConfig { + pub max_receive_amount_sat: u64, + pub min_receive_channel_open_fee_multiplier: f64, +} + +/// An object that holds timezone configuration values necessary for 3L to do timestamp annotation. These values get tied +/// together with every timestamp persisted in the local payment database. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct TzConfig { + /// String identifier whose format is completely arbitrary and can be chosen by the user + pub timezone_id: String, + /// Offset from the UTC timezone in seconds + pub timezone_utc_offset_secs: i32, +} + +/// A UTC timestamp accompanied by the ID of the timezone on which it was recorded and the respective UTC offset. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct TzTime { + pub time: SystemTime, + pub timezone_id: String, + pub timezone_utc_offset_secs: i32, +} + +pub(crate) trait WithTimezone { + fn with_timezone(self, tz_config: TzConfig) -> TzTime; +} + +impl WithTimezone for SystemTime { + fn with_timezone(self, tz_config: TzConfig) -> TzTime { + TzTime { + time: self, + timezone_id: tz_config.timezone_id, + timezone_utc_offset_secs: tz_config.timezone_utc_offset_secs, + } + } +} diff --git a/src/notification_handling.rs b/src/notification_handling.rs index aecbec4d..f4ac0a2f 100644 --- a/src/notification_handling.rs +++ b/src/notification_handling.rs @@ -9,8 +9,8 @@ use crate::exchange_rate_provider::{ExchangeRateProvider, ExchangeRateProviderIm use crate::logger::init_logger_once; use crate::util::LogIgnoreError; use crate::{ - enable_backtrace, register_webhook_url, sanitize_input, start_sdk, Config, EnableStatus, - RuntimeErrorCode, UserPreferences, DB_FILENAME, LOGS_DIR, + enable_backtrace, register_webhook_url, sanitize_input, start_sdk, EnableStatus, + LightningNodeConfig, RuntimeErrorCode, UserPreferences, DB_FILENAME, LOGS_DIR, }; use breez_sdk_core::{ BreezEvent, BreezServices, EventListener, InvoicePaidDetails, OpenChannelFeeRequest, Payment, @@ -75,7 +75,7 @@ pub struct NotificationToggles { /// /// Requires network: **yes** pub fn handle_notification( - config: Config, + config: LightningNodeConfig, notification_payload: String, notification_toggles: NotificationToggles, timeout: Duration, @@ -151,7 +151,7 @@ pub fn handle_notification( } fn build_analytics_interceptor( - config: &Config, + config: &LightningNodeConfig, rt: &AsyncRuntime, ) -> NotificationHandlingResult { let db_path = format!("{}/{DB_FILENAME}", config.local_persistence_path); @@ -297,7 +297,7 @@ fn handle_address_txs_confirmed_notification( fn handle_lnurl_pay_request_notification( rt: AsyncRuntime, sdk: Arc, - config: Config, + config: LightningNodeConfig, data: LnurlPayRequestData, ) -> NotificationHandlingResult { // Prevent payments that need a new channel from being received @@ -529,7 +529,7 @@ fn wait_for_synced_event(event_receiver: &Receiver) -> NotificationH } } -fn get_strong_typed_seed(config: &Config) -> NotificationHandlingResult<[u8; 64]> { +fn get_strong_typed_seed(config: &LightningNodeConfig) -> NotificationHandlingResult<[u8; 64]> { sanitize_input::strong_type_seed(&config.seed) .map_runtime_error_using(NotificationHandlingErrorCode::from_runtime_error) } diff --git a/src/payment.rs b/src/payment.rs index af373541..345137a8 100644 --- a/src/payment.rs +++ b/src/payment.rs @@ -1,8 +1,8 @@ use std::ops::Add; use crate::amount::{AsSats, ToAmount}; -use crate::config::WithTimezone; use crate::lightning::lnurl::parse_metadata; +use crate::node_config::WithTimezone; use crate::phone_number::lightning_address_to_phone_number; use crate::util::unix_timestamp_to_system_time; use crate::{Amount, ExchangeRate, InvoiceDetails, Result, TzConfig, TzTime}; diff --git a/src/phone_number.rs b/src/phone_number.rs index 3320b35c..e475283c 100644 --- a/src/phone_number.rs +++ b/src/phone_number.rs @@ -46,6 +46,7 @@ pub(crate) fn lightning_address_to_phone_number(address: &str, domain: &str) -> None } +#[derive(Clone)] pub(crate) struct PhoneNumberPrefixParser { allowed_country_codes: Vec, } diff --git a/src/support.rs b/src/support.rs index 02fef6d2..6fa0a918 100644 --- a/src/support.rs +++ b/src/support.rs @@ -1,31 +1,40 @@ use crate::amount::{AsSats, ToAmount}; +use crate::analytics::AnalyticsInterceptor; +use crate::async_runtime::AsyncRuntime; +use crate::data_store::DataStore; use crate::errors::Result; use crate::locker::Locker; +use crate::phone_number::PhoneNumberPrefixParser; +use crate::pocketclient::PocketClient; use crate::task_manager::TaskManager; -use crate::{ChannelsInfo, ExchangeRate, NodeInfo, RuntimeErrorCode, UserPreferences}; +use crate::{ + ChannelsInfo, ExchangeRate, LightningNodeConfig, NodeInfo, RuntimeErrorCode, UserPreferences, +}; use breez_sdk_core::BreezServices; +use crow::OfferManager; +use honeybadger::Auth; use perro::MapToError; use std::sync::{Arc, Mutex}; +#[allow(dead_code)] pub(crate) struct Support { - user_preferences: Arc>, - task_manager: Arc>, - sdk: Arc, + pub user_preferences: Arc>, + pub sdk: Arc, + pub auth: Arc, + pub async_auth: Arc, + pub fiat_topup_client: Arc, + pub offer_manager: Arc, + pub rt: Arc, + pub data_store: Arc>, + pub task_manager: Arc>, + pub allowed_countries_country_iso_3166_1_alpha_2: Vec, + pub phone_number_prefix_parser: PhoneNumberPrefixParser, + pub persistence_encryption_key: [u8; 32], + pub node_config: LightningNodeConfig, + pub analytics_interceptor: Arc, } impl Support { - pub fn new( - user_preferences: Arc>, - task_manager: Arc>, - sdk: Arc, - ) -> Self { - Self { - user_preferences, - task_manager, - sdk, - } - } - /// Get exchange rate on the BTC/default currency pair /// Please keep in mind that this method doesn't make any network calls. It simply retrieves /// previously fetched values that are frequently updated by a background task. diff --git a/src/swap.rs b/src/swap.rs index 833d4289..e68e267b 100644 --- a/src/swap.rs +++ b/src/swap.rs @@ -1,5 +1,5 @@ use crate::amount::Amount; -use crate::config::TzTime; +use crate::node_config::TzTime; use breez_sdk_core::OpeningFeeParams; use std::time::SystemTime; diff --git a/tests/register_node_test.rs b/tests/register_node_test.rs index 35d1d0de..937d3143 100644 --- a/tests/register_node_test.rs +++ b/tests/register_node_test.rs @@ -3,7 +3,9 @@ mod setup; use crate::print_events_handler::PrintEventsHandler; -use uniffi_lipalightninglib::{generate_secret, Config, ReceiveLimitsConfig, TzConfig}; +use uniffi_lipalightninglib::{ + generate_secret, LightningNodeConfig, ReceiveLimitsConfig, TzConfig, +}; use uniffi_lipalightninglib::{ BreezSdkConfig, LightningNode, MaxRoutingFeeConfig, RemoteServicesConfig, }; @@ -27,7 +29,7 @@ fn test_register_node() { let mnemonic = secret.mnemonic.join(" "); println!("Mnemonic: {mnemonic}"); - let config = Config { + let config = LightningNodeConfig { seed: secret.seed, default_fiat_currency: "EUR".to_string(), local_persistence_path: LOCAL_PERSISTENCE_PATH.to_string(), diff --git a/tests/setup/mod.rs b/tests/setup/mod.rs index d840bae5..5e5b3df5 100644 --- a/tests/setup/mod.rs +++ b/tests/setup/mod.rs @@ -1,8 +1,9 @@ use crate::print_events_handler::PrintEventsHandler; use uniffi_lipalightninglib::{ - mnemonic_to_secret, AnalyticsConfig, BreezSdkConfig, Config, EventsCallback, LightningNode, - MaxRoutingFeeConfig, ReceiveLimitsConfig, RemoteServicesConfig, RuntimeErrorCode, TzConfig, + mnemonic_to_secret, AnalyticsConfig, BreezSdkConfig, EventsCallback, LightningNode, + LightningNodeConfig, MaxRoutingFeeConfig, ReceiveLimitsConfig, RemoteServicesConfig, + RuntimeErrorCode, TzConfig, }; use log::Level; @@ -98,7 +99,7 @@ pub fn start_specific_node( let mnemonic = mnemonic.split_whitespace().map(String::from).collect(); let seed = mnemonic_to_secret(mnemonic, "".to_string()).unwrap().seed; - let config = Config { + let config = LightningNodeConfig { seed, default_fiat_currency: "EUR".to_string(), local_persistence_path, @@ -146,7 +147,8 @@ pub fn start_specific_node( }; let node = LightningNode::new(config, events_callback)?; - node.set_analytics_config(AnalyticsConfig::Disabled)?; // tests produce misleading noise + node.config() + .set_analytics_config(AnalyticsConfig::Disabled)?; // tests produce misleading noise Ok(node) }