Skip to content

Commit a84adf2

Browse files
committed
feat: logger for bindings consumers
1 parent e8cc3ab commit a84adf2

File tree

7 files changed

+265
-18
lines changed

7 files changed

+265
-18
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "bitkitcore"
3-
version = "0.1.17"
3+
version = "0.1.20"
44
edition = "2021"
55

66
[lib]

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
- Derive addresses for specified paths
3131
- Retrieve account information
3232
- Handle responses from Trezor devices
33+
- Logging
34+
- Callback-based logging allowing Kotlin/Swift to intercept Rust logs
35+
- Multiple log levels (Trace, Debug, Info, Warn, Error)
3336

3437
## Available Modules: Methods
3538
- Scanner
@@ -437,6 +440,72 @@
437440
) -> Result<DeepLinkResult, TrezorConnectError>
438441
```
439442

443+
- Logging
444+
- [set_custom_logger](#logging-usage): Register a custom logger for library consumers
445+
```rust
446+
fn set_custom_logger(log_writer: Arc<dyn LogWriter>)
447+
```
448+
449+
## Logging Usage
450+
451+
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.
452+
453+
### Setup (Kotlin)
454+
455+
```kotlin
456+
import com.synonym.bitkitcore.*
457+
import android.util.Log
458+
459+
// 1. Implement LogWriter interface
460+
class AndroidLogWriter : LogWriter {
461+
override fun log(record: LogRecord) {
462+
val tag = "BitkitCore::${record.modulePath}"
463+
when (record.level) {
464+
LogLevel.TRACE -> Log.v(tag, record.message)
465+
LogLevel.DEBUG -> Log.d(tag, record.message)
466+
LogLevel.INFO -> Log.i(tag, record.message)
467+
LogLevel.WARN -> Log.w(tag, record.message)
468+
LogLevel.ERROR -> Log.e(tag, record.message)
469+
}
470+
}
471+
}
472+
473+
// 2. Register the logger
474+
setCustomLogger(AndroidLogWriter())
475+
```
476+
477+
### Setup (Swift)
478+
479+
```swift
480+
import bitkitcore
481+
482+
// 1. Implement LogWriter protocol
483+
class ConsoleLogWriter: LogWriter {
484+
func log(record: LogRecord) {
485+
let message = "[\(record.level)] \(record.modulePath):\(record.line) - \(record.message)"
486+
switch record.level {
487+
case .error:
488+
NSLog("❌ %@", message)
489+
case .warn:
490+
NSLog("⚠️ %@", message)
491+
default:
492+
NSLog("%@", message)
493+
}
494+
}
495+
}
496+
497+
// 2. Register the logger
498+
setCustomLogger(logWriter: ConsoleLogWriter())
499+
```
500+
501+
### Log Levels
502+
503+
- `TRACE`: Most verbose, detailed diagnostic information
504+
- `DEBUG`: Debug information for development
505+
- `INFO`: General informational messages
506+
- `WARN`: Warnings about potentially problematic situations
507+
- `ERROR`: Error messages for failures
508+
440509
## Building the Bindings
441510

442511
### All Platforms
@@ -480,4 +549,4 @@ cargo test modules::blocktank
480549
481550
# Run tests for the Trezor module
482551
cargo test modules::trezor
483-
```
552+
```

bindings/android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ android.useAndroidX=true
33
android.enableJetifier=true
44
kotlin.code.style=official
55
group=com.synonym
6-
version=0.1.17
6+
version=0.1.20

src/lib.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
uniffi::setup_scaffolding!();
22

3+
mod logger;
34
mod modules;
45

56
use once_cell::sync::OnceCell;
6-
use std::sync::Mutex;
7-
use thiserror::Error;
7+
use std::sync::Arc;
8+
89
pub use modules::scanner::{
910
Scanner,
1011
DecodingError
1112
};
1213
pub use modules::lnurl;
1314
pub use modules::onchain;
1415
pub use modules::activity;
15-
use crate::activity::{ActivityError, ActivityDB, OnchainActivity, LightningActivity, Activity, ActivityFilter, SortDirection, PaymentType, DbError};
16+
use crate::activity::{ActivityError, ActivityDB, Activity, ActivityFilter, SortDirection, PaymentType, DbError};
1617
use crate::modules::blocktank::{BlocktankDB, BlocktankError, IBtInfo, IBtOrder, CreateOrderOptions, BtOrderState2, IBt0ConfMinTxFeeWindow, IBtEstimateFeeResponse, IBtEstimateFeeResponse2, CreateCjitOptions, ICJitEntry, CJitStateEnum, IBtBolt11Invoice, IGift};
1718
use crate::onchain::{AddressError, ValidationResult, GetAddressResponse, Network, GetAddressesResponse};
1819
pub use crate::onchain::WordCount;
20+
pub use logger::{LogLevel, LogRecord, LogWriter};
1921

2022
use std::sync::Mutex as StdMutex;
2123
use tokio::runtime::Runtime;
@@ -45,12 +47,25 @@ fn ensure_runtime() -> &'static Runtime {
4547
})
4648
}
4749

50+
#[uniffi::export]
51+
pub fn set_custom_logger(log_writer: Arc<dyn LogWriter>) {
52+
logger::set_logger(log_writer);
53+
}
54+
4855
#[uniffi::export]
4956
pub async fn decode(invoice: String) -> Result<Scanner, DecodingError> {
57+
log_debug!(logger::get_logger(), "Decoding invoice: {}", invoice);
5058
let rt = ensure_runtime();
51-
rt.spawn(async move {
59+
let result = rt.spawn(async move {
5260
Scanner::decode(invoice).await
53-
}).await.unwrap()
61+
}).await.unwrap();
62+
63+
match &result {
64+
Ok(scanner) => log_info!(logger::get_logger(), "Successfully decoded invoice: {:?}", scanner),
65+
Err(e) => log_error!(logger::get_logger(), "Failed to decode invoice: {:?}", e),
66+
}
67+
68+
result
5469
}
5570

5671
#[uniffi::export]
@@ -104,36 +119,36 @@ pub async fn lnurl_auth(
104119
) -> Result<String, lnurl::LnurlError> {
105120
let mnemonic = Mnemonic::parse(&bip32_mnemonic)
106121
.map_err(|_| lnurl::LnurlError::AuthenticationFailed)?;
107-
122+
108123
let bitcoin_network = match network.unwrap_or(Network::Bitcoin) {
109124
Network::Bitcoin => BitcoinNetwork::Bitcoin,
110125
Network::Testnet => BitcoinNetwork::Testnet,
111126
Network::Testnet4 => BitcoinNetwork::Testnet,
112127
Network::Signet => BitcoinNetwork::Signet,
113128
Network::Regtest => BitcoinNetwork::Regtest,
114129
};
115-
130+
116131
let seed = mnemonic.to_seed(bip39_passphrase.as_deref().unwrap_or(""));
117132
let root = Xpriv::new_master(bitcoin_network, &seed)
118133
.map_err(|_| lnurl::LnurlError::AuthenticationFailed)?;
119-
134+
120135
// Derive hashing key using m/138'/0 path (as per LUD-05)
121136
let hashing_path = bitcoin::bip32::DerivationPath::from_str("m/138'/0")
122137
.map_err(|_| lnurl::LnurlError::AuthenticationFailed)?;
123-
138+
124139
let secp = bitcoin::secp256k1::Secp256k1::new();
125140
let hashing_key_xpriv = root.derive_priv(&secp, &hashing_path)
126141
.map_err(|_| lnurl::LnurlError::AuthenticationFailed)?;
127-
142+
128143
let hashing_key_bytes = hashing_key_xpriv.private_key.secret_bytes();
129-
144+
130145
let params = lnurl::LnurlAuthParams {
131146
domain,
132147
k1,
133148
callback,
134149
hashing_key: hashing_key_bytes,
135150
};
136-
151+
137152
let rt = ensure_runtime();
138153
rt.spawn(async move {
139154
lnurl::lnurl_auth(params).await
@@ -1244,4 +1259,4 @@ pub fn trezor_compose_transaction(
12441259
Ok(result) => Ok(result),
12451260
Err(e) => Err(TrezorConnectError::ClientError { error_details: e.to_string() }),
12461261
}
1247-
}
1262+
}

src/logger.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//! Logging infrastructure for bitkit-core
2+
3+
use std::sync::Arc;
4+
use once_cell::sync::OnceCell;
5+
6+
/// Global logger instance
7+
static LOGGER: OnceCell<Arc<Logger>> = OnceCell::new();
8+
9+
/// Get the global logger instance
10+
pub(crate) fn get_logger() -> Option<&'static Arc<Logger>> {
11+
LOGGER.get()
12+
}
13+
14+
/// Set the global logger instance
15+
///
16+
/// This should be called once during application initialization.
17+
/// Subsequent calls will be ignored.
18+
pub fn set_logger(log_writer: Arc<dyn LogWriter>) {
19+
LOGGER.get_or_init(|| Arc::new(Logger::new_custom_writer(log_writer)));
20+
}
21+
22+
/// Log level for filtering log messages
23+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)]
24+
pub enum LogLevel {
25+
/// Trace-level logs (most verbose)
26+
Trace,
27+
/// Debug-level logs
28+
Debug,
29+
/// Info-level logs
30+
Info,
31+
/// Warning-level logs
32+
Warn,
33+
/// Error-level logs (least verbose)
34+
Error,
35+
}
36+
37+
impl std::fmt::Display for LogLevel {
38+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
39+
match self {
40+
LogLevel::Trace => write!(f, "TRACE"),
41+
LogLevel::Debug => write!(f, "DEBUG"),
42+
LogLevel::Info => write!(f, "INFO"),
43+
LogLevel::Warn => write!(f, "WARN"),
44+
LogLevel::Error => write!(f, "ERROR"),
45+
}
46+
}
47+
}
48+
49+
/// A log record containing metadata and the log message
50+
#[derive(Debug, Clone, uniffi::Record)]
51+
pub struct LogRecord {
52+
/// The log level
53+
pub level: LogLevel,
54+
/// The formatted log message
55+
pub message: String,
56+
/// The module path where the log was generated
57+
pub module_path: String,
58+
/// The line number where the log was generated
59+
pub line: u32,
60+
}
61+
62+
/// Trait for custom log writers that can be implemented by FFI consumers
63+
///
64+
/// This trait should be implemented by Kotlin/Swift code to receive log messages
65+
/// from the Rust library. When Rust code logs, it will call the `log` method
66+
/// on the registered LogWriter implementation.
67+
#[uniffi::export(with_foreign)]
68+
pub trait LogWriter: Send + Sync {
69+
/// Process a log record
70+
fn log(&self, record: LogRecord);
71+
}
72+
73+
/// Internal logger implementation
74+
pub(crate) struct Logger {
75+
writer: Arc<dyn LogWriter>,
76+
}
77+
78+
impl Logger {
79+
/// Create a new logger with a custom writer
80+
pub fn new_custom_writer(log_writer: Arc<dyn LogWriter>) -> Self {
81+
Self { writer: log_writer }
82+
}
83+
84+
/// Log a message
85+
pub fn log(&self, record: LogRecord) {
86+
self.writer.log(record);
87+
}
88+
}
89+
90+
/// Macro for trace-level logging
91+
#[macro_export]
92+
macro_rules! log_trace {
93+
($logger:expr, $($arg:tt)*) => {
94+
if let Some(logger) = $logger.as_ref() {
95+
logger.log($crate::logger::LogRecord {
96+
level: $crate::logger::LogLevel::Trace,
97+
message: format!($($arg)*),
98+
module_path: module_path!().to_string(),
99+
line: line!(),
100+
});
101+
}
102+
};
103+
}
104+
105+
/// Macro for debug-level logging
106+
#[macro_export]
107+
macro_rules! log_debug {
108+
($logger:expr, $($arg:tt)*) => {
109+
if let Some(logger) = $logger.as_ref() {
110+
logger.log($crate::logger::LogRecord {
111+
level: $crate::logger::LogLevel::Debug,
112+
message: format!($($arg)*),
113+
module_path: module_path!().to_string(),
114+
line: line!(),
115+
});
116+
}
117+
};
118+
}
119+
120+
/// Macro for info-level logging
121+
#[macro_export]
122+
macro_rules! log_info {
123+
($logger:expr, $($arg:tt)*) => {
124+
if let Some(logger) = $logger.as_ref() {
125+
logger.log($crate::logger::LogRecord {
126+
level: $crate::logger::LogLevel::Info,
127+
message: format!($($arg)*),
128+
module_path: module_path!().to_string(),
129+
line: line!(),
130+
});
131+
}
132+
};
133+
}
134+
135+
/// Macro for warning-level logging
136+
#[macro_export]
137+
macro_rules! log_warn {
138+
($logger:expr, $($arg:tt)*) => {
139+
if let Some(logger) = $logger.as_ref() {
140+
logger.log($crate::logger::LogRecord {
141+
level: $crate::logger::LogLevel::Warn,
142+
message: format!($($arg)*),
143+
module_path: module_path!().to_string(),
144+
line: line!(),
145+
});
146+
}
147+
};
148+
}
149+
150+
/// Macro for error-level logging
151+
#[macro_export]
152+
macro_rules! log_error {
153+
($logger:expr, $($arg:tt)*) => {
154+
if let Some(logger) = $logger.as_ref() {
155+
logger.log($crate::logger::LogRecord {
156+
level: $crate::logger::LogLevel::Error,
157+
message: format!($($arg)*),
158+
module_path: module_path!().to_string(),
159+
line: line!(),
160+
});
161+
}
162+
};
163+
}

src/modules/blocktank/api.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl BlocktankDB {
3636
options
3737
).await;
3838

39-
println!("Raw API response: {:#?}", response);
39+
crate::log_debug!(crate::logger::get_logger(), "Raw API response: {:#?}", response);
4040

4141
let order = response.map_err(|e| BlocktankError::DataError {
4242
error_details: format!("Failed to create order with Blocktank client: {}", e)

0 commit comments

Comments
 (0)