1#![warn(clippy::unused_async)]
6#![allow(missing_docs)]
7#![allow(missing_debug_implementations)]
8
9pub mod bip321;
10pub mod database;
11pub mod error;
12pub mod logging;
13#[cfg(feature = "npubcash")]
14pub mod npubcash;
15#[cfg(feature = "nwc")]
16pub mod nwc;
17#[cfg(feature = "postgres")]
18pub mod postgres;
19mod runtime;
20pub mod sqlite;
21#[cfg(feature = "supabase")]
22pub mod supabase;
23pub mod token;
24pub mod types;
25pub mod wallet;
26pub mod wallet_repository;
27mod wallet_trait;
28
29pub use database::*;
30pub use error::*;
31pub use logging::*;
32#[cfg(feature = "npubcash")]
33pub use npubcash::*;
34#[cfg(feature = "nwc")]
35pub use nwc::*;
36pub use types::*;
37pub use wallet::*;
38pub use wallet_repository::*;
39
40uniffi::setup_scaffolding!();
41
42#[cfg(test)]
43mod tests {
44 use std::convert::TryInto;
45
46 use super::*;
47
48 #[test]
49 fn test_amount_conversion() {
50 let amount = Amount::new(1000);
51 assert_eq!(amount.value, 1000);
52 assert!(!amount.is_zero());
53
54 let zero = Amount::zero();
55 assert!(zero.is_zero());
56 }
57
58 #[test]
59 fn test_currency_unit_conversion() {
60 use cdk::nuts::CurrencyUnit as CdkCurrencyUnit;
61
62 let unit = CurrencyUnit::Sat;
63 let cdk_unit: CdkCurrencyUnit = unit.into();
64 let back: CurrencyUnit = cdk_unit.into();
65 assert_eq!(back, CurrencyUnit::Sat);
66 }
67
68 #[test]
69 fn test_mint_url_creation() {
70 let url = MintUrl::new("https://mint.example.com".to_string());
71 assert!(url.is_ok());
72
73 let invalid_url = MintUrl::new("not-a-url".to_string());
74 assert!(invalid_url.is_err());
75 }
76
77 #[test]
78 fn test_send_options_default() {
79 let options = SendOptions::default();
80 assert!(options.memo.is_none());
81 assert!(options.conditions.is_none());
82 assert!(matches!(options.amount_split_target, SplitTarget::None));
83 assert!(matches!(options.send_kind, SendKind::OnlineExact));
84 assert!(!options.include_fee);
85 assert!(options.max_proofs.is_none());
86 assert!(options.metadata.is_empty());
87 assert!(options.p2pk_signing_keys.is_empty());
88 assert_eq!(
89 options.p2pk_locked_proof_send_mode,
90 P2PKLockedProofSendMode::Swap
91 );
92 }
93
94 #[test]
95 fn test_receive_options_default() {
96 let options = ReceiveOptions::default();
97 assert!(matches!(options.amount_split_target, SplitTarget::None));
98 assert!(options.p2pk_signing_keys.is_empty());
99 assert!(options.preimages.is_empty());
100 assert!(options.metadata.is_empty());
101 }
102
103 #[test]
104 fn test_send_memo() {
105 let memo_text = "Test memo".to_string();
106 let memo = SendMemo {
107 memo: memo_text.clone(),
108 include_memo: true,
109 };
110
111 assert_eq!(memo.memo, memo_text);
112 assert!(memo.include_memo);
113 }
114
115 #[test]
116 fn test_split_target_variants() {
117 let split_none = SplitTarget::None;
118 assert!(matches!(split_none, SplitTarget::None));
119
120 let amount = Amount::new(1000);
121 let split_value = SplitTarget::Value { amount };
122 assert!(matches!(split_value, SplitTarget::Value { .. }));
123
124 let amounts = vec![Amount::new(100), Amount::new(200)];
125 let split_values = SplitTarget::Values { amounts };
126 assert!(matches!(split_values, SplitTarget::Values { .. }));
127 }
128
129 #[test]
130 fn test_send_kind_variants() {
131 let online_exact = SendKind::OnlineExact;
132 assert!(matches!(online_exact, SendKind::OnlineExact));
133
134 let tolerance = Amount::new(50);
135 let online_tolerance = SendKind::OnlineTolerance { tolerance };
136 assert!(matches!(online_tolerance, SendKind::OnlineTolerance { .. }));
137
138 let offline_exact = SendKind::OfflineExact;
139 assert!(matches!(offline_exact, SendKind::OfflineExact));
140
141 let offline_tolerance = SendKind::OfflineTolerance { tolerance };
142 assert!(matches!(
143 offline_tolerance,
144 SendKind::OfflineTolerance { .. }
145 ));
146 }
147
148 #[test]
149 fn test_secret_key_from_hex() {
150 let valid_hex = "a".repeat(64);
152 let secret_key = SecretKey::from_hex(valid_hex.clone());
153 assert!(secret_key.is_ok());
154 assert_eq!(secret_key.unwrap().hex, valid_hex);
155
156 let invalid_length = "a".repeat(32); let secret_key = SecretKey::from_hex(invalid_length);
159 assert!(secret_key.is_err());
160
161 let invalid_chars = "g".repeat(64); let secret_key = SecretKey::from_hex(invalid_chars);
164 assert!(secret_key.is_err());
165 }
166
167 #[test]
168 fn test_secret_key_random() {
169 let key1 = SecretKey::random();
170 let key2 = SecretKey::random();
171
172 assert_ne!(key1.hex, key2.hex);
174
175 assert_eq!(key1.hex.len(), 64);
177 assert_eq!(key2.hex.len(), 64);
178 assert!(key1.hex.chars().all(|c| c.is_ascii_hexdigit()));
179 assert!(key2.hex.chars().all(|c| c.is_ascii_hexdigit()));
180 }
181
182 #[test]
183 fn test_send_options_with_all_fields() {
184 use std::collections::HashMap;
185
186 let memo = SendMemo {
187 memo: "Test memo".to_string(),
188 include_memo: true,
189 };
190
191 let mut metadata = HashMap::new();
192 metadata.insert("key1".to_string(), "value1".to_string());
193
194 let conditions = SpendingConditions::P2PK {
195 pubkey: "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc"
196 .to_string(),
197 conditions: None,
198 };
199
200 let options = SendOptions {
201 memo: Some(memo),
202 conditions: Some(conditions),
203 amount_split_target: SplitTarget::Value {
204 amount: Amount::new(1000),
205 },
206 send_kind: SendKind::OnlineTolerance {
207 tolerance: Amount::new(50),
208 },
209 include_fee: true,
210 max_proofs: Some(10),
211 metadata,
212 use_p2bk: false,
213 p2pk_signing_keys: Vec::new(),
214 p2pk_locked_proof_send_mode: P2PKLockedProofSendMode::Swap,
215 };
216
217 assert!(options.memo.is_some());
218 assert!(options.conditions.is_some());
219 assert!(matches!(
220 options.amount_split_target,
221 SplitTarget::Value { .. }
222 ));
223 assert!(matches!(
224 options.send_kind,
225 SendKind::OnlineTolerance { .. }
226 ));
227 assert!(options.include_fee);
228 assert_eq!(options.max_proofs, Some(10));
229 assert!(!options.metadata.is_empty());
230 }
231
232 #[test]
233 fn test_receive_options_with_all_fields() {
234 use std::collections::HashMap;
235
236 let secret_key = SecretKey::random();
237 let mut metadata = HashMap::new();
238 metadata.insert("key1".to_string(), "value1".to_string());
239
240 let options = ReceiveOptions {
241 amount_split_target: SplitTarget::Values {
242 amounts: vec![Amount::new(100), Amount::new(200)],
243 },
244 p2pk_signing_keys: vec![secret_key],
245 preimages: vec!["preimage1".to_string(), "preimage2".to_string()],
246 metadata,
247 };
248
249 assert!(matches!(
250 options.amount_split_target,
251 SplitTarget::Values { .. }
252 ));
253 assert_eq!(options.p2pk_signing_keys.len(), 1);
254 assert_eq!(options.preimages.len(), 2);
255 assert!(!options.metadata.is_empty());
256 }
257
258 #[test]
259 fn test_receive_options_invalid_secret_key_returns_error() {
260 let options = ReceiveOptions {
261 amount_split_target: SplitTarget::None,
262 p2pk_signing_keys: vec![SecretKey {
263 hex: "z".repeat(64),
264 }],
265 preimages: Vec::new(),
266 metadata: Default::default(),
267 };
268
269 let result: Result<cdk::wallet::ReceiveOptions, _> = options.try_into();
270
271 assert!(result.is_err());
272 }
273
274 #[test]
275 fn test_send_options_invalid_secret_key_returns_error() {
276 let options = SendOptions {
277 p2pk_signing_keys: vec![SecretKey {
278 hex: "z".repeat(64),
279 }],
280 ..Default::default()
281 };
282
283 let result: Result<cdk::wallet::SendOptions, _> = options.try_into();
284
285 assert!(result.is_err());
286 }
287
288 #[test]
289 fn test_send_options_invalid_conditions_returns_error() {
290 let options = SendOptions {
291 conditions: Some(SpendingConditions::P2PK {
292 pubkey: "not_a_valid_pubkey".to_string(),
293 conditions: None,
294 }),
295 ..Default::default()
296 };
297
298 let result: Result<cdk::wallet::SendOptions, _> = options.try_into();
299
300 assert!(result.is_err());
301 }
302
303 #[test]
304 fn test_send_options_json_preserves_p2pk_signing_keys() {
305 let secret_hex =
306 "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".to_string();
307 let options = SendOptions {
308 p2pk_signing_keys: vec![SecretKey {
309 hex: secret_hex.clone(),
310 }],
311 ..Default::default()
312 };
313
314 let to_json = options.to_json().unwrap();
315 let encoded = crate::types::wallet::encode_send_options(options.clone()).unwrap();
316 let debug = format!("{:?}", options);
317
318 assert!(to_json.contains(&secret_hex));
319 assert!(to_json.contains("p2pk_signing_keys"));
320 assert!(encoded.contains(&secret_hex));
321 assert!(encoded.contains("p2pk_signing_keys"));
322 assert!(!debug.contains(&secret_hex));
323 assert!(debug.contains("[redacted]"));
324
325 let decoded = crate::types::wallet::decode_send_options(encoded).unwrap();
326
327 assert_eq!(decoded.p2pk_signing_keys.len(), 1);
328 assert_eq!(decoded.p2pk_signing_keys[0].hex, secret_hex);
329 }
330
331 #[test]
332 fn test_send_options_json_still_decodes_p2pk_signing_keys() {
333 let secret_hex = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
334 let json = format!(
335 r#"{{
336 "memo": null,
337 "conditions": null,
338 "amount_split_target": "None",
339 "send_kind": "OnlineExact",
340 "include_fee": false,
341 "use_p2bk": false,
342 "max_proofs": null,
343 "metadata": {{}},
344 "p2pk_signing_keys": ["{}"],
345 "p2pk_locked_proof_send_mode": "SignAndSend"
346 }}"#,
347 secret_hex
348 );
349
350 let options = crate::types::wallet::decode_send_options(json).unwrap();
351
352 assert_eq!(options.p2pk_signing_keys.len(), 1);
353 assert_eq!(options.p2pk_signing_keys[0].hex, secret_hex);
354 assert_eq!(
355 options.p2pk_locked_proof_send_mode,
356 P2PKLockedProofSendMode::SignAndSend
357 );
358 }
359
360 #[test]
361 fn test_receive_options_json_preserves_p2pk_signing_keys() {
362 let secret_hex =
363 "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".to_string();
364 let options = ReceiveOptions {
365 p2pk_signing_keys: vec![SecretKey {
366 hex: secret_hex.clone(),
367 }],
368 ..Default::default()
369 };
370
371 let to_json = options.to_json().unwrap();
372 let encoded = crate::types::wallet::encode_receive_options(options.clone()).unwrap();
373 let debug = format!("{:?}", options);
374
375 assert!(to_json.contains(&secret_hex));
376 assert!(to_json.contains("p2pk_signing_keys"));
377 assert!(encoded.contains(&secret_hex));
378 assert!(encoded.contains("p2pk_signing_keys"));
379 assert!(!debug.contains(&secret_hex));
380 assert!(debug.contains("[redacted]"));
381
382 let decoded = crate::types::wallet::decode_receive_options(encoded).unwrap();
383
384 assert_eq!(decoded.p2pk_signing_keys.len(), 1);
385 assert_eq!(decoded.p2pk_signing_keys[0].hex, secret_hex);
386 }
387
388 #[test]
389 fn test_send_options_json_defaults_new_p2pk_fields() {
390 let json = r#"{
391 "memo": null,
392 "conditions": null,
393 "amount_split_target": "None",
394 "send_kind": "OnlineExact",
395 "include_fee": false,
396 "use_p2bk": false,
397 "max_proofs": null,
398 "metadata": {}
399 }"#;
400
401 let options = crate::types::wallet::decode_send_options(json.to_string()).unwrap();
402
403 assert!(options.p2pk_signing_keys.is_empty());
404 assert_eq!(
405 options.p2pk_locked_proof_send_mode,
406 P2PKLockedProofSendMode::Swap
407 );
408 }
409
410 #[test]
411 fn test_proof_with_invalid_dleq_returns_error() {
412 let proof = Proof {
413 amount: Amount::new(1),
414 secret: "test-secret".to_string(),
415 c: "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc".to_string(),
416 keyset_id: "009a1f293253e41e".to_string(),
417 witness: None,
418 dleq: Some(ProofDleq {
419 e: "z".repeat(64),
420 s: "a".repeat(64),
421 r: "b".repeat(64),
422 }),
423 p2pk_e: None,
424 };
425
426 let result: Result<cdk::nuts::Proof, _> = proof.try_into();
427
428 assert!(result.is_err());
429 }
430
431 #[test]
432 fn test_blind_signature_dleq_invalid_hex_returns_error() {
433 let dleq = BlindSignatureDleq {
434 e: "z".repeat(64),
435 s: "a".repeat(64),
436 };
437
438 let result: Result<cdk::nuts::BlindSignatureDleq, _> = dleq.try_into();
439
440 assert!(result.is_err());
441 }
442
443 #[test]
444 fn test_transaction_invalid_saga_id_returns_error() {
445 let transaction = Transaction {
446 id: TransactionId {
447 hex: "a".repeat(64),
448 },
449 mint_url: MintUrl {
450 url: "https://mint.example.com".to_string(),
451 },
452 direction: TransactionDirection::Outgoing,
453 amount: Amount::new(100),
454 fee: Amount::new(0),
455 unit: CurrencyUnit::Sat,
456 ys: vec![],
457 timestamp: 0,
458 memo: None,
459 metadata: Default::default(),
460 quote_id: None,
461 payment_request: None,
462 payment_proof: None,
463 payment_method: None,
464 saga_id: Some("not-a-valid-uuid".to_string()),
465 };
466
467 let result: Result<cdk::wallet::types::Transaction, _> = transaction.try_into();
468
469 assert!(result.is_err());
470 }
471
472 #[test]
473 fn test_mint_quote_pending_state_does_not_inflate_mintable() {
474 let ffi_quote = MintQuote {
475 id: "test-quote".to_string(),
476 amount: Some(Amount::new(100)),
477 unit: CurrencyUnit::Sat,
478 request: "lnbc1...".to_string(),
479 state: QuoteState::Pending,
480 expiry: u64::MAX,
481 mint_url: MintUrl::new("https://mint.example.com".to_string())
482 .expect("valid mint URL should convert successfully"),
483 amount_issued: Amount::zero(),
484 amount_paid: Amount::zero(),
485 estimated_blocks: None,
486 payment_method: PaymentMethod::Bolt11,
487 secret_key: None,
488 used_by_operation: None,
489 version: 0,
490 };
491
492 let mintable =
493 mint_quote_amount_mintable(&ffi_quote).expect("valid mint quote should convert");
494
495 assert_eq!(mintable.value, 0);
496 }
497
498 #[test]
499 fn test_wallet_config() {
500 let config = WalletConfig {
501 target_proof_count: None,
502 };
503 assert!(config.target_proof_count.is_none());
504
505 let config_with_values = WalletConfig {
506 target_proof_count: Some(5),
507 };
508 assert_eq!(config_with_values.target_proof_count, Some(5));
509 }
510
511 #[test]
512 fn test_mnemonic_generation() {
513 let mnemonic = generate_mnemonic().unwrap();
515 assert!(!mnemonic.is_empty());
516 assert_eq!(mnemonic.split_whitespace().count(), 12);
517
518 use bip39::Mnemonic;
520 let parsed = Mnemonic::parse(&mnemonic);
521 assert!(parsed.is_ok());
522 }
523
524 #[test]
525 fn test_mnemonic_validation() {
526 let mnemonic = generate_mnemonic().unwrap();
528 use bip39::Mnemonic;
529 let parsed = Mnemonic::parse(&mnemonic);
530 assert!(parsed.is_ok());
531
532 let invalid_mnemonic = "invalid mnemonic phrase that should not work";
534 let parsed_invalid = Mnemonic::parse(invalid_mnemonic);
535 assert!(parsed_invalid.is_err());
536
537 let mnemonic_12 = generate_mnemonic().unwrap();
539 assert_eq!(mnemonic_12.split_whitespace().count(), 12);
540 }
541
542 #[test]
543 fn test_mnemonic_to_entropy() {
544 let mnemonic = generate_mnemonic().unwrap();
546 let entropy = mnemonic_to_entropy(mnemonic.clone()).unwrap();
547
548 assert_eq!(entropy.len(), 16);
550
551 use bip39::Mnemonic;
553 let recreated_mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
554 assert_eq!(recreated_mnemonic.to_string(), mnemonic);
555
556 let invalid_result = mnemonic_to_entropy("invalid mnemonic".to_string());
558 assert!(invalid_result.is_err());
559 }
560
561 #[test]
562 fn test_keyset_info_try_from_rejects_invalid_id() {
563 let err = <cdk::nuts::KeySetInfo as TryFrom<KeySetInfo>>::try_from(KeySetInfo {
564 id: "invalid".to_string(),
565 unit: CurrencyUnit::Sat,
566 active: true,
567 input_fee_ppk: 0,
568 })
569 .expect_err("invalid keyset ID should return an error");
570
571 assert!(err.to_string().contains("Invalid keyset ID"));
572 }
573
574 #[test]
575 fn test_keyset_info_try_from_rejects_empty_id() {
576 let err = <cdk::nuts::KeySetInfo as TryFrom<KeySetInfo>>::try_from(KeySetInfo {
577 id: String::new(),
578 unit: CurrencyUnit::Sat,
579 active: true,
580 input_fee_ppk: 0,
581 })
582 .expect_err("empty keyset ID should return an error");
583
584 assert!(err.to_string().contains("Invalid keyset ID"));
585 }
586
587 #[test]
588 fn test_id_try_from_rejects_invalid_hex() {
589 let err = <cdk::nuts::Id as TryFrom<Id>>::try_from(Id {
590 hex: "invalid".to_string(),
591 })
592 .expect_err("invalid ID hex should return an error");
593
594 assert!(err.to_string().contains("Invalid ID hex"));
595 }
596
597 #[test]
598 fn test_id_try_from_accepts_valid_hex() {
599 let id: cdk::nuts::Id = Id {
600 hex: "009a1f293253e41e".to_string(),
601 }
602 .try_into()
603 .expect("valid ID hex should convert successfully");
604
605 assert_eq!(id.to_string(), "009a1f293253e41e");
606 }
607}