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