diff --git a/.codex b/.codex new file mode 100644 index 00000000..e69de29b diff --git a/.gitignore b/.gitignore index fd5316fb..c1ff907e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ mostro.db* mostro.log lnurl-test-server/target +vendor # IDE's .idea diff --git a/README.md b/README.md index 7a2b2400..d6dca382 100644 --- a/README.md +++ b/README.md @@ -492,8 +492,8 @@ payment_retries_interval = 60 # seconds between retries #### Nostr Configuration ```toml [nostr] -# Your Mostro daemon's private key (nsec format) -nsec_privkey = 'nsec1...' +# Path to a file containing your Mostro daemon's private key (nsec format) +nsec_privkey_file = '/home/user/.mostro/nostr-key.nsec' # Relays to connect to relays = [ @@ -509,6 +509,10 @@ relays = [ rana --vanity mostro ``` +Save the generated `nsec` in the file referenced by `nsec_privkey_file`. +Mostro expects that file to contain only the `nsec` value. +Inline `nostr.nsec_privkey` is no longer supported and startup will fail until you move the key into a file. + **Important**: Never reuse keys between Mostro instances. Each daemon needs a unique identity. --- diff --git a/docs/STARTUP_AND_CONFIG.md b/docs/STARTUP_AND_CONFIG.md index e614567c..4aa19a7d 100644 --- a/docs/STARTUP_AND_CONFIG.md +++ b/docs/STARTUP_AND_CONFIG.md @@ -89,8 +89,10 @@ Configuration is loaded from `~/.mostro/settings.toml` (template: `settings.tpl. - Example (absolute path; use a real path — **do not** use `~`; SQLx does not expand tilde): `"sqlite:///home/youruser/.mostro/mostro.db"` - Default: `"sqlite://mostro.db"` -**Nostr** (`src/config/types.rs:47-54`): -- `nsec_privkey` (String): Mostro's Nostr private key in nsec format +**Nostr** (`src/config/types.rs`): +- `nsec_privkey_file` (String): Path to a file containing Mostro's Nostr private key in nsec format + - The file should contain only the `nsec` value + - Inline `nsec_privkey` is no longer supported and causes startup validation to fail - `relays` (Vec): List of Nostr relay URLs for event broadcasting - Default: `['ws://localhost:7000']` - Note: At least one relay required diff --git a/settings.tpl.toml b/settings.tpl.toml index 272c2607..bbe5a1ce 100644 --- a/settings.tpl.toml +++ b/settings.tpl.toml @@ -19,7 +19,7 @@ payment_attempts = 3 payment_retries_interval = 60 [nostr] -nsec_privkey = 'nsec1...' +nsec_privkey_file = '/home/user/nostr-key.nsec' relays = ['ws://localhost:7000'] [mostro] diff --git a/src/app/context.rs b/src/app/context.rs index 58f1def7..88060902 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -85,7 +85,7 @@ impl AppContext { /// Mostro's Nostr signing keys. /// - /// Parsed once at startup from `settings.nostr.nsec_privkey`. + /// Parsed once at startup from `settings.nostr.nsec_privkey_file`. /// Use this instead of `get_keys()` to avoid re-parsing on every call. pub fn keys(&self) -> &Keys { &self.keys @@ -99,6 +99,7 @@ pub mod test_utils { DatabaseSettings, ExpirationSettings, LightningSettings, MostroSettings, NostrSettings, RpcSettings, }; + use std::sync::atomic::{AtomicU64, Ordering}; /// Test helper wrapper for inspecting the shared order-message queue. #[derive(Debug, Clone)] @@ -228,10 +229,17 @@ pub mod test_utils { .order_msg_queue .unwrap_or_else(|| Arc::new(RwLock::new(Vec::new()))); - // Use provided keys or parse from settings + // Use provided keys or load from nsec_privkey_file let keys = self.keys.unwrap_or_else(|| { - Keys::parse(&settings.nostr.nsec_privkey) - .expect("TestContextBuilder: invalid nsec_privkey in settings") + let nsec = std::fs::read_to_string(&settings.nostr.nsec_privkey_file) + .unwrap_or_else(|e| { + panic!( + "TestContextBuilder: failed to read nsec_privkey_file '{}': {}", + settings.nostr.nsec_privkey_file, e + ) + }); + Keys::parse(nsec.trim()) + .expect("TestContextBuilder: invalid nsec in nsec_privkey_file") }); AppContext::new(pool, nostr_client, settings, order_msg_queue, keys) @@ -251,17 +259,29 @@ pub mod test_utils { } } + static TEST_KEY_FILE_COUNTER: AtomicU64 = AtomicU64::new(0); + /// Generate deterministic test settings with sensible defaults. pub fn test_settings() -> Settings { + let nsec_key = "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd"; + let counter = TEST_KEY_FILE_COUNTER.fetch_add(1, Ordering::Relaxed); + let nsec_dir = std::env::temp_dir().join(format!( + "mostro-test-{}-{}", + std::process::id(), + counter + )); + std::fs::create_dir_all(&nsec_dir).expect("failed to create test nsec key directory"); + let nsec_path = nsec_dir.join("nostr-key.nsec"); + std::fs::write(&nsec_path, nsec_key).expect("failed to write test nsec key file"); + Settings { database: DatabaseSettings { url: "sqlite::memory:".to_string(), }, nostr: NostrSettings { - // Valid test nsec from src/config/mod.rs tests - nsec_privkey: "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd" - .to_string(), + nsec_privkey_file: nsec_path.to_string_lossy().to_string(), relays: vec!["wss://relay.test".to_string()], + ..Default::default() }, mostro: MostroSettings::default(), lightning: LightningSettings::default(), diff --git a/src/app/release.rs b/src/app/release.rs index 74fe906e..64a68edb 100644 --- a/src/app/release.rs +++ b/src/app/release.rs @@ -678,14 +678,14 @@ async fn create_order_event( // If user has sent the order with his identity key means that he wants to be rate so we can just // check if we have identity key in db - if present we have to send reputation tags otherwise no. - let mostro_pubkey = my_keys.public_key().to_hex(); + let mostro_pubkey = my_keys.public_key(); let tags = match crate::db::is_user_present(pool, identity_pubkey.to_string()).await { Ok(user) => order_to_tags( new_order, Some((user.total_rating, user.total_reviews, user.created_at)), - Some(&mostro_pubkey), + &mostro_pubkey, )?, - Err(_) => order_to_tags(new_order, Some((0.0, 0, 0)), Some(&mostro_pubkey))?, + Err(_) => order_to_tags(new_order, Some((0.0, 0, 0)), &mostro_pubkey)?, }; // Prepare new child order event for sending (kind 38383 for orders) diff --git a/src/bitcoin_price.rs b/src/bitcoin_price.rs index f686b28c..61c14592 100644 --- a/src/bitcoin_price.rs +++ b/src/bitcoin_price.rs @@ -63,7 +63,7 @@ impl BitcoinPriceManager { /// Publishes exchange rates to Nostr as a NIP-33 addressable event (kind 30078) async fn publish_rates_to_nostr(rates: &HashMap) -> Result<(), MostroError> { - let keys = get_keys().map_err(|e| { + let keys = get_keys().await.map_err(|e| { error!("Failed to get Mostro keys: {}", e); MostroInternalErr(ServiceError::IOError(e.to_string())) })?; diff --git a/src/config/mod.rs b/src/config/mod.rs index 56ac0b2a..e256ac44 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -59,7 +59,7 @@ mod tests { // Fake settings for the test const NOSTR_SETTINGS: &str = r#"[nostr] - nsec_privkey = 'nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd' + nsec_privkey_file = '/tmp/mostro-test-nostr-key.nsec' relays = ['wss://relay.damus.io','wss://relay.mostro.network']"#; const LIGHTNING_SETTINGS: &str = r#"[lightning] @@ -194,15 +194,32 @@ mod tests { let nostr_settings: StubSettingsNostr = toml::from_str(NOSTR_SETTINGS).expect("Failed to deserialize"); assert_eq!( - nostr_settings.nostr.nsec_privkey, - "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd" + nostr_settings.nostr.nsec_privkey_file, + "/tmp/mostro-test-nostr-key.nsec" ); + assert_eq!(nostr_settings.nostr.nsec_privkey, None); assert_eq!( nostr_settings.nostr.relays, vec!["wss://relay.damus.io", "wss://relay.mostro.network"] ); } + #[test] + fn test_nostr_settings_legacy_inline_key() { + let legacy_settings = r#"[nostr] + nsec_privkey = 'nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd' + relays = ['wss://relay.damus.io']"#; + let nostr_settings: StubSettingsNostr = + toml::from_str(legacy_settings).expect("Failed to deserialize"); + + assert_eq!(nostr_settings.nostr.nsec_privkey_file, ""); + assert_eq!( + nostr_settings.nostr.nsec_privkey.as_deref(), + Some("nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd") + ); + assert_eq!(nostr_settings.nostr.relays, vec!["wss://relay.damus.io"]); + } + #[test] fn test_mostro_settings() { // Parse TOML content diff --git a/src/config/types.rs b/src/config/types.rs index 0d218829..5c8f211e 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -125,8 +125,12 @@ pub struct LightningSettings { /// Nostr configuration settings #[derive(Debug, Deserialize, Serialize, Default, Clone)] pub struct NostrSettings { - /// Nostr private key - pub nsec_privkey: String, + /// Path to file containing the Nostr private key (nsec) + #[serde(default)] + pub nsec_privkey_file: String, + /// Legacy inline Nostr private key kept for backward compatibility. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nsec_privkey: Option, /// Nostr relays list pub relays: Vec, } diff --git a/src/config/util.rs b/src/config/util.rs index 75394177..afcee35c 100644 --- a/src/config/util.rs +++ b/src/config/util.rs @@ -32,6 +32,19 @@ fn validate_mostro_settings(settings: &Settings) -> Result<(), MostroError> { )))); } + if settings.nostr.nsec_privkey.is_some() { + return Err(MostroInternalErr(ServiceError::IOError( + "nostr.nsec_privkey is no longer supported; move the key to nostr.nsec_privkey_file" + .to_string(), + ))); + } + + if settings.nostr.nsec_privkey_file.trim().is_empty() { + return Err(MostroInternalErr(ServiceError::IOError( + "Missing Nostr private key file configuration".to_string(), + ))); + } + Ok(()) } @@ -100,3 +113,52 @@ pub fn init_configuration_file(config_path: Option) -> Result<(), Mostro Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::types::{ + DatabaseSettings, LightningSettings, MostroSettings, NostrSettings, RpcSettings, + }; + + fn make_settings(nostr: NostrSettings) -> Settings { + Settings { + database: DatabaseSettings::default(), + lightning: LightningSettings::default(), + nostr, + mostro: MostroSettings::default(), + rpc: RpcSettings::default(), + expiration: None, + } + } + + #[test] + fn validate_mostro_settings_rejects_legacy_inline_nsec() { + let settings = make_settings(NostrSettings { + nsec_privkey: Some( + "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd".to_string(), + ), + relays: vec!["wss://relay.test".to_string()], + ..Default::default() + }); + + let error = validate_mostro_settings(&settings).expect_err("inline nsec must be rejected"); + assert!(error + .to_string() + .contains("nostr.nsec_privkey is no longer supported")); + } + + #[test] + fn validate_mostro_settings_requires_private_key_file() { + let settings = make_settings(NostrSettings { + relays: vec!["wss://relay.test".to_string()], + ..Default::default() + }); + + let error = + validate_mostro_settings(&settings).expect_err("missing key file must be rejected"); + assert!(error + .to_string() + .contains("Missing Nostr private key file configuration")); + } +} diff --git a/src/config/wizard.rs b/src/config/wizard.rs index 5c4a6e45..acfd5960 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -58,7 +58,7 @@ fn run_setup_wizard(settings_dir: &Path, config_file_path: &Path) -> Result Result { }) } -fn prompt_nostr_settings() -> Result { +fn prompt_nostr_settings(settings_dir: &Path) -> Result { let has_nsec = Confirm::new() .with_prompt("Do you have an existing nsec key?") .default(false) .interact() .map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?; - let nsec_privkey = if has_nsec { + let nsec = if has_nsec { Input::new() .with_prompt("Enter your nsec private key") .validate_with(|input: &String| validate_nsec(input)) @@ -174,6 +174,34 @@ fn prompt_nostr_settings() -> Result { nsec }; + // Write the nsec key to a file in the settings directory + let nsec_privkey_file = settings_dir.join("nostr-key.nsec"); + { + #[cfg(unix)] + let file = { + use std::os::unix::fs::OpenOptionsExt; + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&nsec_privkey_file) + }; + #[cfg(not(unix))] + let file = { + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&nsec_privkey_file) + }; + let mut file = file.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?; + file.write_all(nsec.as_bytes()) + .map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?; + } + + println!(" Private key saved to {}", nsec_privkey_file.display()); + let relays_input: String = Input::new() .with_prompt("Nostr relays (comma-separated)") .default("wss://relay.mostro.network".to_string()) @@ -188,8 +216,9 @@ fn prompt_nostr_settings() -> Result { .collect(); Ok(NostrSettings { - nsec_privkey, + nsec_privkey_file: nsec_privkey_file.to_string_lossy().to_string(), relays, + ..Default::default() }) } diff --git a/src/main.rs b/src/main.rs index 23e7850a..fb916ccf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,7 +66,7 @@ async fn main() -> Result<()> { }; // Get mostro keys - let mostro_keys = util::get_keys()?; + let mostro_keys = util::get_keys().await?; let subscription = Filter::new() .pubkey(mostro_keys.public_key()) diff --git a/src/nip33.rs b/src/nip33.rs index eadc6b00..bd17afa6 100644 --- a/src/nip33.rs +++ b/src/nip33.rs @@ -1,7 +1,7 @@ use crate::config::constants::NOSTR_EXCHANGE_RATES_EVENT_KIND; use crate::config::settings::Settings; use crate::lightning::LnStatus; -use crate::util::{get_expiration_timestamp_for_kind, get_keys}; +use crate::util::{get_expiration_timestamp_for_kind}; use crate::LN_STATUS; use mostro_core::prelude::*; use nostr::event::builder::Error; @@ -349,14 +349,24 @@ fn create_source_tag( /// # Arguments /// /// * `order` - The order to transform into tags -/// * `reputation_data` - Optional reputation data for the maker -/// * `mostro_pubkey` - Optional Mostro pubkey override. If None, derived from get_keys(). -/// Pass Some() in tests to avoid global state dependencies. +/// * `reputation_data` - Optional maker reputation data as +/// `(average_rating, total_reviews, created_at_timestamp)`. +/// When present, the tuple is encoded into the `rating` tag. +/// When absent, no `rating` tag is emitted. +/// * `mostro_pubkey` - Borrowed Mostro public key used to build the `source` tag. +/// Callers must pass the exact instance key as `&nostr::PublicKey`. +/// The function does not derive or load this key internally. +/// +/// # Returns +/// +/// Returns `Ok(Some(Tags))` when the order status should be represented as a NIP-33 event. +/// Returns `Ok(None)` when no event should be emitted for the order's current state. +/// Returns `Err(MostroError)` if required tags cannot be constructed. /// pub fn order_to_tags( order: &Order, reputation_data: Option<(f64, i64, i64)>, - mostro_pubkey: Option<&str>, + mostro_pubkey: &nostr::PublicKey ) -> Result, MostroError> { // Position of the tags in the list const RATING_TAG_INDEX: usize = 7; @@ -366,10 +376,7 @@ pub fn order_to_tags( let (create_event, status) = create_status_tags(order)?; // Create mostro: scheme link in case of pending order creation // Include the Mostro pubkey so clients can identify the instance - let pubkey = match mostro_pubkey { - Some(pk) => pk.to_string(), - None => get_keys()?.public_key().to_hex(), - }; + let pubkey = mostro_pubkey.to_hex().to_string(); let mostro_link = create_source_tag(order, &Settings::get_nostr().relays, &pubkey)?; // Send just in case the order is pending/in-progress/success/canceled @@ -574,10 +581,6 @@ mod tests { // ── Shared test helpers ────────────────────────────────────────────────────── - /// Test Mostro pubkey (derived from the test nsec in test_settings) - const TEST_MOSTRO_PUBKEY: &str = - "9a0e40e008c6dcfdb3c608a65ddf1c4e72eed7eeefbe1eb88ea0f1ea8b43dc4d"; - /// Initialize global settings once per test binary run using the canonical /// test_settings() helper from AppContext test_utils — consistent with the /// rest of the test infrastructure. @@ -663,7 +666,8 @@ mod tests { init_test_settings(); let order = make_pending_order(); - let tags = order_to_tags(&order, None, Some(TEST_MOSTRO_PUBKEY)) + let default_nostr_pub_key = nostr::Keys::parse("nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd").unwrap().public_key(); + let tags = order_to_tags(&order, None, &default_nostr_pub_key) .expect("order_to_tags must not error") .expect("pending order must produce Some(tags)"); @@ -677,7 +681,8 @@ mod tests { init_test_settings(); let order = make_pending_order(); - let tags = order_to_tags(&order, None, Some(TEST_MOSTRO_PUBKEY)) + let default_nostr_pub_key = nostr::Keys::parse("nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd").unwrap().public_key(); + let tags = order_to_tags(&order, None, &default_nostr_pub_key) .expect("order_to_tags must not error") .expect("pending order must produce Some(tags)"); @@ -709,7 +714,8 @@ mod tests { init_test_settings(); let order = make_pending_order(); - let tags = order_to_tags(&order, None, Some(TEST_MOSTRO_PUBKEY)) + let default_nostr_pub_key = nostr::Keys::parse("nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd").unwrap().public_key(); + let tags = order_to_tags(&order, None, &default_nostr_pub_key) .expect("order_to_tags must not error") .expect("pending order must produce Some(tags)"); @@ -724,9 +730,10 @@ mod tests { source.contains("&mostro="), "source must contain '&mostro=' parameter" ); + let pubkey_hex = default_nostr_pub_key.to_hex(); assert!( - source.contains(&format!("&mostro={}", TEST_MOSTRO_PUBKEY)), - "source must contain the correct Mostro pubkey" + source.contains(&format!("&mostro={}", pubkey_hex)), + "source must contain the correct Mostro pubkey: {}", source ); } diff --git a/src/util.rs b/src/util.rs index 78deffad..f39a0cfb 100644 --- a/src/util.rs +++ b/src/util.rs @@ -26,6 +26,8 @@ use sqlx::SqlitePool; use sqlx_crud::Crud; use std::collections::HashMap; use std::fmt::Write; +use std::path::Path; +use std::path::PathBuf; use std::str::FromStr; use std::thread; use tokio::sync::mpsc::channel; @@ -237,7 +239,7 @@ pub fn get_expiration_date(expire: Option) -> i64 { /// # Examples /// /// ``` -/// // Get expiration for a dispute event (kind 38386) +/// // Get expiration for a dispute event (kind 38386) /// let dispute_expiration = get_expiration_timestamp_for_kind(38386); /// ``` pub fn get_expiration_timestamp_for_kind(kind: u16) -> Option { @@ -298,20 +300,20 @@ pub async fn get_tags_for_new_order( trade_pubkey: &PublicKey, mostro_keys: &Keys, ) -> Result, MostroError> { - let mostro_pubkey = mostro_keys.public_key().to_hex(); + let mostro_pubkey = mostro_keys.public_key(); match is_user_present(pool, identity_pubkey.to_string()).await { Ok(user) => { // We transform the order fields to tags to use in the event order_to_tags( new_order_db, Some((user.total_rating, user.total_reviews, user.created_at)), - Some(&mostro_pubkey), + &mostro_pubkey ) } Err(_) => { // We transform the order fields to tags to use in the event if identity_pubkey == trade_pubkey { - order_to_tags(new_order_db, Some((0.0, 0, 0)), Some(&mostro_pubkey)) + order_to_tags(new_order_db, Some((0.0, 0, 0)), &mostro_pubkey) } else { Err(MostroInternalErr(ServiceError::InvalidPubkey)) } @@ -570,7 +572,7 @@ pub async fn publish_dev_fee_audit_event( None => "unknown".to_string(), }; // Get Mostro keys for signing - let keys = get_keys()?; + let keys = get_keys().await?; // Get Nostr client let client = get_nostr_client()?; @@ -634,13 +636,41 @@ pub async fn publish_dev_fee_audit_event( Ok(()) } -pub fn get_keys() -> Result { +async fn load_nostr_private_key( + path: impl AsRef + Into, +) -> Result { + let path = path.into(); + let private_key = tokio::fs::read(&path).await.map_err(|e| { + tracing::error!("Failed to read nostr private key file: {}, path: {}", e, path.display()); + MostroInternalErr(ServiceError::NostrError(format!( + "Failed to read Nostr private key file: {}", + path.display(), + ))) + })?; + String::from_utf8(private_key) + .map(|s| s.trim().to_string()) + .map_err(|e| { + tracing::error!("Nostr private key file contains invalid UTF-8: {}, path: {}", e, path.display()); + MostroInternalErr(ServiceError::NostrError(format!( + "Nostr private key file contains invalid UTF-8: {}", + path.display(), + ))) + }) +} + +pub async fn get_keys() -> Result { let nostr_settings = Settings::get_nostr(); + if nostr_settings.nsec_privkey_file.trim().is_empty() { + return Err(MostroInternalErr(ServiceError::NostrError( + "Missing Nostr private key file configuration".to_string(), + ))); + } + let nsec_privkey = load_nostr_private_key(&nostr_settings.nsec_privkey_file).await?; // nostr private key - match Keys::parse(&nostr_settings.nsec_privkey) { + match Keys::parse(&nsec_privkey) { Ok(my_keys) => Ok(my_keys), Err(e) => { - tracing::error!("Failed to parse nostr private key: {}", e); + tracing::error!("Failed to parse nostr private key from file: {} {}", e, nostr_settings.nsec_privkey_file); Err(MostroInternalErr(ServiceError::NostrError(e.to_string()))) } } @@ -731,8 +761,8 @@ pub async fn update_order_event( let reputation_data = get_ratings_for_pending_order(&order_updated, status).await?; // We transform the order fields to tags to use in the event - let mostro_pubkey = keys.public_key().to_hex(); - if let Some(tags) = order_to_tags(&order_updated, reputation_data, Some(&mostro_pubkey))? { + let mostro_pubkey = keys.public_key(); + if let Some(tags) = order_to_tags(&order_updated, reputation_data, &mostro_pubkey)? { // nip33 kind with order id as identifier and order fields as tags (kind 38383 for orders) let event = new_order_event(keys, "", order.id.to_string(), tags) .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?; @@ -896,7 +926,7 @@ pub async fn invoice_subscribe(hash: Vec, request_id: Option) -> Result let hash = bytes_to_string(msg.hash.as_ref()); // If this invoice was paid by the seller if msg.state == InvoiceState::Accepted { - let keys = match get_keys() { + let keys = match get_keys().await { Ok(k) => k, Err(e) => { info!("Failed to get keys: {e}");