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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@ jobs:

# FFI bindings
-p cdk-ffi,

-p cdk-ffi --no-default-features,
-p cdk-ffi --no-default-features --features postgres,
-p cdk-ffi --no-default-features --features async-trait,
-p cdk-ffi --no-default-features --features "postgres async-trait",

# Binaries
--bin cdk-cli,
--bin cdk-cli --features sqlcipher,
Expand Down
9 changes: 6 additions & 3 deletions crates/cdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
uniffi = { version = "0.29", features = ["cli", "tokio"] }
uniffi-dart = { path = "/Users/david/Projects/uniffi-dart" }
url = { workspace = true }
uuid = { workspace = true, features = ["v4"] }


[features]
default = ["postgres"]
# Enable Postgres-backed wallet database support in FFI
postgres = ["cdk-postgres"]
default = ["async-trait", "postgres"]
# Enable async trait exports for custom database implementations in foreign languages
async-trait = []
# Enable Postgres-backed wallet database support in FFI (requires async-trait)
postgres = ["cdk-postgres", "async-trait"]

[dev-dependencies]

Expand Down
12 changes: 7 additions & 5 deletions crates/cdk-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ UniFFI bindings for the CDK (Cashu Development Kit), providing foreign function

## Supported Languages

- **🐍 Python** - With REPL integration for development
- **🍎 Swift** - iOS and macOS development
- **🎯 Kotlin** - Android and JVM development
- **Python** - With REPL integration for development
- **Swift** - iOS and macOS development
- **Kotlin** - Android and JVM development
- **Dart** - Flutter development

## Development Tasks

Expand All @@ -24,6 +25,7 @@ just ffi-clean # Clean build artifacts
just ffi-generate python
just ffi-generate swift
just ffi-generate kotlin
just ffi-generate dart

# Generate all languages
just ffi-generate-all
Expand Down Expand Up @@ -57,5 +59,5 @@ just ffi-dev-python
For production use, see language-specific repositories:

