From 5d9a8f18a3cbb59618b6b50f918f5745b47852b4 Mon Sep 17 00:00:00 2001 From: Ross Savage <551697+dangeross@users.noreply.github.com> Date: Sat, 1 Jun 2024 06:32:45 +0200 Subject: [PATCH] Add Config (#267) * Add config * Add rustdocs to Config, send_payment (#271) --------- Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com> --- cli/src/main.rs | 5 +- .../include/breez_liquid_sdk.h | 14 +- .../templates/TopLevelFunctionTemplate.kt | 5 + .../src/gen_kotlin/templates/module.kt | 17 ++- .../templates/TopLevelFunctionTemplate.swift | 3 + .../src/gen_swift/templates/module.swift | 25 +++- .../src/gen_typescript/templates/module.ts | 4 +- lib/bindings/src/breez_liquid_sdk.udl | 13 +- lib/bindings/src/lib.rs | 4 + .../tests/bindings/test_breez_liquid_sdk.kts | 6 +- .../tests/bindings/test_breez_liquid_sdk.py | 6 +- .../bindings/test_breez_liquid_sdk.swift | 6 +- lib/core/src/bindings.rs | 4 + lib/core/src/frb/bridge.io.rs | 54 ++++++- lib/core/src/frb/bridge.rs | 82 +++++++++-- lib/core/src/model.rs | 74 ++++++---- lib/core/src/sdk.rs | 134 +++++++++--------- packages/dart/lib/src/bindings.dart | 3 + packages/dart/lib/src/frb_generated.dart | 80 +++++++++-- packages/dart/lib/src/frb_generated.io.dart | 54 ++++++- packages/dart/lib/src/model.dart | 55 +++++-- ...utter_breez_liquid_bindings_generated.dart | 33 ++++- .../breezliquidsdk/BreezLiquidSDKMapper.kt | 68 +++++++-- .../breezliquidsdk/BreezLiquidSDKModule.kt | 39 ++++- packages/react-native/example/App.js | 7 +- .../ios/BreezLiquidSDKMapper.swift | 76 ++++++++-- packages/react-native/ios/RNBreezLiquidSDK.m | 6 + .../react-native/ios/RNBreezLiquidSDK.swift | 34 ++++- packages/react-native/src/index.ts | 21 ++- 29 files changed, 735 insertions(+), 197 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 1eac507..4c855ea 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -74,10 +74,11 @@ async fn main() -> Result<()> { let mnemonic = persistence.get_or_create_mnemonic()?; let network = args.network.unwrap_or(Network::Testnet); + let mut config = LiquidSdk::default_config(network); + config.working_dir = data_dir_str; let sdk = LiquidSdk::connect(ConnectRequest { mnemonic: mnemonic.to_string(), - data_dir: Some(data_dir_str), - network, + config, }) .await?; let listener_id = sdk diff --git a/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h b/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h index cd2ef3c..b459e77 100644 --- a/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h +++ b/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h @@ -55,10 +55,17 @@ typedef struct wire_cst_prepare_send_response { uint64_t fees_sat; } wire_cst_prepare_send_response; +typedef struct wire_cst_config { + struct wire_cst_list_prim_u_8_strict *boltz_url; + struct wire_cst_list_prim_u_8_strict *electrum_url; + struct wire_cst_list_prim_u_8_strict *working_dir; + int32_t network; + uint64_t payment_timeout_sec; +} wire_cst_config; + typedef struct wire_cst_connect_request { struct wire_cst_list_prim_u_8_strict *mnemonic; - struct wire_cst_list_prim_u_8_strict *data_dir; - int32_t network; + struct wire_cst_config config; } wire_cst_connect_request; typedef struct wire_cst_payment { @@ -271,6 +278,8 @@ void frbgen_breez_liquid_wire__crate__bindings__breez_log_stream(int64_t port_, void frbgen_breez_liquid_wire__crate__bindings__connect(int64_t port_, struct wire_cst_connect_request *req); +void frbgen_breez_liquid_wire__crate__bindings__default_config(int64_t port_, int32_t network); + void frbgen_breez_liquid_wire__crate__bindings__parse_invoice(int64_t port_, struct wire_cst_list_prim_u_8_strict *input); @@ -337,6 +346,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sync); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__breez_log_stream); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__connect); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__default_config); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__parse_invoice); dummy_var ^= ((int64_t) (void*) store_dart_post_cobject); return dummy_var; diff --git a/lib/bindings/langs/react-native/src/gen_kotlin/templates/TopLevelFunctionTemplate.kt b/lib/bindings/langs/react-native/src/gen_kotlin/templates/TopLevelFunctionTemplate.kt index f0d743a..895876f 100644 --- a/lib/bindings/langs/react-native/src/gen_kotlin/templates/TopLevelFunctionTemplate.kt +++ b/lib/bindings/langs/react-native/src/gen_kotlin/templates/TopLevelFunctionTemplate.kt @@ -22,6 +22,11 @@ {%- match func.return_type() -%} {%- when Some with (return_type) %} val res = {{ obj_interface }}{{ func.name()|fn_name|unquote }}({%- call kt::arg_list(func) -%}) +{%- if func.name() == "default_config" %} + val workingDir = File(reactApplicationContext.filesDir.toString() + "/breezLiquidSdk") + + res.workingDir = workingDir.absolutePath +{%- endif -%} {%- match return_type %} {%- when Type::Optional(inner) %} {%- let unboxed = inner.as_ref() %} diff --git a/lib/bindings/langs/react-native/src/gen_kotlin/templates/module.kt b/lib/bindings/langs/react-native/src/gen_kotlin/templates/module.kt index 6bfb42c..debcb07 100644 --- a/lib/bindings/langs/react-native/src/gen_kotlin/templates/module.kt +++ b/lib/bindings/langs/react-native/src/gen_kotlin/templates/module.kt @@ -36,6 +36,19 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext throw LiquidSdkException.Generic("Not initialized") } + @Throws(LiquidSdkException::class) + private fun ensureWorkingDir(workingDir: String) { + try { + val workingDirFile = File(workingDir) + + if (!workingDirFile.exists() && !workingDirFile.mkdirs()) { + throw LiquidSdkException.Generic("Mandatory field workingDir must contain a writable directory") + } + } catch (e: SecurityException) { + throw LiquidSdkException.Generic("Mandatory field workingDir must contain a writable directory") + } + } + @ReactMethod fun addListener(eventName: String) {} @@ -73,7 +86,9 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext executor.execute { try { var connectRequest = asConnectRequest(req) ?: run { throw LiquidSdkException.Generic(errMissingMandatoryField("req", "ConnectRequest")) } - connectRequest.dataDir = connectRequest.dataDir?.takeUnless { it.isEmpty() } ?: run { reactApplicationContext.filesDir.toString() + "/breezLiquidSdk" } + + ensureWorkingDir(connectRequest.config.workingDir) + bindingLiquidSdk = connect(connectRequest) promise.resolve(readableMapOf("status" to "ok")) } catch (e: Exception) { diff --git a/lib/bindings/langs/react-native/src/gen_swift/templates/TopLevelFunctionTemplate.swift b/lib/bindings/langs/react-native/src/gen_swift/templates/TopLevelFunctionTemplate.swift index c65200a..dc8c77c 100644 --- a/lib/bindings/langs/react-native/src/gen_swift/templates/TopLevelFunctionTemplate.swift +++ b/lib/bindings/langs/react-native/src/gen_swift/templates/TopLevelFunctionTemplate.swift @@ -21,6 +21,9 @@ {%- match func.return_type() -%} {%- when Some with (return_type) %} var res = {%- call swift::throws_decl(func) -%}{{ obj_interface }}{{ func.name()|fn_name|unquote }}({%- call swift::arg_list(func) -%}) +{%- if func.name() == "default_config" %} + res.workingDir = RNBreezLiquidSDK.breezLiquidSdkDirectory.path +{%- endif -%} {%- match return_type %} {%- when Type::Optional(inner) %} {%- let unboxed = inner.as_ref() %} diff --git a/lib/bindings/langs/react-native/src/gen_swift/templates/module.swift b/lib/bindings/langs/react-native/src/gen_swift/templates/module.swift index 8776827..c5532ec 100644 --- a/lib/bindings/langs/react-native/src/gen_swift/templates/module.swift +++ b/lib/bindings/langs/react-native/src/gen_swift/templates/module.swift @@ -11,10 +11,16 @@ class RNBreezLiquidSDK: RCTEventEmitter { private var bindingLiquidSdk: BindingLiquidSdk! - static var defaultDataDir: URL { + + static var breezLiquidSdkDirectory: URL { let applicationDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + let breezLiquidSdkDirectory = applicationDirectory.appendingPathComponent("breezLiquidSdk", isDirectory: true) - return applicationDirectory.appendingPathComponent("breezLiquidSdk", isDirectory: true) + if !FileManager.default.fileExists(atPath: breezLiquidSdkDirectory.path) { + try! FileManager.default.createDirectory(atPath: breezLiquidSdkDirectory.path, withIntermediateDirectories: true) + } + + return breezLiquidSdkDirectory } override init() { @@ -55,7 +61,17 @@ class RNBreezLiquidSDK: RCTEventEmitter { throw LiquidSdkError.Generic(message: "Not initialized") } - + + private func ensureWorkingDir(workingDir: String) throws { + do { + if !FileManager.default.fileExists(atPath: workingDir) { + try FileManager.default.createDirectory(atPath: workingDir, withIntermediateDirectories: true) + } + } catch { + throw LiquidSdkError.Generic(message: "Mandatory field workingDir must contain a writable directory") + } + } + {% let obj_interface = "BreezLiquidSDK." -%} {% for func in ci.function_definitions() %} {%- if func.name()|ignored_function == false -%} @@ -81,7 +97,8 @@ class RNBreezLiquidSDK: RCTEventEmitter { do { var connectRequest = try BreezLiquidSDKMapper.asConnectRequest(connectRequest: req) - connectRequest.dataDir = connectRequest.dataDir == nil || connectRequest.dataDir!.isEmpty ? RNBreezLiquidSDK.defaultDataDir.path : connectRequest.dataDir + try ensureWorkingDir(workingDir: connectRequest.config.workingDir) + bindingLiquidSdk = try BreezLiquidSDK.connect(req: connectRequest) resolve(["status": "ok"]) } catch let err { diff --git a/lib/bindings/langs/react-native/src/gen_typescript/templates/module.ts b/lib/bindings/langs/react-native/src/gen_typescript/templates/module.ts index d25ae76..e55430f 100644 --- a/lib/bindings/langs/react-native/src/gen_typescript/templates/module.ts +++ b/lib/bindings/langs/react-native/src/gen_typescript/templates/module.ts @@ -1,4 +1,4 @@ -import { NativeModules, Platform, NativeEventEmitter } from "react-native" +import { NativeModules, Platform, EmitterSubscription, NativeEventEmitter } from "react-native" const LINKING_ERROR = `The package 'react-native-breez-liquid-sdk' doesn't seem to be linked. Make sure: \n\n` + @@ -20,7 +20,7 @@ const BreezLiquidSDK = NativeModules.RNBreezLiquidSDK const BreezLiquidSDKEmitter = new NativeEventEmitter(BreezLiquidSDK) {%- import "macros.ts" as ts %} {%- include "Types.ts" %} -{% include "Helpers.ts" -%} +{% include "Helpers.ts" %} {% for func in ci.function_definitions() %} {%- if func.name()|ignored_function == false -%} {%- include "TopLevelFunctionTemplate.ts" %} diff --git a/lib/bindings/src/breez_liquid_sdk.udl b/lib/bindings/src/breez_liquid_sdk.udl index e8d78ed..f99d339 100644 --- a/lib/bindings/src/breez_liquid_sdk.udl +++ b/lib/bindings/src/breez_liquid_sdk.udl @@ -23,15 +23,22 @@ enum PaymentError { "SignerError", }; +dictionary Config { + string boltz_url; + string electrum_url; + string working_dir; + Network network; + u64 payment_timeout_sec; +}; + enum Network { "Mainnet", "Testnet", }; dictionary ConnectRequest { + Config config; string mnemonic; - Network network; - string? data_dir = null; }; dictionary GetInfoRequest { @@ -165,6 +172,8 @@ namespace breez_liquid_sdk { [Throws=LiquidSdkError] void set_logger(Logger logger); + + Config default_config(Network network); [Throws=PaymentError] LNInvoice parse_invoice(string invoice); diff --git a/lib/bindings/src/lib.rs b/lib/bindings/src/lib.rs index 5933835..3e8bfcd 100644 --- a/lib/bindings/src/lib.rs +++ b/lib/bindings/src/lib.rs @@ -58,6 +58,10 @@ pub fn connect(req: ConnectRequest) -> Result, LiquidSdkEr }) } +pub fn default_config(network: Network) -> Config { + LiquidSdk::default_config(network) +} + pub fn parse_invoice(input: String) -> Result { LiquidSdk::parse_invoice(&input) } diff --git a/lib/bindings/tests/bindings/test_breez_liquid_sdk.kts b/lib/bindings/tests/bindings/test_breez_liquid_sdk.kts index f4f7b27..88a8b21 100644 --- a/lib/bindings/tests/bindings/test_breez_liquid_sdk.kts +++ b/lib/bindings/tests/bindings/test_breez_liquid_sdk.kts @@ -7,9 +7,9 @@ class SDKListener: breez_liquid_sdk.EventListener { try { var mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - - var connectReq = breez_liquid_sdk.ConnectRequest(mnemonic, breez_liquid_sdk.Network.LIQUID_TESTNET) - var sdk = breez_liquid_sdk.connect(connectReq) + var config = breez_liquid_sdk.defaultConfig(breez_liquid_sdk.Network.TESTNET) + var connectRequest = breez_liquid_sdk.ConnectRequest(config, mnemonic) + var sdk = breez_liquid_sdk.connect(connectRequest) var listenerId = sdk.addEventListener(SDKListener()) diff --git a/lib/bindings/tests/bindings/test_breez_liquid_sdk.py b/lib/bindings/tests/bindings/test_breez_liquid_sdk.py index 5c75163..9ae2034 100644 --- a/lib/bindings/tests/bindings/test_breez_liquid_sdk.py +++ b/lib/bindings/tests/bindings/test_breez_liquid_sdk.py @@ -8,9 +8,9 @@ class SDKListener(breez_liquid_sdk.EventListener): def test(): mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - - connect_req = breez_liquid_sdk.ConnectRequest(mnemonic=mnemonic, network=breez_liquid_sdk.Network.LIQUID_TESTNET) - sdk = breez_liquid_sdk.connect(connect_req) + config = breez_liquid_sdk.default_config(breez_liquid_sdk.Network.TESTNET) + connect_request = breez_liquid_sdk.ConnectRequest(config=config, mnemonic=mnemonic) + sdk = breez_liquid_sdk.connect(connect_request) listener_id = sdk.add_event_listener(SDKListener()) diff --git a/lib/bindings/tests/bindings/test_breez_liquid_sdk.swift b/lib/bindings/tests/bindings/test_breez_liquid_sdk.swift index d5d65cd..de1df65 100644 --- a/lib/bindings/tests/bindings/test_breez_liquid_sdk.swift +++ b/lib/bindings/tests/bindings/test_breez_liquid_sdk.swift @@ -7,9 +7,9 @@ class SDKListener: EventListener { } let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - -let connectReq = breez_liquid_sdk.ConnectRequest(mnemonic: mnemonic, network: .liquidTestnet); -let sdk = try breez_liquid_sdk.connect(req: connectReq); +let config = breez_liquid_sdk.defaultConfig(network: .testnet); +let connectRequest = breez_liquid_sdk.ConnectRequest(config: config, mnemonic: mnemonic); +let sdk = try breez_liquid_sdk.connect(req: connectRequest); let listenerId = try sdk.addEventListener(listener: SDKListener()); diff --git a/lib/core/src/bindings.rs b/lib/core/src/bindings.rs index d2cac86..b82c3ae 100644 --- a/lib/core/src/bindings.rs +++ b/lib/core/src/bindings.rs @@ -59,6 +59,10 @@ pub fn breez_log_stream(s: StreamSink) -> Result<()> { Ok(()) } +pub fn default_config(network: Network) -> Config { + LiquidSdk::default_config(network) +} + pub fn parse_invoice(input: String) -> Result { LiquidSdk::parse_invoice(&input) } diff --git a/lib/core/src/frb/bridge.io.rs b/lib/core/src/frb/bridge.io.rs index bc9a94a..65fccb8 100644 --- a/lib/core/src/frb/bridge.io.rs +++ b/lib/core/src/frb/bridge.io.rs @@ -157,13 +157,24 @@ impl CstDecode for *mut u64 { unsafe { *flutter_rust_bridge::for_generated::box_from_leak_ptr(self) } } } +impl CstDecode for wire_cst_config { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::model::Config { + crate::model::Config { + boltz_url: self.boltz_url.cst_decode(), + electrum_url: self.electrum_url.cst_decode(), + working_dir: self.working_dir.cst_decode(), + network: self.network.cst_decode(), + payment_timeout_sec: self.payment_timeout_sec.cst_decode(), + } + } +} impl CstDecode for wire_cst_connect_request { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::model::ConnectRequest { crate::model::ConnectRequest { mnemonic: self.mnemonic.cst_decode(), - data_dir: self.data_dir.cst_decode(), - network: self.network.cst_decode(), + config: self.config.cst_decode(), } } } @@ -472,12 +483,27 @@ impl Default for wire_cst_backup_request { Self::new_with_null_ptr() } } +impl NewWithNullPtr for wire_cst_config { + fn new_with_null_ptr() -> Self { + Self { + boltz_url: core::ptr::null_mut(), + electrum_url: core::ptr::null_mut(), + working_dir: core::ptr::null_mut(), + network: Default::default(), + payment_timeout_sec: Default::default(), + } + } +} +impl Default for wire_cst_config { + fn default() -> Self { + Self::new_with_null_ptr() + } +} impl NewWithNullPtr for wire_cst_connect_request { fn new_with_null_ptr() -> Self { Self { mnemonic: core::ptr::null_mut(), - data_dir: core::ptr::null_mut(), - network: Default::default(), + config: Default::default(), } } } @@ -844,6 +870,14 @@ pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__connect( wire__crate__bindings__connect_impl(port_, req) } +#[no_mangle] +pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__default_config( + port_: i64, + network: i32, +) { + wire__crate__bindings__default_config_impl(port_, network) +} + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__parse_invoice( port_: i64, @@ -1002,10 +1036,18 @@ pub struct wire_cst_backup_request { } #[repr(C)] #[derive(Clone, Copy)] +pub struct wire_cst_config { + boltz_url: *mut wire_cst_list_prim_u_8_strict, + electrum_url: *mut wire_cst_list_prim_u_8_strict, + working_dir: *mut wire_cst_list_prim_u_8_strict, + network: i32, + payment_timeout_sec: u64, +} +#[repr(C)] +#[derive(Clone, Copy)] pub struct wire_cst_connect_request { mnemonic: *mut wire_cst_list_prim_u_8_strict, - data_dir: *mut wire_cst_list_prim_u_8_strict, - network: i32, + config: wire_cst_config, } #[repr(C)] #[derive(Clone, Copy)] diff --git a/lib/core/src/frb/bridge.rs b/lib/core/src/frb/bridge.rs index 91102dd..0f9019b 100644 --- a/lib/core/src/frb/bridge.rs +++ b/lib/core/src/frb/bridge.rs @@ -34,7 +34,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueNom, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.0.0-dev.36"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -532134055; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1028270774; // Section: executor @@ -390,6 +390,26 @@ fn wire__crate__bindings__connect_impl( }, ) } +fn wire__crate__bindings__default_config_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + network: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "default_config", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_network = network.cst_decode(); + move |context| { + transform_result_dco((move || { + Result::<_, ()>::Ok(crate::bindings::default_config(api_network)) + })()) + } + }, + ) +} fn wire__crate__bindings__parse_invoice_impl( port_: flutter_rust_bridge::for_generated::MessagePort, input: impl CstDecode, @@ -553,16 +573,32 @@ impl SseDecode for bool { } } +impl SseDecode for crate::model::Config { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_boltzUrl = ::sse_decode(deserializer); + let mut var_electrumUrl = ::sse_decode(deserializer); + let mut var_workingDir = ::sse_decode(deserializer); + let mut var_network = ::sse_decode(deserializer); + let mut var_paymentTimeoutSec = ::sse_decode(deserializer); + return crate::model::Config { + boltz_url: var_boltzUrl, + electrum_url: var_electrumUrl, + working_dir: var_workingDir, + network: var_network, + payment_timeout_sec: var_paymentTimeoutSec, + }; + } +} + impl SseDecode for crate::model::ConnectRequest { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { let mut var_mnemonic = ::sse_decode(deserializer); - let mut var_dataDir = >::sse_decode(deserializer); - let mut var_network = ::sse_decode(deserializer); + let mut var_config = ::sse_decode(deserializer); return crate::model::ConnectRequest { mnemonic: var_mnemonic, - data_dir: var_dataDir, - network: var_network, + config: var_config, }; } } @@ -1113,12 +1149,30 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::model::Config { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.boltz_url.into_into_dart().into_dart(), + self.electrum_url.into_into_dart().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(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::model::Config {} +impl flutter_rust_bridge::IntoIntoDart for crate::model::Config { + fn into_into_dart(self) -> crate::model::Config { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::model::ConnectRequest { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ self.mnemonic.into_into_dart().into_dart(), - self.data_dir.into_into_dart().into_dart(), - self.network.into_into_dart().into_dart(), + self.config.into_into_dart().into_dart(), ] .into_dart() } @@ -1603,12 +1657,22 @@ impl SseEncode for bool { } } +impl SseEncode for crate::model::Config { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.boltz_url, serializer); + ::sse_encode(self.electrum_url, serializer); + ::sse_encode(self.working_dir, serializer); + ::sse_encode(self.network, serializer); + ::sse_encode(self.payment_timeout_sec, serializer); + } +} + impl SseEncode for crate::model::ConnectRequest { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { ::sse_encode(self.mnemonic, serializer); - >::sse_encode(self.data_dir, serializer); - ::sse_encode(self.network, serializer); + ::sse_encode(self.config, serializer); } } diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index a072f17..1540abe 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -1,11 +1,13 @@ use anyhow::{anyhow, Result}; +use boltz_client::network::electrum::ElectrumConfig; use boltz_client::network::Chain; use boltz_client::swaps::boltzv2::{ - CreateReverseResponse, CreateSubmarineResponse, Leaf, SwapTree, + CreateReverseResponse, CreateSubmarineResponse, Leaf, SwapTree, BOLTZ_MAINNET_URL_V2, + BOLTZ_TESTNET_URL_V2, }; use boltz_client::{Keypair, LBtcSwapScriptV2, ToHex}; use lwk_signer::SwSigner; -use lwk_wollet::{ElectrumUrl, ElementsNetwork, WolletDescriptor}; +use lwk_wollet::{ElectrumClient, ElectrumUrl, ElementsNetwork, WolletDescriptor}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::ToSql; use serde::{Deserialize, Serialize}; @@ -13,6 +15,49 @@ use serde::{Deserialize, Serialize}; use crate::error::PaymentError; use crate::utils; +/// Configuration for the Liquid SDK +#[derive(Clone, Debug, Serialize)] +pub struct Config { + pub boltz_url: String, + pub electrum_url: String, + /// Directory in which all SDK files (DB, log, cache) are stored. + /// + /// Prefix can be a relative or absolute path to this directory. + pub working_dir: String, + pub network: Network, + /// Send payment timeout. See [crate::sdk::LiquidSdk::send_payment] + pub payment_timeout_sec: u64, +} +impl Config { + pub(crate) fn get_electrum_client(&self) -> Result { + ElectrumClient::new(&ElectrumUrl::new(&self.electrum_url, true, true)) + } + + pub(crate) fn get_electrum_config(&self) -> ElectrumConfig { + ElectrumConfig::new(self.network.into(), &self.electrum_url, true, true, 100) + } + + pub fn mainnet() -> Self { + Config { + boltz_url: BOLTZ_MAINNET_URL_V2.to_owned(), + electrum_url: "blockstream.info:995".to_string(), + working_dir: ".".to_string(), + network: Network::Mainnet, + payment_timeout_sec: 15, + } + } + + pub fn testnet() -> Self { + Config { + boltz_url: BOLTZ_TESTNET_URL_V2.to_owned(), + electrum_url: "blockstream.info:465".to_string(), + working_dir: ".".to_string(), + network: Network::Testnet, + payment_timeout_sec: 15, + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Serialize)] pub enum Network { /// Mainnet Bitcoin and Liquid chains @@ -84,39 +129,18 @@ pub enum LiquidSdkEvent { } pub struct LiquidSdkOptions { + pub config: Config, pub signer: SwSigner, - pub network: Network, /// Output script descriptor /// /// See pub descriptor: WolletDescriptor, - /// Absolute or relative path to the data dir, including the dir name. - /// - /// If not set, it defaults to [crate::DEFAULT_DATA_DIR]. - pub data_dir_path: Option, - /// Custom Electrum URL. If set, it must match the specified network. - /// - /// If not set, it defaults to a Blockstream instance. - pub electrum_url: Option, -} - -impl LiquidSdkOptions { - pub(crate) fn get_electrum_url(&self) -> ElectrumUrl { - self.electrum_url.clone().unwrap_or({ - let (url, validate_domain, tls) = match &self.network { - Network::Mainnet => ("blockstream.info:995", true, true), - Network::Testnet => ("blockstream.info:465", true, true), - }; - ElectrumUrl::new(url, tls, validate_domain) - }) - } } #[derive(Debug, Serialize)] pub struct ConnectRequest { pub mnemonic: String, - pub data_dir: Option, - pub network: Network, + pub config: Config, } #[derive(Debug, Serialize)] diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 5c3774c..3898047 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -13,7 +13,6 @@ use boltz_client::network::Chain; use boltz_client::swaps::boltzv2; use boltz_client::ToHex; use boltz_client::{ - network::electrum::ElectrumConfig, swaps::{ boltz::{RevSwapStates, SubSwapStates}, boltzv2::*, @@ -30,8 +29,7 @@ use lwk_wollet::elements::LockTime; use lwk_wollet::hashes::{sha256, Hash}; use lwk_wollet::{ elements::{Address, Transaction}, - BlockchainBackend, ElectrumClient, ElectrumUrl, ElementsNetwork, FsPersister, - Wollet as LwkWollet, WolletDescriptor, + BlockchainBackend, ElementsNetwork, FsPersister, Wollet as LwkWollet, WolletDescriptor, }; use tokio::sync::{watch, Mutex, RwLock}; use tokio::time::MissedTickBehavior; @@ -56,14 +54,12 @@ pub const LIQUID_CLAIM_TX_FEERATE_MSAT: f32 = 100.0; pub const DEFAULT_DATA_DIR: &str = ".data"; pub struct LiquidSdk { - electrum_url: ElectrumUrl, - network: Network, + config: Config, /// LWK Wollet, a watch-only Liquid wallet for this instance lwk_wollet: Arc>, /// LWK Signer, for signing Liquid transactions lwk_signer: SwSigner, persister: Arc, - data_dir_path: String, event_manager: Arc, status_stream: Arc, is_started: RwLock, @@ -73,16 +69,15 @@ pub struct LiquidSdk { impl LiquidSdk { pub async fn connect(req: ConnectRequest) -> Result> { - let is_mainnet = req.network == Network::Mainnet; + let config = req.config; + let is_mainnet = config.network == Network::Mainnet; let signer = SwSigner::new(&req.mnemonic, is_mainnet)?; - let descriptor = LiquidSdk::get_descriptor(&signer, req.network)?; + let descriptor = LiquidSdk::get_descriptor(&signer, config.network)?; let sdk = LiquidSdk::new(LiquidSdkOptions { signer, descriptor, - electrum_url: None, - data_dir_path: req.data_dir, - network: req.network, + config, })?; sdk.start().await?; @@ -90,33 +85,30 @@ impl LiquidSdk { } fn new(opts: LiquidSdkOptions) -> Result> { - let network = opts.network; - let elements_network: ElementsNetwork = opts.network.into(); - let electrum_url = opts.get_electrum_url(); - let data_dir_path = opts.data_dir_path.unwrap_or(DEFAULT_DATA_DIR.to_string()); + let config = opts.config; + let elements_network: ElementsNetwork = config.network.into(); - let lwk_persister = FsPersister::new(&data_dir_path, network.into(), &opts.descriptor)?; + fs::create_dir_all(&config.working_dir)?; + + let lwk_persister = FsPersister::new( + config.working_dir.clone(), + elements_network, + &opts.descriptor, + )?; let lwk_wollet = LwkWollet::new(elements_network, lwk_persister, opts.descriptor)?; - fs::create_dir_all(&data_dir_path)?; - - let persister = Arc::new(Persister::new(&data_dir_path, network)?); + let persister = Arc::new(Persister::new(&config.working_dir, config.network)?); persister.init()?; let event_manager = Arc::new(EventManager::new()); - let status_stream = Arc::new(BoltzStatusStream::new( - Self::boltz_url_v2(network), - persister.clone(), - )); + let status_stream = Arc::new(BoltzStatusStream::new(&config.boltz_url, persister.clone())); let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(()); let sdk = Arc::new(LiquidSdk { + config, lwk_wollet: Arc::new(Mutex::new(lwk_wollet)), - network, - electrum_url, lwk_signer: opts.signer, persister, - data_dir_path, event_manager, status_stream, is_started: RwLock::new(false), @@ -729,24 +721,7 @@ impl LiquidSdk { } pub(crate) fn boltz_client_v2(&self) -> BoltzApiClientV2 { - BoltzApiClientV2::new(Self::boltz_url_v2(self.network)) - } - - pub(crate) fn boltz_url_v2(network: Network) -> &'static str { - match network { - Network::Testnet => BOLTZ_TESTNET_URL_V2, - Network::Mainnet => BOLTZ_MAINNET_URL_V2, - } - } - - fn network_config(&self) -> ElectrumConfig { - ElectrumConfig::new( - self.network.into(), - &self.electrum_url.to_string(), - true, - true, - 100, - ) + BoltzApiClientV2::new(&self.config.boltz_url) } async fn build_tx( @@ -756,7 +731,7 @@ impl LiquidSdk { amount_sat: u64, ) -> Result { let lwk_wollet = self.lwk_wollet.lock().await; - let mut pset = lwk_wollet::TxBuilder::new(self.network.into()) + let mut pset = lwk_wollet::TxBuilder::new(self.config.network.into()) .add_lbtc_recipient( &ElementsAddress::from_str(recipient_address).map_err(|e| { PaymentError::Generic { @@ -780,7 +755,7 @@ impl LiquidSdk { .parse::() .map_err(|_| PaymentError::InvalidInvoice)?; - match (invoice.network().to_string().as_str(), self.network) { + match (invoice.network().to_string().as_str(), self.config.network) { ("bitcoin", Network::Mainnet) => {} ("testnet", Network::Testnet) => {} _ => return Err(PaymentError::InvalidInvoice), @@ -815,7 +790,7 @@ impl LiquidSdk { async fn get_broadcast_fee_estimation(&self, amount_sat: u64) -> Result { // TODO Replace this with own address when LWK supports taproot // https://github.com/Blockstream/lwk/issues/31 - let temp_p2tr_addr = match self.network { + let temp_p2tr_addr = match self.config.network { Network::Mainnet => "lq1pqvzxvqhrf54dd4sny4cag7497pe38252qefk46t92frs7us8r80ja9ha8r5me09nn22m4tmdqp5p4wafq3s59cql3v9n45t5trwtxrmxfsyxjnstkctj", Network::Testnet => "tlq1pq0wqu32e2xacxeyps22x8gjre4qk3u6r70pj4r62hzczxeyz8x3yxucrpn79zy28plc4x37aaf33kwt6dz2nn6gtkya6h02mwpzy4eh69zzexq7cf5y5" }; @@ -871,12 +846,12 @@ impl LiquidSdk { swap_script: &LBtcSwapScriptV2, ) -> Result { let output_address = self.next_unused_address().await?.to_string(); - let network_config = self.network_config(); + let network_config = self.config.get_electrum_config(); Ok(LBtcSwapTxV2::new_refund( swap_script.clone(), &output_address, &network_config, - Self::boltz_url_v2(self.network).to_string(), + self.config.clone().boltz_url, swap_id.to_string(), )?) } @@ -895,7 +870,8 @@ impl LiquidSdk { Some((&self.boltz_client_v2(), &swap.id)), )?; - let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?; + let refund_tx_id = + refund_tx.broadcast(&tx, &self.config.get_electrum_config(), is_lowball)?; info!( "Successfully broadcast cooperative refund for Send Swap {}", &swap.id @@ -925,9 +901,9 @@ impl LiquidSdk { info!("locktime info: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap_script.locktime); match utils::is_locktime_expired(locktime_from_height, swap_script.locktime) { true => { - let tx = - refund_tx.sign_refund(&swap.get_refund_keypair()?, broadcast_fees_sat, None)?; - let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?; + let tx = refund_tx.sign_refund(&swap.get_refund_keypair()?, broadcast_fees_sat, None)?; + let refund_tx_id = + refund_tx.broadcast(&tx, &self.config.get_electrum_config(), is_lowball)?; info!( "Successfully broadcast non-cooperative refund for Send Swap {}", swap.id @@ -950,7 +926,7 @@ impl LiquidSdk { let broadcast_fees_sat = Amount::from_sat(self.get_broadcast_fee_estimation(amount_sat).await?); let client = self.boltz_client_v2(); - let is_lowball = match self.network { + let is_lowball = match self.config.network { Network::Mainnet => None, Network::Testnet => Some((&client, boltz_client::network::Chain::LiquidTestnet)), }; @@ -1031,7 +1007,7 @@ impl LiquidSdk { ) .await?; - let electrum_client = ElectrumClient::new(&self.electrum_url)?; + let electrum_client = self.config.get_electrum_client()?; let lockup_tx_id = electrum_client.broadcast(&lockup_tx)?.to_string(); debug!( @@ -1040,6 +1016,13 @@ impl LiquidSdk { Ok(lockup_tx_id) } + /// Creates, initiates and starts monitoring the progress of a Send Payment. + /// + /// Depending on [Config]'s `payment_timeout_sec`, this function will return: + /// - a [PaymentError::PaymentTimeout], if the payment could not be initiated in this time + /// - a [PaymentState::Pending] payment, if the payment could be initiated, but didn't yet + /// complete in this time + /// - a [PaymentState::Complete] payment, if the payment was successfully completed in this time pub async fn send_payment( &self, req: &PrepareSendResponse, @@ -1110,7 +1093,7 @@ impl LiquidSdk { swap_id: String, accept_zero_conf: bool, ) -> Result { - let timeout_fut = tokio::time::sleep(Duration::from_secs(15)); + let timeout_fut = tokio::time::sleep(Duration::from_secs(self.config.payment_timeout_sec)); tokio::pin!(timeout_fut); let mut events_stream = self.event_manager.subscribe(); @@ -1178,8 +1161,8 @@ impl LiquidSdk { let claim_tx_wrapper = LBtcSwapTxV2::new_claim( swap_script, claim_address, - &self.network_config(), - Self::boltz_url_v2(self.network).into(), + &self.config.get_electrum_config(), + self.config.clone().boltz_url, ongoing_receive_swap.id.clone(), )?; @@ -1194,8 +1177,8 @@ impl LiquidSdk { let claim_tx_id = claim_tx_wrapper.broadcast( &claim_tx, - &self.network_config(), - Some((&self.boltz_client_v2(), self.network.into())), + &self.config.get_electrum_config(), + Some((&self.boltz_client_v2(), self.config.network.into())), )?; info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {swap_id}"); debug!("Claim Tx {:?}", claim_tx); @@ -1329,7 +1312,7 @@ impl LiquidSdk { /// it inserts or updates a corresponding entry in our Payments table. async fn sync_payments_with_chain_data(&self, with_scan: bool) -> Result<()> { if with_scan { - let mut electrum_client = ElectrumClient::new(&self.electrum_url)?; + let mut electrum_client = self.config.get_electrum_client()?; let mut lwk_wollet = self.lwk_wollet.lock().await; lwk_wollet::full_scan_with_electrum_client(&mut lwk_wollet, &mut electrum_client)?; } @@ -1378,9 +1361,11 @@ impl LiquidSdk { info!("Retrieving preimage from non-cooperative claim tx"); let id = &swap.id; - let electrum_client = ElectrumClient::new(&self.electrum_url)?; + let electrum_client = self.config.get_electrum_client()?; let swap_script = swap.get_swap_script()?; - let swap_script_pk = swap_script.to_address(self.network.into())?.script_pubkey(); + let swap_script_pk = swap_script + .to_address(self.config.network.into())? + .script_pubkey(); debug!("Found Send Swap swap_script_pk: {swap_script_pk:?}"); // Get tx history of the swap script (lockup address) @@ -1445,8 +1430,8 @@ impl LiquidSdk { /// Empties all Liquid Wallet caches for this network type. pub fn empty_wallet_cache(&self) -> Result<()> { - let mut path = PathBuf::from(self.data_dir_path.clone()); - path.push(Into::::into(self.network).as_str()); + let mut path = PathBuf::from(self.config.working_dir.clone()); + path.push(Into::::into(self.config.network).as_str()); path.push("enc_cache"); fs::remove_dir_all(&path)?; @@ -1484,6 +1469,13 @@ impl LiquidSdk { self.persister.restore_from_backup(backup_path) } + pub fn default_config(network: Network) -> Config { + match network { + Network::Mainnet => Config::mainnet(), + Network::Testnet => Config::testnet(), + } + } + pub fn parse_invoice(input: &str) -> Result { let input = input .strip_prefix("lightning:") @@ -1566,7 +1558,7 @@ mod tests { use tempdir::TempDir; use crate::model::*; - use crate::sdk::{LiquidSdk, Network}; + use crate::sdk::LiquidSdk; const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; @@ -1594,10 +1586,11 @@ mod tests { #[tokio::test] async fn normal_submarine_swap() -> Result<()> { let (_data_dir, data_dir_str) = create_temp_dir()?; + let mut config = Config::testnet(); + config.working_dir = data_dir_str; let sdk = LiquidSdk::connect(ConnectRequest { mnemonic: TEST_MNEMONIC.to_string(), - data_dir: Some(data_dir_str), - network: Network::Testnet, + config, }) .await?; @@ -1612,10 +1605,11 @@ mod tests { #[tokio::test] async fn reverse_submarine_swap() -> Result<()> { let (_data_dir, data_dir_str) = create_temp_dir()?; + let mut config = Config::testnet(); + config.working_dir = data_dir_str; let sdk = LiquidSdk::connect(ConnectRequest { mnemonic: TEST_MNEMONIC.to_string(), - data_dir: Some(data_dir_str), - network: Network::Testnet, + config, }) .await?; diff --git a/packages/dart/lib/src/bindings.dart b/packages/dart/lib/src/bindings.dart index 132fd90..6b272c3 100644 --- a/packages/dart/lib/src/bindings.dart +++ b/packages/dart/lib/src/bindings.dart @@ -18,6 +18,9 @@ Future connect({required ConnectRequest req, dynamic hint}) => Stream breezLogStream({dynamic hint}) => RustLib.instance.api.crateBindingsBreezLogStream(hint: hint); +Future defaultConfig({required Network network, dynamic hint}) => + RustLib.instance.api.crateBindingsDefaultConfig(network: network, hint: hint); + Future parseInvoice({required String input, dynamic hint}) => RustLib.instance.api.crateBindingsParseInvoice(input: input, hint: hint); diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index df47456..b88164e 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -53,7 +53,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.0.0-dev.36'; @override - int get rustContentHash => -532134055; + int get rustContentHash => 1028270774; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( stem: 'breez_liquid_sdk', @@ -100,6 +100,8 @@ abstract class RustLibApi extends BaseApi { Future crateBindingsConnect({required ConnectRequest req, dynamic hint}); + Future crateBindingsDefaultConfig({required Network network, dynamic hint}); + Future crateBindingsParseInvoice({required String input, dynamic hint}); RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_BindingLiquidSdk; @@ -485,6 +487,29 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["req"], ); + @override + Future crateBindingsDefaultConfig({required Network network, dynamic hint}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_network(network); + return wire.wire__crate__bindings__default_config(port_, arg0); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_config, + decodeErrorData: null, + ), + constMeta: kCrateBindingsDefaultConfigConstMeta, + argValues: [network], + apiImpl: this, + hint: hint, + )); + } + + TaskConstMeta get kCrateBindingsDefaultConfigConstMeta => const TaskConstMeta( + debugName: "default_config", + argNames: ["network"], + ); + @override Future crateBindingsParseInvoice({required String input, dynamic hint}) { return handler.executeNormal(NormalTask( @@ -637,15 +662,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_u_64(raw); } + @protected + Config dco_decode_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return Config( + boltzUrl: dco_decode_String(arr[0]), + electrumUrl: dco_decode_String(arr[1]), + workingDir: dco_decode_String(arr[2]), + network: dco_decode_network(arr[3]), + paymentTimeoutSec: dco_decode_u_64(arr[4]), + ); + } + @protected ConnectRequest dco_decode_connect_request(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); return ConnectRequest( mnemonic: dco_decode_String(arr[0]), - dataDir: dco_decode_opt_String(arr[1]), - network: dco_decode_network(arr[2]), + config: dco_decode_config(arr[1]), ); } @@ -1134,13 +1172,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_u_64(deserializer)); } + @protected + Config sse_decode_config(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_boltzUrl = sse_decode_String(deserializer); + var var_electrumUrl = sse_decode_String(deserializer); + var var_workingDir = sse_decode_String(deserializer); + var var_network = sse_decode_network(deserializer); + var var_paymentTimeoutSec = sse_decode_u_64(deserializer); + return Config( + boltzUrl: var_boltzUrl, + electrumUrl: var_electrumUrl, + workingDir: var_workingDir, + network: var_network, + paymentTimeoutSec: var_paymentTimeoutSec); + } + @protected ConnectRequest sse_decode_connect_request(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs var var_mnemonic = sse_decode_String(deserializer); - var var_dataDir = sse_decode_opt_String(deserializer); - var var_network = sse_decode_network(deserializer); - return ConnectRequest(mnemonic: var_mnemonic, dataDir: var_dataDir, network: var_network); + var var_config = sse_decode_config(deserializer); + return ConnectRequest(mnemonic: var_mnemonic, config: var_config); } @protected @@ -1719,12 +1772,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_64(self, serializer); } + @protected + void sse_encode_config(Config self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.boltzUrl, serializer); + sse_encode_String(self.electrumUrl, serializer); + sse_encode_String(self.workingDir, serializer); + sse_encode_network(self.network, serializer); + sse_encode_u_64(self.paymentTimeoutSec, serializer); + } + @protected void sse_encode_connect_request(ConnectRequest self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_String(self.mnemonic, serializer); - sse_encode_opt_String(self.dataDir, serializer); - sse_encode_network(self.network, serializer); + sse_encode_config(self.config, serializer); } @protected diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index b17092d..e1582b3 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -85,6 +85,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt dco_decode_box_autoadd_u_64(dynamic raw); + @protected + Config dco_decode_config(dynamic raw); + @protected ConnectRequest dco_decode_connect_request(dynamic raw); @@ -246,6 +249,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer); + @protected + Config sse_decode_config(SseDeserializer deserializer); + @protected ConnectRequest sse_decode_connect_request(SseDeserializer deserializer); @@ -575,11 +581,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { cst_api_fill_to_wire_restore_request(apiObj, wireObj.ref); } + @protected + void cst_api_fill_to_wire_config(Config apiObj, wire_cst_config wireObj) { + wireObj.boltz_url = cst_encode_String(apiObj.boltzUrl); + wireObj.electrum_url = cst_encode_String(apiObj.electrumUrl); + wireObj.working_dir = cst_encode_String(apiObj.workingDir); + wireObj.network = cst_encode_network(apiObj.network); + wireObj.payment_timeout_sec = cst_encode_u_64(apiObj.paymentTimeoutSec); + } + @protected void cst_api_fill_to_wire_connect_request(ConnectRequest apiObj, wire_cst_connect_request wireObj) { wireObj.mnemonic = cst_encode_String(apiObj.mnemonic); - wireObj.data_dir = cst_encode_opt_String(apiObj.dataDir); - wireObj.network = cst_encode_network(apiObj.network); + cst_api_fill_to_wire_config(apiObj.config, wireObj.config); } @protected @@ -922,6 +936,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer); + @protected + void sse_encode_config(Config self, SseSerializer serializer); + @protected void sse_encode_connect_request(ConnectRequest self, SseSerializer serializer); @@ -1308,6 +1325,22 @@ class RustLibWire implements BaseWire { late final _wire__crate__bindings__connect = _wire__crate__bindings__connectPtr .asFunction)>(); + void wire__crate__bindings__default_config( + int port_, + int network, + ) { + return _wire__crate__bindings__default_config( + port_, + network, + ); + } + + late final _wire__crate__bindings__default_configPtr = + _lookup>( + 'frbgen_breez_liquid_wire__crate__bindings__default_config'); + late final _wire__crate__bindings__default_config = + _wire__crate__bindings__default_configPtr.asFunction(); + void wire__crate__bindings__parse_invoice( int port_, ffi.Pointer input, @@ -1577,13 +1610,24 @@ final class wire_cst_prepare_send_response extends ffi.Struct { external int fees_sat; } -final class wire_cst_connect_request extends ffi.Struct { - external ffi.Pointer mnemonic; +final class wire_cst_config extends ffi.Struct { + external ffi.Pointer boltz_url; - external ffi.Pointer data_dir; + external ffi.Pointer electrum_url; + + external ffi.Pointer working_dir; @ffi.Int32() external int network; + + @ffi.Uint64() + external int payment_timeout_sec; +} + +final class wire_cst_connect_request extends ffi.Struct { + external ffi.Pointer mnemonic; + + external wire_cst_config config; } final class wire_cst_payment extends ffi.Struct { diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index ae193aa..30c0633 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -28,19 +28,55 @@ class BackupRequest { other is BackupRequest && runtimeType == other.runtimeType && backupPath == other.backupPath; } -class ConnectRequest { - final String mnemonic; - final String? dataDir; - final Network network; +/// Configuration for the Liquid SDK +class Config { + final String boltzUrl; + final String electrumUrl; - const ConnectRequest({ - required this.mnemonic, - this.dataDir, + /// Directory in which all SDK files (DB, log) are stored. + final String workingDir; + final Network network; + final BigInt paymentTimeoutSec; + + const Config({ + required this.boltzUrl, + required this.electrumUrl, + required this.workingDir, required this.network, + required this.paymentTimeoutSec, }); @override - int get hashCode => mnemonic.hashCode ^ dataDir.hashCode ^ network.hashCode; + int get hashCode => + boltzUrl.hashCode ^ + electrumUrl.hashCode ^ + workingDir.hashCode ^ + network.hashCode ^ + paymentTimeoutSec.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Config && + runtimeType == other.runtimeType && + boltzUrl == other.boltzUrl && + electrumUrl == other.electrumUrl && + workingDir == other.workingDir && + network == other.network && + paymentTimeoutSec == other.paymentTimeoutSec; +} + +class ConnectRequest { + final String mnemonic; + final Config config; + + const ConnectRequest({ + required this.mnemonic, + required this.config, + }); + + @override + int get hashCode => mnemonic.hashCode ^ config.hashCode; @override bool operator ==(Object other) => @@ -48,8 +84,7 @@ class ConnectRequest { other is ConnectRequest && runtimeType == other.runtimeType && mnemonic == other.mnemonic && - dataDir == other.dataDir && - network == other.network; + config == other.config; } class GetInfoRequest { diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index 309cfb5..795d7e3 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -294,6 +294,22 @@ class FlutterBreezLiquidBindings { _frbgen_breez_liquid_wire__crate__bindings__connectPtr .asFunction)>(); + void frbgen_breez_liquid_wire__crate__bindings__default_config( + int port_, + int network, + ) { + return _frbgen_breez_liquid_wire__crate__bindings__default_config( + port_, + network, + ); + } + + late final _frbgen_breez_liquid_wire__crate__bindings__default_configPtr = + _lookup>( + 'frbgen_breez_liquid_wire__crate__bindings__default_config'); + late final _frbgen_breez_liquid_wire__crate__bindings__default_config = + _frbgen_breez_liquid_wire__crate__bindings__default_configPtr.asFunction(); + void frbgen_breez_liquid_wire__crate__bindings__parse_invoice( int port_, ffi.Pointer input, @@ -589,13 +605,24 @@ final class wire_cst_prepare_send_response extends ffi.Struct { external int fees_sat; } -final class wire_cst_connect_request extends ffi.Struct { - external ffi.Pointer mnemonic; +final class wire_cst_config extends ffi.Struct { + external ffi.Pointer boltz_url; - external ffi.Pointer data_dir; + external ffi.Pointer electrum_url; + + external ffi.Pointer working_dir; @ffi.Int32() external int network; + + @ffi.Uint64() + external int payment_timeout_sec; +} + +final class wire_cst_connect_request extends ffi.Struct { + external ffi.Pointer mnemonic; + + external wire_cst_config config; } final class wire_cst_payment extends ffi.Struct { diff --git a/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKMapper.kt b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKMapper.kt index 7a9a870..f579504 100644 --- a/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKMapper.kt +++ b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKMapper.kt @@ -34,32 +34,78 @@ fun asBackupRequestList(arr: ReadableArray): List { return list } -fun asConnectRequest(connectRequest: ReadableMap): ConnectRequest? { +fun asConfig(config: ReadableMap): Config? { if (!validateMandatoryFields( - connectRequest, + config, arrayOf( - "mnemonic", + "boltzUrl", + "electrumUrl", + "workingDir", "network", + "paymentTimeoutSec", ), ) ) { return null } - val mnemonic = connectRequest.getString("mnemonic")!! - val network = connectRequest.getString("network")?.let { asNetwork(it) }!! - val dataDir = if (hasNonNullKey(connectRequest, "dataDir")) connectRequest.getString("dataDir") else null - return ConnectRequest( - mnemonic, + val boltzUrl = config.getString("boltzUrl")!! + val electrumUrl = config.getString("electrumUrl")!! + val workingDir = config.getString("workingDir")!! + val network = config.getString("network")?.let { asNetwork(it) }!! + val paymentTimeoutSec = config.getDouble("paymentTimeoutSec").toULong() + return Config( + boltzUrl, + electrumUrl, + workingDir, network, - dataDir, + paymentTimeoutSec, + ) +} + +fun readableMapOf(config: Config): ReadableMap { + return readableMapOf( + "boltzUrl" to config.boltzUrl, + "electrumUrl" to config.electrumUrl, + "workingDir" to config.workingDir, + "network" to config.network.name.lowercase(), + "paymentTimeoutSec" to config.paymentTimeoutSec, + ) +} + +fun asConfigList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + when (value) { + is ReadableMap -> list.add(asConfig(value)!!) + else -> throw LiquidSdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} + +fun asConnectRequest(connectRequest: ReadableMap): ConnectRequest? { + if (!validateMandatoryFields( + connectRequest, + arrayOf( + "config", + "mnemonic", + ), + ) + ) { + return null + } + val config = connectRequest.getMap("config")?.let { asConfig(it) }!! + val mnemonic = connectRequest.getString("mnemonic")!! + return ConnectRequest( + config, + mnemonic, ) } fun readableMapOf(connectRequest: ConnectRequest): ReadableMap { return readableMapOf( + "config" to readableMapOf(connectRequest.config), "mnemonic" to connectRequest.mnemonic, - "network" to connectRequest.network.name.lowercase(), - "dataDir" to connectRequest.dataDir, ) } diff --git a/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKModule.kt b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKModule.kt index c647bd1..6eb28ce 100644 --- a/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKModule.kt +++ b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKModule.kt @@ -3,6 +3,7 @@ package com.breezliquidsdk import breez_liquid_sdk.* import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter +import java.io.File import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -34,12 +35,44 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext throw LiquidSdkException.Generic("Not initialized") } + @Throws(LiquidSdkException::class) + private fun ensureWorkingDir(workingDir: String) { + try { + val workingDirFile = File(workingDir) + + if (!workingDirFile.exists() && !workingDirFile.mkdirs()) { + throw LiquidSdkException.Generic("Mandatory field workingDir must contain a writable directory") + } + } catch (e: SecurityException) { + throw LiquidSdkException.Generic("Mandatory field workingDir must contain a writable directory") + } + } + @ReactMethod fun addListener(eventName: String) {} @ReactMethod fun removeListeners(count: Int) {} + @ReactMethod + fun defaultConfig( + network: String, + promise: Promise, + ) { + executor.execute { + try { + val networkTmp = asNetwork(network) + val res = defaultConfig(networkTmp) + val workingDir = File(reactApplicationContext.filesDir.toString() + "/breezLiquidSdk") + + res.workingDir = workingDir.absolutePath + promise.resolve(readableMapOf(res)) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + @ReactMethod fun parseInvoice( invoice: String, @@ -86,9 +119,9 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext asConnectRequest( req, ) ?: run { throw LiquidSdkException.Generic(errMissingMandatoryField("req", "ConnectRequest")) } - connectRequest.dataDir = connectRequest.dataDir?.takeUnless { - it.isEmpty() - } ?: run { reactApplicationContext.filesDir.toString() + "/breezLiquidSdk" } + + ensureWorkingDir(connectRequest.config.workingDir) + bindingLiquidSdk = connect(connectRequest) promise.resolve(readableMapOf("status" to "ok")) } catch (e: Exception) { diff --git a/packages/react-native/example/App.js b/packages/react-native/example/App.js index 2487182..6a16467 100644 --- a/packages/react-native/example/App.js +++ b/packages/react-native/example/App.js @@ -8,7 +8,7 @@ import React, { useState } from "react" import { SafeAreaView, ScrollView, StatusBar, Text, TouchableOpacity, View } from "react-native" -import { addEventListener, Network, getInfo, connect, removeEventListener } from "@breeztech/react-native-breez-liquid-sdk" +import { addEventListener, connect, defaultConfig, getInfo, Network, removeEventListener } from "@breeztech/react-native-breez-liquid-sdk" import { generateMnemonic } from "@dreson4/react-native-quick-bip39" import { getSecureItem, setSecureItem } from "./utils/storage" @@ -49,7 +49,10 @@ const App = () => { setSecureItem(MNEMONIC_STORE, mnemonic) } - await connect({ mnemonic, network: Network.LIQUID_TESTNET }) + const config = await defaultConfig(Network.TESTNET) + addLine("defaultConfig", JSON.stringify(config)) + + await connect({ config, mnemonic }) addLine("connect", null) listenerId = await addEventListener(eventHandler) diff --git a/packages/react-native/ios/BreezLiquidSDKMapper.swift b/packages/react-native/ios/BreezLiquidSDKMapper.swift index 203bd63..75e3ee6 100644 --- a/packages/react-native/ios/BreezLiquidSDKMapper.swift +++ b/packages/react-native/ios/BreezLiquidSDKMapper.swift @@ -38,35 +38,81 @@ enum BreezLiquidSDKMapper { return backupRequestList.map { v -> [String: Any?] in dictionaryOf(backupRequest: v) } } - static func asConnectRequest(connectRequest: [String: Any?]) throws -> ConnectRequest { - guard let mnemonic = connectRequest["mnemonic"] as? String else { - throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "mnemonic", typeName: "ConnectRequest")) + static func asConfig(config: [String: Any?]) throws -> Config { + guard let boltzUrl = config["boltzUrl"] as? String else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "boltzUrl", typeName: "Config")) } - guard let networkTmp = connectRequest["network"] as? String else { - throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "network", typeName: "ConnectRequest")) + guard let electrumUrl = config["electrumUrl"] as? String else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "electrumUrl", typeName: "Config")) + } + guard let workingDir = config["workingDir"] as? String else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "workingDir", typeName: "Config")) + } + guard let networkTmp = config["network"] as? String else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "network", typeName: "Config")) } let network = try asNetwork(network: networkTmp) - var dataDir: String? - if hasNonNilKey(data: connectRequest, key: "dataDir") { - guard let dataDirTmp = connectRequest["dataDir"] as? String else { - throw LiquidSdkError.Generic(message: errUnexpectedValue(fieldName: "dataDir")) + guard let paymentTimeoutSec = config["paymentTimeoutSec"] as? UInt64 else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "paymentTimeoutSec", typeName: "Config")) + } + + return Config( + boltzUrl: boltzUrl, + electrumUrl: electrumUrl, + workingDir: workingDir, + network: network, + paymentTimeoutSec: paymentTimeoutSec + ) + } + + static func dictionaryOf(config: Config) -> [String: Any?] { + return [ + "boltzUrl": config.boltzUrl, + "electrumUrl": config.electrumUrl, + "workingDir": config.workingDir, + "network": valueOf(network: config.network), + "paymentTimeoutSec": config.paymentTimeoutSec, + ] + } + + static func asConfigList(arr: [Any]) throws -> [Config] { + var list = [Config]() + for value in arr { + if let val = value as? [String: Any?] { + var config = try asConfig(config: val) + list.append(config) + } else { + throw LiquidSdkError.Generic(message: errUnexpectedType(typeName: "Config")) } - dataDir = dataDirTmp + } + return list + } + + static func arrayOf(configList: [Config]) -> [Any] { + return configList.map { v -> [String: Any?] in dictionaryOf(config: v) } + } + + static func asConnectRequest(connectRequest: [String: Any?]) throws -> ConnectRequest { + guard let configTmp = connectRequest["config"] as? [String: Any?] else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "config", typeName: "ConnectRequest")) + } + let config = try asConfig(config: configTmp) + + guard let mnemonic = connectRequest["mnemonic"] as? String else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "mnemonic", typeName: "ConnectRequest")) } return ConnectRequest( - mnemonic: mnemonic, - network: network, - dataDir: dataDir + config: config, + mnemonic: mnemonic ) } static func dictionaryOf(connectRequest: ConnectRequest) -> [String: Any?] { return [ + "config": dictionaryOf(config: connectRequest.config), "mnemonic": connectRequest.mnemonic, - "network": valueOf(network: connectRequest.network), - "dataDir": connectRequest.dataDir == nil ? nil : connectRequest.dataDir, ] } diff --git a/packages/react-native/ios/RNBreezLiquidSDK.m b/packages/react-native/ios/RNBreezLiquidSDK.m index 0f3dded..2f20e42 100644 --- a/packages/react-native/ios/RNBreezLiquidSDK.m +++ b/packages/react-native/ios/RNBreezLiquidSDK.m @@ -3,6 +3,12 @@ @interface RCT_EXTERN_MODULE(RNBreezLiquidSDK, RCTEventEmitter) +RCT_EXTERN_METHOD( + defaultConfig: (NSString*)network + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( parseInvoice: (NSString*)invoice resolve: (RCTPromiseResolveBlock)resolve diff --git a/packages/react-native/ios/RNBreezLiquidSDK.swift b/packages/react-native/ios/RNBreezLiquidSDK.swift index 1a018a2..6cb821c 100644 --- a/packages/react-native/ios/RNBreezLiquidSDK.swift +++ b/packages/react-native/ios/RNBreezLiquidSDK.swift @@ -11,10 +11,15 @@ class RNBreezLiquidSDK: RCTEventEmitter { private var bindingLiquidSdk: BindingLiquidSdk! - static var defaultDataDir: URL { + static var breezLiquidSdkDirectory: URL { let applicationDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + let breezLiquidSdkDirectory = applicationDirectory.appendingPathComponent("breezLiquidSdk", isDirectory: true) - return applicationDirectory.appendingPathComponent("breezLiquidSdk", isDirectory: true) + if !FileManager.default.fileExists(atPath: breezLiquidSdkDirectory.path) { + try! FileManager.default.createDirectory(atPath: breezLiquidSdkDirectory.path, withIntermediateDirectories: true) + } + + return breezLiquidSdkDirectory } override init() { @@ -56,6 +61,28 @@ class RNBreezLiquidSDK: RCTEventEmitter { throw LiquidSdkError.Generic(message: "Not initialized") } + private func ensureWorkingDir(workingDir: String) throws { + do { + if !FileManager.default.fileExists(atPath: workingDir) { + try FileManager.default.createDirectory(atPath: workingDir, withIntermediateDirectories: true) + } + } catch { + throw LiquidSdkError.Generic(message: "Mandatory field workingDir must contain a writable directory") + } + } + + @objc(defaultConfig:resolve:reject:) + func defaultConfig(_ network: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + let networkTmp = try BreezLiquidSDKMapper.asNetwork(network: network) + var res = BreezLiquidSDK.defaultConfig(network: networkTmp) + res.workingDir = RNBreezLiquidSDK.breezLiquidSdkDirectory.path + resolve(BreezLiquidSDKMapper.dictionaryOf(config: res)) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + @objc(parseInvoice:resolve:reject:) func parseInvoice(_ invoice: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { do { @@ -85,7 +112,8 @@ class RNBreezLiquidSDK: RCTEventEmitter { do { var connectRequest = try BreezLiquidSDKMapper.asConnectRequest(connectRequest: req) - connectRequest.dataDir = connectRequest.dataDir == nil || connectRequest.dataDir!.isEmpty ? RNBreezLiquidSDK.defaultDataDir.path : connectRequest.dataDir + try ensureWorkingDir(workingDir: connectRequest.config.workingDir) + bindingLiquidSdk = try BreezLiquidSDK.connect(req: connectRequest) resolve(["status": "ok"]) } catch let err { diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index a08a6de..92e8f24 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -1,4 +1,4 @@ -import { NativeModules, Platform, NativeEventEmitter } from "react-native" +import { NativeModules, Platform, EmitterSubscription, NativeEventEmitter } from "react-native" const LINKING_ERROR = `The package 'react-native-breez-liquid-sdk' doesn't seem to be linked. Make sure: \n\n` + @@ -23,10 +23,17 @@ export interface BackupRequest { backupPath?: string } -export interface ConnectRequest { - mnemonic: string +export interface Config { + boltzUrl: string + electrumUrl: string + workingDir: string network: Network - dataDir?: string + paymentTimeoutSec: number +} + +export interface ConnectRequest { + config: Config + mnemonic: string } export interface GetInfoRequest { @@ -193,6 +200,12 @@ export const setLogger = async (logger: Logger): Promise => return subscription } + +export const defaultConfig = async (network: Network): Promise => { + const response = await BreezLiquidSDK.defaultConfig(network) + return response +} + export const parseInvoice = async (invoice: string): Promise => { const response = await BreezLiquidSDK.parseInvoice(invoice) return response