From fabc1a727f4adeaf35d9581ac56a375a368cf7f4 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Fri, 25 Oct 2024 09:43:20 +0200 Subject: [PATCH] wrap breez services with replaceable variant If an app hibernates, the breez sdk may still run. However, any connections to the outside world, like grpc connections will (may) stop functioning. This means after hibernation the sdk needs to reconnect. Hibernation is detected by awaiting a 'sleep' in a loop. If the sleep has taken a long time, that means the app has been in hibernation. Because many services have references to the node api, and other services than the greenlight client may have been affected by hibernation, the entire breezservices instance is recreated, reconnected. In order to allow this, an internal variant of breezservices was made. This internal instance can be replaced at any time. Because the breezservices code was moved to another file, with edits, I took the liberty of putting all functions inside breezservices in alphabetical order. --- libs/sdk-bindings/src/uniffi_binding.rs | 6 +- libs/sdk-core/src/backup.rs | 2 +- libs/sdk-core/src/binding.rs | 21 +- libs/sdk-core/src/breez_services.rs | 3661 +++--------------- libs/sdk-core/src/bridge_generated.rs | 16 +- libs/sdk-core/src/internal_breez_services.rs | 2996 ++++++++++++++ libs/sdk-core/src/lib.rs | 11 +- libs/sdk-core/src/lnurl/pay.rs | 24 +- libs/sdk-core/src/lsps0/transport.rs | 2 +- libs/sdk-core/src/models.rs | 1 + libs/sdk-core/src/swap_in/swap.rs | 4 +- libs/sdk-core/src/swap_out/reverseswap.rs | 4 +- libs/sdk-core/src/test_utils.rs | 2 +- tools/sdk-cli/src/command_handlers.rs | 4 +- 14 files changed, 3574 insertions(+), 3180 deletions(-) create mode 100644 libs/sdk-core/src/internal_breez_services.rs diff --git a/libs/sdk-bindings/src/uniffi_binding.rs b/libs/sdk-bindings/src/uniffi_binding.rs index ec446fd29..18069a110 100644 --- a/libs/sdk-bindings/src/uniffi_binding.rs +++ b/libs/sdk-bindings/src/uniffi_binding.rs @@ -152,11 +152,11 @@ impl BlockingBreezServices { } pub fn node_credentials(&self) -> SdkResult> { - self.breez_services.node_credentials() + rt().block_on(self.breez_services.node_credentials()) } pub fn node_info(&self) -> SdkResult { - self.breez_services.node_info() + rt().block_on(self.breez_services.node_info()) } pub fn sign_message(&self, req: SignMessageRequest) -> SdkResult { @@ -168,7 +168,7 @@ impl BlockingBreezServices { } pub fn backup_status(&self) -> SdkResult { - self.breez_services.backup_status() + rt().block_on(self.breez_services.backup_status()) } pub fn backup(&self) -> SdkResult<()> { diff --git a/libs/sdk-core/src/backup.rs b/libs/sdk-core/src/backup.rs index ef44c567d..9fe40bbde 100644 --- a/libs/sdk-core/src/backup.rs +++ b/libs/sdk-core/src/backup.rs @@ -1,6 +1,6 @@ use crate::{ - breez_services::BackupFailedData, error::SdkResult, + internal_breez_services::BackupFailedData, persist::db::{HookEvent, SqliteStorage}, BreezEvent, Config, }; diff --git a/libs/sdk-core/src/binding.rs b/libs/sdk-core/src/binding.rs index 6d0f4d58a..dd6b655d2 100644 --- a/libs/sdk-core/src/binding.rs +++ b/libs/sdk-core/src/binding.rs @@ -28,12 +28,13 @@ pub use sdk_common::prelude::{ }; use tokio::sync::Mutex; -use crate::breez_services::{self, BreezEvent, BreezServices, EventListener}; +use crate::breez_services::{self, BreezServices}; use crate::chain::RecommendedFees; use crate::error::{ ConnectError, ReceiveOnchainError, ReceivePaymentError, RedeemOnchainError, SdkError, SendOnchainError, SendPaymentError, }; +use crate::internal_breez_services::{BreezEvent, EventListener}; use crate::lsp::LspInformation; use crate::models::{Config, LogEntry, NodeState, Payment, SwapInfo}; use crate::{ @@ -332,22 +333,14 @@ pub fn sync() -> Result<()> { /// See [BreezServices::node_credentials] pub fn node_credentials() -> Result> { - block_on(async { - get_breez_services() - .await? - .node_credentials() - .map_err(anyhow::Error::new::) - }) + block_on(async { get_breez_services().await?.node_credentials().await }) + .map_err(anyhow::Error::new::) } /// See [BreezServices::node_info] pub fn node_info() -> Result { - block_on(async { - get_breez_services() - .await? - .node_info() - .map_err(anyhow::Error::new::) - }) + block_on(async { get_breez_services().await?.node_info().await }) + .map_err(anyhow::Error::new::) } /// See [BreezServices::configure_node] @@ -497,7 +490,7 @@ pub fn backup() -> Result<()> { /// See [BreezServices::backup_status] pub fn backup_status() -> Result { - block_on(async { get_breez_services().await?.backup_status() }) + block_on(async { get_breez_services().await?.backup_status().await }) .map_err(anyhow::Error::new::) } diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 63806917d..e57451364 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -1,172 +1,53 @@ -use std::fs::OpenOptions; -use std::io::Write; -use std::str::FromStr; -use std::sync::Arc; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::{ + fs::OpenOptions, + io::Write, + sync::Arc, + time::{Duration, SystemTime}, +}; use anyhow::{anyhow, Result}; -use bip39::*; -use bitcoin::hashes::hex::ToHex; -use bitcoin::hashes::{sha256, Hash}; -use bitcoin::util::bip32::ChildNumber; +use bip39::{Language, Mnemonic, Seed}; use chrono::Local; -use futures::TryFutureExt; use log::{LevelFilter, Metadata, Record}; -use reqwest::{header::CONTENT_TYPE, Body}; -use sdk_common::grpc; -use sdk_common::prelude::*; -use serde::Serialize; -use serde_json::{json, Value}; -use strum_macros::EnumString; -use tokio::sync::{mpsc, watch, Mutex}; -use tokio::time::{sleep, MissedTickBehavior}; - -use crate::backup::{BackupRequest, BackupTransport, BackupWatcher}; -use crate::buy::{BuyBitcoinApi, BuyBitcoinService}; -use crate::chain::{ - ChainService, Outspend, RecommendedFees, RedundantChainService, RedundantChainServiceTrait, - DEFAULT_MEMPOOL_SPACE_URL, -}; -use crate::error::{ - ConnectError, ReceiveOnchainError, ReceiveOnchainResult, ReceivePaymentError, - RedeemOnchainResult, SdkError, SdkResult, SendOnchainError, SendPaymentError, +use sdk_common::prelude::{ + BreezServer, FiatCurrency, LnUrlAuthError, LnUrlAuthRequestData, LnUrlCallbackStatus, + LnUrlPayError, LnUrlPayRequest, LnUrlWithdrawError, LnUrlWithdrawRequest, LnUrlWithdrawResult, + Rate, PRODUCTION_BREEZSERVER_URL, }; -use crate::greenlight::{GLBackupTransport, Greenlight}; -use crate::lnurl::auth::SdkLnurlAuthSigner; -use crate::lnurl::pay::*; -use crate::lsp::LspInformation; -use crate::models::{ - sanitize::*, ChannelState, ClosedChannelPaymentDetails, Config, EnvironmentType, LspAPI, - NodeState, Payment, PaymentDetails, PaymentType, ReverseSwapPairInfo, ReverseSwapServiceAPI, - SwapInfo, SwapperAPI, INVOICE_PAYMENT_FEE_EXPIRY_SECONDS, +use tokio::sync::Mutex; + +use crate::{ + error::{ + ReceiveOnchainError, ReceiveOnchainResult, ReceivePaymentError, RedeemOnchainResult, + SdkResult, SendOnchainError, SendPaymentError, + }, + internal_breez_services::{ + BreezServicesResult, CheckMessageRequest, CheckMessageResponse, EventListener, + InternalBreezServices, SignMessageRequest, SignMessageResponse, + }, + lnurl::pay::LnUrlPayResult, + persist::db::SqliteStorage, + BackupStatus, BuyBitcoinRequest, BuyBitcoinResponse, Config, ConfigureNodeRequest, + ConnectRequest, EnvironmentType, ListPaymentsRequest, LspInformation, + MaxReverseSwapAmountResponse, NodeConfig, NodeCredentials, NodeState, + OnchainPaymentLimitsResponse, OpenChannelFeeRequest, OpenChannelFeeResponse, PayOnchainRequest, + PayOnchainResponse, Payment, PrepareOnchainPaymentRequest, PrepareOnchainPaymentResponse, + PrepareRedeemOnchainFundsRequest, PrepareRedeemOnchainFundsResponse, PrepareRefundRequest, + PrepareRefundResponse, ReceiveOnchainRequest, ReceivePaymentRequest, ReceivePaymentResponse, + RecommendedFees, RedeemOnchainFundsRequest, RedeemOnchainFundsResponse, RefundRequest, + RefundResponse, ReportIssueRequest, ReverseSwapFeesRequest, ReverseSwapInfo, + ReverseSwapPairInfo, SendOnchainRequest, SendOnchainResponse, SendPaymentRequest, + SendPaymentResponse, SendSpontaneousPaymentRequest, ServiceHealthCheckResponse, + StaticBackupRequest, StaticBackupResponse, SupportAPI, SwapInfo, }; -use crate::node_api::{CreateInvoiceRequest, NodeAPI}; -use crate::persist::db::SqliteStorage; -use crate::swap_in::swap::BTCReceiveSwap; -use crate::swap_out::boltzswap::BoltzApi; -use crate::swap_out::reverseswap::{BTCSendSwap, CreateReverseSwapArg}; -use crate::*; - -pub type BreezServicesResult = Result; - -/// Trait that can be used to react to various [BreezEvent]s emitted by the SDK. -pub trait EventListener: Send + Sync { - fn on_event(&self, e: BreezEvent); -} - -/// Event emitted by the SDK. To listen for and react to these events, use an [EventListener] when -/// initializing the [BreezServices]. -#[derive(Clone, Debug, PartialEq)] -#[allow(clippy::large_enum_variant)] -pub enum BreezEvent { - /// Indicates that a new block has just been found - NewBlock { block: u32 }, - /// Indicates that a new invoice has just been paid - InvoicePaid { details: InvoicePaidDetails }, - /// Indicates that the local SDK state has just been sync-ed with the remote components - Synced, - /// Indicates that an outgoing payment has been completed successfully - PaymentSucceed { details: Payment }, - /// Indicates that an outgoing payment has been failed to complete - PaymentFailed { details: PaymentFailedData }, - /// Indicates that the backup process has just started - BackupStarted, - /// Indicates that the backup process has just finished successfully - BackupSucceeded, - /// Indicates that the backup process has just failed - BackupFailed { details: BackupFailedData }, - /// Indicates that a reverse swap has been updated which may also - /// include a status change - ReverseSwapUpdated { details: ReverseSwapInfo }, - /// Indicates that a swap has been updated which may also - /// include a status change - SwapUpdated { details: SwapInfo }, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct BackupFailedData { - pub error: String, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct PaymentFailedData { - pub error: String, - pub node_id: String, - pub invoice: Option, - pub label: Option, -} - -/// Details of an invoice that has been paid, included as payload in an emitted [BreezEvent] -#[derive(Clone, Debug, PartialEq)] -pub struct InvoicePaidDetails { - pub payment_hash: String, - pub bolt11: String, - pub payment: Option, -} - -pub trait LogStream: Send + Sync { - fn log(&self, l: LogEntry); -} - -/// Request to sign a message with the node's private key. -#[derive(Clone, Debug, PartialEq)] -pub struct SignMessageRequest { - /// The message to be signed by the node's private key. - pub message: String, -} - -/// Response to a [SignMessageRequest]. -#[derive(Clone, Debug, PartialEq)] -pub struct SignMessageResponse { - /// The signature that covers the message of SignMessageRequest. Zbase - /// encoded. - pub signature: String, -} - -/// Request to check a message was signed by a specific node id. -#[derive(Clone, Debug, PartialEq)] -pub struct CheckMessageRequest { - /// The message that was signed. - pub message: String, - /// The public key of the node that signed the message. - pub pubkey: String, - /// The zbase encoded signature to verify. - pub signature: String, -} - -/// Response to a [CheckMessageRequest] -#[derive(Clone, Debug, PartialEq)] -pub struct CheckMessageResponse { - /// Boolean value indicating whether the signature covers the message and - /// was signed by the given pubkey. - pub is_valid: bool, -} - -#[derive(Clone, PartialEq, EnumString, Serialize)] -enum DevCommand { - /// Generates diagnostic data report. - #[strum(serialize = "generatediagnosticdata")] - GenerateDiagnosticData, -} +const DETECT_HIBERNATE_SLEEP_DURATION: Duration = Duration::from_secs(1); +const DETECT_HIBERNATE_MAX_OFFSET: Duration = Duration::from_secs(2); /// BreezServices is a facade and the single entry point for the SDK. pub struct BreezServices { - config: Config, - started: Mutex, - node_api: Arc, - lsp_api: Arc, - fiat_api: Arc, - buy_bitcoin_api: Arc, - support_api: Arc, - chain_service: Arc, - persister: Arc, - payment_receiver: Arc, - btc_receive_swapper: Arc, - btc_send_swapper: Arc, - event_listener: Option>, - backup_watcher: Arc, - shutdown_sender: watch::Sender<()>, - shutdown_receiver: watch::Receiver<()>, + services: Mutex>, + req: ConnectRequest, + event_listener: Arc>, } impl BreezServices { @@ -185,581 +66,429 @@ impl BreezServices { req: ConnectRequest, event_listener: Box, ) -> BreezServicesResult> { - let (sdk_version, sdk_git_hash) = Self::get_sdk_version(); - info!("SDK v{sdk_version} ({sdk_git_hash})"); - let start = Instant::now(); - let services = BreezServicesBuilder::new(req.config) - .seed(req.seed) - .build(req.restore_only, Some(event_listener)) - .await?; - services.start().await?; - let connect_duration = start.elapsed(); - info!("SDK connected in: {connect_duration:?}"); + let event_listener = Arc::new(event_listener); + let services = + InternalBreezServices::connect(req.clone(), Arc::clone(&event_listener)).await?; + + let services = Arc::new(BreezServices { + event_listener, + req, + services: Mutex::new(services), + }); + services.detect_hibernation(); Ok(services) } - fn get_sdk_version() -> (&'static str, &'static str) { - let sdk_version = option_env!("CARGO_PKG_VERSION").unwrap_or_default(); - let sdk_git_hash = option_env!("SDK_GIT_HASH").unwrap_or_default(); - (sdk_version, sdk_git_hash) + /// Get the full default config for a specific environment type + pub fn default_config( + env_type: EnvironmentType, + api_key: String, + node_config: NodeConfig, + ) -> Config { + match env_type { + EnvironmentType::Production => Config::production(api_key, node_config), + EnvironmentType::Staging => Config::staging(api_key, node_config), + } } - /// Internal utility method that starts the BreezServices background tasks for this instance. - /// - /// It should be called once right after creating [BreezServices], since it is essential for the - /// communicating with the node. - /// - /// It should be called only once when the app is started, regardless whether the app is sent to - /// background and back. - async fn start(self: &Arc) -> BreezServicesResult<()> { - let mut started = self.started.lock().await; - ensure_sdk!( - !*started, - ConnectError::Generic { - err: "BreezServices already started".into() - } - ); + fn detect_hibernation(self: &Arc) { + let cloned = Arc::clone(self); + tokio::spawn(async move { + loop { + let now = SystemTime::now(); + tokio::time::sleep(DETECT_HIBERNATE_SLEEP_DURATION).await; + let elapsed = match now.elapsed() { + Ok(elapsed) => elapsed, + Err(e) => { + error!("track_hibernation failed with: {:?}", e); + continue; + } + }; - let start = Instant::now(); - self.start_background_tasks().await?; - let start_duration = start.elapsed(); - info!("SDK initialized in: {start_duration:?}"); - *started = true; - Ok(()) - } + if elapsed + .saturating_sub(DETECT_HIBERNATE_SLEEP_DURATION) + .ge(&DETECT_HIBERNATE_MAX_OFFSET) + { + debug!("Hibernation detected: time diff {}s", elapsed.as_secs_f32()); + let mut services = cloned.services.lock().await; + debug!("Hibernation detected: disconnecting services"); + let _ = services.disconnect().await; + debug!("Hibernation detected: services disconnected"); + debug!("Hibernation detected: reconnecting services"); + let new_services = match InternalBreezServices::connect( + cloned.req.clone(), + Arc::clone(&cloned.event_listener), + ) + .await + { + Ok(new_services) => new_services, + Err(e) => { + // TODO: retry this reconnect later. + error!( + "Failed to reconnect breez services after hibernation: {:?}", + e + ); + continue; + } + }; - /// Trigger the stopping of BreezServices background threads for this instance. - pub async fn disconnect(&self) -> SdkResult<()> { - let mut started = self.started.lock().await; - ensure_sdk!( - *started, - SdkError::Generic { - err: "BreezServices is not running".into(), + debug!("Hibernation detected: services reconnected"); + *services = new_services; + } } - ); - self.shutdown_sender - .send(()) - .map_err(|e| SdkError::Generic { - err: format!("Shutdown failed: {e}"), - })?; - *started = false; - Ok(()) + }); } - /// Configure the node + /// Configures a global SDK logger that will log to file and will forward log events to + /// an optional application-specific logger. /// - /// This calls [NodeAPI::configure_node] to make changes to the active node's configuration. - /// Configuring the [ConfigureNodeRequest::close_to_address] only needs to be done one time - /// when registering the node or when the close to address need to be changed. Otherwise it is - /// stored by the node and used when neccessary. - pub async fn configure_node(&self, req: ConfigureNodeRequest) -> SdkResult<()> { - Ok(self.node_api.configure_node(req.close_to_address).await?) - } - - /// Pay a bolt11 invoice + /// If called, it should be called before any SDK methods (for example, before `connect`). /// - /// Calling `send_payment` ensures that the payment is not already completed, if so it will result in an error. - /// If the invoice doesn't specify an amount, the amount is taken from the `amount_msat` arg. - pub async fn send_payment( - &self, - req: SendPaymentRequest, - ) -> Result { - let parsed_invoice = parse_invoice(req.bolt11.as_str())?; - let invoice_expiration = parsed_invoice.timestamp + parsed_invoice.expiry; - let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - if invoice_expiration < current_time { - return Err(SendPaymentError::InvoiceExpired { - err: format!("Invoice expired at {}", invoice_expiration), - }); - } - let invoice_amount_msat = parsed_invoice.amount_msat.unwrap_or_default(); - let provided_amount_msat = req.amount_msat.unwrap_or_default(); - - // Valid the invoice network against the config network - validate_network(parsed_invoice.clone(), self.config.network)?; + /// It must be called only once in the application lifecycle. Alternatively, If the application + /// already uses a globally-registered logger, this method shouldn't be called at all. + /// + /// ### Arguments + /// + /// - `log_dir`: Location where the the SDK log file will be created. The directory must already exist. + /// + /// - `app_logger`: Optional application logger. + /// + /// If the application is to use it's own logger, but would also like the SDK to log SDK-specific + /// log output to a file in the configured `log_dir`, then do not register the + /// app-specific logger as a global logger and instead call this method with the app logger as an arg. + /// + /// ### Logging Configuration + /// + /// Setting `breez_sdk_core::input_parser=debug` will include in the logs the raw payloads received + /// when interacting with JSON endpoints, for example those used during all LNURL workflows. + /// + /// ### Errors + /// + /// An error is thrown if the log file cannot be created in the working directory. + /// + /// An error is thrown if a global logger is already configured. + pub fn init_logging(log_dir: &str, app_logger: Option>) -> Result<()> { + let target_log_file = Box::new( + OpenOptions::new() + .create(true) + .append(true) + .open(format!("{log_dir}/sdk.log")) + .map_err(|e| anyhow!("Can't create log file: {e}"))?, + ); + let logger = env_logger::Builder::new() + .target(env_logger::Target::Pipe(target_log_file)) + .parse_filters( + r#" + debug, + breez_sdk_core::input_parser=warn, + breez_sdk_core::backup=info, + breez_sdk_core::persist::reverseswap=info, + breez_sdk_core::reverseswap=info, + gl_client=debug, + h2=warn, + hyper=warn, + lightning_signer=warn, + reqwest=warn, + rustls=warn, + rustyline=warn, + vls_protocol_signer=warn + "#, + ) + .format(|buf, record| { + writeln!( + buf, + "[{} {} {}:{}] {}", + Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), + record.level(), + record.module_path().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.args() + ) + }) + .build(); - let amount_msat = match (provided_amount_msat, invoice_amount_msat) { - (0, 0) => { - return Err(SendPaymentError::InvalidAmount { - err: "Amount must be provided when paying a zero invoice".into(), - }) - } - (0, amount_msat) => amount_msat, - (amount_msat, 0) => amount_msat, - (_amount_1, _amount_2) => { - return Err(SendPaymentError::InvalidAmount { - err: "Amount should not be provided when paying a non zero invoice".into(), - }) - } + let global_logger = GlobalSdkLogger { + logger, + log_listener: app_logger, }; - if self - .persister - .get_completed_payment_by_hash(&parsed_invoice.payment_hash)? - .is_some() - { - return Err(SendPaymentError::AlreadyPaid); - } + log::set_boxed_logger(Box::new(global_logger)) + .map_err(|e| anyhow!("Failed to set global logger: {e}"))?; + log::set_max_level(LevelFilter::Trace); - // If there is an lsp, the invoice route hint does not contain the - // lsp in the hint, and trampoline payments are requested, attempt a - // trampoline payment. - let maybe_trampoline_id = self.get_trampoline_id(&req, &parsed_invoice)?; - - self.persist_pending_payment(&parsed_invoice, amount_msat, req.label.clone())?; - - // If trampoline is an option, try trampoline first. - let trampoline_result = if let Some(trampoline_id) = maybe_trampoline_id { - debug!("attempting trampoline payment"); - match self - .node_api - .send_trampoline_payment( - parsed_invoice.bolt11.clone(), - amount_msat, - req.label.clone(), - trampoline_id, - ) - .await - { - Ok(res) => Some(res), - Err(e) => { - warn!("trampoline payment failed: {:?}", e); - None - } - } - } else { - debug!("not attempting trampoline payment"); - None - }; + Ok(()) + } - // If trampoline failed or didn't happen, fall back to regular payment. - let payment_res = match trampoline_result { - Some(res) => Ok(res), - None => { - debug!("attempting normal payment"); - self.node_api - .send_payment( - parsed_invoice.bolt11.clone(), - req.amount_msat, - req.label.clone(), - ) - .map_err(Into::into) - .await - } - }; + /// Fetches the service health check from the support API. + pub async fn service_health_check(api_key: String) -> SdkResult { + let support_api: Arc = Arc::new(BreezServer::new( + PRODUCTION_BREEZSERVER_URL.to_string(), + Some(api_key), + )?); - debug!("payment returned {:?}", payment_res); - let payment = self - .on_payment_completed( - parsed_invoice.payee_pubkey.clone(), - Some(parsed_invoice), - req.label, - payment_res, - ) - .await?; - Ok(SendPaymentResponse { payment }) + support_api.service_health_check().await } - fn get_trampoline_id( - &self, - req: &SendPaymentRequest, - invoice: &LNInvoice, - ) -> Result>, SendPaymentError> { - // If trampoline is turned off, return immediately - if !req.use_trampoline { - return Ok(None); - } - - // Get the persisted LSP id. If no LSP, return early. - let lsp_pubkey = match self.persister.get_lsp_pubkey()? { - Some(lsp_pubkey) => lsp_pubkey, - None => return Ok(None), - }; + /// Get the static backup data from the persistent storage. + /// This data enables the user to recover the node in an external core ligntning node. + /// See here for instructions on how to recover using this data: + pub fn static_backup(req: StaticBackupRequest) -> SdkResult { + let storage = SqliteStorage::new(req.working_dir); + Ok(StaticBackupResponse { + backup: storage.get_static_backup()?, + }) + } - // If the LSP is in the routing hint, don't use trampoline, but rather - // pay directly to the destination. - if invoice.routing_hints.iter().any(|hint| { - hint.hops - .last() - .map(|hop| hop.src_node_id == lsp_pubkey) - .unwrap_or(false) - }) { - return Ok(None); - } + /// Force running backup + pub async fn backup(&self) -> SdkResult<()> { + let services = Arc::clone(&*self.services.lock().await); + services.backup().await + } - // If ended up here, this payment will attempt trampoline. - Ok(Some(hex::decode(lsp_pubkey).map_err(|_| { - SendPaymentError::Generic { - err: "failed to decode lsp pubkey".to_string(), - } - })?)) + /// Retrieve the node up to date BackupStatus + pub async fn backup_status(&self) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.backup_status() } - /// Pay directly to a node id using keysend - pub async fn send_spontaneous_payment( + /// Generates an url that can be used by a third part provider to buy Bitcoin with fiat currency. + /// + /// A user-selected [OpeningFeeParams] can be optionally set in the argument. If set, and the + /// operation requires a new channel, the SDK will try to use the given fee params. + pub async fn buy_bitcoin( &self, - req: SendSpontaneousPaymentRequest, - ) -> Result { - let payment_res = self - .node_api - .send_spontaneous_payment( - req.node_id.clone(), - req.amount_msat, - req.extra_tlvs, - req.label.clone(), - ) - .map_err(Into::into) - .await; - let payment = self - .on_payment_completed(req.node_id, None, req.label, payment_res) - .await?; - Ok(SendPaymentResponse { payment }) + req: BuyBitcoinRequest, + ) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.buy_bitcoin(req).await } - /// Second step of LNURL-pay. The first step is `parse()`, which also validates the LNURL destination - /// and generates the `LnUrlPayRequest` payload needed here. + /// Check whether given message was signed by the private key or the given + /// pubkey and the signature (zbase encoded) is valid. + pub async fn check_message(&self, req: CheckMessageRequest) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.check_message(req).await + } + + /// Claims an individual reverse swap. /// - /// This call will validate the `amount_msat` and `comment` parameters of `req` against the parameters - /// of the LNURL endpoint (`req_data`). If they match the endpoint requirements, the LNURL payment - /// is made. + /// To be used only in the context of mobile notifications, where the notification triggers + /// an individual reverse swap to be claimed. /// - /// This method will return an [anyhow::Error] when any validation check fails. - pub async fn lnurl_pay(&self, req: LnUrlPayRequest) -> Result { - match validate_lnurl_pay( - req.amount_msat, - &req.comment, - &req.data, - self.config.network, - req.validate_success_action_url, - ) - .await? - { - ValidatedCallbackResponse::EndpointError { data: e } => { - Ok(LnUrlPayResult::EndpointError { data: e }) - } - ValidatedCallbackResponse::EndpointSuccess { data: cb } => { - let pay_req = SendPaymentRequest { - bolt11: cb.pr.clone(), - amount_msat: None, - use_trampoline: req.use_trampoline, - label: req.payment_label, - }; - let invoice = parse_invoice(cb.pr.as_str())?; - - let payment = match self.send_payment(pay_req).await { - Ok(p) => Ok(p), - e @ Err( - SendPaymentError::InvalidInvoice { .. } - | SendPaymentError::ServiceConnectivity { .. }, - ) => e, - Err(e) => { - return Ok(LnUrlPayResult::PayError { - data: LnUrlPayErrorData { - payment_hash: invoice.payment_hash, - reason: e.to_string(), - }, - }) - } - }? - .payment; - let details = match &payment.details { - PaymentDetails::ClosedChannel { .. } => { - return Err(LnUrlPayError::Generic { - err: "Payment lookup found unexpected payment type".into(), - }); - } - PaymentDetails::Ln { data } => data, - }; - - let maybe_sa_processed: Option = match cb.success_action { - Some(sa) => { - let processed_sa = match sa { - // For AES, we decrypt the contents on the fly - SuccessAction::Aes { data } => { - let preimage = sha256::Hash::from_str(&details.payment_preimage)?; - let preimage_arr: [u8; 32] = preimage.into_inner(); - let result = match (data, &preimage_arr).try_into() { - Ok(data) => AesSuccessActionDataResult::Decrypted { data }, - Err(e) => AesSuccessActionDataResult::ErrorStatus { - reason: e.to_string(), - }, - }; - SuccessActionProcessed::Aes { result } - } - SuccessAction::Message { data } => { - SuccessActionProcessed::Message { data } - } - SuccessAction::Url { data } => SuccessActionProcessed::Url { data }, - }; - Some(processed_sa) - } - None => None, - }; - - let lnurl_pay_domain = match req.data.ln_address { - Some(_) => None, - None => Some(req.data.domain), - }; - // Store SA (if available) + LN Address in separate table, associated to payment_hash - self.persister.insert_payment_external_info( - &details.payment_hash, - PaymentExternalInfo { - lnurl_pay_success_action: maybe_sa_processed.clone(), - lnurl_pay_domain, - lnurl_pay_comment: req.comment, - lnurl_metadata: Some(req.data.metadata_str), - ln_address: req.data.ln_address, - lnurl_withdraw_endpoint: None, - attempted_amount_msat: invoice.amount_msat, - attempted_error: None, - }, - )?; - - Ok(LnUrlPayResult::EndpointSuccess { - data: lnurl::pay::LnUrlPaySuccessData { - payment, - success_action: maybe_sa_processed, - }, - }) - } - } + /// This is taken care of automatically in the context of typical SDK usage. + pub async fn claim_reverse_swap(&self, lockup_address: String) -> SdkResult<()> { + let services = Arc::clone(&*self.services.lock().await); + services.claim_reverse_swap(lockup_address).await } - /// Second step of LNURL-withdraw. The first step is `parse()`, which also validates the LNURL destination - /// and generates the `LnUrlWithdrawRequest` payload needed here. + /// Close all channels with the current LSP. /// - /// This call will validate the given `amount_msat` against the parameters - /// of the LNURL endpoint (`data`). If they match the endpoint requirements, the LNURL withdraw - /// request is made. A successful result here means the endpoint started the payment. - pub async fn lnurl_withdraw( - &self, - req: LnUrlWithdrawRequest, - ) -> Result { - let invoice = self - .receive_payment(ReceivePaymentRequest { - amount_msat: req.amount_msat, - description: req.description.unwrap_or_default(), - use_description_hash: Some(false), - ..Default::default() - }) - .await? - .ln_invoice; - - let lnurl_w_endpoint = req.data.callback.clone(); - let res = validate_lnurl_withdraw(req.data, invoice).await?; - - if let LnUrlWithdrawResult::Ok { ref data } = res { - // If endpoint was successfully called, store the LNURL-withdraw endpoint URL as metadata linked to the invoice - self.persister.insert_payment_external_info( - &data.invoice.payment_hash, - PaymentExternalInfo { - lnurl_pay_success_action: None, - lnurl_pay_domain: None, - lnurl_pay_comment: None, - lnurl_metadata: None, - ln_address: None, - lnurl_withdraw_endpoint: Some(lnurl_w_endpoint), - attempted_amount_msat: None, - attempted_error: None, - }, - )?; - } - - Ok(res) + /// Should be called when the user wants to close all the channels. + pub async fn close_lsp_channels(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.close_lsp_channels().await } - /// Third and last step of LNURL-auth. The first step is `parse()`, which also validates the LNURL destination - /// and generates the `LnUrlAuthRequestData` payload needed here. The second step is user approval of auth action. + /// Configure the node /// - /// This call will sign `k1` of the LNURL endpoint (`req_data`) on `secp256k1` using `linkingPrivKey` and DER-encodes the signature. - /// If they match the endpoint requirements, the LNURL auth request is made. A successful result here means the client signature is verified. - pub async fn lnurl_auth( - &self, - req_data: LnUrlAuthRequestData, - ) -> Result { - Ok(perform_lnurl_auth(&req_data, &SdkLnurlAuthSigner::new(self.node_api.clone())).await?) + /// This calls [NodeAPI::configure_node] to make changes to the active node's configuration. + /// Configuring the [ConfigureNodeRequest::close_to_address] only needs to be done one time + /// when registering the node or when the close to address need to be changed. Otherwise it is + /// stored by the node and used when neccessary. + pub async fn configure_node(&self, req: ConfigureNodeRequest) -> SdkResult<()> { + let services = self.services.lock().await; + services.configure_node(req).await } - /// Creates an bolt11 payment request. - /// This also works when the node doesn't have any channels and need inbound liquidity. - /// In such case when the invoice is paid a new zero-conf channel will be open by the LSP, - /// providing inbound liquidity and the payment will be routed via this new channel. - pub async fn receive_payment( - &self, - req: ReceivePaymentRequest, - ) -> Result { - self.payment_receiver.receive_payment(req).await + /// Select the LSP to be used and provide inbound liquidity + pub async fn connect_lsp(&self, lsp_id: String) -> SdkResult<()> { + let services = Arc::clone(&*self.services.lock().await); + services.connect_lsp(lsp_id).await } - /// Report an issue. + /// Trigger the stopping of BreezServices background threads for this instance. + pub async fn disconnect(&self) -> SdkResult<()> { + let services = self.services.lock().await; + services.disconnect().await + } + + /// Execute a command directly on the NodeAPI interface. + /// Mainly used to debugging. + pub async fn execute_dev_command(&self, command: String) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.execute_dev_command(command).await + } + + /// Fetch live rates of fiat currencies, sorted by name + pub async fn fetch_fiat_rates(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.fetch_fiat_rates().await + } + + /// Convenience method to look up [LspInformation] for a given LSP ID + pub async fn fetch_lsp_info(&self, id: String) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.fetch_lsp_info(id).await + } + + /// Lookup the reverse swap fees (see [ReverseSwapServiceAPI::fetch_reverse_swap_fees]). /// - /// Calling `report_issue` with a [ReportIssueRequest] enum param sends an issue report using the Support API. - /// - [ReportIssueRequest::PaymentFailure] sends a payment failure report to the Support API - /// using the provided `payment_hash` to lookup the failed payment and the current [NodeState]. - pub async fn report_issue(&self, req: ReportIssueRequest) -> SdkResult<()> { - match self.persister.get_node_state()? { - Some(node_state) => match req { - ReportIssueRequest::PaymentFailure { data } => { - let payment = self - .persister - .get_payment_by_hash(&data.payment_hash)? - .ok_or(SdkError::Generic { - err: "Payment not found".into(), - })?; - let lsp_id = self.persister.get_lsp_id()?; - - self.support_api - .report_payment_failure(node_state, payment, lsp_id, data.comment) - .await - } - }, - None => Err(SdkError::Generic { - err: "Node state not found".into(), - }), - } + /// If the request has the `send_amount_sat` set, the returned [ReverseSwapPairInfo] will have + /// the total estimated fees for the reverse swap in its `total_estimated_fees`. + /// + /// If, in addition to that, the request has the `claim_tx_feerate` set as well, then + /// - `fees_claim` will have the actual claim transaction fees, instead of an estimate, and + /// - `total_estimated_fees` will have the actual total fees for the given parameters + /// + /// ### Errors + /// + /// If a `send_amount_sat` is specified in the `req`, but is outside the `min` and `max`, + /// this will result in an error. If you are not sure what are the `min` and `max`, please call + /// this with `send_amount_sat` as `None` first, then repeat the call with the desired amount. + pub async fn fetch_reverse_swap_fees( + &self, + req: ReverseSwapFeesRequest, + ) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.fetch_reverse_swap_fees(req).await } - /// Retrieve the decrypted credentials from the node. - pub fn node_credentials(&self) -> SdkResult> { - Ok(self.node_api.node_credentials()?) + // Collects various user data from the node and the sdk storage. + // This is used for debugging and support purposes only. + pub async fn generate_diagnostic_data(&self) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.generate_diagnostic_data().await } - /// Retrieve the node state from the persistent storage. + /// Returns the blocking [ReverseSwapInfo]s that are in progress. /// - /// Fail if it could not be retrieved or if `None` was found. - pub fn node_info(&self) -> SdkResult { - self.persister.get_node_state()?.ok_or(SdkError::Generic { - err: "Node info not found".into(), - }) + /// Supersedes [BreezServices::in_progress_reverse_swaps] + pub async fn in_progress_onchain_payments(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.in_progress_onchain_payments().await } - /// Sign given message with the private key of the node id. Returns a zbase - /// encoded signature. - pub async fn sign_message(&self, req: SignMessageRequest) -> SdkResult { - let signature = self.node_api.sign_message(&req.message).await?; - Ok(SignMessageResponse { signature }) + /// Returns the blocking [ReverseSwapInfo]s that are in progress + #[deprecated(note = "use in_progress_onchain_payments instead")] + pub async fn in_progress_reverse_swaps(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + #[allow(deprecated)] + services.in_progress_reverse_swaps().await } - /// Check whether given message was signed by the private key or the given - /// pubkey and the signature (zbase encoded) is valid. - pub async fn check_message(&self, req: CheckMessageRequest) -> SdkResult { - let is_valid = self - .node_api - .check_message(&req.message, &req.pubkey, &req.signature) - .await?; - Ok(CheckMessageResponse { is_valid }) + /// Returns an optional in-progress [SwapInfo]. + /// A [SwapInfo] is in-progress if it is waiting for confirmation to be redeemed and complete the swap. + pub async fn in_progress_swap(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.in_progress_swap().await } - /// Retrieve the node up to date BackupStatus - pub fn backup_status(&self) -> SdkResult { - let backup_time = self.persister.get_last_backup_time()?; - let sync_request = self.persister.get_last_sync_request()?; - Ok(BackupStatus { - last_backup_time: backup_time, - backed_up: sync_request.is_none(), - }) + /// List all supported fiat currencies for which there is a known exchange rate. + /// List is sorted by the canonical name of the currency + pub async fn list_fiat_currencies(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.list_fiat_currencies().await } - /// Force running backup - pub async fn backup(&self) -> SdkResult<()> { - let (on_complete, mut on_complete_receiver) = mpsc::channel::>(1); - let req = BackupRequest::with(on_complete, true); - self.backup_watcher.request_backup(req).await?; - - match on_complete_receiver.recv().await { - Some(res) => res.map_err(|e| SdkError::Generic { - err: format!("Backup failed: {e}"), - }), - None => Err(SdkError::Generic { - err: "Backup process failed to complete".into(), - }), - } + /// List available LSPs that can be selected by the user + pub async fn list_lsps(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.list_lsps().await } /// List payments matching the given filters, as retrieved from persistent storage pub async fn list_payments(&self, req: ListPaymentsRequest) -> SdkResult> { - Ok(self.persister.list_payments(req)?) + let services = Arc::clone(&*self.services.lock().await); + services.list_payments(req).await } - /// Fetch a specific payment by its hash. - pub async fn payment_by_hash(&self, hash: String) -> SdkResult> { - Ok(self.persister.get_payment_by_hash(&hash)?) + /// list non-completed expired swaps that should be refunded by calling [BreezServices::refund] + pub async fn list_refundables(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.list_refundables().await } - /// Set the external metadata of a payment as a valid JSON string - pub async fn set_payment_metadata(&self, hash: String, metadata: String) -> SdkResult<()> { - Ok(self - .persister - .set_payment_external_metadata(hash, metadata)?) + /// Third and last step of LNURL-auth. The first step is `parse()`, which also validates the LNURL destination + /// and generates the `LnUrlAuthRequestData` payload needed here. The second step is user approval of auth action. + /// + /// This call will sign `k1` of the LNURL endpoint (`req_data`) on `secp256k1` using `linkingPrivKey` and DER-encodes the signature. + /// If they match the endpoint requirements, the LNURL auth request is made. A successful result here means the client signature is verified. + pub async fn lnurl_auth( + &self, + req_data: LnUrlAuthRequestData, + ) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.lnurl_auth(req_data).await } - /// Redeem on-chain funds from closed channels to the specified on-chain address, with the given feerate - pub async fn redeem_onchain_funds( - &self, - req: RedeemOnchainFundsRequest, - ) -> RedeemOnchainResult { - let txid = self - .node_api - .redeem_onchain_funds(req.to_address, req.sat_per_vbyte) - .await?; - self.sync().await?; - Ok(RedeemOnchainFundsResponse { txid }) + /// Second step of LNURL-pay. The first step is `parse()`, which also validates the LNURL destination + /// and generates the `LnUrlPayRequest` payload needed here. + /// + /// This call will validate the `amount_msat` and `comment` parameters of `req` against the parameters + /// of the LNURL endpoint (`req_data`). If they match the endpoint requirements, the LNURL payment + /// is made. + /// + /// This method will return an [anyhow::Error] when any validation check fails. + pub async fn lnurl_pay(&self, req: LnUrlPayRequest) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.lnurl_pay(req).await } - pub async fn prepare_redeem_onchain_funds( + /// Second step of LNURL-withdraw. The first step is `parse()`, which also validates the LNURL destination + /// and generates the `LnUrlWithdrawRequest` payload needed here. + /// + /// This call will validate the given `amount_msat` against the parameters + /// of the LNURL endpoint (`data`). If they match the endpoint requirements, the LNURL withdraw + /// request is made. A successful result here means the endpoint started the payment. + pub async fn lnurl_withdraw( &self, - req: PrepareRedeemOnchainFundsRequest, - ) -> RedeemOnchainResult { - let response = self.node_api.prepare_redeem_onchain_funds(req).await?; - Ok(response) + req: LnUrlWithdrawRequest, + ) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.lnurl_withdraw(req).await } - /// Fetch live rates of fiat currencies, sorted by name - pub async fn fetch_fiat_rates(&self) -> SdkResult> { - self.fiat_api.fetch_fiat_rates().await.map_err(Into::into) + /// Get the current LSP's ID + pub async fn lsp_id(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.lsp_id().await } - /// List all supported fiat currencies for which there is a known exchange rate. - /// List is sorted by the canonical name of the currency - pub async fn list_fiat_currencies(&self) -> SdkResult> { - self.fiat_api - .list_fiat_currencies() - .await - .map_err(Into::into) + /// Convenience method to look up LSP info based on current LSP ID + pub async fn lsp_info(&self) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.lsp_info().await } - /// List available LSPs that can be selected by the user - pub async fn list_lsps(&self) -> SdkResult> { - self.lsp_api.list_lsps(self.node_info()?.id).await + /// Returns the max amount that can be sent on-chain using the send_onchain method. + /// The returned amount is the sum of the max amount that can be sent on each channel + /// minus the expected fees. + /// This is possible since the route to the swapper node is known in advance and is expected + /// to consist of maximum 3 hops. + #[deprecated(note = "use onchain_payment_limits instead")] + pub async fn max_reverse_swap_amount(&self) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + #[allow(deprecated)] + services.max_reverse_swap_amount().await } - /// Select the LSP to be used and provide inbound liquidity - pub async fn connect_lsp(&self, lsp_id: String) -> SdkResult<()> { - let lsp_pubkey = match self.list_lsps().await?.iter().find(|lsp| lsp.id == lsp_id) { - Some(lsp) => lsp.pubkey.clone(), - None => { - return Err(SdkError::Generic { - err: format!("Unknown LSP: {lsp_id}"), - }) - } - }; - - self.persister.set_lsp(lsp_id, Some(lsp_pubkey))?; - self.sync().await?; - if let Some(webhook_url) = self.persister.get_webhook_url()? { - self.register_payment_notifications(webhook_url).await? - } - Ok(()) + /// Retrieve the decrypted credentials from the node. + pub async fn node_credentials(&self) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.node_credentials() } - /// Get the current LSP's ID - pub async fn lsp_id(&self) -> SdkResult> { - Ok(self.persister.get_lsp_id()?) + /// Retrieve the node state from the persistent storage. + /// + /// Fail if it could not be retrieved or if `None` was found. + pub async fn node_info(&self) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.node_info() } - /// Convenience method to look up [LspInformation] for a given LSP ID - pub async fn fetch_lsp_info(&self, id: String) -> SdkResult> { - get_lsp_by_id(self.persister.clone(), self.lsp_api.clone(), id.as_str()).await + pub async fn onchain_payment_limits(&self) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.onchain_payment_limits().await } /// Gets the fees required to open a channel for a given amount. @@ -768,35 +497,72 @@ impl BreezServices { &self, req: OpenChannelFeeRequest, ) -> SdkResult { - let lsp_info = self.lsp_info().await?; - let fee_params = lsp_info - .cheapest_open_channel_fee(req.expiry.unwrap_or(INVOICE_PAYMENT_FEE_EXPIRY_SECONDS))? - .clone(); - - let node_state = self.node_info()?; - let fee_msat = req.amount_msat.map(|req_amount_msat| { - match node_state.max_receivable_single_payment_amount_msat >= req_amount_msat { - // In case we have enough inbound liquidity we return zero fee. - true => 0, - // Otherwise we need to calculate the fee for opening a new channel. - false => fee_params.get_channel_fees_msat_for(req_amount_msat), - } - }); + let services = Arc::clone(&*self.services.lock().await); + services.open_channel_fee(req).await + } - Ok(OpenChannelFeeResponse { - fee_msat, - fee_params, - }) + /// Creates a reverse swap and attempts to pay the HODL invoice + /// + /// Supersedes [BreezServices::send_onchain] + pub async fn pay_onchain( + &self, + req: PayOnchainRequest, + ) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.pay_onchain(req).await } - /// Close all channels with the current LSP. + /// Fetch a specific payment by its hash. + pub async fn payment_by_hash(&self, hash: String) -> SdkResult> { + let services = Arc::clone(&*self.services.lock().await); + services.payment_by_hash(hash).await + } + + /// Supersedes [BreezServices::fetch_reverse_swap_fees] /// - /// Should be called when the user wants to close all the channels. - pub async fn close_lsp_channels(&self) -> SdkResult> { - let lsp = self.lsp_info().await?; - let tx_ids = self.node_api.close_peer_channels(lsp.pubkey).await?; - self.sync().await?; - Ok(tx_ids) + /// ### Errors + /// + /// - `OutOfRange`: This indicates the send amount is outside the range of minimum and maximum + /// values returned by [BreezServices::onchain_payment_limits]. When you get this error, please first call + /// [BreezServices::onchain_payment_limits] to get the new limits, before calling this method again. + pub async fn prepare_onchain_payment( + &self, + req: PrepareOnchainPaymentRequest, + ) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.prepare_onchain_payment(req).await + } + + pub async fn prepare_redeem_onchain_funds( + &self, + req: PrepareRedeemOnchainFundsRequest, + ) -> RedeemOnchainResult { + let services = Arc::clone(&*self.services.lock().await); + services.prepare_redeem_onchain_funds(req).await + } + + /// Prepares a refund transaction for a failed/expired swap. + /// + /// Can optionally be used before [BreezServices::refund] to know how much fees will be paid + /// to perform the refund. + pub async fn prepare_refund( + &self, + req: PrepareRefundRequest, + ) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.prepare_refund(req).await + } + + /// Creates an bolt11 payment request. + /// This also works when the node doesn't have any channels and need inbound liquidity. + /// In such case when the invoice is paid a new zero-conf channel will be open by the LSP, + /// providing inbound liquidity and the payment will be routed via this new channel. + pub async fn receive_payment( + &self, + req: ReceivePaymentRequest, + ) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.receive_payment(req).await } /// Onchain receive swap API @@ -814,50 +580,23 @@ impl BreezServices { &self, req: ReceiveOnchainRequest, ) -> ReceiveOnchainResult { - if let Some(in_progress) = self.in_progress_swap().await? { - return Err(ReceiveOnchainError::SwapInProgress{ err:format!( - "A swap was detected for address {}. Use in_progress_swap method to get the current swap state", - in_progress.bitcoin_address - )}); - } - let channel_opening_fees = req.opening_fee_params.unwrap_or( - self.lsp_info() - .await? - .cheapest_open_channel_fee(SWAP_PAYMENT_FEE_EXPIRY_SECONDS)? - .clone(), - ); - - let swap_info = self - .btc_receive_swapper - .create_swap_address(channel_opening_fees) - .await?; - if let Some(webhook_url) = self.persister.get_webhook_url()? { - let address = &swap_info.bitcoin_address; - info!("Registering for onchain tx notification for address {address}"); - self.register_onchain_tx_notification(address, &webhook_url) - .await?; - } - Ok(swap_info) + let services = Arc::clone(&*self.services.lock().await); + services.receive_onchain(req).await } - /// Returns an optional in-progress [SwapInfo]. - /// A [SwapInfo] is in-progress if it is waiting for confirmation to be redeemed and complete the swap. - pub async fn in_progress_swap(&self) -> SdkResult> { - let tip = self.chain_service.current_tip().await?; - self.btc_receive_swapper.rescan_monitored_swaps(tip).await?; - let in_progress = self.btc_receive_swapper.list_in_progress()?; - if !in_progress.is_empty() { - return Ok(Some(in_progress[0].clone())); - } - Ok(None) + /// Get the recommended fees for onchain transactions + pub async fn recommended_fees(&self) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.recommended_fees().await } - /// Iterate all historical swap addresses and fetch their current status from the blockchain. - /// The status is then updated in the persistent storage. - pub async fn rescan_swaps(&self) -> SdkResult<()> { - let tip = self.chain_service.current_tip().await?; - self.btc_receive_swapper.rescan_swaps(tip).await?; - Ok(()) + /// Redeem on-chain funds from closed channels to the specified on-chain address, with the given feerate + pub async fn redeem_onchain_funds( + &self, + req: RedeemOnchainFundsRequest, + ) -> RedeemOnchainResult { + let services = Arc::clone(&*self.services.lock().await); + services.redeem_onchain_funds(req).await } /// Redeems an individual swap. @@ -867,92 +606,48 @@ impl BreezServices { /// /// This is taken care of automatically in the context of typical SDK usage. pub async fn redeem_swap(&self, swap_address: String) -> SdkResult<()> { - let tip = self.chain_service.current_tip().await?; - self.btc_receive_swapper - .refresh_swap_on_chain_status(swap_address.clone(), tip) - .await?; - self.btc_receive_swapper.redeem_swap(swap_address).await?; - Ok(()) + let services = Arc::clone(&*self.services.lock().await); + services.redeem_swap(swap_address).await } - /// Claims an individual reverse swap. - /// - /// To be used only in the context of mobile notifications, where the notification triggers - /// an individual reverse swap to be claimed. + /// Construct and broadcast a refund transaction for a failed/expired swap /// - /// This is taken care of automatically in the context of typical SDK usage. - pub async fn claim_reverse_swap(&self, lockup_address: String) -> SdkResult<()> { - Ok(self - .btc_send_swapper - .claim_reverse_swap(lockup_address) - .await?) + /// Returns the txid of the refund transaction. + pub async fn refund(&self, req: RefundRequest) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.refund(req).await } - /// Lookup the reverse swap fees (see [ReverseSwapServiceAPI::fetch_reverse_swap_fees]). - /// - /// If the request has the `send_amount_sat` set, the returned [ReverseSwapPairInfo] will have - /// the total estimated fees for the reverse swap in its `total_estimated_fees`. - /// - /// If, in addition to that, the request has the `claim_tx_feerate` set as well, then - /// - `fees_claim` will have the actual claim transaction fees, instead of an estimate, and - /// - `total_estimated_fees` will have the actual total fees for the given parameters + /// Register for webhook callbacks at the given `webhook_url`. /// - /// ### Errors + /// More specifically, it registers for the following types of callbacks: + /// - a payment is received + /// - a swap tx is confirmed /// - /// If a `send_amount_sat` is specified in the `req`, but is outside the `min` and `max`, - /// this will result in an error. If you are not sure what are the `min` and `max`, please call - /// this with `send_amount_sat` as `None` first, then repeat the call with the desired amount. - pub async fn fetch_reverse_swap_fees( - &self, - req: ReverseSwapFeesRequest, - ) -> SdkResult { - let mut res = self.btc_send_swapper.fetch_reverse_swap_fees().await?; - - if let Some(amt) = req.send_amount_sat { - ensure_sdk!(amt <= res.max, SdkError::generic("Send amount is too high")); - ensure_sdk!(amt >= res.min, SdkError::generic("Send amount is too low")); - - if let Some(claim_tx_feerate) = req.claim_tx_feerate { - res.fees_claim = BTCSendSwap::calculate_claim_tx_fee(claim_tx_feerate)?; - } - - let service_fee_sat = swap_out::get_service_fee_sat(amt, res.fees_percentage); - res.total_fees = Some(service_fee_sat + res.fees_lockup + res.fees_claim); - } - - Ok(res) + /// This method should be called every time the application is started and when the `webhook_url` changes. + /// For example, if the `webhook_url` contains a push notification token and the token changes after + /// the application was started, then this method should be called to register for callbacks at + /// the new correct `webhook_url`. To unregister a webhook call [BreezServices::unregister_webhook]. + pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> { + let services = Arc::clone(&*self.services.lock().await); + services.register_webhook(webhook_url).await } - /// Returns the max amount that can be sent on-chain using the send_onchain method. - /// The returned amount is the sum of the max amount that can be sent on each channel - /// minus the expected fees. - /// This is possible since the route to the swapper node is known in advance and is expected - /// to consist of maximum 3 hops. - #[deprecated(note = "use onchain_payment_limits instead")] - pub async fn max_reverse_swap_amount(&self) -> SdkResult { - // fetch the last hop hints from the swapper - let last_hop = self.btc_send_swapper.last_hop_for_payment().await?; - info!("max_reverse_swap_amount last_hop={:?}", last_hop); - // calculate the largest payment we can send over this route using maximum 3 hops - // as follows: - // User Node -> LSP Node -> Routing Node -> Swapper Node - let max_to_pay = self - .node_api - .max_sendable_amount( - Some( - hex::decode(&last_hop.src_node_id).map_err(|e| SdkError::Generic { - err: format!("Failed to decode hex node_id: {e}"), - })?, - ), - swap_out::reverseswap::MAX_PAYMENT_PATH_HOPS, - Some(&last_hop), - ) - .await?; + /// Report an issue. + /// + /// Calling `report_issue` with a [ReportIssueRequest] enum param sends an issue report using the Support API. + /// - [ReportIssueRequest::PaymentFailure] sends a payment failure report to the Support API + /// using the provided `payment_hash` to lookup the failed payment and the current [NodeState]. + pub async fn report_issue(&self, req: ReportIssueRequest) -> SdkResult<()> { + let services = Arc::clone(&*self.services.lock().await); + services.report_issue(req).await + } - // Sum the max amount per channel and return the result - let total_msat: u64 = max_to_pay.into_iter().map(|m| m.amount_msat).sum(); - let total_sat = total_msat / 1000; - Ok(MaxReverseSwapAmountResponse { total_sat }) + /// Iterate all historical swap addresses and fetch their current status from the blockchain. + /// The status is then updated in the persistent storage. + pub async fn rescan_swaps(&self) -> SdkResult<()> { + let services = Arc::clone(&*self.services.lock().await); + services.rescan_swaps().await } /// Creates a reverse swap and attempts to pay the HODL invoice @@ -961,1043 +656,53 @@ impl BreezServices { &self, req: SendOnchainRequest, ) -> Result { - let reverse_swap_info = self - .pay_onchain_common(CreateReverseSwapArg::V1(req)) - .await?; - Ok(SendOnchainResponse { reverse_swap_info }) - } - - /// Returns the blocking [ReverseSwapInfo]s that are in progress - #[deprecated(note = "use in_progress_onchain_payments instead")] - pub async fn in_progress_reverse_swaps(&self) -> SdkResult> { - let full_rsis = self.btc_send_swapper.list_blocking().await?; - - let mut rsis = vec![]; - for full_rsi in full_rsis { - let rsi = self - .btc_send_swapper - .convert_reverse_swap_info(full_rsi) - .await?; - rsis.push(rsi); - } - - Ok(rsis) - } - - /// list non-completed expired swaps that should be refunded by calling [BreezServices::refund] - pub async fn list_refundables(&self) -> SdkResult> { - Ok(self.btc_receive_swapper.list_refundables()?) - } - - /// Prepares a refund transaction for a failed/expired swap. - /// - /// Can optionally be used before [BreezServices::refund] to know how much fees will be paid - /// to perform the refund. - pub async fn prepare_refund( - &self, - req: PrepareRefundRequest, - ) -> SdkResult { - Ok(self.btc_receive_swapper.prepare_refund_swap(req).await?) - } - - /// Construct and broadcast a refund transaction for a failed/expired swap - /// - /// Returns the txid of the refund transaction. - pub async fn refund(&self, req: RefundRequest) -> SdkResult { - Ok(self.btc_receive_swapper.refund_swap(req).await?) - } - - pub async fn onchain_payment_limits(&self) -> SdkResult { - let fee_info = self.btc_send_swapper.fetch_reverse_swap_fees().await?; - debug!("Reverse swap pair info: {fee_info:?}"); + let services = Arc::clone(&*self.services.lock().await); #[allow(deprecated)] - let max_amt_current_channels = self.max_reverse_swap_amount().await?; - debug!("Max send amount possible with current channels: {max_amt_current_channels:?}"); - - Ok(OnchainPaymentLimitsResponse { - min_sat: fee_info.min, - max_sat: fee_info.max, - max_payable_sat: max_amt_current_channels.total_sat, - }) + services.send_onchain(req).await } - /// Supersedes [BreezServices::fetch_reverse_swap_fees] - /// - /// ### Errors - /// - /// - `OutOfRange`: This indicates the send amount is outside the range of minimum and maximum - /// values returned by [BreezServices::onchain_payment_limits]. When you get this error, please first call - /// [BreezServices::onchain_payment_limits] to get the new limits, before calling this method again. - pub async fn prepare_onchain_payment( - &self, - req: PrepareOnchainPaymentRequest, - ) -> Result { - let fees_claim = BTCSendSwap::calculate_claim_tx_fee(req.claim_tx_feerate)?; - BTCSendSwap::validate_claim_tx_fee(fees_claim)?; - - let fee_info = self.btc_send_swapper.fetch_reverse_swap_fees().await?; - - // Calculate (send_amt, recv_amt) from the inputs and fees - let fees_lockup = fee_info.fees_lockup; - let p = fee_info.fees_percentage; - let fees_claim = BTCSendSwap::calculate_claim_tx_fee(req.claim_tx_feerate)?; - let (send_amt, recv_amt) = match req.amount_type { - SwapAmountType::Send => { - let temp_send_amt = req.amount_sat; - let service_fees = swap_out::get_service_fee_sat(temp_send_amt, p); - let total_fees = service_fees + fees_lockup + fees_claim; - ensure_sdk!( - temp_send_amt > total_fees, - SendOnchainError::generic( - "Send amount is not high enough to account for all fees" - ) - ); - - (temp_send_amt, temp_send_amt - total_fees) - } - SwapAmountType::Receive => { - let temp_recv_amt = req.amount_sat; - let send_amt_minus_service_fee = temp_recv_amt + fees_lockup + fees_claim; - let temp_send_amt = swap_out::get_invoice_amount_sat(send_amt_minus_service_fee, p); - - (temp_send_amt, temp_recv_amt) - } - }; - - let is_send_in_range = send_amt >= fee_info.min && send_amt <= fee_info.max; - ensure_sdk!(is_send_in_range, SendOnchainError::OutOfRange); - - Ok(PrepareOnchainPaymentResponse { - fees_hash: fee_info.fees_hash.clone(), - fees_percentage: p, - fees_lockup, - fees_claim, - sender_amount_sat: send_amt, - recipient_amount_sat: recv_amt, - total_fees: send_amt - recv_amt, - }) - } - - /// Creates a reverse swap and attempts to pay the HODL invoice - /// - /// Supersedes [BreezServices::send_onchain] - pub async fn pay_onchain( - &self, - req: PayOnchainRequest, - ) -> Result { - ensure_sdk!( - req.prepare_res.sender_amount_sat > req.prepare_res.recipient_amount_sat, - SendOnchainError::generic("Send amount must be bigger than receive amount") - ); - - let reverse_swap_info = self - .pay_onchain_common(CreateReverseSwapArg::V2(req)) - .await?; - Ok(PayOnchainResponse { reverse_swap_info }) - } - - async fn pay_onchain_common(&self, req: CreateReverseSwapArg) -> SdkResult { - ensure_sdk!(self.in_progress_onchain_payments().await?.is_empty(), SdkError::Generic { err: - "You can only start a new one after after the ongoing ones finish. \ - Use the in_progress_reverse_swaps method to get an overview of currently ongoing reverse swaps".into(), - }); - - let full_rsi = self.btc_send_swapper.create_reverse_swap(req).await?; - let reverse_swap_info = self - .btc_send_swapper - .convert_reverse_swap_info(full_rsi.clone()) - .await?; - self.do_sync(false).await?; - - if let Some(webhook_url) = self.persister.get_webhook_url()? { - let address = &full_rsi - .get_lockup_address(self.config.network)? - .to_string(); - info!("Registering for onchain tx notification for address {address}"); - self.register_onchain_tx_notification(address, &webhook_url) - .await?; - } - Ok(reverse_swap_info) - } - - /// Returns the blocking [ReverseSwapInfo]s that are in progress. - /// - /// Supersedes [BreezServices::in_progress_reverse_swaps] - pub async fn in_progress_onchain_payments(&self) -> SdkResult> { - #[allow(deprecated)] - self.in_progress_reverse_swaps().await - } - - /// Execute a command directly on the NodeAPI interface. - /// Mainly used to debugging. - pub async fn execute_dev_command(&self, command: String) -> SdkResult { - let dev_cmd_res = DevCommand::from_str(&command); - - match dev_cmd_res { - Ok(dev_cmd) => match dev_cmd { - DevCommand::GenerateDiagnosticData => self.generate_diagnostic_data().await, - }, - Err(_) => Ok(crate::serializer::to_string_pretty( - &self.node_api.execute_command(command).await?, - )?), - } - } - - // Collects various user data from the node and the sdk storage. - // This is used for debugging and support purposes only. - pub async fn generate_diagnostic_data(&self) -> SdkResult { - let now_sec = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or_default(); - let node_data = self - .node_api - .generate_diagnostic_data() - .await - .unwrap_or_else(|e| json!({"error": e.to_string()})); - let sdk_data = self - .generate_sdk_diagnostic_data() - .await - .unwrap_or_else(|e| json!({"error": e.to_string()})); - let result = json!({ - "timestamp": now_sec, - "node": node_data, - "sdk": sdk_data - }); - Ok(crate::serializer::to_string_pretty(&result)?) - } - - /// This method sync the local state with the remote node state. - /// The synced items are as follows: - /// * node state - General information about the node and its liquidity status - /// * channels - The list of channels and their status - /// * payments - The incoming/outgoing payments - pub async fn sync(&self) -> SdkResult<()> { - Ok(self.do_sync(false).await?) - } - - async fn do_sync(&self, match_local_balance: bool) -> Result<()> { - let start = Instant::now(); - let node_pubkey = self.node_api.node_id().await?; - self.connect_lsp_peer(node_pubkey).await?; - - // First query the changes since last sync state. - let sync_state = self.persister.get_sync_state()?; - let new_data = &self - .node_api - .pull_changed(sync_state.clone(), match_local_balance) - .await?; - - debug!( - "pull changed old state={:?} new state={:?}", - sync_state, new_data.sync_state - ); - - // update node state and channels state - self.persister.set_node_state(&new_data.node_state)?; - - let channels_before_update = self.persister.list_channels()?; - self.persister.update_channels(&new_data.channels)?; - let channels_after_update = self.persister.list_channels()?; - - // Fetch the static backup if needed and persist it - if channels_before_update.len() != channels_after_update.len() { - info!("fetching static backup file from node"); - let backup = self.node_api.static_backup().await?; - self.persister.set_static_backup(backup)?; - } - - //fetch closed_channel and convert them to Payment items. - let mut closed_channel_payments: Vec = vec![]; - for closed_channel in - self.persister.list_channels()?.into_iter().filter(|c| { - c.state == ChannelState::Closed || c.state == ChannelState::PendingClose - }) - { - let closed_channel_tx = self.closed_channel_to_transaction(closed_channel).await?; - closed_channel_payments.push(closed_channel_tx); - } - - // update both closed channels and lightning transaction payments - let mut payments = closed_channel_payments; - payments.extend(new_data.payments.clone()); - self.persister.insert_or_update_payments(&payments)?; - let duration = start.elapsed(); - info!("Sync duration: {:?}", duration); - - // update the cached sync state - self.persister.set_sync_state(&new_data.sync_state)?; - self.notify_event_listeners(BreezEvent::Synced).await?; - Ok(()) - } - - /// Connects to the selected LSP peer. - /// This validates if the selected LSP is still in [`list_lsps`]. - /// If not or no LSP is selected, it selects the first LSP in [`list_lsps`]. - async fn connect_lsp_peer(&self, node_pubkey: String) -> SdkResult<()> { - let lsps = self.lsp_api.list_lsps(node_pubkey).await?; - let lsp = match self - .persister - .get_lsp_id()? - .and_then(|lsp_id| lsps.iter().find(|lsp| lsp.id == lsp_id)) - .or_else(|| lsps.first()) - { - Some(lsp) => lsp.clone(), - None => return Ok(()), - }; - - self.persister.set_lsp(lsp.id, Some(lsp.pubkey.clone()))?; - let node_state = match self.node_info() { - Ok(node_state) => node_state, - Err(_) => return Ok(()), - }; - - let node_id = lsp.pubkey; - let address = lsp.host; - let lsp_connected = node_state - .connected_peers - .iter() - .any(|e| e == node_id.as_str()); - if !lsp_connected { - debug!("connecting to lsp {}@{}", node_id.clone(), address.clone()); - self.node_api - .connect_peer(node_id.clone(), address.clone()) - .await - .map_err(|e| SdkError::ServiceConnectivity { - err: format!("(LSP: {node_id}) Failed to connect: {e}"), - })?; - debug!("connected to lsp {node_id}@{address}"); - } - - Ok(()) - } - - fn persist_pending_payment( - &self, - invoice: &LNInvoice, - amount_msat: u64, - label: Option, - ) -> Result<(), SendPaymentError> { - self.persister.insert_or_update_payments(&[Payment { - id: invoice.payment_hash.clone(), - payment_type: PaymentType::Sent, - payment_time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64, - amount_msat, - fee_msat: 0, - status: PaymentStatus::Pending, - error: None, - description: invoice.description.clone(), - details: PaymentDetails::Ln { - data: LnPaymentDetails { - payment_hash: invoice.payment_hash.clone(), - label: label.unwrap_or_default(), - destination_pubkey: invoice.payee_pubkey.clone(), - payment_preimage: String::new(), - keysend: false, - bolt11: invoice.bolt11.clone(), - lnurl_success_action: None, - lnurl_pay_domain: None, - lnurl_pay_comment: None, - ln_address: None, - lnurl_metadata: None, - lnurl_withdraw_endpoint: None, - swap_info: None, - reverse_swap_info: None, - pending_expiration_block: None, - open_channel_bolt11: None, - }, - }, - metadata: None, - }])?; - - self.persister.insert_payment_external_info( - &invoice.payment_hash, - PaymentExternalInfo { - lnurl_pay_success_action: None, - lnurl_pay_domain: None, - lnurl_pay_comment: None, - lnurl_metadata: None, - ln_address: None, - lnurl_withdraw_endpoint: None, - attempted_amount_msat: invoice.amount_msat.map_or(Some(amount_msat), |_| None), - attempted_error: None, - }, - )?; - Ok(()) - } - - async fn on_payment_completed( - &self, - node_id: String, - invoice: Option, - label: Option, - payment_res: Result, - ) -> Result { - self.do_sync(false).await?; - match payment_res { - Ok(payment) => { - self.notify_event_listeners(BreezEvent::PaymentSucceed { - details: payment.clone(), - }) - .await?; - Ok(payment) - } - Err(e) => { - if let Some(invoice) = invoice.clone() { - self.persister.update_payment_attempted_error( - &invoice.payment_hash, - Some(e.to_string()), - )?; - } - self.notify_event_listeners(BreezEvent::PaymentFailed { - details: PaymentFailedData { - error: e.to_string(), - node_id, - invoice, - label, - }, - }) - .await?; - Err(e) - } - } - } - - async fn on_event(&self, e: BreezEvent) -> Result<()> { - debug!("breez services got event {:?}", e); - self.notify_event_listeners(e.clone()).await - } - - async fn notify_event_listeners(&self, e: BreezEvent) -> Result<()> { - if let Err(err) = self.btc_receive_swapper.on_event(e.clone()).await { - debug!( - "btc_receive_swapper failed to process event {:?}: {:?}", - e, err - ) - }; - if let Err(err) = self.btc_send_swapper.on_event(e.clone()).await { - debug!( - "btc_send_swapper failed to process event {:?}: {:?}", - e, err - ) - }; - - if self.event_listener.is_some() { - self.event_listener.as_ref().unwrap().on_event(e.clone()) - } - Ok(()) - } - - /// Convenience method to look up LSP info based on current LSP ID - pub async fn lsp_info(&self) -> SdkResult { - get_lsp(self.persister.clone(), self.lsp_api.clone()).await - } - - /// Get the recommended fees for onchain transactions - pub async fn recommended_fees(&self) -> SdkResult { - self.chain_service.recommended_fees().await - } - - /// Get the full default config for a specific environment type - pub fn default_config( - env_type: EnvironmentType, - api_key: String, - node_config: NodeConfig, - ) -> Config { - match env_type { - EnvironmentType::Production => Config::production(api_key, node_config), - EnvironmentType::Staging => Config::staging(api_key, node_config), - } - } - - /// Get the static backup data from the persistent storage. - /// This data enables the user to recover the node in an external core ligntning node. - /// See here for instructions on how to recover using this data: - pub fn static_backup(req: StaticBackupRequest) -> SdkResult { - let storage = SqliteStorage::new(req.working_dir); - Ok(StaticBackupResponse { - backup: storage.get_static_backup()?, - }) - } - - /// Fetches the service health check from the support API. - pub async fn service_health_check(api_key: String) -> SdkResult { - let support_api: Arc = Arc::new(BreezServer::new( - PRODUCTION_BREEZSERVER_URL.to_string(), - Some(api_key), - )?); - - support_api.service_health_check().await - } - - /// Generates an url that can be used by a third part provider to buy Bitcoin with fiat currency. - /// - /// A user-selected [OpeningFeeParams] can be optionally set in the argument. If set, and the - /// operation requires a new channel, the SDK will try to use the given fee params. - pub async fn buy_bitcoin( - &self, - req: BuyBitcoinRequest, - ) -> Result { - let swap_info = self - .receive_onchain(ReceiveOnchainRequest { - opening_fee_params: req.opening_fee_params, - }) - .await?; - let url = self - .buy_bitcoin_api - .buy_bitcoin(req.provider, &swap_info, req.redirect_url) - .await?; - - Ok(BuyBitcoinResponse { - url, - opening_fee_params: swap_info.channel_opening_fees, - }) - } - - /// Starts the BreezServices background threads. - /// - /// Internal method. Should only be used as part of [BreezServices::start] - async fn start_background_tasks(self: &Arc) -> SdkResult<()> { - // start the signer - let (shutdown_signer_sender, signer_signer_receiver) = mpsc::channel(1); - self.start_signer(signer_signer_receiver).await; - self.start_node_keep_alive(self.shutdown_receiver.clone()) - .await; - - // Sync node state - match self.persister.get_node_state()? { - Some(node) => { - info!("Starting existing node {}", node.id); - self.connect_lsp_peer(node.id).await?; - } - None => { - // In case it is a first run we sync in foreground to get the node state. - info!("First run, syncing in foreground"); - self.sync().await?; - info!("First run, finished running syncing in foreground"); - } - } - - // start backup watcher - self.start_backup_watcher().await?; - - //track backup events - self.track_backup_events().await; - - //track swap events - self.track_swap_events().await; - - // track paid invoices - self.track_invoices().await; - - // track new blocks - self.track_new_blocks().await; - - // track logs - self.track_logs().await; - - // Stop signer on shutdown - let mut shutdown_receiver = self.shutdown_receiver.clone(); - tokio::spawn(async move { - // start the backup watcher - _ = shutdown_receiver.changed().await; - _ = shutdown_signer_sender.send(()).await; - debug!("Received the signal to exit event polling loop"); - }); - - self.init_chainservice_urls().await?; - - Ok(()) - } - - async fn start_signer(self: &Arc, shutdown_receiver: mpsc::Receiver<()>) { - let signer_api = self.clone(); - tokio::spawn(async move { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - signer_api.node_api.start_signer(shutdown_receiver).await; - }); - } - - async fn start_node_keep_alive( - self: &Arc, - shutdown_receiver: watch::Receiver<()>, - ) { - let cloned = self.clone(); - tokio::spawn(async move { - cloned.node_api.start_keep_alive(shutdown_receiver).await; - }); - } - - async fn start_backup_watcher(self: &Arc) -> Result<()> { - self.backup_watcher - .start(self.shutdown_receiver.clone()) - .await - .map_err(|e| anyhow!("Failed to start backup watcher: {e}"))?; - - // Restore backup state and request backup on start if needed - let force_backup = self - .persister - .get_last_sync_version() - .map_err(|e| anyhow!("Failed to read last sync version: {e}"))? - .is_none(); - self.backup_watcher - .request_backup(BackupRequest::new(force_backup)) - .await - .map_err(|e| anyhow!("Failed to request backup: {e}")) - } - - async fn track_backup_events(self: &Arc) { - let cloned = self.clone(); - tokio::spawn(async move { - let mut events_stream = cloned.backup_watcher.subscribe_events(); - let mut shutdown_receiver = cloned.shutdown_receiver.clone(); - loop { - tokio::select! { - backup_event = events_stream.recv() => { - if let Ok(e) = backup_event { - if let Err(err) = cloned.notify_event_listeners(e).await { - error!("error handling backup event: {:?}", err); - } - } - let backup_status = cloned.backup_status(); - info!("backup status: {:?}", backup_status); - }, - _ = shutdown_receiver.changed() => { - debug!("Backup watcher task completed"); - break; - } - } - } - }); - } - - async fn track_swap_events(self: &Arc) { - let cloned = self.clone(); - tokio::spawn(async move { - let mut swap_events_stream = cloned.btc_receive_swapper.subscribe_status_changes(); - let mut rev_swap_events_stream = cloned.btc_send_swapper.subscribe_status_changes(); - let mut shutdown_receiver = cloned.shutdown_receiver.clone(); - loop { - tokio::select! { - swap_event = swap_events_stream.recv() => { - if let Ok(e) = swap_event { - if let Err(err) = cloned.notify_event_listeners(e).await { - error!("error handling swap event: {:?}", err); - } - } - }, - rev_swap_event = rev_swap_events_stream.recv() => { - if let Ok(e) = rev_swap_event { - if let Err(err) = cloned.notify_event_listeners(e).await { - error!("error handling reverse swap event: {:?}", err); - } - } - }, - _ = shutdown_receiver.changed() => { - debug!("Swap events handling task completed"); - break; - } - } - } - }); - } - - async fn track_invoices(self: &Arc) { - let cloned = self.clone(); - tokio::spawn(async move { - let mut shutdown_receiver = cloned.shutdown_receiver.clone(); - loop { - if shutdown_receiver.has_changed().unwrap_or(true) { - return; - } - let invoice_stream_res = cloned.node_api.stream_incoming_payments().await; - if let Ok(mut invoice_stream) = invoice_stream_res { - loop { - tokio::select! { - paid_invoice_res = invoice_stream.message() => { - match paid_invoice_res { - Ok(Some(i)) => { - debug!("invoice stream got new invoice"); - if let Some(gl_client::signer::model::greenlight::incoming_payment::Details::Offchain(p)) = i.details { - let mut payment: Option = p.clone().try_into().ok(); - if let Some(ref p) = payment { - let res = cloned - .persister - .insert_or_update_payments(&vec![p.clone()]); - debug!("paid invoice was added to payments list {res:?}"); - if let Ok(Some(mut node_info)) = cloned.persister.get_node_state() { - node_info.channels_balance_msat += p.amount_msat; - let res = cloned.persister.set_node_state(&node_info); - debug!("channel balance was updated {res:?}"); - } - payment = cloned.persister.get_payment_by_hash(&p.id).unwrap_or(payment); - } - _ = cloned.on_event(BreezEvent::InvoicePaid { - details: InvoicePaidDetails { - payment_hash: hex::encode(p.payment_hash), - bolt11: p.bolt11, - payment, - }, - }).await; - if let Err(e) = cloned.do_sync(true).await { - error!("failed to sync after paid invoice: {:?}", e); - } - } - } - Ok(None) => { - debug!("invoice stream got None"); - break; - } - Err(err) => { - debug!("invoice stream got error: {:?}", err); - break; - } - } - } - - _ = shutdown_receiver.changed() => { - debug!("Invoice tracking task has completed"); - return; - } - } - } - } - sleep(Duration::from_secs(1)).await; - } - }); - } - - async fn track_logs(self: &Arc) { - let cloned = self.clone(); - tokio::spawn(async move { - let mut shutdown_receiver = cloned.shutdown_receiver.clone(); - loop { - if shutdown_receiver.has_changed().unwrap_or(true) { - return; - } - let log_stream_res = cloned.node_api.stream_log_messages().await; - if let Ok(mut log_stream) = log_stream_res { - loop { - tokio::select! { - log_message_res = log_stream.message() => { - match log_message_res { - Ok(Some(l)) => { - info!("node-logs: {}", l.line); - }, - // stream is closed, renew it - Ok(None) => { - break; - } - Err(err) => { - debug!("failed to process log entry {:?}", err); - break; - } - }; - } - - _ = shutdown_receiver.changed() => { - debug!("Track logs task has completed"); - return; - } - } - } - } - sleep(Duration::from_secs(1)).await; - } - }); - } - - async fn track_new_blocks(self: &Arc) { - let cloned = self.clone(); - tokio::spawn(async move { - let mut current_block: u32 = 0; - let mut shutdown_receiver = cloned.shutdown_receiver.clone(); - let mut interval = tokio::time::interval(Duration::from_secs(30)); - interval.set_missed_tick_behavior(MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = interval.tick() => { - let tip_res = cloned.chain_service.current_tip().await; - match tip_res { - Ok(next_block) => { - debug!("got tip {:?}", next_block); - if next_block > current_block { - _ = cloned.sync().await; - _ = cloned.on_event(BreezEvent::NewBlock{block: next_block}).await; - } - current_block = next_block - }, - Err(e) => { - error!("failed to fetch next block {}", e) - } - }; - } - - _ = shutdown_receiver.changed() => { - debug!("New blocks task has completed"); - return; - } - } - } - }); - } - - async fn init_chainservice_urls(&self) -> Result<()> { - let breez_server = Arc::new(BreezServer::new( - PRODUCTION_BREEZSERVER_URL.to_string(), - None, - )?); - let persister = &self.persister; - - let cloned_breez_server = breez_server.clone(); - let cloned_persister = persister.clone(); - tokio::spawn(async move { - match cloned_breez_server.fetch_mempoolspace_urls().await { - Ok(fresh_urls) => { - if let Err(e) = cloned_persister.set_mempoolspace_base_urls(fresh_urls) { - error!("Failed to cache mempool.space URLs: {e}"); - } - } - Err(e) => error!("Failed to fetch mempool.space URLs: {e}"), - } - }); - - Ok(()) - } - - /// Configures a global SDK logger that will log to file and will forward log events to - /// an optional application-specific logger. - /// - /// If called, it should be called before any SDK methods (for example, before `connect`). - /// - /// It must be called only once in the application lifecycle. Alternatively, If the application - /// already uses a globally-registered logger, this method shouldn't be called at all. - /// - /// ### Arguments - /// - /// - `log_dir`: Location where the the SDK log file will be created. The directory must already exist. - /// - /// - `app_logger`: Optional application logger. - /// - /// If the application is to use it's own logger, but would also like the SDK to log SDK-specific - /// log output to a file in the configured `log_dir`, then do not register the - /// app-specific logger as a global logger and instead call this method with the app logger as an arg. - /// - /// ### Logging Configuration - /// - /// Setting `breez_sdk_core::input_parser=debug` will include in the logs the raw payloads received - /// when interacting with JSON endpoints, for example those used during all LNURL workflows. - /// - /// ### Errors - /// - /// An error is thrown if the log file cannot be created in the working directory. - /// - /// An error is thrown if a global logger is already configured. - pub fn init_logging(log_dir: &str, app_logger: Option>) -> Result<()> { - let target_log_file = Box::new( - OpenOptions::new() - .create(true) - .append(true) - .open(format!("{log_dir}/sdk.log")) - .map_err(|e| anyhow!("Can't create log file: {e}"))?, - ); - let logger = env_logger::Builder::new() - .target(env_logger::Target::Pipe(target_log_file)) - .parse_filters( - r#" - debug, - breez_sdk_core::input_parser=warn, - breez_sdk_core::backup=info, - breez_sdk_core::persist::reverseswap=info, - breez_sdk_core::reverseswap=info, - gl_client=debug, - h2=warn, - hyper=warn, - lightning_signer=warn, - reqwest=warn, - rustls=warn, - rustyline=warn, - vls_protocol_signer=warn - "#, - ) - .format(|buf, record| { - writeln!( - buf, - "[{} {} {}:{}] {}", - Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), - record.level(), - record.module_path().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.args() - ) - }) - .build(); - - let global_logger = GlobalSdkLogger { - logger, - log_listener: app_logger, - }; - - log::set_boxed_logger(Box::new(global_logger)) - .map_err(|e| anyhow!("Failed to set global logger: {e}"))?; - log::set_max_level(LevelFilter::Trace); - - Ok(()) - } - - async fn lookup_chain_service_closing_outspend( - &self, - channel: crate::models::Channel, - ) -> Result> { - match channel.funding_outnum { - None => Ok(None), - Some(outnum) => { - // Find the output tx that was used to fund the channel - let outspends = self - .chain_service - .transaction_outspends(channel.funding_txid.clone()) - .await?; - - Ok(outspends.get(outnum as usize).cloned()) - } - } - } - - /// Chain service lookup of relevant channel closing fields (closed_at, closing_txid). + /// Pay a bolt11 invoice /// - /// Should be used sparingly because it involves a network lookup. - async fn lookup_channel_closing_data( - &self, - channel: &crate::models::Channel, - ) -> Result<(Option, Option)> { - let maybe_outspend_res = self - .lookup_chain_service_closing_outspend(channel.clone()) - .await; - let maybe_outspend: Option = match maybe_outspend_res { - Ok(s) => s, - Err(e) => { - error!("Failed to lookup channel closing data: {:?}", e); - None - } - }; - - let maybe_closed_at = maybe_outspend - .clone() - .and_then(|outspend| outspend.status) - .and_then(|s| s.block_time); - let maybe_closing_txid = maybe_outspend.and_then(|outspend| outspend.txid); - - Ok((maybe_closed_at, maybe_closing_txid)) + /// Calling `send_payment` ensures that the payment is not already completed, if so it will result in an error. + /// If the invoice doesn't specify an amount, the amount is taken from the `amount_msat` arg. + pub async fn send_payment( + &self, + req: SendPaymentRequest, + ) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.send_payment(req).await } - async fn closed_channel_to_transaction( + /// Pay directly to a node id using keysend + pub async fn send_spontaneous_payment( &self, - channel: crate::models::Channel, - ) -> Result { - let (payment_time, closing_txid) = match (channel.closed_at, channel.closing_txid.clone()) { - (Some(closed_at), Some(closing_txid)) => (closed_at as i64, Some(closing_txid)), - (_, _) => { - // If any of the two closing-related fields are empty, we look them up and persist them - let (maybe_closed_at, maybe_closing_txid) = - self.lookup_channel_closing_data(&channel).await?; - - let processed_closed_at = match maybe_closed_at { - None => { - warn!("Blocktime could not be determined for from closing outspend, defaulting closed_at to epoch time"); - SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() - } - Some(block_time) => block_time, - }; - - let mut updated_channel = channel.clone(); - updated_channel.closed_at = Some(processed_closed_at); - // If no closing txid found, we persist it as None, so it will be looked-up next time - updated_channel.closing_txid.clone_from(&maybe_closing_txid); - self.persister.insert_or_update_channel(updated_channel)?; - - (processed_closed_at as i64, maybe_closing_txid) - } - }; - - Ok(Payment { - id: channel.funding_txid.clone(), - payment_type: PaymentType::ClosedChannel, - payment_time, - amount_msat: channel.local_balance_msat, - fee_msat: 0, - status: match channel.state { - ChannelState::PendingClose => PaymentStatus::Pending, - _ => PaymentStatus::Complete, - }, - description: Some("Closed Channel".to_string()), - details: PaymentDetails::ClosedChannel { - data: ClosedChannelPaymentDetails { - short_channel_id: channel.short_channel_id, - state: channel.state, - funding_txid: channel.funding_txid, - closing_txid, - }, - }, - error: None, - metadata: None, - }) + req: SendSpontaneousPaymentRequest, + ) -> Result { + let services = Arc::clone(&*self.services.lock().await); + services.send_spontaneous_payment(req).await } - /// Register for webhook callbacks at the given `webhook_url`. - /// - /// More specifically, it registers for the following types of callbacks: - /// - a payment is received - /// - a swap tx is confirmed - /// - /// This method should be called every time the application is started and when the `webhook_url` changes. - /// For example, if the `webhook_url` contains a push notification token and the token changes after - /// the application was started, then this method should be called to register for callbacks at - /// the new correct `webhook_url`. To unregister a webhook call [BreezServices::unregister_webhook]. - pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> { - info!("Registering for webhook notifications"); - let is_new_webhook_url = match self.persister.get_webhook_url()? { - None => true, - Some(cached_webhook_url) => cached_webhook_url != webhook_url, - }; - match is_new_webhook_url { - false => debug!("Webhook URL not changed, no need to (re-)register for monitored swap tx notifications"), - true => { - for swap in self - .btc_receive_swapper - .list_monitored()? - .iter() - .filter(|swap| !swap.refundable()) - { - let swap_address = &swap.bitcoin_address; - info!("Found non-refundable monitored swap with address {swap_address}, registering for onchain tx notifications"); - self.register_onchain_tx_notification(swap_address, &webhook_url) - .await?; - } - - for rev_swap in self - .btc_send_swapper - .list_monitored() - .await? - .iter() - { - let lockup_address = &rev_swap.get_lockup_address(self.config.network)?.to_string(); - info!("Found monitored reverse swap with address {lockup_address}, registering for onchain tx notifications"); - self.register_onchain_tx_notification(lockup_address, &webhook_url) - .await?; - } - } - } + /// Set the external metadata of a payment as a valid JSON string + pub async fn set_payment_metadata(&self, hash: String, metadata: String) -> SdkResult<()> { + let services = Arc::clone(&*self.services.lock().await); + services.set_payment_metadata(hash, metadata).await + } - // Register for LN payment notifications on every call, since these webhook registrations - // timeout after 14 days of not being used - self.register_payment_notifications(webhook_url.clone()) - .await?; + /// Sign given message with the private key of the node id. Returns a zbase + /// encoded signature. + pub async fn sign_message(&self, req: SignMessageRequest) -> SdkResult { + let services = Arc::clone(&*self.services.lock().await); + services.sign_message(req).await + } - // Only cache the webhook URL if callbacks were successfully registered for it. - // If any step above failed, not caching it allows the caller to re-trigger the registrations - // by calling the method again - self.persister.set_webhook_url(webhook_url)?; - Ok(()) + /// This method sync the local state with the remote node state. + /// The synced items are as follows: + /// * node state - General information about the node and its liquidity status + /// * channels - The list of channels and their status + /// * payments - The incoming/outgoing payments + pub async fn sync(&self) -> SdkResult<()> { + let services = Arc::clone(&*self.services.lock().await); + services.sync().await } /// Unregister webhook callbacks for the given `webhook_url`. @@ -2010,175 +715,8 @@ impl BreezServices { /// has changed such that it needs unregistering. For example, the token is valid but the locale changes. /// To register a webhook call [BreezServices::register_webhook]. pub async fn unregister_webhook(&self, webhook_url: String) -> SdkResult<()> { - info!("Unregistering for webhook notifications"); - self.unregister_onchain_tx_notifications(&webhook_url) - .await?; - self.unregister_payment_notifications(webhook_url).await?; - self.persister.remove_webhook_url()?; - Ok(()) - } - - /// Registers for lightning payment notifications. When a payment is intercepted by the LSP - /// to this node, a callback will be triggered to the `webhook_url`. - /// - /// Note: these notifications are registered for all LSPs (active and historical) with whom - /// we have a channel. - async fn register_payment_notifications(&self, webhook_url: String) -> SdkResult<()> { - let message = webhook_url.clone(); - let sign_request = SignMessageRequest { message }; - let sign_response = self.sign_message(sign_request).await?; - - // Attempt register call for all relevant LSPs - let mut error_found = false; - for lsp_info in get_notification_lsps( - self.persister.clone(), - self.lsp_api.clone(), - self.node_api.clone(), - ) - .await? - { - let lsp_id = lsp_info.id; - let res = self - .lsp_api - .register_payment_notifications( - lsp_id.clone(), - lsp_info.lsp_pubkey, - webhook_url.clone(), - sign_response.signature.clone(), - ) - .await; - if res.is_err() { - error_found = true; - warn!("Failed to register notifications for LSP {lsp_id}: {res:?}"); - } - } - - match error_found { - true => Err(SdkError::generic( - "Failed to register notifications for at least one LSP, see logs for details", - )), - false => Ok(()), - } - } - - /// Unregisters lightning payment notifications with the current LSP for the `webhook_url`. - /// - /// Note: these notifications are unregistered for all LSPs (active and historical) with whom - /// we have a channel. - async fn unregister_payment_notifications(&self, webhook_url: String) -> SdkResult<()> { - let message = webhook_url.clone(); - let sign_request = SignMessageRequest { message }; - let sign_response = self.sign_message(sign_request).await?; - - // Attempt register call for all relevant LSPs - let mut error_found = false; - for lsp_info in get_notification_lsps( - self.persister.clone(), - self.lsp_api.clone(), - self.node_api.clone(), - ) - .await? - { - let lsp_id = lsp_info.id; - let res = self - .lsp_api - .unregister_payment_notifications( - lsp_id.clone(), - lsp_info.lsp_pubkey, - webhook_url.clone(), - sign_response.signature.clone(), - ) - .await; - if res.is_err() { - error_found = true; - warn!("Failed to un-register notifications for LSP {lsp_id}: {res:?}"); - } - } - - match error_found { - true => Err(SdkError::generic( - "Failed to un-register notifications for at least one LSP, see logs for details", - )), - false => Ok(()), - } - } - - /// Registers for a onchain tx notification. When a new transaction to the specified `address` - /// is confirmed, a callback will be triggered to the `webhook_url`. - async fn register_onchain_tx_notification( - &self, - address: &str, - webhook_url: &str, - ) -> SdkResult<()> { - get_reqwest_client()? - .post(format!("{}/api/v1/register", self.config.chainnotifier_url)) - .header(CONTENT_TYPE, "application/json") - .body(Body::from( - json!({ - "address": address, - "webhook": webhook_url - }) - .to_string(), - )) - .send() - .await - .map(|_| ()) - .map_err(|e| SdkError::ServiceConnectivity { - err: format!("Failed to register for tx confirmation notifications: {e}"), - }) - } - - /// Unregisters all onchain tx notifications for the `webhook_url`. - async fn unregister_onchain_tx_notifications(&self, webhook_url: &str) -> SdkResult<()> { - get_reqwest_client()? - .post(format!( - "{}/api/v1/unregister", - self.config.chainnotifier_url - )) - .header(CONTENT_TYPE, "application/json") - .body(Body::from( - json!({ - "webhook": webhook_url - }) - .to_string(), - )) - .send() - .await - .map(|_| ()) - .map_err(|e| SdkError::ServiceConnectivity { - err: format!("Failed to unregister for tx confirmation notifications: {e}"), - }) - } - - async fn generate_sdk_diagnostic_data(&self) -> SdkResult { - let (sdk_version, sdk_git_hash) = Self::get_sdk_version(); - let version = format!("SDK v{sdk_version} ({sdk_git_hash})"); - let state = crate::serializer::value::to_value(&self.persister.get_node_state()?)?; - let payments = crate::serializer::value::to_value( - &self - .persister - .list_payments(ListPaymentsRequest::default())?, - )?; - let channels = crate::serializer::value::to_value(&self.persister.list_channels()?)?; - let settings = crate::serializer::value::to_value(&self.persister.list_settings()?)?; - let reverse_swaps = crate::serializer::value::to_value( - self.persister.list_reverse_swaps().map(sanitize_vec)?, - )?; - let swaps = - crate::serializer::value::to_value(self.persister.list_swaps().map(sanitize_vec)?)?; - let lsp_id = crate::serializer::value::to_value(&self.persister.get_lsp_id()?)?; - - let res = json!({ - "version": version, - "node_state": state, - "payments": payments, - "channels": channels, - "settings": settings, - "reverse_swaps": reverse_swaps, - "swaps": swaps, - "lsp_id": lsp_id, - }); - Ok(res) + let services = Arc::clone(&*self.services.lock().await); + services.unregister_webhook(webhook_url).await } } @@ -2208,1146 +746,11 @@ impl log::Log for GlobalSdkLogger { fn flush(&self) {} } -/// A helper struct to configure and build BreezServices -struct BreezServicesBuilder { - config: Config, - node_api: Option>, - backup_transport: Option>, - seed: Option>, - lsp_api: Option>, - fiat_api: Option>, - persister: Option>, - support_api: Option>, - swapper_api: Option>, - /// Reverse swap functionality on the Breez Server - reverse_swapper_api: Option>, - /// Reverse swap functionality on the 3rd party reverse swap service - reverse_swap_service_api: Option>, - buy_bitcoin_api: Option>, -} - -#[allow(dead_code)] -impl BreezServicesBuilder { - pub fn new(config: Config) -> BreezServicesBuilder { - BreezServicesBuilder { - config, - node_api: None, - seed: None, - lsp_api: None, - fiat_api: None, - persister: None, - support_api: None, - swapper_api: None, - reverse_swapper_api: None, - reverse_swap_service_api: None, - buy_bitcoin_api: None, - backup_transport: None, - } - } - - pub fn node_api(&mut self, node_api: Arc) -> &mut Self { - self.node_api = Some(node_api); - self - } - - pub fn lsp_api(&mut self, lsp_api: Arc) -> &mut Self { - self.lsp_api = Some(lsp_api.clone()); - self - } - - pub fn fiat_api(&mut self, fiat_api: Arc) -> &mut Self { - self.fiat_api = Some(fiat_api.clone()); - self - } - - pub fn buy_bitcoin_api(&mut self, buy_bitcoin_api: Arc) -> &mut Self { - self.buy_bitcoin_api = Some(buy_bitcoin_api.clone()); - self - } - - pub fn persister(&mut self, persister: Arc) -> &mut Self { - self.persister = Some(persister); - self - } - - pub fn support_api(&mut self, support_api: Arc) -> &mut Self { - self.support_api = Some(support_api.clone()); - self - } - - pub fn swapper_api(&mut self, swapper_api: Arc) -> &mut Self { - self.swapper_api = Some(swapper_api.clone()); - self - } - - pub fn reverse_swapper_api( - &mut self, - reverse_swapper_api: Arc, - ) -> &mut Self { - self.reverse_swapper_api = Some(reverse_swapper_api.clone()); - self - } - - pub fn reverse_swap_service_api( - &mut self, - reverse_swap_service_api: Arc, - ) -> &mut Self { - self.reverse_swap_service_api = Some(reverse_swap_service_api.clone()); - self - } - - pub fn backup_transport(&mut self, backup_transport: Arc) -> &mut Self { - self.backup_transport = Some(backup_transport.clone()); - self - } - - pub fn seed(&mut self, seed: Vec) -> &mut Self { - self.seed = Some(seed); - self - } - - pub async fn build( - &self, - restore_only: Option, - event_listener: Option>, - ) -> BreezServicesResult> { - if self.node_api.is_none() && self.seed.is_none() { - return Err(ConnectError::Generic { - err: "Either node_api or both credentials and seed should be provided".into(), - }); - } - - // The storage is implemented via sqlite. - let persister = self - .persister - .clone() - .unwrap_or_else(|| Arc::new(SqliteStorage::new(self.config.working_dir.clone()))); - persister.init()?; - - let mut node_api = self.node_api.clone(); - let mut backup_transport = self.backup_transport.clone(); - if node_api.is_none() { - let greenlight = Greenlight::connect( - self.config.clone(), - self.seed.clone().unwrap(), - restore_only, - persister.clone(), - ) - .await?; - let gl_arc = Arc::new(greenlight); - node_api = Some(gl_arc.clone()); - if backup_transport.is_none() { - backup_transport = Some(Arc::new(GLBackupTransport { inner: gl_arc })); - } - } - - if backup_transport.is_none() { - return Err(ConnectError::Generic { - err: "State synchronizer should be provided".into(), - }); - } - - let unwrapped_node_api = node_api.unwrap(); - let unwrapped_backup_transport = backup_transport.unwrap(); - - // create the backup encryption key and then the backup watcher - let backup_encryption_key = unwrapped_node_api.derive_bip32_key(vec![ - ChildNumber::from_hardened_idx(139)?, - ChildNumber::from(0), - ])?; - - // We calculate the legacy key as a fallback for the case where the backup is still - // encrypted with the old key. - let legacy_backup_encryption_key = unwrapped_node_api.legacy_derive_bip32_key(vec![ - ChildNumber::from_hardened_idx(139)?, - ChildNumber::from(0), - ])?; - let backup_watcher = BackupWatcher::new( - self.config.clone(), - unwrapped_backup_transport.clone(), - persister.clone(), - backup_encryption_key.to_priv().to_bytes(), - legacy_backup_encryption_key.to_priv().to_bytes(), - ); - - // breez_server provides both FiatAPI & LspAPI implementations - let breez_server = Arc::new( - BreezServer::new(self.config.breezserver.clone(), self.config.api_key.clone()) - .map_err(|e| ConnectError::ServiceConnectivity { - err: format!("Failed to create BreezServer: {e}"), - })?, - ); - - // Ensure breez server connection is established in the background - let cloned_breez_server = breez_server.clone(); - tokio::spawn(async move { - if let Err(e) = cloned_breez_server.ping().await { - error!("Failed to ping breez server: {e}"); - } - }); - - let current_lsp_id = persister.get_lsp_id()?; - if current_lsp_id.is_none() && self.config.default_lsp_id.is_some() { - persister.set_lsp(self.config.default_lsp_id.clone().unwrap(), None)?; - } - - let payment_receiver = Arc::new(PaymentReceiver { - config: self.config.clone(), - node_api: unwrapped_node_api.clone(), - lsp: breez_server.clone(), - persister: persister.clone(), - }); - - // mempool space is used to monitor the chain - let mempoolspace_urls = match self.config.mempoolspace_url.clone() { - None => { - let cached = persister.get_mempoolspace_base_urls()?; - match cached.len() { - // If we have no cached values, or we cached an empty list, fetch new ones - 0 => { - let fresh_urls = breez_server - .fetch_mempoolspace_urls() - .await - .unwrap_or(vec![DEFAULT_MEMPOOL_SPACE_URL.into()]); - persister.set_mempoolspace_base_urls(fresh_urls.clone())?; - fresh_urls - } - // If we already have cached values, return those - _ => cached, - } - } - Some(mempoolspace_url_from_config) => vec![mempoolspace_url_from_config], - }; - let chain_service = Arc::new(RedundantChainService::from_base_urls(mempoolspace_urls)); - - let btc_receive_swapper = Arc::new(BTCReceiveSwap::new( - self.config.network.into(), - unwrapped_node_api.clone(), - self.swapper_api - .clone() - .unwrap_or_else(|| breez_server.clone()), - persister.clone(), - chain_service.clone(), - payment_receiver.clone(), - )); - - let btc_send_swapper = Arc::new(BTCSendSwap::new( - self.config.clone(), - self.reverse_swapper_api - .clone() - .unwrap_or_else(|| breez_server.clone()), - self.reverse_swap_service_api - .clone() - .unwrap_or_else(|| Arc::new(BoltzApi {})), - persister.clone(), - chain_service.clone(), - unwrapped_node_api.clone(), - )); - - // create a shutdown channel (sender and receiver) - let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(()); - - let buy_bitcoin_api = self - .buy_bitcoin_api - .clone() - .unwrap_or_else(|| Arc::new(BuyBitcoinService::new(breez_server.clone()))); - - // Create the node services and it them statically - let breez_services = Arc::new(BreezServices { - config: self.config.clone(), - started: Mutex::new(false), - node_api: unwrapped_node_api.clone(), - lsp_api: self.lsp_api.clone().unwrap_or_else(|| breez_server.clone()), - fiat_api: self - .fiat_api - .clone() - .unwrap_or_else(|| breez_server.clone()), - support_api: self - .support_api - .clone() - .unwrap_or_else(|| breez_server.clone()), - buy_bitcoin_api, - chain_service, - persister: persister.clone(), - btc_receive_swapper, - btc_send_swapper, - payment_receiver, - event_listener, - backup_watcher: Arc::new(backup_watcher), - shutdown_sender, - shutdown_receiver, - }); - - Ok(breez_services) - } -} - /// Attempts to convert the phrase to a mnemonic, then to a seed. /// /// If the phrase is not a valid mnemonic, an error is returned. -pub fn mnemonic_to_seed(phrase: String) -> Result> { +pub fn mnemonic_to_seed(phrase: String) -> anyhow::Result> { let mnemonic = Mnemonic::from_phrase(&phrase, Language::English)?; let seed = Seed::new(&mnemonic, ""); Ok(seed.as_bytes().to_vec()) } - -pub struct OpenChannelParams { - pub payer_amount_msat: u64, - pub opening_fee_params: models::OpeningFeeParams, -} - -#[tonic::async_trait] -pub trait Receiver: Send + Sync { - async fn receive_payment( - &self, - req: ReceivePaymentRequest, - ) -> Result; - async fn wrap_node_invoice( - &self, - invoice: &str, - params: Option, - lsp_info: Option, - ) -> Result; -} - -pub(crate) struct PaymentReceiver { - config: Config, - node_api: Arc, - lsp: Arc, - persister: Arc, -} - -#[tonic::async_trait] -impl Receiver for PaymentReceiver { - async fn receive_payment( - &self, - req: ReceivePaymentRequest, - ) -> Result { - let lsp_info = get_lsp(self.persister.clone(), self.lsp.clone()).await?; - let node_state = self - .persister - .get_node_state()? - .ok_or(ReceivePaymentError::Generic { - err: "Node info not found".into(), - })?; - let expiry = req.expiry.unwrap_or(INVOICE_PAYMENT_FEE_EXPIRY_SECONDS); - - ensure_sdk!( - req.amount_msat > 0, - ReceivePaymentError::InvalidAmount { - err: "Receive amount must be more than 0".into() - } - ); - - let mut destination_invoice_amount_msat = req.amount_msat; - let mut channel_opening_fee_params = None; - let mut channel_fees_msat = None; - - // check if we need to open channel - let open_channel_needed = - node_state.max_receivable_single_payment_amount_msat < req.amount_msat; - if open_channel_needed { - info!("We need to open a channel"); - - // we need to open channel so we are calculating the fees for the LSP (coming either from the user, or from the LSP) - let ofp = match req.opening_fee_params { - Some(fee_params) => fee_params, - None => lsp_info.cheapest_open_channel_fee(expiry)?.clone(), - }; - - channel_opening_fee_params = Some(ofp.clone()); - channel_fees_msat = Some(ofp.get_channel_fees_msat_for(req.amount_msat)); - if let Some(channel_fees_msat) = channel_fees_msat { - info!("zero-conf fee calculation option: lsp fee rate (proportional): {}: (minimum {}), total fees for channel: {}", - ofp.proportional, ofp.min_msat, channel_fees_msat); - - if req.amount_msat < channel_fees_msat + 1000 { - return Err( - ReceivePaymentError::InvalidAmount{err: format!( - "Amount should be more than the minimum fees {channel_fees_msat} msat, but is {} msat", - req.amount_msat - )} - ); - } - // remove the fees from the amount to get the small amount on the current node invoice. - destination_invoice_amount_msat = req.amount_msat - channel_fees_msat; - } - } - - info!("Creating invoice on NodeAPI"); - let invoice = self - .node_api - .create_invoice(CreateInvoiceRequest { - amount_msat: destination_invoice_amount_msat, - description: req.description, - payer_amount_msat: match open_channel_needed { - true => Some(req.amount_msat), - false => None, - }, - preimage: req.preimage, - use_description_hash: req.use_description_hash, - expiry: Some(expiry), - cltv: Some(req.cltv.unwrap_or(144)), - }) - .await?; - info!("Invoice created {}", invoice); - - let open_channel_params = match open_channel_needed { - true => Some(OpenChannelParams { - payer_amount_msat: req.amount_msat, - opening_fee_params: channel_opening_fee_params.clone().ok_or( - ReceivePaymentError::Generic { - err: "We need to open a channel, but no channel opening fee params found" - .into(), - }, - )?, - }), - false => None, - }; - - let invoice = self - .wrap_node_invoice(&invoice, open_channel_params, Some(lsp_info)) - .await?; - let parsed_invoice = parse_invoice(&invoice)?; - - // return the signed, converted invoice with hints - Ok(ReceivePaymentResponse { - ln_invoice: parsed_invoice, - opening_fee_params: channel_opening_fee_params, - opening_fee_msat: channel_fees_msat, - }) - } - - async fn wrap_node_invoice( - &self, - invoice: &str, - params: Option, - lsp_info: Option, - ) -> Result { - let lsp_info = match lsp_info { - Some(lsp_info) => lsp_info, - None => get_lsp(self.persister.clone(), self.lsp.clone()).await?, - }; - - match params { - Some(params) => { - self.wrap_open_channel_invoice(invoice, params, &lsp_info) - .await - } - None => self.ensure_hint(invoice, &lsp_info).await, - } - } -} - -impl PaymentReceiver { - async fn ensure_hint( - &self, - invoice: &str, - lsp_info: &LspInformation, - ) -> Result { - info!("Getting routing hints from node"); - let (mut hints, has_public_channel) = self.node_api.get_routing_hints(lsp_info).await?; - if !has_public_channel && hints.is_empty() { - return Err(ReceivePaymentError::InvoiceNoRoutingHints { - err: "Must have at least one active channel".into(), - }); - } - - let parsed_invoice = parse_invoice(invoice)?; - - // check if the lsp hint already exists - info!("Existing routing hints {:?}", parsed_invoice.routing_hints); - - // limit the hints to max 3 and extract the lsp one. - if let Some(lsp_hint) = Self::limit_and_extract_lsp_hint(&mut hints, lsp_info) { - if parsed_invoice.contains_hint_for_node(lsp_info.pubkey.as_str()) { - return Ok(String::from(invoice)); - } - - info!("Adding lsp hint: {:?}", lsp_hint); - let modified = - add_routing_hints(invoice, true, &vec![lsp_hint], parsed_invoice.amount_msat)?; - - let invoice = self.node_api.sign_invoice(modified)?; - info!("Signed invoice with hint = {}", invoice); - return Ok(invoice); - } - - if parsed_invoice.routing_hints.is_empty() { - info!("Adding custom hints: {:?}", hints); - let modified = add_routing_hints(invoice, false, &hints, parsed_invoice.amount_msat)?; - let invoice = self.node_api.sign_invoice(modified)?; - info!("Signed invoice with hints = {}", invoice); - return Ok(invoice); - } - - Ok(String::from(invoice)) - } - - async fn wrap_open_channel_invoice( - &self, - invoice: &str, - params: OpenChannelParams, - lsp_info: &LspInformation, - ) -> Result { - let parsed_invoice = parse_invoice(invoice)?; - let open_channel_hint = RouteHint { - hops: vec![RouteHintHop { - src_node_id: lsp_info.pubkey.clone(), - short_channel_id: "1x0x0".to_string(), - fees_base_msat: lsp_info.base_fee_msat as u32, - fees_proportional_millionths: (lsp_info.fee_rate * 1000000.0) as u32, - cltv_expiry_delta: lsp_info.time_lock_delta as u64, - htlc_minimum_msat: Some(lsp_info.min_htlc_msat as u64), - htlc_maximum_msat: None, - }], - }; - info!("Adding open channel hint: {:?}", open_channel_hint); - let invoice_with_hint = add_routing_hints( - invoice, - false, - &vec![open_channel_hint], - Some(params.payer_amount_msat), - )?; - let signed_invoice = self.node_api.sign_invoice(invoice_with_hint)?; - - info!("Registering payment with LSP"); - let api_key = self.config.api_key.clone().unwrap_or_default(); - let api_key_hash = sha256::Hash::hash(api_key.as_bytes()).to_hex(); - - self.lsp - .register_payment( - lsp_info.id.clone(), - lsp_info.lsp_pubkey.clone(), - grpc::PaymentInformation { - payment_hash: hex::decode(parsed_invoice.payment_hash.clone()) - .map_err(|e| anyhow!("Failed to decode hex payment hash: {e}"))?, - payment_secret: parsed_invoice.payment_secret.clone(), - destination: hex::decode(parsed_invoice.payee_pubkey.clone()) - .map_err(|e| anyhow!("Failed to decode hex payee pubkey: {e}"))?, - incoming_amount_msat: params.payer_amount_msat as i64, - outgoing_amount_msat: parsed_invoice - .amount_msat - .ok_or(anyhow!("Open channel invoice must have an amount"))? - as i64, - tag: json!({ "apiKeyHash": api_key_hash }).to_string(), - opening_fee_params: Some(params.opening_fee_params.into()), - }, - ) - .await?; - // Make sure we save the large amount so we can deduce the fees later. - self.persister.insert_open_channel_payment_info( - &parsed_invoice.payment_hash, - params.payer_amount_msat, - invoice, - )?; - - Ok(signed_invoice) - } - - fn limit_and_extract_lsp_hint( - routing_hints: &mut Vec, - lsp_info: &LspInformation, - ) -> Option { - let mut lsp_hint: Option = None; - if let Some(lsp_index) = routing_hints.iter().position(|r| { - r.hops - .iter() - .any(|h| h.src_node_id == lsp_info.pubkey.clone()) - }) { - lsp_hint = Some(routing_hints.remove(lsp_index)); - } - if routing_hints.len() > 3 { - routing_hints.drain(3..); - } - lsp_hint - } -} - -/// Convenience method to look up LSP info based on current LSP ID -async fn get_lsp( - persister: Arc, - lsp_api: Arc, -) -> SdkResult { - let lsp_id = persister - .get_lsp_id()? - .ok_or(SdkError::generic("No LSP ID found"))?; - - get_lsp_by_id(persister, lsp_api, lsp_id.as_str()) - .await? - .ok_or_else(|| SdkError::Generic { - err: format!("No LSP found for id {lsp_id}"), - }) -} - -async fn get_lsp_by_id( - persister: Arc, - lsp_api: Arc, - lsp_id: &str, -) -> SdkResult> { - let node_pubkey = persister - .get_node_state()? - .ok_or(SdkError::generic("Node info not found"))? - .id; - - Ok(lsp_api - .list_lsps(node_pubkey) - .await? - .iter() - .find(|&lsp| lsp.id.as_str() == lsp_id) - .cloned()) -} - -/// Convenience method to get all LSPs (active and historical) relevant for registering or -/// unregistering webhook notifications -async fn get_notification_lsps( - persister: Arc, - lsp_api: Arc, - node_api: Arc, -) -> SdkResult> { - let node_pubkey = persister - .get_node_state()? - .ok_or(SdkError::generic("Node info not found"))? - .id; - let open_peers = node_api.get_open_peers().await?; - - let mut notification_lsps = vec![]; - for lsp in lsp_api.list_used_lsps(node_pubkey).await? { - match !lsp.opening_fee_params_list.values.is_empty() { - true => { - // Non-empty fee params list = this is the active LSP - // Always consider the active LSP for notifications - notification_lsps.push(lsp); - } - false => { - // Consider only historical LSPs with whom we have an active channel - let lsp_pubkey = hex::decode(&lsp.pubkey) - .map_err(|e| anyhow!("Failed decode lsp pubkey: {e}"))?; - let has_active_channel_to_lsp = open_peers.contains(&lsp_pubkey); - if has_active_channel_to_lsp { - notification_lsps.push(lsp); - } - } - } - } - Ok(notification_lsps) -} - -#[cfg(test)] -pub(crate) mod tests { - use std::collections::HashMap; - use std::sync::Arc; - - use anyhow::{anyhow, Result}; - use regex::Regex; - use reqwest::Url; - use sdk_common::prelude::Rate; - - use crate::breez_services::{BreezServices, BreezServicesBuilder}; - use crate::models::{LnPaymentDetails, NodeState, Payment, PaymentDetails, PaymentTypeFilter}; - use crate::node_api::NodeAPI; - use crate::test_utils::*; - use crate::*; - - use super::{PaymentReceiver, Receiver}; - - #[tokio::test] - async fn test_node_state() -> Result<()> { - // let storage_path = format!("{}/storage.sql", get_test_working_dir()); - // std::fs::remove_file(storage_path).ok(); - - let dummy_node_state = get_dummy_node_state(); - - let lnurl_metadata = "{'key': 'sample-metadata-val'}"; - let test_ln_address = "test@ln-address.com"; - let test_lnurl_withdraw_endpoint = "https://test.endpoint.lnurl-w"; - let sa = SuccessActionProcessed::Message { - data: MessageSuccessActionData { - message: "test message".into(), - }, - }; - - let payment_hash_lnurl_withdraw = "2222"; - let payment_hash_with_lnurl_success_action = "3333"; - let payment_hash_swap: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let swap_info = SwapInfo { - bitcoin_address: "123".to_string(), - created_at: 12345678, - lock_height: 654321, - payment_hash: payment_hash_swap.clone(), - preimage: vec![], - private_key: vec![], - public_key: vec![], - swapper_public_key: vec![], - script: vec![], - bolt11: Some("312".into()), - paid_msat: 1000, - confirmed_sats: 1, - unconfirmed_sats: 0, - total_incoming_txs: 1, - status: SwapStatus::Refundable, - refund_tx_ids: vec![], - unconfirmed_tx_ids: vec![], - confirmed_tx_ids: vec![], - min_allowed_deposit: 5_000, - max_allowed_deposit: 1_000_000, - max_swapper_payable: 2_000_000, - last_redeem_error: None, - channel_opening_fees: Some(OpeningFeeParams { - min_msat: 5_000_000, - proportional: 50, - valid_until: "date".to_string(), - max_idle_time: 12345, - max_client_to_self_delay: 234, - promise: "promise".to_string(), - }), - confirmed_at: Some(555), - }; - let payment_hash_rev_swap: Vec = vec![8, 7, 6, 5, 4, 3, 2, 1]; - let preimage_rev_swap: Vec = vec![6, 6, 6, 6]; - let full_ref_swap_info = FullReverseSwapInfo { - id: "rev_swap_id".to_string(), - created_at_block_height: 0, - preimage: preimage_rev_swap.clone(), - private_key: vec![], - claim_pubkey: "claim_pubkey".to_string(), - timeout_block_height: 600_000, - invoice: "645".to_string(), - redeem_script: "redeem_script".to_string(), - onchain_amount_sat: 250, - sat_per_vbyte: Some(50), - receive_amount_sat: None, - cache: ReverseSwapInfoCached { - status: ReverseSwapStatus::CompletedConfirmed, - lockup_txid: Some("lockup_txid".to_string()), - claim_txid: Some("claim_txid".to_string()), - }, - }; - let rev_swap_info = ReverseSwapInfo { - id: "rev_swap_id".to_string(), - claim_pubkey: "claim_pubkey".to_string(), - lockup_txid: Some("lockup_txid".to_string()), - claim_txid: Some("claim_txid".to_string()), - onchain_amount_sat: 250, - status: ReverseSwapStatus::CompletedConfirmed, - }; - let dummy_transactions = vec![ - Payment { - id: "1111".to_string(), - payment_type: PaymentType::Received, - payment_time: 100000, - amount_msat: 10, - fee_msat: 0, - status: PaymentStatus::Complete, - error: None, - description: Some("test receive".to_string()), - details: PaymentDetails::Ln { - data: LnPaymentDetails { - payment_hash: "1111".to_string(), - label: "".to_string(), - destination_pubkey: "1111".to_string(), - payment_preimage: "2222".to_string(), - keysend: false, - bolt11: "1111".to_string(), - lnurl_success_action: None, - lnurl_pay_domain: None, - lnurl_pay_comment: None, - lnurl_metadata: None, - ln_address: None, - lnurl_withdraw_endpoint: None, - swap_info: None, - reverse_swap_info: None, - pending_expiration_block: None, - open_channel_bolt11: None, - }, - }, - metadata: None, - }, - Payment { - id: payment_hash_lnurl_withdraw.to_string(), - payment_type: PaymentType::Received, - payment_time: 150000, - amount_msat: 10, - fee_msat: 0, - status: PaymentStatus::Complete, - error: None, - description: Some("test lnurl-withdraw receive".to_string()), - details: PaymentDetails::Ln { - data: LnPaymentDetails { - payment_hash: payment_hash_lnurl_withdraw.to_string(), - label: "".to_string(), - destination_pubkey: "1111".to_string(), - payment_preimage: "3333".to_string(), - keysend: false, - bolt11: "1111".to_string(), - lnurl_success_action: None, - lnurl_pay_domain: None, - lnurl_pay_comment: None, - lnurl_metadata: None, - ln_address: None, - lnurl_withdraw_endpoint: Some(test_lnurl_withdraw_endpoint.to_string()), - swap_info: None, - reverse_swap_info: None, - pending_expiration_block: None, - open_channel_bolt11: None, - }, - }, - metadata: None, - }, - Payment { - id: payment_hash_with_lnurl_success_action.to_string(), - payment_type: PaymentType::Sent, - payment_time: 200000, - amount_msat: 8, - fee_msat: 2, - status: PaymentStatus::Complete, - error: None, - description: Some("test payment".to_string()), - details: PaymentDetails::Ln { - data: LnPaymentDetails { - payment_hash: payment_hash_with_lnurl_success_action.to_string(), - label: "".to_string(), - destination_pubkey: "123".to_string(), - payment_preimage: "4444".to_string(), - keysend: false, - bolt11: "123".to_string(), - lnurl_success_action: Some(sa.clone()), - lnurl_pay_domain: None, - lnurl_pay_comment: None, - lnurl_metadata: Some(lnurl_metadata.to_string()), - ln_address: Some(test_ln_address.to_string()), - lnurl_withdraw_endpoint: None, - swap_info: None, - reverse_swap_info: None, - pending_expiration_block: None, - open_channel_bolt11: None, - }, - }, - metadata: None, - }, - Payment { - id: hex::encode(payment_hash_swap.clone()), - payment_type: PaymentType::Received, - payment_time: 250000, - amount_msat: 1_000, - fee_msat: 0, - status: PaymentStatus::Complete, - error: None, - description: Some("test receive".to_string()), - details: PaymentDetails::Ln { - data: LnPaymentDetails { - payment_hash: hex::encode(payment_hash_swap), - label: "".to_string(), - destination_pubkey: "321".to_string(), - payment_preimage: "5555".to_string(), - keysend: false, - bolt11: "312".to_string(), - lnurl_success_action: None, - lnurl_pay_domain: None, - lnurl_pay_comment: None, - lnurl_metadata: None, - ln_address: None, - lnurl_withdraw_endpoint: None, - swap_info: Some(swap_info.clone()), - reverse_swap_info: None, - pending_expiration_block: None, - open_channel_bolt11: None, - }, - }, - metadata: None, - }, - Payment { - id: hex::encode(payment_hash_rev_swap.clone()), - payment_type: PaymentType::Sent, - payment_time: 300000, - amount_msat: 50_000_000, - fee_msat: 2_000, - status: PaymentStatus::Complete, - error: None, - description: Some("test send onchain".to_string()), - details: PaymentDetails::Ln { - data: LnPaymentDetails { - payment_hash: hex::encode(payment_hash_rev_swap), - label: "".to_string(), - destination_pubkey: "321".to_string(), - payment_preimage: hex::encode(preimage_rev_swap), - keysend: false, - bolt11: "312".to_string(), - lnurl_success_action: None, - lnurl_metadata: None, - lnurl_pay_domain: None, - lnurl_pay_comment: None, - ln_address: None, - lnurl_withdraw_endpoint: None, - swap_info: None, - reverse_swap_info: Some(rev_swap_info.clone()), - pending_expiration_block: None, - open_channel_bolt11: None, - }, - }, - metadata: None, - }, - ]; - let node_api = Arc::new(MockNodeAPI::new(dummy_node_state.clone())); - - let test_config = create_test_config(); - let persister = Arc::new(create_test_persister(test_config.clone())); - persister.init()?; - persister.insert_or_update_payments(&dummy_transactions)?; - persister.insert_payment_external_info( - payment_hash_with_lnurl_success_action, - PaymentExternalInfo { - lnurl_pay_success_action: Some(sa.clone()), - lnurl_pay_domain: None, - lnurl_pay_comment: None, - lnurl_metadata: Some(lnurl_metadata.to_string()), - ln_address: Some(test_ln_address.to_string()), - lnurl_withdraw_endpoint: None, - attempted_amount_msat: None, - attempted_error: None, - }, - )?; - persister.insert_payment_external_info( - payment_hash_lnurl_withdraw, - PaymentExternalInfo { - lnurl_pay_success_action: None, - lnurl_pay_domain: None, - lnurl_pay_comment: None, - lnurl_metadata: None, - ln_address: None, - lnurl_withdraw_endpoint: Some(test_lnurl_withdraw_endpoint.to_string()), - attempted_amount_msat: None, - attempted_error: None, - }, - )?; - persister.insert_swap(swap_info.clone())?; - persister.update_swap_bolt11( - swap_info.bitcoin_address.clone(), - swap_info.bolt11.clone().unwrap(), - )?; - persister.insert_reverse_swap(&full_ref_swap_info)?; - persister - .update_reverse_swap_status("rev_swap_id", &ReverseSwapStatus::CompletedConfirmed)?; - persister - .update_reverse_swap_lockup_txid("rev_swap_id", Some("lockup_txid".to_string()))?; - persister.update_reverse_swap_claim_txid("rev_swap_id", Some("claim_txid".to_string()))?; - - let mut builder = BreezServicesBuilder::new(test_config.clone()); - let breez_services = builder - .lsp_api(Arc::new(MockBreezServer {})) - .fiat_api(Arc::new(MockBreezServer {})) - .node_api(node_api) - .persister(persister) - .backup_transport(Arc::new(MockBackupTransport::new())) - .build(None, None) - .await?; - - breez_services.sync().await?; - let fetched_state = breez_services.node_info()?; - assert_eq!(fetched_state, dummy_node_state); - - let all = breez_services - .list_payments(ListPaymentsRequest::default()) - .await?; - let mut cloned = all.clone(); - - // test the right order - cloned.reverse(); - assert_eq!(dummy_transactions, cloned); - - let received = breez_services - .list_payments(ListPaymentsRequest { - filters: Some(vec![PaymentTypeFilter::Received]), - ..Default::default() - }) - .await?; - assert_eq!( - received, - vec![cloned[3].clone(), cloned[1].clone(), cloned[0].clone()] - ); - - let sent = breez_services - .list_payments(ListPaymentsRequest { - filters: Some(vec![ - PaymentTypeFilter::Sent, - PaymentTypeFilter::ClosedChannel, - ]), - ..Default::default() - }) - .await?; - assert_eq!(sent, vec![cloned[4].clone(), cloned[2].clone()]); - assert!(matches!( - &sent[1].details, - PaymentDetails::Ln {data: LnPaymentDetails {lnurl_success_action, ..}} - if lnurl_success_action == &Some(sa))); - assert!(matches!( - &sent[1].details, - PaymentDetails::Ln {data: LnPaymentDetails {lnurl_pay_domain, ln_address, ..}} - if lnurl_pay_domain.is_none() && ln_address == &Some(test_ln_address.to_string()))); - assert!(matches!( - &received[1].details, - PaymentDetails::Ln {data: LnPaymentDetails {lnurl_withdraw_endpoint, ..}} - if lnurl_withdraw_endpoint == &Some(test_lnurl_withdraw_endpoint.to_string()))); - assert!(matches!( - &received[0].details, - PaymentDetails::Ln {data: LnPaymentDetails {swap_info: swap, ..}} - if swap == &Some(swap_info))); - assert!(matches!( - &sent[0].details, - PaymentDetails::Ln {data: LnPaymentDetails {reverse_swap_info: rev_swap, ..}} - if rev_swap == &Some(rev_swap_info))); - - Ok(()) - } - - #[tokio::test] - async fn test_receive_with_open_channel() -> Result<()> { - let config = create_test_config(); - let persister = Arc::new(create_test_persister(config.clone())); - persister.init().unwrap(); - - let dummy_node_state = get_dummy_node_state(); - - let node_api = Arc::new(MockNodeAPI::new(dummy_node_state.clone())); - - let breez_server = Arc::new(MockBreezServer {}); - persister.set_lsp(breez_server.lsp_id(), None).unwrap(); - persister.set_node_state(&dummy_node_state).unwrap(); - - let receiver: Arc = Arc::new(PaymentReceiver { - config, - node_api, - persister, - lsp: breez_server.clone(), - }); - let ln_invoice = receiver - .receive_payment(ReceivePaymentRequest { - amount_msat: 3_000_000, - description: "should populate lsp hints".to_string(), - use_description_hash: Some(false), - ..Default::default() - }) - .await? - .ln_invoice; - assert_eq!(ln_invoice.routing_hints[0].hops.len(), 1); - let lsp_hop = &ln_invoice.routing_hints[0].hops[0]; - assert_eq!(lsp_hop.src_node_id, breez_server.clone().lsp_pub_key()); - assert_eq!(lsp_hop.short_channel_id, "1x0x0"); - Ok(()) - } - - #[tokio::test] - async fn test_list_lsps() -> Result<()> { - let storage_path = format!("{}/storage.sql", get_test_working_dir()); - std::fs::remove_file(storage_path).ok(); - - let breez_services = breez_services() - .await - .map_err(|e| anyhow!("Failed to get the BreezServices: {e}"))?; - breez_services.sync().await?; - - let node_pubkey = breez_services.node_info()?.id; - let lsps = breez_services.lsp_api.list_lsps(node_pubkey).await?; - assert_eq!(lsps.len(), 1); - - Ok(()) - } - - #[tokio::test] - async fn test_fetch_rates() -> Result<(), Box> { - let breez_services = breez_services().await?; - breez_services.sync().await?; - - let rates = breez_services.fiat_api.fetch_fiat_rates().await?; - assert_eq!(rates.len(), 1); - assert_eq!( - rates[0], - Rate { - coin: "USD".to_string(), - value: 20_000.00, - } - ); - - Ok(()) - } - - #[tokio::test] - async fn test_buy_bitcoin_with_moonpay() -> Result<(), Box> { - let breez_services = breez_services().await?; - breez_services.sync().await?; - let moonpay_url = breez_services - .buy_bitcoin(BuyBitcoinRequest { - provider: BuyBitcoinProvider::Moonpay, - opening_fee_params: None, - redirect_url: None, - }) - .await? - .url; - let parsed = Url::parse(&moonpay_url)?; - let query_pairs = parsed.query_pairs().into_owned().collect::>(); - - assert_eq!(parsed.host_str(), Some("mock.moonpay")); - assert_eq!(parsed.path(), "/"); - - let wallet_address = parse(query_pairs.get("wa").unwrap()).await?; - assert!(matches!(wallet_address, InputType::BitcoinAddress { .. })); - - let max_amount = query_pairs.get("ma").unwrap(); - assert!(Regex::new(r"^\d+\.\d{8}$").unwrap().is_match(max_amount)); - - Ok(()) - } - - /// Build node service for tests - pub(crate) async fn breez_services() -> Result> { - breez_services_with(None, vec![]).await - } - - /// Build node service for tests with a list of known payments - pub(crate) async fn breez_services_with( - node_api: Option>, - known_payments: Vec, - ) -> Result> { - let node_api = - node_api.unwrap_or_else(|| Arc::new(MockNodeAPI::new(get_dummy_node_state()))); - - let test_config = create_test_config(); - let persister = Arc::new(create_test_persister(test_config.clone())); - persister.init()?; - persister.insert_or_update_payments(&known_payments)?; - persister.set_lsp(MockBreezServer {}.lsp_id(), None)?; - - let mut builder = BreezServicesBuilder::new(test_config.clone()); - let breez_services = builder - .lsp_api(Arc::new(MockBreezServer {})) - .fiat_api(Arc::new(MockBreezServer {})) - .reverse_swap_service_api(Arc::new(MockReverseSwapperAPI {})) - .buy_bitcoin_api(Arc::new(MockBuyBitcoinService {})) - .persister(persister) - .node_api(node_api) - .backup_transport(Arc::new(MockBackupTransport::new())) - .build(None, None) - .await?; - - Ok(breez_services) - } - - /// Build dummy NodeState for tests - pub(crate) fn get_dummy_node_state() -> NodeState { - NodeState { - id: "tx1".to_string(), - block_height: 1, - channels_balance_msat: 100, - onchain_balance_msat: 1_000, - pending_onchain_balance_msat: 100, - utxos: vec![], - max_payable_msat: 95, - max_receivable_msat: 4_000_000_000, - max_single_payment_amount_msat: 1_000, - max_chan_reserve_msats: 0, - connected_peers: vec!["1111".to_string()], - max_receivable_single_payment_amount_msat: 2_000, - total_inbound_liquidity_msats: 10_000, - } - } -} diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index 5d6c32529..8377999bc 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -20,15 +20,15 @@ use std::sync::Arc; // Section: imports -use crate::breez_services::BackupFailedData; -use crate::breez_services::BreezEvent; -use crate::breez_services::CheckMessageRequest; -use crate::breez_services::CheckMessageResponse; -use crate::breez_services::InvoicePaidDetails; -use crate::breez_services::PaymentFailedData; -use crate::breez_services::SignMessageRequest; -use crate::breez_services::SignMessageResponse; use crate::chain::RecommendedFees; +use crate::internal_breez_services::BackupFailedData; +use crate::internal_breez_services::BreezEvent; +use crate::internal_breez_services::CheckMessageRequest; +use crate::internal_breez_services::CheckMessageResponse; +use crate::internal_breez_services::InvoicePaidDetails; +use crate::internal_breez_services::PaymentFailedData; +use crate::internal_breez_services::SignMessageRequest; +use crate::internal_breez_services::SignMessageResponse; use crate::lnurl::pay::LnUrlPayResult; use crate::lnurl::pay::LnUrlPaySuccessData; use crate::lsp::LspInformation; diff --git a/libs/sdk-core/src/internal_breez_services.rs b/libs/sdk-core/src/internal_breez_services.rs new file mode 100644 index 000000000..f781c16d0 --- /dev/null +++ b/libs/sdk-core/src/internal_breez_services.rs @@ -0,0 +1,2996 @@ +use std::str::FromStr; +use std::sync::Arc; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use anyhow::{anyhow, Result}; +use bitcoin::hashes::hex::ToHex; +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::util::bip32::ChildNumber; +use futures::TryFutureExt; +use reqwest::{header::CONTENT_TYPE, Body}; +use sdk_common::grpc; +use sdk_common::prelude::*; +use serde::Serialize; +use serde_json::{json, Value}; +use strum_macros::EnumString; +use tokio::sync::{mpsc, watch, Mutex}; +use tokio::time::{sleep, MissedTickBehavior}; + +use crate::backup::{BackupRequest, BackupTransport, BackupWatcher}; +use crate::buy::{BuyBitcoinApi, BuyBitcoinService}; +use crate::chain::{ + ChainService, Outspend, RecommendedFees, RedundantChainService, RedundantChainServiceTrait, + DEFAULT_MEMPOOL_SPACE_URL, +}; +use crate::error::{ + ConnectError, ReceiveOnchainError, ReceiveOnchainResult, ReceivePaymentError, + RedeemOnchainResult, SdkError, SdkResult, SendOnchainError, SendPaymentError, +}; +use crate::greenlight::{GLBackupTransport, Greenlight}; +use crate::lnurl::auth::SdkLnurlAuthSigner; +use crate::lnurl::pay::*; +use crate::lsp::LspInformation; +use crate::models::{ + sanitize::*, ChannelState, ClosedChannelPaymentDetails, Config, LspAPI, NodeState, Payment, + PaymentDetails, PaymentType, ReverseSwapPairInfo, ReverseSwapServiceAPI, SwapInfo, SwapperAPI, + INVOICE_PAYMENT_FEE_EXPIRY_SECONDS, +}; +use crate::node_api::{CreateInvoiceRequest, NodeAPI}; +use crate::persist::db::SqliteStorage; +use crate::swap_in::swap::BTCReceiveSwap; +use crate::swap_out::boltzswap::BoltzApi; +use crate::swap_out::reverseswap::{BTCSendSwap, CreateReverseSwapArg}; +use crate::*; + +pub type BreezServicesResult = Result; + +/// Trait that can be used to react to various [BreezEvent]s emitted by the SDK. +pub trait EventListener: Send + Sync { + fn on_event(&self, e: BreezEvent); +} + +/// Event emitted by the SDK. To listen for and react to these events, use an [EventListener] when +/// initializing the [BreezServices]. +#[derive(Clone, Debug, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum BreezEvent { + /// Indicates that a new block has just been found + NewBlock { block: u32 }, + /// Indicates that a new invoice has just been paid + InvoicePaid { details: InvoicePaidDetails }, + /// Indicates that the local SDK state has just been sync-ed with the remote components + Synced, + /// Indicates that an outgoing payment has been completed successfully + PaymentSucceed { details: Payment }, + /// Indicates that an outgoing payment has been failed to complete + PaymentFailed { details: PaymentFailedData }, + /// Indicates that the backup process has just started + BackupStarted, + /// Indicates that the backup process has just finished successfully + BackupSucceeded, + /// Indicates that the backup process has just failed + BackupFailed { details: BackupFailedData }, + /// Indicates that a reverse swap has been updated which may also + /// include a status change + ReverseSwapUpdated { details: ReverseSwapInfo }, + /// Indicates that a swap has been updated which may also + /// include a status change + SwapUpdated { details: SwapInfo }, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BackupFailedData { + pub error: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PaymentFailedData { + pub error: String, + pub node_id: String, + pub invoice: Option, + pub label: Option, +} + +/// Details of an invoice that has been paid, included as payload in an emitted [BreezEvent] +#[derive(Clone, Debug, PartialEq)] +pub struct InvoicePaidDetails { + pub payment_hash: String, + pub bolt11: String, + pub payment: Option, +} + +pub trait LogStream: Send + Sync { + fn log(&self, l: LogEntry); +} + +/// Request to sign a message with the node's private key. +#[derive(Clone, Debug, PartialEq)] +pub struct SignMessageRequest { + /// The message to be signed by the node's private key. + pub message: String, +} + +/// Response to a [SignMessageRequest]. +#[derive(Clone, Debug, PartialEq)] +pub struct SignMessageResponse { + /// The signature that covers the message of SignMessageRequest. Zbase + /// encoded. + pub signature: String, +} + +/// Request to check a message was signed by a specific node id. +#[derive(Clone, Debug, PartialEq)] +pub struct CheckMessageRequest { + /// The message that was signed. + pub message: String, + /// The public key of the node that signed the message. + pub pubkey: String, + /// The zbase encoded signature to verify. + pub signature: String, +} + +/// Response to a [CheckMessageRequest] +#[derive(Clone, Debug, PartialEq)] +pub struct CheckMessageResponse { + /// Boolean value indicating whether the signature covers the message and + /// was signed by the given pubkey. + pub is_valid: bool, +} + +#[derive(Clone, PartialEq, EnumString, Serialize)] +enum DevCommand { + /// Generates diagnostic data report. + #[strum(serialize = "generatediagnosticdata")] + GenerateDiagnosticData, +} + +/// BreezServices is a facade and the single entry point for the SDK. +pub(super) struct InternalBreezServices { + config: Config, + started: Mutex, + node_api: Arc, + lsp_api: Arc, + fiat_api: Arc, + buy_bitcoin_api: Arc, + support_api: Arc, + chain_service: Arc, + persister: Arc, + payment_receiver: Arc, + btc_receive_swapper: Arc, + btc_send_swapper: Arc, + event_listener: Option>>, + backup_watcher: Arc, + shutdown_sender: watch::Sender<()>, + shutdown_receiver: watch::Receiver<()>, +} + +impl InternalBreezServices { + pub async fn connect( + req: ConnectRequest, + event_listener: Arc>, + ) -> BreezServicesResult> { + let (sdk_version, sdk_git_hash) = Self::get_sdk_version(); + info!("SDK v{sdk_version} ({sdk_git_hash})"); + let start = Instant::now(); + let services = BreezServicesBuilder::new(req.config) + .seed(req.seed) + .build(req.restore_only, Some(event_listener)) + .await?; + services.start().await?; + let connect_duration = start.elapsed(); + info!("SDK connected in: {connect_duration:?}"); + Ok(services) + } + + fn get_sdk_version() -> (&'static str, &'static str) { + let sdk_version = option_env!("CARGO_PKG_VERSION").unwrap_or_default(); + let sdk_git_hash = option_env!("SDK_GIT_HASH").unwrap_or_default(); + (sdk_version, sdk_git_hash) + } + + pub async fn backup(&self) -> SdkResult<()> { + let (on_complete, mut on_complete_receiver) = mpsc::channel::>(1); + let req = BackupRequest::with(on_complete, true); + self.backup_watcher.request_backup(req).await?; + + match on_complete_receiver.recv().await { + Some(res) => res.map_err(|e| SdkError::Generic { + err: format!("Backup failed: {e}"), + }), + None => Err(SdkError::Generic { + err: "Backup process failed to complete".into(), + }), + } + } + + pub fn backup_status(&self) -> SdkResult { + let backup_time = self.persister.get_last_backup_time()?; + let sync_request = self.persister.get_last_sync_request()?; + Ok(BackupStatus { + last_backup_time: backup_time, + backed_up: sync_request.is_none(), + }) + } + + pub async fn buy_bitcoin( + &self, + req: BuyBitcoinRequest, + ) -> Result { + let swap_info = self + .receive_onchain(ReceiveOnchainRequest { + opening_fee_params: req.opening_fee_params, + }) + .await?; + let url = self + .buy_bitcoin_api + .buy_bitcoin(req.provider, &swap_info, req.redirect_url) + .await?; + + Ok(BuyBitcoinResponse { + url, + opening_fee_params: swap_info.channel_opening_fees, + }) + } + + pub async fn check_message(&self, req: CheckMessageRequest) -> SdkResult { + let is_valid = self + .node_api + .check_message(&req.message, &req.pubkey, &req.signature) + .await?; + Ok(CheckMessageResponse { is_valid }) + } + + pub async fn claim_reverse_swap(&self, lockup_address: String) -> SdkResult<()> { + Ok(self + .btc_send_swapper + .claim_reverse_swap(lockup_address) + .await?) + } + + pub async fn close_lsp_channels(&self) -> SdkResult> { + let lsp = self.lsp_info().await?; + let tx_ids = self.node_api.close_peer_channels(lsp.pubkey).await?; + self.sync().await?; + Ok(tx_ids) + } + + async fn closed_channel_to_transaction( + &self, + channel: crate::models::Channel, + ) -> Result { + let (payment_time, closing_txid) = match (channel.closed_at, channel.closing_txid.clone()) { + (Some(closed_at), Some(closing_txid)) => (closed_at as i64, Some(closing_txid)), + (_, _) => { + // If any of the two closing-related fields are empty, we look them up and persist them + let (maybe_closed_at, maybe_closing_txid) = + self.lookup_channel_closing_data(&channel).await?; + + let processed_closed_at = match maybe_closed_at { + None => { + warn!("Blocktime could not be determined for from closing outspend, defaulting closed_at to epoch time"); + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + } + Some(block_time) => block_time, + }; + + let mut updated_channel = channel.clone(); + updated_channel.closed_at = Some(processed_closed_at); + // If no closing txid found, we persist it as None, so it will be looked-up next time + updated_channel.closing_txid.clone_from(&maybe_closing_txid); + self.persister.insert_or_update_channel(updated_channel)?; + + (processed_closed_at as i64, maybe_closing_txid) + } + }; + + Ok(Payment { + id: channel.funding_txid.clone(), + payment_type: PaymentType::ClosedChannel, + payment_time, + amount_msat: channel.local_balance_msat, + fee_msat: 0, + status: match channel.state { + ChannelState::PendingClose => PaymentStatus::Pending, + _ => PaymentStatus::Complete, + }, + description: Some("Closed Channel".to_string()), + details: PaymentDetails::ClosedChannel { + data: ClosedChannelPaymentDetails { + short_channel_id: channel.short_channel_id, + state: channel.state, + funding_txid: channel.funding_txid, + closing_txid, + }, + }, + error: None, + metadata: None, + }) + } + + pub async fn connect_lsp(&self, lsp_id: String) -> SdkResult<()> { + let lsp_pubkey = match self.list_lsps().await?.iter().find(|lsp| lsp.id == lsp_id) { + Some(lsp) => lsp.pubkey.clone(), + None => { + return Err(SdkError::Generic { + err: format!("Unknown LSP: {lsp_id}"), + }) + } + }; + + self.persister.set_lsp(lsp_id, Some(lsp_pubkey))?; + self.sync().await?; + if let Some(webhook_url) = self.persister.get_webhook_url()? { + self.register_payment_notifications(webhook_url).await? + } + Ok(()) + } + + async fn connect_lsp_peer(&self, node_pubkey: String) -> SdkResult<()> { + let lsps = self.lsp_api.list_lsps(node_pubkey).await?; + let lsp = match self + .persister + .get_lsp_id()? + .and_then(|lsp_id| lsps.iter().find(|lsp| lsp.id == lsp_id)) + .or_else(|| lsps.first()) + { + Some(lsp) => lsp.clone(), + None => return Ok(()), + }; + + self.persister.set_lsp(lsp.id, Some(lsp.pubkey.clone()))?; + let node_state = match self.node_info() { + Ok(node_state) => node_state, + Err(_) => return Ok(()), + }; + + let node_id = lsp.pubkey; + let address = lsp.host; + let lsp_connected = node_state + .connected_peers + .iter() + .any(|e| e == node_id.as_str()); + if !lsp_connected { + debug!("connecting to lsp {}@{}", node_id.clone(), address.clone()); + self.node_api + .connect_peer(node_id.clone(), address.clone()) + .await + .map_err(|e| SdkError::ServiceConnectivity { + err: format!("(LSP: {node_id}) Failed to connect: {e}"), + })?; + debug!("connected to lsp {node_id}@{address}"); + } + + Ok(()) + } + + pub async fn configure_node(&self, req: ConfigureNodeRequest) -> SdkResult<()> { + Ok(self.node_api.configure_node(req.close_to_address).await?) + } + + pub async fn disconnect(&self) -> SdkResult<()> { + let mut started = self.started.lock().await; + ensure_sdk!( + *started, + SdkError::Generic { + err: "BreezServices is not running".into(), + } + ); + self.shutdown_sender + .send(()) + .map_err(|e| SdkError::Generic { + err: format!("Shutdown failed: {e}"), + })?; + *started = false; + Ok(()) + } + + async fn do_sync(&self, match_local_balance: bool) -> Result<()> { + let start = Instant::now(); + let node_pubkey = self.node_api.node_id().await?; + self.connect_lsp_peer(node_pubkey).await?; + + // First query the changes since last sync state. + let sync_state = self.persister.get_sync_state()?; + let new_data = &self + .node_api + .pull_changed(sync_state.clone(), match_local_balance) + .await?; + + debug!( + "pull changed old state={:?} new state={:?}", + sync_state, new_data.sync_state + ); + + // update node state and channels state + self.persister.set_node_state(&new_data.node_state)?; + + let channels_before_update = self.persister.list_channels()?; + self.persister.update_channels(&new_data.channels)?; + let channels_after_update = self.persister.list_channels()?; + + // Fetch the static backup if needed and persist it + if channels_before_update.len() != channels_after_update.len() { + info!("fetching static backup file from node"); + let backup = self.node_api.static_backup().await?; + self.persister.set_static_backup(backup)?; + } + + //fetch closed_channel and convert them to Payment items. + let mut closed_channel_payments: Vec = vec![]; + for closed_channel in + self.persister.list_channels()?.into_iter().filter(|c| { + c.state == ChannelState::Closed || c.state == ChannelState::PendingClose + }) + { + let closed_channel_tx = self.closed_channel_to_transaction(closed_channel).await?; + closed_channel_payments.push(closed_channel_tx); + } + + // update both closed channels and lightning transaction payments + let mut payments = closed_channel_payments; + payments.extend(new_data.payments.clone()); + self.persister.insert_or_update_payments(&payments)?; + let duration = start.elapsed(); + info!("Sync duration: {:?}", duration); + + // update the cached sync state + self.persister.set_sync_state(&new_data.sync_state)?; + self.notify_event_listeners(BreezEvent::Synced).await?; + Ok(()) + } + + pub async fn execute_dev_command(&self, command: String) -> SdkResult { + let dev_cmd_res = DevCommand::from_str(&command); + + match dev_cmd_res { + Ok(dev_cmd) => match dev_cmd { + DevCommand::GenerateDiagnosticData => self.generate_diagnostic_data().await, + }, + Err(_) => Ok(crate::serializer::to_string_pretty( + &self.node_api.execute_command(command).await?, + )?), + } + } + + pub async fn fetch_fiat_rates(&self) -> SdkResult> { + self.fiat_api.fetch_fiat_rates().await.map_err(Into::into) + } + + pub async fn fetch_lsp_info(&self, id: String) -> SdkResult> { + get_lsp_by_id(self.persister.clone(), self.lsp_api.clone(), id.as_str()).await + } + + pub async fn fetch_reverse_swap_fees( + &self, + req: ReverseSwapFeesRequest, + ) -> SdkResult { + let mut res = self.btc_send_swapper.fetch_reverse_swap_fees().await?; + + if let Some(amt) = req.send_amount_sat { + ensure_sdk!(amt <= res.max, SdkError::generic("Send amount is too high")); + ensure_sdk!(amt >= res.min, SdkError::generic("Send amount is too low")); + + if let Some(claim_tx_feerate) = req.claim_tx_feerate { + res.fees_claim = BTCSendSwap::calculate_claim_tx_fee(claim_tx_feerate)?; + } + + let service_fee_sat = swap_out::get_service_fee_sat(amt, res.fees_percentage); + res.total_fees = Some(service_fee_sat + res.fees_lockup + res.fees_claim); + } + + Ok(res) + } + + pub async fn generate_diagnostic_data(&self) -> SdkResult { + let now_sec = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or_default(); + let node_data = self + .node_api + .generate_diagnostic_data() + .await + .unwrap_or_else(|e| json!({"error": e.to_string()})); + let sdk_data = self + .generate_sdk_diagnostic_data() + .await + .unwrap_or_else(|e| json!({"error": e.to_string()})); + let result = json!({ + "timestamp": now_sec, + "node": node_data, + "sdk": sdk_data + }); + Ok(crate::serializer::to_string_pretty(&result)?) + } + + async fn generate_sdk_diagnostic_data(&self) -> SdkResult { + let (sdk_version, sdk_git_hash) = Self::get_sdk_version(); + let version = format!("SDK v{sdk_version} ({sdk_git_hash})"); + let state = crate::serializer::value::to_value(&self.persister.get_node_state()?)?; + let payments = crate::serializer::value::to_value( + &self + .persister + .list_payments(ListPaymentsRequest::default())?, + )?; + let channels = crate::serializer::value::to_value(&self.persister.list_channels()?)?; + let settings = crate::serializer::value::to_value(&self.persister.list_settings()?)?; + let reverse_swaps = crate::serializer::value::to_value( + self.persister.list_reverse_swaps().map(sanitize_vec)?, + )?; + let swaps = + crate::serializer::value::to_value(self.persister.list_swaps().map(sanitize_vec)?)?; + let lsp_id = crate::serializer::value::to_value(&self.persister.get_lsp_id()?)?; + + let res = json!({ + "version": version, + "node_state": state, + "payments": payments, + "channels": channels, + "settings": settings, + "reverse_swaps": reverse_swaps, + "swaps": swaps, + "lsp_id": lsp_id, + }); + Ok(res) + } + + fn get_trampoline_id( + &self, + req: &SendPaymentRequest, + invoice: &LNInvoice, + ) -> Result>, SendPaymentError> { + // If trampoline is turned off, return immediately + if !req.use_trampoline { + return Ok(None); + } + + // Get the persisted LSP id. If no LSP, return early. + let lsp_pubkey = match self.persister.get_lsp_pubkey()? { + Some(lsp_pubkey) => lsp_pubkey, + None => return Ok(None), + }; + + // If the LSP is in the routing hint, don't use trampoline, but rather + // pay directly to the destination. + if invoice.routing_hints.iter().any(|hint| { + hint.hops + .last() + .map(|hop| hop.src_node_id == lsp_pubkey) + .unwrap_or(false) + }) { + return Ok(None); + } + + // If ended up here, this payment will attempt trampoline. + Ok(Some(hex::decode(lsp_pubkey).map_err(|_| { + SendPaymentError::Generic { + err: "failed to decode lsp pubkey".to_string(), + } + })?)) + } + + pub async fn in_progress_onchain_payments(&self) -> SdkResult> { + #[allow(deprecated)] + self.in_progress_reverse_swaps().await + } + + #[deprecated(note = "use in_progress_onchain_payments instead")] + pub async fn in_progress_reverse_swaps(&self) -> SdkResult> { + let full_rsis = self.btc_send_swapper.list_blocking().await?; + + let mut rsis = vec![]; + for full_rsi in full_rsis { + let rsi = self + .btc_send_swapper + .convert_reverse_swap_info(full_rsi) + .await?; + rsis.push(rsi); + } + + Ok(rsis) + } + + pub async fn in_progress_swap(&self) -> SdkResult> { + let tip = self.chain_service.current_tip().await?; + self.btc_receive_swapper.rescan_monitored_swaps(tip).await?; + let in_progress = self.btc_receive_swapper.list_in_progress()?; + if !in_progress.is_empty() { + return Ok(Some(in_progress[0].clone())); + } + Ok(None) + } + + async fn init_chainservice_urls(&self) -> Result<()> { + let breez_server = Arc::new(BreezServer::new( + PRODUCTION_BREEZSERVER_URL.to_string(), + None, + )?); + let persister = &self.persister; + + let cloned_breez_server = breez_server.clone(); + let cloned_persister = persister.clone(); + tokio::spawn(async move { + match cloned_breez_server.fetch_mempoolspace_urls().await { + Ok(fresh_urls) => { + if let Err(e) = cloned_persister.set_mempoolspace_base_urls(fresh_urls) { + error!("Failed to cache mempool.space URLs: {e}"); + } + } + Err(e) => error!("Failed to fetch mempool.space URLs: {e}"), + } + }); + + Ok(()) + } + + pub async fn list_fiat_currencies(&self) -> SdkResult> { + self.fiat_api + .list_fiat_currencies() + .await + .map_err(Into::into) + } + + pub async fn list_lsps(&self) -> SdkResult> { + self.lsp_api.list_lsps(self.node_info()?.id).await + } + + pub async fn list_payments(&self, req: ListPaymentsRequest) -> SdkResult> { + Ok(self.persister.list_payments(req)?) + } + + pub async fn list_refundables(&self) -> SdkResult> { + Ok(self.btc_receive_swapper.list_refundables()?) + } + + pub async fn lnurl_auth( + &self, + req_data: LnUrlAuthRequestData, + ) -> Result { + Ok(perform_lnurl_auth(&req_data, &SdkLnurlAuthSigner::new(self.node_api.clone())).await?) + } + + pub async fn lnurl_pay(&self, req: LnUrlPayRequest) -> Result { + match validate_lnurl_pay( + req.amount_msat, + &req.comment, + &req.data, + self.config.network, + req.validate_success_action_url, + ) + .await? + { + ValidatedCallbackResponse::EndpointError { data: e } => { + Ok(LnUrlPayResult::EndpointError { data: e }) + } + ValidatedCallbackResponse::EndpointSuccess { data: cb } => { + let pay_req = SendPaymentRequest { + bolt11: cb.pr.clone(), + amount_msat: None, + use_trampoline: req.use_trampoline, + label: req.payment_label, + }; + let invoice = parse_invoice(cb.pr.as_str())?; + + let payment = match self.send_payment(pay_req).await { + Ok(p) => Ok(p), + e @ Err( + SendPaymentError::InvalidInvoice { .. } + | SendPaymentError::ServiceConnectivity { .. }, + ) => e, + Err(e) => { + return Ok(LnUrlPayResult::PayError { + data: LnUrlPayErrorData { + payment_hash: invoice.payment_hash, + reason: e.to_string(), + }, + }) + } + }? + .payment; + let details = match &payment.details { + PaymentDetails::ClosedChannel { .. } => { + return Err(LnUrlPayError::Generic { + err: "Payment lookup found unexpected payment type".into(), + }); + } + PaymentDetails::Ln { data } => data, + }; + + let maybe_sa_processed: Option = match cb.success_action { + Some(sa) => { + let processed_sa = match sa { + // For AES, we decrypt the contents on the fly + SuccessAction::Aes { data } => { + let preimage = sha256::Hash::from_str(&details.payment_preimage)?; + let preimage_arr: [u8; 32] = preimage.into_inner(); + let result = match (data, &preimage_arr).try_into() { + Ok(data) => AesSuccessActionDataResult::Decrypted { data }, + Err(e) => AesSuccessActionDataResult::ErrorStatus { + reason: e.to_string(), + }, + }; + SuccessActionProcessed::Aes { result } + } + SuccessAction::Message { data } => { + SuccessActionProcessed::Message { data } + } + SuccessAction::Url { data } => SuccessActionProcessed::Url { data }, + }; + Some(processed_sa) + } + None => None, + }; + + let lnurl_pay_domain = match req.data.ln_address { + Some(_) => None, + None => Some(req.data.domain), + }; + // Store SA (if available) + LN Address in separate table, associated to payment_hash + self.persister.insert_payment_external_info( + &details.payment_hash, + PaymentExternalInfo { + lnurl_pay_success_action: maybe_sa_processed.clone(), + lnurl_pay_domain, + lnurl_pay_comment: req.comment, + lnurl_metadata: Some(req.data.metadata_str), + ln_address: req.data.ln_address, + lnurl_withdraw_endpoint: None, + attempted_amount_msat: invoice.amount_msat, + attempted_error: None, + }, + )?; + + Ok(LnUrlPayResult::EndpointSuccess { + data: lnurl::pay::LnUrlPaySuccessData { + payment, + success_action: maybe_sa_processed, + }, + }) + } + } + } + + pub async fn lnurl_withdraw( + &self, + req: LnUrlWithdrawRequest, + ) -> Result { + let invoice = self + .receive_payment(ReceivePaymentRequest { + amount_msat: req.amount_msat, + description: req.description.unwrap_or_default(), + use_description_hash: Some(false), + ..Default::default() + }) + .await? + .ln_invoice; + + let lnurl_w_endpoint = req.data.callback.clone(); + let res = validate_lnurl_withdraw(req.data, invoice).await?; + + if let LnUrlWithdrawResult::Ok { ref data } = res { + // If endpoint was successfully called, store the LNURL-withdraw endpoint URL as metadata linked to the invoice + self.persister.insert_payment_external_info( + &data.invoice.payment_hash, + PaymentExternalInfo { + lnurl_pay_success_action: None, + lnurl_pay_domain: None, + lnurl_pay_comment: None, + lnurl_metadata: None, + ln_address: None, + lnurl_withdraw_endpoint: Some(lnurl_w_endpoint), + attempted_amount_msat: None, + attempted_error: None, + }, + )?; + } + + Ok(res) + } + + async fn lookup_chain_service_closing_outspend( + &self, + channel: crate::models::Channel, + ) -> Result> { + match channel.funding_outnum { + None => Ok(None), + Some(outnum) => { + // Find the output tx that was used to fund the channel + let outspends = self + .chain_service + .transaction_outspends(channel.funding_txid.clone()) + .await?; + + Ok(outspends.get(outnum as usize).cloned()) + } + } + } + + async fn lookup_channel_closing_data( + &self, + channel: &crate::models::Channel, + ) -> Result<(Option, Option)> { + let maybe_outspend_res = self + .lookup_chain_service_closing_outspend(channel.clone()) + .await; + let maybe_outspend: Option = match maybe_outspend_res { + Ok(s) => s, + Err(e) => { + error!("Failed to lookup channel closing data: {:?}", e); + None + } + }; + + let maybe_closed_at = maybe_outspend + .clone() + .and_then(|outspend| outspend.status) + .and_then(|s| s.block_time); + let maybe_closing_txid = maybe_outspend.and_then(|outspend| outspend.txid); + + Ok((maybe_closed_at, maybe_closing_txid)) + } + + pub async fn lsp_id(&self) -> SdkResult> { + Ok(self.persister.get_lsp_id()?) + } + + pub async fn lsp_info(&self) -> SdkResult { + get_lsp(self.persister.clone(), self.lsp_api.clone()).await + } + + #[deprecated(note = "use onchain_payment_limits instead")] + pub async fn max_reverse_swap_amount(&self) -> SdkResult { + // fetch the last hop hints from the swapper + let last_hop = self.btc_send_swapper.last_hop_for_payment().await?; + info!("max_reverse_swap_amount last_hop={:?}", last_hop); + // calculate the largest payment we can send over this route using maximum 3 hops + // as follows: + // User Node -> LSP Node -> Routing Node -> Swapper Node + let max_to_pay = self + .node_api + .max_sendable_amount( + Some( + hex::decode(&last_hop.src_node_id).map_err(|e| SdkError::Generic { + err: format!("Failed to decode hex node_id: {e}"), + })?, + ), + swap_out::reverseswap::MAX_PAYMENT_PATH_HOPS, + Some(&last_hop), + ) + .await?; + + // Sum the max amount per channel and return the result + let total_msat: u64 = max_to_pay.into_iter().map(|m| m.amount_msat).sum(); + let total_sat = total_msat / 1000; + Ok(MaxReverseSwapAmountResponse { total_sat }) + } + + pub fn node_credentials(&self) -> SdkResult> { + Ok(self.node_api.node_credentials()?) + } + + pub fn node_info(&self) -> SdkResult { + self.persister.get_node_state()?.ok_or(SdkError::Generic { + err: "Node info not found".into(), + }) + } + + async fn notify_event_listeners(&self, e: BreezEvent) -> Result<()> { + if let Err(err) = self.btc_receive_swapper.on_event(e.clone()).await { + debug!( + "btc_receive_swapper failed to process event {:?}: {:?}", + e, err + ) + }; + if let Err(err) = self.btc_send_swapper.on_event(e.clone()).await { + debug!( + "btc_send_swapper failed to process event {:?}: {:?}", + e, err + ) + }; + + if self.event_listener.is_some() { + self.event_listener.as_ref().unwrap().on_event(e.clone()) + } + Ok(()) + } + + async fn on_event(&self, e: BreezEvent) -> Result<()> { + debug!("breez services got event {:?}", e); + self.notify_event_listeners(e.clone()).await + } + + async fn on_payment_completed( + &self, + node_id: String, + invoice: Option, + label: Option, + payment_res: Result, + ) -> Result { + self.do_sync(false).await?; + match payment_res { + Ok(payment) => { + self.notify_event_listeners(BreezEvent::PaymentSucceed { + details: payment.clone(), + }) + .await?; + Ok(payment) + } + Err(e) => { + if let Some(invoice) = invoice.clone() { + self.persister.update_payment_attempted_error( + &invoice.payment_hash, + Some(e.to_string()), + )?; + } + self.notify_event_listeners(BreezEvent::PaymentFailed { + details: PaymentFailedData { + error: e.to_string(), + node_id, + invoice, + label, + }, + }) + .await?; + Err(e) + } + } + } + + pub async fn onchain_payment_limits(&self) -> SdkResult { + let fee_info = self.btc_send_swapper.fetch_reverse_swap_fees().await?; + debug!("Reverse swap pair info: {fee_info:?}"); + #[allow(deprecated)] + let max_amt_current_channels = self.max_reverse_swap_amount().await?; + debug!("Max send amount possible with current channels: {max_amt_current_channels:?}"); + + Ok(OnchainPaymentLimitsResponse { + min_sat: fee_info.min, + max_sat: fee_info.max, + max_payable_sat: max_amt_current_channels.total_sat, + }) + } + + pub async fn open_channel_fee( + &self, + req: OpenChannelFeeRequest, + ) -> SdkResult { + let lsp_info = self.lsp_info().await?; + let fee_params = lsp_info + .cheapest_open_channel_fee(req.expiry.unwrap_or(INVOICE_PAYMENT_FEE_EXPIRY_SECONDS))? + .clone(); + + let node_state = self.node_info()?; + let fee_msat = req.amount_msat.map(|req_amount_msat| { + match node_state.max_receivable_single_payment_amount_msat >= req_amount_msat { + // In case we have enough inbound liquidity we return zero fee. + true => 0, + // Otherwise we need to calculate the fee for opening a new channel. + false => fee_params.get_channel_fees_msat_for(req_amount_msat), + } + }); + + Ok(OpenChannelFeeResponse { + fee_msat, + fee_params, + }) + } + + pub async fn pay_onchain( + &self, + req: PayOnchainRequest, + ) -> Result { + ensure_sdk!( + req.prepare_res.sender_amount_sat > req.prepare_res.recipient_amount_sat, + SendOnchainError::generic("Send amount must be bigger than receive amount") + ); + + let reverse_swap_info = self + .pay_onchain_common(CreateReverseSwapArg::V2(req)) + .await?; + Ok(PayOnchainResponse { reverse_swap_info }) + } + + async fn pay_onchain_common(&self, req: CreateReverseSwapArg) -> SdkResult { + ensure_sdk!(self.in_progress_onchain_payments().await?.is_empty(), SdkError::Generic { err: + "You can only start a new one after after the ongoing ones finish. \ + Use the in_progress_reverse_swaps method to get an overview of currently ongoing reverse swaps".into(), + }); + + let full_rsi = self.btc_send_swapper.create_reverse_swap(req).await?; + let reverse_swap_info = self + .btc_send_swapper + .convert_reverse_swap_info(full_rsi.clone()) + .await?; + self.do_sync(false).await?; + + if let Some(webhook_url) = self.persister.get_webhook_url()? { + let address = &full_rsi + .get_lockup_address(self.config.network)? + .to_string(); + info!("Registering for onchain tx notification for address {address}"); + self.register_onchain_tx_notification(address, &webhook_url) + .await?; + } + Ok(reverse_swap_info) + } + + pub async fn payment_by_hash(&self, hash: String) -> SdkResult> { + Ok(self.persister.get_payment_by_hash(&hash)?) + } + + fn persist_pending_payment( + &self, + invoice: &LNInvoice, + amount_msat: u64, + label: Option, + ) -> Result<(), SendPaymentError> { + self.persister.insert_or_update_payments(&[Payment { + id: invoice.payment_hash.clone(), + payment_type: PaymentType::Sent, + payment_time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64, + amount_msat, + fee_msat: 0, + status: PaymentStatus::Pending, + error: None, + description: invoice.description.clone(), + details: PaymentDetails::Ln { + data: LnPaymentDetails { + payment_hash: invoice.payment_hash.clone(), + label: label.unwrap_or_default(), + destination_pubkey: invoice.payee_pubkey.clone(), + payment_preimage: String::new(), + keysend: false, + bolt11: invoice.bolt11.clone(), + lnurl_success_action: None, + lnurl_pay_domain: None, + lnurl_pay_comment: None, + ln_address: None, + lnurl_metadata: None, + lnurl_withdraw_endpoint: None, + swap_info: None, + reverse_swap_info: None, + pending_expiration_block: None, + open_channel_bolt11: None, + }, + }, + metadata: None, + }])?; + + self.persister.insert_payment_external_info( + &invoice.payment_hash, + PaymentExternalInfo { + lnurl_pay_success_action: None, + lnurl_pay_domain: None, + lnurl_pay_comment: None, + lnurl_metadata: None, + ln_address: None, + lnurl_withdraw_endpoint: None, + attempted_amount_msat: invoice.amount_msat.map_or(Some(amount_msat), |_| None), + attempted_error: None, + }, + )?; + Ok(()) + } + + pub async fn prepare_onchain_payment( + &self, + req: PrepareOnchainPaymentRequest, + ) -> Result { + let fees_claim = BTCSendSwap::calculate_claim_tx_fee(req.claim_tx_feerate)?; + BTCSendSwap::validate_claim_tx_fee(fees_claim)?; + + let fee_info = self.btc_send_swapper.fetch_reverse_swap_fees().await?; + + // Calculate (send_amt, recv_amt) from the inputs and fees + let fees_lockup = fee_info.fees_lockup; + let p = fee_info.fees_percentage; + let fees_claim = BTCSendSwap::calculate_claim_tx_fee(req.claim_tx_feerate)?; + let (send_amt, recv_amt) = match req.amount_type { + SwapAmountType::Send => { + let temp_send_amt = req.amount_sat; + let service_fees = swap_out::get_service_fee_sat(temp_send_amt, p); + let total_fees = service_fees + fees_lockup + fees_claim; + ensure_sdk!( + temp_send_amt > total_fees, + SendOnchainError::generic( + "Send amount is not high enough to account for all fees" + ) + ); + + (temp_send_amt, temp_send_amt - total_fees) + } + SwapAmountType::Receive => { + let temp_recv_amt = req.amount_sat; + let send_amt_minus_service_fee = temp_recv_amt + fees_lockup + fees_claim; + let temp_send_amt = swap_out::get_invoice_amount_sat(send_amt_minus_service_fee, p); + + (temp_send_amt, temp_recv_amt) + } + }; + + let is_send_in_range = send_amt >= fee_info.min && send_amt <= fee_info.max; + ensure_sdk!(is_send_in_range, SendOnchainError::OutOfRange); + + Ok(PrepareOnchainPaymentResponse { + fees_hash: fee_info.fees_hash.clone(), + fees_percentage: p, + fees_lockup, + fees_claim, + sender_amount_sat: send_amt, + recipient_amount_sat: recv_amt, + total_fees: send_amt - recv_amt, + }) + } + + pub async fn prepare_redeem_onchain_funds( + &self, + req: PrepareRedeemOnchainFundsRequest, + ) -> RedeemOnchainResult { + let response = self.node_api.prepare_redeem_onchain_funds(req).await?; + Ok(response) + } + + pub async fn prepare_refund( + &self, + req: PrepareRefundRequest, + ) -> SdkResult { + Ok(self.btc_receive_swapper.prepare_refund_swap(req).await?) + } + + pub async fn receive_onchain( + &self, + req: ReceiveOnchainRequest, + ) -> ReceiveOnchainResult { + if let Some(in_progress) = self.in_progress_swap().await? { + return Err(ReceiveOnchainError::SwapInProgress{ err:format!( + "A swap was detected for address {}. Use in_progress_swap method to get the current swap state", + in_progress.bitcoin_address + )}); + } + let channel_opening_fees = req.opening_fee_params.unwrap_or( + self.lsp_info() + .await? + .cheapest_open_channel_fee(SWAP_PAYMENT_FEE_EXPIRY_SECONDS)? + .clone(), + ); + + let swap_info = self + .btc_receive_swapper + .create_swap_address(channel_opening_fees) + .await?; + if let Some(webhook_url) = self.persister.get_webhook_url()? { + let address = &swap_info.bitcoin_address; + info!("Registering for onchain tx notification for address {address}"); + self.register_onchain_tx_notification(address, &webhook_url) + .await?; + } + Ok(swap_info) + } + + pub async fn receive_payment( + &self, + req: ReceivePaymentRequest, + ) -> Result { + self.payment_receiver.receive_payment(req).await + } + + pub async fn recommended_fees(&self) -> SdkResult { + self.chain_service.recommended_fees().await + } + + pub async fn redeem_onchain_funds( + &self, + req: RedeemOnchainFundsRequest, + ) -> RedeemOnchainResult { + let txid = self + .node_api + .redeem_onchain_funds(req.to_address, req.sat_per_vbyte) + .await?; + self.sync().await?; + Ok(RedeemOnchainFundsResponse { txid }) + } + + pub async fn redeem_swap(&self, swap_address: String) -> SdkResult<()> { + let tip = self.chain_service.current_tip().await?; + self.btc_receive_swapper + .refresh_swap_on_chain_status(swap_address.clone(), tip) + .await?; + self.btc_receive_swapper.redeem_swap(swap_address).await?; + Ok(()) + } + + pub async fn refund(&self, req: RefundRequest) -> SdkResult { + Ok(self.btc_receive_swapper.refund_swap(req).await?) + } + + async fn register_onchain_tx_notification( + &self, + address: &str, + webhook_url: &str, + ) -> SdkResult<()> { + get_reqwest_client()? + .post(format!("{}/api/v1/register", self.config.chainnotifier_url)) + .header(CONTENT_TYPE, "application/json") + .body(Body::from( + json!({ + "address": address, + "webhook": webhook_url + }) + .to_string(), + )) + .send() + .await + .map(|_| ()) + .map_err(|e| SdkError::ServiceConnectivity { + err: format!("Failed to register for tx confirmation notifications: {e}"), + }) + } + + async fn register_payment_notifications(&self, webhook_url: String) -> SdkResult<()> { + let message = webhook_url.clone(); + let sign_request = SignMessageRequest { message }; + let sign_response = self.sign_message(sign_request).await?; + + // Attempt register call for all relevant LSPs + let mut error_found = false; + for lsp_info in get_notification_lsps( + self.persister.clone(), + self.lsp_api.clone(), + self.node_api.clone(), + ) + .await? + { + let lsp_id = lsp_info.id; + let res = self + .lsp_api + .register_payment_notifications( + lsp_id.clone(), + lsp_info.lsp_pubkey, + webhook_url.clone(), + sign_response.signature.clone(), + ) + .await; + if res.is_err() { + error_found = true; + warn!("Failed to register notifications for LSP {lsp_id}: {res:?}"); + } + } + + match error_found { + true => Err(SdkError::generic( + "Failed to register notifications for at least one LSP, see logs for details", + )), + false => Ok(()), + } + } + + pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> { + info!("Registering for webhook notifications"); + let is_new_webhook_url = match self.persister.get_webhook_url()? { + None => true, + Some(cached_webhook_url) => cached_webhook_url != webhook_url, + }; + match is_new_webhook_url { + false => debug!("Webhook URL not changed, no need to (re-)register for monitored swap tx notifications"), + true => { + for swap in self + .btc_receive_swapper + .list_monitored()? + .iter() + .filter(|swap| !swap.refundable()) + { + let swap_address = &swap.bitcoin_address; + info!("Found non-refundable monitored swap with address {swap_address}, registering for onchain tx notifications"); + self.register_onchain_tx_notification(swap_address, &webhook_url) + .await?; + } + + for rev_swap in self + .btc_send_swapper + .list_monitored() + .await? + .iter() + { + let lockup_address = &rev_swap.get_lockup_address(self.config.network)?.to_string(); + info!("Found monitored reverse swap with address {lockup_address}, registering for onchain tx notifications"); + self.register_onchain_tx_notification(lockup_address, &webhook_url) + .await?; + } + } + } + + // Register for LN payment notifications on every call, since these webhook registrations + // timeout after 14 days of not being used + self.register_payment_notifications(webhook_url.clone()) + .await?; + + // Only cache the webhook URL if callbacks were successfully registered for it. + // If any step above failed, not caching it allows the caller to re-trigger the registrations + // by calling the method again + self.persister.set_webhook_url(webhook_url)?; + Ok(()) + } + + pub async fn report_issue(&self, req: ReportIssueRequest) -> SdkResult<()> { + match self.persister.get_node_state()? { + Some(node_state) => match req { + ReportIssueRequest::PaymentFailure { data } => { + let payment = self + .persister + .get_payment_by_hash(&data.payment_hash)? + .ok_or(SdkError::Generic { + err: "Payment not found".into(), + })?; + let lsp_id = self.persister.get_lsp_id()?; + + self.support_api + .report_payment_failure(node_state, payment, lsp_id, data.comment) + .await + } + }, + None => Err(SdkError::Generic { + err: "Node state not found".into(), + }), + } + } + + pub async fn rescan_swaps(&self) -> SdkResult<()> { + let tip = self.chain_service.current_tip().await?; + self.btc_receive_swapper.rescan_swaps(tip).await?; + Ok(()) + } + + #[deprecated(note = "use pay_onchain instead")] + pub async fn send_onchain( + &self, + req: SendOnchainRequest, + ) -> Result { + let reverse_swap_info = self + .pay_onchain_common(CreateReverseSwapArg::V1(req)) + .await?; + Ok(SendOnchainResponse { reverse_swap_info }) + } + + pub async fn send_payment( + &self, + req: SendPaymentRequest, + ) -> Result { + let parsed_invoice = parse_invoice(req.bolt11.as_str())?; + let invoice_expiration = parsed_invoice.timestamp + parsed_invoice.expiry; + let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + if invoice_expiration < current_time { + return Err(SendPaymentError::InvoiceExpired { + err: format!("Invoice expired at {}", invoice_expiration), + }); + } + let invoice_amount_msat = parsed_invoice.amount_msat.unwrap_or_default(); + let provided_amount_msat = req.amount_msat.unwrap_or_default(); + + // Valid the invoice network against the config network + validate_network(parsed_invoice.clone(), self.config.network)?; + + let amount_msat = match (provided_amount_msat, invoice_amount_msat) { + (0, 0) => { + return Err(SendPaymentError::InvalidAmount { + err: "Amount must be provided when paying a zero invoice".into(), + }) + } + (0, amount_msat) => amount_msat, + (amount_msat, 0) => amount_msat, + (_amount_1, _amount_2) => { + return Err(SendPaymentError::InvalidAmount { + err: "Amount should not be provided when paying a non zero invoice".into(), + }) + } + }; + + if self + .persister + .get_completed_payment_by_hash(&parsed_invoice.payment_hash)? + .is_some() + { + return Err(SendPaymentError::AlreadyPaid); + } + + // If there is an lsp, the invoice route hint does not contain the + // lsp in the hint, and trampoline payments are requested, attempt a + // trampoline payment. + let maybe_trampoline_id = self.get_trampoline_id(&req, &parsed_invoice)?; + + self.persist_pending_payment(&parsed_invoice, amount_msat, req.label.clone())?; + + // If trampoline is an option, try trampoline first. + let trampoline_result = if let Some(trampoline_id) = maybe_trampoline_id { + debug!("attempting trampoline payment"); + match self + .node_api + .send_trampoline_payment( + parsed_invoice.bolt11.clone(), + amount_msat, + req.label.clone(), + trampoline_id, + ) + .await + { + Ok(res) => Some(res), + Err(e) => { + warn!("trampoline payment failed: {:?}", e); + None + } + } + } else { + debug!("not attempting trampoline payment"); + None + }; + + // If trampoline failed or didn't happen, fall back to regular payment. + let payment_res = match trampoline_result { + Some(res) => Ok(res), + None => { + debug!("attempting normal payment"); + self.node_api + .send_payment( + parsed_invoice.bolt11.clone(), + req.amount_msat, + req.label.clone(), + ) + .map_err(Into::into) + .await + } + }; + + debug!("payment returned {:?}", payment_res); + let payment = self + .on_payment_completed( + parsed_invoice.payee_pubkey.clone(), + Some(parsed_invoice), + req.label, + payment_res, + ) + .await?; + Ok(SendPaymentResponse { payment }) + } + + pub async fn send_spontaneous_payment( + &self, + req: SendSpontaneousPaymentRequest, + ) -> Result { + let payment_res = self + .node_api + .send_spontaneous_payment( + req.node_id.clone(), + req.amount_msat, + req.extra_tlvs, + req.label.clone(), + ) + .map_err(Into::into) + .await; + let payment = self + .on_payment_completed(req.node_id, None, req.label, payment_res) + .await?; + Ok(SendPaymentResponse { payment }) + } + + pub async fn set_payment_metadata(&self, hash: String, metadata: String) -> SdkResult<()> { + Ok(self + .persister + .set_payment_external_metadata(hash, metadata)?) + } + + pub async fn sign_message(&self, req: SignMessageRequest) -> SdkResult { + let signature = self.node_api.sign_message(&req.message).await?; + Ok(SignMessageResponse { signature }) + } + + async fn start(self: &Arc) -> BreezServicesResult<()> { + let mut started = self.started.lock().await; + ensure_sdk!( + !*started, + ConnectError::Generic { + err: "BreezServices already started".into() + } + ); + + let start = Instant::now(); + self.start_background_tasks().await?; + let start_duration = start.elapsed(); + info!("SDK initialized in: {start_duration:?}"); + *started = true; + Ok(()) + } + + async fn start_background_tasks(self: &Arc) -> SdkResult<()> { + // start the signer + let (shutdown_signer_sender, signer_signer_receiver) = mpsc::channel(1); + self.start_signer(signer_signer_receiver).await; + self.start_node_keep_alive(self.shutdown_receiver.clone()) + .await; + + // Sync node state + match self.persister.get_node_state()? { + Some(node) => { + info!("Starting existing node {}", node.id); + self.connect_lsp_peer(node.id).await?; + } + None => { + // In case it is a first run we sync in foreground to get the node state. + info!("First run, syncing in foreground"); + self.sync().await?; + info!("First run, finished running syncing in foreground"); + } + } + + // start backup watcher + self.start_backup_watcher().await?; + + //track backup events + self.track_backup_events().await; + + //track swap events + self.track_swap_events().await; + + // track paid invoices + self.track_invoices().await; + + // track new blocks + self.track_new_blocks().await; + + // track logs + self.track_logs().await; + + // Stop signer on shutdown + let mut shutdown_receiver = self.shutdown_receiver.clone(); + tokio::spawn(async move { + // start the backup watcher + _ = shutdown_receiver.changed().await; + _ = shutdown_signer_sender.send(()).await; + debug!("Received the signal to exit event polling loop"); + }); + + self.init_chainservice_urls().await?; + + Ok(()) + } + + async fn start_backup_watcher(self: &Arc) -> Result<()> { + self.backup_watcher + .start(self.shutdown_receiver.clone()) + .await + .map_err(|e| anyhow!("Failed to start backup watcher: {e}"))?; + + // Restore backup state and request backup on start if needed + let force_backup = self + .persister + .get_last_sync_version() + .map_err(|e| anyhow!("Failed to read last sync version: {e}"))? + .is_none(); + self.backup_watcher + .request_backup(BackupRequest::new(force_backup)) + .await + .map_err(|e| anyhow!("Failed to request backup: {e}")) + } + + async fn start_node_keep_alive( + self: &Arc, + shutdown_receiver: watch::Receiver<()>, + ) { + let cloned = self.clone(); + tokio::spawn(async move { + cloned.node_api.start_keep_alive(shutdown_receiver).await; + }); + } + + async fn start_signer( + self: &Arc, + shutdown_receiver: mpsc::Receiver<()>, + ) { + let signer_api = self.clone(); + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + signer_api.node_api.start_signer(shutdown_receiver).await; + }); + } + + pub async fn sync(&self) -> SdkResult<()> { + Ok(self.do_sync(false).await?) + } + + async fn track_backup_events(self: &Arc) { + let cloned = self.clone(); + tokio::spawn(async move { + let mut events_stream = cloned.backup_watcher.subscribe_events(); + let mut shutdown_receiver = cloned.shutdown_receiver.clone(); + loop { + tokio::select! { + backup_event = events_stream.recv() => { + if let Ok(e) = backup_event { + if let Err(err) = cloned.notify_event_listeners(e).await { + error!("error handling backup event: {:?}", err); + } + } + let backup_status = cloned.backup_status(); + info!("backup status: {:?}", backup_status); + }, + _ = shutdown_receiver.changed() => { + debug!("Backup watcher task completed"); + break; + } + } + } + }); + } + + async fn track_invoices(self: &Arc) { + let cloned = self.clone(); + tokio::spawn(async move { + let mut shutdown_receiver = cloned.shutdown_receiver.clone(); + loop { + if shutdown_receiver.has_changed().unwrap_or(true) { + return; + } + let invoice_stream_res = cloned.node_api.stream_incoming_payments().await; + if let Ok(mut invoice_stream) = invoice_stream_res { + loop { + tokio::select! { + paid_invoice_res = invoice_stream.message() => { + match paid_invoice_res { + Ok(Some(i)) => { + debug!("invoice stream got new invoice"); + if let Some(gl_client::signer::model::greenlight::incoming_payment::Details::Offchain(p)) = i.details { + let mut payment: Option = p.clone().try_into().ok(); + if let Some(ref p) = payment { + let res = cloned + .persister + .insert_or_update_payments(&vec![p.clone()]); + debug!("paid invoice was added to payments list {res:?}"); + if let Ok(Some(mut node_info)) = cloned.persister.get_node_state() { + node_info.channels_balance_msat += p.amount_msat; + let res = cloned.persister.set_node_state(&node_info); + debug!("channel balance was updated {res:?}"); + } + payment = cloned.persister.get_payment_by_hash(&p.id).unwrap_or(payment); + } + _ = cloned.on_event(BreezEvent::InvoicePaid { + details: InvoicePaidDetails { + payment_hash: hex::encode(p.payment_hash), + bolt11: p.bolt11, + payment, + }, + }).await; + if let Err(e) = cloned.do_sync(true).await { + error!("failed to sync after paid invoice: {:?}", e); + } + } + } + Ok(None) => { + debug!("invoice stream got None"); + break; + } + Err(err) => { + debug!("invoice stream got error: {:?}", err); + break; + } + } + } + + _ = shutdown_receiver.changed() => { + debug!("Invoice tracking task has completed"); + return; + } + } + } + } + sleep(Duration::from_secs(1)).await; + } + }); + } + + async fn track_logs(self: &Arc) { + let cloned = self.clone(); + tokio::spawn(async move { + let mut shutdown_receiver = cloned.shutdown_receiver.clone(); + loop { + if shutdown_receiver.has_changed().unwrap_or(true) { + return; + } + let log_stream_res = cloned.node_api.stream_log_messages().await; + if let Ok(mut log_stream) = log_stream_res { + loop { + tokio::select! { + log_message_res = log_stream.message() => { + match log_message_res { + Ok(Some(l)) => { + info!("node-logs: {}", l.line); + }, + // stream is closed, renew it + Ok(None) => { + break; + } + Err(err) => { + debug!("failed to process log entry {:?}", err); + break; + } + }; + } + + _ = shutdown_receiver.changed() => { + debug!("Track logs task has completed"); + return; + } + } + } + } + sleep(Duration::from_secs(1)).await; + } + }); + } + + async fn track_new_blocks(self: &Arc) { + let cloned = self.clone(); + tokio::spawn(async move { + let mut current_block: u32 = 0; + let mut shutdown_receiver = cloned.shutdown_receiver.clone(); + let mut interval = tokio::time::interval(Duration::from_secs(30)); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + loop { + tokio::select! { + _ = interval.tick() => { + let tip_res = cloned.chain_service.current_tip().await; + match tip_res { + Ok(next_block) => { + debug!("got tip {:?}", next_block); + if next_block > current_block { + _ = cloned.sync().await; + _ = cloned.on_event(BreezEvent::NewBlock{block: next_block}).await; + } + current_block = next_block + }, + Err(e) => { + error!("failed to fetch next block {}", e) + } + }; + } + + _ = shutdown_receiver.changed() => { + debug!("New blocks task has completed"); + return; + } + } + } + }); + } + + async fn track_swap_events(self: &Arc) { + let cloned = self.clone(); + tokio::spawn(async move { + let mut swap_events_stream = cloned.btc_receive_swapper.subscribe_status_changes(); + let mut rev_swap_events_stream = cloned.btc_send_swapper.subscribe_status_changes(); + let mut shutdown_receiver = cloned.shutdown_receiver.clone(); + loop { + tokio::select! { + swap_event = swap_events_stream.recv() => { + if let Ok(e) = swap_event { + if let Err(err) = cloned.notify_event_listeners(e).await { + error!("error handling swap event: {:?}", err); + } + } + }, + rev_swap_event = rev_swap_events_stream.recv() => { + if let Ok(e) = rev_swap_event { + if let Err(err) = cloned.notify_event_listeners(e).await { + error!("error handling reverse swap event: {:?}", err); + } + } + }, + _ = shutdown_receiver.changed() => { + debug!("Swap events handling task completed"); + break; + } + } + } + }); + } + + async fn unregister_onchain_tx_notifications(&self, webhook_url: &str) -> SdkResult<()> { + get_reqwest_client()? + .post(format!( + "{}/api/v1/unregister", + self.config.chainnotifier_url + )) + .header(CONTENT_TYPE, "application/json") + .body(Body::from( + json!({ + "webhook": webhook_url + }) + .to_string(), + )) + .send() + .await + .map(|_| ()) + .map_err(|e| SdkError::ServiceConnectivity { + err: format!("Failed to unregister for tx confirmation notifications: {e}"), + }) + } + + async fn unregister_payment_notifications(&self, webhook_url: String) -> SdkResult<()> { + let message = webhook_url.clone(); + let sign_request = SignMessageRequest { message }; + let sign_response = self.sign_message(sign_request).await?; + + // Attempt register call for all relevant LSPs + let mut error_found = false; + for lsp_info in get_notification_lsps( + self.persister.clone(), + self.lsp_api.clone(), + self.node_api.clone(), + ) + .await? + { + let lsp_id = lsp_info.id; + let res = self + .lsp_api + .unregister_payment_notifications( + lsp_id.clone(), + lsp_info.lsp_pubkey, + webhook_url.clone(), + sign_response.signature.clone(), + ) + .await; + if res.is_err() { + error_found = true; + warn!("Failed to un-register notifications for LSP {lsp_id}: {res:?}"); + } + } + + match error_found { + true => Err(SdkError::generic( + "Failed to un-register notifications for at least one LSP, see logs for details", + )), + false => Ok(()), + } + } + + pub async fn unregister_webhook(&self, webhook_url: String) -> SdkResult<()> { + info!("Unregistering for webhook notifications"); + self.unregister_onchain_tx_notifications(&webhook_url) + .await?; + self.unregister_payment_notifications(webhook_url).await?; + self.persister.remove_webhook_url()?; + Ok(()) + } +} + +/// A helper struct to configure and build BreezServices +struct BreezServicesBuilder { + config: Config, + node_api: Option>, + backup_transport: Option>, + seed: Option>, + lsp_api: Option>, + fiat_api: Option>, + persister: Option>, + support_api: Option>, + swapper_api: Option>, + /// Reverse swap functionality on the Breez Server + reverse_swapper_api: Option>, + /// Reverse swap functionality on the 3rd party reverse swap service + reverse_swap_service_api: Option>, + buy_bitcoin_api: Option>, +} + +#[allow(dead_code)] +impl BreezServicesBuilder { + pub fn new(config: Config) -> BreezServicesBuilder { + BreezServicesBuilder { + config, + node_api: None, + seed: None, + lsp_api: None, + fiat_api: None, + persister: None, + support_api: None, + swapper_api: None, + reverse_swapper_api: None, + reverse_swap_service_api: None, + buy_bitcoin_api: None, + backup_transport: None, + } + } + + pub fn node_api(&mut self, node_api: Arc) -> &mut Self { + self.node_api = Some(node_api); + self + } + + pub fn lsp_api(&mut self, lsp_api: Arc) -> &mut Self { + self.lsp_api = Some(lsp_api.clone()); + self + } + + pub fn fiat_api(&mut self, fiat_api: Arc) -> &mut Self { + self.fiat_api = Some(fiat_api.clone()); + self + } + + pub fn buy_bitcoin_api(&mut self, buy_bitcoin_api: Arc) -> &mut Self { + self.buy_bitcoin_api = Some(buy_bitcoin_api.clone()); + self + } + + pub fn persister(&mut self, persister: Arc) -> &mut Self { + self.persister = Some(persister); + self + } + + pub fn support_api(&mut self, support_api: Arc) -> &mut Self { + self.support_api = Some(support_api.clone()); + self + } + + pub fn swapper_api(&mut self, swapper_api: Arc) -> &mut Self { + self.swapper_api = Some(swapper_api.clone()); + self + } + + pub fn reverse_swapper_api( + &mut self, + reverse_swapper_api: Arc, + ) -> &mut Self { + self.reverse_swapper_api = Some(reverse_swapper_api.clone()); + self + } + + pub fn reverse_swap_service_api( + &mut self, + reverse_swap_service_api: Arc, + ) -> &mut Self { + self.reverse_swap_service_api = Some(reverse_swap_service_api.clone()); + self + } + + pub fn backup_transport(&mut self, backup_transport: Arc) -> &mut Self { + self.backup_transport = Some(backup_transport.clone()); + self + } + + pub fn seed(&mut self, seed: Vec) -> &mut Self { + self.seed = Some(seed); + self + } + + pub async fn build( + &self, + restore_only: Option, + event_listener: Option>>, + ) -> BreezServicesResult> { + if self.node_api.is_none() && self.seed.is_none() { + return Err(ConnectError::Generic { + err: "Either node_api or both credentials and seed should be provided".into(), + }); + } + + // The storage is implemented via sqlite. + let persister = self + .persister + .clone() + .unwrap_or_else(|| Arc::new(SqliteStorage::new(self.config.working_dir.clone()))); + persister.init()?; + + let mut node_api = self.node_api.clone(); + let mut backup_transport = self.backup_transport.clone(); + if node_api.is_none() { + let greenlight = Greenlight::connect( + self.config.clone(), + self.seed.clone().unwrap(), + restore_only, + persister.clone(), + ) + .await?; + let gl_arc = Arc::new(greenlight); + node_api = Some(gl_arc.clone()); + if backup_transport.is_none() { + backup_transport = Some(Arc::new(GLBackupTransport { inner: gl_arc })); + } + } + + if backup_transport.is_none() { + return Err(ConnectError::Generic { + err: "State synchronizer should be provided".into(), + }); + } + + let unwrapped_node_api = node_api.unwrap(); + let unwrapped_backup_transport = backup_transport.unwrap(); + + // create the backup encryption key and then the backup watcher + let backup_encryption_key = unwrapped_node_api.derive_bip32_key(vec![ + ChildNumber::from_hardened_idx(139)?, + ChildNumber::from(0), + ])?; + + // We calculate the legacy key as a fallback for the case where the backup is still + // encrypted with the old key. + let legacy_backup_encryption_key = unwrapped_node_api.legacy_derive_bip32_key(vec![ + ChildNumber::from_hardened_idx(139)?, + ChildNumber::from(0), + ])?; + let backup_watcher = BackupWatcher::new( + self.config.clone(), + unwrapped_backup_transport.clone(), + persister.clone(), + backup_encryption_key.to_priv().to_bytes(), + legacy_backup_encryption_key.to_priv().to_bytes(), + ); + + // breez_server provides both FiatAPI & LspAPI implementations + let breez_server = Arc::new( + BreezServer::new(self.config.breezserver.clone(), self.config.api_key.clone()) + .map_err(|e| ConnectError::ServiceConnectivity { + err: format!("Failed to create BreezServer: {e}"), + })?, + ); + + // Ensure breez server connection is established in the background + let cloned_breez_server = breez_server.clone(); + tokio::spawn(async move { + if let Err(e) = cloned_breez_server.ping().await { + error!("Failed to ping breez server: {e}"); + } + }); + + let current_lsp_id = persister.get_lsp_id()?; + if current_lsp_id.is_none() && self.config.default_lsp_id.is_some() { + persister.set_lsp(self.config.default_lsp_id.clone().unwrap(), None)?; + } + + let payment_receiver = Arc::new(PaymentReceiver { + config: self.config.clone(), + node_api: unwrapped_node_api.clone(), + lsp: breez_server.clone(), + persister: persister.clone(), + }); + + // mempool space is used to monitor the chain + let mempoolspace_urls = match self.config.mempoolspace_url.clone() { + None => { + let cached = persister.get_mempoolspace_base_urls()?; + match cached.len() { + // If we have no cached values, or we cached an empty list, fetch new ones + 0 => { + let fresh_urls = breez_server + .fetch_mempoolspace_urls() + .await + .unwrap_or(vec![DEFAULT_MEMPOOL_SPACE_URL.into()]); + persister.set_mempoolspace_base_urls(fresh_urls.clone())?; + fresh_urls + } + // If we already have cached values, return those + _ => cached, + } + } + Some(mempoolspace_url_from_config) => vec![mempoolspace_url_from_config], + }; + let chain_service = Arc::new(RedundantChainService::from_base_urls(mempoolspace_urls)); + + let btc_receive_swapper = Arc::new(BTCReceiveSwap::new( + self.config.network.into(), + unwrapped_node_api.clone(), + self.swapper_api + .clone() + .unwrap_or_else(|| breez_server.clone()), + persister.clone(), + chain_service.clone(), + payment_receiver.clone(), + )); + + let btc_send_swapper = Arc::new(BTCSendSwap::new( + self.config.clone(), + self.reverse_swapper_api + .clone() + .unwrap_or_else(|| breez_server.clone()), + self.reverse_swap_service_api + .clone() + .unwrap_or_else(|| Arc::new(BoltzApi {})), + persister.clone(), + chain_service.clone(), + unwrapped_node_api.clone(), + )); + + // create a shutdown channel (sender and receiver) + let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(()); + + let buy_bitcoin_api = self + .buy_bitcoin_api + .clone() + .unwrap_or_else(|| Arc::new(BuyBitcoinService::new(breez_server.clone()))); + + // Create the node services and it them statically + let breez_services = Arc::new(InternalBreezServices { + config: self.config.clone(), + started: Mutex::new(false), + node_api: unwrapped_node_api.clone(), + lsp_api: self.lsp_api.clone().unwrap_or_else(|| breez_server.clone()), + fiat_api: self + .fiat_api + .clone() + .unwrap_or_else(|| breez_server.clone()), + support_api: self + .support_api + .clone() + .unwrap_or_else(|| breez_server.clone()), + buy_bitcoin_api, + chain_service, + persister: persister.clone(), + btc_receive_swapper, + btc_send_swapper, + payment_receiver, + event_listener, + backup_watcher: Arc::new(backup_watcher), + shutdown_sender, + shutdown_receiver, + }); + + Ok(breez_services) + } +} + +pub struct OpenChannelParams { + pub payer_amount_msat: u64, + pub opening_fee_params: models::OpeningFeeParams, +} + +#[tonic::async_trait] +pub trait Receiver: Send + Sync { + async fn receive_payment( + &self, + req: ReceivePaymentRequest, + ) -> Result; + async fn wrap_node_invoice( + &self, + invoice: &str, + params: Option, + lsp_info: Option, + ) -> Result; +} + +pub(crate) struct PaymentReceiver { + config: Config, + node_api: Arc, + lsp: Arc, + persister: Arc, +} + +#[tonic::async_trait] +impl Receiver for PaymentReceiver { + async fn receive_payment( + &self, + req: ReceivePaymentRequest, + ) -> Result { + let lsp_info = get_lsp(self.persister.clone(), self.lsp.clone()).await?; + let node_state = self + .persister + .get_node_state()? + .ok_or(ReceivePaymentError::Generic { + err: "Node info not found".into(), + })?; + let expiry = req.expiry.unwrap_or(INVOICE_PAYMENT_FEE_EXPIRY_SECONDS); + + ensure_sdk!( + req.amount_msat > 0, + ReceivePaymentError::InvalidAmount { + err: "Receive amount must be more than 0".into() + } + ); + + let mut destination_invoice_amount_msat = req.amount_msat; + let mut channel_opening_fee_params = None; + let mut channel_fees_msat = None; + + // check if we need to open channel + let open_channel_needed = + node_state.max_receivable_single_payment_amount_msat < req.amount_msat; + if open_channel_needed { + info!("We need to open a channel"); + + // we need to open channel so we are calculating the fees for the LSP (coming either from the user, or from the LSP) + let ofp = match req.opening_fee_params { + Some(fee_params) => fee_params, + None => lsp_info.cheapest_open_channel_fee(expiry)?.clone(), + }; + + channel_opening_fee_params = Some(ofp.clone()); + channel_fees_msat = Some(ofp.get_channel_fees_msat_for(req.amount_msat)); + if let Some(channel_fees_msat) = channel_fees_msat { + info!("zero-conf fee calculation option: lsp fee rate (proportional): {}: (minimum {}), total fees for channel: {}", + ofp.proportional, ofp.min_msat, channel_fees_msat); + + if req.amount_msat < channel_fees_msat + 1000 { + return Err( + ReceivePaymentError::InvalidAmount{err: format!( + "Amount should be more than the minimum fees {channel_fees_msat} msat, but is {} msat", + req.amount_msat + )} + ); + } + // remove the fees from the amount to get the small amount on the current node invoice. + destination_invoice_amount_msat = req.amount_msat - channel_fees_msat; + } + } + + info!("Creating invoice on NodeAPI"); + let invoice = self + .node_api + .create_invoice(CreateInvoiceRequest { + amount_msat: destination_invoice_amount_msat, + description: req.description, + payer_amount_msat: match open_channel_needed { + true => Some(req.amount_msat), + false => None, + }, + preimage: req.preimage, + use_description_hash: req.use_description_hash, + expiry: Some(expiry), + cltv: Some(req.cltv.unwrap_or(144)), + }) + .await?; + info!("Invoice created {}", invoice); + + let open_channel_params = match open_channel_needed { + true => Some(OpenChannelParams { + payer_amount_msat: req.amount_msat, + opening_fee_params: channel_opening_fee_params.clone().ok_or( + ReceivePaymentError::Generic { + err: "We need to open a channel, but no channel opening fee params found" + .into(), + }, + )?, + }), + false => None, + }; + + let invoice = self + .wrap_node_invoice(&invoice, open_channel_params, Some(lsp_info)) + .await?; + let parsed_invoice = parse_invoice(&invoice)?; + + // return the signed, converted invoice with hints + Ok(ReceivePaymentResponse { + ln_invoice: parsed_invoice, + opening_fee_params: channel_opening_fee_params, + opening_fee_msat: channel_fees_msat, + }) + } + + async fn wrap_node_invoice( + &self, + invoice: &str, + params: Option, + lsp_info: Option, + ) -> Result { + let lsp_info = match lsp_info { + Some(lsp_info) => lsp_info, + None => get_lsp(self.persister.clone(), self.lsp.clone()).await?, + }; + + match params { + Some(params) => { + self.wrap_open_channel_invoice(invoice, params, &lsp_info) + .await + } + None => self.ensure_hint(invoice, &lsp_info).await, + } + } +} + +impl PaymentReceiver { + async fn ensure_hint( + &self, + invoice: &str, + lsp_info: &LspInformation, + ) -> Result { + info!("Getting routing hints from node"); + let (mut hints, has_public_channel) = self.node_api.get_routing_hints(lsp_info).await?; + if !has_public_channel && hints.is_empty() { + return Err(ReceivePaymentError::InvoiceNoRoutingHints { + err: "Must have at least one active channel".into(), + }); + } + + let parsed_invoice = parse_invoice(invoice)?; + + // check if the lsp hint already exists + info!("Existing routing hints {:?}", parsed_invoice.routing_hints); + + // limit the hints to max 3 and extract the lsp one. + if let Some(lsp_hint) = Self::limit_and_extract_lsp_hint(&mut hints, lsp_info) { + if parsed_invoice.contains_hint_for_node(lsp_info.pubkey.as_str()) { + return Ok(String::from(invoice)); + } + + info!("Adding lsp hint: {:?}", lsp_hint); + let modified = + add_routing_hints(invoice, true, &vec![lsp_hint], parsed_invoice.amount_msat)?; + + let invoice = self.node_api.sign_invoice(modified)?; + info!("Signed invoice with hint = {}", invoice); + return Ok(invoice); + } + + if parsed_invoice.routing_hints.is_empty() { + info!("Adding custom hints: {:?}", hints); + let modified = add_routing_hints(invoice, false, &hints, parsed_invoice.amount_msat)?; + let invoice = self.node_api.sign_invoice(modified)?; + info!("Signed invoice with hints = {}", invoice); + return Ok(invoice); + } + + Ok(String::from(invoice)) + } + + async fn wrap_open_channel_invoice( + &self, + invoice: &str, + params: OpenChannelParams, + lsp_info: &LspInformation, + ) -> Result { + let parsed_invoice = parse_invoice(invoice)?; + let open_channel_hint = RouteHint { + hops: vec![RouteHintHop { + src_node_id: lsp_info.pubkey.clone(), + short_channel_id: "1x0x0".to_string(), + fees_base_msat: lsp_info.base_fee_msat as u32, + fees_proportional_millionths: (lsp_info.fee_rate * 1000000.0) as u32, + cltv_expiry_delta: lsp_info.time_lock_delta as u64, + htlc_minimum_msat: Some(lsp_info.min_htlc_msat as u64), + htlc_maximum_msat: None, + }], + }; + info!("Adding open channel hint: {:?}", open_channel_hint); + let invoice_with_hint = add_routing_hints( + invoice, + false, + &vec![open_channel_hint], + Some(params.payer_amount_msat), + )?; + let signed_invoice = self.node_api.sign_invoice(invoice_with_hint)?; + + info!("Registering payment with LSP"); + let api_key = self.config.api_key.clone().unwrap_or_default(); + let api_key_hash = sha256::Hash::hash(api_key.as_bytes()).to_hex(); + + self.lsp + .register_payment( + lsp_info.id.clone(), + lsp_info.lsp_pubkey.clone(), + grpc::PaymentInformation { + payment_hash: hex::decode(parsed_invoice.payment_hash.clone()) + .map_err(|e| anyhow!("Failed to decode hex payment hash: {e}"))?, + payment_secret: parsed_invoice.payment_secret.clone(), + destination: hex::decode(parsed_invoice.payee_pubkey.clone()) + .map_err(|e| anyhow!("Failed to decode hex payee pubkey: {e}"))?, + incoming_amount_msat: params.payer_amount_msat as i64, + outgoing_amount_msat: parsed_invoice + .amount_msat + .ok_or(anyhow!("Open channel invoice must have an amount"))? + as i64, + tag: json!({ "apiKeyHash": api_key_hash }).to_string(), + opening_fee_params: Some(params.opening_fee_params.into()), + }, + ) + .await?; + // Make sure we save the large amount so we can deduce the fees later. + self.persister.insert_open_channel_payment_info( + &parsed_invoice.payment_hash, + params.payer_amount_msat, + invoice, + )?; + + Ok(signed_invoice) + } + + fn limit_and_extract_lsp_hint( + routing_hints: &mut Vec, + lsp_info: &LspInformation, + ) -> Option { + let mut lsp_hint: Option = None; + if let Some(lsp_index) = routing_hints.iter().position(|r| { + r.hops + .iter() + .any(|h| h.src_node_id == lsp_info.pubkey.clone()) + }) { + lsp_hint = Some(routing_hints.remove(lsp_index)); + } + if routing_hints.len() > 3 { + routing_hints.drain(3..); + } + lsp_hint + } +} + +/// Convenience method to look up LSP info based on current LSP ID +async fn get_lsp( + persister: Arc, + lsp_api: Arc, +) -> SdkResult { + let lsp_id = persister + .get_lsp_id()? + .ok_or(SdkError::generic("No LSP ID found"))?; + + get_lsp_by_id(persister, lsp_api, lsp_id.as_str()) + .await? + .ok_or_else(|| SdkError::Generic { + err: format!("No LSP found for id {lsp_id}"), + }) +} + +async fn get_lsp_by_id( + persister: Arc, + lsp_api: Arc, + lsp_id: &str, +) -> SdkResult> { + let node_pubkey = persister + .get_node_state()? + .ok_or(SdkError::generic("Node info not found"))? + .id; + + Ok(lsp_api + .list_lsps(node_pubkey) + .await? + .iter() + .find(|&lsp| lsp.id.as_str() == lsp_id) + .cloned()) +} + +/// Convenience method to get all LSPs (active and historical) relevant for registering or +/// unregistering webhook notifications +async fn get_notification_lsps( + persister: Arc, + lsp_api: Arc, + node_api: Arc, +) -> SdkResult> { + let node_pubkey = persister + .get_node_state()? + .ok_or(SdkError::generic("Node info not found"))? + .id; + let open_peers = node_api.get_open_peers().await?; + + let mut notification_lsps = vec![]; + for lsp in lsp_api.list_used_lsps(node_pubkey).await? { + match !lsp.opening_fee_params_list.values.is_empty() { + true => { + // Non-empty fee params list = this is the active LSP + // Always consider the active LSP for notifications + notification_lsps.push(lsp); + } + false => { + // Consider only historical LSPs with whom we have an active channel + let lsp_pubkey = hex::decode(&lsp.pubkey) + .map_err(|e| anyhow!("Failed decode lsp pubkey: {e}"))?; + let has_active_channel_to_lsp = open_peers.contains(&lsp_pubkey); + if has_active_channel_to_lsp { + notification_lsps.push(lsp); + } + } + } + } + Ok(notification_lsps) +} + +#[cfg(test)] +pub(crate) mod tests { + use std::collections::HashMap; + use std::sync::Arc; + + use anyhow::{anyhow, Result}; + use regex::Regex; + use reqwest::Url; + use sdk_common::prelude::Rate; + + use crate::internal_breez_services::{BreezServicesBuilder, InternalBreezServices}; + use crate::models::{LnPaymentDetails, NodeState, Payment, PaymentDetails, PaymentTypeFilter}; + use crate::node_api::NodeAPI; + use crate::test_utils::*; + use crate::*; + + use super::{PaymentReceiver, Receiver}; + + #[tokio::test] + async fn test_node_state() -> Result<()> { + // let storage_path = format!("{}/storage.sql", get_test_working_dir()); + // std::fs::remove_file(storage_path).ok(); + + let dummy_node_state = get_dummy_node_state(); + + let lnurl_metadata = "{'key': 'sample-metadata-val'}"; + let test_ln_address = "test@ln-address.com"; + let test_lnurl_withdraw_endpoint = "https://test.endpoint.lnurl-w"; + let sa = SuccessActionProcessed::Message { + data: MessageSuccessActionData { + message: "test message".into(), + }, + }; + + let payment_hash_lnurl_withdraw = "2222"; + let payment_hash_with_lnurl_success_action = "3333"; + let payment_hash_swap: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let swap_info = SwapInfo { + bitcoin_address: "123".to_string(), + created_at: 12345678, + lock_height: 654321, + payment_hash: payment_hash_swap.clone(), + preimage: vec![], + private_key: vec![], + public_key: vec![], + swapper_public_key: vec![], + script: vec![], + bolt11: Some("312".into()), + paid_msat: 1000, + confirmed_sats: 1, + unconfirmed_sats: 0, + total_incoming_txs: 1, + status: SwapStatus::Refundable, + refund_tx_ids: vec![], + unconfirmed_tx_ids: vec![], + confirmed_tx_ids: vec![], + min_allowed_deposit: 5_000, + max_allowed_deposit: 1_000_000, + max_swapper_payable: 2_000_000, + last_redeem_error: None, + channel_opening_fees: Some(OpeningFeeParams { + min_msat: 5_000_000, + proportional: 50, + valid_until: "date".to_string(), + max_idle_time: 12345, + max_client_to_self_delay: 234, + promise: "promise".to_string(), + }), + confirmed_at: Some(555), + }; + let payment_hash_rev_swap: Vec = vec![8, 7, 6, 5, 4, 3, 2, 1]; + let preimage_rev_swap: Vec = vec![6, 6, 6, 6]; + let full_ref_swap_info = FullReverseSwapInfo { + id: "rev_swap_id".to_string(), + created_at_block_height: 0, + preimage: preimage_rev_swap.clone(), + private_key: vec![], + claim_pubkey: "claim_pubkey".to_string(), + timeout_block_height: 600_000, + invoice: "645".to_string(), + redeem_script: "redeem_script".to_string(), + onchain_amount_sat: 250, + sat_per_vbyte: Some(50), + receive_amount_sat: None, + cache: ReverseSwapInfoCached { + status: ReverseSwapStatus::CompletedConfirmed, + lockup_txid: Some("lockup_txid".to_string()), + claim_txid: Some("claim_txid".to_string()), + }, + }; + let rev_swap_info = ReverseSwapInfo { + id: "rev_swap_id".to_string(), + claim_pubkey: "claim_pubkey".to_string(), + lockup_txid: Some("lockup_txid".to_string()), + claim_txid: Some("claim_txid".to_string()), + onchain_amount_sat: 250, + status: ReverseSwapStatus::CompletedConfirmed, + }; + let dummy_transactions = vec![ + Payment { + id: "1111".to_string(), + payment_type: PaymentType::Received, + payment_time: 100000, + amount_msat: 10, + fee_msat: 0, + status: PaymentStatus::Complete, + error: None, + description: Some("test receive".to_string()), + details: PaymentDetails::Ln { + data: LnPaymentDetails { + payment_hash: "1111".to_string(), + label: "".to_string(), + destination_pubkey: "1111".to_string(), + payment_preimage: "2222".to_string(), + keysend: false, + bolt11: "1111".to_string(), + lnurl_success_action: None, + lnurl_pay_domain: None, + lnurl_pay_comment: None, + lnurl_metadata: None, + ln_address: None, + lnurl_withdraw_endpoint: None, + swap_info: None, + reverse_swap_info: None, + pending_expiration_block: None, + open_channel_bolt11: None, + }, + }, + metadata: None, + }, + Payment { + id: payment_hash_lnurl_withdraw.to_string(), + payment_type: PaymentType::Received, + payment_time: 150000, + amount_msat: 10, + fee_msat: 0, + status: PaymentStatus::Complete, + error: None, + description: Some("test lnurl-withdraw receive".to_string()), + details: PaymentDetails::Ln { + data: LnPaymentDetails { + payment_hash: payment_hash_lnurl_withdraw.to_string(), + label: "".to_string(), + destination_pubkey: "1111".to_string(), + payment_preimage: "3333".to_string(), + keysend: false, + bolt11: "1111".to_string(), + lnurl_success_action: None, + lnurl_pay_domain: None, + lnurl_pay_comment: None, + lnurl_metadata: None, + ln_address: None, + lnurl_withdraw_endpoint: Some(test_lnurl_withdraw_endpoint.to_string()), + swap_info: None, + reverse_swap_info: None, + pending_expiration_block: None, + open_channel_bolt11: None, + }, + }, + metadata: None, + }, + Payment { + id: payment_hash_with_lnurl_success_action.to_string(), + payment_type: PaymentType::Sent, + payment_time: 200000, + amount_msat: 8, + fee_msat: 2, + status: PaymentStatus::Complete, + error: None, + description: Some("test payment".to_string()), + details: PaymentDetails::Ln { + data: LnPaymentDetails { + payment_hash: payment_hash_with_lnurl_success_action.to_string(), + label: "".to_string(), + destination_pubkey: "123".to_string(), + payment_preimage: "4444".to_string(), + keysend: false, + bolt11: "123".to_string(), + lnurl_success_action: Some(sa.clone()), + lnurl_pay_domain: None, + lnurl_pay_comment: None, + lnurl_metadata: Some(lnurl_metadata.to_string()), + ln_address: Some(test_ln_address.to_string()), + lnurl_withdraw_endpoint: None, + swap_info: None, + reverse_swap_info: None, + pending_expiration_block: None, + open_channel_bolt11: None, + }, + }, + metadata: None, + }, + Payment { + id: hex::encode(payment_hash_swap.clone()), + payment_type: PaymentType::Received, + payment_time: 250000, + amount_msat: 1_000, + fee_msat: 0, + status: PaymentStatus::Complete, + error: None, + description: Some("test receive".to_string()), + details: PaymentDetails::Ln { + data: LnPaymentDetails { + payment_hash: hex::encode(payment_hash_swap), + label: "".to_string(), + destination_pubkey: "321".to_string(), + payment_preimage: "5555".to_string(), + keysend: false, + bolt11: "312".to_string(), + lnurl_success_action: None, + lnurl_pay_domain: None, + lnurl_pay_comment: None, + lnurl_metadata: None, + ln_address: None, + lnurl_withdraw_endpoint: None, + swap_info: Some(swap_info.clone()), + reverse_swap_info: None, + pending_expiration_block: None, + open_channel_bolt11: None, + }, + }, + metadata: None, + }, + Payment { + id: hex::encode(payment_hash_rev_swap.clone()), + payment_type: PaymentType::Sent, + payment_time: 300000, + amount_msat: 50_000_000, + fee_msat: 2_000, + status: PaymentStatus::Complete, + error: None, + description: Some("test send onchain".to_string()), + details: PaymentDetails::Ln { + data: LnPaymentDetails { + payment_hash: hex::encode(payment_hash_rev_swap), + label: "".to_string(), + destination_pubkey: "321".to_string(), + payment_preimage: hex::encode(preimage_rev_swap), + keysend: false, + bolt11: "312".to_string(), + lnurl_success_action: None, + lnurl_metadata: None, + lnurl_pay_domain: None, + lnurl_pay_comment: None, + ln_address: None, + lnurl_withdraw_endpoint: None, + swap_info: None, + reverse_swap_info: Some(rev_swap_info.clone()), + pending_expiration_block: None, + open_channel_bolt11: None, + }, + }, + metadata: None, + }, + ]; + let node_api = Arc::new(MockNodeAPI::new(dummy_node_state.clone())); + + let test_config = create_test_config(); + let persister = Arc::new(create_test_persister(test_config.clone())); + persister.init()?; + persister.insert_or_update_payments(&dummy_transactions)?; + persister.insert_payment_external_info( + payment_hash_with_lnurl_success_action, + PaymentExternalInfo { + lnurl_pay_success_action: Some(sa.clone()), + lnurl_pay_domain: None, + lnurl_pay_comment: None, + lnurl_metadata: Some(lnurl_metadata.to_string()), + ln_address: Some(test_ln_address.to_string()), + lnurl_withdraw_endpoint: None, + attempted_amount_msat: None, + attempted_error: None, + }, + )?; + persister.insert_payment_external_info( + payment_hash_lnurl_withdraw, + PaymentExternalInfo { + lnurl_pay_success_action: None, + lnurl_pay_domain: None, + lnurl_pay_comment: None, + lnurl_metadata: None, + ln_address: None, + lnurl_withdraw_endpoint: Some(test_lnurl_withdraw_endpoint.to_string()), + attempted_amount_msat: None, + attempted_error: None, + }, + )?; + persister.insert_swap(swap_info.clone())?; + persister.update_swap_bolt11( + swap_info.bitcoin_address.clone(), + swap_info.bolt11.clone().unwrap(), + )?; + persister.insert_reverse_swap(&full_ref_swap_info)?; + persister + .update_reverse_swap_status("rev_swap_id", &ReverseSwapStatus::CompletedConfirmed)?; + persister + .update_reverse_swap_lockup_txid("rev_swap_id", Some("lockup_txid".to_string()))?; + persister.update_reverse_swap_claim_txid("rev_swap_id", Some("claim_txid".to_string()))?; + + let mut builder = BreezServicesBuilder::new(test_config.clone()); + let breez_services = builder + .lsp_api(Arc::new(MockBreezServer {})) + .fiat_api(Arc::new(MockBreezServer {})) + .node_api(node_api) + .persister(persister) + .backup_transport(Arc::new(MockBackupTransport::new())) + .build(None, None) + .await?; + + breez_services.sync().await?; + let fetched_state = breez_services.node_info()?; + assert_eq!(fetched_state, dummy_node_state); + + let all = breez_services + .list_payments(ListPaymentsRequest::default()) + .await?; + let mut cloned = all.clone(); + + // test the right order + cloned.reverse(); + assert_eq!(dummy_transactions, cloned); + + let received = breez_services + .list_payments(ListPaymentsRequest { + filters: Some(vec![PaymentTypeFilter::Received]), + ..Default::default() + }) + .await?; + assert_eq!( + received, + vec![cloned[3].clone(), cloned[1].clone(), cloned[0].clone()] + ); + + let sent = breez_services + .list_payments(ListPaymentsRequest { + filters: Some(vec![ + PaymentTypeFilter::Sent, + PaymentTypeFilter::ClosedChannel, + ]), + ..Default::default() + }) + .await?; + assert_eq!(sent, vec![cloned[4].clone(), cloned[2].clone()]); + assert!(matches!( + &sent[1].details, + PaymentDetails::Ln {data: LnPaymentDetails {lnurl_success_action, ..}} + if lnurl_success_action == &Some(sa))); + assert!(matches!( + &sent[1].details, + PaymentDetails::Ln {data: LnPaymentDetails {lnurl_pay_domain, ln_address, ..}} + if lnurl_pay_domain.is_none() && ln_address == &Some(test_ln_address.to_string()))); + assert!(matches!( + &received[1].details, + PaymentDetails::Ln {data: LnPaymentDetails {lnurl_withdraw_endpoint, ..}} + if lnurl_withdraw_endpoint == &Some(test_lnurl_withdraw_endpoint.to_string()))); + assert!(matches!( + &received[0].details, + PaymentDetails::Ln {data: LnPaymentDetails {swap_info: swap, ..}} + if swap == &Some(swap_info))); + assert!(matches!( + &sent[0].details, + PaymentDetails::Ln {data: LnPaymentDetails {reverse_swap_info: rev_swap, ..}} + if rev_swap == &Some(rev_swap_info))); + + Ok(()) + } + + #[tokio::test] + async fn test_receive_with_open_channel() -> Result<()> { + let config = create_test_config(); + let persister = Arc::new(create_test_persister(config.clone())); + persister.init().unwrap(); + + let dummy_node_state = get_dummy_node_state(); + + let node_api = Arc::new(MockNodeAPI::new(dummy_node_state.clone())); + + let breez_server = Arc::new(MockBreezServer {}); + persister.set_lsp(breez_server.lsp_id(), None).unwrap(); + persister.set_node_state(&dummy_node_state).unwrap(); + + let receiver: Arc = Arc::new(PaymentReceiver { + config, + node_api, + persister, + lsp: breez_server.clone(), + }); + let ln_invoice = receiver + .receive_payment(ReceivePaymentRequest { + amount_msat: 3_000_000, + description: "should populate lsp hints".to_string(), + use_description_hash: Some(false), + ..Default::default() + }) + .await? + .ln_invoice; + assert_eq!(ln_invoice.routing_hints[0].hops.len(), 1); + let lsp_hop = &ln_invoice.routing_hints[0].hops[0]; + assert_eq!(lsp_hop.src_node_id, breez_server.clone().lsp_pub_key()); + assert_eq!(lsp_hop.short_channel_id, "1x0x0"); + Ok(()) + } + + #[tokio::test] + async fn test_list_lsps() -> Result<()> { + let storage_path = format!("{}/storage.sql", get_test_working_dir()); + std::fs::remove_file(storage_path).ok(); + + let breez_services = breez_services() + .await + .map_err(|e| anyhow!("Failed to get the BreezServices: {e}"))?; + breez_services.sync().await?; + + let node_pubkey = breez_services.node_info()?.id; + let lsps = breez_services.lsp_api.list_lsps(node_pubkey).await?; + assert_eq!(lsps.len(), 1); + + Ok(()) + } + + #[tokio::test] + async fn test_fetch_rates() -> Result<(), Box> { + let breez_services = breez_services().await?; + breez_services.sync().await?; + + let rates = breez_services.fiat_api.fetch_fiat_rates().await?; + assert_eq!(rates.len(), 1); + assert_eq!( + rates[0], + Rate { + coin: "USD".to_string(), + value: 20_000.00, + } + ); + + Ok(()) + } + + #[tokio::test] + async fn test_buy_bitcoin_with_moonpay() -> Result<(), Box> { + let breez_services = breez_services().await?; + breez_services.sync().await?; + let moonpay_url = breez_services + .buy_bitcoin(BuyBitcoinRequest { + provider: BuyBitcoinProvider::Moonpay, + opening_fee_params: None, + redirect_url: None, + }) + .await? + .url; + let parsed = Url::parse(&moonpay_url)?; + let query_pairs = parsed.query_pairs().into_owned().collect::>(); + + assert_eq!(parsed.host_str(), Some("mock.moonpay")); + assert_eq!(parsed.path(), "/"); + + let wallet_address = parse(query_pairs.get("wa").unwrap()).await?; + assert!(matches!(wallet_address, InputType::BitcoinAddress { .. })); + + let max_amount = query_pairs.get("ma").unwrap(); + assert!(Regex::new(r"^\d+\.\d{8}$").unwrap().is_match(max_amount)); + + Ok(()) + } + + /// Build node service for tests + pub(crate) async fn breez_services() -> Result> { + breez_services_with(None, vec![]).await + } + + /// Build node service for tests with a list of known payments + pub(crate) async fn breez_services_with( + node_api: Option>, + known_payments: Vec, + ) -> Result> { + let node_api = + node_api.unwrap_or_else(|| Arc::new(MockNodeAPI::new(get_dummy_node_state()))); + + let test_config = create_test_config(); + let persister = Arc::new(create_test_persister(test_config.clone())); + persister.init()?; + persister.insert_or_update_payments(&known_payments)?; + persister.set_lsp(MockBreezServer {}.lsp_id(), None)?; + + let mut builder = BreezServicesBuilder::new(test_config.clone()); + let breez_services = builder + .lsp_api(Arc::new(MockBreezServer {})) + .fiat_api(Arc::new(MockBreezServer {})) + .reverse_swap_service_api(Arc::new(MockReverseSwapperAPI {})) + .buy_bitcoin_api(Arc::new(MockBuyBitcoinService {})) + .persister(persister) + .node_api(node_api) + .backup_transport(Arc::new(MockBackupTransport::new())) + .build(None, None) + .await?; + + Ok(breez_services) + } + + /// Build dummy NodeState for tests + pub(crate) fn get_dummy_node_state() -> NodeState { + NodeState { + id: "tx1".to_string(), + block_height: 1, + channels_balance_msat: 100, + onchain_balance_msat: 1_000, + pending_onchain_balance_msat: 100, + utxos: vec![], + max_payable_msat: 95, + max_receivable_msat: 4_000_000_000, + max_single_payment_amount_msat: 1_000, + max_chan_reserve_msats: 0, + connected_peers: vec!["1111".to_string()], + max_receivable_single_payment_amount_msat: 2_000, + total_inbound_liquidity_msats: 10_000, + } + } +} diff --git a/libs/sdk-core/src/lib.rs b/libs/sdk-core/src/lib.rs index ef4583bb0..2e97e056d 100644 --- a/libs/sdk-core/src/lib.rs +++ b/libs/sdk-core/src/lib.rs @@ -173,6 +173,7 @@ mod greenlight; #[rustfmt::skip] pub mod lnurl; mod buy; +mod internal_breez_services; mod lsp; mod lsps0; mod lsps2; @@ -188,12 +189,12 @@ mod swap_out; mod test_utils; mod tonic_wrap; -pub use breez_services::{ - mnemonic_to_seed, BackupFailedData, BreezEvent, BreezServices, CheckMessageRequest, - CheckMessageResponse, EventListener, InvoicePaidDetails, LogStream, PaymentFailedData, - SignMessageRequest, SignMessageResponse, -}; +pub use breez_services::{mnemonic_to_seed, BreezServices}; pub use chain::RecommendedFees; +pub use internal_breez_services::{ + BackupFailedData, BreezEvent, CheckMessageRequest, CheckMessageResponse, EventListener, + InvoicePaidDetails, LogStream, PaymentFailedData, SignMessageRequest, SignMessageResponse, +}; pub use lsp::LspInformation; pub use models::*; pub use sdk_common::prelude::*; diff --git a/libs/sdk-core/src/lnurl/pay.rs b/libs/sdk-core/src/lnurl/pay.rs index 727bb3133..a39ca076a 100644 --- a/libs/sdk-core/src/lnurl/pay.rs +++ b/libs/sdk-core/src/lnurl/pay.rs @@ -39,7 +39,7 @@ pub(crate) mod tests { use rand::random; use crate::bitcoin::hashes::{sha256, Hash}; - use crate::breez_services::tests::get_dummy_node_state; + use crate::internal_breez_services::tests::get_dummy_node_state; use crate::lnurl::pay::*; use crate::lnurl::tests::MOCK_HTTP_SERVER; use crate::{test_utils::*, LnUrlPayRequest}; @@ -397,7 +397,7 @@ pub(crate) mod tests { comment: comment.clone(), })?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -443,7 +443,7 @@ pub(crate) mod tests { comment: comment.clone(), })?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; let r = mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -475,7 +475,7 @@ pub(crate) mod tests { comment: comment.clone(), })?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -510,7 +510,7 @@ pub(crate) mod tests { comment: comment.clone(), })?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -560,7 +560,7 @@ pub(crate) mod tests { comment: comment.clone(), })?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; assert!(mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -592,7 +592,7 @@ pub(crate) mod tests { comment: comment.clone(), })?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; let res = mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -634,7 +634,7 @@ pub(crate) mod tests { None, )?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -692,7 +692,7 @@ pub(crate) mod tests { Some("http://different.localhost:8080/test-url"), )?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; let r = mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -727,7 +727,7 @@ pub(crate) mod tests { Some("http://different.localhost:8080/test-url"), )?; - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let mock_breez_services = crate::internal_breez_services::tests::breez_services().await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -811,7 +811,7 @@ pub(crate) mod tests { .await?; let known_payments: Vec = vec![model_payment]; - let mock_breez_services = crate::breez_services::tests::breez_services_with( + let mock_breez_services = crate::internal_breez_services::tests::breez_services_with( Some(Arc::new(mock_node_api)), known_payments, ) @@ -897,7 +897,7 @@ pub(crate) mod tests { .await?; let known_payments: Vec = vec![model_payment]; - let mock_breez_services = crate::breez_services::tests::breez_services_with( + let mock_breez_services = crate::internal_breez_services::tests::breez_services_with( Some(Arc::new(mock_node_api)), known_payments, ) diff --git a/libs/sdk-core/src/lsps0/transport.rs b/libs/sdk-core/src/lsps0/transport.rs index cb03069c4..614401f3f 100644 --- a/libs/sdk-core/src/lsps0/transport.rs +++ b/libs/sdk-core/src/lsps0/transport.rs @@ -313,7 +313,7 @@ mod tests { use tokio::sync::{mpsc, watch}; use crate::{ - breez_services::tests::get_dummy_node_state, + internal_breez_services::tests::get_dummy_node_state, lsps0::{ error::Error, jsonrpc::{RpcError, RpcRequest, RpcServerMessage, RpcServerMessageBody}, diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 4accb9dda..5d4db4424 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -557,6 +557,7 @@ pub struct ConfigureNodeRequest { pub close_to_address: Option, } +#[derive(Clone)] /// Represents a connect request. pub struct ConnectRequest { pub config: Config, diff --git a/libs/sdk-core/src/swap_in/swap.rs b/libs/sdk-core/src/swap_in/swap.rs index f3758677e..07bbf7f35 100644 --- a/libs/sdk-core/src/swap_in/swap.rs +++ b/libs/sdk-core/src/swap_in/swap.rs @@ -20,9 +20,9 @@ use crate::bitcoin::util::sighash::SighashCache; use crate::bitcoin::{ Address, EcdsaSighashType, Script, Sequence, Transaction, TxIn, TxOut, Witness, }; -use crate::breez_services::{BreezEvent, OpenChannelParams, Receiver}; use crate::chain::{get_total_incoming_txs, get_utxos, AddressUtxos, ChainService}; use crate::error::ReceivePaymentError; +use crate::internal_breez_services::{BreezEvent, OpenChannelParams, Receiver}; use crate::models::{Swap, SwapInfo, SwapStatus, SwapperAPI}; use crate::node_api::NodeAPI; use crate::persist::error::PersistResult; @@ -803,8 +803,8 @@ mod tests { secp256k1::{Message, PublicKey, Secp256k1, SecretKey}, OutPoint, Transaction, Txid, }, - breez_services::tests::get_dummy_node_state, chain::{ChainService, OnchainTx}, + internal_breez_services::tests::get_dummy_node_state, models::*, persist::db::SqliteStorage, test_utils::{ diff --git a/libs/sdk-core/src/swap_out/reverseswap.rs b/libs/sdk-core/src/swap_out/reverseswap.rs index 4d725e346..38c2346df 100644 --- a/libs/sdk-core/src/swap_out/reverseswap.rs +++ b/libs/sdk-core/src/swap_out/reverseswap.rs @@ -839,7 +839,7 @@ mod tests { #[tokio::test] async fn test_prepare_onchain_payment_in_range() -> Result<()> { - let sdk = crate::breez_services::tests::breez_services().await?; + let sdk = crate::internal_breez_services::tests::breez_services().await?; // User-specified send amount is within range assert_in_range_prep_payment_response( @@ -866,7 +866,7 @@ mod tests { #[tokio::test] async fn test_prepare_onchain_payment_out_of_range() -> Result<()> { - let sdk = crate::breez_services::tests::breez_services().await?; + let sdk = crate::internal_breez_services::tests::breez_services().await?; // User-specified send amount is out of range (below min) assert!(sdk diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs index 6cfc19b54..4185aee1a 100644 --- a/libs/sdk-core/src/test_utils.rs +++ b/libs/sdk-core/src/test_utils.rs @@ -27,10 +27,10 @@ use crate::bitcoin::secp256k1::ecdsa::RecoverableSignature; use crate::bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey}; use crate::bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; use crate::bitcoin::Network; -use crate::breez_services::{OpenChannelParams, Receiver}; use crate::buy::BuyBitcoinApi; use crate::chain::{ChainService, OnchainTx, Outspend, RecommendedFees, TxStatus}; use crate::error::{ReceivePaymentError, SdkError, SdkResult}; +use crate::internal_breez_services::{OpenChannelParams, Receiver}; use crate::invoice::{InvoiceError, InvoiceResult}; use crate::lightning::ln::PaymentSecret; use crate::lightning_invoice::{Currency, InvoiceBuilder, RawBolt11Invoice}; diff --git a/tools/sdk-cli/src/command_handlers.rs b/tools/sdk-cli/src/command_handlers.rs index 0c00e081e..b4f53c534 100644 --- a/tools/sdk-cli/src/command_handlers.rs +++ b/tools/sdk-cli/src/command_handlers.rs @@ -372,12 +372,12 @@ pub(crate) async fn handle_command( .await?; serde_json::to_string_pretty(&res).map_err(|e| e.into()) } - Commands::NodeCredentials {} => match sdk()?.node_credentials()? { + Commands::NodeCredentials {} => match sdk()?.node_credentials().await? { Some(credentials) => serde_json::to_string_pretty(&credentials).map_err(|e| e.into()), None => Ok("No credentials".into()), }, Commands::NodeInfo {} => { - serde_json::to_string_pretty(&sdk()?.node_info()?).map_err(|e| e.into()) + serde_json::to_string_pretty(&sdk()?.node_info().await?).map_err(|e| e.into()) } Commands::ConfigureNode { close_to_address } => { sdk()?