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