human_readable_payment.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. //! # Human Readable Payment Example
  2. //!
  3. //! This example demonstrates how to use both BIP-353 and Lightning Address (LNURL-pay)
  4. //! with the CDK wallet. Both allow users to share simple email-like addresses instead
  5. //! of complex Bitcoin addresses or Lightning invoices.
  6. //!
  7. //! ## BIP-353 (Bitcoin URI Payment Instructions)
  8. //!
  9. //! BIP-353 uses DNS TXT records to resolve human-readable addresses to BOLT12 offers.
  10. //! 1. Parse a human-readable address like `user@domain.com`
  11. //! 2. Query DNS TXT records at `user.user._bitcoin-payment.domain.com`
  12. //! 3. Extract Lightning offers (BOLT12) from the TXT records
  13. //! 4. Use the offer to create a melt quote
  14. //!
  15. //! ## Lightning Address (LNURL-pay)
  16. //!
  17. //! Lightning Address uses HTTPS to fetch BOLT11 invoices.
  18. //! 1. Parse a Lightning address like `user@domain.com`
  19. //! 2. Query HTTPS endpoint at `https://domain.com/.well-known/lnurlp/user`
  20. //! 3. Get callback URL and amount constraints
  21. //! 4. Request BOLT11 invoice with the specified amount
  22. //!
  23. //! ## Unified API
  24. //!
  25. //! The `melt_human_readable_quote()` method automatically tries BIP-353 first
  26. //! (if the mint supports BOLT12), then falls back to Lightning Address if needed.
  27. //!
  28. //! ## Usage
  29. //!
  30. //! ```bash
  31. //! cargo run --example human_readable_payment --features="wallet bip353"
  32. //! ```
  33. use std::sync::Arc;
  34. use std::time::Duration;
  35. use cdk::amount::SplitTarget;
  36. use cdk::nuts::nut00::ProofsMethods;
  37. use cdk::nuts::{CurrencyUnit, PaymentMethod};
  38. use cdk::wallet::{Wallet, WalletTrait};
  39. use cdk::Amount;
  40. use cdk_sqlite::wallet::memory;
  41. use rand::random;
  42. #[tokio::main]
  43. async fn main() -> anyhow::Result<()> {
  44. println!("Human Readable Payment Example");
  45. println!("================================\n");
  46. // Example addresses
  47. let bip353_address = "tsk@thesimplekid.com";
  48. let lnurl_address =
  49. "npub1qjgcmlpkeyl8mdkvp4s0xls4ytcux6my606tgfx9xttut907h0zs76lgjw@npubx.cash";
  50. // Generate a random seed for the wallet
  51. let seed = random::<[u8; 64]>();
  52. // Mint URL and currency unit
  53. let mint_url = "https://fake.thesimplekid.dev";
  54. let unit = CurrencyUnit::Sat;
  55. let initial_amount = Amount::from(2000); // Start with 2000 sats (enough for both payments)
  56. // Initialize the memory store
  57. let localstore = Arc::new(memory::empty().await?);
  58. // Create a new wallet
  59. let wallet = Wallet::new(mint_url, unit, localstore, seed, None)?;
  60. println!("Step 1: Funding the wallet");
  61. println!("---------------------------");
  62. // First, we need to fund the wallet
  63. println!("Requesting mint quote for {} sats...", initial_amount);
  64. let mint_quote = wallet
  65. .mint_quote(PaymentMethod::BOLT12, Some(initial_amount), None, None)
  66. .await?;
  67. println!(
  68. "Pay this invoice to fund the wallet:\n{}",
  69. mint_quote.request
  70. );
  71. println!("\nQuote ID: {}", mint_quote.id);
  72. // Wait for payment and mint tokens automatically
  73. println!("\nWaiting for payment... (in real use, pay the above invoice)");
  74. let proofs = wallet
  75. .wait_and_mint_quote(
  76. mint_quote,
  77. SplitTarget::default(),
  78. None,
  79. Duration::from_secs(300), // 5 minutes timeout
  80. )
  81. .await?;
  82. let received_amount = proofs.total_amount()?;
  83. println!("✓ Successfully minted {} sats\n", received_amount);
  84. // ============================================================================
  85. // Part 1: BIP-353 Payment
  86. // ============================================================================
  87. println!("\n╔════════════════════════════════════════════════════════════════╗");
  88. println!("║ Part 1: BIP-353 Payment (BOLT12 Offer via DNS) ║");
  89. println!("╚════════════════════════════════════════════════════════════════╝\n");
  90. let bip353_amount_sats = 100; // Example: paying 100 sats
  91. println!("BIP-353 Address: {}", bip353_address);
  92. println!("Payment Amount: {} sats", bip353_amount_sats);
  93. println!("\nHow BIP-353 works:");
  94. println!("1. Parse address into user@domain");
  95. println!("2. Query DNS TXT records at: tsk.user._bitcoin-payment.thesimplekid.com");
  96. println!("3. Extract BOLT12 offer from DNS records");
  97. println!("4. Create melt quote with the offer\n");
  98. // Use the specific BIP353 method
  99. println!("Attempting BIP-353 payment...");
  100. match wallet
  101. .melt_bip353_quote(bip353_address, bip353_amount_sats * 1_000)
  102. .await
  103. {
  104. Ok(melt_quote) => {
  105. println!("✓ BIP-353 melt quote received:");
  106. println!(" Quote ID: {}", melt_quote.id);
  107. println!(" Amount: {} sats", melt_quote.amount);
  108. println!(" Fee Reserve: {} sats", melt_quote.fee_reserve);
  109. println!(" State: {}", melt_quote.state);
  110. println!(" Payment Method: {}", melt_quote.payment_method);
  111. // Prepare the payment - shows fees before confirming
  112. println!("\nPreparing payment...");
  113. match wallet
  114. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  115. .await
  116. {
  117. Ok(prepared) => {
  118. println!("✓ Prepared melt:");
  119. println!(" Amount: {} sats", prepared.amount());
  120. println!(" Total Fee: {} sats", prepared.total_fee());
  121. // Execute the payment
  122. println!("\nExecuting payment...");
  123. match prepared.confirm().await {
  124. Ok(confirmed) => {
  125. println!("✓ BIP-353 payment successful!");
  126. println!(" State: {:?}", confirmed.state());
  127. println!(" Amount paid: {} sats", confirmed.amount());
  128. println!(" Fee paid: {} sats", confirmed.fee_paid());
  129. if let Some(preimage) = confirmed.payment_proof() {
  130. println!(" Payment preimage: {}", preimage);
  131. }
  132. }
  133. Err(e) => {
  134. println!("✗ BIP-353 payment failed: {}", e);
  135. }
  136. }
  137. }
  138. Err(e) => {
  139. println!("✗ Failed to prepare melt: {}", e);
  140. }
  141. }
  142. }
  143. Err(e) => {
  144. println!("✗ Failed to get BIP-353 melt quote: {}", e);
  145. println!("\nPossible reasons:");
  146. println!(" • DNS resolution failed or no DNS records found");
  147. println!(" • No Lightning offer (BOLT12) in DNS TXT records");
  148. println!(" • DNSSEC validation failed");
  149. println!(" • Mint doesn't support BOLT12");
  150. println!(" • Network connectivity issues");
  151. }
  152. }
  153. // ============================================================================
  154. // Part 2: Lightning Address (LNURL-pay) Payment
  155. // ============================================================================
  156. println!("\n\n╔════════════════════════════════════════════════════════════════╗");
  157. println!("║ Part 2: Lightning Address Payment (BOLT11 via LNURL-pay) ║");
  158. println!("╚════════════════════════════════════════════════════════════════╝\n");
  159. let lnurl_amount_sats = 100; // Example: paying 100 sats
  160. println!("Lightning Address: {}", lnurl_address);
  161. println!("Payment Amount: {} sats", lnurl_amount_sats);
  162. println!("\nHow Lightning Address works:");
  163. println!("1. Parse address into user@domain");
  164. println!("2. Query HTTPS: https://npubx.cash/.well-known/lnurlp/npub1qj...");
  165. println!("3. Get callback URL and amount constraints");
  166. println!("4. Request BOLT11 invoice for the amount");
  167. println!("5. Create melt quote with the invoice\n");
  168. // Use the specific Lightning Address method
  169. println!("Attempting Lightning Address payment...");
  170. match wallet
  171. .melt_lightning_address_quote(lnurl_address, lnurl_amount_sats * 1_000)
  172. .await
  173. {
  174. Ok(melt_quote) => {
  175. println!("✓ Lightning Address melt quote received:");
  176. println!(" Quote ID: {}", melt_quote.id);
  177. println!(" Amount: {} sats", melt_quote.amount);
  178. println!(" Fee Reserve: {} sats", melt_quote.fee_reserve);
  179. println!(" State: {}", melt_quote.state);
  180. println!(" Payment Method: {}", melt_quote.payment_method);
  181. // Prepare the payment - shows fees before confirming
  182. println!("\nPreparing payment...");
  183. match wallet
  184. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  185. .await
  186. {
  187. Ok(prepared) => {
  188. println!("✓ Prepared melt:");
  189. println!(" Amount: {} sats", prepared.amount());
  190. println!(" Total Fee: {} sats", prepared.total_fee());
  191. // Execute the payment
  192. println!("\nExecuting payment...");
  193. match prepared.confirm().await {
  194. Ok(confirmed) => {
  195. println!("✓ Lightning Address payment successful!");
  196. println!(" State: {:?}", confirmed.state());
  197. println!(" Amount paid: {} sats", confirmed.amount());
  198. println!(" Fee paid: {} sats", confirmed.fee_paid());
  199. if let Some(preimage) = confirmed.payment_proof() {
  200. println!(" Payment preimage: {}", preimage);
  201. }
  202. }
  203. Err(e) => {
  204. println!("✗ Lightning Address payment failed: {}", e);
  205. }
  206. }
  207. }
  208. Err(e) => {
  209. println!("✗ Failed to prepare melt: {}", e);
  210. }
  211. }
  212. }
  213. Err(e) => {
  214. println!("✗ Failed to get Lightning Address melt quote: {}", e);
  215. println!("\nPossible reasons:");
  216. println!(" • HTTPS request to .well-known/lnurlp failed");
  217. println!(" • Invalid Lightning Address format");
  218. println!(" • Amount outside min/max constraints");
  219. println!(" • Service unavailable or network issues");
  220. }
  221. }
  222. // ============================================================================
  223. // Part 3: Unified Human Readable API (Smart Fallback)
  224. // ============================================================================
  225. println!("\n\n╔════════════════════════════════════════════════════════════════╗");
  226. println!("║ Part 3: Unified API (Automatic BIP-353 → LNURL Fallback) ║");
  227. println!("╚════════════════════════════════════════════════════════════════╝\n");
  228. println!("The `melt_human_readable_quote()` method intelligently chooses:");
  229. println!("1. If mint supports BOLT12 AND address has BIP-353 DNS: Use BIP-353");
  230. println!("2. If BIP-353 DNS fails OR address has no DNS: Fall back to LNURL");
  231. println!("3. If mint doesn't support BOLT12: Use LNURL directly\n");
  232. // Test 1: Address with BIP-353 support (has DNS records)
  233. let unified_amount_sats = 50;
  234. println!("Test 1: Address with BIP-353 DNS support");
  235. println!("Address: {}", bip353_address);
  236. println!("Payment Amount: {} sats", unified_amount_sats);
  237. println!("Expected: BIP-353 (BOLT12) via DNS resolution\n");
  238. println!("Attempting unified payment...");
  239. match wallet
  240. .melt_human_readable_quote(bip353_address, unified_amount_sats * 1_000)
  241. .await
  242. {
  243. Ok(melt_quote) => {
  244. println!("✓ Unified melt quote received:");
  245. println!(" Quote ID: {}", melt_quote.id);
  246. println!(" Amount: {} sats", melt_quote.amount);
  247. println!(" Fee Reserve: {} sats", melt_quote.fee_reserve);
  248. println!(" Payment Method: {}", melt_quote.payment_method);
  249. let method_str = melt_quote.payment_method.to_string().to_lowercase();
  250. let used_method = if method_str.contains("bolt12") {
  251. "BIP-353 (BOLT12)"
  252. } else if method_str.contains("bolt11") {
  253. "Lightning Address (LNURL-pay)"
  254. } else {
  255. "Unknown method"
  256. };
  257. println!("\n → Used: {}", used_method);
  258. }
  259. Err(e) => {
  260. println!("✗ Failed to get unified melt quote: {}", e);
  261. println!(" Both BIP-353 and Lightning Address resolution failed");
  262. }
  263. }
  264. // Test 2: Address without BIP-353 support (LNURL only)
  265. println!("\n\nTest 2: Address without BIP-353 (LNURL-only)");
  266. println!("Address: {}", lnurl_address);
  267. println!("Payment Amount: {} sats", unified_amount_sats);
  268. println!("Expected: Lightning Address (LNURL-pay) fallback\n");
  269. println!("Attempting unified payment...");
  270. match wallet
  271. .melt_human_readable_quote(lnurl_address, unified_amount_sats * 1_000)
  272. .await
  273. {
  274. Ok(melt_quote) => {
  275. println!("✓ Unified melt quote received:");
  276. println!(" Quote ID: {}", melt_quote.id);
  277. println!(" Amount: {} sats", melt_quote.amount);
  278. println!(" Fee Reserve: {} sats", melt_quote.fee_reserve);
  279. println!(" Payment Method: {}", melt_quote.payment_method);
  280. let method_str = melt_quote.payment_method.to_string().to_lowercase();
  281. let used_method = if method_str.contains("bolt12") {
  282. "BIP-353 (BOLT12)"
  283. } else if method_str.contains("bolt11") {
  284. "Lightning Address (LNURL-pay)"
  285. } else {
  286. "Unknown method"
  287. };
  288. println!("\n → Used: {}", used_method);
  289. println!("\n Note: This address doesn't have BIP-353 DNS records,");
  290. println!(" so it automatically fell back to LNURL-pay.");
  291. }
  292. Err(e) => {
  293. println!("✗ Failed to get unified melt quote: {}", e);
  294. println!(" Both BIP-353 and Lightning Address resolution failed");
  295. }
  296. }
  297. Ok(())
  298. }