multimint-npubcash.rs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. //! Example: MultiMint Wallet with NpubCash - Switching Active Mints
  2. //!
  3. //! This example demonstrates:
  4. //! 1. Creating a MultiMintWallet with multiple mints
  5. //! 2. Using NpubCash integration with the MultiMintWallet API
  6. //! 3. Switching the active mint for NpubCash deposits
  7. //! 4. Receiving payments to different mints and verifying balances
  8. //!
  9. //! Key concept: Since all wallets in a MultiMintWallet share the same seed, they all
  10. //! derive the same Nostr keypair. This means your npub.cash address stays the same,
  11. //! but you can change which mint receives the deposits.
  12. use std::sync::Arc;
  13. use std::time::Duration;
  14. use cdk::amount::SplitTarget;
  15. use cdk::mint_url::MintUrl;
  16. use cdk::nuts::nut00::ProofsMethods;
  17. use cdk::nuts::CurrencyUnit;
  18. use cdk::wallet::WalletRepositoryBuilder;
  19. use cdk::StreamExt;
  20. use cdk_sqlite::wallet::memory;
  21. use nostr_sdk::ToBech32;
  22. const NPUBCASH_URL: &str = "https://npubx.cash";
  23. const MINT_URL_1: &str = "https://fake.thesimplekid.dev";
  24. const MINT_URL_2: &str = "https://testnut.cashu.space";
  25. const PAYMENT_AMOUNT_MSATS: u64 = 10000; // 10 sats
  26. #[tokio::main]
  27. async fn main() -> Result<(), Box<dyn std::error::Error>> {
  28. println!("=== MultiMint Wallet with NpubCash Example ===\n");
  29. // -------------------------------------------------------------------------
  30. // Step 1: Create MultiMintWallet and add mints
  31. // -------------------------------------------------------------------------
  32. println!("Step 1: Setting up MultiMintWallet...\n");
  33. let seed: [u8; 64] = {
  34. let mut s = [0u8; 64];
  35. use std::time::{SystemTime, UNIX_EPOCH};
  36. let timestamp = SystemTime::now()
  37. .duration_since(UNIX_EPOCH)
  38. .unwrap()
  39. .as_nanos();
  40. for (i, byte) in s.iter_mut().enumerate() {
  41. *byte = ((timestamp >> (i % 16)) & 0xFF) as u8;
  42. }
  43. s
  44. };
  45. let localstore = memory::empty().await?;
  46. let wallet_repository = WalletRepositoryBuilder::new()
  47. .localstore(Arc::new(localstore))
  48. .seed(seed)
  49. .build()
  50. .await?;
  51. let mint_url_1: MintUrl = MINT_URL_1.parse()?;
  52. let mint_url_2: MintUrl = MINT_URL_2.parse()?;
  53. wallet_repository.add_wallet(mint_url_1.clone()).await?;
  54. wallet_repository.add_wallet(mint_url_2.clone()).await?;
  55. println!(" Added mints: {}, {}\n", mint_url_1, mint_url_2);
  56. // -------------------------------------------------------------------------
  57. // Step 2: Enable NpubCash on mint 1
  58. // -------------------------------------------------------------------------
  59. println!("Step 2: Enabling NpubCash on mint 1...\n");
  60. let wallet = wallet_repository
  61. .get_wallet(&mint_url_1.clone(), &CurrencyUnit::Sat)
  62. .await?;
  63. wallet.enable_npubcash(NPUBCASH_URL.to_string()).await?;
  64. let keys = wallet.get_npubcash_keys().unwrap();
  65. let npub = keys.public_key().to_bech32()?;
  66. let display_url = NPUBCASH_URL.trim_start_matches("https://");
  67. println!(" Your npub.cash address: {}@{}", npub, display_url);
  68. println!(" Active mint: {}\n", mint_url_1);
  69. // -------------------------------------------------------------------------
  70. // Step 3: Request and receive payment on mint 1
  71. // -------------------------------------------------------------------------
  72. println!("Step 3: Receiving payment on mint 1...\n");
  73. request_invoice(&npub, PAYMENT_AMOUNT_MSATS).await?;
  74. println!(" Waiting for payment...");
  75. let mut stream = wallet.npubcash_proof_stream(
  76. SplitTarget::default(),
  77. None, // no spending conditions
  78. Duration::from_secs(1),
  79. );
  80. let (_, proofs_1) = stream.next().await.ok_or("Stream ended unexpectedly")??;
  81. let amount_1: u64 = proofs_1.total_amount()?.into();
  82. println!(" Received {} sats on mint 1!\n", amount_1);
  83. // -------------------------------------------------------------------------
  84. // Step 4: Switch to mint 2 and receive payment
  85. // -------------------------------------------------------------------------
  86. println!("Step 4: Switching to mint 2 and receiving payment...\n");
  87. let wallet = wallet_repository
  88. .get_wallet(&mint_url_1.clone(), &CurrencyUnit::Sat)
  89. .await?;
  90. wallet.enable_npubcash(NPUBCASH_URL.to_string()).await?;
  91. println!(" Switched to mint: {}", mint_url_2);
  92. request_invoice(&npub, PAYMENT_AMOUNT_MSATS).await?;
  93. println!(" Waiting for payment...");
  94. // The stream is for the multimint wallet so it handles switching mints automatically
  95. let (_, proofs_2) = stream.next().await.ok_or("Stream ended unexpectedly")??;
  96. let amount_2: u64 = proofs_2.total_amount()?.into();
  97. println!(" Received {} sats on mint 2!\n", amount_2);
  98. // -------------------------------------------------------------------------
  99. // Step 5: Verify balances
  100. // -------------------------------------------------------------------------
  101. println!("Step 5: Verifying balances...\n");
  102. let balances = wallet_repository.get_balances().await?;
  103. for (mint, balance) in &balances {
  104. println!(" {}: {} sats", mint, balance);
  105. }
  106. let total = wallet.total_balance().await?;
  107. let expected_total = amount_1 + amount_2;
  108. println!(
  109. "\n Total: {} sats (expected: {} sats)",
  110. total, expected_total
  111. );
  112. println!(
  113. " Status: {}\n",
  114. if total == expected_total.into() {
  115. "OK"
  116. } else {
  117. "MISMATCH"
  118. }
  119. );
  120. Ok(())
  121. }
  122. /// Request an invoice via LNURL-pay
  123. async fn request_invoice(npub: &str, amount_msats: u64) -> Result<(), Box<dyn std::error::Error>> {
  124. let http_client = cdk_common::HttpClient::new();
  125. let lnurlp_url = format!("{}/.well-known/lnurlp/{}", NPUBCASH_URL, npub);
  126. let lnurlp_response: serde_json::Value = http_client.fetch(&lnurlp_url).await?;
  127. let callback = lnurlp_response["callback"]
  128. .as_str()
  129. .ok_or("No callback URL")?;
  130. let invoice_url = format!("{}?amount={}", callback, amount_msats);
  131. let invoice_response: serde_json::Value = http_client.fetch(&invoice_url).await?;
  132. let pr = invoice_response["pr"]
  133. .as_str()
  134. .ok_or("No payment request")?;
  135. println!(" Invoice: {}...", &pr[..50.min(pr.len())]);
  136. Ok(())
  137. }