Skip to content
Closed
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ strum = "0.27.1"
strum_macros = "0.27.1"
rustls = { version = "0.23.27", default-features = false, features = ["ring"] }
prometheus = { version = "0.13.4", features = ["process"], default-features = false }
zeroize = "1"



Expand Down
12 changes: 11 additions & 1 deletion crates/cdk-common/src/database/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::Error;
use crate::common::ProofInfo;
use crate::mint_url::MintUrl;
use crate::nuts::{
CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SecretKey, SpendingConditions, State,
};
use crate::wallet::{
self, MintQuote as WalletMintQuote, Transaction, TransactionDirection, TransactionId,
Expand Down Expand Up @@ -125,4 +125,14 @@ pub trait Database: Debug {
) -> Result<Vec<Transaction>, Self::Err>;
/// Remove transaction from storage
async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err>;

// --- P2PK signing key storage ---
/// Store a P2PK signing key. Implementations must upsert by derived pubkey.
async fn add_p2pk_key(&self, secret_key: SecretKey) -> Result<(), Self::Err>;
/// Get a stored P2PK secret key by pubkey.
async fn get_p2pk_key(&self, pubkey: PublicKey) -> Result<Option<SecretKey>, Self::Err>;
/// List all stored P2PK signing keys.
async fn list_p2pk_keys(&self) -> Result<Vec<(PublicKey, SecretKey)>, Self::Err>;
/// Remove a stored P2PK signing key by pubkey.
async fn remove_p2pk_key(&self, pubkey: PublicKey) -> Result<(), Self::Err>;
}
1 change: 0 additions & 1 deletion crates/cdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ uniffi = { version = "0.29", features = ["cli", "tokio"] }
url = { workspace = true }
uuid = { workspace = true, features = ["v4"] }


