Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bitkitcore"
version = "0.1.18"
version = "0.1.20"
edition = "2021"

[lib]
Expand Down
71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -437,6 +440,72 @@
) -> Result<DeepLinkResult, TrezorConnectError>
```

- Logging
- [set_custom_logger](#logging-usage): Register a custom logger for library consumers
```rust
fn set_custom_logger(log_writer: Arc<dyn LogWriter>)
```

## 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
Expand Down Expand Up @@ -480,4 +549,4 @@ cargo test modules::blocktank

# Run tests for the Trezor module
cargo test modules::trezor
```
```
2 changes: 1 addition & 1 deletion bindings/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 28 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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
};
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;
Expand Down Expand Up @@ -45,12 +47,25 @@ fn ensure_runtime() -> &'static Runtime {
})
}

#[uniffi::export]
pub fn set_custom_logger(log_writer: Arc<dyn LogWriter>) {
logger::set_logger(log_writer);
}

#[uniffi::export]
pub async fn decode(invoice: String) -> Result<Scanner, DecodingError> {
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]
Expand Down Expand Up @@ -104,36 +119,36 @@ pub async fn lnurl_auth(
) -> Result<String, lnurl::LnurlError> {
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,
Network::Testnet4 => BitcoinNetwork::Testnet,
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
Expand Down Expand Up @@ -1244,4 +1259,4 @@ pub fn trezor_compose_transaction(
Ok(result) => Ok(result),
Err(e) => Err(TrezorConnectError::ClientError { error_details: e.to_string() }),
}
}
}
163 changes: 163 additions & 0 deletions src/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Logging infrastructure for bitkit-core

use std::sync::Arc;
use once_cell::sync::OnceCell;

/// Global logger instance
static LOGGER: OnceCell<Arc<Logger>> = OnceCell::new();

/// Get the global logger instance
pub(crate) fn get_logger() -> Option<&'static Arc<Logger>> {
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<dyn LogWriter>) {
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<dyn LogWriter>,
}

impl Logger {
/// Create a new logger with a custom writer
pub fn new_custom_writer(log_writer: Arc<dyn LogWriter>) -> 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!(),
});
}
};
}
2 changes: 1 addition & 1 deletion src/modules/blocktank/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down