From 412030e5052dce70fb262dbd6287f6b7000e7afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Tue, 29 Oct 2024 17:02:58 +0000 Subject: [PATCH] Change fiat currency config to a default --- examples/node/cli.rs | 22 +++++------ examples/node/main.rs | 2 +- examples/notification_handler/main.rs | 2 +- src/config.rs | 6 ++- src/data_store.rs | 54 ++++++++++++++++++++++++++- src/lib.rs | 24 +++++++++--- src/lipalightninglib.udl | 3 +- src/migrations.rs | 9 +++++ src/notification_handling.rs | 23 +++++++++--- tests/register_node_test.rs | 2 +- tests/setup/mod.rs | 2 +- 11 files changed, 119 insertions(+), 30 deletions(-) diff --git a/examples/node/cli.rs b/examples/node/cli.rs index cba9f18f..4573e1f2 100644 --- a/examples/node/cli.rs +++ b/examples/node/cli.rs @@ -77,17 +77,9 @@ pub(crate) fn poll_for_user_input(node: &LightningNode, log_file_path: &str) { list_currency_codes(node); } "changecurrency" => { - match words - .next() - .ok_or_else(|| "Error: fiat currency code is required".to_string()) - { - Ok(c) => { - change_currency(node, c); - } - Err(e) => { - println!("{}", e.red()); - } - }; + if let Err(message) = change_currency(node, &mut words) { + println!("{}", format!("{message:#}").red()); + } } "changetimezone" => { if let Err(message) = change_timezone(node, &mut words) { @@ -706,8 +698,12 @@ fn list_currency_codes(node: &LightningNode) { println!("Supported currencies: {codes:?}"); } -fn change_currency(node: &LightningNode, fiat_currency: &str) { - node.change_fiat_currency(String::from(fiat_currency)); +fn change_currency(node: &LightningNode, words: &mut dyn Iterator) -> Result<()> { + let fiat_currency = words + .next() + .ok_or(anyhow!("Fiat currency code is required"))?; + node.change_fiat_currency(String::from(fiat_currency))?; + Ok(()) } fn change_timezone(node: &LightningNode, words: &mut dyn Iterator) -> Result<()> { diff --git a/examples/node/main.rs b/examples/node/main.rs index fcc5c297..ba06e7c2 100644 --- a/examples/node/main.rs +++ b/examples/node/main.rs @@ -55,7 +55,7 @@ fn main() { let config = Config { seed, - fiat_currency: "EUR".to_string(), + default_fiat_currency: "EUR".to_string(), local_persistence_path: base_dir.clone(), timezone_config: TzConfig { timezone_id: String::from("Africa/Tunis"), diff --git a/examples/notification_handler/main.rs b/examples/notification_handler/main.rs index c5e7aeb3..2a0c0016 100644 --- a/examples/notification_handler/main.rs +++ b/examples/notification_handler/main.rs @@ -122,7 +122,7 @@ fn get_config() -> Config { Config { seed, - fiat_currency: "EUR".to_string(), + default_fiat_currency: "EUR".to_string(), local_persistence_path: base_dir.clone(), timezone_config: TzConfig { timezone_id: String::from("Africa/Tunis"), diff --git a/src/config.rs b/src/config.rs index ba910440..2337a128 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,11 @@ pub struct Config { /// 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. - pub fiat_currency: String, + /// + /// 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. diff --git a/src/data_store.rs b/src/data_store.rs index 4ece8938..b581360c 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -465,7 +465,7 @@ impl DataStore { Ok(()) } - pub fn retrieve_hidden_unresolved_failed_swaps(&mut self) -> Result> { + pub fn retrieve_hidden_unresolved_failed_swaps(&self) -> Result> { Ok(self .conn .prepare("SELECT swap_address FROM hidden_failed_swaps") @@ -475,6 +475,28 @@ impl DataStore { .filter_map(|r| r.ok()) .collect::>()) } + + pub fn store_selected_fiat_currency(&mut self, fiat_currency: &str) -> Result<()> { + self.backup_status = BackupStatus::WaitingForBackup; + self.conn + .execute( + "INSERT INTO fiat_currency (fiat_currency) VALUES (?1)", + params![fiat_currency], + ) + .map_to_permanent_failure("Failed to store fiat currency in db")?; + Ok(()) + } + + pub fn retrieve_last_set_fiat_currency(&self) -> Result> { + self.conn + .query_row( + "SELECT fiat_currency FROM fiat_currency ORDER BY id DESC LIMIT 1", + (), + |r| r.get(0), + ) + .optional() + .map_to_permanent_failure("Failed to query last hidden channel close amount") + } } fn lightning_address_from_row(row: &Row) -> rusqlite::Result<(String, EnableStatus)> { @@ -1417,6 +1439,36 @@ mod tests { ); } + #[test] + fn test_storing_fiat_currency() { + let db_name = String::from("fiat_currency.db3"); + reset_db(&db_name); + let mut data_store = DataStore::new(&format!("{TEST_DB_PATH}/{db_name}")).unwrap(); + + assert!(data_store + .retrieve_last_set_fiat_currency() + .unwrap() + .is_none()); + + data_store.store_selected_fiat_currency("EUR").unwrap(); + assert_eq!( + data_store + .retrieve_last_set_fiat_currency() + .unwrap() + .unwrap(), + "EUR" + ); + + data_store.store_selected_fiat_currency("CHF").unwrap(); + assert_eq!( + data_store + .retrieve_last_set_fiat_currency() + .unwrap() + .unwrap(), + "CHF" + ); + } + fn reset_db(db_name: &str) { let _ = fs::create_dir(TEST_DB_PATH); let _ = fs::remove_file(format!("{TEST_DB_PATH}/{db_name}")); diff --git a/src/lib.rs b/src/lib.rs index de26b300..357eb0e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -384,8 +384,21 @@ impl LightningNode { &config.remote_services_config.backend_url, )?); + let db_path = format!("{}/{DB_FILENAME}", 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() + } + Some(c) => c, + }; + + let data_store = Arc::new(Mutex::new(data_store)); + let user_preferences = Arc::new(Mutex::new(UserPreferences { - fiat_currency: config.fiat_currency.clone(), + fiat_currency, timezone_config: config.timezone_config.clone(), })); @@ -395,9 +408,6 @@ impl LightningNode { Arc::clone(&async_auth), ); - let db_path = format!("{}/{DB_FILENAME}", config.local_persistence_path); - let data_store = Arc::new(Mutex::new(DataStore::new(&db_path)?)); - let analytics_config = data_store.lock_unwrap().retrieve_analytics_config()?; let analytics_interceptor = Arc::new(AnalyticsInterceptor::new( analytics_client, @@ -1567,8 +1577,12 @@ impl LightningNode { /// The method [`LightningNode::list_currency_codes`] can used to list supported codes. /// /// Requires network: **no** - pub fn change_fiat_currency(&self, fiat_currency: String) { + 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(()) } /// Change the timezone config. diff --git a/src/lipalightninglib.udl b/src/lipalightninglib.udl index 1154f91c..b8b022ce 100644 --- a/src/lipalightninglib.udl +++ b/src/lipalightninglib.udl @@ -70,6 +70,7 @@ interface LightningNode { ExchangeRate? get_exchange_rate(); + [Throws=LnError] void change_fiat_currency(string fiat_currency); void change_timezone_config(TzConfig timezone_config); @@ -194,7 +195,7 @@ interface LightningNode { dictionary Config { bytes seed; - string fiat_currency; + string default_fiat_currency; string local_persistence_path; TzConfig timezone_config; Level? file_logging_level; diff --git a/src/migrations.rs b/src/migrations.rs index 917e70be..eb1f08b4 100644 --- a/src/migrations.rs +++ b/src/migrations.rs @@ -154,6 +154,14 @@ const MIGRATION_17_HIDDEN_FAILED_SWAPS: &str = " ); "; +const MIGRATION_18_FIAT_CURRENCY: &str = " + CREATE TABLE fiat_currency ( + id INTEGER NOT NULL PRIMARY KEY, + fiat_currency TEXT NOT NULL, + inserter_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP + ); +"; + pub(crate) fn migrate(conn: &mut Connection) -> Result<()> { migrations() .to_latest(conn) @@ -179,6 +187,7 @@ fn migrations() -> Migrations<'static> { M::up(MIGRATION_15_LIGHTNING_ADDRESSES_ENABLE_STATUS), M::up(MIGRATION_16_HIDDEN_CHANNEL_CLOSE_AMOUNT), M::up(MIGRATION_17_HIDDEN_FAILED_SWAPS), + M::up(MIGRATION_18_FIAT_CURRENCY), ]) } diff --git a/src/notification_handling.rs b/src/notification_handling.rs index eec03e3e..aecbec4d 100644 --- a/src/notification_handling.rs +++ b/src/notification_handling.rs @@ -154,8 +154,18 @@ fn build_analytics_interceptor( config: &Config, rt: &AsyncRuntime, ) -> NotificationHandlingResult { + let db_path = format!("{}/{DB_FILENAME}", config.local_persistence_path); + let data_store = DataStore::new(&db_path) + .map_runtime_error_using(NotificationHandlingErrorCode::from_runtime_error)?; + + let fiat_currency = data_store + .retrieve_last_set_fiat_currency() + .map_runtime_error_using(NotificationHandlingErrorCode::from_runtime_error)? + .ok_or(permanent_failure( + "No fiat currency set. Node must be started before handling notifications", + ))?; let user_preferences = Arc::new(Mutex::new(UserPreferences { - fiat_currency: config.fiat_currency.clone(), + fiat_currency, timezone_config: config.timezone_config.clone(), })); @@ -175,9 +185,6 @@ fn build_analytics_interceptor( Arc::clone(&async_auth), ); - let db_path = format!("{}/{DB_FILENAME}", config.local_persistence_path); - let data_store = DataStore::new(&db_path) - .map_runtime_error_using(NotificationHandlingErrorCode::from_runtime_error)?; let analytics_config = data_store .retrieve_analytics_config() .map_runtime_error_using(NotificationHandlingErrorCode::from_runtime_error)?; @@ -391,9 +398,15 @@ fn handle_lnurl_pay_request_notification( // Invoice is not persisted in invoices table because we are not interested in unpaid invoices // resulting from incoming LNURL payments + let fiat_currency = data_store + .retrieve_last_set_fiat_currency() + .map_runtime_error_using(NotificationHandlingErrorCode::from_runtime_error)? + .ok_or(permanent_failure( + "No fiat currency set. Node must be started before handling notifications", + ))?; // Store payment info (exchange rates, user preferences, etc...) let user_preferences = UserPreferences { - fiat_currency: config.fiat_currency.clone(), + fiat_currency, timezone_config: config.timezone_config.clone(), }; let exchange_rate_provider = ExchangeRateProviderImpl::new( diff --git a/tests/register_node_test.rs b/tests/register_node_test.rs index bc96ca09..35d1d0de 100644 --- a/tests/register_node_test.rs +++ b/tests/register_node_test.rs @@ -29,7 +29,7 @@ fn test_register_node() { let config = Config { seed: secret.seed, - fiat_currency: "EUR".to_string(), + default_fiat_currency: "EUR".to_string(), local_persistence_path: LOCAL_PERSISTENCE_PATH.to_string(), timezone_config: TzConfig { timezone_id: String::from("int_test_timezone_id"), diff --git a/tests/setup/mod.rs b/tests/setup/mod.rs index 4313d667..d840bae5 100644 --- a/tests/setup/mod.rs +++ b/tests/setup/mod.rs @@ -100,7 +100,7 @@ pub fn start_specific_node( let config = Config { seed, - fiat_currency: "EUR".to_string(), + default_fiat_currency: "EUR".to_string(), local_persistence_path, timezone_config: TzConfig { timezone_id: String::from("int_test_timezone_id"),