From a7ee4842b1e618317f9047ec9676304a5ccd6f34 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Wed, 27 Mar 2024 22:34:08 +0100 Subject: [PATCH] ChainService: Fallback to next mempool.space endpoint on error (#898) * Fetch and cache mempool.space endpoints on startup * Update ChainService to accept multiple base_urls * Update ChainService to fallback to next URL on error * Add config rustdoc for mempoolspace_url * Update flutter bridge * Update RN bindings * Refactor to use a RedundantChainService model * Update flutter bridge files * Add a default mempool URL if fetching list fails * Move fetch_mempoolspace_urls() call to start() --- libs/sdk-bindings/src/breez_sdk.udl | 2 +- libs/sdk-core/src/breez_services.rs | 79 ++++++++- libs/sdk-core/src/bridge_generated.rs | 2 +- libs/sdk-core/src/chain.rs | 154 +++++++++++++++--- libs/sdk-core/src/grpc/proto/breez.proto | 10 ++ libs/sdk-core/src/input_parser.rs | 13 ++ libs/sdk-core/src/models.rs | 12 +- libs/sdk-core/src/persist/cache.rs | 18 ++ libs/sdk-core/src/swap_out/reverseswap.rs | 4 +- libs/sdk-flutter/lib/bridge_generated.dart | 15 +- .../main/java/com/breezsdk/BreezSDKMapper.kt | 3 +- .../sdk-react-native/ios/BreezSDKMapper.swift | 10 +- libs/sdk-react-native/src/index.ts | 2 +- 13 files changed, 278 insertions(+), 46 deletions(-) diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index d4b78a866..43807c951 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -133,7 +133,7 @@ interface NodeConfig { dictionary Config { string breezserver; string chainnotifier_url; - string mempoolspace_url; + string? mempoolspace_url; string working_dir; Network network; u32 payment_timeout_sec; diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 840a00d23..6ee49e293 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -25,7 +25,10 @@ use tonic::transport::{Channel, Endpoint}; use tonic::{Request, Status}; use crate::backup::{BackupRequest, BackupTransport, BackupWatcher}; -use crate::chain::{ChainService, MempoolSpace, Outspend, RecommendedFees}; +use crate::chain::{ + ChainService, Outspend, RecommendedFees, RedundantChainService, RedundantChainServiceTrait, + DEFAULT_MEMPOOL_SPACE_URL, +}; use crate::error::{ LnUrlAuthError, LnUrlPayError, LnUrlWithdrawError, ReceiveOnchainError, ReceiveOnchainResult, ReceivePaymentError, SdkError, SdkResult, SendOnchainError, SendPaymentError, @@ -38,7 +41,7 @@ use crate::grpc::payment_notifier_client::PaymentNotifierClient; use crate::grpc::signer_client::SignerClient; use crate::grpc::support_client::SupportClient; use crate::grpc::swapper_client::SwapperClient; -use crate::grpc::PaymentInformation; +use crate::grpc::{ChainApiServersRequest, PaymentInformation}; use crate::input_parser::get_reqwest_client; use crate::invoice::{ add_routing_hints, parse_invoice, validate_network, LNInvoice, RouteHint, RouteHintHop, @@ -1376,6 +1379,8 @@ impl BreezServices { debug!("Received the signal to exit event polling loop"); }); + self.init_chainservice_urls().await?; + Ok(()) } @@ -1592,6 +1597,29 @@ impl BreezServices { }); } + 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. /// @@ -1994,11 +2022,6 @@ impl BreezServicesBuilder { .unwrap_or_else(|| Arc::new(SqliteStorage::new(self.config.working_dir.clone()))); persister.init()?; - // mempool space is used to monitor the chain - let chain_service = Arc::new(MempoolSpace::from_base_url( - self.config.mempoolspace_url.clone(), - )); - let mut node_api = self.node_api.clone(); let mut backup_transport = self.backup_transport.clone(); if node_api.is_none() { @@ -2073,6 +2096,28 @@ impl BreezServicesBuilder { 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(), @@ -2207,6 +2252,26 @@ impl BreezServer { .version; Ok(response) } + + pub(crate) async fn fetch_mempoolspace_urls(&self) -> SdkResult> { + let mut client = self.get_information_client().await?; + + let chain_api_servers = client + .chain_api_servers(ChainApiServersRequest {}) + .await? + .into_inner() + .servers; + trace!("Received chain_api_servers: {chain_api_servers:?}"); + + let mempoolspace_urls = chain_api_servers + .iter() + .filter(|s| s.server_type == "MEMPOOL_SPACE") + .map(|s| s.server_base_url.clone()) + .collect(); + trace!("Received mempoolspace_urls: {mempoolspace_urls:?}"); + + Ok(mempoolspace_urls) + } } pub(crate) struct ApiKeyInterceptor { diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index cde5963ea..d2392535f 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -1189,7 +1189,7 @@ impl support::IntoDart for Config { vec![ self.breezserver.into_into_dart().into_dart(), self.chainnotifier_url.into_into_dart().into_dart(), - self.mempoolspace_url.into_into_dart().into_dart(), + self.mempoolspace_url.into_dart(), self.working_dir.into_into_dart().into_dart(), self.network.into_into_dart().into_dart(), self.payment_timeout_sec.into_into_dart().into_dart(), diff --git a/libs/sdk-core/src/chain.rs b/libs/sdk-core/src/chain.rs index c91fe006e..336e81d57 100644 --- a/libs/sdk-core/src/chain.rs +++ b/libs/sdk-core/src/chain.rs @@ -3,7 +3,9 @@ use serde::{Deserialize, Serialize}; use crate::bitcoin::hashes::hex::FromHex; use crate::bitcoin::{OutPoint, Txid}; -use crate::input_parser::{get_parse_and_log_response, get_reqwest_client}; +use crate::input_parser::{get_parse_and_log_response, get_reqwest_client, post_and_log_response}; + +pub const DEFAULT_MEMPOOL_SPACE_URL: &str = "https://mempool.space/api"; #[tonic::async_trait] pub trait ChainService: Send + Sync { @@ -21,6 +23,89 @@ pub trait ChainService: Send + Sync { async fn broadcast_transaction(&self, tx: Vec) -> Result; } +pub trait RedundantChainServiceTrait: ChainService { + fn from_base_urls(base_urls: Vec) -> Self; +} + +#[derive(Clone)] +pub struct RedundantChainService { + instances: Vec, +} +impl RedundantChainServiceTrait for RedundantChainService { + fn from_base_urls(base_urls: Vec) -> Self { + Self { + instances: base_urls + .iter() + .map(|url: &String| url.trim_end_matches('/')) + .map(MempoolSpace::from_base_url) + .collect(), + } + } +} + +#[tonic::async_trait] +impl ChainService for RedundantChainService { + async fn recommended_fees(&self) -> Result { + for inst in &self.instances { + match inst.recommended_fees().await { + Ok(res) => { + return Ok(res); + } + Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url), + } + } + Err(anyhow!("All chain service instances failed")) + } + + async fn address_transactions(&self, address: String) -> Result> { + for inst in &self.instances { + match inst.address_transactions(address.clone()).await { + Ok(res) => { + return Ok(res); + } + Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url), + } + } + Err(anyhow!("All chain service instances failed")) + } + + async fn current_tip(&self) -> Result { + for inst in &self.instances { + match inst.current_tip().await { + Ok(res) => { + return Ok(res); + } + Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url), + } + } + Err(anyhow!("All chain service instances failed")) + } + + async fn transaction_outspends(&self, txid: String) -> Result> { + for inst in &self.instances { + match inst.transaction_outspends(txid.clone()).await { + Ok(res) => { + return Ok(res); + } + Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url), + } + } + Err(anyhow!("All chain service instances failed")) + } + + async fn broadcast_transaction(&self, tx: Vec) -> Result { + for inst in &self.instances { + match inst.broadcast_transaction(tx.clone()).await { + Ok(res) => { + return Ok(res); + } + Err(e) => error!("Call to chain service {} failed: {e}", inst.base_url), + } + } + Err(anyhow!("All chain service instances failed")) + } +} + #[derive(Clone)] pub struct Utxo { pub out: OutPoint, @@ -225,45 +310,41 @@ pub struct Outspend { impl Default for MempoolSpace { fn default() -> Self { MempoolSpace { - base_url: "https://mempool.space".to_string(), + base_url: DEFAULT_MEMPOOL_SPACE_URL.into(), } } } impl MempoolSpace { - pub fn from_base_url(base_url: String) -> MempoolSpace { - MempoolSpace { base_url } + pub fn from_base_url(base_url: &str) -> MempoolSpace { + MempoolSpace { + base_url: base_url.into(), + } } } #[tonic::async_trait] impl ChainService for MempoolSpace { async fn recommended_fees(&self) -> Result { - get_parse_and_log_response(&format!("{}/api/v1/fees/recommended", self.base_url)).await + get_parse_and_log_response(&format!("{}/v1/fees/recommended", self.base_url)).await } async fn address_transactions(&self, address: String) -> Result> { - get_parse_and_log_response(&format!("{}/api/address/{address}/txs", self.base_url)).await + get_parse_and_log_response(&format!("{}/address/{address}/txs", self.base_url)).await } async fn current_tip(&self) -> Result { - get_parse_and_log_response(&format!("{}/api/blocks/tip/height", self.base_url)).await + get_parse_and_log_response(&format!("{}/blocks/tip/height", self.base_url)).await } async fn transaction_outspends(&self, txid: String) -> Result> { - let url = format!("{}/api/tx/{txid}/outspends", self.base_url); + let url = format!("{}/tx/{txid}/outspends", self.base_url); Ok(get_reqwest_client()?.get(url).send().await?.json().await?) } async fn broadcast_transaction(&self, tx: Vec) -> Result { - let client = get_reqwest_client()?; - let txid_or_error = client - .post(format!("{}/api/tx", self.base_url)) - .body(hex::encode(tx)) - .send() - .await? - .text() - .await?; + let txid_or_error = + post_and_log_response(&format!("{}/tx", self.base_url), Some(hex::encode(tx))).await?; match txid_or_error.contains("error") { true => Err(anyhow!("Error fetching tx: {txid_or_error}")), false => Ok(txid_or_error), @@ -272,7 +353,9 @@ impl ChainService for MempoolSpace { } #[cfg(test)] mod tests { - use crate::chain::{MempoolSpace, OnchainTx}; + use crate::chain::{ + MempoolSpace, OnchainTx, RedundantChainService, RedundantChainServiceTrait, + }; use anyhow::Result; use tokio::test; @@ -280,9 +363,7 @@ mod tests { #[test] async fn test_recommended_fees() -> Result<()> { - let ms = Box::new(MempoolSpace::from_base_url( - "https://mempool.space".to_string(), - )); + let ms = MempoolSpace::default(); let fees = ms.recommended_fees().await?; assert!(fees.economy_fee > 0); assert!(fees.fastest_fee > 0); @@ -293,9 +374,38 @@ mod tests { Ok(()) } + #[test] + async fn test_recommended_fees_with_fallback() -> Result<()> { + let ms = RedundantChainService::from_base_urls(vec![ + "https://mempool-url-unreachable.space/api/".into(), + ]); + assert!(ms.recommended_fees().await.is_err()); + + let ms = RedundantChainService::from_base_urls(vec![ + "https://mempool-url-unreachable.space/api/".into(), + "https://mempool.emzy.de/api/".into(), + ]); + assert!(ms.recommended_fees().await.is_ok()); + + let ms = RedundantChainService::from_base_urls(vec![ + "https://mempool-url-unreachable.space/api/".into(), + "https://another-mempool-url-unreachable.space/api/".into(), + ]); + assert!(ms.recommended_fees().await.is_err()); + + let ms = RedundantChainService::from_base_urls(vec![ + "https://mempool-url-unreachable.space/api/".into(), + "https://another-mempool-url-unreachable.space/api/".into(), + "https://mempool.emzy.de/api/".into(), + ]); + assert!(ms.recommended_fees().await.is_ok()); + + Ok(()) + } + #[test] async fn test_address_transactions() -> Result<()> { - let ms = MempoolSpace::from_base_url("https://mempool.space".to_string()); + let ms = MempoolSpace::default(); let txs = ms .address_transactions("bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2".to_string()) .await?; @@ -312,7 +422,7 @@ mod tests { // #[test] // async fn test_address_transactions_mempool() { - // let ms = MempoolSpace::from_base_url("https://mempool.space".to_string()); + // let ms = MempoolSpace::default(); // let txs = ms // .address_transactions("1N4f3y3LYJZ2Qd9FyPt3AcHp451qt12paR".to_string()) // .await diff --git a/libs/sdk-core/src/grpc/proto/breez.proto b/libs/sdk-core/src/grpc/proto/breez.proto index 5093d8f59..449d8a332 100644 --- a/libs/sdk-core/src/grpc/proto/breez.proto +++ b/libs/sdk-core/src/grpc/proto/breez.proto @@ -26,6 +26,7 @@ service Information { rpc BreezAppVersions(BreezAppVersionsRequest) returns (BreezAppVersionsReply) {} rpc ReceiverInfo(ReceiverInfoRequest) returns (ReceiverInfoReply) {} + rpc ChainApiServers(ChainApiServersRequest) returns (ChainApiServersReply) {} } service ChannelOpener { @@ -429,6 +430,15 @@ message BreezStatusReply { BreezStatus status = 1; } +message ChainApiServersRequest {} +message ChainApiServersReply { + message ChainAPIServer { + string server_type = 1; + string server_base_url = 2; + } + repeated ChainAPIServer servers = 1; +} + ///////////////////////////////////////////// // From lspd.proto ///////////////////////////////////////////// diff --git a/libs/sdk-core/src/input_parser.rs b/libs/sdk-core/src/input_parser.rs index d51432daf..e47a1379f 100644 --- a/libs/sdk-core/src/input_parser.rs +++ b/libs/sdk-core/src/input_parser.rs @@ -230,6 +230,19 @@ pub async fn parse(input: &str) -> Result { Err(anyhow!("Unrecognized input type")) } +pub(crate) async fn post_and_log_response(url: &str, body: Option) -> Result { + debug!("Making POST request to: {url}"); + + let mut req = get_reqwest_client()?.post(url); + if let Some(body) = body { + req = req.body(body); + } + let raw_body = req.send().await?.text().await?; + debug!("Received raw response body: {raw_body}"); + + Ok(raw_body) +} + /// Makes a GET request to the specified `url` and logs on DEBUG: /// - the URL /// - the raw response body diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 7b7adc817..608885695 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -463,7 +463,13 @@ pub struct LogEntry { pub struct Config { pub breezserver: String, pub chainnotifier_url: String, - pub mempoolspace_url: String, + /// If set, this is the mempool.space URL that will be used. + /// + /// If not set, a list of mempool.space URLs will be used to provide fault-tolerance. If calls + /// to the first URL fail, then the call will be repeated to the next URL, and so on. + /// + /// Note that, if specified, the URL has to be in the format: `https://mempool.space/api` + pub mempoolspace_url: Option, /// Directory in which all SDK files (DB, log) are stored. Defaults to ".", otherwise if it's customized, /// the folder should exist before starting the SDK. pub working_dir: String, @@ -486,7 +492,7 @@ impl Config { Config { breezserver: PRODUCTION_BREEZSERVER_URL.to_string(), chainnotifier_url: "https://chainnotifier.breez.technology".to_string(), - mempoolspace_url: "https://mempool.space".to_string(), + mempoolspace_url: None, working_dir: ".".to_string(), network: Bitcoin, payment_timeout_sec: 60, @@ -502,7 +508,7 @@ impl Config { Config { breezserver: STAGING_BREEZSERVER_URL.to_string(), chainnotifier_url: "https://chainnotifier.breez.technology".to_string(), - mempoolspace_url: "https://mempool.space".to_string(), + mempoolspace_url: None, working_dir: ".".to_string(), network: Bitcoin, payment_timeout_sec: 60, diff --git a/libs/sdk-core/src/persist/cache.rs b/libs/sdk-core/src/persist/cache.rs index 1ba2a2677..61238ca5a 100644 --- a/libs/sdk-core/src/persist/cache.rs +++ b/libs/sdk-core/src/persist/cache.rs @@ -8,6 +8,7 @@ const KEY_LAST_SYNC_TIME: &str = "last_sync_time"; const KEY_NODE_STATE: &str = "node_state"; const KEY_STATIC_BACKUP: &str = "static_backup"; const KEY_WEBHOOK_URL: &str = "webhook_url"; +const KEY_MEMPOOLSPACE_BASE_URLS: &str = "mempoolspace_base_urls"; impl SqliteStorage { pub fn get_cached_item(&self, key: &str) -> PersistResult> { @@ -108,6 +109,23 @@ impl SqliteStorage { pub fn get_webhook_url(&self) -> PersistResult> { self.get_cached_item(KEY_WEBHOOK_URL) } + + pub fn set_mempoolspace_base_urls( + &self, + mempool_space_endpoints: Vec, + ) -> PersistResult<()> { + let serialized = serde_json::to_string(&mempool_space_endpoints)?; + self.update_cached_item(KEY_MEMPOOLSPACE_BASE_URLS, serialized) + } + + pub fn get_mempoolspace_base_urls(&self) -> PersistResult> { + let res = match self.get_cached_item(KEY_MEMPOOLSPACE_BASE_URLS)? { + Some(str) => serde_json::from_str(str.as_str())?, + None => vec![], + }; + + Ok(res) + } } #[test] diff --git a/libs/sdk-core/src/swap_out/reverseswap.rs b/libs/sdk-core/src/swap_out/reverseswap.rs index 30a5a70a9..0db9efb52 100644 --- a/libs/sdk-core/src/swap_out/reverseswap.rs +++ b/libs/sdk-core/src/swap_out/reverseswap.rs @@ -16,7 +16,7 @@ use crate::bitcoin::util::sighash::SighashCache; use crate::bitcoin::{ Address, AddressType, EcdsaSighashType, Script, Sequence, Transaction, TxIn, TxOut, Witness, }; -use crate::chain::{get_utxos, ChainService, MempoolSpace, OnchainTx}; +use crate::chain::{get_utxos, ChainService, OnchainTx}; use crate::error::SdkResult; use crate::models::{ReverseSwapServiceAPI, ReverseSwapperRoutingAPI}; use crate::node_api::{NodeAPI, NodeError}; @@ -118,7 +118,7 @@ impl BTCSendSwap { reverse_swapper_api: Arc, reverse_swap_service_api: Arc, persister: Arc, - chain_service: Arc, + chain_service: Arc, node_api: Arc, ) -> Self { Self { diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index 0f23d5340..7f858c664 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -502,7 +502,14 @@ class ClosedChannelPaymentDetails { class Config { final String breezserver; final String chainnotifierUrl; - final String mempoolspaceUrl; + + /// If set, this is the mempool.space URL that will be used. + /// + /// If not set, a list of mempool.space URLs will be used to provide fault-tolerance. If calls + /// to the first URL fail, then the call will be repeated to the next URL, and so on. + /// + /// Note that, if specified, the URL has to be in the format: `https://mempool.space/api` + final String? mempoolspaceUrl; /// Directory in which all SDK files (DB, log) are stored. Defaults to ".", otherwise if it's customized, /// the folder should exist before starting the SDK. @@ -522,7 +529,7 @@ class Config { const Config({ required this.breezserver, required this.chainnotifierUrl, - required this.mempoolspaceUrl, + this.mempoolspaceUrl, required this.workingDir, required this.network, required this.paymentTimeoutSec, @@ -3317,7 +3324,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { return Config( breezserver: _wire2api_String(arr[0]), chainnotifierUrl: _wire2api_String(arr[1]), - mempoolspaceUrl: _wire2api_String(arr[2]), + mempoolspaceUrl: _wire2api_opt_String(arr[2]), workingDir: _wire2api_String(arr[3]), network: _wire2api_network(arr[4]), paymentTimeoutSec: _wire2api_u32(arr[5]), @@ -4765,7 +4772,7 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { void _api_fill_to_wire_config(Config apiObj, wire_Config wireObj) { wireObj.breezserver = api2wire_String(apiObj.breezserver); wireObj.chainnotifier_url = api2wire_String(apiObj.chainnotifierUrl); - wireObj.mempoolspace_url = api2wire_String(apiObj.mempoolspaceUrl); + wireObj.mempoolspace_url = api2wire_opt_String(apiObj.mempoolspaceUrl); wireObj.working_dir = api2wire_String(apiObj.workingDir); wireObj.network = api2wire_network(apiObj.network); wireObj.payment_timeout_sec = api2wire_u32(apiObj.paymentTimeoutSec); diff --git a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt index 230b82eaf..042e6c44e 100644 --- a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt +++ b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt @@ -382,7 +382,6 @@ fun asConfig(config: ReadableMap): Config? { arrayOf( "breezserver", "chainnotifierUrl", - "mempoolspaceUrl", "workingDir", "network", "paymentTimeoutSec", @@ -396,7 +395,7 @@ fun asConfig(config: ReadableMap): Config? { } val breezserver = config.getString("breezserver")!! val chainnotifierUrl = config.getString("chainnotifierUrl")!! - val mempoolspaceUrl = config.getString("mempoolspaceUrl")!! + val mempoolspaceUrl = if (hasNonNullKey(config, "mempoolspaceUrl")) config.getString("mempoolspaceUrl") else null val workingDir = config.getString("workingDir")!! val network = config.getString("network")?.let { asNetwork(it) }!! val paymentTimeoutSec = config.getInt("paymentTimeoutSec").toUInt() diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift index 067516d84..34e0c0a2b 100644 --- a/libs/sdk-react-native/ios/BreezSDKMapper.swift +++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift @@ -401,8 +401,12 @@ enum BreezSDKMapper { guard let chainnotifierUrl = config["chainnotifierUrl"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "chainnotifierUrl", typeName: "Config")) } - guard let mempoolspaceUrl = config["mempoolspaceUrl"] as? String else { - throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "mempoolspaceUrl", typeName: "Config")) + var mempoolspaceUrl: String? + if hasNonNilKey(data: config, key: "mempoolspaceUrl") { + guard let mempoolspaceUrlTmp = config["mempoolspaceUrl"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "mempoolspaceUrl")) + } + mempoolspaceUrl = mempoolspaceUrlTmp } guard let workingDir = config["workingDir"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "workingDir", typeName: "Config")) @@ -459,7 +463,7 @@ enum BreezSDKMapper { return [ "breezserver": config.breezserver, "chainnotifierUrl": config.chainnotifierUrl, - "mempoolspaceUrl": config.mempoolspaceUrl, + "mempoolspaceUrl": config.mempoolspaceUrl == nil ? nil : config.mempoolspaceUrl, "workingDir": config.workingDir, "network": valueOf(network: config.network), "paymentTimeoutSec": config.paymentTimeoutSec, diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts index 13c3a52d6..50652ca3e 100644 --- a/libs/sdk-react-native/src/index.ts +++ b/libs/sdk-react-native/src/index.ts @@ -71,7 +71,7 @@ export type ClosedChannelPaymentDetails = { export type Config = { breezserver: string chainnotifierUrl: string - mempoolspaceUrl: string + mempoolspaceUrl?: string workingDir: string network: Network paymentTimeoutSec: number