diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000..b25afd8 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1,6 @@ +target +liquid*/ +history.txt +phrase +*.sql +*.log \ No newline at end of file diff --git a/cli/Cargo.lock b/cli/Cargo.lock index da41277..916a94f 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -401,6 +401,7 @@ dependencies = [ "rustyline", "serde", "serde_json", + "tokio", ] [[package]] @@ -416,6 +417,7 @@ dependencies = [ "lwk_common", "lwk_signer", "lwk_wollet", + "once_cell", "openssl", "rusqlite", "rusqlite_migration", @@ -424,6 +426,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "tokio", "tungstenite", ] @@ -441,9 +444,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" [[package]] name = "byteorder" @@ -2399,9 +2402,21 @@ dependencies = [ "num_cpus", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-rustls" version = "0.25.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2a0eb1f..a3ae7a0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,6 +16,7 @@ qrcode-rs = { version = "0.1", default-features = false } rustyline = { version = "13.0.0", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" +tokio = { version = "1", features = ["macros"] } [patch.crates-io] # https://github.com/BlockstreamResearch/rust-secp256k1-zkp/pull/48/commits diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 0bc3968..0218490 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -38,6 +38,8 @@ pub(crate) enum Command { ListPayments, /// Get the balance and general info of the current instance GetInfo, + /// Sync local data with mempool and onchain data + Sync, /// Empties the encrypted transaction cache EmptyCache, /// Backs up the current pending swaps @@ -89,7 +91,7 @@ macro_rules! wait_confirmation { }; } -pub(crate) fn handle_command( +pub(crate) async fn handle_command( _rl: &mut Editor, sdk: &Arc, command: Command, @@ -116,8 +118,9 @@ pub(crate) fn handle_command( result } Command::SendPayment { bolt11, delay } => { - let prepare_response = - sdk.prepare_send_payment(&PrepareSendRequest { invoice: bolt11 })?; + let prepare_response = sdk + .prepare_send_payment(&PrepareSendRequest { invoice: bolt11 }) + .await?; wait_confirmation!( format!( @@ -131,23 +134,27 @@ pub(crate) fn handle_command( let sdk_cloned = sdk.clone(); let prepare_cloned = prepare_response.clone(); - thread::spawn(move || { + tokio::spawn(async move { thread::sleep(Duration::from_secs(delay)); - sdk_cloned.send_payment(&prepare_cloned).unwrap(); + sdk_cloned.send_payment(&prepare_cloned).await.unwrap(); }); command_result!(prepare_response) } else { - let response = sdk.send_payment(&prepare_response)?; + let response = sdk.send_payment(&prepare_response).await?; command_result!(response) } } Command::GetInfo => { - command_result!(sdk.get_info(GetInfoRequest { with_scan: true })?) + command_result!(sdk.get_info(GetInfoRequest { with_scan: true }).await?) } Command::ListPayments => { let payments = sdk.list_payments()?; command_result!(payments) } + Command::Sync => { + sdk.sync().await?; + command_result!("Synced successfully") + } Command::EmptyCache => { sdk.empty_wallet_cache()?; command_result!("Cache emptied successfully") diff --git a/cli/src/main.rs b/cli/src/main.rs index 210ed3f..28d6db8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -45,7 +45,16 @@ fn show_results(result: Result) -> Result<()> { Ok(println!("{result_str}")) } -fn main() -> Result<()> { +struct CliEventListener {} + +impl EventListener for CliEventListener { + fn on_event(&self, e: LiquidSdkEvent) { + info!("Received event: {:?}", e); + } +} + +#[tokio::main] +async fn main() -> Result<()> { let args = Args::parse(); let data_dir_str = args.data_dir.unwrap_or(DEFAULT_DATA_DIR.to_string()); @@ -66,6 +75,8 @@ fn main() -> Result<()> { env_logger::builder() .target(env_logger::Target::Pipe(Box::new(log_file))) + .filter(None, log::LevelFilter::Debug) + .filter(Some("rustyline"), log::LevelFilter::Warn) .init(); let persistence = CliPersistence { data_dir }; @@ -86,7 +97,11 @@ fn main() -> Result<()> { mnemonic: mnemonic.to_string(), data_dir: Some(data_dir_str), network, - })?; + }) + .await?; + let listener_id = sdk + .add_event_listener(Box::new(CliEventListener {})) + .await?; let cli_prompt = match network { Network::Liquid => "breez-liquid-cli [mainnet]> ", @@ -105,7 +120,7 @@ fn main() -> Result<()> { println!("{}", cli_res.unwrap_err()); continue; } - let res = handle_command(rl, &sdk, cli_res.unwrap()); + let res = handle_command(rl, &sdk, cli_res.unwrap()).await; show_results(res)?; } Err(ReadlineError::Interrupted) => { @@ -123,5 +138,6 @@ fn main() -> Result<()> { } } + sdk.remove_event_listener(listener_id).await?; rl.save_history(history_file).map_err(|e| anyhow!(e)) } diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 901600e..a7965c8 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -520,6 +520,7 @@ dependencies = [ "lwk_common", "lwk_signer", "lwk_wollet", + "once_cell", "openssl", "rusqlite", "rusqlite_migration", @@ -529,6 +530,7 @@ dependencies = [ "serde_json", "tempdir", "thiserror", + "tokio", "tungstenite", "uuid", ] @@ -541,7 +543,9 @@ dependencies = [ "breez-liquid-sdk", "camino", "glob", + "once_cell", "thiserror", + "tokio", "uniffi 0.27.1", "uniffi-kotlin-multiplatform", "uniffi_bindgen 0.25.3", diff --git a/lib/bindings/Cargo.toml b/lib/bindings/Cargo.toml index e18ae46..2ccd863 100644 --- a/lib/bindings/Cargo.toml +++ b/lib/bindings/Cargo.toml @@ -11,7 +11,6 @@ path = "uniffi-bindgen.rs" name = "breez_liquid_sdk_bindings" crate-type = ["staticlib", "cdylib", "lib"] - [dependencies] anyhow = { workspace = true } breez-liquid-sdk = { path = "../core" } @@ -21,6 +20,8 @@ uniffi_bindgen = "0.25.2" uniffi-kotlin-multiplatform = { git = "https://gitlab.com/trixnity/uniffi-kotlin-multiplatform-bindings", rev = "55d51f3abf9819b32bd81756053dcfc10f8d5522" } camino = "1.1.1" thiserror = { workspace = true } +tokio = { version = "1", features = ["rt"] } +once_cell = "*" [build-dependencies] uniffi = { workspace = true, features = [ "build" ] } 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 45d1aea..c60bbd6 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 @@ -20,6 +20,11 @@ typedef struct _Dart_Handle* Dart_Handle; */ #define LIQUID_CLAIM_TX_FEERATE_MSAT 100.0 +typedef struct wire_cst_list_prim_u_8_strict { + uint8_t *ptr; + int32_t len; +} wire_cst_list_prim_u_8_strict; + typedef struct wire_cst_get_info_request { bool with_scan; } wire_cst_get_info_request; @@ -28,11 +33,6 @@ typedef struct wire_cst_prepare_receive_request { uint64_t payer_amount_sat; } wire_cst_prepare_receive_request; -typedef struct wire_cst_list_prim_u_8_strict { - uint8_t *ptr; - int32_t len; -} wire_cst_list_prim_u_8_strict; - typedef struct wire_cst_prepare_send_request { struct wire_cst_list_prim_u_8_strict *invoice; } wire_cst_prepare_send_request; @@ -93,6 +93,44 @@ typedef struct wire_cst_liquid_sdk_error { union LiquidSdkErrorKind kind; } wire_cst_liquid_sdk_error; +typedef struct wire_cst_LiquidSdkEvent_PaymentFailed { + struct wire_cst_payment *details; +} wire_cst_LiquidSdkEvent_PaymentFailed; + +typedef struct wire_cst_LiquidSdkEvent_PaymentPending { + struct wire_cst_payment *details; +} wire_cst_LiquidSdkEvent_PaymentPending; + +typedef struct wire_cst_LiquidSdkEvent_PaymentRefunded { + struct wire_cst_payment *details; +} wire_cst_LiquidSdkEvent_PaymentRefunded; + +typedef struct wire_cst_LiquidSdkEvent_PaymentRefundPending { + struct wire_cst_payment *details; +} wire_cst_LiquidSdkEvent_PaymentRefundPending; + +typedef struct wire_cst_LiquidSdkEvent_PaymentSucceed { + struct wire_cst_payment *details; +} wire_cst_LiquidSdkEvent_PaymentSucceed; + +typedef struct wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation { + struct wire_cst_payment *details; +} wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation; + +typedef union LiquidSdkEventKind { + struct wire_cst_LiquidSdkEvent_PaymentFailed PaymentFailed; + struct wire_cst_LiquidSdkEvent_PaymentPending PaymentPending; + struct wire_cst_LiquidSdkEvent_PaymentRefunded PaymentRefunded; + struct wire_cst_LiquidSdkEvent_PaymentRefundPending PaymentRefundPending; + struct wire_cst_LiquidSdkEvent_PaymentSucceed PaymentSucceed; + struct wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation PaymentWaitingConfirmation; +} LiquidSdkEventKind; + +typedef struct wire_cst_liquid_sdk_event { + int32_t tag; + union LiquidSdkEventKind kind; +} wire_cst_liquid_sdk_event; + typedef struct wire_cst_PaymentError_Generic { struct wire_cst_list_prim_u_8_strict *err; } wire_cst_PaymentError_Generic; @@ -136,6 +174,10 @@ typedef struct wire_cst_send_payment_response { struct wire_cst_list_prim_u_8_strict *txid; } wire_cst_send_payment_response; +void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listener(int64_t port_, + uintptr_t that, + struct wire_cst_list_prim_u_8_strict *listener); + void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backup(int64_t port_, uintptr_t that); @@ -183,6 +225,8 @@ struct wire_cst_connect_request *frbgen_breez_liquid_cst_new_box_autoadd_connect struct wire_cst_get_info_request *frbgen_breez_liquid_cst_new_box_autoadd_get_info_request(void); +struct wire_cst_payment *frbgen_breez_liquid_cst_new_box_autoadd_payment(void); + struct wire_cst_prepare_receive_request *frbgen_breez_liquid_cst_new_box_autoadd_prepare_receive_request(void); struct wire_cst_prepare_receive_response *frbgen_breez_liquid_cst_new_box_autoadd_prepare_receive_response(void); @@ -202,6 +246,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { int64_t dummy_var = 0; dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_connect_request); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_get_info_request); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_payment); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_prepare_receive_request); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_prepare_receive_response); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_prepare_send_request); @@ -212,6 +257,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_prim_u_8_strict); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listener); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backup); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_get_info); diff --git a/lib/bindings/langs/flutter/pubspec.lock b/lib/bindings/langs/flutter/pubspec.lock index c616dc9..1e7db1c 100644 --- a/lib/bindings/langs/flutter/pubspec.lock +++ b/lib/bindings/langs/flutter/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.0" http_parser: dependency: transitive description: @@ -309,10 +309,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.4.2" yaml: dependency: transitive description: @@ -330,4 +330,4 @@ packages: source: hosted version: "2.2.0" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.2.0 <4.0.0" diff --git a/lib/bindings/langs/flutter/scripts/build_other.dart b/lib/bindings/langs/flutter/scripts/build_other.dart index 2f4dc83..def0953 100755 --- a/lib/bindings/langs/flutter/scripts/build_other.dart +++ b/lib/bindings/langs/flutter/scripts/build_other.dart @@ -46,7 +46,8 @@ Future mainImpl(List args) async { final triple = target.triple; final flutterIdentifier = target.flutterIdentifier; await run('rustup target add $triple'); - await run('${target.compiler} --target $triple $profileArg', args: compilerOpts); + await run('${target.compiler} --package breez-liquid-sdk --target $triple $profileArg', + args: compilerOpts); await run('mkdir -p $flutterIdentifier'); await run('cp ../../../../target/$triple/$profile/${target.libName} $flutterIdentifier/'); } diff --git a/lib/bindings/langs/react-native/src/gen_kotlin/mod.rs b/lib/bindings/langs/react-native/src/gen_kotlin/mod.rs index a63d870..d7c8847 100644 --- a/lib/bindings/langs/react-native/src/gen_kotlin/mod.rs +++ b/lib/bindings/langs/react-native/src/gen_kotlin/mod.rs @@ -10,7 +10,7 @@ pub use uniffi_bindgen::bindings::kotlin::gen_kotlin::*; use crate::generator::RNConfig; static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { - let list: Vec<&str> = vec!["connect"]; + let list: Vec<&str> = vec!["connect", "add_event_listener"]; HashSet::from_iter(list.into_iter().map(|s| s.to_string())) }); diff --git a/lib/bindings/langs/react-native/src/gen_kotlin/templates/Objects.kt b/lib/bindings/langs/react-native/src/gen_kotlin/templates/Objects.kt index 31f122c..c748390 100644 --- a/lib/bindings/langs/react-native/src/gen_kotlin/templates/Objects.kt +++ b/lib/bindings/langs/react-native/src/gen_kotlin/templates/Objects.kt @@ -5,7 +5,9 @@ {% let obj = ci.get_object_definition(name).unwrap() %} {% let obj_interface = "getBindingLiquidSdk()." %} {%- for func in obj.methods() -%} +{%- if func.name()|ignored_function == false -%} {%- include "TopLevelFunctionTemplate.kt" %} +{% endif -%} {% endfor %} {%- else -%} {%- endmatch -%} 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 1004217..e83d9c4 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 @@ -67,6 +67,22 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext } } } + + @ReactMethod + fun addEventListener(promise: Promise) { + executor.execute { + try { + val emitter = reactApplicationContext.getJSModule(RCTDeviceEventEmitter::class.java) + var eventListener = BreezLiquidSDKEventListener(emitter) + val res = getBindingLiquidSdk().addEventListener(eventListener) + + eventListener.setId(res) + promise.resolve(res) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } {%- include "Objects.kt" %} } diff --git a/lib/bindings/langs/react-native/src/gen_swift/mod.rs b/lib/bindings/langs/react-native/src/gen_swift/mod.rs index 0cc7015..1a06e28 100644 --- a/lib/bindings/langs/react-native/src/gen_swift/mod.rs +++ b/lib/bindings/langs/react-native/src/gen_swift/mod.rs @@ -9,7 +9,7 @@ use crate::generator::RNConfig; pub use uniffi_bindgen::bindings::swift::gen_swift::*; static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { - let list: Vec<&str> = vec!["connect"]; + let list: Vec<&str> = vec!["connect", "add_event_listener"]; HashSet::from_iter(list.into_iter().map(|s| s.to_string())) }); diff --git a/lib/bindings/langs/react-native/src/gen_swift/templates/Objects.swift b/lib/bindings/langs/react-native/src/gen_swift/templates/Objects.swift index 2d6ab18..29228a4 100644 --- a/lib/bindings/langs/react-native/src/gen_swift/templates/Objects.swift +++ b/lib/bindings/langs/react-native/src/gen_swift/templates/Objects.swift @@ -5,7 +5,9 @@ {% let obj = ci.get_object_definition(name).unwrap() %} {% let obj_interface = "getBindingLiquidSdk()." %} {%- for func in obj.methods() -%} +{%- if func.name()|ignored_function == false -%} {%- include "TopLevelFunctionTemplate.swift" %} +{% endif -%} {% endfor %} {%- else -%} {%- endmatch -%} diff --git a/lib/bindings/langs/react-native/src/gen_swift/templates/extern.m b/lib/bindings/langs/react-native/src/gen_swift/templates/extern.m index 691f204..1d2483e 100644 --- a/lib/bindings/langs/react-native/src/gen_swift/templates/extern.m +++ b/lib/bindings/langs/react-native/src/gen_swift/templates/extern.m @@ -12,13 +12,20 @@ RCT_EXTERN_METHOD( resolve: (RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject ) + +RCT_EXTERN_METHOD( + addEventListener: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) {%- for type_ in ci.iter_types() %} {%- let type_name = type_|type_name %} {%- match type_ %} {%- when Type::Object ( name ) %} {% let obj = ci.get_object_definition(name).unwrap() %} {%- for func in obj.methods() -%} +{%- if func.name()|ignored_function == false -%} {%- include "ExternFunctionTemplate.m" %} +{% endif -%} {% endfor %} {%- else -%} {%- endmatch -%} 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 4271754..8a68d1d 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 @@ -7,6 +7,7 @@ class RNBreezLiquidSDK: RCTEventEmitter { public static var emitter: RCTEventEmitter! public static var hasListeners: Bool = false + public static var supportedEvents: [String] = [] private var bindingLiquidSdk: BindingLiquidSdk! @@ -25,9 +26,13 @@ class RNBreezLiquidSDK: RCTEventEmitter { override static func moduleName() -> String! { TAG } + + static func addSupportedEvent(name: String) { + RNBreezLiquidSDK.supportedEvents.append(name) + } override func supportedEvents() -> [String]! { - return [] + return RNBreezLiquidSDK.supportedEvents } override func startObserving() { @@ -73,6 +78,19 @@ class RNBreezLiquidSDK: RCTEventEmitter { rejectErr(err: err, reject: reject) } } + + @objc(addEventListener:reject:) + func addEventListener(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + var eventListener = BreezLiquidSDKEventListener() + var res = try getBindingLiquidSdk().addEventListener(listener: eventListener) + + eventListener.setId(id: res) + resolve(res) + } catch let err { + rejectErr(err: err, reject: reject) + } + } {%- include "Objects.swift" %} func rejectErr(err: Error, reject: @escaping RCTPromiseRejectBlock) { diff --git a/lib/bindings/langs/react-native/src/gen_typescript/mod.rs b/lib/bindings/langs/react-native/src/gen_typescript/mod.rs index f9b4fcd..851ecb7 100644 --- a/lib/bindings/langs/react-native/src/gen_typescript/mod.rs +++ b/lib/bindings/langs/react-native/src/gen_typescript/mod.rs @@ -26,7 +26,7 @@ static KEYWORDS: Lazy> = Lazy::new(|| { }); static IGNORED_FUNCTIONS: Lazy> = Lazy::new(|| { - let list: Vec<&str> = vec!["connect"]; + let list: Vec<&str> = vec!["connect", "add_event_listener"]; HashSet::from_iter(list.into_iter().map(|s| s.to_string())) }); diff --git a/lib/bindings/langs/react-native/src/gen_typescript/templates/Helpers.ts b/lib/bindings/langs/react-native/src/gen_typescript/templates/Helpers.ts index 939f580..381ec21 100644 --- a/lib/bindings/langs/react-native/src/gen_typescript/templates/Helpers.ts +++ b/lib/bindings/langs/react-native/src/gen_typescript/templates/Helpers.ts @@ -1,5 +1,14 @@ +export type EventListener = (e: LiquidSdkEvent) => void + export const connect = async (req: ConnectRequest): Promise => { const response = await BreezLiquidSDK.connect(req) return response } + +export const addEventListener = async (listener: EventListener): Promise => { + const response = await BreezLiquidSDK.addEventListener() + BreezLiquidSDKEmitter.addListener(`event-${response}`, listener) + + return response +} diff --git a/lib/bindings/langs/react-native/src/gen_typescript/templates/Objects.ts b/lib/bindings/langs/react-native/src/gen_typescript/templates/Objects.ts index d7d07f6..7cbe958 100644 --- a/lib/bindings/langs/react-native/src/gen_typescript/templates/Objects.ts +++ b/lib/bindings/langs/react-native/src/gen_typescript/templates/Objects.ts @@ -4,7 +4,9 @@ {%- when Type::Object ( name ) %} {% let obj = ci.get_object_definition(name).unwrap() %} {%- for func in obj.methods() -%} +{%- if func.name()|ignored_function == false -%} {%- include "TopLevelFunctionTemplate.ts" %} +{% endif -%} {% endfor %} {%- else -%} {%- endmatch -%} 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 bb069e1..d25ae76 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 } from "react-native" +import { NativeModules, Platform, NativeEventEmitter } from "react-native" const LINKING_ERROR = `The package 'react-native-breez-liquid-sdk' doesn't seem to be linked. Make sure: \n\n` + @@ -17,6 +17,7 @@ const BreezLiquidSDK = NativeModules.RNBreezLiquidSDK } ) +const BreezLiquidSDKEmitter = new NativeEventEmitter(BreezLiquidSDK) {%- import "macros.ts" as ts %} {%- include "Types.ts" %} {% include "Helpers.ts" -%} diff --git a/lib/bindings/src/breez_liquid_sdk.udl b/lib/bindings/src/breez_liquid_sdk.udl index d4c2514..9176c06 100644 --- a/lib/bindings/src/breez_liquid_sdk.udl +++ b/lib/bindings/src/breez_liquid_sdk.udl @@ -96,12 +96,33 @@ enum PaymentState { "Failed", }; +[Enum] +interface LiquidSdkEvent { + PaymentFailed(Payment details); + PaymentPending(Payment details); + PaymentRefunded(Payment details); + PaymentRefundPending(Payment details); + PaymentSucceed(Payment details); + PaymentWaitingConfirmation(Payment details); + Synced(); +}; + +callback interface EventListener { + void on_event(LiquidSdkEvent e); +}; + namespace breez_liquid_sdk { [Throws=LiquidSdkError] BindingLiquidSdk connect(ConnectRequest req); }; interface BindingLiquidSdk { + [Throws=LiquidSdkError] + string add_event_listener(EventListener listener); + + [Throws=LiquidSdkError] + void remove_event_listener(string id); + [Throws=LiquidSdkError] GetInfoResponse get_info(GetInfoRequest req); diff --git a/lib/bindings/src/lib.rs b/lib/bindings/src/lib.rs index a1cba5f..2ce87c9 100644 --- a/lib/bindings/src/lib.rs +++ b/lib/bindings/src/lib.rs @@ -2,10 +2,20 @@ use std::sync::Arc; use anyhow::Result; use breez_liquid_sdk::{error::*, model::*, sdk::LiquidSdk}; +use once_cell::sync::Lazy; +use tokio::runtime::Runtime; + +static RT: Lazy = Lazy::new(|| Runtime::new().unwrap()); + +fn rt() -> &'static Runtime { + &RT +} pub fn connect(req: ConnectRequest) -> Result, LiquidSdkError> { - let ln_sdk = LiquidSdk::connect(req)?; - Ok(Arc::from(BindingLiquidSdk { sdk: ln_sdk })) + rt().block_on(async { + let sdk = LiquidSdk::connect(req).await?; + Ok(Arc::from(BindingLiquidSdk { sdk })) + }) } pub struct BindingLiquidSdk { @@ -13,22 +23,30 @@ pub struct BindingLiquidSdk { } impl BindingLiquidSdk { + pub fn add_event_listener(&self, listener: Box) -> LiquidSdkResult { + rt().block_on(self.sdk.add_event_listener(listener)) + } + + pub fn remove_event_listener(&self, id: String) -> LiquidSdkResult<()> { + rt().block_on(self.sdk.remove_event_listener(id)) + } + pub fn get_info(&self, req: GetInfoRequest) -> Result { - self.sdk.get_info(req).map_err(Into::into) + rt().block_on(self.sdk.get_info(req)).map_err(Into::into) } pub fn prepare_send_payment( &self, req: PrepareSendRequest, ) -> Result { - self.sdk.prepare_send_payment(&req) + rt().block_on(self.sdk.prepare_send_payment(&req)) } pub fn send_payment( &self, req: PrepareSendResponse, ) -> Result { - self.sdk.send_payment(&req) + rt().block_on(self.sdk.send_payment(&req)) } pub fn prepare_receive_payment( @@ -49,15 +67,19 @@ impl BindingLiquidSdk { self.sdk.list_payments() } - pub fn sync(&self) -> Result<(), LiquidSdkError> { - self.sdk.sync().map_err(Into::into) + pub fn sync(&self) -> LiquidSdkResult<()> { + rt().block_on(self.sdk.sync()).map_err(Into::into) } - pub fn backup(&self) -> Result<(), LiquidSdkError> { + pub fn empty_wallet_cache(&self) -> LiquidSdkResult<()> { + self.sdk.empty_wallet_cache().map_err(Into::into) + } + + pub fn backup(&self) -> LiquidSdkResult<()> { self.sdk.backup().map_err(Into::into) } - pub fn restore(&self, req: RestoreRequest) -> Result<(), LiquidSdkError> { + pub fn restore(&self, req: RestoreRequest) -> LiquidSdkResult<()> { self.sdk.restore(req).map_err(Into::into) } } diff --git a/lib/bindings/tests/bindings/test_breez_liquid_sdk.kts b/lib/bindings/tests/bindings/test_breez_liquid_sdk.kts index ffe9d13..f4f7b27 100644 --- a/lib/bindings/tests/bindings/test_breez_liquid_sdk.kts +++ b/lib/bindings/tests/bindings/test_breez_liquid_sdk.kts @@ -1,12 +1,23 @@ + +class SDKListener: breez_liquid_sdk.EventListener { + override fun onEvent(e: breez_liquid_sdk.LiquidSdkEvent) { + println(e.toString()); + } +} + 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 listenerId = sdk.addEventListener(SDKListener()) + var getInfoReq = breez_liquid_sdk.GetInfoRequest(false) var nodeInfo = sdk.getInfo(getInfoReq) + sdk.removeEventListener(listenerId) + println("$nodeInfo") assert(nodeInfo.pubkey.equals("03d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee0494")) } catch (ex: Exception) { diff --git a/lib/bindings/tests/bindings/test_breez_liquid_sdk.py b/lib/bindings/tests/bindings/test_breez_liquid_sdk.py index 43e89b2..5c75163 100644 --- a/lib/bindings/tests/bindings/test_breez_liquid_sdk.py +++ b/lib/bindings/tests/bindings/test_breez_liquid_sdk.py @@ -1,14 +1,24 @@ import breez_liquid_sdk + +class SDKListener(breez_liquid_sdk.EventListener): + def on_event(self, event): + print(event) + + 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) + listener_id = sdk.add_event_listener(SDKListener()) + get_info_req = breez_liquid_sdk.GetInfoRequest(with_scan=False) node_info = sdk.get_info(get_info_req) + sdk.remove_event_listener(listener_id) + print(node_info) assert node_info.pubkey == "03d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee0494" diff --git a/lib/bindings/tests/bindings/test_breez_liquid_sdk.swift b/lib/bindings/tests/bindings/test_breez_liquid_sdk.swift index 5cbcb9d..d5d65cd 100644 --- a/lib/bindings/tests/bindings/test_breez_liquid_sdk.swift +++ b/lib/bindings/tests/bindings/test_breez_liquid_sdk.swift @@ -1,12 +1,22 @@ import breez_liquid_sdk +class SDKListener: EventListener { + func onEvent(e: LiquidSdkEvent) { + print("Received event ", e); + } +} + 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 listenerId = try sdk.addEventListener(listener: SDKListener()); + let getInfoReq = breez_liquid_sdk.GetInfoRequest(withScan: false); let nodeInfo = try sdk.getInfo(req: getInfoReq); +try sdk.removeEventListener(id: listenerId); + print(nodeInfo); assert(nodeInfo.pubkey == "03d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee0494", "nodeInfo.pubkey"); \ No newline at end of file diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index eb5fc93..75e7012 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -29,7 +29,9 @@ serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.116" thiserror = { workspace = true } tungstenite = { version = "0.21.0", features = ["native-tls-vendored"] } +once_cell = "1" openssl = { version = "0.10", features = ["vendored"] } +tokio = { version = "1", features = ["rt"] } # Pin these versions to fix iOS build issues security-framework = "=2.10.0" diff --git a/lib/core/src/bindings.rs b/lib/core/src/bindings.rs index 07f8e0a..781ae30 100644 --- a/lib/core/src/bindings.rs +++ b/lib/core/src/bindings.rs @@ -1,10 +1,30 @@ -use crate::{error::*, model::*, sdk::LiquidSdk}; +use crate::{error::*, frb::bridge::StreamSink, model::*, sdk::LiquidSdk}; use anyhow::Result; +use once_cell::sync::Lazy; use std::sync::Arc; +use tokio::runtime::Runtime; + +static RT: Lazy = Lazy::new(|| Runtime::new().unwrap()); + +pub(crate) fn rt() -> &'static Runtime { + &RT +} + +struct BindingEventListener { + stream: StreamSink, +} + +impl EventListener for BindingEventListener { + fn on_event(&self, e: LiquidSdkEvent) { + let _ = self.stream.add(e); + } +} pub fn connect(req: ConnectRequest) -> Result { - let ln_sdk = LiquidSdk::connect(req)?; - Ok(BindingLiquidSdk { sdk: ln_sdk }) + rt().block_on(async { + let ln_sdk = LiquidSdk::connect(req).await?; + Ok(BindingLiquidSdk { sdk: ln_sdk }) + }) } pub struct BindingLiquidSdk { @@ -13,21 +33,31 @@ pub struct BindingLiquidSdk { impl BindingLiquidSdk { pub fn get_info(&self, req: GetInfoRequest) -> Result { - self.sdk.get_info(req).map_err(Into::into) + rt().block_on(self.sdk.get_info(req)).map_err(Into::into) + } + + pub fn add_event_listener( + &self, + listener: StreamSink, + ) -> Result { + rt().block_on( + self.sdk + .add_event_listener(Box::new(BindingEventListener { stream: listener })), + ) } pub fn prepare_send_payment( &self, req: PrepareSendRequest, ) -> Result { - self.sdk.prepare_send_payment(&req) + rt().block_on(self.sdk.prepare_send_payment(&req)) } pub fn send_payment( &self, req: PrepareSendResponse, ) -> Result { - self.sdk.send_payment(&req) + rt().block_on(self.sdk.send_payment(&req)) } pub fn prepare_receive_payment( @@ -49,17 +79,17 @@ impl BindingLiquidSdk { } pub fn sync(&self) -> Result<(), LiquidSdkError> { - self.sdk.sync().map_err(Into::into) - } - - pub fn backup(&self) -> Result<(), LiquidSdkError> { - self.sdk.backup().map_err(Into::into) + rt().block_on(self.sdk.sync()).map_err(Into::into) } pub fn empty_wallet_cache(&self) -> Result<(), LiquidSdkError> { self.sdk.empty_wallet_cache().map_err(Into::into) } + pub fn backup(&self) -> Result<(), LiquidSdkError> { + self.sdk.backup().map_err(Into::into) + } + pub fn restore(&self, req: RestoreRequest) -> Result<(), LiquidSdkError> { self.sdk.restore(req).map_err(Into::into) } diff --git a/lib/core/src/boltz_status_stream.rs b/lib/core/src/boltz_status_stream.rs index 7bb27f5..9ac8e1f 100644 --- a/lib/core/src/boltz_status_stream.rs +++ b/lib/core/src/boltz_status_stream.rs @@ -69,7 +69,7 @@ impl BoltzStatusStream { Ok(socket) } - pub(super) fn track_pending_swaps(sdk: Arc) -> Result<()> { + pub(super) async fn track_pending_swaps(sdk: Arc) -> Result<()> { let mut socket = Self::connect(sdk.clone())?; let reconnect_delay = Duration::from_secs(15); @@ -77,138 +77,141 @@ impl BoltzStatusStream { let mut keep_alive_last_ping_ts = Instant::now(); // Outer loop: reconnects in case the connection is lost - thread::spawn(move || loop { - // Initially subscribe to all ongoing swaps - match sdk.list_ongoing_swaps() { - Ok(initial_ongoing_swaps) => { - info!("Got {} initial ongoing swaps", initial_ongoing_swaps.len()); - for ongoing_swap in &initial_ongoing_swaps { - Self::maybe_subscribe_fn(ongoing_swap, &mut socket); - } - } - Err(e) => error!("Failed to list initial ongoing swaps: {e:?}"), - } - - // Inner loop: iterates over incoming messages and handles them + tokio::spawn(async move { loop { - // Decide if we send a keep-alive ping or not - if Instant::now() - .duration_since(keep_alive_last_ping_ts) - .gt(&keep_alive_ping_interval) - { - match socket.send(Message::Ping(vec![])) { - Ok(_) => debug!("Sent keep-alive ping"), - Err(e) => warn!("Failed to send keep-alive ping: {e:?}"), + // Initially subscribe to all ongoing swaps + match sdk.list_ongoing_swaps() { + Ok(initial_ongoing_swaps) => { + info!("Got {} initial ongoing swaps", initial_ongoing_swaps.len()); + for ongoing_swap in &initial_ongoing_swaps { + Self::maybe_subscribe_fn(ongoing_swap, &mut socket); + } } - keep_alive_last_ping_ts = Instant::now(); + Err(e) => error!("Failed to list initial ongoing swaps: {e:?}"), } - match &socket.read() { - Ok(Message::Close(_)) => { - warn!("Received close msg, exiting socket loop"); - break; - } - Ok(msg) => { - info!("Received msg : {msg:?}"); - - // Each time socket.read() returns, we have the opportunity to socket.send(). - // We use this window to subscribe to any new ongoing swaps. - // This happens on any non-close socket messages, in particular: - // Ping (periodic keep-alive), Text (status update) - match sdk.list_ongoing_swaps() { - Ok(ongoing_swaps) => { - for ongoing_swap in &ongoing_swaps { - Self::maybe_subscribe_fn(ongoing_swap, &mut socket); - } - } - Err(e) => error!("Failed to list new ongoing swaps: {e:?}"), + // Inner loop: iterates over incoming messages and handles them + loop { + // Decide if we send a keep-alive ping or not + if Instant::now() + .duration_since(keep_alive_last_ping_ts) + .gt(&keep_alive_ping_interval) + { + match socket.send(Message::Ping(vec![])) { + Ok(_) => debug!("Sent keep-alive ping"), + Err(e) => warn!("Failed to send keep-alive ping: {e:?}"), } + keep_alive_last_ping_ts = Instant::now(); + } - // We parse and handle any Text websocket messages, which are likely status updates - if msg.is_text() { - info!("Received text msg (status update) : {msg:?}"); + match &socket.read() { + Ok(Message::Close(_)) => { + warn!("Received close msg, exiting socket loop"); + break; + } + Ok(msg) => { + info!("Received msg : {msg:?}"); - match serde_json::from_str::(&msg.to_string()) { - // Subscription confirmation - Ok(SwapUpdate::Subscription { .. }) => {} - - // Status update(s) - Ok(SwapUpdate::Update { - event: _, - channel: _, - args, - }) => { - for boltz_client::swaps::boltzv2::Update { id, status } in args - { - if Self::is_tracked_send_swap(&id) { - match SubSwapStates::from_str(&status) { - Ok(new_state) => { - let res = sdk.try_handle_send_swap_boltz_status( - new_state, - &id, - ); - info!("Handled new Send Swap status from Boltz, result: {res:?}"); - } - Err(_) => error!("Received invalid SubSwapState for Send Swap {id}: {status}") - } - } else if Self::is_tracked_receive_swap(&id) { - match RevSwapStates::from_str(&status) { - Ok(new_state) => { - let res = sdk.try_handle_receive_swap_boltz_status( - new_state, &id, - ); - info!("Handled new Receive Swap status from Boltz, result: {res:?}"); - } - Err(_) => error!("Received invalid RevSwapState for Receive Swap {id}: {status}"), - } - } else { - warn!("Received a status update for swap {id}, which is not tracked as ongoing") - } + // Each time socket.read() returns, we have the opportunity to socket.send(). + // We use this window to subscribe to any new ongoing swaps. + // This happens on any non-close socket messages, in particular: + // Ping (periodic keep-alive), Text (status update) + match sdk.list_ongoing_swaps() { + Ok(ongoing_swaps) => { + for ongoing_swap in &ongoing_swaps { + Self::maybe_subscribe_fn(ongoing_swap, &mut socket); } } + Err(e) => error!("Failed to list new ongoing swaps: {e:?}"), + } - // Error related to subscription, like "Unknown swap ID" - Ok(SwapUpdate::Error { - event: _, - channel: _, - args, - }) => error!("Received a status update error: {args:?}"), + // We parse and handle any Text websocket messages, which are likely status updates + if msg.is_text() { + info!("Received text msg (status update) : {msg:?}"); - Err(e) => warn!("WS response is invalid SwapUpdate: {e:?}"), + match serde_json::from_str::(&msg.to_string()) { + // Subscription confirmation + Ok(SwapUpdate::Subscription { .. }) => {} + + // Status update(s) + Ok(SwapUpdate::Update { + event: _, + channel: _, + args, + }) => { + for boltz_client::swaps::boltzv2::Update { id, status } in + args + { + if Self::is_tracked_send_swap(&id) { + match SubSwapStates::from_str(&status) { + Ok(new_state) => { + let res = sdk.try_handle_send_swap_boltz_status( + new_state, + &id, + ).await; + info!("Handled new Send Swap status from Boltz, result: {res:?}"); + } + Err(_) => error!("Received invalid SubSwapState for Send Swap {id}: {status}") + } + } else if Self::is_tracked_receive_swap(&id) { + match RevSwapStates::from_str(&status) { + Ok(new_state) => { + let res = sdk.try_handle_receive_swap_boltz_status( + new_state, &id, + ).await; + info!("Handled new Receive Swap status from Boltz, result: {res:?}"); + } + Err(_) => error!("Received invalid RevSwapState for Receive Swap {id}: {status}"), + } + } else { + warn!("Received a status update for swap {id}, which is not tracked as ongoing") + } + } + } + + // Error related to subscription, like "Unknown swap ID" + Ok(SwapUpdate::Error { + event: _, + channel: _, + args, + }) => error!("Received a status update error: {args:?}"), + + Err(e) => warn!("WS response is invalid SwapUpdate: {e:?}"), + } } } - } - Err(tungstenite::Error::Io(io_err)) => { - match io_err.kind() { - // Calling socket.read() on a non-blocking stream when there is nothing - // to read results in an WouldBlock error. In this case, we do nothing - // and continue the loop. - ErrorKind::WouldBlock => {} - _ => { - error!("Received stream IO error : {io_err:?}"); - break; + Err(tungstenite::Error::Io(io_err)) => { + match io_err.kind() { + // Calling socket.read() on a non-blocking stream when there is nothing + // to read results in an WouldBlock error. In this case, we do nothing + // and continue the loop. + ErrorKind::WouldBlock => {} + _ => { + error!("Received stream IO error : {io_err:?}"); + break; + } } } - } - Err(tungstenite::Error::AlreadyClosed) => { - thread::sleep(reconnect_delay); - info!("Re-connecting..."); - match Self::connect(sdk.clone()) { - Ok(new_socket) => { - socket = new_socket; - info!("Re-connected to WS stream"); + Err(tungstenite::Error::AlreadyClosed) => { + thread::sleep(reconnect_delay); + info!("Re-connecting..."); + match Self::connect(sdk.clone()) { + Ok(new_socket) => { + socket = new_socket; + info!("Re-connected to WS stream"); - // Clear monitored swaps, so on re-connect we re-subscribe to them - send_swap_ids().lock().unwrap().clear(); - receive_swap_ids().lock().unwrap().clear(); - } - Err(e) => warn!("Failed to re-connected to WS stream: {e:}"), - }; - break; - } - Err(e) => { - error!("Received stream error : {e:?}"); - break; + // Clear monitored swaps, so on re-connect we re-subscribe to them + send_swap_ids().lock().unwrap().clear(); + receive_swap_ids().lock().unwrap().clear(); + } + Err(e) => warn!("Failed to re-connected to WS stream: {e:}"), + }; + break; + } + Err(e) => { + error!("Received stream error : {e:?}"); + break; + } } } } diff --git a/lib/core/src/error.rs b/lib/core/src/error.rs index cf46050..ae544f8 100644 --- a/lib/core/src/error.rs +++ b/lib/core/src/error.rs @@ -1,5 +1,7 @@ use anyhow::Error; +pub type LiquidSdkResult = Result; + #[macro_export] macro_rules! ensure_sdk { ($cond:expr, $err:expr) => { diff --git a/lib/core/src/event.rs b/lib/core/src/event.rs new file mode 100644 index 0000000..8ab14e9 --- /dev/null +++ b/lib/core/src/event.rs @@ -0,0 +1,38 @@ +use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Result; +use tokio::sync::RwLock; + +use crate::model::{EventListener, LiquidSdkEvent}; + +pub(crate) struct EventManager { + listeners: RwLock>>, +} + +impl EventManager { + pub fn new() -> Self { + Self { + listeners: Default::default(), + } + } + + pub async fn add(&self, listener: Box) -> Result { + let id = format!( + "{:X}", + SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() + ); + (*self.listeners.write().await).insert(id.clone(), listener); + Ok(id) + } + + pub async fn remove(&self, id: String) { + (*self.listeners.write().await).remove(&id); + } + + pub async fn notify(&self, e: LiquidSdkEvent) { + for listener in (*self.listeners.read().await).values() { + listener.on_event(e.clone()); + } + } +} diff --git a/lib/core/src/frb/bridge.io.rs b/lib/core/src/frb/bridge.io.rs index 23808ad..21141de 100644 --- a/lib/core/src/frb/bridge.io.rs +++ b/lib/core/src/frb/bridge.io.rs @@ -39,6 +39,20 @@ impl unsafe { decode_rust_opaque_nom(self as _) } } } +impl + CstDecode< + StreamSink, + > for *mut wire_cst_list_prim_u_8_strict +{ + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode( + self, + ) -> StreamSink + { + let raw: String = self.cst_decode(); + StreamSink::deserialize(raw) + } +} impl CstDecode for *mut wire_cst_list_prim_u_8_strict { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> String { @@ -60,6 +74,13 @@ impl CstDecode for *mut wire_cst_get_info_request CstDecode::::cst_decode(*wrap).into() } } +impl CstDecode for *mut wire_cst_payment { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::model::Payment { + let wrap = unsafe { flutter_rust_bridge::for_generated::box_from_leak_ptr(self) }; + CstDecode::::cst_decode(*wrap).into() + } +} impl CstDecode for *mut wire_cst_prepare_receive_request { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::model::PrepareReceiveRequest { @@ -144,6 +165,51 @@ impl CstDecode for wire_cst_liquid_sdk_error { } } } +impl CstDecode for wire_cst_liquid_sdk_event { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::model::LiquidSdkEvent { + match self.tag { + 0 => { + let ans = unsafe { self.kind.PaymentFailed }; + crate::model::LiquidSdkEvent::PaymentFailed { + details: ans.details.cst_decode(), + } + } + 1 => { + let ans = unsafe { self.kind.PaymentPending }; + crate::model::LiquidSdkEvent::PaymentPending { + details: ans.details.cst_decode(), + } + } + 2 => { + let ans = unsafe { self.kind.PaymentRefunded }; + crate::model::LiquidSdkEvent::PaymentRefunded { + details: ans.details.cst_decode(), + } + } + 3 => { + let ans = unsafe { self.kind.PaymentRefundPending }; + crate::model::LiquidSdkEvent::PaymentRefundPending { + details: ans.details.cst_decode(), + } + } + 4 => { + let ans = unsafe { self.kind.PaymentSucceed }; + crate::model::LiquidSdkEvent::PaymentSucceed { + details: ans.details.cst_decode(), + } + } + 5 => { + let ans = unsafe { self.kind.PaymentWaitingConfirmation }; + crate::model::LiquidSdkEvent::PaymentWaitingConfirmation { + details: ans.details.cst_decode(), + } + } + 6 => crate::model::LiquidSdkEvent::Synced, + _ => unreachable!(), + } + } +} impl CstDecode> for *mut wire_cst_list_payment { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> Vec { @@ -338,6 +404,19 @@ impl Default for wire_cst_liquid_sdk_error { Self::new_with_null_ptr() } } +impl NewWithNullPtr for wire_cst_liquid_sdk_event { + fn new_with_null_ptr() -> Self { + Self { + tag: -1, + kind: LiquidSdkEventKind { nil__: () }, + } + } +} +impl Default for wire_cst_liquid_sdk_event { + fn default() -> Self { + Self::new_with_null_ptr() + } +} impl NewWithNullPtr for wire_cst_payment { fn new_with_null_ptr() -> Self { Self { @@ -458,6 +537,15 @@ impl Default for wire_cst_send_payment_response { } } +#[no_mangle] +pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listener( + port_: i64, + that: usize, + listener: *mut wire_cst_list_prim_u_8_strict, +) { + wire__crate__bindings__BindingLiquidSdk_add_event_listener_impl(port_, that, listener) +} + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backup( port_: i64, @@ -586,6 +674,11 @@ pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_get_info_request( ) } +#[no_mangle] +pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_payment() -> *mut wire_cst_payment { + flutter_rust_bridge::for_generated::new_leak_box_ptr(wire_cst_payment::new_with_null_ptr()) +} + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_prepare_receive_request( ) -> *mut wire_cst_prepare_receive_request { @@ -693,6 +786,53 @@ pub struct wire_cst_LiquidSdkError_Generic { } #[repr(C)] #[derive(Clone, Copy)] +pub struct wire_cst_liquid_sdk_event { + tag: i32, + kind: LiquidSdkEventKind, +} +#[repr(C)] +#[derive(Clone, Copy)] +pub union LiquidSdkEventKind { + PaymentFailed: wire_cst_LiquidSdkEvent_PaymentFailed, + PaymentPending: wire_cst_LiquidSdkEvent_PaymentPending, + PaymentRefunded: wire_cst_LiquidSdkEvent_PaymentRefunded, + PaymentRefundPending: wire_cst_LiquidSdkEvent_PaymentRefundPending, + PaymentSucceed: wire_cst_LiquidSdkEvent_PaymentSucceed, + PaymentWaitingConfirmation: wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation, + nil__: (), +} +#[repr(C)] +#[derive(Clone, Copy)] +pub struct wire_cst_LiquidSdkEvent_PaymentFailed { + details: *mut wire_cst_payment, +} +#[repr(C)] +#[derive(Clone, Copy)] +pub struct wire_cst_LiquidSdkEvent_PaymentPending { + details: *mut wire_cst_payment, +} +#[repr(C)] +#[derive(Clone, Copy)] +pub struct wire_cst_LiquidSdkEvent_PaymentRefunded { + details: *mut wire_cst_payment, +} +#[repr(C)] +#[derive(Clone, Copy)] +pub struct wire_cst_LiquidSdkEvent_PaymentRefundPending { + details: *mut wire_cst_payment, +} +#[repr(C)] +#[derive(Clone, Copy)] +pub struct wire_cst_LiquidSdkEvent_PaymentSucceed { + details: *mut wire_cst_payment, +} +#[repr(C)] +#[derive(Clone, Copy)] +pub struct wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation { + details: *mut wire_cst_payment, +} +#[repr(C)] +#[derive(Clone, Copy)] pub struct wire_cst_list_payment { ptr: *mut wire_cst_payment, len: i32, diff --git a/lib/core/src/frb/bridge.rs b/lib/core/src/frb/bridge.rs index 73dc26e..b1cbe5d 100644 --- a/lib/core/src/frb/bridge.rs +++ b/lib/core/src/frb/bridge.rs @@ -33,7 +33,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.35"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1284301568; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 692273053; // Section: executor @@ -41,6 +41,46 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs +fn wire__crate__bindings__BindingLiquidSdk_add_event_listener_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + that: impl CstDecode< + RustOpaqueNom>, + >, + listener: impl CstDecode< + StreamSink, + >, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "BindingLiquidSdk_add_event_listener", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_that = that.cst_decode(); + let api_listener = listener.cst_decode(); + move |context| { + transform_result_dco((move || { + let mut api_that_decoded = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::rust_auto_opaque_decode_compute_order( + vec![api_that.rust_auto_opaque_lock_order_info(0, false)], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_decoded = Some(api_that.rust_auto_opaque_decode_sync_ref()) + } + _ => unreachable!(), + } + } + let api_that = api_that_decoded.unwrap(); + crate::bindings::BindingLiquidSdk::add_event_listener(&api_that, api_listener) + })()) + } + }, + ) +} fn wire__crate__bindings__BindingLiquidSdk_backup_impl( port_: flutter_rust_bridge::for_generated::MessagePort, that: impl CstDecode< @@ -520,6 +560,16 @@ impl SseDecode } } +impl SseDecode + for StreamSink +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return StreamSink::deserialize(inner); + } +} + impl SseDecode for String { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -598,6 +648,57 @@ impl SseDecode for crate::error::LiquidSdkError { } } +impl SseDecode for crate::model::LiquidSdkEvent { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_details = ::sse_decode(deserializer); + return crate::model::LiquidSdkEvent::PaymentFailed { + details: var_details, + }; + } + 1 => { + let mut var_details = ::sse_decode(deserializer); + return crate::model::LiquidSdkEvent::PaymentPending { + details: var_details, + }; + } + 2 => { + let mut var_details = ::sse_decode(deserializer); + return crate::model::LiquidSdkEvent::PaymentRefunded { + details: var_details, + }; + } + 3 => { + let mut var_details = ::sse_decode(deserializer); + return crate::model::LiquidSdkEvent::PaymentRefundPending { + details: var_details, + }; + } + 4 => { + let mut var_details = ::sse_decode(deserializer); + return crate::model::LiquidSdkEvent::PaymentSucceed { + details: var_details, + }; + } + 5 => { + let mut var_details = ::sse_decode(deserializer); + return crate::model::LiquidSdkEvent::PaymentWaitingConfirmation { + details: var_details, + }; + } + 6 => { + return crate::model::LiquidSdkEvent::Synced; + } + _ => { + unimplemented!(""); + } + } + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -987,6 +1088,40 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::model::LiquidSdkEvent { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::model::LiquidSdkEvent::PaymentFailed { details } => { + [0.into_dart(), details.into_into_dart().into_dart()].into_dart() + } + crate::model::LiquidSdkEvent::PaymentPending { details } => { + [1.into_dart(), details.into_into_dart().into_dart()].into_dart() + } + crate::model::LiquidSdkEvent::PaymentRefunded { details } => { + [2.into_dart(), details.into_into_dart().into_dart()].into_dart() + } + crate::model::LiquidSdkEvent::PaymentRefundPending { details } => { + [3.into_dart(), details.into_into_dart().into_dart()].into_dart() + } + crate::model::LiquidSdkEvent::PaymentSucceed { details } => { + [4.into_dart(), details.into_into_dart().into_dart()].into_dart() + } + crate::model::LiquidSdkEvent::PaymentWaitingConfirmation { details } => { + [5.into_dart(), details.into_into_dart().into_dart()].into_dart() + } + crate::model::LiquidSdkEvent::Synced => [6.into_dart()].into_dart(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::model::LiquidSdkEvent {} +impl flutter_rust_bridge::IntoIntoDart + for crate::model::LiquidSdkEvent +{ + fn into_into_dart(self) -> crate::model::LiquidSdkEvent { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::model::Network { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { @@ -1241,6 +1376,15 @@ impl SseEncode } } +impl SseEncode + for StreamSink +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + unimplemented!("") + } +} + impl SseEncode for String { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1300,6 +1444,41 @@ impl SseEncode for crate::error::LiquidSdkError { } } +impl SseEncode for crate::model::LiquidSdkEvent { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::model::LiquidSdkEvent::PaymentFailed { details } => { + ::sse_encode(0, serializer); + ::sse_encode(details, serializer); + } + crate::model::LiquidSdkEvent::PaymentPending { details } => { + ::sse_encode(1, serializer); + ::sse_encode(details, serializer); + } + crate::model::LiquidSdkEvent::PaymentRefunded { details } => { + ::sse_encode(2, serializer); + ::sse_encode(details, serializer); + } + crate::model::LiquidSdkEvent::PaymentRefundPending { details } => { + ::sse_encode(3, serializer); + ::sse_encode(details, serializer); + } + crate::model::LiquidSdkEvent::PaymentSucceed { details } => { + ::sse_encode(4, serializer); + ::sse_encode(details, serializer); + } + crate::model::LiquidSdkEvent::PaymentWaitingConfirmation { details } => { + ::sse_encode(5, serializer); + ::sse_encode(details, serializer); + } + crate::model::LiquidSdkEvent::Synced => { + ::sse_encode(6, serializer); + } + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/lib/core/src/frb/mod.rs b/lib/core/src/frb/mod.rs index 8ce8b6e..83d9377 100644 --- a/lib/core/src/frb/mod.rs +++ b/lib/core/src/frb/mod.rs @@ -1 +1 @@ -mod bridge; +pub(crate) mod bridge; diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index 5e202cb..113df0f 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -2,6 +2,7 @@ pub mod bindings; pub(crate) mod boltz_status_stream; pub mod error; +pub(crate) mod event; #[cfg(feature = "frb")] pub mod frb; pub mod model; diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 7749094..e86030a 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -49,7 +49,24 @@ impl TryFrom<&str> for Network { } } -#[derive(Debug)] +/// Trait that can be used to react to various [LiquidSdkEvent]s emitted by the SDK. +pub trait EventListener: Send + Sync { + fn on_event(&self, e: LiquidSdkEvent); +} + +/// Event emitted by the SDK. To listen for and react to these events, use an [EventListener] when +/// initializing the [LiquidSdk]. +#[derive(Clone, Debug, PartialEq)] +pub enum LiquidSdkEvent { + PaymentFailed { details: Payment }, + PaymentPending { details: Payment }, + PaymentRefunded { details: Payment }, + PaymentRefundPending { details: Payment }, + PaymentSucceed { details: Payment }, + PaymentWaitingConfirmation { details: Payment }, + Synced, +} + pub struct LiquidSdkOptions { pub signer: SwSigner, pub network: Network, @@ -66,6 +83,7 @@ pub struct LiquidSdkOptions { /// 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({ @@ -440,7 +458,7 @@ pub struct PaymentSwapData { /// Represents an SDK payment. /// /// By default, this is an onchain tx. It may represent a swap, if swap metadata is available. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub struct Payment { /// The tx ID of the onchain transaction pub tx_id: String, diff --git a/lib/core/src/persist/mod.rs b/lib/core/src/persist/mod.rs index 2980be7..1b9e923 100644 --- a/lib/core/src/persist/mod.rs +++ b/lib/core/src/persist/mod.rs @@ -7,7 +7,7 @@ use std::{collections::HashMap, fs::create_dir_all, path::PathBuf, str::FromStr} use anyhow::Result; use migrations::current_migrations; -use rusqlite::{params, Connection}; +use rusqlite::{params, Connection, OptionalExtension, Row}; use rusqlite_migration::{Migrations, M}; use crate::model::{Network::*, *}; @@ -92,12 +92,8 @@ impl Persister { Ok([ongoing_send_swaps, ongoing_receive_swaps].concat()) } - pub fn get_payments(&self) -> Result> { - let con = self.get_connection()?; - - // TODO For refund txs, do not create a new Payment - // Assumes there is no swap chaining (send swap lockup tx = receive swap claim tx) - let mut stmt = con.prepare( + fn select_payment_query(&self, where_clause: Option<&str>) -> String { + format!( " SELECT ptx.tx_id, @@ -121,51 +117,76 @@ impl Persister { ON ptx.tx_id = rs.claim_tx_id LEFT JOIN send_swaps AS ss ON ptx.tx_id = ss.lockup_tx_id - ", - )?; + WHERE {} + ", + where_clause.unwrap_or("true") + ) + } + + fn sql_row_to_payment(&self, row: &Row) -> Result { + let tx = PaymentTxData { + tx_id: row.get(0)?, + timestamp: row.get(1)?, + amount_sat: row.get(2)?, + payment_type: row.get(3)?, + is_confirmed: row.get(4)?, + }; + + let maybe_receive_swap_id: Option = row.get(5)?; + let maybe_receive_swap_created_at: Option = row.get(6)?; + let maybe_receive_swap_payer_amount_sat: Option = row.get(7)?; + let maybe_receive_swap_receiver_amount_sat: Option = row.get(8)?; + let maybe_receive_swap_receiver_state: Option = row.get(9)?; + let maybe_send_swap_id: Option = row.get(10)?; + let maybe_send_swap_created_at: Option = row.get(11)?; + let maybe_send_swap_preimage: Option = row.get(12)?; + let maybe_send_swap_payer_amount_sat: Option = row.get(13)?; + let maybe_send_swap_receiver_amount_sat: Option = row.get(14)?; + let maybe_send_swap_state: Option = row.get(15)?; + + let swap = match maybe_receive_swap_id { + Some(receive_swap_id) => Some(PaymentSwapData { + swap_id: receive_swap_id, + created_at: maybe_receive_swap_created_at.unwrap_or(utils::now()), + preimage: None, + payer_amount_sat: maybe_receive_swap_payer_amount_sat.unwrap_or(0), + receiver_amount_sat: maybe_receive_swap_receiver_amount_sat.unwrap_or(0), + status: maybe_receive_swap_receiver_state.unwrap_or(PaymentState::Created), + }), + None => maybe_send_swap_id.map(|send_swap_id| PaymentSwapData { + swap_id: send_swap_id, + created_at: maybe_send_swap_created_at.unwrap_or(utils::now()), + preimage: maybe_send_swap_preimage, + payer_amount_sat: maybe_send_swap_payer_amount_sat.unwrap_or(0), + receiver_amount_sat: maybe_send_swap_receiver_amount_sat.unwrap_or(0), + status: maybe_send_swap_state.unwrap_or(PaymentState::Created), + }), + }; + + Ok(Payment::from(tx, swap)) + } + + pub fn get_payment(&self, id: String) -> Result> { + Ok(self + .get_connection()? + .query_row( + &self.select_payment_query(Some(&format!("ptx.tx_id = ?1"))), + params![id], + |row| self.sql_row_to_payment(row), + ) + .optional()?) + } + + pub fn get_payments(&self) -> Result> { + let con = self.get_connection()?; + + // TODO For refund txs, do not create a new Payment + // Assumes there is no swap chaining (send swap lockup tx = receive swap claim tx) + let mut stmt = con.prepare(&self.select_payment_query(None))?; let data = stmt .query_map(params![], |row| { - let tx = PaymentTxData { - tx_id: row.get(0)?, - timestamp: row.get(1)?, - amount_sat: row.get(2)?, - payment_type: row.get(3)?, - is_confirmed: row.get(4)?, - }; - - let maybe_receive_swap_id: Option = row.get(5)?; - let maybe_receive_swap_created_at: Option = row.get(6)?; - let maybe_receive_swap_payer_amount_sat: Option = row.get(7)?; - let maybe_receive_swap_receiver_amount_sat: Option = row.get(8)?; - let maybe_receive_swap_receiver_state: Option = row.get(9)?; - let maybe_send_swap_id: Option = row.get(10)?; - let maybe_send_swap_created_at: Option = row.get(11)?; - let maybe_send_swap_preimage: Option = row.get(12)?; - let maybe_send_swap_payer_amount_sat: Option = row.get(13)?; - let maybe_send_swap_receiver_amount_sat: Option = row.get(14)?; - let maybe_send_swap_state: Option = row.get(15)?; - - let swap = match maybe_receive_swap_id { - Some(receive_swap_id) => Some(PaymentSwapData { - swap_id: receive_swap_id, - created_at: maybe_receive_swap_created_at.unwrap_or(utils::now()), - preimage: None, - payer_amount_sat: maybe_receive_swap_payer_amount_sat.unwrap_or(0), - receiver_amount_sat: maybe_receive_swap_receiver_amount_sat.unwrap_or(0), - status: maybe_receive_swap_receiver_state.unwrap_or(PaymentState::Created), - }), - None => maybe_send_swap_id.map(|send_swap_id| PaymentSwapData { - swap_id: send_swap_id, - created_at: maybe_send_swap_created_at.unwrap_or(utils::now()), - preimage: maybe_send_swap_preimage, - payer_amount_sat: maybe_send_swap_payer_amount_sat.unwrap_or(0), - receiver_amount_sat: maybe_send_swap_receiver_amount_sat.unwrap_or(0), - status: maybe_send_swap_state.unwrap_or(PaymentState::Created), - }), - }; - - Ok((tx.tx_id.clone(), Payment::from(tx, swap))) + self.sql_row_to_payment(row).map(|p| (p.tx_id.clone(), p)) })? .map(|i| i.unwrap()) .collect(); diff --git a/lib/core/src/persist/receive.rs b/lib/core/src/persist/receive.rs index 9bbd1ff..1e38956 100644 --- a/lib/core/src/persist/receive.rs +++ b/lib/core/src/persist/receive.rs @@ -7,7 +7,7 @@ use crate::persist::Persister; use anyhow::Result; use boltz_client::swaps::boltzv2::CreateReverseResponse; -use rusqlite::{named_params, params, Connection, OptionalExtension, Row}; +use rusqlite::{named_params, params, Connection, Row}; use serde::{Deserialize, Serialize}; impl Persister { @@ -73,13 +73,12 @@ impl Persister { ) } - pub(crate) fn fetch_receive_swap( - con: &Connection, - id: &str, - ) -> rusqlite::Result> { + pub(crate) fn fetch_receive_swap(&self, id: &str) -> Result> { + let con: Connection = self.get_connection()?; let query = Self::list_receive_swaps_query(vec!["id = ?1".to_string()]); - con.query_row(&query, [id], Self::sql_row_to_receive_swap) - .optional() + let res = con.query_row(&query, [id], Self::sql_row_to_receive_swap); + + Ok(res.ok()) } fn sql_row_to_receive_swap(row: &Row) -> rusqlite::Result { @@ -128,10 +127,8 @@ impl Persister { self.list_receive_swaps(con, where_clause) } - pub(crate) fn list_pending_receive_swaps( - &self, - con: &Connection, - ) -> rusqlite::Result> { + pub(crate) fn list_pending_receive_swaps(&self) -> Result> { + let con: Connection = self.get_connection()?; let query = Self::list_receive_swaps_query(vec!["state = ?1".to_string()]); let res = con .prepare(&query)? @@ -147,10 +144,9 @@ impl Persister { /// Pending Receive Swaps, indexed by claim_tx_id pub(crate) fn list_pending_receive_swaps_by_claim_tx_id( &self, - con: &Connection, - ) -> rusqlite::Result> { + ) -> Result> { let res = self - .list_pending_receive_swaps(con)? + .list_pending_receive_swaps()? .iter() .filter_map(|pending_receive_swap| { pending_receive_swap @@ -164,12 +160,12 @@ impl Persister { pub(crate) fn try_handle_receive_swap_update( &self, - con: &Connection, swap_id: &str, to_state: PaymentState, claim_tx_id: Option<&str>, ) -> Result<(), PaymentError> { // Do not overwrite claim_tx_id + let con: Connection = self.get_connection()?; con.execute( "UPDATE receive_swaps SET diff --git a/lib/core/src/persist/send.rs b/lib/core/src/persist/send.rs index 10f8c1a..8d5884e 100644 --- a/lib/core/src/persist/send.rs +++ b/lib/core/src/persist/send.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::Result; use boltz_client::swaps::boltzv2::CreateSubmarineResponse; -use rusqlite::{named_params, params, Connection, OptionalExtension, Row}; +use rusqlite::{named_params, params, Connection, Row}; use serde::{Deserialize, Serialize}; use crate::ensure_sdk; @@ -70,13 +70,12 @@ impl Persister { ) } - pub(crate) fn fetch_send_swap( - con: &Connection, - id: &str, - ) -> rusqlite::Result> { + pub(crate) fn fetch_send_swap(&self, id: &str) -> Result> { + let con: Connection = self.get_connection()?; let query = Self::list_send_swaps_query(vec!["id = ?1".to_string()]); - con.query_row(&query, [id], Self::sql_row_to_send_swap) - .optional() + let res = con.query_row(&query, [id], Self::sql_row_to_send_swap); + + Ok(res.ok()) } fn sql_row_to_send_swap(row: &Row) -> rusqlite::Result { @@ -124,10 +123,8 @@ impl Persister { self.list_send_swaps(con, where_clause) } - pub(crate) fn list_pending_send_swaps( - &self, - con: &Connection, - ) -> rusqlite::Result> { + pub(crate) fn list_pending_send_swaps(&self) -> Result> { + let con: Connection = self.get_connection()?; let query = Self::list_send_swaps_query(vec!["state = ?1".to_string()]); let res = con .prepare(&query)? @@ -140,10 +137,9 @@ impl Persister { /// Pending Send swaps, indexed by refund tx id pub(crate) fn list_pending_send_swaps_by_refund_tx_id( &self, - con: &Connection, - ) -> rusqlite::Result> { + ) -> Result> { let res: HashMap = self - .list_pending_send_swaps(con)? + .list_pending_send_swaps()? .iter() .filter_map(|pending_send_swap| { pending_send_swap @@ -157,7 +153,6 @@ impl Persister { pub(crate) fn try_handle_send_swap_update( &self, - con: &Connection, swap_id: &str, to_state: PaymentState, preimage: Option<&str>, @@ -165,6 +160,7 @@ impl Persister { refund_tx_id: Option<&str>, ) -> Result<(), PaymentError> { // Do not overwrite preimage, lockup_tx_id, refund_tx_id + let con: Connection = self.get_connection()?; con.execute( "UPDATE send_swaps SET diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index e3cd5c7..3083d74 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -1,14 +1,3 @@ -use std::collections::HashMap; -use std::time::Instant; -use std::{ - fs, - path::PathBuf, - str::FromStr, - sync::{Arc, Mutex}, - thread, - time::Duration, -}; - use anyhow::{anyhow, Result}; use boltz_client::{ network::electrum::ElectrumConfig, @@ -31,12 +20,21 @@ use lwk_wollet::{ BlockchainBackend, ElectrumClient, ElectrumUrl, ElementsNetwork, FsPersister, Wollet as LwkWollet, WolletDescriptor, }; +use std::time::Instant; +use std::{fs, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; +use tokio::sync::Mutex; use crate::boltz_status_stream::set_stream_nonblocking; use crate::model::PaymentState::*; use crate::{ - boltz_status_stream::BoltzStatusStream, ensure_sdk, error::PaymentError, get_invoice_amount, - model::*, persist::Persister, utils, + boltz_status_stream::BoltzStatusStream, + ensure_sdk, + error::{LiquidSdkResult, PaymentError}, + event::EventManager, + get_invoice_amount, + model::*, + persist::Persister, + utils, }; /// Claim tx feerate, in sats per vbyte. @@ -54,10 +52,11 @@ pub struct LiquidSdk { lwk_signer: SwSigner, persister: Persister, data_dir_path: String, + event_manager: EventManager, } impl LiquidSdk { - pub fn connect(req: ConnectRequest) -> Result> { + pub async fn connect(req: ConnectRequest) -> Result> { let is_mainnet = req.network == Network::Liquid; let signer = SwSigner::new(&req.mnemonic, is_mainnet)?; let descriptor = LiquidSdk::get_descriptor(&signer, req.network)?; @@ -70,17 +69,19 @@ impl LiquidSdk { network: req.network, })?; - BoltzStatusStream::track_pending_swaps(sdk.clone())?; + BoltzStatusStream::track_pending_swaps(sdk.clone()).await?; // Periodically run sync() in the background let sdk_clone = sdk.clone(); - thread::spawn(move || loop { - thread::sleep(Duration::from_secs(30)); - _ = sdk_clone.sync(); + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(30)).await; + _ = sdk_clone.sync().await; + } }); // Initial sync() before returning the instance - sdk.sync()?; + sdk.sync().await?; Ok(sdk) } @@ -103,6 +104,8 @@ impl LiquidSdk { let persister = Persister::new(&data_dir_path, network)?; persister.init()?; + let event_manager = EventManager::new(); + let sdk = Arc::new(LiquidSdk { lwk_wollet, network, @@ -110,11 +113,29 @@ impl LiquidSdk { lwk_signer: opts.signer, persister, data_dir_path, + event_manager, }); Ok(sdk) } + async fn notify_event_listeners(&self, e: LiquidSdkEvent) -> Result<()> { + self.event_manager.notify(e).await; + Ok(()) + } + + pub async fn add_event_listener( + &self, + listener: Box, + ) -> LiquidSdkResult { + Ok(self.event_manager.add(listener).await?) + } + + pub async fn remove_event_listener(&self, id: String) -> LiquidSdkResult<()> { + self.event_manager.remove(id).await; + Ok(()) + } + fn get_descriptor(signer: &SwSigner, network: Network) -> Result { let is_mainnet = network == Network::Liquid; let descriptor_str = singlesig_desc( @@ -168,7 +189,7 @@ impl LiquidSdk { } /// Transitions a Receive swap to a new state - pub(crate) fn try_handle_receive_swap_update( + pub(crate) async fn try_handle_receive_swap_update( &self, swap_id: &str, to_state: PaymentState, @@ -178,20 +199,24 @@ impl LiquidSdk { "Transitioning Receive swap {swap_id} to {to_state:?} (claim_tx_id = {claim_tx_id:?})" ); - let con = self.persister.get_connection()?; - let swap = Persister::fetch_receive_swap(&con, swap_id) + let swap = self + .persister + .fetch_receive_swap(swap_id) .map_err(|_| PaymentError::PersistError)? .ok_or(PaymentError::Generic { err: format!("Receive Swap not found {swap_id}"), })?; + let payment_id = claim_tx_id.map(|c| c.to_string()).or(swap.claim_tx_id); Self::validate_state_transition(swap.state, to_state)?; self.persister - .try_handle_receive_swap_update(&con, swap_id, to_state, claim_tx_id) + .try_handle_receive_swap_update(swap_id, to_state, claim_tx_id)?; + + Ok(self.emit_payment_updated(payment_id).await?) } /// Transitions a Send swap to a new state - pub(crate) fn try_handle_send_swap_update( + pub(crate) async fn try_handle_send_swap_update( &self, swap_id: &str, to_state: PaymentState, @@ -201,36 +226,132 @@ impl LiquidSdk { ) -> Result<(), PaymentError> { info!("Transitioning Send swap {swap_id} to {to_state:?} (lockup_tx_id = {lockup_tx_id:?}, refund_tx_id = {refund_tx_id:?})"); - let con = self.persister.get_connection()?; - let swap = Persister::fetch_send_swap(&con, swap_id) + let swap: SendSwap = self + .persister + .fetch_send_swap(swap_id) .map_err(|_| PaymentError::PersistError)? .ok_or(PaymentError::Generic { err: format!("Send Swap not found {swap_id}"), })?; + let payment_id = lockup_tx_id.map(|c| c.to_string()).or(swap.lockup_tx_id); Self::validate_state_transition(swap.state, to_state)?; self.persister.try_handle_send_swap_update( - &con, swap_id, to_state, preimage, lockup_tx_id, refund_tx_id, - ) + )?; + Ok(self.emit_payment_updated(payment_id).await?) + } + + async fn emit_payment_updated(&self, payment_id: Option) -> Result<()> { + if let Some(id) = payment_id { + match self.persister.get_payment(id.clone())? { + Some(payment) => { + match payment.status { + Complete => { + self.notify_event_listeners(LiquidSdkEvent::PaymentSucceed { + details: payment, + }) + .await? + } + Pending => { + // The swap state has changed to Pending + match payment.swap_id.clone() { + Some(swap_id) => match payment.payment_type { + PaymentType::Receive => { + match self.persister.fetch_receive_swap(&swap_id)? { + Some(swap) => match swap.claim_tx_id { + Some(_) => { + // The claim tx has now been broadcast + self.notify_event_listeners( + LiquidSdkEvent::PaymentWaitingConfirmation { + details: payment, + }, + ) + .await? + } + None => { + // The lockup tx is in the mempool/confirmed + self.notify_event_listeners( + LiquidSdkEvent::PaymentPending { + details: payment, + }, + ) + .await? + } + }, + None => debug!("Swap not found: {swap_id}"), + } + } + PaymentType::Send => { + match self.persister.fetch_send_swap(&swap_id)? { + Some(swap) => match swap.refund_tx_id { + Some(_) => { + // The refund tx has now been broadcast + self.notify_event_listeners( + LiquidSdkEvent::PaymentRefundPending { + details: payment, + }, + ) + .await? + } + None => { + // The lockup tx is in the mempool/confirmed + self.notify_event_listeners( + LiquidSdkEvent::PaymentPending { + details: payment, + }, + ) + .await? + } + }, + None => debug!("Swap not found: {swap_id}"), + } + } + }, + None => debug!("Payment has no swap id"), + } + } + Failed => match payment.payment_type { + PaymentType::Receive => { + self.notify_event_listeners(LiquidSdkEvent::PaymentFailed { + details: payment, + }) + .await? + } + PaymentType::Send => { + // The refund tx is confirmed + self.notify_event_listeners(LiquidSdkEvent::PaymentRefunded { + details: payment, + }) + .await? + } + }, + _ => (), + }; + } + None => debug!("Payment not found: {id}"), + } + } + Ok(()) } /// Handles status updates from Boltz for Receive swaps - pub(crate) fn try_handle_receive_swap_boltz_status( + pub(crate) async fn try_handle_receive_swap_boltz_status( &self, swap_state: RevSwapStates, id: &str, ) -> Result<()> { - self.sync()?; + self.sync().await?; info!("Handling Receive Swap transition to {swap_state:?} for swap {id}"); - let con = self.persister.get_connection()?; - let receive_swap = Persister::fetch_receive_swap(&con, id)? + let receive_swap = self + .persister + .fetch_receive_swap(id)? .ok_or(anyhow!("No ongoing Receive Swap found for ID {id}"))?; match swap_state { @@ -239,7 +360,7 @@ impl LiquidSdk { | RevSwapStates::TransactionFailed | RevSwapStates::TransactionRefunded => { error!("Swap {id} entered into an unrecoverable state: {swap_state:?}"); - self.try_handle_receive_swap_update(id, Failed, None)?; + self.try_handle_receive_swap_update(id, Failed, None).await?; } // The lockup tx is in the mempool and we accept 0-conf => try to claim @@ -251,7 +372,7 @@ impl LiquidSdk { Some(claim_tx_id) => { warn!("Claim tx for Receive Swap {id} was already broadcast: txid {claim_tx_id}") } - None => match self.try_claim(&receive_swap) { + None => match self.try_claim(&receive_swap).await { Ok(()) => {} Err(err) => match err { PaymentError::AlreadyClaimed => warn!("Funds already claimed for Receive Swap {id}"), @@ -272,17 +393,18 @@ impl LiquidSdk { } /// Handles status updates from Boltz for Send swaps - pub(crate) fn try_handle_send_swap_boltz_status( + pub(crate) async fn try_handle_send_swap_boltz_status( &self, swap_state: SubSwapStates, id: &str, ) -> Result<()> { - self.sync()?; + self.sync().await?; info!("Handling Send Swap transition to {swap_state:?} for swap {id}"); - let con = self.persister.get_connection()?; - let ongoing_send_swap = Persister::fetch_send_swap(&con, id)? + let ongoing_send_swap = self + .persister + .fetch_send_swap(id)? .ok_or(anyhow!("No ongoing Send Swap found for ID {id}"))?; let create_response: CreateSubmarineResponse = ongoing_send_swap.get_boltz_create_response()?; @@ -308,6 +430,7 @@ impl LiquidSdk { &ongoing_send_swap.invoice, &keypair, ) + .await .map_err(|e| anyhow!("Could not post claim details. Err: {e:?}"))?; // We insert a pseudo-lockup-tx in case LWK fails to pick up the new mempool tx for a while @@ -327,7 +450,8 @@ impl LiquidSdk { warn!("Swap-in {id} has already been claimed"); let preimage = self.get_preimage_from_script_path_claim_spend(&ongoing_send_swap)?; - self.validate_send_swap_preimage(id, &ongoing_send_swap.invoice, &preimage)?; + self.validate_send_swap_preimage(id, &ongoing_send_swap.invoice, &preimage) + .await?; Ok(()) } @@ -343,10 +467,12 @@ impl LiquidSdk { ) .map_err(|e| anyhow!("Could not rebuild refund details for swap-in {id}: {e:?}"))?; - let refund_tx_id = - self.try_refund(id, &swap_script, &keypair, receiver_amount_sat)?; + let refund_tx_id = self + .try_refund(id, &swap_script, &keypair, receiver_amount_sat) + .await?; info!("Broadcast refund tx for Swap-in {id}. Tx id: {refund_tx_id}"); - self.try_handle_send_swap_update(id, Pending, None, None, Some(&refund_tx_id))?; + self.try_handle_send_swap_update(id, Pending, None, None, Some(&refund_tx_id)) + .await?; Ok(()) } @@ -359,16 +485,16 @@ impl LiquidSdk { } /// Gets the next unused onchain Liquid address - fn next_unused_address(&self) -> Result { - let lwk_wollet = self.lwk_wollet.lock().unwrap(); + async fn next_unused_address(&self) -> Result { + let lwk_wollet = self.lwk_wollet.lock().await; Ok(lwk_wollet.address(None)?.address().clone()) } - pub fn get_info(&self, req: GetInfoRequest) -> Result { - debug!("next_unused_address: {}", self.next_unused_address()?); + pub async fn get_info(&self, req: GetInfoRequest) -> Result { + debug!("next_unused_address: {}", self.next_unused_address().await?); if req.with_scan { - self.sync()?; + self.sync().await?; } let mut pending_send_sat = 0; @@ -420,13 +546,13 @@ impl LiquidSdk { ) } - fn build_tx( + async fn build_tx( &self, fee_rate: Option, recipient_address: &str, amount_sat: u64, ) -> Result { - let lwk_wollet = self.lwk_wollet.lock().unwrap(); + let lwk_wollet = self.lwk_wollet.lock().await; let mut pset = lwk_wollet::TxBuilder::new(self.network.into()) .add_lbtc_recipient( &ElementsAddress::from_str(recipient_address).map_err(|e| { @@ -483,7 +609,7 @@ impl LiquidSdk { Ok(lbtc_pair) } - fn get_broadcast_fee_estimation(&self, amount_sat: u64) -> Result { + 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 { @@ -493,13 +619,14 @@ impl LiquidSdk { // Create a throw-away tx similar to the lockup tx, in order to estimate fees Ok(self - .build_tx(None, temp_p2tr_addr, amount_sat)? + .build_tx(None, temp_p2tr_addr, amount_sat) + .await? .all_fees() .values() .sum()) } - pub fn prepare_send_payment( + pub async fn prepare_send_payment( &self, req: &PrepareSendRequest, ) -> Result { @@ -512,7 +639,9 @@ impl LiquidSdk { let client = self.boltz_client_v2(); let lbtc_pair = Self::validate_submarine_pairs(&client, receiver_amount_sat)?; - let broadcast_fees_sat = self.get_broadcast_fee_estimation(receiver_amount_sat)?; + let broadcast_fees_sat = self + .get_broadcast_fee_estimation(receiver_amount_sat) + .await?; Ok(PrepareSendResponse { invoice: req.invoice.clone(), @@ -531,8 +660,11 @@ impl LiquidSdk { .ok_or(PaymentError::InvalidPreimage) } - fn new_refund_tx(&self, swap_script: &LBtcSwapScriptV2) -> Result { - let wallet = self.lwk_wollet.lock().unwrap(); + async fn new_refund_tx( + &self, + swap_script: &LBtcSwapScriptV2, + ) -> Result { + let wallet = self.lwk_wollet.lock().await; let output_address = wallet.address(Some(0))?.address().to_string(); let network_config = self.network_config(); Ok(LBtcSwapTxV2::new_refund( @@ -542,16 +674,17 @@ impl LiquidSdk { )?) } - fn try_refund( + async fn try_refund( &self, swap_id: &str, swap_script: &LBtcSwapScriptV2, keypair: &Keypair, amount_sat: u64, ) -> Result { - let refund_tx = self.new_refund_tx(swap_script)?; + let refund_tx = self.new_refund_tx(swap_script).await?; - let broadcast_fees_sat = Amount::from_sat(self.get_broadcast_fee_estimation(amount_sat)?); + let broadcast_fees_sat = + Amount::from_sat(self.get_broadcast_fee_estimation(amount_sat).await?); let client = self.boltz_client_v2(); let is_lowball = Some((&client, boltz_client::network::Chain::from(self.network))); @@ -578,7 +711,7 @@ impl LiquidSdk { } /// Check if the provided preimage matches our invoice. If so, mark the Send payment as [Complete]. - fn validate_send_swap_preimage( + async fn validate_send_swap_preimage( &self, swap_id: &str, invoice: &str, @@ -587,10 +720,11 @@ impl LiquidSdk { Self::verify_payment_hash(preimage, invoice)?; info!("Preimage is valid for Send Swap {swap_id}"); self.try_handle_send_swap_update(swap_id, Complete, Some(preimage), None, None) + .await } /// Interact with Boltz to assist in them doing a cooperative claim - fn cooperate_send_swap_claim( + async fn cooperate_send_swap_claim( &self, swap_id: &str, swap_script: &LBtcSwapScriptV2, @@ -599,12 +733,13 @@ impl LiquidSdk { ) -> Result<(), PaymentError> { debug!("Claim is pending for swap-in {swap_id}. Initiating cooperative claim"); let client = self.boltz_client_v2(); - let refund_tx = self.new_refund_tx(swap_script)?; + let refund_tx = self.new_refund_tx(swap_script).await?; let claim_tx_response = client.get_claim_tx_details(&swap_id.to_string())?; debug!("Received claim tx details: {:?}", &claim_tx_response); - self.validate_send_swap_preimage(swap_id, invoice, &claim_tx_response.preimage)?; + self.validate_send_swap_preimage(swap_id, invoice, &claim_tx_response.preimage) + .await?; let (partial_sig, pub_nonce) = refund_tx.submarine_partial_sig(keypair, &claim_tx_response)?; @@ -614,7 +749,7 @@ impl LiquidSdk { Ok(()) } - fn lockup_funds( + async fn lockup_funds( &self, swap_id: &str, create_response: &CreateSubmarineResponse, @@ -624,11 +759,13 @@ impl LiquidSdk { create_response.expected_amount, create_response.address ); - let lockup_tx = self.build_tx( - None, - &create_response.address, - create_response.expected_amount, - )?; + let lockup_tx = self + .build_tx( + None, + &create_response.address, + create_response.expected_amount, + ) + .await?; let electrum_client = ElectrumClient::new(&self.electrum_url)?; let lockup_tx_id = electrum_client.broadcast(&lockup_tx)?.to_string(); @@ -639,7 +776,7 @@ impl LiquidSdk { Ok(lockup_tx_id) } - pub fn send_payment( + pub async fn send_payment( &self, req: &PrepareSendResponse, ) -> Result { @@ -648,7 +785,9 @@ impl LiquidSdk { let client = self.boltz_client_v2(); let lbtc_pair = Self::validate_submarine_pairs(&client, receiver_amount_sat)?; - let broadcast_fees_sat = self.get_broadcast_fee_estimation(receiver_amount_sat)?; + let broadcast_fees_sat = self + .get_broadcast_fee_estimation(receiver_amount_sat) + .await?; ensure_sdk!( req.fees_sat == lbtc_pair.fees.total(receiver_amount_sat) + broadcast_fees_sat, PaymentError::InvalidOrExpiredFees @@ -719,16 +858,16 @@ impl LiquidSdk { })?; // Sync before handling new state - self.sync()?; + self.sync().await?; // See https://docs.boltz.exchange/v/api/lifecycle#normal-submarine-swaps match state { // Boltz has locked the HTLC, we proceed with locking up the funds SubSwapStates::InvoiceSet => { // Check that we have not persisted the swap already - let con = self.persister.get_connection()?; - - if let Some(ongoing_swap) = Persister::fetch_send_swap(&con, swap_id) + if let Some(ongoing_swap) = self + .persister + .fetch_send_swap(swap_id) .map_err(|_| PaymentError::PersistError)? { if ongoing_swap.lockup_tx_id.is_some() { @@ -736,14 +875,15 @@ impl LiquidSdk { } }; - lockup_tx_id = self.lockup_funds(swap_id, &create_response)?; + lockup_tx_id = self.lockup_funds(swap_id, &create_response).await?; self.try_handle_send_swap_update( swap_id, Pending, None, Some(&lockup_tx_id), None, - )?; + ) + .await?; } // Boltz has detected the lockup in the mempool, we can speed up @@ -751,7 +891,8 @@ impl LiquidSdk { SubSwapStates::TransactionClaimPending => { // TODO Consolidate status handling: merge with and reuse try_handle_send_swap_boltz_status - self.cooperate_send_swap_claim(swap_id, &swap_script, &req.invoice, &keypair)?; + self.cooperate_send_swap_claim(swap_id, &swap_script, &req.invoice, &keypair) + .await?; debug!("Boltz successfully claimed the funds"); BoltzStatusStream::unmark_swap_as_tracked(swap_id, SwapType::Submarine); @@ -770,8 +911,9 @@ impl LiquidSdk { SubSwapStates::InvoiceFailedToPay | SubSwapStates::SwapExpired | SubSwapStates::TransactionLockupFailed => { - let refund_tx_id = - self.try_refund(swap_id, &swap_script, &keypair, receiver_amount_sat)?; + let refund_tx_id = self + .try_refund(swap_id, &swap_script, &keypair, receiver_amount_sat) + .await?; result = Err(PaymentError::Refunded { err: format!( @@ -790,7 +932,7 @@ impl LiquidSdk { result } - fn try_claim(&self, ongoing_receive_swap: &ReceiveSwap) -> Result<(), PaymentError> { + async fn try_claim(&self, ongoing_receive_swap: &ReceiveSwap) -> Result<(), PaymentError> { ensure_sdk!( ongoing_receive_swap.claim_tx_id.is_none(), PaymentError::AlreadyClaimed @@ -799,6 +941,9 @@ impl LiquidSdk { let swap_id = &ongoing_receive_swap.id; debug!("Trying to claim Receive Swap {swap_id}",); + self.try_handle_receive_swap_update(swap_id, Pending, None) + .await?; + let lsk = self.get_liquid_swap_key()?; let our_keys = lsk.keypair; @@ -808,7 +953,7 @@ impl LiquidSdk { our_keys.public_key().into(), )?; - let claim_address = self.next_unused_address()?.to_string(); + let claim_address = self.next_unused_address().await?.to_string(); let claim_tx_wrapper = LBtcSwapTxV2::new_claim( swap_script, claim_address, @@ -834,18 +979,19 @@ impl LiquidSdk { info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {swap_id}"); debug!("Claim Tx {:?}", claim_tx); - self.try_handle_receive_swap_update(swap_id, Pending, Some(&claim_tx_id))?; - // We insert a pseudo-claim-tx in case LWK fails to pick up the new mempool tx for a while // This makes the tx known to the SDK (get_info, list_payments) instantly self.persister.insert_or_update_payment(PaymentTxData { - tx_id: claim_tx_id, + tx_id: claim_tx_id.clone(), timestamp: None, amount_sat: ongoing_receive_swap.receiver_amount_sat, payment_type: PaymentType::Receive, is_confirmed: false, })?; + self.try_handle_receive_swap_update(swap_id, Pending, Some(&claim_tx_id)) + .await?; + Ok(()) } @@ -954,22 +1100,19 @@ impl LiquidSdk { /// This method fetches the chain tx data (onchain and mempool) using LWK. For every wallet tx, /// it inserts or updates a corresponding entry in our Payments table. - fn sync_payments_with_chain_data(&self, with_scan: bool) -> Result<()> { + 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 lwk_wollet = self.lwk_wollet.lock().unwrap(); + let mut lwk_wollet = self.lwk_wollet.lock().await; lwk_wollet::full_scan_with_electrum_client(&mut lwk_wollet, &mut electrum_client)?; } - let con = self.persister.get_connection()?; - let pending_receive_swaps_by_claim_tx_id: HashMap = self - .persister - .list_pending_receive_swaps_by_claim_tx_id(&con)?; - let pending_send_swaps_by_refund_tx_id: HashMap = self - .persister - .list_pending_send_swaps_by_refund_tx_id(&con)?; + let pending_receive_swaps_by_claim_tx_id = + self.persister.list_pending_receive_swaps_by_claim_tx_id()?; + let pending_send_swaps_by_refund_tx_id = + self.persister.list_pending_send_swaps_by_refund_tx_id()?; - for tx in self.lwk_wollet.lock().unwrap().transactions()? { + for tx in self.lwk_wollet.lock().await.transactions()? { let tx_id = tx.txid.to_string(); let is_tx_confirmed = tx.height.is_some(); let amount_sat = tx.balance.values().sum::(); @@ -977,10 +1120,12 @@ impl LiquidSdk { // Transition the swaps whose state depends on this tx being confirmed if is_tx_confirmed { if let Some(swap) = pending_receive_swaps_by_claim_tx_id.get(&tx_id) { - self.try_handle_receive_swap_update(&swap.id, Complete, None)?; + self.try_handle_receive_swap_update(&swap.id, Complete, None) + .await?; } if let Some(swap) = pending_send_swaps_by_refund_tx_id.get(&tx_id) { - self.try_handle_send_swap_update(&swap.id, Failed, None, None, None)?; + self.try_handle_send_swap_update(&swap.id, Failed, None, None, None) + .await?; } } @@ -1096,12 +1241,13 @@ impl LiquidSdk { } /// Synchronize the DB with mempool and onchain data - pub fn sync(&self) -> Result<()> { + pub async fn sync(&self) -> Result<()> { let t0 = Instant::now(); - self.sync_payments_with_chain_data(true)?; + self.sync_payments_with_chain_data(true).await?; let duration_ms = Instant::now().duration_since(t0).as_millis(); info!("Synchronized with mempool and onchain data (t = {duration_ms} ms)"); + self.notify_event_listeners(LiquidSdkEvent::Synced).await?; Ok(()) } @@ -1155,30 +1301,33 @@ mod tests { .collect()) } - #[test] - fn normal_submarine_swap() -> Result<()> { + #[tokio::test] + async fn normal_submarine_swap() -> Result<()> { let (_data_dir, data_dir_str) = create_temp_dir()?; let sdk = LiquidSdk::connect(ConnectRequest { mnemonic: TEST_MNEMONIC.to_string(), data_dir: Some(data_dir_str), network: Network::LiquidTestnet, - })?; + }) + .await?; let invoice = "lntb10u1pnqwkjrpp5j8ucv9mgww0ajk95yfpvuq0gg5825s207clrzl5thvtuzfn68h0sdqqcqzzsxqr23srzjqv8clnrfs9keq3zlg589jvzpw87cqh6rjks0f9g2t9tvuvcqgcl45f6pqqqqqfcqqyqqqqlgqqqqqqgq2qsp5jnuprlxrargr6hgnnahl28nvutj3gkmxmmssu8ztfhmmey3gq2ss9qyyssq9ejvcp6frwklf73xvskzdcuhnnw8dmxag6v44pffwqrxznsly4nqedem3p3zhn6u4ln7k79vk6zv55jjljhnac4gnvr677fyhfgn07qp4x6wrq".to_string(); - sdk.prepare_send_payment(&PrepareSendRequest { invoice })?; + sdk.prepare_send_payment(&PrepareSendRequest { invoice }) + .await?; assert!(!list_pending(&sdk)?.is_empty()); Ok(()) } - #[test] - fn reverse_submarine_swap() -> Result<()> { + #[tokio::test] + async fn reverse_submarine_swap() -> Result<()> { let (_data_dir, data_dir_str) = create_temp_dir()?; let sdk = LiquidSdk::connect(ConnectRequest { mnemonic: TEST_MNEMONIC.to_string(), data_dir: Some(data_dir_str), network: Network::LiquidTestnet, - })?; + }) + .await?; let prepare_response = sdk.prepare_receive_payment(&PrepareReceiveRequest { payer_amount_sat: 1_000, diff --git a/packages/dart/lib/src/bindings.dart b/packages/dart/lib/src/bindings.dart index 3d5a263..7bce9b0 100644 --- a/packages/dart/lib/src/bindings.dart +++ b/packages/dart/lib/src/bindings.dart @@ -8,6 +8,8 @@ import 'frb_generated.dart'; import 'model.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +// The type `BindingEventListener` is not used by any `pub` functions, thus it is ignored. + Future connect({required ConnectRequest req, dynamic hint}) => RustLib.instance.api.crateBindingsConnect(req: req, hint: hint); @@ -25,6 +27,9 @@ class BindingLiquidSdk extends RustOpaque { rustArcDecrementStrongCountPtr: RustLib.instance.api.rust_arc_decrement_strong_count_BindingLiquidSdkPtr, ); + Stream addEventListener({dynamic hint}) => + RustLib.instance.api.crateBindingsBindingLiquidSdkAddEventListener(that: this, hint: hint); + Future backup({dynamic hint}) => RustLib.instance.api.crateBindingsBindingLiquidSdkBackup(that: this, hint: hint); diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index 723a04f..e8eff31 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.35'; @override - int get rustContentHash => 1284301568; + int get rustContentHash => 692273053; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( stem: 'breez_liquid_sdk', @@ -63,6 +63,9 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { + Stream crateBindingsBindingLiquidSdkAddEventListener( + {required BindingLiquidSdk that, dynamic hint}); + Future crateBindingsBindingLiquidSdkBackup({required BindingLiquidSdk that, dynamic hint}); Future crateBindingsBindingLiquidSdkEmptyWalletCache({required BindingLiquidSdk that, dynamic hint}); @@ -107,6 +110,35 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required super.portManager, }); + @override + Stream crateBindingsBindingLiquidSdkAddEventListener( + {required BindingLiquidSdk that, dynamic hint}) { + final listener = RustStreamSink(); + unawaited(handler.executeNormal(NormalTask( + callFfi: (port_) { + var arg0 = + cst_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( + that); + var arg1 = cst_encode_StreamSink_liquid_sdk_event_Dco(listener); + return wire.wire__crate__bindings__BindingLiquidSdk_add_event_listener(port_, arg0, arg1); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_String, + decodeErrorData: dco_decode_liquid_sdk_error, + ), + constMeta: kCrateBindingsBindingLiquidSdkAddEventListenerConstMeta, + argValues: [that, listener], + apiImpl: this, + hint: hint, + ))); + return listener.stream; + } + + TaskConstMeta get kCrateBindingsBindingLiquidSdkAddEventListenerConstMeta => const TaskConstMeta( + debugName: "BindingLiquidSdk_add_event_listener", + argNames: ["that", "listener"], + ); + @override Future crateBindingsBindingLiquidSdkBackup({required BindingLiquidSdk that, dynamic hint}) { return handler.executeNormal(NormalTask( @@ -423,6 +455,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return BindingLiquidSdk.dcoDecode(raw as List); } + @protected + RustStreamSink dco_decode_StreamSink_liquid_sdk_event_Dco(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(); + } + @protected String dco_decode_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -447,6 +485,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_get_info_request(raw); } + @protected + Payment dco_decode_box_autoadd_payment(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_payment(raw); + } + @protected PrepareReceiveRequest dco_decode_box_autoadd_prepare_receive_request(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -537,6 +581,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + LiquidSdkEvent dco_decode_liquid_sdk_event(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return LiquidSdkEvent_PaymentFailed( + details: dco_decode_box_autoadd_payment(raw[1]), + ); + case 1: + return LiquidSdkEvent_PaymentPending( + details: dco_decode_box_autoadd_payment(raw[1]), + ); + case 2: + return LiquidSdkEvent_PaymentRefunded( + details: dco_decode_box_autoadd_payment(raw[1]), + ); + case 3: + return LiquidSdkEvent_PaymentRefundPending( + details: dco_decode_box_autoadd_payment(raw[1]), + ); + case 4: + return LiquidSdkEvent_PaymentSucceed( + details: dco_decode_box_autoadd_payment(raw[1]), + ); + case 5: + return LiquidSdkEvent_PaymentWaitingConfirmation( + details: dco_decode_box_autoadd_payment(raw[1]), + ); + case 6: + return LiquidSdkEvent_Synced(); + default: + throw Exception("unreachable"); + } + } + @protected List dco_decode_list_payment(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -768,6 +847,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return BindingLiquidSdk.sseDecode(sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); } + @protected + RustStreamSink sse_decode_StreamSink_liquid_sdk_event_Dco(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + throw UnimplementedError('Unreachable ()'); + } + @protected String sse_decode_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -793,6 +878,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_get_info_request(deserializer)); } + @protected + Payment sse_decode_box_autoadd_payment(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_payment(deserializer)); + } + @protected PrepareReceiveRequest sse_decode_box_autoadd_prepare_receive_request(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -879,6 +970,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + LiquidSdkEvent sse_decode_liquid_sdk_event(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + var var_details = sse_decode_box_autoadd_payment(deserializer); + return LiquidSdkEvent_PaymentFailed(details: var_details); + case 1: + var var_details = sse_decode_box_autoadd_payment(deserializer); + return LiquidSdkEvent_PaymentPending(details: var_details); + case 2: + var var_details = sse_decode_box_autoadd_payment(deserializer); + return LiquidSdkEvent_PaymentRefunded(details: var_details); + case 3: + var var_details = sse_decode_box_autoadd_payment(deserializer); + return LiquidSdkEvent_PaymentRefundPending(details: var_details); + case 4: + var var_details = sse_decode_box_autoadd_payment(deserializer); + return LiquidSdkEvent_PaymentSucceed(details: var_details); + case 5: + var var_details = sse_decode_box_autoadd_payment(deserializer); + return LiquidSdkEvent_PaymentWaitingConfirmation(details: var_details); + case 6: + return LiquidSdkEvent_Synced(); + default: + throw UnimplementedError(''); + } + } + @protected List sse_decode_list_payment(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1186,6 +1308,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_usize(self.sseEncode(move: null), serializer); } + @protected + void sse_encode_StreamSink_liquid_sdk_event_Dco( + RustStreamSink self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String( + self.setupAndSerialize( + codec: DcoCodec(decodeSuccessData: dco_decode_liquid_sdk_event, decodeErrorData: null)), + serializer); + } + @protected void sse_encode_String(String self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1210,6 +1342,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_get_info_request(self, serializer); } + @protected + void sse_encode_box_autoadd_payment(Payment self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_payment(self, serializer); + } + @protected void sse_encode_box_autoadd_prepare_receive_request(PrepareReceiveRequest self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1286,6 +1424,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_liquid_sdk_event(LiquidSdkEvent self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case LiquidSdkEvent_PaymentFailed(details: final details): + sse_encode_i_32(0, serializer); + sse_encode_box_autoadd_payment(details, serializer); + case LiquidSdkEvent_PaymentPending(details: final details): + sse_encode_i_32(1, serializer); + sse_encode_box_autoadd_payment(details, serializer); + case LiquidSdkEvent_PaymentRefunded(details: final details): + sse_encode_i_32(2, serializer); + sse_encode_box_autoadd_payment(details, serializer); + case LiquidSdkEvent_PaymentRefundPending(details: final details): + sse_encode_i_32(3, serializer); + sse_encode_box_autoadd_payment(details, serializer); + case LiquidSdkEvent_PaymentSucceed(details: final details): + sse_encode_i_32(4, serializer); + sse_encode_box_autoadd_payment(details, serializer); + case LiquidSdkEvent_PaymentWaitingConfirmation(details: final details): + sse_encode_i_32(5, serializer); + sse_encode_box_autoadd_payment(details, serializer); + case LiquidSdkEvent_Synced(): + sse_encode_i_32(6, serializer); + } + } + @protected void sse_encode_list_payment(List self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index b2e1c95..172f695 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -37,6 +37,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { BindingLiquidSdk dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( dynamic raw); + @protected + RustStreamSink dco_decode_StreamSink_liquid_sdk_event_Dco(dynamic raw); + @protected String dco_decode_String(dynamic raw); @@ -49,6 +52,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected GetInfoRequest dco_decode_box_autoadd_get_info_request(dynamic raw); + @protected + Payment dco_decode_box_autoadd_payment(dynamic raw); + @protected PrepareReceiveRequest dco_decode_box_autoadd_prepare_receive_request(dynamic raw); @@ -82,6 +88,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected LiquidSdkError dco_decode_liquid_sdk_error(dynamic raw); + @protected + LiquidSdkEvent dco_decode_liquid_sdk_event(dynamic raw); + @protected List dco_decode_list_payment(dynamic raw); @@ -159,6 +168,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { BindingLiquidSdk sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( SseDeserializer deserializer); + @protected + RustStreamSink sse_decode_StreamSink_liquid_sdk_event_Dco(SseDeserializer deserializer); + @protected String sse_decode_String(SseDeserializer deserializer); @@ -171,6 +183,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected GetInfoRequest sse_decode_box_autoadd_get_info_request(SseDeserializer deserializer); + @protected + Payment sse_decode_box_autoadd_payment(SseDeserializer deserializer); + @protected PrepareReceiveRequest sse_decode_box_autoadd_prepare_receive_request(SseDeserializer deserializer); @@ -204,6 +219,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected LiquidSdkError sse_decode_liquid_sdk_error(SseDeserializer deserializer); + @protected + LiquidSdkEvent sse_decode_liquid_sdk_event(SseDeserializer deserializer); + @protected List sse_decode_list_payment(SseDeserializer deserializer); @@ -267,6 +285,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_usize(SseDeserializer deserializer); + @protected + ffi.Pointer cst_encode_StreamSink_liquid_sdk_event_Dco( + RustStreamSink raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return cst_encode_String(raw.setupAndSerialize( + codec: DcoCodec(decodeSuccessData: dco_decode_liquid_sdk_event, decodeErrorData: null))); + } + @protected ffi.Pointer cst_encode_String(String raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -289,6 +315,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return ptr; } + @protected + ffi.Pointer cst_encode_box_autoadd_payment(Payment raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + final ptr = wire.cst_new_box_autoadd_payment(); + cst_api_fill_to_wire_payment(raw, ptr.ref); + return ptr; + } + @protected ffi.Pointer cst_encode_box_autoadd_prepare_receive_request( PrepareReceiveRequest raw) { @@ -387,6 +421,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { cst_api_fill_to_wire_get_info_request(apiObj, wireObj.ref); } + @protected + void cst_api_fill_to_wire_box_autoadd_payment(Payment apiObj, ffi.Pointer wireObj) { + cst_api_fill_to_wire_payment(apiObj, wireObj.ref); + } + @protected void cst_api_fill_to_wire_box_autoadd_prepare_receive_request( PrepareReceiveRequest apiObj, ffi.Pointer wireObj) { @@ -447,6 +486,50 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { } } + @protected + void cst_api_fill_to_wire_liquid_sdk_event(LiquidSdkEvent apiObj, wire_cst_liquid_sdk_event wireObj) { + if (apiObj is LiquidSdkEvent_PaymentFailed) { + var pre_details = cst_encode_box_autoadd_payment(apiObj.details); + wireObj.tag = 0; + wireObj.kind.PaymentFailed.details = pre_details; + return; + } + if (apiObj is LiquidSdkEvent_PaymentPending) { + var pre_details = cst_encode_box_autoadd_payment(apiObj.details); + wireObj.tag = 1; + wireObj.kind.PaymentPending.details = pre_details; + return; + } + if (apiObj is LiquidSdkEvent_PaymentRefunded) { + var pre_details = cst_encode_box_autoadd_payment(apiObj.details); + wireObj.tag = 2; + wireObj.kind.PaymentRefunded.details = pre_details; + return; + } + if (apiObj is LiquidSdkEvent_PaymentRefundPending) { + var pre_details = cst_encode_box_autoadd_payment(apiObj.details); + wireObj.tag = 3; + wireObj.kind.PaymentRefundPending.details = pre_details; + return; + } + if (apiObj is LiquidSdkEvent_PaymentSucceed) { + var pre_details = cst_encode_box_autoadd_payment(apiObj.details); + wireObj.tag = 4; + wireObj.kind.PaymentSucceed.details = pre_details; + return; + } + if (apiObj is LiquidSdkEvent_PaymentWaitingConfirmation) { + var pre_details = cst_encode_box_autoadd_payment(apiObj.details); + wireObj.tag = 5; + wireObj.kind.PaymentWaitingConfirmation.details = pre_details; + return; + } + if (apiObj is LiquidSdkEvent_Synced) { + wireObj.tag = 6; + return; + } + } + @protected void cst_api_fill_to_wire_payment(Payment apiObj, wire_cst_payment wireObj) { wireObj.tx_id = cst_encode_String(apiObj.txId); @@ -622,6 +705,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( BindingLiquidSdk self, SseSerializer serializer); + @protected + void sse_encode_StreamSink_liquid_sdk_event_Dco( + RustStreamSink self, SseSerializer serializer); + @protected void sse_encode_String(String self, SseSerializer serializer); @@ -634,6 +721,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_get_info_request(GetInfoRequest self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_payment(Payment self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_prepare_receive_request(PrepareReceiveRequest self, SseSerializer serializer); @@ -667,6 +757,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_liquid_sdk_error(LiquidSdkError self, SseSerializer serializer); + @protected + void sse_encode_liquid_sdk_event(LiquidSdkEvent self, SseSerializer serializer); + @protected void sse_encode_list_payment(List self, SseSerializer serializer); @@ -766,6 +859,26 @@ class RustLibWire implements BaseWire { late final _store_dart_post_cobject = _store_dart_post_cobjectPtr.asFunction(); + void wire__crate__bindings__BindingLiquidSdk_add_event_listener( + int port_, + int that, + ffi.Pointer listener, + ) { + return _wire__crate__bindings__BindingLiquidSdk_add_event_listener( + port_, + that, + listener, + ); + } + + late final _wire__crate__bindings__BindingLiquidSdk_add_event_listenerPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.UintPtr, ffi.Pointer)>>( + 'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listener'); + late final _wire__crate__bindings__BindingLiquidSdk_add_event_listener = + _wire__crate__bindings__BindingLiquidSdk_add_event_listenerPtr + .asFunction)>(); + void wire__crate__bindings__BindingLiquidSdk_backup( int port_, int that, @@ -1018,6 +1131,16 @@ class RustLibWire implements BaseWire { late final _cst_new_box_autoadd_get_info_request = _cst_new_box_autoadd_get_info_requestPtr .asFunction Function()>(); + ffi.Pointer cst_new_box_autoadd_payment() { + return _cst_new_box_autoadd_payment(); + } + + late final _cst_new_box_autoadd_paymentPtr = + _lookup Function()>>( + 'frbgen_breez_liquid_cst_new_box_autoadd_payment'); + late final _cst_new_box_autoadd_payment = + _cst_new_box_autoadd_paymentPtr.asFunction Function()>(); + ffi.Pointer cst_new_box_autoadd_prepare_receive_request() { return _cst_new_box_autoadd_prepare_receive_request(); } @@ -1127,6 +1250,13 @@ typedef DartDartPostCObjectFnTypeFunction = bool Function( typedef DartPort = ffi.Int64; typedef DartDartPort = int; +final class wire_cst_list_prim_u_8_strict extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_get_info_request extends ffi.Struct { @ffi.Bool() external bool with_scan; @@ -1137,13 +1267,6 @@ final class wire_cst_prepare_receive_request extends ffi.Struct { external int payer_amount_sat; } -final class wire_cst_list_prim_u_8_strict extends ffi.Struct { - external ffi.Pointer ptr; - - @ffi.Int32() - external int len; -} - final class wire_cst_prepare_send_request extends ffi.Struct { external ffi.Pointer invoice; } @@ -1233,6 +1356,51 @@ final class wire_cst_liquid_sdk_error extends ffi.Struct { external LiquidSdkErrorKind kind; } +final class wire_cst_LiquidSdkEvent_PaymentFailed extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentPending extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentRefunded extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentRefundPending extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentSucceed extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation extends ffi.Struct { + external ffi.Pointer details; +} + +final class LiquidSdkEventKind extends ffi.Union { + external wire_cst_LiquidSdkEvent_PaymentFailed PaymentFailed; + + external wire_cst_LiquidSdkEvent_PaymentPending PaymentPending; + + external wire_cst_LiquidSdkEvent_PaymentRefunded PaymentRefunded; + + external wire_cst_LiquidSdkEvent_PaymentRefundPending PaymentRefundPending; + + external wire_cst_LiquidSdkEvent_PaymentSucceed PaymentSucceed; + + external wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation PaymentWaitingConfirmation; +} + +final class wire_cst_liquid_sdk_event extends ffi.Struct { + @ffi.Int32() + external int tag; + + external LiquidSdkEventKind kind; +} + final class wire_cst_PaymentError_Generic extends ffi.Struct { external ffi.Pointer err; } diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index f5e9bb4..a148c68 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -5,6 +5,8 @@ import 'frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:freezed_annotation/freezed_annotation.dart' hide protected; +part 'model.freezed.dart'; class ConnectRequest { final String mnemonic; @@ -79,6 +81,31 @@ class GetInfoResponse { pubkey == other.pubkey; } +@freezed +sealed class LiquidSdkEvent with _$LiquidSdkEvent { + const LiquidSdkEvent._(); + + const factory LiquidSdkEvent.paymentFailed({ + required Payment details, + }) = LiquidSdkEvent_PaymentFailed; + const factory LiquidSdkEvent.paymentPending({ + required Payment details, + }) = LiquidSdkEvent_PaymentPending; + const factory LiquidSdkEvent.paymentRefunded({ + required Payment details, + }) = LiquidSdkEvent_PaymentRefunded; + const factory LiquidSdkEvent.paymentRefundPending({ + required Payment details, + }) = LiquidSdkEvent_PaymentRefundPending; + const factory LiquidSdkEvent.paymentSucceed({ + required Payment details, + }) = LiquidSdkEvent_PaymentSucceed; + const factory LiquidSdkEvent.paymentWaitingConfirmation({ + required Payment details, + }) = LiquidSdkEvent_PaymentWaitingConfirmation; + const factory LiquidSdkEvent.synced() = LiquidSdkEvent_Synced; +} + enum Network { liquid, liquidTestnet, diff --git a/packages/dart/lib/src/model.freezed.dart b/packages/dart/lib/src/model.freezed.dart new file mode 100644 index 0000000..1c16fa6 --- /dev/null +++ b/packages/dart/lib/src/model.freezed.dart @@ -0,0 +1,522 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LiquidSdkEvent {} + +/// @nodoc +abstract class $LiquidSdkEventCopyWith<$Res> { + factory $LiquidSdkEventCopyWith(LiquidSdkEvent value, $Res Function(LiquidSdkEvent) then) = + _$LiquidSdkEventCopyWithImpl<$Res, LiquidSdkEvent>; +} + +/// @nodoc +class _$LiquidSdkEventCopyWithImpl<$Res, $Val extends LiquidSdkEvent> + implements $LiquidSdkEventCopyWith<$Res> { + _$LiquidSdkEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$LiquidSdkEvent_PaymentFailedImplCopyWith<$Res> { + factory _$$LiquidSdkEvent_PaymentFailedImplCopyWith( + _$LiquidSdkEvent_PaymentFailedImpl value, $Res Function(_$LiquidSdkEvent_PaymentFailedImpl) then) = + __$$LiquidSdkEvent_PaymentFailedImplCopyWithImpl<$Res>; + @useResult + $Res call({Payment details}); +} + +/// @nodoc +class __$$LiquidSdkEvent_PaymentFailedImplCopyWithImpl<$Res> + extends _$LiquidSdkEventCopyWithImpl<$Res, _$LiquidSdkEvent_PaymentFailedImpl> + implements _$$LiquidSdkEvent_PaymentFailedImplCopyWith<$Res> { + __$$LiquidSdkEvent_PaymentFailedImplCopyWithImpl( + _$LiquidSdkEvent_PaymentFailedImpl _value, $Res Function(_$LiquidSdkEvent_PaymentFailedImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? details = null, + }) { + return _then(_$LiquidSdkEvent_PaymentFailedImpl( + details: null == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as Payment, + )); + } +} + +/// @nodoc + +class _$LiquidSdkEvent_PaymentFailedImpl extends LiquidSdkEvent_PaymentFailed { + const _$LiquidSdkEvent_PaymentFailedImpl({required this.details}) : super._(); + + @override + final Payment details; + + @override + String toString() { + return 'LiquidSdkEvent.paymentFailed(details: $details)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LiquidSdkEvent_PaymentFailedImpl && + (identical(other.details, details) || other.details == details)); + } + + @override + int get hashCode => Object.hash(runtimeType, details); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LiquidSdkEvent_PaymentFailedImplCopyWith<_$LiquidSdkEvent_PaymentFailedImpl> get copyWith => + __$$LiquidSdkEvent_PaymentFailedImplCopyWithImpl<_$LiquidSdkEvent_PaymentFailedImpl>(this, _$identity); +} + +abstract class LiquidSdkEvent_PaymentFailed extends LiquidSdkEvent { + const factory LiquidSdkEvent_PaymentFailed({required final Payment details}) = + _$LiquidSdkEvent_PaymentFailedImpl; + const LiquidSdkEvent_PaymentFailed._() : super._(); + + Payment get details; + @JsonKey(ignore: true) + _$$LiquidSdkEvent_PaymentFailedImplCopyWith<_$LiquidSdkEvent_PaymentFailedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LiquidSdkEvent_PaymentPendingImplCopyWith<$Res> { + factory _$$LiquidSdkEvent_PaymentPendingImplCopyWith(_$LiquidSdkEvent_PaymentPendingImpl value, + $Res Function(_$LiquidSdkEvent_PaymentPendingImpl) then) = + __$$LiquidSdkEvent_PaymentPendingImplCopyWithImpl<$Res>; + @useResult + $Res call({Payment details}); +} + +/// @nodoc +class __$$LiquidSdkEvent_PaymentPendingImplCopyWithImpl<$Res> + extends _$LiquidSdkEventCopyWithImpl<$Res, _$LiquidSdkEvent_PaymentPendingImpl> + implements _$$LiquidSdkEvent_PaymentPendingImplCopyWith<$Res> { + __$$LiquidSdkEvent_PaymentPendingImplCopyWithImpl( + _$LiquidSdkEvent_PaymentPendingImpl _value, $Res Function(_$LiquidSdkEvent_PaymentPendingImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? details = null, + }) { + return _then(_$LiquidSdkEvent_PaymentPendingImpl( + details: null == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as Payment, + )); + } +} + +/// @nodoc + +class _$LiquidSdkEvent_PaymentPendingImpl extends LiquidSdkEvent_PaymentPending { + const _$LiquidSdkEvent_PaymentPendingImpl({required this.details}) : super._(); + + @override + final Payment details; + + @override + String toString() { + return 'LiquidSdkEvent.paymentPending(details: $details)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LiquidSdkEvent_PaymentPendingImpl && + (identical(other.details, details) || other.details == details)); + } + + @override + int get hashCode => Object.hash(runtimeType, details); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LiquidSdkEvent_PaymentPendingImplCopyWith<_$LiquidSdkEvent_PaymentPendingImpl> get copyWith => + __$$LiquidSdkEvent_PaymentPendingImplCopyWithImpl<_$LiquidSdkEvent_PaymentPendingImpl>( + this, _$identity); +} + +abstract class LiquidSdkEvent_PaymentPending extends LiquidSdkEvent { + const factory LiquidSdkEvent_PaymentPending({required final Payment details}) = + _$LiquidSdkEvent_PaymentPendingImpl; + const LiquidSdkEvent_PaymentPending._() : super._(); + + Payment get details; + @JsonKey(ignore: true) + _$$LiquidSdkEvent_PaymentPendingImplCopyWith<_$LiquidSdkEvent_PaymentPendingImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LiquidSdkEvent_PaymentRefundedImplCopyWith<$Res> { + factory _$$LiquidSdkEvent_PaymentRefundedImplCopyWith(_$LiquidSdkEvent_PaymentRefundedImpl value, + $Res Function(_$LiquidSdkEvent_PaymentRefundedImpl) then) = + __$$LiquidSdkEvent_PaymentRefundedImplCopyWithImpl<$Res>; + @useResult + $Res call({Payment details}); +} + +/// @nodoc +class __$$LiquidSdkEvent_PaymentRefundedImplCopyWithImpl<$Res> + extends _$LiquidSdkEventCopyWithImpl<$Res, _$LiquidSdkEvent_PaymentRefundedImpl> + implements _$$LiquidSdkEvent_PaymentRefundedImplCopyWith<$Res> { + __$$LiquidSdkEvent_PaymentRefundedImplCopyWithImpl( + _$LiquidSdkEvent_PaymentRefundedImpl _value, $Res Function(_$LiquidSdkEvent_PaymentRefundedImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? details = null, + }) { + return _then(_$LiquidSdkEvent_PaymentRefundedImpl( + details: null == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as Payment, + )); + } +} + +/// @nodoc + +class _$LiquidSdkEvent_PaymentRefundedImpl extends LiquidSdkEvent_PaymentRefunded { + const _$LiquidSdkEvent_PaymentRefundedImpl({required this.details}) : super._(); + + @override + final Payment details; + + @override + String toString() { + return 'LiquidSdkEvent.paymentRefunded(details: $details)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LiquidSdkEvent_PaymentRefundedImpl && + (identical(other.details, details) || other.details == details)); + } + + @override + int get hashCode => Object.hash(runtimeType, details); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LiquidSdkEvent_PaymentRefundedImplCopyWith<_$LiquidSdkEvent_PaymentRefundedImpl> get copyWith => + __$$LiquidSdkEvent_PaymentRefundedImplCopyWithImpl<_$LiquidSdkEvent_PaymentRefundedImpl>( + this, _$identity); +} + +abstract class LiquidSdkEvent_PaymentRefunded extends LiquidSdkEvent { + const factory LiquidSdkEvent_PaymentRefunded({required final Payment details}) = + _$LiquidSdkEvent_PaymentRefundedImpl; + const LiquidSdkEvent_PaymentRefunded._() : super._(); + + Payment get details; + @JsonKey(ignore: true) + _$$LiquidSdkEvent_PaymentRefundedImplCopyWith<_$LiquidSdkEvent_PaymentRefundedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LiquidSdkEvent_PaymentRefundPendingImplCopyWith<$Res> { + factory _$$LiquidSdkEvent_PaymentRefundPendingImplCopyWith(_$LiquidSdkEvent_PaymentRefundPendingImpl value, + $Res Function(_$LiquidSdkEvent_PaymentRefundPendingImpl) then) = + __$$LiquidSdkEvent_PaymentRefundPendingImplCopyWithImpl<$Res>; + @useResult + $Res call({Payment details}); +} + +/// @nodoc +class __$$LiquidSdkEvent_PaymentRefundPendingImplCopyWithImpl<$Res> + extends _$LiquidSdkEventCopyWithImpl<$Res, _$LiquidSdkEvent_PaymentRefundPendingImpl> + implements _$$LiquidSdkEvent_PaymentRefundPendingImplCopyWith<$Res> { + __$$LiquidSdkEvent_PaymentRefundPendingImplCopyWithImpl(_$LiquidSdkEvent_PaymentRefundPendingImpl _value, + $Res Function(_$LiquidSdkEvent_PaymentRefundPendingImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? details = null, + }) { + return _then(_$LiquidSdkEvent_PaymentRefundPendingImpl( + details: null == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as Payment, + )); + } +} + +/// @nodoc + +class _$LiquidSdkEvent_PaymentRefundPendingImpl extends LiquidSdkEvent_PaymentRefundPending { + const _$LiquidSdkEvent_PaymentRefundPendingImpl({required this.details}) : super._(); + + @override + final Payment details; + + @override + String toString() { + return 'LiquidSdkEvent.paymentRefundPending(details: $details)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LiquidSdkEvent_PaymentRefundPendingImpl && + (identical(other.details, details) || other.details == details)); + } + + @override + int get hashCode => Object.hash(runtimeType, details); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LiquidSdkEvent_PaymentRefundPendingImplCopyWith<_$LiquidSdkEvent_PaymentRefundPendingImpl> + get copyWith => + __$$LiquidSdkEvent_PaymentRefundPendingImplCopyWithImpl<_$LiquidSdkEvent_PaymentRefundPendingImpl>( + this, _$identity); +} + +abstract class LiquidSdkEvent_PaymentRefundPending extends LiquidSdkEvent { + const factory LiquidSdkEvent_PaymentRefundPending({required final Payment details}) = + _$LiquidSdkEvent_PaymentRefundPendingImpl; + const LiquidSdkEvent_PaymentRefundPending._() : super._(); + + Payment get details; + @JsonKey(ignore: true) + _$$LiquidSdkEvent_PaymentRefundPendingImplCopyWith<_$LiquidSdkEvent_PaymentRefundPendingImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LiquidSdkEvent_PaymentSucceedImplCopyWith<$Res> { + factory _$$LiquidSdkEvent_PaymentSucceedImplCopyWith(_$LiquidSdkEvent_PaymentSucceedImpl value, + $Res Function(_$LiquidSdkEvent_PaymentSucceedImpl) then) = + __$$LiquidSdkEvent_PaymentSucceedImplCopyWithImpl<$Res>; + @useResult + $Res call({Payment details}); +} + +/// @nodoc +class __$$LiquidSdkEvent_PaymentSucceedImplCopyWithImpl<$Res> + extends _$LiquidSdkEventCopyWithImpl<$Res, _$LiquidSdkEvent_PaymentSucceedImpl> + implements _$$LiquidSdkEvent_PaymentSucceedImplCopyWith<$Res> { + __$$LiquidSdkEvent_PaymentSucceedImplCopyWithImpl( + _$LiquidSdkEvent_PaymentSucceedImpl _value, $Res Function(_$LiquidSdkEvent_PaymentSucceedImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? details = null, + }) { + return _then(_$LiquidSdkEvent_PaymentSucceedImpl( + details: null == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as Payment, + )); + } +} + +/// @nodoc + +class _$LiquidSdkEvent_PaymentSucceedImpl extends LiquidSdkEvent_PaymentSucceed { + const _$LiquidSdkEvent_PaymentSucceedImpl({required this.details}) : super._(); + + @override + final Payment details; + + @override + String toString() { + return 'LiquidSdkEvent.paymentSucceed(details: $details)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LiquidSdkEvent_PaymentSucceedImpl && + (identical(other.details, details) || other.details == details)); + } + + @override + int get hashCode => Object.hash(runtimeType, details); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LiquidSdkEvent_PaymentSucceedImplCopyWith<_$LiquidSdkEvent_PaymentSucceedImpl> get copyWith => + __$$LiquidSdkEvent_PaymentSucceedImplCopyWithImpl<_$LiquidSdkEvent_PaymentSucceedImpl>( + this, _$identity); +} + +abstract class LiquidSdkEvent_PaymentSucceed extends LiquidSdkEvent { + const factory LiquidSdkEvent_PaymentSucceed({required final Payment details}) = + _$LiquidSdkEvent_PaymentSucceedImpl; + const LiquidSdkEvent_PaymentSucceed._() : super._(); + + Payment get details; + @JsonKey(ignore: true) + _$$LiquidSdkEvent_PaymentSucceedImplCopyWith<_$LiquidSdkEvent_PaymentSucceedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWith<$Res> { + factory _$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWith( + _$LiquidSdkEvent_PaymentWaitingConfirmationImpl value, + $Res Function(_$LiquidSdkEvent_PaymentWaitingConfirmationImpl) then) = + __$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWithImpl<$Res>; + @useResult + $Res call({Payment details}); +} + +/// @nodoc +class __$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWithImpl<$Res> + extends _$LiquidSdkEventCopyWithImpl<$Res, _$LiquidSdkEvent_PaymentWaitingConfirmationImpl> + implements _$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWith<$Res> { + __$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWithImpl( + _$LiquidSdkEvent_PaymentWaitingConfirmationImpl _value, + $Res Function(_$LiquidSdkEvent_PaymentWaitingConfirmationImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? details = null, + }) { + return _then(_$LiquidSdkEvent_PaymentWaitingConfirmationImpl( + details: null == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as Payment, + )); + } +} + +/// @nodoc + +class _$LiquidSdkEvent_PaymentWaitingConfirmationImpl extends LiquidSdkEvent_PaymentWaitingConfirmation { + const _$LiquidSdkEvent_PaymentWaitingConfirmationImpl({required this.details}) : super._(); + + @override + final Payment details; + + @override + String toString() { + return 'LiquidSdkEvent.paymentWaitingConfirmation(details: $details)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LiquidSdkEvent_PaymentWaitingConfirmationImpl && + (identical(other.details, details) || other.details == details)); + } + + @override + int get hashCode => Object.hash(runtimeType, details); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWith<_$LiquidSdkEvent_PaymentWaitingConfirmationImpl> + get copyWith => __$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWithImpl< + _$LiquidSdkEvent_PaymentWaitingConfirmationImpl>(this, _$identity); +} + +abstract class LiquidSdkEvent_PaymentWaitingConfirmation extends LiquidSdkEvent { + const factory LiquidSdkEvent_PaymentWaitingConfirmation({required final Payment details}) = + _$LiquidSdkEvent_PaymentWaitingConfirmationImpl; + const LiquidSdkEvent_PaymentWaitingConfirmation._() : super._(); + + Payment get details; + @JsonKey(ignore: true) + _$$LiquidSdkEvent_PaymentWaitingConfirmationImplCopyWith<_$LiquidSdkEvent_PaymentWaitingConfirmationImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LiquidSdkEvent_SyncedImplCopyWith<$Res> { + factory _$$LiquidSdkEvent_SyncedImplCopyWith( + _$LiquidSdkEvent_SyncedImpl value, $Res Function(_$LiquidSdkEvent_SyncedImpl) then) = + __$$LiquidSdkEvent_SyncedImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LiquidSdkEvent_SyncedImplCopyWithImpl<$Res> + extends _$LiquidSdkEventCopyWithImpl<$Res, _$LiquidSdkEvent_SyncedImpl> + implements _$$LiquidSdkEvent_SyncedImplCopyWith<$Res> { + __$$LiquidSdkEvent_SyncedImplCopyWithImpl( + _$LiquidSdkEvent_SyncedImpl _value, $Res Function(_$LiquidSdkEvent_SyncedImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$LiquidSdkEvent_SyncedImpl extends LiquidSdkEvent_Synced { + const _$LiquidSdkEvent_SyncedImpl() : super._(); + + @override + String toString() { + return 'LiquidSdkEvent.synced()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LiquidSdkEvent_SyncedImpl); + } + + @override + int get hashCode => runtimeType.hashCode; +} + +abstract class LiquidSdkEvent_Synced extends LiquidSdkEvent { + const factory LiquidSdkEvent_Synced() = _$LiquidSdkEvent_SyncedImpl; + const LiquidSdkEvent_Synced._() : super._(); +} diff --git a/packages/flutter/example/macos/Podfile.lock b/packages/flutter/example/macos/Podfile.lock index 96d3e00..9f50a52 100644 --- a/packages/flutter/example/macos/Podfile.lock +++ b/packages/flutter/example/macos/Podfile.lock @@ -21,7 +21,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: flutter_breez_liquid: 90494dd8df26d6258f0d2a90663204ee6257ede2 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index 4ddb85d..801fd22 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -37,6 +37,26 @@ class FlutterBreezLiquidBindings { late final _store_dart_post_cobject = _store_dart_post_cobjectPtr.asFunction(); + void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listener( + int port_, + int that, + ffi.Pointer listener, + ) { + return _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listener( + port_, + that, + listener, + ); + } + + late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listenerPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.UintPtr, ffi.Pointer)>>( + 'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listener'); + late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listener = + _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_add_event_listenerPtr + .asFunction)>(); + void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backup( int port_, int that, @@ -297,6 +317,17 @@ class FlutterBreezLiquidBindings { _frbgen_breez_liquid_cst_new_box_autoadd_get_info_requestPtr .asFunction Function()>(); + ffi.Pointer frbgen_breez_liquid_cst_new_box_autoadd_payment() { + return _frbgen_breez_liquid_cst_new_box_autoadd_payment(); + } + + late final _frbgen_breez_liquid_cst_new_box_autoadd_paymentPtr = + _lookup Function()>>( + 'frbgen_breez_liquid_cst_new_box_autoadd_payment'); + late final _frbgen_breez_liquid_cst_new_box_autoadd_payment = + _frbgen_breez_liquid_cst_new_box_autoadd_paymentPtr + .asFunction Function()>(); + ffi.Pointer frbgen_breez_liquid_cst_new_box_autoadd_prepare_receive_request() { return _frbgen_breez_liquid_cst_new_box_autoadd_prepare_receive_request(); @@ -426,6 +457,13 @@ typedef DartDartPort = int; final class _Dart_Handle extends ffi.Opaque {} +final class wire_cst_list_prim_u_8_strict extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_get_info_request extends ffi.Struct { @ffi.Bool() external bool with_scan; @@ -436,13 +474,6 @@ final class wire_cst_prepare_receive_request extends ffi.Struct { external int payer_amount_sat; } -final class wire_cst_list_prim_u_8_strict extends ffi.Struct { - external ffi.Pointer ptr; - - @ffi.Int32() - external int len; -} - final class wire_cst_prepare_send_request extends ffi.Struct { external ffi.Pointer invoice; } @@ -532,6 +563,51 @@ final class wire_cst_liquid_sdk_error extends ffi.Struct { external LiquidSdkErrorKind kind; } +final class wire_cst_LiquidSdkEvent_PaymentFailed extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentPending extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentRefunded extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentRefundPending extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentSucceed extends ffi.Struct { + external ffi.Pointer details; +} + +final class wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation extends ffi.Struct { + external ffi.Pointer details; +} + +final class LiquidSdkEventKind extends ffi.Union { + external wire_cst_LiquidSdkEvent_PaymentFailed PaymentFailed; + + external wire_cst_LiquidSdkEvent_PaymentPending PaymentPending; + + external wire_cst_LiquidSdkEvent_PaymentRefunded PaymentRefunded; + + external wire_cst_LiquidSdkEvent_PaymentRefundPending PaymentRefundPending; + + external wire_cst_LiquidSdkEvent_PaymentSucceed PaymentSucceed; + + external wire_cst_LiquidSdkEvent_PaymentWaitingConfirmation PaymentWaitingConfirmation; +} + +final class wire_cst_liquid_sdk_event extends ffi.Struct { + @ffi.Int32() + external int tag; + + external LiquidSdkEventKind kind; +} + final class wire_cst_PaymentError_Generic extends ffi.Struct { external ffi.Pointer err; } diff --git a/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKEventListener.kt b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKEventListener.kt new file mode 100644 index 0000000..31fea0e --- /dev/null +++ b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKEventListener.kt @@ -0,0 +1,19 @@ +package com.breezliquidsdk + +import breez_liquid_sdk.LiquidSdkEvent +import breez_liquid_sdk.EventListener +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter + +class BreezLiquidSDKEventListener(private val emitter: RCTDeviceEventEmitter) : EventListener { + private var id: String? = null + + fun setId(id: String) { + this.id = id + } + + override fun onEvent(e: LiquidSdkEvent) { + this.id?.let { + emitter.emit("event-$it", readableMapOf(e)) + } + } +} 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 0144fb6..8258cb7 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 @@ -420,6 +420,78 @@ fun asSendPaymentResponseList(arr: ReadableArray): List { return list } +fun asLiquidSdkEvent(liquidSdkEvent: ReadableMap): LiquidSdkEvent? { + val type = liquidSdkEvent.getString("type") + + if (type == "paymentFailed") { + return LiquidSdkEvent.PaymentFailed(liquidSdkEvent.getMap("details")?.let { asPayment(it) }!!) + } + if (type == "paymentPending") { + return LiquidSdkEvent.PaymentPending(liquidSdkEvent.getMap("details")?.let { asPayment(it) }!!) + } + if (type == "paymentRefunded") { + return LiquidSdkEvent.PaymentRefunded(liquidSdkEvent.getMap("details")?.let { asPayment(it) }!!) + } + if (type == "paymentRefundPending") { + return LiquidSdkEvent.PaymentRefundPending(liquidSdkEvent.getMap("details")?.let { asPayment(it) }!!) + } + if (type == "paymentSucceed") { + return LiquidSdkEvent.PaymentSucceed(liquidSdkEvent.getMap("details")?.let { asPayment(it) }!!) + } + if (type == "paymentWaitingConfirmation") { + return LiquidSdkEvent.PaymentWaitingConfirmation(liquidSdkEvent.getMap("details")?.let { asPayment(it) }!!) + } + if (type == "synced") { + return LiquidSdkEvent.Synced + } + return null +} + +fun readableMapOf(liquidSdkEvent: LiquidSdkEvent): ReadableMap? { + val map = Arguments.createMap() + when (liquidSdkEvent) { + is LiquidSdkEvent.PaymentFailed -> { + pushToMap(map, "type", "paymentFailed") + pushToMap(map, "details", readableMapOf(liquidSdkEvent.details)) + } + is LiquidSdkEvent.PaymentPending -> { + pushToMap(map, "type", "paymentPending") + pushToMap(map, "details", readableMapOf(liquidSdkEvent.details)) + } + is LiquidSdkEvent.PaymentRefunded -> { + pushToMap(map, "type", "paymentRefunded") + pushToMap(map, "details", readableMapOf(liquidSdkEvent.details)) + } + is LiquidSdkEvent.PaymentRefundPending -> { + pushToMap(map, "type", "paymentRefundPending") + pushToMap(map, "details", readableMapOf(liquidSdkEvent.details)) + } + is LiquidSdkEvent.PaymentSucceed -> { + pushToMap(map, "type", "paymentSucceed") + pushToMap(map, "details", readableMapOf(liquidSdkEvent.details)) + } + is LiquidSdkEvent.PaymentWaitingConfirmation -> { + pushToMap(map, "type", "paymentWaitingConfirmation") + pushToMap(map, "details", readableMapOf(liquidSdkEvent.details)) + } + is LiquidSdkEvent.Synced -> { + pushToMap(map, "type", "synced") + } + } + return map +} + +fun asLiquidSdkEventList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + when (value) { + is ReadableMap -> list.add(asLiquidSdkEvent(value)!!) + else -> throw LiquidSdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} + fun asNetwork(type: String): Network { return Network.valueOf(camelToUpperSnakeCase(type)) } 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 7421078..70af62f 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 @@ -2,6 +2,7 @@ package com.breezliquidsdk import breez_liquid_sdk.* import com.facebook.react.bridge.* +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -66,6 +67,37 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext } } + @ReactMethod + fun addEventListener(promise: Promise) { + executor.execute { + try { + val emitter = reactApplicationContext.getJSModule(RCTDeviceEventEmitter::class.java) + var eventListener = BreezLiquidSDKEventListener(emitter) + val res = getBindingLiquidSdk().addEventListener(eventListener) + + eventListener.setId(res) + promise.resolve(res) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + + @ReactMethod + fun removeEventListener( + id: String, + promise: Promise, + ) { + executor.execute { + try { + getBindingLiquidSdk().removeEventListener(id) + promise.resolve(readableMapOf("status" to "ok")) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + @ReactMethod fun getInfo( req: ReadableMap, diff --git a/packages/react-native/example/App.js b/packages/react-native/example/App.js index e8ed607..2487182 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 { Network, getInfo, connect } from "@breeztech/react-native-breez-liquid-sdk" +import { addEventListener, Network, getInfo, connect, removeEventListener } from "@breeztech/react-native-breez-liquid-sdk" import { generateMnemonic } from "@dreson4/react-native-quick-bip39" import { getSecureItem, setSecureItem } from "./utils/storage" @@ -33,7 +33,13 @@ const App = () => { console.log(`${title}${text && text.length > 0 ? ": " + text : ""}`) } + const eventHandler = (e) => { + addLine("event", JSON.stringify(e)) + } + React.useEffect(() => { + let listenerId = null + const asyncFn = async () => { try { let mnemonic = await getSecureItem(MNEMONIC_STORE) @@ -43,10 +49,13 @@ const App = () => { setSecureItem(MNEMONIC_STORE, mnemonic) } - await connect({mnemonic, network: Network.LIQUID_TESTNET}) + await connect({ mnemonic, network: Network.LIQUID_TESTNET }) addLine("connect", null) - let walletInfo = await getInfo({withScan: false}) + listenerId = await addEventListener(eventHandler) + addLine("addEventListener", listenerId) + + let walletInfo = await getInfo({ withScan: false }) addLine("getInfo", JSON.stringify(walletInfo)) } catch (e) { addLine("error", e.toString()) @@ -55,6 +64,12 @@ const App = () => { } asyncFn() + + return () => { + if (listenerId) { + removeEventListener(listenerId) + } + } }, []) return ( diff --git a/packages/react-native/example/ios/BreezLiquidSDKExample.xcodeproj/project.pbxproj b/packages/react-native/example/ios/BreezLiquidSDKExample.xcodeproj/project.pbxproj index 1cdc9c4..7448c85 100644 --- a/packages/react-native/example/ios/BreezLiquidSDKExample.xcodeproj/project.pbxproj +++ b/packages/react-native/example/ios/BreezLiquidSDKExample.xcodeproj/project.pbxproj @@ -383,8 +383,13 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.example.breezliquidsdk; PRODUCT_NAME = BreezLiquidSDKExample; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -485,7 +490,12 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.example.breezliquidsdk; PRODUCT_NAME = BreezLiquidSDKExample; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/packages/react-native/example/ios/Podfile.lock b/packages/react-native/example/ios/Podfile.lock index 60b2e71..45dacf6 100644 --- a/packages/react-native/example/ios/Podfile.lock +++ b/packages/react-native/example/ios/Podfile.lock @@ -552,7 +552,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 9fa78656d705f55b1220151d997e57e2a3f2cde0 - BreezLiquidSDK: 4bca3771d7dfe9c834dbe26836ffadfe8f283c76 + BreezLiquidSDK: 287a36ed15beff2b9ba61a869e1868f790b4fa20 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 9cf707e46f9bd90816b7c91b2c1c8b8a2f549527 diff --git a/packages/react-native/ios/BreezLiquidSDKEventListener.swift b/packages/react-native/ios/BreezLiquidSDKEventListener.swift new file mode 100644 index 0000000..330eea4 --- /dev/null +++ b/packages/react-native/ios/BreezLiquidSDKEventListener.swift @@ -0,0 +1,20 @@ +import Foundation +import BreezLiquidSDK + +class BreezLiquidSDKEventListener: EventListener { + private var id: String? + + func setId(id: String) { + self.id = id + RNBreezLiquidSDK.addSupportedEvent(name: "event-\(id)") + } + + func onEvent(e: LiquidSdkEvent) { + if let id = self.id { + if RNBreezLiquidSDK.hasListeners { + RNBreezLiquidSDK.emitter.sendEvent(withName: "event-\(id)", + body: BreezLiquidSDKMapper.dictionaryOf(liquidSdkEvent: e)) + } + } + } +} diff --git a/packages/react-native/ios/BreezLiquidSDKMapper.swift b/packages/react-native/ios/BreezLiquidSDKMapper.swift index ee6e8da..59d8700 100644 --- a/packages/react-native/ios/BreezLiquidSDKMapper.swift +++ b/packages/react-native/ios/BreezLiquidSDKMapper.swift @@ -460,6 +460,137 @@ enum BreezLiquidSDKMapper { return sendPaymentResponseList.map { v -> [String: Any?] in dictionaryOf(sendPaymentResponse: v) } } + static func asLiquidSdkEvent(liquidSdkEvent: [String: Any?]) throws -> LiquidSdkEvent { + let type = liquidSdkEvent["type"] as! String + if type == "paymentFailed" { + guard let detailsTmp = liquidSdkEvent["details"] as? [String: Any?] else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "details", typeName: "LiquidSdkEvent")) + } + let _details = try asPayment(payment: detailsTmp) + + return LiquidSdkEvent.paymentFailed(details: _details) + } + if type == "paymentPending" { + guard let detailsTmp = liquidSdkEvent["details"] as? [String: Any?] else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "details", typeName: "LiquidSdkEvent")) + } + let _details = try asPayment(payment: detailsTmp) + + return LiquidSdkEvent.paymentPending(details: _details) + } + if type == "paymentRefunded" { + guard let detailsTmp = liquidSdkEvent["details"] as? [String: Any?] else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "details", typeName: "LiquidSdkEvent")) + } + let _details = try asPayment(payment: detailsTmp) + + return LiquidSdkEvent.paymentRefunded(details: _details) + } + if type == "paymentRefundPending" { + guard let detailsTmp = liquidSdkEvent["details"] as? [String: Any?] else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "details", typeName: "LiquidSdkEvent")) + } + let _details = try asPayment(payment: detailsTmp) + + return LiquidSdkEvent.paymentRefundPending(details: _details) + } + if type == "paymentSucceed" { + guard let detailsTmp = liquidSdkEvent["details"] as? [String: Any?] else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "details", typeName: "LiquidSdkEvent")) + } + let _details = try asPayment(payment: detailsTmp) + + return LiquidSdkEvent.paymentSucceed(details: _details) + } + if type == "paymentWaitingConfirmation" { + guard let detailsTmp = liquidSdkEvent["details"] as? [String: Any?] else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "details", typeName: "LiquidSdkEvent")) + } + let _details = try asPayment(payment: detailsTmp) + + return LiquidSdkEvent.paymentWaitingConfirmation(details: _details) + } + if type == "synced" { + return LiquidSdkEvent.synced + } + + throw LiquidSdkError.Generic(message: "Unexpected type \(type) for enum LiquidSdkEvent") + } + + static func dictionaryOf(liquidSdkEvent: LiquidSdkEvent) -> [String: Any?] { + switch liquidSdkEvent { + case let .paymentFailed( + details + ): + return [ + "type": "paymentFailed", + "details": dictionaryOf(payment: details), + ] + + case let .paymentPending( + details + ): + return [ + "type": "paymentPending", + "details": dictionaryOf(payment: details), + ] + + case let .paymentRefunded( + details + ): + return [ + "type": "paymentRefunded", + "details": dictionaryOf(payment: details), + ] + + case let .paymentRefundPending( + details + ): + return [ + "type": "paymentRefundPending", + "details": dictionaryOf(payment: details), + ] + + case let .paymentSucceed( + details + ): + return [ + "type": "paymentSucceed", + "details": dictionaryOf(payment: details), + ] + + case let .paymentWaitingConfirmation( + details + ): + return [ + "type": "paymentWaitingConfirmation", + "details": dictionaryOf(payment: details), + ] + + case .synced: + return [ + "type": "synced", + ] + } + } + + static func arrayOf(liquidSdkEventList: [LiquidSdkEvent]) -> [Any] { + return liquidSdkEventList.map { v -> [String: Any?] in dictionaryOf(liquidSdkEvent: v) } + } + + static func asLiquidSdkEventList(arr: [Any]) throws -> [LiquidSdkEvent] { + var list = [LiquidSdkEvent]() + for value in arr { + if let val = value as? [String: Any?] { + var liquidSdkEvent = try asLiquidSdkEvent(liquidSdkEvent: val) + list.append(liquidSdkEvent) + } else { + throw LiquidSdkError.Generic(message: errUnexpectedType(typeName: "LiquidSdkEvent")) + } + } + return list + } + static func asNetwork(network: String) throws -> Network { switch network { case "liquid": diff --git a/packages/react-native/ios/RNBreezLiquidSDK.m b/packages/react-native/ios/RNBreezLiquidSDK.m index 528ac3e..085444f 100644 --- a/packages/react-native/ios/RNBreezLiquidSDK.m +++ b/packages/react-native/ios/RNBreezLiquidSDK.m @@ -9,6 +9,17 @@ RCT_EXTERN_METHOD( reject: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + addEventListener: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + +RCT_EXTERN_METHOD( + removeEventListener: (NSString*)id + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( getInfo: (NSDictionary*)req resolve: (RCTPromiseResolveBlock)resolve diff --git a/packages/react-native/ios/RNBreezLiquidSDK.swift b/packages/react-native/ios/RNBreezLiquidSDK.swift index 2649c81..9d4fa6d 100644 --- a/packages/react-native/ios/RNBreezLiquidSDK.swift +++ b/packages/react-native/ios/RNBreezLiquidSDK.swift @@ -7,6 +7,7 @@ class RNBreezLiquidSDK: RCTEventEmitter { public static var emitter: RCTEventEmitter! public static var hasListeners: Bool = false + public static var supportedEvents: [String] = [] private var bindingLiquidSdk: BindingLiquidSdk! @@ -26,8 +27,12 @@ class RNBreezLiquidSDK: RCTEventEmitter { TAG } + static func addSupportedEvent(name: String) { + RNBreezLiquidSDK.supportedEvents.append(name) + } + override func supportedEvents() -> [String]! { - return [] + return RNBreezLiquidSDK.supportedEvents } override func startObserving() { @@ -68,6 +73,29 @@ class RNBreezLiquidSDK: RCTEventEmitter { } } + @objc(addEventListener:reject:) + func addEventListener(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + var eventListener = BreezLiquidSDKEventListener() + var res = try getBindingLiquidSdk().addEventListener(listener: eventListener) + + eventListener.setId(id: res) + resolve(res) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + + @objc(removeEventListener:resolve:reject:) + func removeEventListener(_ id: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + try getBindingLiquidSdk().removeEventListener(id: id) + resolve(["status": "ok"]) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + @objc(getInfo:resolve:reject:) func getInfo(_ req: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { do { diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index fef89d4..01afb62 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -1,4 +1,4 @@ -import { NativeModules, Platform } from "react-native" +import { NativeModules, Platform, NativeEventEmitter } from "react-native" const LINKING_ERROR = `The package 'react-native-breez-liquid-sdk' doesn't seem to be linked. Make sure: \n\n` + @@ -17,6 +17,8 @@ const BreezLiquidSDK = NativeModules.RNBreezLiquidSDK } ) +const BreezLiquidSDKEmitter = new NativeEventEmitter(BreezLiquidSDK) + export interface ConnectRequest { mnemonic: string network: Network @@ -76,6 +78,38 @@ export interface SendPaymentResponse { txid: string } +export enum LiquidSdkEventVariant { + PAYMENT_FAILED = "paymentFailed", + PAYMENT_PENDING = "paymentPending", + PAYMENT_REFUNDED = "paymentRefunded", + PAYMENT_REFUND_PENDING = "paymentRefundPending", + PAYMENT_SUCCEED = "paymentSucceed", + PAYMENT_WAITING_CONFIRMATION = "paymentWaitingConfirmation", + SYNCED = "synced" +} + +export type LiquidSdkEvent = { + type: LiquidSdkEventVariant.PAYMENT_FAILED, + details: Payment +} | { + type: LiquidSdkEventVariant.PAYMENT_PENDING, + details: Payment +} | { + type: LiquidSdkEventVariant.PAYMENT_REFUNDED, + details: Payment +} | { + type: LiquidSdkEventVariant.PAYMENT_REFUND_PENDING, + details: Payment +} | { + type: LiquidSdkEventVariant.PAYMENT_SUCCEED, + details: Payment +} | { + type: LiquidSdkEventVariant.PAYMENT_WAITING_CONFIRMATION, + details: Payment +} | { + type: LiquidSdkEventVariant.SYNCED +} + export enum Network { LIQUID = "liquid", LIQUID_TESTNET = "liquidTestnet" @@ -93,11 +127,24 @@ export enum PaymentType { SEND = "send" } +export type EventListener = (e: LiquidSdkEvent) => void + export const connect = async (req: ConnectRequest): Promise => { const response = await BreezLiquidSDK.connect(req) return response } +export const addEventListener = async (listener: EventListener): Promise => { + const response = await BreezLiquidSDK.addEventListener() + BreezLiquidSDKEmitter.addListener(`event-${response}`, listener) + + return response +} + +export const removeEventListener = async (id: string): Promise => { + await BreezLiquidSDK.removeEventListener(id) +} + export const getInfo = async (req: GetInfoRequest): Promise => { const response = await BreezLiquidSDK.getInfo(req) return response