Skip to main content

cdk_ffi/
lib.rs

1//! CDK FFI Bindings
2//!
3//! UniFFI bindings for the CDK Wallet and related types.
4
5#![warn(clippy::unused_async)]
6#![allow(missing_docs)]
7#![allow(missing_debug_implementations)]
8
9pub mod database;
10pub mod error;
11pub mod logging;
12#[cfg(feature = "npubcash")]
13pub mod npubcash;
14#[cfg(feature = "postgres")]
15pub mod postgres;
16pub mod sqlite;
17pub mod token;
18pub mod types;
19pub mod wallet;
20pub mod wallet_repository;
21
22pub use database::*;
23pub use error::*;
24pub use logging::*;
25#[cfg(feature = "npubcash")]
26pub use npubcash::*;
27pub use types::*;
28pub use wallet::*;
29pub use wallet_repository::*;
30
31uniffi::setup_scaffolding!();
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    #[test]
38    fn test_amount_conversion() {
39        let amount = Amount::new(1000);
40        assert_eq!(amount.value, 1000);
41        assert!(!amount.is_zero());
42
43        let zero = Amount::zero();
44        assert!(zero.is_zero());
45    }
46
47    #[test]
48    fn test_currency_unit_conversion() {
49        use cdk::nuts::CurrencyUnit as CdkCurrencyUnit;
50
51        let unit = CurrencyUnit::Sat;
52        let cdk_unit: CdkCurrencyUnit = unit.into();
53        let back: CurrencyUnit = cdk_unit.into();
54        assert_eq!(back, CurrencyUnit::Sat);
55    }
56
57    #[test]
58    fn test_mint_url_creation() {
59        let url = MintUrl::new("https://mint.example.com".to_string());
60        assert!(url.is_ok());
61
62        let invalid_url = MintUrl::new("not-a-url".to_string());
63        assert!(invalid_url.is_err());
64    }
65
66    #[test]
67    fn test_send_options_default() {
68        let options = SendOptions::default();
69        assert!(options.memo.is_none());
70        assert!(options.conditions.is_none());
71        assert!(matches!(options.amount_split_target, SplitTarget::None));
72        assert!(matches!(options.send_kind, SendKind::OnlineExact));
73        assert!(!options.include_fee);
74        assert!(options.max_proofs.is_none());
75        assert!(options.metadata.is_empty());
76    }
77
78    #[test]
79    fn test_receive_options_default() {
80        let options = ReceiveOptions::default();
81        assert!(matches!(options.amount_split_target, SplitTarget::None));
82        assert!(options.p2pk_signing_keys.is_empty());
83        assert!(options.preimages.is_empty());
84        assert!(options.metadata.is_empty());
85    }
86
87    #[test]
88    fn test_send_memo() {
89        let memo_text = "Test memo".to_string();
90        let memo = SendMemo {
91            memo: memo_text.clone(),
92            include_memo: true,
93        };
94
95        assert_eq!(memo.memo, memo_text);
96        assert!(memo.include_memo);
97    }
98
99    #[test]
100    fn test_split_target_variants() {
101        let split_none = SplitTarget::None;
102        assert!(matches!(split_none, SplitTarget::None));
103
104        let amount = Amount::new(1000);
105        let split_value = SplitTarget::Value { amount };
106        assert!(matches!(split_value, SplitTarget::Value { .. }));
107
108        let amounts = vec![Amount::new(100), Amount::new(200)];
109        let split_values = SplitTarget::Values { amounts };
110        assert!(matches!(split_values, SplitTarget::Values { .. }));
111    }
112
113    #[test]
114    fn test_send_kind_variants() {
115        let online_exact = SendKind::OnlineExact;
116        assert!(matches!(online_exact, SendKind::OnlineExact));
117
118        let tolerance = Amount::new(50);
119        let online_tolerance = SendKind::OnlineTolerance { tolerance };
120        assert!(matches!(online_tolerance, SendKind::OnlineTolerance { .. }));
121
122        let offline_exact = SendKind::OfflineExact;
123        assert!(matches!(offline_exact, SendKind::OfflineExact));
124
125        let offline_tolerance = SendKind::OfflineTolerance { tolerance };
126        assert!(matches!(
127            offline_tolerance,
128            SendKind::OfflineTolerance { .. }
129        ));
130    }
131
132    #[test]
133    fn test_secret_key_from_hex() {
134        // Test valid hex string (64 characters)
135        let valid_hex = "a".repeat(64);
136        let secret_key = SecretKey::from_hex(valid_hex.clone());
137        assert!(secret_key.is_ok());
138        assert_eq!(secret_key.unwrap().hex, valid_hex);
139
140        // Test invalid length
141        let invalid_length = "a".repeat(32); // 32 chars instead of 64
142        let secret_key = SecretKey::from_hex(invalid_length);
143        assert!(secret_key.is_err());
144
145        // Test invalid characters
146        let invalid_chars = "g".repeat(64); // 'g' is not a valid hex character
147        let secret_key = SecretKey::from_hex(invalid_chars);
148        assert!(secret_key.is_err());
149    }
150
151    #[test]
152    fn test_secret_key_random() {
153        let key1 = SecretKey::random();
154        let key2 = SecretKey::random();
155
156        // Keys should be different
157        assert_ne!(key1.hex, key2.hex);
158
159        // Keys should be valid hex (64 characters)
160        assert_eq!(key1.hex.len(), 64);
161        assert_eq!(key2.hex.len(), 64);
162        assert!(key1.hex.chars().all(|c| c.is_ascii_hexdigit()));
163        assert!(key2.hex.chars().all(|c| c.is_ascii_hexdigit()));
164    }
165
166    #[test]
167    fn test_send_options_with_all_fields() {
168        use std::collections::HashMap;
169
170        let memo = SendMemo {
171            memo: "Test memo".to_string(),
172            include_memo: true,
173        };
174
175        let mut metadata = HashMap::new();
176        metadata.insert("key1".to_string(), "value1".to_string());
177
178        let conditions = SpendingConditions::P2PK {
179            pubkey: "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc"
180                .to_string(),
181            conditions: None,
182        };
183
184        let options = SendOptions {
185            memo: Some(memo),
186            conditions: Some(conditions),
187            amount_split_target: SplitTarget::Value {
188                amount: Amount::new(1000),
189            },
190            send_kind: SendKind::OnlineTolerance {
191                tolerance: Amount::new(50),
192            },
193            include_fee: true,
194            max_proofs: Some(10),
195            metadata,
196        };
197
198        assert!(options.memo.is_some());
199        assert!(options.conditions.is_some());
200        assert!(matches!(
201            options.amount_split_target,
202            SplitTarget::Value { .. }
203        ));
204        assert!(matches!(
205            options.send_kind,
206            SendKind::OnlineTolerance { .. }
207        ));
208        assert!(options.include_fee);
209        assert_eq!(options.max_proofs, Some(10));
210        assert!(!options.metadata.is_empty());
211    }
212
213    #[test]
214    fn test_receive_options_with_all_fields() {
215        use std::collections::HashMap;
216
217        let secret_key = SecretKey::random();
218        let mut metadata = HashMap::new();
219        metadata.insert("key1".to_string(), "value1".to_string());
220
221        let options = ReceiveOptions {
222            amount_split_target: SplitTarget::Values {
223                amounts: vec![Amount::new(100), Amount::new(200)],
224            },
225            p2pk_signing_keys: vec![secret_key],
226            preimages: vec!["preimage1".to_string(), "preimage2".to_string()],
227            metadata,
228        };
229
230        assert!(matches!(
231            options.amount_split_target,
232            SplitTarget::Values { .. }
233        ));
234        assert_eq!(options.p2pk_signing_keys.len(), 1);
235        assert_eq!(options.preimages.len(), 2);
236        assert!(!options.metadata.is_empty());
237    }
238
239    #[test]
240    fn test_wallet_config() {
241        let config = WalletConfig {
242            target_proof_count: None,
243        };
244        assert!(config.target_proof_count.is_none());
245
246        let config_with_values = WalletConfig {
247            target_proof_count: Some(5),
248        };
249        assert_eq!(config_with_values.target_proof_count, Some(5));
250    }
251
252    #[test]
253    fn test_mnemonic_generation() {
254        // Test mnemonic generation
255        let mnemonic = generate_mnemonic().unwrap();
256        assert!(!mnemonic.is_empty());
257        assert_eq!(mnemonic.split_whitespace().count(), 12);
258
259        // Verify it's a valid mnemonic by trying to parse it
260        use bip39::Mnemonic;
261        let parsed = Mnemonic::parse(&mnemonic);
262        assert!(parsed.is_ok());
263    }
264
265    #[test]
266    fn test_mnemonic_validation() {
267        // Test with valid mnemonic
268        let mnemonic = generate_mnemonic().unwrap();
269        use bip39::Mnemonic;
270        let parsed = Mnemonic::parse(&mnemonic);
271        assert!(parsed.is_ok());
272
273        // Test with invalid mnemonic
274        let invalid_mnemonic = "invalid mnemonic phrase that should not work";
275        let parsed_invalid = Mnemonic::parse(invalid_mnemonic);
276        assert!(parsed_invalid.is_err());
277
278        // Test mnemonic word count variations
279        let mnemonic_12 = generate_mnemonic().unwrap();
280        assert_eq!(mnemonic_12.split_whitespace().count(), 12);
281    }
282
283    #[test]
284    fn test_mnemonic_to_entropy() {
285        // Test with generated mnemonic
286        let mnemonic = generate_mnemonic().unwrap();
287        let entropy = mnemonic_to_entropy(mnemonic.clone()).unwrap();
288
289        // For a 12-word mnemonic, entropy should be 16 bytes (128 bits)
290        assert_eq!(entropy.len(), 16);
291
292        // Test that we can recreate the mnemonic from entropy
293        use bip39::Mnemonic;
294        let recreated_mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
295        assert_eq!(recreated_mnemonic.to_string(), mnemonic);
296
297        // Test with invalid mnemonic
298        let invalid_result = mnemonic_to_entropy("invalid mnemonic".to_string());
299        assert!(invalid_result.is_err());
300    }
301}