diff --git a/Cargo.lock b/Cargo.lock index 7b96cb2..f2e49a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,7 +395,7 @@ checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bitkitcore" -version = "0.1.18" +version = "0.1.20" dependencies = [ "async-trait", "bip39", diff --git a/Cargo.toml b/Cargo.toml index 3b38944..da71a70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitkitcore" -version = "0.1.18" +version = "0.1.20" edition = "2021" [lib] diff --git a/README.md b/README.md index ddc1b45..b69bd39 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ - Derive addresses for specified paths - Retrieve account information - Handle responses from Trezor devices +- Logging + - Callback-based logging allowing Kotlin/Swift to intercept Rust logs + - Multiple log levels (Trace, Debug, Info, Warn, Error) ## Available Modules: Methods - Scanner @@ -437,6 +440,72 @@ ) -> Result ``` +- Logging + - [set_custom_logger](#logging-usage): Register a custom logger for library consumers + ```rust + fn set_custom_logger(log_writer: Arc) + ``` + +## Logging Usage + +The logging module allows Kotlin/Swift code to intercept and handle logs from Rust. This enables routing Rust logs to platform-specific destinations like Android Logcat or iOS Console. + +### Setup (Kotlin) + +```kotlin +import com.synonym.bitkitcore.* +import android.util.Log + +// 1. Implement LogWriter interface +class AndroidLogWriter : LogWriter { + override fun log(record: LogRecord) { + val tag = "BitkitCore::${record.modulePath}" + when (record.level) { + LogLevel.TRACE -> Log.v(tag, record.message) + LogLevel.DEBUG -> Log.d(tag, record.message) + LogLevel.INFO -> Log.i(tag, record.message) + LogLevel.WARN -> Log.w(tag, record.message) + LogLevel.ERROR -> Log.e(tag, record.message) + } + } +} + +// 2. Register the logger +setCustomLogger(AndroidLogWriter()) +``` + +### Setup (Swift) + +```swift +import bitkitcore + +// 1. Implement LogWriter protocol +class ConsoleLogWriter: LogWriter { + func log(record: LogRecord) { + let message = "[\(record.level)] \(record.modulePath):\(record.line) - \(record.message)" + switch record.level { + case .error: + NSLog("❌ %@", message) + case .warn: + NSLog("⚠️ %@", message) + default: + NSLog("%@", message) + } + } +} + +// 2. Register the logger +setCustomLogger(logWriter: ConsoleLogWriter()) +``` + +### Log Levels + +- `TRACE`: Most verbose, detailed diagnostic information +- `DEBUG`: Debug information for development +- `INFO`: General informational messages +- `WARN`: Warnings about potentially problematic situations +- `ERROR`: Error messages for failures + ## Building the Bindings ### All Platforms @@ -480,4 +549,4 @@ cargo test modules::blocktank # Run tests for the Trezor module cargo test modules::trezor -``` \ No newline at end of file +``` diff --git a/bindings/android/gradle.properties b/bindings/android/gradle.properties index 0fd01d8..68da61b 100644 --- a/bindings/android/gradle.properties +++ b/bindings/android/gradle.properties @@ -3,4 +3,4 @@ android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official group=com.synonym -version=0.1.18 +version=0.1.20 diff --git a/src/lib.rs b/src/lib.rs index 8c23bf7..fdd8068 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,11 @@ uniffi::setup_scaffolding!(); +mod logger; mod modules; use once_cell::sync::OnceCell; -use std::sync::Mutex; -use thiserror::Error; +use std::sync::Arc; + pub use modules::scanner::{ Scanner, DecodingError @@ -12,10 +13,11 @@ pub use modules::scanner::{ pub use modules::lnurl; pub use modules::onchain; pub use modules::activity; -use crate::activity::{ActivityError, ActivityDB, OnchainActivity, LightningActivity, Activity, ActivityFilter, SortDirection, PaymentType, DbError}; +use crate::activity::{ActivityError, ActivityDB, Activity, ActivityFilter, SortDirection, PaymentType, DbError}; use crate::modules::blocktank::{BlocktankDB, BlocktankError, IBtInfo, IBtOrder, CreateOrderOptions, BtOrderState2, IBt0ConfMinTxFeeWindow, IBtEstimateFeeResponse, IBtEstimateFeeResponse2, CreateCjitOptions, ICJitEntry, CJitStateEnum, IBtBolt11Invoice, IGift}; use crate::onchain::{AddressError, ValidationResult, GetAddressResponse, Network, GetAddressesResponse}; pub use crate::onchain::WordCount; +pub use logger::{LogLevel, LogRecord, LogWriter}; use std::sync::Mutex as StdMutex; use tokio::runtime::Runtime; @@ -45,12 +47,25 @@ fn ensure_runtime() -> &'static Runtime { }) } +#[uniffi::export] +pub fn set_custom_logger(log_writer: Arc) { + logger::set_logger(log_writer); +} + #[uniffi::export] pub async fn decode(invoice: String) -> Result { + log_debug!(logger::get_logger(), "Decoding invoice: {}", invoice); let rt = ensure_runtime(); - rt.spawn(async move { + let result = rt.spawn(async move { Scanner::decode(invoice).await - }).await.unwrap() + }).await.unwrap(); + + match &result { + Ok(scanner) => log_info!(logger::get_logger(), "Successfully decoded invoice: {:?}", scanner), + Err(e) => log_error!(logger::get_logger(), "Failed to decode invoice: {:?}", e), + } + + result } #[uniffi::export] @@ -104,7 +119,7 @@ pub async fn lnurl_auth( ) -> Result { let mnemonic = Mnemonic::parse(&bip32_mnemonic) .map_err(|_| lnurl::LnurlError::AuthenticationFailed)?; - + let bitcoin_network = match network.unwrap_or(Network::Bitcoin) { Network::Bitcoin => BitcoinNetwork::Bitcoin, Network::Testnet => BitcoinNetwork::Testnet, @@ -112,28 +127,28 @@ pub async fn lnurl_auth( Network::Signet => BitcoinNetwork::Signet, Network::Regtest => BitcoinNetwork::Regtest, }; - + let seed = mnemonic.to_seed(bip39_passphrase.as_deref().unwrap_or("")); let root = Xpriv::new_master(bitcoin_network, &seed) .map_err(|_| lnurl::LnurlError::AuthenticationFailed)?; - + // Derive hashing key using m/138'/0 path (as per LUD-05) let hashing_path = bitcoin::bip32::DerivationPath::from_str("m/138'/0") .map_err(|_| lnurl::LnurlError::AuthenticationFailed)?; - + let secp = bitcoin::secp256k1::Secp256k1::new(); let hashing_key_xpriv = root.derive_priv(&secp, &hashing_path) .map_err(|_| lnurl::LnurlError::AuthenticationFailed)?; - + let hashing_key_bytes = hashing_key_xpriv.private_key.secret_bytes(); - + let params = lnurl::LnurlAuthParams { domain, k1, callback, hashing_key: hashing_key_bytes, }; - + let rt = ensure_runtime(); rt.spawn(async move { lnurl::lnurl_auth(params).await @@ -1244,4 +1259,4 @@ pub fn trezor_compose_transaction( Ok(result) => Ok(result), Err(e) => Err(TrezorConnectError::ClientError { error_details: e.to_string() }), } -} \ No newline at end of file +} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..889fc4a --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,163 @@ +//! Logging infrastructure for bitkit-core + +use std::sync::Arc; +use once_cell::sync::OnceCell; + +/// Global logger instance +static LOGGER: OnceCell> = OnceCell::new(); + +/// Get the global logger instance +pub(crate) fn get_logger() -> Option<&'static Arc> { + LOGGER.get() +} + +/// Set the global logger instance +/// +/// This should be called once during application initialization. +/// Subsequent calls will be ignored. +pub fn set_logger(log_writer: Arc) { + LOGGER.get_or_init(|| Arc::new(Logger::new_custom_writer(log_writer))); +} + +/// Log level for filtering log messages +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)] +pub enum LogLevel { + /// Trace-level logs (most verbose) + Trace, + /// Debug-level logs + Debug, + /// Info-level logs + Info, + /// Warning-level logs + Warn, + /// Error-level logs (least verbose) + Error, +} + +impl std::fmt::Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + LogLevel::Trace => write!(f, "TRACE"), + LogLevel::Debug => write!(f, "DEBUG"), + LogLevel::Info => write!(f, "INFO"), + LogLevel::Warn => write!(f, "WARN"), + LogLevel::Error => write!(f, "ERROR"), + } + } +} + +/// A log record containing metadata and the log message +#[derive(Debug, Clone, uniffi::Record)] +pub struct LogRecord { + /// The log level + pub level: LogLevel, + /// The formatted log message + pub message: String, + /// The module path where the log was generated + pub module_path: String, + /// The line number where the log was generated + pub line: u32, +} + +/// Trait for custom log writers that can be implemented by FFI consumers +/// +/// This trait should be implemented by Kotlin/Swift code to receive log messages +/// from the Rust library. When Rust code logs, it will call the `log` method +/// on the registered LogWriter implementation. +#[uniffi::export(with_foreign)] +pub trait LogWriter: Send + Sync { + /// Process a log record + fn log(&self, record: LogRecord); +} + +/// Internal logger implementation +pub(crate) struct Logger { + writer: Arc, +} + +impl Logger { + /// Create a new logger with a custom writer + pub fn new_custom_writer(log_writer: Arc) -> Self { + Self { writer: log_writer } + } + + /// Log a message + pub fn log(&self, record: LogRecord) { + self.writer.log(record); + } +} + +/// Macro for trace-level logging +#[macro_export] +macro_rules! log_trace { + ($logger:expr, $($arg:tt)*) => { + if let Some(logger) = $logger.as_ref() { + logger.log($crate::logger::LogRecord { + level: $crate::logger::LogLevel::Trace, + message: format!($($arg)*), + module_path: module_path!().to_string(), + line: line!(), + }); + } + }; +} + +/// Macro for debug-level logging +#[macro_export] +macro_rules! log_debug { + ($logger:expr, $($arg:tt)*) => { + if let Some(logger) = $logger.as_ref() { + logger.log($crate::logger::LogRecord { + level: $crate::logger::LogLevel::Debug, + message: format!($($arg)*), + module_path: module_path!().to_string(), + line: line!(), + }); + } + }; +} + +/// Macro for info-level logging +#[macro_export] +macro_rules! log_info { + ($logger:expr, $($arg:tt)*) => { + if let Some(logger) = $logger.as_ref() { + logger.log($crate::logger::LogRecord { + level: $crate::logger::LogLevel::Info, + message: format!($($arg)*), + module_path: module_path!().to_string(), + line: line!(), + }); + } + }; +} + +/// Macro for warning-level logging +#[macro_export] +macro_rules! log_warn { + ($logger:expr, $($arg:tt)*) => { + if let Some(logger) = $logger.as_ref() { + logger.log($crate::logger::LogRecord { + level: $crate::logger::LogLevel::Warn, + message: format!($($arg)*), + module_path: module_path!().to_string(), + line: line!(), + }); + } + }; +} + +/// Macro for error-level logging +#[macro_export] +macro_rules! log_error { + ($logger:expr, $($arg:tt)*) => { + if let Some(logger) = $logger.as_ref() { + logger.log($crate::logger::LogRecord { + level: $crate::logger::LogLevel::Error, + message: format!($($arg)*), + module_path: module_path!().to_string(), + line: line!(), + }); + } + }; +} diff --git a/src/modules/blocktank/api.rs b/src/modules/blocktank/api.rs index 8445605..22afc38 100644 --- a/src/modules/blocktank/api.rs +++ b/src/modules/blocktank/api.rs @@ -36,7 +36,7 @@ impl BlocktankDB { options ).await; - println!("Raw API response: {:#?}", response); + crate::log_debug!(crate::logger::get_logger(), "Raw API response: {:#?}", response); let order = response.map_err(|e| BlocktankError::DataError { error_details: format!("Failed to create order with Blocktank client: {}", e)