[features]
default = ["postgres"]
# Enable Postgres-backed wallet database support in FFI
Expand Down
67 changes: 67 additions & 0 deletions crates/cdk-ffi/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ pub trait WalletDatabase: Send + Sync {
/// Remove Keys from storage
async fn remove_keys(&self, id: Id) -> Result<(), FfiError>;

// P2PK signing key storage
/// Store a P2PK signing key
async fn add_p2pk_key(&self, secret_key: Arc<SecretKey>) -> Result<(), FfiError>;
/// Fetch a P2PK signing key by public key
async fn get_p2pk_key(&self, pubkey: PublicKey) -> Result<Option<Arc<SecretKey>>, FfiError>;
/// List stored P2PK signing keys
async fn list_p2pk_keys(&self) -> Result<Vec<Arc<P2pkSigningKey>>, FfiError>;
/// Remove a stored P2PK signing key by public key
async fn remove_p2pk_key(&self, pubkey: PublicKey) -> Result<(), FfiError>;

// Proof Management
/// Update the proofs in storage by adding new proofs or removing proofs by their Y value
async fn update_proofs(
Expand Down Expand Up @@ -411,6 +421,63 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
.map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
}

async fn add_p2pk_key(&self, secret_key: cdk::nuts::SecretKey) -> Result<(), Self::Err> {
let ffi_secret: Arc<SecretKey> = Arc::new(secret_key.into());
self.ffi_db
.add_p2pk_key(ffi_secret)
.await
.map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
}

async fn get_p2pk_key(
&self,
pubkey: cdk::nuts::PublicKey,
) -> Result<Option<cdk::nuts::SecretKey>, Self::Err> {
let ffi_pubkey: PublicKey = pubkey.into();
let result = self
.ffi_db
.get_p2pk_key(ffi_pubkey)
.await
.map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;

result
.map(|sk| {
(*sk)
.clone()
.try_into()
.map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
})
.transpose()
}

async fn list_p2pk_keys(
&self,
) -> Result<Vec<(cdk::nuts::PublicKey, cdk::nuts::SecretKey)>, Self::Err> {
let result = self
.ffi_db
.list_p2pk_keys()
.await
.map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;

result
.into_iter()
.map(|entry| {
let (pubkey, secret) = (*entry).clone().try_into().map_err(|e: FfiError| {
cdk::cdk_database::Error::Database(e.to_string().into())
})?;
Ok((pubkey, secret))
})
.collect()
}

async fn remove_p2pk_key(&self, pubkey: cdk::nuts::PublicKey) -> Result<(), Self::Err> {
let ffi_pubkey: PublicKey = pubkey.into();
self.ffi_db
.remove_p2pk_key(ffi_pubkey)
.await
.map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
}

// Proof Management
async fn update_proofs(
&self,
Expand Down
62 changes: 45 additions & 17 deletions crates/cdk-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,12 @@ mod tests {
#[test]
fn test_secret_key_from_hex() {
// Test valid hex string (64 characters)
let valid_hex = "a".repeat(64);
let secret_key = SecretKey::from_hex(valid_hex.clone());
let valid_hex = "0000000000000000000000000000000000000000000000000000000000000001";
let secret_key = SecretKey::from_hex(valid_hex.to_string());
assert!(secret_key.is_ok());
assert_eq!(secret_key.unwrap().hex, valid_hex);
let sk = secret_key.unwrap();
assert_eq!(sk.to_bytes().len(), 32);
assert_eq!(sk.to_hex(), valid_hex);

// Test invalid length
let invalid_length = "a".repeat(32); // 32 chars instead of 64
Expand All @@ -140,18 +142,43 @@ mod tests {
}

#[test]
fn test_secret_key_random() {
let key1 = SecretKey::random();
let key2 = SecretKey::random();

// Keys should be different
assert_ne!(key1.hex, key2.hex);

// Keys should be valid hex (64 characters)
assert_eq!(key1.hex.len(), 64);
assert_eq!(key2.hex.len(), 64);
assert!(key1.hex.chars().all(|c| c.is_ascii_hexdigit()));
assert!(key2.hex.chars().all(|c| c.is_ascii_hexdigit()));
fn test_secret_key_from_bytes() {
// Test valid bytes (32 bytes)
let valid_bytes = vec![1u8; 32];
let secret_key = SecretKey::from_bytes(valid_bytes.clone());
assert!(secret_key.is_ok());
let sk = secret_key.unwrap();
assert_eq!(sk.to_bytes().len(), 32);
assert_eq!(sk.to_bytes(), valid_bytes);

// Test invalid length
let invalid_length = vec![1u8; 16]; // 16 bytes instead of 32
let secret_key = SecretKey::from_bytes(invalid_length);
assert!(secret_key.is_err());

// Test empty
let empty = vec![];
let secret_key = SecretKey::from_bytes(empty);
assert!(secret_key.is_err());
}

#[test]
fn test_secret_key_conversions() {
// Test round-trip conversion
let cdk_secret = cdk::nuts::SecretKey::generate();
let ffi_secret: SecretKey = cdk_secret.clone().into();
let cdk_secret_back: cdk::nuts::SecretKey = ffi_secret.clone().try_into().unwrap();

// Should be equal
assert_eq!(
cdk_secret.to_secret_bytes(),
cdk_secret_back.to_secret_bytes()
);

// Test bytes match - Vec should be exactly 32 bytes
let bytes = ffi_secret.to_bytes();
assert_eq!(bytes.len(), 32);
assert_eq!(bytes, cdk_secret.to_secret_bytes().to_vec());
}

#[test]
Expand Down Expand Up @@ -204,16 +231,17 @@ mod tests {
#[test]
fn test_receive_options_with_all_fields() {
use std::collections::HashMap;
use std::sync::Arc;

let secret_key = SecretKey::random();
let secret_key = cdk::nuts::SecretKey::generate();
let mut metadata = HashMap::new();
metadata.insert("key1".to_string(), "value1".to_string());

let options = ReceiveOptions {
amount_split_target: SplitTarget::Values {
amounts: vec![Amount::new(100), Amount::new(200)],
},
p2pk_signing_keys: vec![secret_key],
p2pk_signing_keys: vec![Arc::new(secret_key.into())],
preimages: vec!["preimage1".to_string(), "preimage2".to_string()],
metadata,
};
Expand Down
42 changes: 40 additions & 2 deletions crates/cdk-ffi/src/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ 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,
P2pkSigningKey, ProofInfo, ProofState, PublicKey, SecretKey, SpendingConditions, Transaction,
TransactionDirection, TransactionId, WalletDatabase,
};

#[derive(uniffi::Object)]
Expand Down Expand Up @@ -226,6 +226,44 @@ impl WalletDatabase for WalletPostgresDatabase {
.map_err(|e| FfiError::Database { msg: e.to_string() })
}

// P2PK Key Management
async fn add_p2pk_key(&self, secret_key: Arc<SecretKey>) -> Result<(), FfiError> {
let cdk_secret: cdk::nuts::SecretKey = (*secret_key).clone().try_into()?;
self.inner
.add_p2pk_key(cdk_secret)
.await
.map_err(|e| FfiError::Database { msg: e.to_string() })
}

async fn get_p2pk_key(&self, pubkey: PublicKey) -> Result<Option<Arc<SecretKey>>, FfiError> {
let cdk_pubkey = pubkey.try_into()?;
let result = self
.inner
.get_p2pk_key(cdk_pubkey)
.await
.map_err(|e| FfiError::Database { msg: e.to_string() })?;

Ok(result.map(|sk| Arc::new(sk.into())))
}

async fn list_p2pk_keys(&self) -> Result<Vec<Arc<P2pkSigningKey>>, FfiError> {
let result = self
.inner
.list_p2pk_keys()
.await
.map_err(|e| FfiError::Database { msg: e.to_string() })?;

Ok(result.into_iter().map(|k| Arc::new(k.into())).collect())
}

async fn remove_p2pk_key(&self, pubkey: PublicKey) -> Result<(), FfiError> {
let cdk_pubkey = pubkey.try_into()?;
self.inner
.remove_p2pk_key(cdk_pubkey)
.await
.map_err(|e| FfiError::Database { msg: e.to_string() })
}

// Proof Management
async fn update_proofs(
&self,
Expand Down
42 changes: 40 additions & 2 deletions crates/cdk-ffi/src/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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,
P2pkSigningKey, ProofInfo, ProofState, PublicKey, SecretKey, SpendingConditions, Transaction,
TransactionDirection, TransactionId, WalletDatabase,
};

/// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabase trait
Expand Down Expand Up @@ -261,6 +261,44 @@ impl WalletDatabase for WalletSqliteDatabase {
.map_err(|e| FfiError::Database { msg: e.to_string() })
}

// P2PK Key Management
async fn add_p2pk_key(&self, secret_key: Arc<SecretKey>) -> Result<(), FfiError> {
let cdk_secret: cdk::nuts::SecretKey = (*secret_key).clone().try_into()?;
self.inner
.add_p2pk_key(cdk_secret)
.await
.map_err(|e| FfiError::Database { msg: e.to_string() })
}

async fn get_p2pk_key(&self, pubkey: PublicKey) -> Result<Option<Arc<SecretKey>>, FfiError> {
let cdk_pubkey = pubkey.try_into()?;
let result = self
.inner
.get_p2pk_key(cdk_pubkey)
.await
.map_err(|e| FfiError::Database { msg: e.to_string() })?;

Ok(result.map(|sk| Arc::new(sk.into())))
}

async fn list_p2pk_keys(&self) -> Result<Vec<Arc<P2pkSigningKey>>, FfiError> {
let result = self
.inner
.list_p2pk_keys()
.await
.map_err(|e| FfiError::Database { msg: e.to_string() })?;

Ok(result.into_iter().map(|k| Arc::new(k.into())).collect())
}

async fn remove_p2pk_key(&self, pubkey: PublicKey) -> Result<(), FfiError> {
let cdk_pubkey = pubkey.try_into()?;
self.inner
.remove_p2pk_key(cdk_pubkey)
.await
.map_err(|e| FfiError::Database { msg: e.to_string() })
}

// Proof Management
async fn update_proofs(
&self,
Expand Down
Loading
Loading