mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-18 21:25:09 +01:00
@@ -17,6 +17,7 @@ async-trait = { workspace = true }
|
|||||||
bip39 = { workspace = true }
|
bip39 = { workspace = true }
|
||||||
cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "bip353"] }
|
cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "bip353"] }
|
||||||
cdk-sqlite = { workspace = true }
|
cdk-sqlite = { workspace = true }
|
||||||
|
cdk-postgres = { workspace = true, optional = true }
|
||||||
ctor = "0.2"
|
ctor = "0.2"
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
@@ -30,6 +31,11 @@ url = { workspace = true }
|
|||||||
uuid = { workspace = true, features = ["v4"] }
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
|
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["postgres"]
|
||||||
|
# Enable Postgres-backed wallet database support in FFI
|
||||||
|
postgres = ["cdk-postgres"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
|
use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
|
||||||
use cdk_sqlite::wallet::WalletSqliteDatabase as CdkWalletSqliteDatabase;
|
|
||||||
|
|
||||||
use crate::error::FfiError;
|
use crate::error::FfiError;
|
||||||
|
use crate::postgres::WalletPostgresDatabase;
|
||||||
|
use crate::sqlite::WalletSqliteDatabase;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
|
||||||
/// FFI-compatible trait for wallet database operations
|
/// FFI-compatible trait for wallet database operations
|
||||||
@@ -171,7 +172,6 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
|
|||||||
) -> Result<(), Self::Err> {
|
) -> Result<(), Self::Err> {
|
||||||
let ffi_mint_url = mint_url.into();
|
let ffi_mint_url = mint_url.into();
|
||||||
let ffi_mint_info = mint_info.map(Into::into);
|
let ffi_mint_info = mint_info.map(Into::into);
|
||||||
|
|
||||||
self.ffi_db
|
self.ffi_db
|
||||||
.add_mint(ffi_mint_url, ffi_mint_info)
|
.add_mint(ffi_mint_url, ffi_mint_info)
|
||||||
.await
|
.await
|
||||||
@@ -556,394 +556,30 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabase trait
|
/// FFI-safe wallet database backend selection
|
||||||
#[derive(uniffi::Object)]
|
#[derive(uniffi::Enum)]
|
||||||
pub struct WalletSqliteDatabase {
|
pub enum WalletDbBackend {
|
||||||
inner: Arc<CdkWalletSqliteDatabase>,
|
Sqlite {
|
||||||
}
|
path: String,
|
||||||
|
},
|
||||||
impl WalletSqliteDatabase {
|
#[cfg(feature = "postgres")]
|
||||||
// No additional methods needed beyond the trait implementation
|
Postgres {
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Factory helpers returning a CDK wallet database behind the FFI trait
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
impl WalletSqliteDatabase {
|
pub fn create_wallet_db(backend: WalletDbBackend) -> Result<Arc<dyn WalletDatabase>, FfiError> {
|
||||||
/// Create a new WalletSqliteDatabase with the given work directory
|
match backend {
|
||||||
#[uniffi::constructor]
|
WalletDbBackend::Sqlite { path } => {
|
||||||
pub fn new(file_path: String) -> Result<Arc<Self>, FfiError> {
|
let sqlite = WalletSqliteDatabase::new(path)?;
|
||||||
let db = match tokio::runtime::Handle::try_current() {
|
Ok(sqlite as Arc<dyn WalletDatabase>)
|
||||||
Ok(handle) => tokio::task::block_in_place(|| {
|
|
||||||
handle
|
|
||||||
.block_on(async move { CdkWalletSqliteDatabase::new(file_path.as_str()).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 { CdkWalletSqliteDatabase::new(file_path.as_str()).await })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
WalletDbBackend::Postgres { url } => {
|
||||||
Ok(Arc::new(Self {
|
let pg = WalletPostgresDatabase::new(url)?;
|
||||||
inner: Arc::new(db),
|
Ok(pg as Arc<dyn WalletDatabase>)
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an in-memory database
|
|
||||||
#[uniffi::constructor]
|
|
||||||
pub fn new_in_memory() -> Result<Arc<Self>, FfiError> {
|
|
||||||
let db = match tokio::runtime::Handle::try_current() {
|
|
||||||
Ok(handle) => tokio::task::block_in_place(|| {
|
|
||||||
handle.block_on(async move { cdk_sqlite::wallet::memory::empty().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 { cdk_sqlite::wallet::memory::empty().await })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(Arc::new(Self {
|
|
||||||
inner: Arc::new(db),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[uniffi::export(async_runtime = "tokio")]
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl WalletDatabase for WalletSqliteDatabase {
|
|
||||||
// Mint Management
|
|
||||||
async fn add_mint(
|
|
||||||
&self,
|
|
||||||
mint_url: MintUrl,
|
|
||||||
mint_info: Option<MintInfo>,
|
|
||||||
) -> Result<(), FfiError> {
|
|
||||||
let cdk_mint_url = mint_url.try_into()?;
|
|
||||||
let cdk_mint_info = mint_info.map(Into::into);
|
|
||||||
self.inner
|
|
||||||
.add_mint(cdk_mint_url, cdk_mint_info)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
|
|
||||||
let cdk_mint_url = mint_url.try_into()?;
|
|
||||||
self.inner
|
|
||||||
.remove_mint(cdk_mint_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
|
|
||||||
let cdk_mint_url = mint_url.try_into()?;
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_mint(cdk_mint_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.map(Into::into))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_mints()
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result
|
|
||||||
.into_iter()
|
|
||||||
.map(|(k, v)| (k.into(), v.map(Into::into)))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_mint_url(
|
|
||||||
&self,
|
|
||||||
old_mint_url: MintUrl,
|
|
||||||
new_mint_url: MintUrl,
|
|
||||||
) -> Result<(), FfiError> {
|
|
||||||
let cdk_old_mint_url = old_mint_url.try_into()?;
|
|
||||||
let cdk_new_mint_url = new_mint_url.try_into()?;
|
|
||||||
self.inner
|
|
||||||
.update_mint_url(cdk_old_mint_url, cdk_new_mint_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyset Management
|
|
||||||
async fn add_mint_keysets(
|
|
||||||
&self,
|
|
||||||
mint_url: MintUrl,
|
|
||||||
keysets: Vec<KeySetInfo>,
|
|
||||||
) -> Result<(), FfiError> {
|
|
||||||
let cdk_mint_url = mint_url.try_into()?;
|
|
||||||
let cdk_keysets: Vec<cdk::nuts::KeySetInfo> = keysets.into_iter().map(Into::into).collect();
|
|
||||||
self.inner
|
|
||||||
.add_mint_keysets(cdk_mint_url, cdk_keysets)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_mint_keysets(
|
|
||||||
&self,
|
|
||||||
mint_url: MintUrl,
|
|
||||||
) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
|
|
||||||
let cdk_mint_url = mint_url.try_into()?;
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_mint_keysets(cdk_mint_url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
|
|
||||||
let cdk_id = keyset_id.into();
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_keyset_by_id(&cdk_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.map(Into::into))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mint Quote Management
|
|
||||||
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
|
|
||||||
let cdk_quote = quote.try_into()?;
|
|
||||||
self.inner
|
|
||||||
.add_mint_quote(cdk_quote)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_mint_quote("e_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.map(|q| q.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_mint_quotes()
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.into_iter().map(|q| q.into()).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
|
|
||||||
self.inner
|
|
||||||
.remove_mint_quote("e_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Melt Quote Management
|
|
||||||
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
|
|
||||||
let cdk_quote = quote.try_into()?;
|
|
||||||
self.inner
|
|
||||||
.add_melt_quote(cdk_quote)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_melt_quote("e_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.map(|q| q.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_melt_quotes()
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.into_iter().map(|q| q.into()).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
|
|
||||||
self.inner
|
|
||||||
.remove_melt_quote("e_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys Management
|
|
||||||
async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
|
|
||||||
// Convert FFI KeySet to cdk::nuts::KeySet
|
|
||||||
let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?;
|
|
||||||
self.inner
|
|
||||||
.add_keys(cdk_keyset)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
|
|
||||||
let cdk_id = id.into();
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_keys(&cdk_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.map(Into::into))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
|
|
||||||
let cdk_id = id.into();
|
|
||||||
self.inner
|
|
||||||
.remove_keys(&cdk_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proof Management
|
|
||||||
async fn update_proofs(
|
|
||||||
&self,
|
|
||||||
added: Vec<ProofInfo>,
|
|
||||||
removed_ys: Vec<PublicKey>,
|
|
||||||
) -> Result<(), FfiError> {
|
|
||||||
// Convert FFI types to CDK types
|
|
||||||
let cdk_added: Result<Vec<cdk::types::ProofInfo>, FfiError> = added
|
|
||||||
.into_iter()
|
|
||||||
.map(|info| {
|
|
||||||
Ok::<cdk::types::ProofInfo, FfiError>(cdk::types::ProofInfo {
|
|
||||||
proof: info.proof.inner.clone(),
|
|
||||||
y: info.y.try_into()?,
|
|
||||||
mint_url: info.mint_url.try_into()?,
|
|
||||||
state: info.state.into(),
|
|
||||||
spending_condition: info
|
|
||||||
.spending_condition
|
|
||||||
.map(|sc| sc.try_into())
|
|
||||||
.transpose()?,
|
|
||||||
unit: info.unit.into(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let cdk_added = cdk_added?;
|
|
||||||
|
|
||||||
let cdk_removed_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
|
|
||||||
removed_ys.into_iter().map(|pk| pk.try_into()).collect();
|
|
||||||
let cdk_removed_ys = cdk_removed_ys?;
|
|
||||||
|
|
||||||
self.inner
|
|
||||||
.update_proofs(cdk_added, cdk_removed_ys)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_proofs(
|
|
||||||
&self,
|
|
||||||
mint_url: Option<MintUrl>,
|
|
||||||
unit: Option<CurrencyUnit>,
|
|
||||||
state: Option<Vec<ProofState>>,
|
|
||||||
spending_conditions: Option<Vec<SpendingConditions>>,
|
|
||||||
) -> Result<Vec<ProofInfo>, FfiError> {
|
|
||||||
let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
|
|
||||||
let cdk_unit = unit.map(Into::into);
|
|
||||||
let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
|
|
||||||
let cdk_spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>> =
|
|
||||||
spending_conditions
|
|
||||||
.map(|sc| {
|
|
||||||
sc.into_iter()
|
|
||||||
.map(|c| c.try_into())
|
|
||||||
.collect::<Result<Vec<_>, FfiError>>()
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
|
|
||||||
Ok(result.into_iter().map(Into::into).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_proofs_state(
|
|
||||||
&self,
|
|
||||||
ys: Vec<PublicKey>,
|
|
||||||
state: ProofState,
|
|
||||||
) -> Result<(), FfiError> {
|
|
||||||
let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
|
|
||||||
ys.into_iter().map(|pk| pk.try_into()).collect();
|
|
||||||
let cdk_ys = cdk_ys?;
|
|
||||||
let cdk_state = state.into();
|
|
||||||
|
|
||||||
self.inner
|
|
||||||
.update_proofs_state(cdk_ys, cdk_state)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyset Counter Management
|
|
||||||
async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
|
|
||||||
let cdk_id = keyset_id.into();
|
|
||||||
self.inner
|
|
||||||
.increment_keyset_counter(&cdk_id, count)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction Management
|
|
||||||
async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
|
|
||||||
// Convert FFI Transaction to CDK Transaction using TryFrom
|
|
||||||
let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?;
|
|
||||||
|
|
||||||
self.inner
|
|
||||||
.add_transaction(cdk_transaction)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_transaction(
|
|
||||||
&self,
|
|
||||||
transaction_id: TransactionId,
|
|
||||||
) -> Result<Option<Transaction>, FfiError> {
|
|
||||||
let cdk_id = transaction_id.try_into()?;
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.get_transaction(cdk_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
Ok(result.map(Into::into))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_transactions(
|
|
||||||
&self,
|
|
||||||
mint_url: Option<MintUrl>,
|
|
||||||
direction: Option<TransactionDirection>,
|
|
||||||
unit: Option<CurrencyUnit>,
|
|
||||||
) -> Result<Vec<Transaction>, FfiError> {
|
|
||||||
let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
|
|
||||||
let cdk_direction = direction.map(Into::into);
|
|
||||||
let cdk_unit = unit.map(Into::into);
|
|
||||||
|
|
||||||
let result = self
|
|
||||||
.inner
|
|
||||||
.list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
|
||||||
|
|
||||||
Ok(result.into_iter().map(Into::into).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
|
|
||||||
let cdk_id = transaction_id.try_into()?;
|
|
||||||
self.inner
|
|
||||||
.remove_transaction(cdk_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod multi_mint_wallet;
|
pub mod multi_mint_wallet;
|
||||||
|
pub mod postgres;
|
||||||
|
pub mod sqlite;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
|
|||||||
401
crates/cdk-ffi/src/postgres.rs
Normal file
401
crates/cdk-ffi/src/postgres.rs
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
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::{
|
||||||
|
CurrencyUnit, FfiError, Id, KeySet, KeySetInfo, Keys, MeltQuote, MintInfo, MintQuote, MintUrl,
|
||||||
|
ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction, TransactionDirection,
|
||||||
|
TransactionId, WalletDatabase,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(uniffi::Object)]
|
||||||
|
pub struct WalletPostgresDatabase {
|
||||||
|
inner: Arc<CdkWalletPgDatabase>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
.enable_all()
|
||||||
|
.thread_name("cdk-ffi-pg")
|
||||||
|
.build()
|
||||||
|
.expect("failed to build pg runtime")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the local WalletDatabase trait (simple trait path required by uniffi)
|
||||||
|
#[uniffi::export(async_runtime = "tokio")]
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl WalletDatabase for WalletPostgresDatabase {
|
||||||
|
// Forward all trait methods to inner CDK database via the bridge adapter
|
||||||
|
async fn add_mint(
|
||||||
|
&self,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
mint_info: Option<MintInfo>,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
let cdk_mint_info = mint_info.map(Into::into);
|
||||||
|
println!("adding new mint");
|
||||||
|
self.inner
|
||||||
|
.add_mint(cdk_mint_url, cdk_mint_info)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
println!("ffi error {:?}", e);
|
||||||
|
FfiError::Database { msg: e.to_string() }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.remove_mint(cdk_mint_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mint(cdk_mint_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(Into::into))
|
||||||
|
}
|
||||||
|
async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mints()
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k.into(), v.map(Into::into)))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
async fn update_mint_url(
|
||||||
|
&self,
|
||||||
|
old_mint_url: MintUrl,
|
||||||
|
new_mint_url: MintUrl,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
let cdk_old_mint_url = old_mint_url.try_into()?;
|
||||||
|
let cdk_new_mint_url = new_mint_url.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.update_mint_url(cdk_old_mint_url, cdk_new_mint_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
async fn add_mint_keysets(
|
||||||
|
&self,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
keysets: Vec<KeySetInfo>,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
let cdk_keysets: Vec<cdk::nuts::KeySetInfo> = keysets.into_iter().map(Into::into).collect();
|
||||||
|
self.inner
|
||||||
|
.add_mint_keysets(cdk_mint_url, cdk_keysets)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
async fn get_mint_keysets(
|
||||||
|
&self,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mint_keysets(cdk_mint_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
|
||||||
|
let cdk_id = keyset_id.into();
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_keyset_by_id(&cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mint Quote Management
|
||||||
|
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
|
||||||
|
let cdk_quote = quote.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.add_mint_quote(cdk_quote)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mint_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(|q| q.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mint_quotes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.into_iter().map(|q| q.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
|
||||||
|
self.inner
|
||||||
|
.remove_mint_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Melt Quote Management
|
||||||
|
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
|
||||||
|
let cdk_quote = quote.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.add_melt_quote(cdk_quote)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_melt_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(|q| q.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_melt_quotes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.into_iter().map(|q| q.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
|
||||||
|
self.inner
|
||||||
|
.remove_melt_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys Management
|
||||||
|
async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
|
||||||
|
// Convert FFI KeySet to cdk::nuts::KeySet
|
||||||
|
let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.add_keys(cdk_keyset)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
|
||||||
|
let cdk_id = id.into();
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_keys(&cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
|
||||||
|
let cdk_id = id.into();
|
||||||
|
self.inner
|
||||||
|
.remove_keys(&cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proof Management
|
||||||
|
async fn update_proofs(
|
||||||
|
&self,
|
||||||
|
added: Vec<ProofInfo>,
|
||||||
|
removed_ys: Vec<PublicKey>,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
// Convert FFI types to CDK types
|
||||||
|
let cdk_added: Result<Vec<cdk::types::ProofInfo>, FfiError> = added
|
||||||
|
.into_iter()
|
||||||
|
.map(|info| {
|
||||||
|
Ok::<cdk::types::ProofInfo, FfiError>(cdk::types::ProofInfo {
|
||||||
|
proof: info.proof.inner.clone(),
|
||||||
|
y: info.y.try_into()?,
|
||||||
|
mint_url: info.mint_url.try_into()?,
|
||||||
|
state: info.state.into(),
|
||||||
|
spending_condition: info
|
||||||
|
.spending_condition
|
||||||
|
.map(|sc| sc.try_into())
|
||||||
|
.transpose()?,
|
||||||
|
unit: info.unit.into(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let cdk_added = cdk_added?;
|
||||||
|
|
||||||
|
let cdk_removed_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
|
||||||
|
removed_ys.into_iter().map(|pk| pk.try_into()).collect();
|
||||||
|
let cdk_removed_ys = cdk_removed_ys?;
|
||||||
|
|
||||||
|
self.inner
|
||||||
|
.update_proofs(cdk_added, cdk_removed_ys)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_proofs(
|
||||||
|
&self,
|
||||||
|
mint_url: Option<MintUrl>,
|
||||||
|
unit: Option<CurrencyUnit>,
|
||||||
|
state: Option<Vec<ProofState>>,
|
||||||
|
spending_conditions: Option<Vec<SpendingConditions>>,
|
||||||
|
) -> Result<Vec<ProofInfo>, FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
|
||||||
|
let cdk_unit = unit.map(Into::into);
|
||||||
|
let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
|
||||||
|
let cdk_spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>> =
|
||||||
|
spending_conditions
|
||||||
|
.map(|sc| {
|
||||||
|
sc.into_iter()
|
||||||
|
.map(|c| c.try_into())
|
||||||
|
.collect::<Result<Vec<_>, FfiError>>()
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
|
||||||
|
Ok(result.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_proofs_state(
|
||||||
|
&self,
|
||||||
|
ys: Vec<PublicKey>,
|
||||||
|
state: ProofState,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
|
||||||
|
ys.into_iter().map(|pk| pk.try_into()).collect();
|
||||||
|
let cdk_ys = cdk_ys?;
|
||||||
|
let cdk_state = state.into();
|
||||||
|
|
||||||
|
self.inner
|
||||||
|
.update_proofs_state(cdk_ys, cdk_state)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyset Counter Management
|
||||||
|
async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
|
||||||
|
let cdk_id = keyset_id.into();
|
||||||
|
self.inner
|
||||||
|
.increment_keyset_counter(&cdk_id, count)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction Management
|
||||||
|
async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
|
||||||
|
// Convert FFI Transaction to CDK Transaction using TryFrom
|
||||||
|
let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?;
|
||||||
|
|
||||||
|
self.inner
|
||||||
|
.add_transaction(cdk_transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_transaction(
|
||||||
|
&self,
|
||||||
|
transaction_id: TransactionId,
|
||||||
|
) -> Result<Option<Transaction>, FfiError> {
|
||||||
|
let cdk_id = transaction_id.try_into()?;
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_transaction(cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_transactions(
|
||||||
|
&self,
|
||||||
|
mint_url: Option<MintUrl>,
|
||||||
|
direction: Option<TransactionDirection>,
|
||||||
|
unit: Option<CurrencyUnit>,
|
||||||
|
) -> Result<Vec<Transaction>, FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
|
||||||
|
let cdk_direction = direction.map(Into::into);
|
||||||
|
let cdk_unit = unit.map(Into::into);
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
|
||||||
|
Ok(result.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
|
||||||
|
let cdk_id = transaction_id.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.remove_transaction(cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
impl WalletPostgresDatabase {
|
||||||
|
/// Create a new Postgres-backed wallet database
|
||||||
|
/// Requires cdk-ffi to be built with feature "postgres".
|
||||||
|
/// 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() {
|
||||||
|
Ok(handle) => tokio::task::block_in_place(|| {
|
||||||
|
handle.block_on(
|
||||||
|
async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await },
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
// Important: use a process-long runtime so background connection tasks stay alive.
|
||||||
|
Err(_) => pg_runtime()
|
||||||
|
.block_on(async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await }),
|
||||||
|
}
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(Arc::new(WalletPostgresDatabase {
|
||||||
|
inner: Arc::new(inner),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_as_trait(&self) -> Arc<dyn WalletDatabase> {
|
||||||
|
// Safety: UniFFI objects are reference counted and Send+Sync via Arc
|
||||||
|
let obj: Arc<dyn WalletDatabase> = Arc::new(WalletPostgresDatabase {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
});
|
||||||
|
obj
|
||||||
|
}
|
||||||
|
}
|
||||||
402
crates/cdk-ffi/src/sqlite.rs
Normal file
402
crates/cdk-ffi/src/sqlite.rs
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use cdk_sqlite::wallet::WalletSqliteDatabase as CdkWalletSqliteDatabase;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
CurrencyUnit, FfiError, Id, KeySet, KeySetInfo, Keys, MeltQuote, MintInfo, MintQuote, MintUrl,
|
||||||
|
ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction, TransactionDirection,
|
||||||
|
TransactionId, WalletDatabase,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabase trait
|
||||||
|
#[derive(uniffi::Object)]
|
||||||
|
pub struct WalletSqliteDatabase {
|
||||||
|
inner: Arc<CdkWalletSqliteDatabase>,
|
||||||
|
}
|
||||||
|
use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
|
||||||
|
|
||||||
|
impl WalletSqliteDatabase {
|
||||||
|
// No additional methods needed beyond the trait implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
impl WalletSqliteDatabase {
|
||||||
|
/// Create a new WalletSqliteDatabase with the given work directory
|
||||||
|
#[uniffi::constructor]
|
||||||
|
pub fn new(file_path: String) -> Result<Arc<Self>, FfiError> {
|
||||||
|
let db = match tokio::runtime::Handle::try_current() {
|
||||||
|
Ok(handle) => tokio::task::block_in_place(|| {
|
||||||
|
handle
|
||||||
|
.block_on(async move { CdkWalletSqliteDatabase::new(file_path.as_str()).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 { CdkWalletSqliteDatabase::new(file_path.as_str()).await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
inner: Arc::new(db),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an in-memory database
|
||||||
|
#[uniffi::constructor]
|
||||||
|
pub fn new_in_memory() -> Result<Arc<Self>, FfiError> {
|
||||||
|
let db = match tokio::runtime::Handle::try_current() {
|
||||||
|
Ok(handle) => tokio::task::block_in_place(|| {
|
||||||
|
handle.block_on(async move { cdk_sqlite::wallet::memory::empty().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 { cdk_sqlite::wallet::memory::empty().await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
inner: Arc::new(db),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export(async_runtime = "tokio")]
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl WalletDatabase for WalletSqliteDatabase {
|
||||||
|
// Mint Management
|
||||||
|
async fn add_mint(
|
||||||
|
&self,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
mint_info: Option<MintInfo>,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
let cdk_mint_info = mint_info.map(Into::into);
|
||||||
|
self.inner
|
||||||
|
.add_mint(cdk_mint_url, cdk_mint_info)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.remove_mint(cdk_mint_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mint(cdk_mint_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mints()
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k.into(), v.map(Into::into)))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_mint_url(
|
||||||
|
&self,
|
||||||
|
old_mint_url: MintUrl,
|
||||||
|
new_mint_url: MintUrl,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
let cdk_old_mint_url = old_mint_url.try_into()?;
|
||||||
|
let cdk_new_mint_url = new_mint_url.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.update_mint_url(cdk_old_mint_url, cdk_new_mint_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyset Management
|
||||||
|
async fn add_mint_keysets(
|
||||||
|
&self,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
keysets: Vec<KeySetInfo>,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
let cdk_keysets: Vec<cdk::nuts::KeySetInfo> = keysets.into_iter().map(Into::into).collect();
|
||||||
|
self.inner
|
||||||
|
.add_mint_keysets(cdk_mint_url, cdk_keysets)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mint_keysets(
|
||||||
|
&self,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.try_into()?;
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mint_keysets(cdk_mint_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
|
||||||
|
let cdk_id = keyset_id.into();
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_keyset_by_id(&cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mint Quote Management
|
||||||
|
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
|
||||||
|
let cdk_quote = quote.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.add_mint_quote(cdk_quote)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mint_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(|q| q.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_mint_quotes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.into_iter().map(|q| q.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
|
||||||
|
self.inner
|
||||||
|
.remove_mint_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Melt Quote Management
|
||||||
|
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
|
||||||
|
let cdk_quote = quote.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.add_melt_quote(cdk_quote)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_melt_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(|q| q.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_melt_quotes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.into_iter().map(|q| q.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
|
||||||
|
self.inner
|
||||||
|
.remove_melt_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys Management
|
||||||
|
async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
|
||||||
|
// Convert FFI KeySet to cdk::nuts::KeySet
|
||||||
|
let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.add_keys(cdk_keyset)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
|
||||||
|
let cdk_id = id.into();
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_keys(&cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
|
||||||
|
let cdk_id = id.into();
|
||||||
|
self.inner
|
||||||
|
.remove_keys(&cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proof Management
|
||||||
|
async fn update_proofs(
|
||||||
|
&self,
|
||||||
|
added: Vec<ProofInfo>,
|
||||||
|
removed_ys: Vec<PublicKey>,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
// Convert FFI types to CDK types
|
||||||
|
let cdk_added: Result<Vec<cdk::types::ProofInfo>, FfiError> = added
|
||||||
|
.into_iter()
|
||||||
|
.map(|info| {
|
||||||
|
Ok::<cdk::types::ProofInfo, FfiError>(cdk::types::ProofInfo {
|
||||||
|
proof: info.proof.inner.clone(),
|
||||||
|
y: info.y.try_into()?,
|
||||||
|
mint_url: info.mint_url.try_into()?,
|
||||||
|
state: info.state.into(),
|
||||||
|
spending_condition: info
|
||||||
|
.spending_condition
|
||||||
|
.map(|sc| sc.try_into())
|
||||||
|
.transpose()?,
|
||||||
|
unit: info.unit.into(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let cdk_added = cdk_added?;
|
||||||
|
|
||||||
|
let cdk_removed_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
|
||||||
|
removed_ys.into_iter().map(|pk| pk.try_into()).collect();
|
||||||
|
let cdk_removed_ys = cdk_removed_ys?;
|
||||||
|
|
||||||
|
self.inner
|
||||||
|
.update_proofs(cdk_added, cdk_removed_ys)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_proofs(
|
||||||
|
&self,
|
||||||
|
mint_url: Option<MintUrl>,
|
||||||
|
unit: Option<CurrencyUnit>,
|
||||||
|
state: Option<Vec<ProofState>>,
|
||||||
|
spending_conditions: Option<Vec<SpendingConditions>>,
|
||||||
|
) -> Result<Vec<ProofInfo>, FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
|
||||||
|
let cdk_unit = unit.map(Into::into);
|
||||||
|
let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
|
||||||
|
let cdk_spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>> =
|
||||||
|
spending_conditions
|
||||||
|
.map(|sc| {
|
||||||
|
sc.into_iter()
|
||||||
|
.map(|c| c.try_into())
|
||||||
|
.collect::<Result<Vec<_>, FfiError>>()
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
|
||||||
|
Ok(result.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_proofs_state(
|
||||||
|
&self,
|
||||||
|
ys: Vec<PublicKey>,
|
||||||
|
state: ProofState,
|
||||||
|
) -> Result<(), FfiError> {
|
||||||
|
let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
|
||||||
|
ys.into_iter().map(|pk| pk.try_into()).collect();
|
||||||
|
let cdk_ys = cdk_ys?;
|
||||||
|
let cdk_state = state.into();
|
||||||
|
|
||||||
|
self.inner
|
||||||
|
.update_proofs_state(cdk_ys, cdk_state)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyset Counter Management
|
||||||
|
async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
|
||||||
|
let cdk_id = keyset_id.into();
|
||||||
|
self.inner
|
||||||
|
.increment_keyset_counter(&cdk_id, count)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction Management
|
||||||
|
async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
|
||||||
|
// Convert FFI Transaction to CDK Transaction using TryFrom
|
||||||
|
let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?;
|
||||||
|
|
||||||
|
self.inner
|
||||||
|
.add_transaction(cdk_transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_transaction(
|
||||||
|
&self,
|
||||||
|
transaction_id: TransactionId,
|
||||||
|
) -> Result<Option<Transaction>, FfiError> {
|
||||||
|
let cdk_id = transaction_id.try_into()?;
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.get_transaction(cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
Ok(result.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_transactions(
|
||||||
|
&self,
|
||||||
|
mint_url: Option<MintUrl>,
|
||||||
|
direction: Option<TransactionDirection>,
|
||||||
|
unit: Option<CurrencyUnit>,
|
||||||
|
) -> Result<Vec<Transaction>, FfiError> {
|
||||||
|
let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
|
||||||
|
let cdk_direction = direction.map(Into::into);
|
||||||
|
let cdk_unit = unit.map(Into::into);
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })?;
|
||||||
|
|
||||||
|
Ok(result.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
|
||||||
|
let cdk_id = transaction_id.try_into()?;
|
||||||
|
self.inner
|
||||||
|
.remove_transaction(cdk_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FfiError::Database { msg: e.to_string() })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bip39::Mnemonic;
|
use bip39::Mnemonic;
|
||||||
use cdk_ffi::database::WalletSqliteDatabase;
|
use cdk_ffi::sqlite::WalletSqliteDatabase;
|
||||||
use cdk_ffi::types::{Amount, CurrencyUnit, QuoteState, SplitTarget};
|
use cdk_ffi::types::{Amount, CurrencyUnit, QuoteState, SplitTarget};
|
||||||
use cdk_ffi::wallet::Wallet as FfiWallet;
|
use cdk_ffi::wallet::Wallet as FfiWallet;
|
||||||
use cdk_ffi::WalletConfig;
|
use cdk_ffi::WalletConfig;
|
||||||
|
|||||||
@@ -319,9 +319,15 @@ pub type MintPgDatabase = SQLMintDatabase<PgConnectionPool>;
|
|||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
pub type MintPgAuthDatabase = SQLMintAuthDatabase<PgConnectionPool>;
|
pub type MintPgAuthDatabase = SQLMintAuthDatabase<PgConnectionPool>;
|
||||||
|
|
||||||
/// Mint DB implementation with PostgresSQL
|
/// Wallet DB implementation with PostgreSQL
|
||||||
pub type WalletPgDatabase = SQLWalletDatabase<PgConnectionPool>;
|
pub type WalletPgDatabase = SQLWalletDatabase<PgConnectionPool>;
|
||||||
|
|
||||||
|
/// Convenience free functions (cannot add inherent impls for a foreign type).
|
||||||
|
/// These mirror the Mint patterns and call through to the generic constructors.
|
||||||
|
pub async fn new_wallet_pg_database(conn_str: &str) -> Result<WalletPgDatabase, Error> {
|
||||||
|
<SQLWalletDatabase<PgConnectionPool>>::new(conn_str).await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use cdk_common::mint_db_test;
|
use cdk_common::mint_db_test;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Add u32 representation column to key table with unique constraint
|
||||||
|
ALTER TABLE key ADD COLUMN keyset_u32 INTEGER;
|
||||||
|
|
||||||
|
-- Add unique constraint on the new column
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS keyset_u32_unique ON key(keyset_u32);
|
||||||
|
|
||||||
|
-- Add u32 representation column to keyset table with unique constraint
|
||||||
|
ALTER TABLE keyset ADD COLUMN keyset_u32 INTEGER;
|
||||||
|
|
||||||
|
-- Add unique constraint on the new column
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS keyset_u32_unique_keyset ON keyset(keyset_u32);
|
||||||
4
justfile
4
justfile
@@ -429,7 +429,7 @@ _ffi-lib-ext:
|
|||||||
|
|
||||||
# Build the FFI library
|
# Build the FFI library
|
||||||
ffi-build *ARGS="--release":
|
ffi-build *ARGS="--release":
|
||||||
cargo build {{ARGS}} --package cdk-ffi
|
cargo build {{ARGS}} --package cdk-ffi --features postgres
|
||||||
|
|
||||||
# Generate bindings for a specific language
|
# Generate bindings for a specific language
|
||||||
ffi-generate LANGUAGE *ARGS="--release": ffi-build
|
ffi-generate LANGUAGE *ARGS="--release": ffi-build
|
||||||
@@ -460,7 +460,7 @@ ffi-generate LANGUAGE *ARGS="--release": ffi-build
|
|||||||
BUILD_TYPE="release"
|
BUILD_TYPE="release"
|
||||||
else
|
else
|
||||||
BUILD_TYPE="debug"
|
BUILD_TYPE="debug"
|
||||||
cargo build --package cdk-ffi
|
cargo build --package cdk-ffi --features postgres
|
||||||
fi
|
fi
|
||||||
|
|
||||||
LIB_EXT=$(just _ffi-lib-ext)
|
LIB_EXT=$(just _ffi-lib-ext)
|
||||||
|
|||||||
Reference in New Issue
Block a user