- [cdk-swift](https://github.com/cashubtc/cdk-swift) - iOS/macOS packages
- [cdk-kotlin](https://github.com/cashubtc/cdk-kotlin) - Android/JVM packages
- [cdk-python](https://github.com/cashubtc/cdk-python) - PyPI packages
- [cdk-kotlin](https://github.com/cashubtc/cdk-kotlin) - Android/JVM packages
- [cdk-dart](https://github.com/cashubtc/cdk-dart) - Flutter/Dart packages (coming soon)
35 changes: 34 additions & 1 deletion crates/cdk-ffi/src/bin/uniffi-bindgen.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
fn main() {
uniffi::uniffi_bindgen_main()
uniffi_bindgen()
}

fn uniffi_bindgen() {
// Manually parse command line arguments for language and library path
let args: Vec<String> = std::env::args().collect();
let language = args
.iter()
.position(|arg| arg == "--language")
.and_then(|idx| args.get(idx + 1));
let library_path = args
.iter()
.position(|arg| arg == "--library")
.and_then(|idx| args.get(idx + 1))
.expect("specify the library path with --library");
let output_dir = args
.iter()
.position(|arg| arg == "--out-dir")
.and_then(|idx| args.get(idx + 1))
.expect("--out-dir is required when using --library");

match language {
Some(lang) if lang == "dart" => {
uniffi_dart::gen::generate_dart_bindings(
"src/cdk_ffi.udl".into(),
None,
Some(output_dir.as_str().into()),
library_path.as_str().into(),
true,
)
.expect("Failed to generate dart bindings");
}
_ => uniffi::uniffi_bindgen_main(),
}
}
1 change: 1 addition & 0 deletions crates/cdk-ffi/src/cdk_ffi.udl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
namespace cdk_ffi {};
14 changes: 13 additions & 1 deletion crates/cdk-ffi/src/database.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
//! FFI Database bindings

use std::collections::HashMap;
#[cfg(feature = "async-trait")]
use std::sync::Arc;

#[cfg(feature = "async-trait")]
use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;

use crate::error::FfiError;
#[cfg(all(feature = "postgres", feature = "async-trait"))]
use crate::postgres::WalletPostgresDatabase;
#[cfg(feature = "async-trait")]
use crate::sqlite::WalletSqliteDatabase;
use crate::types::*;

/// FFI-compatible trait for wallet database operations
/// This trait mirrors the CDK WalletDatabase trait but uses FFI-compatible types
#[uniffi::export(with_foreign)]
#[cfg_attr(feature = "async-trait", uniffi::export(with_foreign))]
#[async_trait::async_trait]
pub trait WalletDatabase: Send + Sync {
// Mint Management
Expand Down Expand Up @@ -152,22 +156,26 @@ pub trait WalletDatabase: Send + Sync {

/// Internal bridge trait to convert from the FFI trait to the CDK database trait
/// This allows us to bridge between the UniFFI trait and the CDK's internal database trait
#[cfg(feature = "async-trait")]
struct WalletDatabaseBridge {
ffi_db: Arc<dyn WalletDatabase>,
}

#[cfg(feature = "async-trait")]
impl WalletDatabaseBridge {
fn new(ffi_db: Arc<dyn WalletDatabase>) -> Self {
Self { ffi_db }
}
}

#[cfg(feature = "async-trait")]
impl std::fmt::Debug for WalletDatabaseBridge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WalletDatabaseBridge")
}
}

#[cfg(feature = "async-trait")]
#[async_trait::async_trait]
impl CdkWalletDatabase for WalletDatabaseBridge {
type Err = cdk::cdk_database::Error;
Expand Down Expand Up @@ -583,6 +591,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
}

/// FFI-safe wallet database backend selection
#[cfg(feature = "async-trait")]
#[derive(uniffi::Enum)]
pub enum WalletDbBackend {
Sqlite {
Expand All @@ -595,13 +604,15 @@ pub enum WalletDbBackend {
}

/// Factory helpers returning a CDK wallet database behind the FFI trait
#[cfg(feature = "async-trait")]
#[uniffi::export]
pub fn create_wallet_db(backend: WalletDbBackend) -> Result<Arc<dyn WalletDatabase>, FfiError> {
match backend {
WalletDbBackend::Sqlite { path } => {
let sqlite = WalletSqliteDatabase::new(path)?;
Ok(sqlite as Arc<dyn WalletDatabase>)
}
#[cfg(feature = "postgres")]
WalletDbBackend::Postgres { url } => {
let pg = WalletPostgresDatabase::new(url)?;
Ok(pg as Arc<dyn WalletDatabase>)
Expand All @@ -610,6 +621,7 @@ pub fn create_wallet_db(backend: WalletDbBackend) -> Result<Arc<dyn WalletDataba
}

/// Helper function to create a CDK database from the FFI trait
#[cfg(feature = "async-trait")]
pub fn create_cdk_database_from_ffi(
ffi_db: Arc<dyn WalletDatabase>,
) -> Arc<dyn CdkWalletDatabase<Err = cdk::cdk_database::Error> + Send + Sync> {
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
pub mod database;
pub mod error;
pub mod multi_mint_wallet;
#[cfg(all(feature = "postgres", feature = "async-trait"))]
pub mod postgres;
pub mod sqlite;
pub mod token;
Expand Down
99 changes: 98 additions & 1 deletion crates/cdk-ffi/src/multi_mint_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ pub struct MultiMintWallet {
inner: Arc<CdkMultiMintWallet>,
}

#[uniffi::export(async_runtime = "tokio")]
// Conditional constructor implementations - async-trait enabled
#[cfg(feature = "async-trait")]
#[uniffi::export]
impl MultiMintWallet {
/// Create a new MultiMintWallet from mnemonic using WalletDatabase trait
#[uniffi::constructor]
Expand Down Expand Up @@ -105,6 +107,101 @@ impl MultiMintWallet {
inner: Arc::new(wallet),
})
}
}

// Conditional constructor implementations - async-trait disabled
#[cfg(not(feature = "async-trait"))]
#[uniffi::export]
impl MultiMintWallet {
/// Create a new MultiMintWallet from mnemonic using WalletSqliteDatabase
#[uniffi::constructor]
pub fn new(
unit: CurrencyUnit,
mnemonic: String,
db: Arc<crate::sqlite::WalletSqliteDatabase>,
) -> Result<Self, FfiError> {
// Parse mnemonic and generate seed without passphrase
let m = Mnemonic::parse(&mnemonic)
.map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
let seed = m.to_seed_normalized("");

// Convert the SQLite database to a CDK database implementation
let localstore: Arc<dyn cdk::cdk_database::WalletDatabase<Err = cdk::cdk_database::Error> + Send + Sync> =
db.inner.clone() as Arc<dyn cdk::cdk_database::WalletDatabase<Err = cdk::cdk_database::Error> + Send + Sync>;

let wallet = match tokio::runtime::Handle::try_current() {
Ok(handle) => tokio::task::block_in_place(|| {
handle.block_on(async move {
CdkMultiMintWallet::new(localstore, seed, unit.into()).await
})
}),
Err(_) => {
// No current runtime, create a new one
tokio::runtime::Runtime::new()
.map_err(|e| FfiError::Database {
msg: format!("Failed to create runtime: {}", e),
})?
.block_on(async move {
CdkMultiMintWallet::new(localstore, seed, unit.into()).await
})
}
}?;

Ok(Self {
inner: Arc::new(wallet),
})
}

/// Create a new MultiMintWallet with proxy configuration using WalletSqliteDatabase
#[uniffi::constructor]
pub fn new_with_proxy(
unit: CurrencyUnit,
mnemonic: String,
db: Arc<crate::sqlite::WalletSqliteDatabase>,
proxy_url: String,
) -> Result<Self, FfiError> {
// Parse mnemonic and generate seed without passphrase
let m = Mnemonic::parse(&mnemonic)
.map_err(|e| FfiError::InvalidMnemonic { msg: e.to_string() })?;
let seed = m.to_seed_normalized("");

// Convert the SQLite database to a CDK database implementation
let localstore: Arc<dyn cdk::cdk_database::WalletDatabase<Err = cdk::cdk_database::Error> + Send + Sync> =
db.inner.clone() as Arc<dyn cdk::cdk_database::WalletDatabase<Err = cdk::cdk_database::Error> + Send + Sync>;

// Parse proxy URL
let proxy_url =
url::Url::parse(&proxy_url).map_err(|e| FfiError::InvalidUrl { msg: e.to_string() })?;

let wallet = match tokio::runtime::Handle::try_current() {
Ok(handle) => tokio::task::block_in_place(|| {
handle.block_on(async move {
CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
.await
})
}),
Err(_) => {
// No current runtime, create a new one
tokio::runtime::Runtime::new()
.map_err(|e| FfiError::Database {
msg: format!("Failed to create runtime: {}", e),
})?
.block_on(async move {
CdkMultiMintWallet::new_with_proxy(localstore, seed, unit.into(), proxy_url)
.await
})
}
}?;

Ok(Self {
inner: Arc::new(wallet),
})
}
}

// Methods impl block (shared between both feature configurations)
#[uniffi::export(async_runtime = "tokio")]
impl MultiMintWallet {

/// Get the currency unit for this wallet
pub fn unit(&self) -> CurrencyUnit {
Expand Down
11 changes: 6 additions & 5 deletions crates/cdk-ffi/src/postgres.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
//! Postgres database backend for FFI wallet
//! This module is only available with both the "postgres" and "async-trait" features enabled.

#![cfg(all(feature = "postgres", feature = "async-trait"))]

use std::collections::HashMap;
use std::sync::Arc;

// Bring the CDK wallet database trait into scope so trait methods resolve on the inner DB
use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
#[cfg(feature = "postgres")]
use cdk_postgres::WalletPgDatabase as CdkWalletPgDatabase;

use crate::{
Expand All @@ -20,11 +24,9 @@ pub struct WalletPostgresDatabase {
// Keep a long-lived Tokio runtime for Postgres-created resources so that
// background tasks (e.g., tokio-postgres connection drivers spawned during
// construction) are not tied to a short-lived, ad-hoc runtime.
#[cfg(feature = "postgres")]
static PG_RUNTIME: once_cell::sync::OnceCell<tokio::runtime::Runtime> =
once_cell::sync::OnceCell::new();

#[cfg(feature = "postgres")]
fn pg_runtime() -> &'static tokio::runtime::Runtime {
PG_RUNTIME.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
Expand Down Expand Up @@ -385,10 +387,9 @@ impl WalletDatabase for WalletPostgresDatabase {
#[uniffi::export]
impl WalletPostgresDatabase {
/// Create a new Postgres-backed wallet database
/// Requires cdk-ffi to be built with feature "postgres".
/// Requires cdk-ffi to be built with features "postgres" and "async-trait".
/// Example URL:
/// "host=localhost user=test password=test dbname=testdb port=5433 schema=wallet sslmode=prefer"
#[cfg(feature = "postgres")]
#[uniffi::constructor]
pub fn new(url: String) -> Result<Arc<Self>, FfiError> {
let inner = match tokio::runtime::Handle::try_current() {
Expand Down
2 changes: 1 addition & 1 deletion crates/cdk-ffi/src/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
/// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabase trait
#[derive(uniffi::Object)]
pub struct WalletSqliteDatabase {
inner: Arc<CdkWalletSqliteDatabase>,
pub(crate) inner: Arc<CdkWalletSqliteDatabase>,
}
use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;

Expand Down
Loading
Loading