diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 72712de79..4b4c095bd 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -395,7 +395,6 @@ dependencies = [ "bip39", "breez-liquid-sdk", "clap", - "env_logger 0.11.3", "log", "qrcode-rs", "rustyline", @@ -411,6 +410,8 @@ dependencies = [ "anyhow", "bip39", "boltz-client", + "chrono", + "env_logger 0.11.3", "flutter_rust_bridge", "futures-util", "glob", @@ -418,7 +419,6 @@ dependencies = [ "lwk_common", "lwk_signer", "lwk_wollet", - "once_cell", "openssl", "rusqlite", "rusqlite_migration", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a3ae7a017..c0669e330 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -10,7 +10,6 @@ anyhow = "1.0.80" bip39 = "2.0.0" breez-liquid-sdk = { path = "../lib/core" } clap = { version = "4.5.1", features = ["derive"] } -env_logger = "0.11" log = "0.4.20" qrcode-rs = { version = "0.1", default-features = false } rustyline = { version = "13.0.0", features = ["derive"] } diff --git a/cli/README.md b/cli/README.md index ebade1dad..77c59e71a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -24,14 +24,3 @@ To specify a custom data directory, use ```bash cargo run -- --data-dir temp-dir ``` - -To set a custom log level, use - -```bash -RUST_LOG=info|debug|warn cargo run -``` - -To specify a file to pipe logs to, use -```bash -RUST_LOG=info|debug|warn cargo run -- --log-file /tmp/log -``` diff --git a/cli/src/main.rs b/cli/src/main.rs index a8930fc19..1eac507fc 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,10 +1,7 @@ mod commands; mod persist; -use std::{ - fs::{self, OpenOptions}, - path::PathBuf, -}; +use std::{fs, path::PathBuf}; use anyhow::{anyhow, Result}; use breez_liquid_sdk::{ @@ -61,23 +58,7 @@ async fn main() -> Result<()> { let data_dir = PathBuf::from(&data_dir_str); fs::create_dir_all(&data_dir)?; - let log_path = args.log_file.unwrap_or( - data_dir - .join("cli.log") - .to_str() - .ok_or(anyhow!("Could not create log file"))? - .to_string(), - ); - let log_file = OpenOptions::new() - .create(true) - .append(true) - .open(log_path)?; - - env_logger::builder() - .target(env_logger::Target::Pipe(Box::new(log_file))) - .filter(None, log::LevelFilter::Debug) - .filter(Some("rustyline"), log::LevelFilter::Warn) - .init(); + LiquidSdk::init_logging(&data_dir_str, None)?; let persistence = CliPersistence { data_dir }; let history_file = &persistence.history_file(); diff --git a/lib/Cargo.lock b/lib/Cargo.lock index cb29db4aa..41d617b4f 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -514,6 +514,8 @@ dependencies = [ "anyhow", "bip39", "boltz-client", + "chrono", + "env_logger 0.11.3", "flutter_rust_bridge", "futures-util", "glob", @@ -521,7 +523,6 @@ dependencies = [ "lwk_common", "lwk_signer", "lwk_wollet", - "once_cell", "openssl", "rusqlite", "rusqlite_migration", @@ -545,6 +546,7 @@ dependencies = [ "breez-liquid-sdk", "camino", "glob", + "log", "once_cell", "thiserror", "tokio", @@ -894,6 +896,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -901,7 +913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", - "humantime", + "humantime 1.3.0", "log", "regex", "termcolor", @@ -917,6 +929,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime 2.1.0", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1344,6 +1369,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "1.3.1" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 6b7addef3..e608cf53e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -32,6 +32,8 @@ version = "0.0.1" [workspace.dependencies] anyhow = "1.0" +log = "0.4.20" +once_cell = "1.19" thiserror = "1.0" uniffi = "0.27.1" uniffi_macros = "0.27.1" diff --git a/lib/bindings/Cargo.toml b/lib/bindings/Cargo.toml index 2ccd8636e..8656ac753 100644 --- a/lib/bindings/Cargo.toml +++ b/lib/bindings/Cargo.toml @@ -14,6 +14,7 @@ crate-type = ["staticlib", "cdylib", "lib"] [dependencies] anyhow = { workspace = true } breez-liquid-sdk = { path = "../core" } +log = { workspace = true } uniffi = { workspace = true, features = [ "bindgen-tests", "cli" ] } # Bindgen used by KMP, version has to match the one supported by KMP uniffi_bindgen = "0.25.2" @@ -21,7 +22,7 @@ uniffi-kotlin-multiplatform = { git = "https://gitlab.com/trixnity/uniffi-kotlin camino = "1.1.1" thiserror = { workspace = true } tokio = { version = "1", features = ["rt"] } -once_cell = "*" +once_cell = { workspace = true } [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 6fc4f6403..cd2ef3ccb 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 @@ -176,6 +176,11 @@ typedef struct wire_cst_ln_invoice { uint64_t min_final_cltv_expiry_delta; } wire_cst_ln_invoice; +typedef struct wire_cst_log_entry { + struct wire_cst_list_prim_u_8_strict *line; + struct wire_cst_list_prim_u_8_strict *level; +} wire_cst_log_entry; + typedef struct wire_cst_PaymentError_Generic { struct wire_cst_list_prim_u_8_strict *err; } wire_cst_PaymentError_Generic; @@ -260,6 +265,9 @@ void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_send_payment(in void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sync(int64_t port_, uintptr_t that); +void frbgen_breez_liquid_wire__crate__bindings__breez_log_stream(int64_t port_, + struct wire_cst_list_prim_u_8_strict *s); + void frbgen_breez_liquid_wire__crate__bindings__connect(int64_t port_, struct wire_cst_connect_request *req); @@ -327,6 +335,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_restore); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_send_payment); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sync); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__breez_log_stream); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__connect); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__parse_invoice); dummy_var ^= ((int64_t) (void*) store_dart_post_cobject); diff --git a/lib/bindings/langs/react-native/Cargo.toml b/lib/bindings/langs/react-native/Cargo.toml index 5160c2432..40f2cf70a 100644 --- a/lib/bindings/langs/react-native/Cargo.toml +++ b/lib/bindings/langs/react-native/Cargo.toml @@ -13,14 +13,14 @@ uniffi = { version = "0.23.0", features = ["bindgen-tests", "cli"] } uniffi_bindgen = "0.23.0" uniffi_macros = "0.23.0" camino = "1.1.1" -log = "*" +log = { workspace = true } serde = "*" askama = { version = "0.11.1", default-features = false, features = ["config"] } toml = "0.5" clap = { version = "3.2.22", features = ["derive"] } heck = "0.4" paste = "1.0" -once_cell = "1.12" +once_cell = { workspace = true } [build-dependencies] uniffi_build = { version = "0.23.0" } 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 d7c88477f..0eaf099c2 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", "add_event_listener"]; + let list: Vec<&str> = vec!["connect", "add_event_listener", "set_logger"]; HashSet::from_iter(list.into_iter().map(|s| s.to_string())) }); 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 e83d9c420..6bfb42ccc 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 @@ -47,7 +47,21 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext {%- if func.name()|ignored_function == false -%} {% include "TopLevelFunctionTemplate.kt" %} {% endif -%} - {%- endfor %} + {%- endfor %} + @ReactMethod + fun setLogger(promise: Promise) { + executor.execute { + try { + val emitter = reactApplicationContext.getJSModule(RCTDeviceEventEmitter::class.java) + + setLogger(BreezLiquidSDKLogger(emitter)) + promise.resolve(readableMapOf("status" to "ok")) + } catch (e: Exception) { + e.printStackTrace() + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } @ReactMethod fun connect(req: ReadableMap, promise: Promise) { 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 1a06e2825..1ee706373 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", "add_event_listener"]; + let list: Vec<&str> = vec!["connect", "add_event_listener", "set_logger"]; HashSet::from_iter(list.into_iter().map(|s| s.to_string())) }); 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 1d2483ea9..9441f0bf2 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 @@ -7,6 +7,11 @@ @interface RCT_EXTERN_MODULE(RNBreezLiquidSDK, RCTEventEmitter) {% include "ExternFunctionTemplate.m" %} {% endif %} {%- endfor %} +RCT_EXTERN_METHOD( + setLogger: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( connect: (NSDictionary*)req resolve: (RCTPromiseResolveBlock)resolve 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 8a68d1d26..87768279a 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,7 +7,7 @@ class RNBreezLiquidSDK: RCTEventEmitter { public static var emitter: RCTEventEmitter! public static var hasListeners: Bool = false - public static var supportedEvents: [String] = [] + public static var supportedEvents: [String] = ["breezLiquidSdkLog"] private var bindingLiquidSdk: BindingLiquidSdk! @@ -62,6 +62,16 @@ class RNBreezLiquidSDK: RCTEventEmitter { {% include "TopLevelFunctionTemplate.swift" %} {% endif -%} {%- endfor %} + @objc(setLogger:reject:) + func setLogger(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + do { + try BreezLiquidSDK.setLogger(Logger: BreezLiquidSDKLogger()) + resolve(["status": "ok"]) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + @objc(connect:resolve:reject:) func connect(_ req:[String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { if bindingLiquidSdk != nil { 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 851ecb794..a8bb6912c 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", "add_event_listener"]; + let list: Vec<&str> = vec!["connect", "add_event_listener", "set_logger"]; 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 381ec21ba..97a2443d1 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,6 +1,8 @@ export type EventListener = (e: LiquidSdkEvent) => void +export type Logger = (logEntry: LogEntry) => void + export const connect = async (req: ConnectRequest): Promise => { const response = await BreezLiquidSDK.connect(req) return response @@ -12,3 +14,13 @@ export const addEventListener = async (listener: EventListener): Promise return response } + +export const setLogger = async (logger: Logger): Promise => { + const subscription = BreezLiquidSDKEmitter.addListener("breezLiquidSdkLog", logger) + + try { + await BreezLiquidSDK.setLogger() + } catch {} + + return subscription +} \ No newline at end of file diff --git a/lib/bindings/src/breez_liquid_sdk.udl b/lib/bindings/src/breez_liquid_sdk.udl index fd3693b76..e8d78ed03 100644 --- a/lib/bindings/src/breez_liquid_sdk.udl +++ b/lib/bindings/src/breez_liquid_sdk.udl @@ -150,10 +150,22 @@ callback interface EventListener { void on_event(LiquidSdkEvent e); }; +callback interface Logger { + void log(LogEntry l); +}; + +dictionary LogEntry { + string line; + string level; +}; + namespace breez_liquid_sdk { [Throws=LiquidSdkError] BindingLiquidSdk connect(ConnectRequest req); + [Throws=LiquidSdkError] + void set_logger(Logger logger); + [Throws=PaymentError] LNInvoice parse_invoice(string invoice); }; diff --git a/lib/bindings/src/lib.rs b/lib/bindings/src/lib.rs index e9325d7fc..5933835a8 100644 --- a/lib/bindings/src/lib.rs +++ b/lib/bindings/src/lib.rs @@ -1,9 +1,14 @@ +//! Uniffi bindings + use std::sync::Arc; use anyhow::Result; +use breez_liquid_sdk::logger::Logger; use breez_liquid_sdk::{error::*, model::*, sdk::LiquidSdk}; +use log::{Metadata, Record, SetLoggerError}; use once_cell::sync::Lazy; use tokio::runtime::Runtime; +use uniffi::deps::log::{Level, LevelFilter}; static RT: Lazy = Lazy::new(|| Runtime::new().unwrap()); @@ -11,6 +16,41 @@ fn rt() -> &'static Runtime { &RT } +struct UniffiBindingLogger { + logger: Box, +} + +impl UniffiBindingLogger { + fn init(logger: Box) -> Result<(), SetLoggerError> { + let binding_logger: UniffiBindingLogger = UniffiBindingLogger { logger }; + log::set_boxed_logger(Box::new(binding_logger)) + .map(|_| log::set_max_level(LevelFilter::Trace)) + } +} + +impl log::Log for UniffiBindingLogger { + fn enabled(&self, m: &Metadata) -> bool { + // ignore the internal uniffi log to prevent infinite loop. + return m.level() <= Level::Trace && *m.target() != *"breez_liquid_sdk_bindings"; + } + + fn log(&self, record: &Record) { + self.logger.log(LogEntry { + line: record.args().to_string(), + level: record.level().as_str().to_string(), + }); + } + fn flush(&self) {} +} + +/// If used, this must be called before `connect` +pub fn set_logger(logger: Box) -> Result<(), LiquidSdkError> { + UniffiBindingLogger::init(logger).map_err(|_| LiquidSdkError::Generic { + err: "Logger already created".into(), + })?; + Ok(()) +} + pub fn connect(req: ConnectRequest) -> Result, LiquidSdkError> { rt().block_on(async { let sdk = LiquidSdk::connect(req).await?; diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 72c409240..54347b5b2 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -16,8 +16,10 @@ anyhow = { workspace = true } bip39 = { version = "2.0.0", features = ["serde"] } #boltz-client = { git = "https://github.com/SatoshiPortal/boltz-rust", rev = "a05731cc33030ada9ae14afcafe0cded22842ba6" } boltz-client = { git = "https://github.com/ok300/boltz-rust", branch = "ok300-breez-latest-05-28" } +chrono = "0.4" +env_logger = "0.11" flutter_rust_bridge = { version = "=2.0.0-dev.36", features = ["chrono"], optional = true } -log = "0.4.20" +log = { workspace = true } lwk_common = "0.5.1" lwk_signer = "0.5.1" # Switch back to published version once this PR is merged: https://github.com/Blockstream/lwk/pull/34 @@ -29,7 +31,6 @@ serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.116" thiserror = { workspace = true } tokio-tungstenite = { version = "0.21.0", features = ["native-tls-vendored"] } -once_cell = "1" openssl = { version = "0.10", features = ["vendored"] } tokio = { version = "1", features = ["rt", "macros"] } url = "2.5.0" diff --git a/lib/core/src/bindings.rs b/lib/core/src/bindings.rs index e22c7d6ac..d2cac8686 100644 --- a/lib/core/src/bindings.rs +++ b/lib/core/src/bindings.rs @@ -1,7 +1,12 @@ -use crate::{error::*, frb::bridge::StreamSink, model::*, sdk::LiquidSdk}; +//! Dart / flutter bindings + +use std::sync::Arc; + use anyhow::Result; use flutter_rust_bridge::frb; -use std::sync::Arc; +use log::{Level, LevelFilter, Metadata, Record, SetLoggerError}; + +use crate::{error::*, frb::bridge::StreamSink, model::*, sdk::LiquidSdk}; struct BindingEventListener { stream: StreamSink, @@ -13,11 +18,47 @@ impl EventListener for BindingEventListener { } } +struct DartBindingLogger { + log_stream: StreamSink, +} + +impl DartBindingLogger { + fn init(log_stream: StreamSink) -> Result<(), SetLoggerError> { + let binding_logger: DartBindingLogger = DartBindingLogger { log_stream }; + log::set_boxed_logger(Box::new(binding_logger)) + .map(|_| log::set_max_level(LevelFilter::Trace)) + } +} + +impl log::Log for DartBindingLogger { + fn enabled(&self, m: &Metadata) -> bool { + m.level() <= Level::Trace + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + let _ = self.log_stream.add(LogEntry { + line: record.args().to_string(), + level: record.level().as_str().to_string(), + }); + } + } + fn flush(&self) {} +} + pub async fn connect(req: ConnectRequest) -> Result { let ln_sdk = LiquidSdk::connect(req).await?; Ok(BindingLiquidSdk { sdk: ln_sdk }) } +/// If used, this must be called before `connect`. It can only be called once. +pub fn breez_log_stream(s: StreamSink) -> Result<()> { + DartBindingLogger::init(s).map_err(|_| LiquidSdkError::Generic { + err: "Log stream already created".into(), + })?; + Ok(()) +} + pub fn parse_invoice(input: String) -> Result { LiquidSdk::parse_invoice(&input) } diff --git a/lib/core/src/frb/bridge.io.rs b/lib/core/src/frb/bridge.io.rs index d02c6ad8d..bc9a94a66 100644 --- a/lib/core/src/frb/bridge.io.rs +++ b/lib/core/src/frb/bridge.io.rs @@ -15,6 +15,14 @@ flutter_rust_bridge::frb_generated_boilerplate_io!(); // Section: dart2rust +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) -> flutter_rust_bridge::for_generated::anyhow::Error { + unimplemented!() + } +} impl CstDecode for usize { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> BindingLiquidSdk { @@ -54,6 +62,17 @@ impl 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, + ) -> 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 { @@ -286,6 +305,15 @@ impl CstDecode for wire_cst_ln_invoice { } } } +impl CstDecode for wire_cst_log_entry { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::model::LogEntry { + crate::model::LogEntry { + line: self.line.cst_decode(), + level: self.level.cst_decode(), + } + } +} impl CstDecode for wire_cst_payment { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::model::Payment { @@ -534,6 +562,19 @@ impl Default for wire_cst_ln_invoice { Self::new_with_null_ptr() } } +impl NewWithNullPtr for wire_cst_log_entry { + fn new_with_null_ptr() -> Self { + Self { + line: core::ptr::null_mut(), + level: core::ptr::null_mut(), + } + } +} +impl Default for wire_cst_log_entry { + fn default() -> Self { + Self::new_with_null_ptr() + } +} impl NewWithNullPtr for wire_cst_payment { fn new_with_null_ptr() -> Self { Self { @@ -787,6 +828,14 @@ pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sy wire__crate__bindings__BindingLiquidSdk_sync_impl(port_, that) } +#[no_mangle] +pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__breez_log_stream( + port_: i64, + s: *mut wire_cst_list_prim_u_8_strict, +) { + wire__crate__bindings__breez_log_stream_impl(port_, s) +} + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__connect( port_: i64, @@ -1077,6 +1126,12 @@ pub struct wire_cst_ln_invoice { } #[repr(C)] #[derive(Clone, Copy)] +pub struct wire_cst_log_entry { + line: *mut wire_cst_list_prim_u_8_strict, + level: *mut wire_cst_list_prim_u_8_strict, +} +#[repr(C)] +#[derive(Clone, Copy)] pub struct wire_cst_payment { tx_id: *mut wire_cst_list_prim_u_8_strict, swap_id: *mut wire_cst_list_prim_u_8_strict, diff --git a/lib/core/src/frb/bridge.rs b/lib/core/src/frb/bridge.rs index 935b2af46..91102dd71 100644 --- a/lib/core/src/frb/bridge.rs +++ b/lib/core/src/frb/bridge.rs @@ -34,7 +34,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueNom, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.0.0-dev.36"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 84136112; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -532134055; // Section: executor @@ -360,6 +360,16 @@ let decode_indices_ = flutter_rust_bridge::for_generated::rust_auto_opaque_decod })().await) } }) } +fn wire__crate__bindings__breez_log_stream_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + s: impl CstDecode>, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "breez_log_stream", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { let api_s = s.cst_decode(); move |context| { + transform_result_dco((move || { + crate::bindings::breez_log_stream(api_s) + })()) + } }) +} fn wire__crate__bindings__connect_impl( port_: flutter_rust_bridge::for_generated::MessagePort, req: impl CstDecode, @@ -470,6 +480,14 @@ impl CstDecode for usize { self } } +impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { + // 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 flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); + } +} + impl SseDecode for BindingLiquidSdk { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -500,6 +518,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 { @@ -725,6 +753,18 @@ impl SseDecode for crate::model::LNInvoice { } } +impl SseDecode for crate::model::LogEntry { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_line = ::sse_decode(deserializer); + let mut var_level = ::sse_decode(deserializer); + return crate::model::LogEntry { + line: var_line, + level: var_level, + }; + } +} + impl SseDecode for crate::model::Network { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1208,6 +1248,22 @@ impl flutter_rust_bridge::IntoIntoDart for crate::model } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::model::LogEntry { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.line.into_into_dart().into_dart(), + self.level.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::model::LogEntry {} +impl flutter_rust_bridge::IntoIntoDart for crate::model::LogEntry { + fn into_into_dart(self) -> crate::model::LogEntry { + 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 { @@ -1483,6 +1539,13 @@ impl flutter_rust_bridge::IntoIntoDart } } +impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(format!("{:?}", self), serializer); + } +} + impl SseEncode for BindingLiquidSdk { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1510,6 +1573,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) { @@ -1675,6 +1747,14 @@ impl SseEncode for crate::model::LNInvoice { } } +impl SseEncode for crate::model::LogEntry { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.line, serializer); + ::sse_encode(self.level, serializer); + } +} + impl SseEncode for crate::model::Network { // 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/lib.rs b/lib/core/src/lib.rs index 113df0ffa..645ed379d 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -5,6 +5,7 @@ pub mod error; pub(crate) mod event; #[cfg(feature = "frb")] pub mod frb; +pub mod logger; pub mod model; pub mod persist; pub mod sdk; diff --git a/lib/core/src/logger.rs b/lib/core/src/logger.rs new file mode 100644 index 000000000..6ebf6a6df --- /dev/null +++ b/lib/core/src/logger.rs @@ -0,0 +1,84 @@ +use std::fs::OpenOptions; +use std::io::Write; + +use anyhow::{anyhow, Result}; +use chrono::Local; +use log::{LevelFilter, Metadata, Record}; + +use crate::model::LogEntry; + +pub(crate) struct GlobalSdkLogger { + /// SDK internal logger, which logs to file + pub(crate) logger: env_logger::Logger, + /// Optional external log listener, that can receive a stream of log statements + pub(crate) log_listener: Option>, +} +impl log::Log for GlobalSdkLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::Level::Trace + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + self.logger.log(record); + + if let Some(s) = &self.log_listener.as_ref() { + if s.enabled(record.metadata()) { + s.log(record); + } + } + } + } + + fn flush(&self) {} +} + +pub(super) fn init_logging(log_dir: &str, app_logger: Option>) -> Result<()> { + let target_log_file = Box::new( + OpenOptions::new() + .create(true) + .append(true) + .open(format!("{log_dir}/sdk.log")) + .map_err(|e| anyhow!("Can't create log file: {e}"))?, + ); + let logger = env_logger::Builder::new() + .target(env_logger::Target::Pipe(target_log_file)) + .parse_filters( + r#" + debug, + breez_liquid_sdk=debug, + electrum_client::raw_client=warn, + lwk_wollet=info, + rustls=warn, + rustyline=warn, + tungstenite=warn + "#, + ) + .format(|buf, record| { + writeln!( + buf, + "[{} {} {}:{}] {}", + Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), + record.level(), + record.module_path().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.args() + ) + }) + .build(); + + let global_logger = GlobalSdkLogger { + logger, + log_listener: app_logger, + }; + + log::set_boxed_logger(Box::new(global_logger)) + .map_err(|e| anyhow!("Failed to set global logger: {e}"))?; + log::set_max_level(LevelFilter::Trace); + + Ok(()) +} + +pub trait Logger: Send + Sync { + fn log(&self, l: LogEntry); +} diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 47eada2a2..ce8d49c2f 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -567,6 +567,13 @@ impl Payment { } } +/// Internal SDK log entry used in the Uniffi and Dart bindings +#[derive(Clone, Debug)] +pub struct LogEntry { + pub line: String, + pub level: String, +} + /// Wrapper for a BOLT11 LN invoice #[derive(Clone, Debug, PartialEq)] pub struct LNInvoice { diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 59e2fc227..bceb65ae1 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -1484,6 +1484,33 @@ impl LiquidSdk { }; Ok(res) } + + /// Configures a global SDK logger that will log to file and will forward log events to + /// an optional application-specific logger. + /// + /// If called, it should be called before any SDK methods (for example, before `connect`). + /// + /// It must be called only once in the application lifecycle. Alternatively, If the application + /// already uses a globally-registered logger, this method shouldn't be called at all. + /// + /// ### Arguments + /// + /// - `log_dir`: Location where the the SDK log file will be created. The directory must already exist. + /// + /// - `app_logger`: Optional application logger. + /// + /// If the application is to use it's own logger, but would also like the SDK to log SDK-specific + /// log output to a file in the configured `log_dir`, then do not register the + /// app-specific logger as a global logger and instead call this method with the app logger as an arg. + /// + /// ### Errors + /// + /// An error is thrown if the log file cannot be created in the working directory. + /// + /// An error is thrown if a global logger is already configured. + pub fn init_logging(log_dir: &str, app_logger: Option>) -> Result<()> { + crate::logger::init_logging(log_dir, app_logger) + } } #[cfg(test)] diff --git a/packages/dart/lib/src/bindings.dart b/packages/dart/lib/src/bindings.dart index dcd32fc64..132fd90ae 100644 --- a/packages/dart/lib/src/bindings.dart +++ b/packages/dart/lib/src/bindings.dart @@ -9,10 +9,15 @@ 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. +// The type `DartBindingLogger` 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); +/// If used, this must be called before `connect`. It can only be called once. +Stream breezLogStream({dynamic hint}) => + RustLib.instance.api.crateBindingsBreezLogStream(hint: hint); + Future parseInvoice({required String input, dynamic hint}) => RustLib.instance.api.crateBindingsParseInvoice(input: input, hint: hint); diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index 68ed0f9ad..df4745687 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -53,7 +53,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.0.0-dev.36'; @override - int get rustContentHash => 84136112; + int get rustContentHash => -532134055; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( stem: 'breez_liquid_sdk', @@ -96,6 +96,8 @@ abstract class RustLibApi extends BaseApi { Future crateBindingsBindingLiquidSdkSync({required BindingLiquidSdk that, dynamic hint}); + Stream crateBindingsBreezLogStream({dynamic hint}); + Future crateBindingsConnect({required ConnectRequest req, dynamic hint}); Future crateBindingsParseInvoice({required String input, dynamic hint}); @@ -434,6 +436,31 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that"], ); + @override + Stream crateBindingsBreezLogStream({dynamic hint}) { + final s = RustStreamSink(); + unawaited(handler.executeNormal(NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_StreamSink_log_entry_Dco(s); + return wire.wire__crate__bindings__breez_log_stream(port_, arg0); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateBindingsBreezLogStreamConstMeta, + argValues: [s], + apiImpl: this, + hint: hint, + ))); + return s.stream; + } + + TaskConstMeta get kCrateBindingsBreezLogStreamConstMeta => const TaskConstMeta( + debugName: "breez_log_stream", + argNames: ["s"], + ); + @override Future crateBindingsConnect({required ConnectRequest req, dynamic hint}) { return handler.executeNormal(NormalTask( @@ -487,6 +514,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { RustArcDecrementStrongCountFnType get rust_arc_decrement_strong_count_BindingLiquidSdk => wire .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk; + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AnyhowException(raw as String); + } + @protected BindingLiquidSdk dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( @@ -516,6 +549,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError(); } + @protected + RustStreamSink dco_decode_StreamSink_log_entry_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 @@ -736,6 +775,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + LogEntry dco_decode_log_entry(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return LogEntry( + line: dco_decode_String(arr[0]), + level: dco_decode_String(arr[1]), + ); + } + @protected Network dco_decode_network(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -962,6 +1012,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dcoDecodeU64(raw); } + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_String(deserializer); + return AnyhowException(inner); + } + @protected BindingLiquidSdk sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( @@ -991,6 +1048,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError('Unreachable ()'); } + @protected + RustStreamSink sse_decode_StreamSink_log_entry_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 @@ -1229,6 +1292,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { minFinalCltvExpiryDelta: var_minFinalCltvExpiryDelta); } + @protected + LogEntry sse_decode_log_entry(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_line = sse_decode_String(deserializer); + var var_level = sse_decode_String(deserializer); + return LogEntry(line: var_line, level: var_level); + } + @protected Network sse_decode_network(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1523,6 +1594,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw; } + @protected + void sse_encode_AnyhowException(AnyhowException self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.message, serializer); + } + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( BindingLiquidSdk self, SseSerializer serializer) { @@ -1554,6 +1631,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer); } + @protected + void sse_encode_StreamSink_log_entry_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_log_entry, decodeErrorData: null)), + serializer); + } + @protected void sse_encode_String(String self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1754,6 +1840,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_64(self.minFinalCltvExpiryDelta, serializer); } + @protected + void sse_encode_log_entry(LogEntry self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.line, serializer); + sse_encode_String(self.level, serializer); + } + @protected void sse_encode_network(Network 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 3c6c11b9a..b17092d07 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -23,6 +23,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_BindingLiquidSdkPtr => wire ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdkPtr; + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw); + @protected BindingLiquidSdk dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( @@ -40,6 +43,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RustStreamSink dco_decode_StreamSink_liquid_sdk_event_Dco(dynamic raw); + @protected + RustStreamSink dco_decode_StreamSink_log_entry_Dco(dynamic raw); + @protected String dco_decode_String(dynamic raw); @@ -112,6 +118,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected LNInvoice dco_decode_ln_invoice(dynamic raw); + @protected + LogEntry dco_decode_log_entry(dynamic raw); + @protected Network dco_decode_network(dynamic raw); @@ -175,6 +184,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt dco_decode_usize(dynamic raw); + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + @protected BindingLiquidSdk sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( @@ -192,6 +204,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RustStreamSink sse_decode_StreamSink_liquid_sdk_event_Dco(SseDeserializer deserializer); + @protected + RustStreamSink sse_decode_StreamSink_log_entry_Dco(SseDeserializer deserializer); + @protected String sse_decode_String(SseDeserializer deserializer); @@ -264,6 +279,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected LNInvoice sse_decode_ln_invoice(SseDeserializer deserializer); + @protected + LogEntry sse_decode_log_entry(SseDeserializer deserializer); + @protected Network sse_decode_network(SseDeserializer deserializer); @@ -327,6 +345,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt sse_decode_usize(SseDeserializer deserializer); + @protected + ffi.Pointer cst_encode_AnyhowException(AnyhowException raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + throw UnimplementedError(); + } + @protected ffi.Pointer cst_encode_StreamSink_liquid_sdk_event_Dco( RustStreamSink raw) { @@ -335,6 +359,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { codec: DcoCodec(decodeSuccessData: dco_decode_liquid_sdk_event, decodeErrorData: null))); } + @protected + ffi.Pointer cst_encode_StreamSink_log_entry_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_log_entry, decodeErrorData: null))); + } + @protected ffi.Pointer cst_encode_String(String raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -641,6 +673,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { wireObj.min_final_cltv_expiry_delta = cst_encode_u_64(apiObj.minFinalCltvExpiryDelta); } + @protected + void cst_api_fill_to_wire_log_entry(LogEntry apiObj, wire_cst_log_entry wireObj) { + wireObj.line = cst_encode_String(apiObj.line); + wireObj.level = cst_encode_String(apiObj.level); + } + @protected void cst_api_fill_to_wire_payment(Payment apiObj, wire_cst_payment wireObj) { wireObj.tx_id = cst_encode_String(apiObj.txId); @@ -823,6 +861,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void cst_encode_unit(void raw); + @protected + void sse_encode_AnyhowException(AnyhowException self, SseSerializer serializer); + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( BindingLiquidSdk self, SseSerializer serializer); @@ -839,6 +880,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_StreamSink_liquid_sdk_event_Dco( RustStreamSink self, SseSerializer serializer); + @protected + void sse_encode_StreamSink_log_entry_Dco(RustStreamSink self, SseSerializer serializer); + @protected void sse_encode_String(String self, SseSerializer serializer); @@ -911,6 +955,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_ln_invoice(LNInvoice self, SseSerializer serializer); + @protected + void sse_encode_log_entry(LogEntry self, SseSerializer serializer); + @protected void sse_encode_network(Network self, SseSerializer serializer); @@ -1229,6 +1276,22 @@ class RustLibWire implements BaseWire { late final _wire__crate__bindings__BindingLiquidSdk_sync = _wire__crate__bindings__BindingLiquidSdk_syncPtr.asFunction(); + void wire__crate__bindings__breez_log_stream( + int port_, + ffi.Pointer s, + ) { + return _wire__crate__bindings__breez_log_stream( + port_, + s, + ); + } + + late final _wire__crate__bindings__breez_log_streamPtr = + _lookup)>>( + 'frbgen_breez_liquid_wire__crate__bindings__breez_log_stream'); + late final _wire__crate__bindings__breez_log_stream = _wire__crate__bindings__breez_log_streamPtr + .asFunction)>(); + void wire__crate__bindings__connect( int port_, ffi.Pointer req, @@ -1697,6 +1760,12 @@ final class wire_cst_ln_invoice extends ffi.Struct { external int min_final_cltv_expiry_delta; } +final class wire_cst_log_entry extends ffi.Struct { + external ffi.Pointer line; + + external ffi.Pointer level; +} + 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 fc00b764b..ae193aa0c 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -190,6 +190,25 @@ class LNInvoice { minFinalCltvExpiryDelta == other.minFinalCltvExpiryDelta; } +/// Internal SDK log entry used in the Uniffi and Dart bindings +class LogEntry { + final String line; + final String level; + + const LogEntry({ + required this.line, + required this.level, + }); + + @override + int get hashCode => line.hashCode ^ level.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LogEntry && runtimeType == other.runtimeType && line == other.line && level == other.level; +} + enum Network { /// Mainnet Bitcoin and Liquid chains mainnet, diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index d06830124..309cfb56e 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -260,6 +260,23 @@ class FlutterBreezLiquidBindings { _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_syncPtr .asFunction(); + void frbgen_breez_liquid_wire__crate__bindings__breez_log_stream( + int port_, + ffi.Pointer s, + ) { + return _frbgen_breez_liquid_wire__crate__bindings__breez_log_stream( + port_, + s, + ); + } + + late final _frbgen_breez_liquid_wire__crate__bindings__breez_log_streamPtr = + _lookup)>>( + 'frbgen_breez_liquid_wire__crate__bindings__breez_log_stream'); + late final _frbgen_breez_liquid_wire__crate__bindings__breez_log_stream = + _frbgen_breez_liquid_wire__crate__bindings__breez_log_streamPtr + .asFunction)>(); + void frbgen_breez_liquid_wire__crate__bindings__connect( int port_, ffi.Pointer req, @@ -755,6 +772,12 @@ final class wire_cst_ln_invoice extends ffi.Struct { external int min_final_cltv_expiry_delta; } +final class wire_cst_log_entry extends ffi.Struct { + external ffi.Pointer line; + + external ffi.Pointer level; +} + 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/BreezLiquidSDKLogStream.kt b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKLogStream.kt new file mode 100644 index 000000000..efabc8cdf --- /dev/null +++ b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKLogStream.kt @@ -0,0 +1,15 @@ +package com.breezliquidsdk + +import breez_liquid_sdk.LogEntry +import breez_liquid_sdk.Logger +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter + +class BreezLiquidSDKLogger(private val emitter: RCTDeviceEventEmitter) : Logger { + companion object { + var emitterName = "breezLiquidSdkLog" + } + + override fun log(l: LogEntry) { + emitter.emit(emitterName, readableMapOf(l)) + } +} \ No newline at end of file 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 3305a98de..7a9a8705d 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 @@ -226,6 +226,43 @@ fun asLnInvoiceList(arr: ReadableArray): List { return list } +fun asLogEntry(logEntry: ReadableMap): LogEntry? { + if (!validateMandatoryFields( + logEntry, + arrayOf( + "line", + "level", + ), + ) + ) { + return null + } + val line = logEntry.getString("line")!! + val level = logEntry.getString("level")!! + return LogEntry( + line, + level, + ) +} + +fun readableMapOf(logEntry: LogEntry): ReadableMap { + return readableMapOf( + "line" to logEntry.line, + "level" to logEntry.level, + ) +} + +fun asLogEntryList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + when (value) { + is ReadableMap -> list.add(asLogEntry(value)!!) + else -> throw LiquidSdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} + fun asPayment(payment: ReadableMap): Payment? { if (!validateMandatoryFields( payment, 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 4f0a3783f..c647bd1da 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 @@ -55,6 +55,21 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext } } + @ReactMethod + fun setLogger(promise: Promise) { + executor.execute { + try { + val emitter = reactApplicationContext.getJSModule(RCTDeviceEventEmitter::class.java) + + setLogger(BreezLiquidSDKLogger(emitter)) + promise.resolve(readableMapOf("status" to "ok")) + } catch (e: Exception) { + e.printStackTrace() + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + @ReactMethod fun connect( req: ReadableMap, diff --git a/packages/react-native/ios/BreezLiquidSDKLogStream.swift b/packages/react-native/ios/BreezLiquidSDKLogStream.swift new file mode 100644 index 000000000..a7f0749b1 --- /dev/null +++ b/packages/react-native/ios/BreezLiquidSDKLogStream.swift @@ -0,0 +1,13 @@ +import Foundation +import BreezLiquidSDK + +class BreezLiquidSDKLogger: Logger { + static let emitterName: String = "breezLiquidSdkLog" + + func log(l: LogEntry) { + if RNBreezLiquidSDK.hasListeners { + RNBreezLiquidSDK.emitter.sendEvent(withName: BreezLiquidSDKLogger.emitterName, + body: BreezLiquidSDKMapper.dictionaryOf(logEntry: l)) + } + } +} \ No newline at end of file diff --git a/packages/react-native/ios/BreezLiquidSDKMapper.swift b/packages/react-native/ios/BreezLiquidSDKMapper.swift index 11b3f3670..203bd63c5 100644 --- a/packages/react-native/ios/BreezLiquidSDKMapper.swift +++ b/packages/react-native/ios/BreezLiquidSDKMapper.swift @@ -271,6 +271,44 @@ enum BreezLiquidSDKMapper { return lnInvoiceList.map { v -> [String: Any?] in dictionaryOf(lnInvoice: v) } } + static func asLogEntry(logEntry: [String: Any?]) throws -> LogEntry { + guard let line = logEntry["line"] as? String else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "line", typeName: "LogEntry")) + } + guard let level = logEntry["level"] as? String else { + throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "level", typeName: "LogEntry")) + } + + return LogEntry( + line: line, + level: level + ) + } + + static func dictionaryOf(logEntry: LogEntry) -> [String: Any?] { + return [ + "line": logEntry.line, + "level": logEntry.level, + ] + } + + static func asLogEntryList(arr: [Any]) throws -> [LogEntry] { + var list = [LogEntry]() + for value in arr { + if let val = value as? [String: Any?] { + var logEntry = try asLogEntry(logEntry: val) + list.append(logEntry) + } else { + throw LiquidSdkError.Generic(message: errUnexpectedType(typeName: "LogEntry")) + } + } + return list + } + + static func arrayOf(logEntryList: [LogEntry]) -> [Any] { + return logEntryList.map { v -> [String: Any?] in dictionaryOf(logEntry: v) } + } + static func asPayment(payment: [String: Any?]) throws -> Payment { guard let txId = payment["txId"] as? String else { throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "txId", typeName: "Payment")) diff --git a/packages/react-native/ios/RNBreezLiquidSDK.m b/packages/react-native/ios/RNBreezLiquidSDK.m index cf9d68582..0f3dded57 100644 --- a/packages/react-native/ios/RNBreezLiquidSDK.m +++ b/packages/react-native/ios/RNBreezLiquidSDK.m @@ -9,6 +9,11 @@ @interface RCT_EXTERN_MODULE(RNBreezLiquidSDK, RCTEventEmitter) reject: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + setLogger: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( connect: (NSDictionary*)req resolve: (RCTPromiseResolveBlock)resolve diff --git a/packages/react-native/ios/RNBreezLiquidSDK.swift b/packages/react-native/ios/RNBreezLiquidSDK.swift index 166f20136..1a018a2f2 100644 --- a/packages/react-native/ios/RNBreezLiquidSDK.swift +++ b/packages/react-native/ios/RNBreezLiquidSDK.swift @@ -7,7 +7,7 @@ class RNBreezLiquidSDK: RCTEventEmitter { public static var emitter: RCTEventEmitter! public static var hasListeners: Bool = false - public static var supportedEvents: [String] = [] + public static var supportedEvents: [String] = ["breezLiquidSdkLog"] private var bindingLiquidSdk: BindingLiquidSdk! @@ -66,6 +66,16 @@ class RNBreezLiquidSDK: RCTEventEmitter { } } + @objc(setLogger:reject:) + func setLogger(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + try BreezLiquidSDK.setLogger(Logger: BreezLiquidSDKLogger()) + resolve(["status": "ok"]) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + @objc(connect:resolve:reject:) func connect(_ req: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { if bindingLiquidSdk != nil { diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index df9acb294..a08a6de95 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -55,6 +55,11 @@ export interface LnInvoice { minFinalCltvExpiryDelta: number } +export interface LogEntry { + line: string + level: string +} + export interface Payment { txId: string swapId?: string @@ -165,6 +170,8 @@ export enum PaymentType { export type EventListener = (e: LiquidSdkEvent) => void +export type Logger = (logEntry: LogEntry) => void + export const connect = async (req: ConnectRequest): Promise => { const response = await BreezLiquidSDK.connect(req) return response @@ -176,6 +183,16 @@ export const addEventListener = async (listener: EventListener): Promise return response } + +export const setLogger = async (logger: Logger): Promise => { + const subscription = BreezLiquidSDKEmitter.addListener("breezLiquidSdkLog", logger) + + try { + await BreezLiquidSDK.setLogger() + } catch {} + + return subscription +} export const parseInvoice = async (invoice: string): Promise => { const response = await BreezLiquidSDK.parseInvoice(invoice) return response