cdk_fake_wallet/
lib.rs

1//! CDK Fake LN Backend
2//!
3//! Used for testing where quotes are auto filled.
4//!
5//! The fake wallet now includes a secondary repayment system that continuously repays any-amount
6//! invoices (amount = 0) at random intervals between 30 seconds and 3 minutes to simulate
7//! real-world behavior where invoices might get multiple payments. Payments continue to be
8//! processed until they are evicted from the queue when the queue reaches its maximum size
9//! (default 100 items). This is in addition to the original immediate payment processing
10//! which is maintained for all invoice types.
11
12#![doc = include_str!("../README.md")]
13#![warn(missing_docs)]
14#![warn(rustdoc::bare_urls)]
15
16use std::cmp::max;
17use std::collections::{HashMap, HashSet, VecDeque};
18use std::pin::Pin;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::Arc;
21use std::time::{Duration, Instant};
22
23use async_trait::async_trait;
24use bitcoin::hashes::{sha256, Hash};
25use bitcoin::secp256k1::{Secp256k1, SecretKey};
26use cdk_common::amount::{to_unit, Amount};
27use cdk_common::common::FeeReserve;
28use cdk_common::ensure_cdk;
29use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
30use cdk_common::payment::{
31    self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
32    MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
33    PaymentQuoteResponse, WaitPaymentResponse,
34};
35use error::Error;
36use futures::stream::StreamExt;
37use futures::Stream;
38use lightning::offers::offer::OfferBuilder;
39use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
40use serde::{Deserialize, Serialize};
41use serde_json::Value;
42use tokio::sync::{Mutex, RwLock};
43use tokio::time;
44use tokio_stream::wrappers::ReceiverStream;
45use tokio_util::sync::CancellationToken;
46use tracing::instrument;
47use uuid::Uuid;
48
49pub mod error;
50
51/// Default maximum size for the secondary repayment queue
52const DEFAULT_REPAY_QUEUE_MAX_SIZE: usize = 100;
53
54/// Cache duration for exchange rate (5 minutes)
55const RATE_CACHE_DURATION: Duration = Duration::from_secs(300);
56
57/// Mempool.space prices API response structure
58#[derive(Debug, Deserialize)]
59struct MempoolPricesResponse {
60    #[serde(rename = "USD")]
61    usd: f64,
62    #[serde(rename = "EUR")]
63    eur: f64,
64}
65
66/// Exchange rate cache with built-in fallback rates
67#[derive(Debug, Clone)]
68struct ExchangeRateCache {
69    rates: Arc<Mutex<Option<(MempoolPricesResponse, Instant)>>>,
70}
71
72impl ExchangeRateCache {
73    fn new() -> Self {
74        Self {
75            rates: Arc::new(Mutex::new(None)),
76        }
77    }
78
79    /// Get current BTC rate for the specified currency with caching and fallback
80    async fn get_btc_rate(&self, currency: &CurrencyUnit) -> Result<f64, Error> {
81        // Return cached rate if still valid
82        {
83            let cached_rates = self.rates.lock().await;
84            if let Some((rates, timestamp)) = &*cached_rates {
85                if timestamp.elapsed() < RATE_CACHE_DURATION {
86                    return Self::rate_for_currency(rates, currency);
87                }
88            }
89        }
90
91        // Try to fetch fresh rates, fallback on error
92        match self.fetch_fresh_rate(currency).await {
93            Ok(rate) => Ok(rate),
94            Err(e) => {
95                tracing::warn!(
96                    "Failed to fetch exchange rates, using fallback for {:?}: {}",
97                    currency,
98                    e
99                );
100                Self::fallback_rate(currency)
101            }
102        }
103    }
104
105    /// Fetch fresh rate and update cache
106    async fn fetch_fresh_rate(&self, currency: &CurrencyUnit) -> Result<f64, Error> {
107        let url = "https://mempool.space/api/v1/prices";
108        let response = reqwest::get(url)
109            .await
110            .map_err(|_| Error::UnknownInvoiceAmount)?
111            .json::<MempoolPricesResponse>()
112            .await
113            .map_err(|_| Error::UnknownInvoiceAmount)?;
114
115        let rate = Self::rate_for_currency(&response, currency)?;
116        *self.rates.lock().await = Some((response, Instant::now()));
117        Ok(rate)
118    }
119
120    fn rate_for_currency(
121        rates: &MempoolPricesResponse,
122        currency: &CurrencyUnit,
123    ) -> Result<f64, Error> {
124        match currency {
125            CurrencyUnit::Usd => Ok(rates.usd),
126            CurrencyUnit::Eur => Ok(rates.eur),
127            _ => Err(Error::UnknownInvoiceAmount),
128        }
129    }
130
131    fn fallback_rate(currency: &CurrencyUnit) -> Result<f64, Error> {
132        match currency {
133            CurrencyUnit::Usd => Ok(110_000.0), // $110k per BTC
134            CurrencyUnit::Eur => Ok(95_000.0),  // €95k per BTC
135            _ => Err(Error::UnknownInvoiceAmount),
136        }
137    }
138}
139
140async fn convert_currency_amount(
141    amount: u64,
142    from_unit: &CurrencyUnit,
143    target_unit: &CurrencyUnit,
144    rate_cache: &ExchangeRateCache,
145) -> Result<Amount, Error> {
146    use CurrencyUnit::*;
147
148    // Try basic unit conversion first (handles SAT/MSAT and same-unit conversions)
149    if let Ok(converted) = to_unit(amount, from_unit, target_unit) {
150        return Ok(converted);
151    }
152
153    // Handle fiat <-> bitcoin conversions that require exchange rates
154    match (from_unit, target_unit) {
155        // Fiat to Bitcoin conversions
156        (Usd | Eur, Sat) => {
157            let rate = rate_cache.get_btc_rate(from_unit).await?;
158            let fiat_amount = amount as f64 / 100.0; // cents to dollars/euros
159            Ok(Amount::from(
160                (fiat_amount / rate * 100_000_000.0).round() as u64
161            )) // to sats
162        }
163        (Usd | Eur, Msat) => {
164            let rate = rate_cache.get_btc_rate(from_unit).await?;
165            let fiat_amount = amount as f64 / 100.0; // cents to dollars/euros
166            Ok(Amount::from(
167                (fiat_amount / rate * 100_000_000_000.0).round() as u64,
168            )) // to msats
169        }
170
171        // Bitcoin to fiat conversions
172        (Sat, Usd | Eur) => {
173            let rate = rate_cache.get_btc_rate(target_unit).await?;
174            let btc_amount = amount as f64 / 100_000_000.0; // sats to BTC
175            Ok(Amount::from((btc_amount * rate * 100.0).round() as u64)) // to cents
176        }
177        (Msat, Usd | Eur) => {
178            let rate = rate_cache.get_btc_rate(target_unit).await?;
179            let btc_amount = amount as f64 / 100_000_000_000.0; // msats to BTC
180            Ok(Amount::from((btc_amount * rate * 100.0).round() as u64)) // to cents
181        }
182
183        _ => Err(Error::UnknownInvoiceAmount), // Unsupported conversion
184    }
185}
186
187/// Secondary repayment queue manager for any-amount invoices
188#[derive(Debug, Clone)]
189struct SecondaryRepaymentQueue {
190    queue: Arc<Mutex<VecDeque<PaymentIdentifier>>>,
191    max_size: usize,
192    sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
193    unit: CurrencyUnit,
194}
195
196impl SecondaryRepaymentQueue {
197    fn new(
198        max_size: usize,
199        sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
200        unit: CurrencyUnit,
201    ) -> Self {
202        let queue = Arc::new(Mutex::new(VecDeque::new()));
203        let repayment_queue = Self {
204            queue: queue.clone(),
205            max_size,
206            sender,
207            unit,
208        };
209
210        // Start the background secondary repayment processor
211        repayment_queue.start_secondary_repayment_processor();
212
213        repayment_queue
214    }
215
216    /// Add a payment to the secondary repayment queue
217    async fn enqueue_for_repayment(&self, payment: PaymentIdentifier) {
218        let mut queue = self.queue.lock().await;
219
220        // If queue is at max capacity, remove the oldest item
221        if queue.len() >= self.max_size {
222            if let Some(dropped) = queue.pop_front() {
223                tracing::debug!(
224                    "Secondary repayment queue at capacity, dropping oldest payment: {:?}",
225                    dropped
226                );
227            }
228        }
229
230        queue.push_back(payment);
231        tracing::debug!(
232            "Added payment to secondary repayment queue, current size: {}",
233            queue.len()
234        );
235    }
236
237    /// Start the background task that randomly processes secondary repayments from the queue
238    fn start_secondary_repayment_processor(&self) {
239        let queue = self.queue.clone();
240        let sender = self.sender.clone();
241        let unit = self.unit.clone();
242
243        tokio::spawn(async move {
244            use bitcoin::secp256k1::rand::rngs::OsRng;
245            use bitcoin::secp256k1::rand::Rng;
246            let mut rng = OsRng;
247
248            loop {
249                // Wait for a random interval between 30 seconds and 3 minutes (180 seconds)
250                let delay_secs = rng.gen_range(1..=3);
251                time::sleep(time::Duration::from_secs(delay_secs)).await;
252
253                // Try to process a random payment from the queue without removing it
254                let payment_to_process = {
255                    let q = queue.lock().await;
256                    if q.is_empty() {
257                        None
258                    } else {
259                        // Pick a random index from the queue but don't remove it
260                        let index = rng.gen_range(0..q.len());
261                        q.get(index).cloned()
262                    }
263                };
264
265                if let Some(payment) = payment_to_process {
266                    // Generate a random amount for this secondary payment (same range as initial payment: 1-1000)
267                    let random_amount: u64 = rng.gen_range(1..=1000);
268
269                    // Create amount based on unit, ensuring minimum of 1 sat worth
270                    let secondary_amount = match &unit {
271                        CurrencyUnit::Sat => Amount::from(random_amount),
272                        CurrencyUnit::Msat => Amount::from(u64::max(random_amount * 1000, 1000)),
273                        _ => Amount::from(u64::max(random_amount, 1)), // fallback
274                    };
275
276                    // Generate a unique payment identifier for this secondary payment
277                    // We'll create a new payment hash by appending a timestamp and random bytes
278                    use bitcoin::hashes::{sha256, Hash};
279                    let mut random_bytes = [0u8; 16];
280                    rng.fill(&mut random_bytes);
281                    let timestamp = std::time::SystemTime::now()
282                        .duration_since(std::time::UNIX_EPOCH)
283                        .unwrap()
284                        .as_nanos() as u64;
285
286                    // Create a unique hash combining the original payment identifier, timestamp, and random bytes
287                    let mut hasher_input = Vec::new();
288                    hasher_input.extend_from_slice(payment.to_string().as_bytes());
289                    hasher_input.extend_from_slice(&timestamp.to_le_bytes());
290                    hasher_input.extend_from_slice(&random_bytes);
291
292                    let unique_hash = sha256::Hash::hash(&hasher_input);
293                    let unique_payment_id = PaymentIdentifier::PaymentHash(*unique_hash.as_ref());
294
295                    tracing::info!(
296                        "Processing secondary repayment: original={:?}, new_id={:?}, amount={}",
297                        payment,
298                        unique_payment_id,
299                        secondary_amount
300                    );
301
302                    // Send the payment notification using the original payment identifier
303                    // The mint will process this through the normal payment stream
304                    let secondary_response = WaitPaymentResponse {
305                        payment_identifier: payment.clone(),
306                        payment_amount: secondary_amount,
307                        unit: unit.clone(),
308                        payment_id: unique_payment_id.to_string(),
309                    };
310
311                    if let Err(e) = sender.send(secondary_response).await {
312                        tracing::error!(
313                            "Failed to send secondary repayment notification for {:?}: {}",
314                            unique_payment_id,
315                            e
316                        );
317                    }
318                }
319            }
320        });
321    }
322}
323
324/// Fake Wallet
325#[derive(Clone)]
326pub struct FakeWallet {
327    fee_reserve: FeeReserve,
328    sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
329    receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<WaitPaymentResponse>>>>,
330    payment_states: Arc<Mutex<HashMap<String, (MeltQuoteState, Amount)>>>,
331    failed_payment_check: Arc<Mutex<HashSet<String>>>,
332    payment_delay: u64,
333    wait_invoice_cancel_token: CancellationToken,
334    wait_invoice_is_active: Arc<AtomicBool>,
335    incoming_payments: Arc<RwLock<HashMap<PaymentIdentifier, Vec<WaitPaymentResponse>>>>,
336    unit: CurrencyUnit,
337    secondary_repayment_queue: SecondaryRepaymentQueue,
338    exchange_rate_cache: ExchangeRateCache,
339}
340
341impl FakeWallet {
342    /// Create new [`FakeWallet`]
343    pub fn new(
344        fee_reserve: FeeReserve,
345        payment_states: HashMap<String, (MeltQuoteState, Amount)>,
346        fail_payment_check: HashSet<String>,
347        payment_delay: u64,
348        unit: CurrencyUnit,
349    ) -> Self {
350        Self::new_with_repay_queue_size(
351            fee_reserve,
352            payment_states,
353            fail_payment_check,
354            payment_delay,
355            unit,
356            DEFAULT_REPAY_QUEUE_MAX_SIZE,
357        )
358    }
359
360    /// Create new [`FakeWallet`] with custom secondary repayment queue size
361    pub fn new_with_repay_queue_size(
362        fee_reserve: FeeReserve,
363        payment_states: HashMap<String, (MeltQuoteState, Amount)>,
364        fail_payment_check: HashSet<String>,
365        payment_delay: u64,
366        unit: CurrencyUnit,
367        repay_queue_max_size: usize,
368    ) -> Self {
369        let (sender, receiver) = tokio::sync::mpsc::channel(8);
370        let incoming_payments = Arc::new(RwLock::new(HashMap::new()));
371
372        let secondary_repayment_queue =
373            SecondaryRepaymentQueue::new(repay_queue_max_size, sender.clone(), unit.clone());
374
375        Self {
376            fee_reserve,
377            sender,
378            receiver: Arc::new(Mutex::new(Some(receiver))),
379            payment_states: Arc::new(Mutex::new(payment_states)),
380            failed_payment_check: Arc::new(Mutex::new(fail_payment_check)),
381            payment_delay,
382            wait_invoice_cancel_token: CancellationToken::new(),
383            wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
384            incoming_payments,
385            unit,
386            secondary_repayment_queue,
387            exchange_rate_cache: ExchangeRateCache::new(),
388        }
389    }
390}
391
392/// Struct for signaling what methods should respond via invoice description
393#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
394pub struct FakeInvoiceDescription {
395    /// State to be returned from pay invoice state
396    pub pay_invoice_state: MeltQuoteState,
397    /// State to be returned by check payment state
398    pub check_payment_state: MeltQuoteState,
399    /// Should pay invoice error
400    pub pay_err: bool,
401    /// Should check failure
402    pub check_err: bool,
403}
404
405impl Default for FakeInvoiceDescription {
406    fn default() -> Self {
407        Self {
408            pay_invoice_state: MeltQuoteState::Paid,
409            check_payment_state: MeltQuoteState::Paid,
410            pay_err: false,
411            check_err: false,
412        }
413    }
414}
415
416#[async_trait]
417impl MintPayment for FakeWallet {
418    type Err = payment::Error;
419
420    #[instrument(skip_all)]
421    async fn get_settings(&self) -> Result<Value, Self::Err> {
422        Ok(serde_json::to_value(Bolt11Settings {
423            mpp: true,
424            unit: self.unit.clone(),
425            invoice_description: true,
426            amountless: false,
427            bolt12: true,
428        })?)
429    }
430
431    #[instrument(skip_all)]
432    fn is_wait_invoice_active(&self) -> bool {
433        self.wait_invoice_is_active.load(Ordering::SeqCst)
434    }
435
436    #[instrument(skip_all)]
437    fn cancel_wait_invoice(&self) {
438        self.wait_invoice_cancel_token.cancel()
439    }
440
441    #[instrument(skip_all)]
442    async fn wait_payment_event(
443        &self,
444    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
445        tracing::info!("Starting stream for fake invoices");
446        let receiver = self
447            .receiver
448            .lock()
449            .await
450            .take()
451            .ok_or(Error::NoReceiver)
452            .unwrap();
453        let receiver_stream = ReceiverStream::new(receiver);
454        Ok(Box::pin(receiver_stream.map(move |wait_response| {
455            Event::PaymentReceived(wait_response)
456        })))
457    }
458
459    #[instrument(skip_all)]
460    async fn get_payment_quote(
461        &self,
462        unit: &CurrencyUnit,
463        options: OutgoingPaymentOptions,
464    ) -> Result<PaymentQuoteResponse, Self::Err> {
465        let (amount_msat, request_lookup_id) = match options {
466            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
467                // If we have specific amount options, use those
468                let amount_msat: u64 = if let Some(melt_options) = bolt11_options.melt_options {
469                    let msats = match melt_options {
470                        MeltOptions::Amountless { amountless } => {
471                            let amount_msat = amountless.amount_msat;
472
473                            if let Some(invoice_amount) =
474                                bolt11_options.bolt11.amount_milli_satoshis()
475                            {
476                                ensure_cdk!(
477                                    invoice_amount == u64::from(amount_msat),
478                                    Error::UnknownInvoiceAmount.into()
479                                );
480                            }
481                            amount_msat
482                        }
483                        MeltOptions::Mpp { mpp } => mpp.amount,
484                    };
485
486                    u64::from(msats)
487                } else {
488                    // Fall back to invoice amount
489                    bolt11_options
490                        .bolt11
491                        .amount_milli_satoshis()
492                        .ok_or(Error::UnknownInvoiceAmount)?
493                };
494                let payment_id =
495                    PaymentIdentifier::PaymentHash(*bolt11_options.bolt11.payment_hash().as_ref());
496                (amount_msat, Some(payment_id))
497            }
498            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
499                let offer = bolt12_options.offer;
500
501                let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
502                    amount.amount_msat().into()
503                } else {
504                    // Fall back to offer amount
505                    let amount = offer.amount().ok_or(Error::UnknownInvoiceAmount)?;
506                    match amount {
507                        lightning::offers::offer::Amount::Bitcoin { amount_msats } => amount_msats,
508                        _ => return Err(Error::UnknownInvoiceAmount.into()),
509                    }
510                };
511                (amount_msat, None)
512            }
513        };
514
515        let amount = convert_currency_amount(
516            amount_msat,
517            &CurrencyUnit::Msat,
518            unit,
519            &self.exchange_rate_cache,
520        )
521        .await?;
522
523        let relative_fee_reserve =
524            (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
525
526        let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
527
528        let fee = max(relative_fee_reserve, absolute_fee_reserve);
529
530        Ok(PaymentQuoteResponse {
531            request_lookup_id,
532            amount,
533            fee: fee.into(),
534            state: MeltQuoteState::Unpaid,
535            unit: unit.clone(),
536        })
537    }
538
539    #[instrument(skip_all)]
540    async fn make_payment(
541        &self,
542        unit: &CurrencyUnit,
543        options: OutgoingPaymentOptions,
544    ) -> Result<MakePaymentResponse, Self::Err> {
545        match options {
546            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
547                let bolt11 = bolt11_options.bolt11;
548                let payment_hash = bolt11.payment_hash().to_string();
549
550                let description = bolt11.description().to_string();
551
552                let status: Option<FakeInvoiceDescription> =
553                    serde_json::from_str(&description).ok();
554
555                let mut payment_states = self.payment_states.lock().await;
556                let payment_status = status
557                    .clone()
558                    .map(|s| s.pay_invoice_state)
559                    .unwrap_or(MeltQuoteState::Paid);
560
561                let checkout_going_status = status
562                    .clone()
563                    .map(|s| s.check_payment_state)
564                    .unwrap_or(MeltQuoteState::Paid);
565
566                let amount_msat: u64 = if let Some(melt_options) = bolt11_options.melt_options {
567                    melt_options.amount_msat().into()
568                } else {
569                    // Fall back to invoice amount
570                    bolt11
571                        .amount_milli_satoshis()
572                        .ok_or(Error::UnknownInvoiceAmount)?
573                };
574
575                let amount_spent = if checkout_going_status == MeltQuoteState::Paid {
576                    amount_msat.into()
577                } else {
578                    Amount::ZERO
579                };
580
581                payment_states.insert(payment_hash.clone(), (checkout_going_status, amount_spent));
582
583                if let Some(description) = status {
584                    if description.check_err {
585                        let mut fail = self.failed_payment_check.lock().await;
586                        fail.insert(payment_hash.clone());
587                    }
588
589                    ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
590                }
591
592                let total_spent = convert_currency_amount(
593                    amount_msat,
594                    &CurrencyUnit::Msat,
595                    unit,
596                    &self.exchange_rate_cache,
597                )
598                .await?;
599
600                Ok(MakePaymentResponse {
601                    payment_proof: Some("".to_string()),
602                    payment_lookup_id: PaymentIdentifier::PaymentHash(
603                        *bolt11.payment_hash().as_ref(),
604                    ),
605                    status: payment_status,
606                    total_spent: total_spent + 1.into(),
607                    unit: unit.clone(),
608                })
609            }
610            OutgoingPaymentOptions::Bolt12(bolt12_options) => {
611                let bolt12 = bolt12_options.offer;
612                let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
613                    amount.amount_msat().into()
614                } else {
615                    // Fall back to offer amount
616                    let amount = bolt12.amount().ok_or(Error::UnknownInvoiceAmount)?;
617                    match amount {
618                        lightning::offers::offer::Amount::Bitcoin { amount_msats } => amount_msats,
619                        _ => return Err(Error::UnknownInvoiceAmount.into()),
620                    }
621                };
622
623                let total_spent = convert_currency_amount(
624                    amount_msat,
625                    &CurrencyUnit::Msat,
626                    unit,
627                    &self.exchange_rate_cache,
628                )
629                .await?;
630
631                Ok(MakePaymentResponse {
632                    payment_proof: Some("".to_string()),
633                    payment_lookup_id: PaymentIdentifier::CustomId(Uuid::new_v4().to_string()),
634                    status: MeltQuoteState::Paid,
635                    total_spent: total_spent + 1.into(),
636                    unit: unit.clone(),
637                })
638            }
639        }
640    }
641
642    #[instrument(skip_all)]
643    async fn create_incoming_payment_request(
644        &self,
645        unit: &CurrencyUnit,
646        options: IncomingPaymentOptions,
647    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
648        let (payment_hash, request, amount, expiry) = match options {
649            IncomingPaymentOptions::Bolt12(bolt12_options) => {
650                let description = bolt12_options.description.unwrap_or_default();
651                let amount = bolt12_options.amount;
652                let expiry = bolt12_options.unix_expiry;
653
654                let secret_key = SecretKey::new(&mut bitcoin::secp256k1::rand::rngs::OsRng);
655                let secp_ctx = Secp256k1::new();
656
657                let offer_builder = OfferBuilder::new(secret_key.public_key(&secp_ctx))
658                    .description(description.clone());
659
660                let offer_builder = match amount {
661                    Some(amount) => {
662                        let amount_msat = convert_currency_amount(
663                            u64::from(amount),
664                            unit,
665                            &CurrencyUnit::Msat,
666                            &self.exchange_rate_cache,
667                        )
668                        .await?;
669                        offer_builder.amount_msats(amount_msat.into())
670                    }
671                    None => offer_builder,
672                };
673
674                let offer = offer_builder.build().unwrap();
675
676                (
677                    PaymentIdentifier::OfferId(offer.id().to_string()),
678                    offer.to_string(),
679                    amount.unwrap_or(Amount::ZERO),
680                    expiry,
681                )
682            }
683            IncomingPaymentOptions::Bolt11(bolt11_options) => {
684                let description = bolt11_options.description.unwrap_or_default();
685                let amount = bolt11_options.amount;
686                let expiry = bolt11_options.unix_expiry;
687
688                let amount_msat = convert_currency_amount(
689                    u64::from(amount),
690                    unit,
691                    &CurrencyUnit::Msat,
692                    &self.exchange_rate_cache,
693                )
694                .await?
695                .into();
696
697                let invoice = create_fake_invoice(amount_msat, description.clone());
698                let payment_hash = invoice.payment_hash();
699
700                (
701                    PaymentIdentifier::PaymentHash(*payment_hash.as_ref()),
702                    invoice.to_string(),
703                    amount,
704                    expiry,
705                )
706            }
707        };
708
709        // ALL invoices get immediate payment processing (original behavior)
710        let sender = self.sender.clone();
711        let duration = time::Duration::from_secs(self.payment_delay);
712        let payment_hash_clone = payment_hash.clone();
713        let incoming_payment = self.incoming_payments.clone();
714        let unit_clone = self.unit.clone();
715
716        let final_amount = if amount == Amount::ZERO {
717            // For any-amount invoices, generate a random amount for the initial payment
718            use bitcoin::secp256k1::rand::rngs::OsRng;
719            use bitcoin::secp256k1::rand::Rng;
720            let mut rng = OsRng;
721            let random_amount: u64 = rng.gen_range(1000..=10000);
722            // Use the same unit as the wallet for any-amount invoices
723            Amount::from(random_amount)
724        } else {
725            amount
726        };
727
728        // Schedule the immediate payment (original behavior maintained)
729        tokio::spawn(async move {
730            // Wait for the random delay to elapse
731            time::sleep(duration).await;
732
733            let response = WaitPaymentResponse {
734                payment_identifier: payment_hash_clone.clone(),
735                payment_amount: final_amount,
736                unit: unit_clone,
737                payment_id: payment_hash_clone.to_string(),
738            };
739            let mut incoming = incoming_payment.write().await;
740            incoming
741                .entry(payment_hash_clone.clone())
742                .or_insert_with(Vec::new)
743                .push(response.clone());
744
745            // Send the message after waiting for the specified duration
746            if sender.send(response.clone()).await.is_err() {
747                tracing::error!("Failed to send label: {:?}", payment_hash_clone);
748            }
749        });
750
751        // For any-amount invoices ONLY, also add to the secondary repayment queue
752        if amount == Amount::ZERO {
753            tracing::info!(
754                "Adding any-amount invoice to secondary repayment queue: {:?}",
755                payment_hash
756            );
757
758            self.secondary_repayment_queue
759                .enqueue_for_repayment(payment_hash.clone())
760                .await;
761        }
762
763        Ok(CreateIncomingPaymentResponse {
764            request_lookup_id: payment_hash,
765            request,
766            expiry,
767        })
768    }
769
770    #[instrument(skip_all)]
771    async fn check_incoming_payment_status(
772        &self,
773        request_lookup_id: &PaymentIdentifier,
774    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
775        Ok(self
776            .incoming_payments
777            .read()
778            .await
779            .get(request_lookup_id)
780            .cloned()
781            .unwrap_or(vec![]))
782    }
783
784    #[instrument(skip_all)]
785    async fn check_outgoing_payment(
786        &self,
787        request_lookup_id: &PaymentIdentifier,
788    ) -> Result<MakePaymentResponse, Self::Err> {
789        // For fake wallet if the state is not explicitly set default to paid
790        let states = self.payment_states.lock().await;
791        let status = states.get(&request_lookup_id.to_string()).cloned();
792
793        let (status, total_spent) = status.unwrap_or((MeltQuoteState::Unknown, Amount::default()));
794
795        let fail_payments = self.failed_payment_check.lock().await;
796
797        if fail_payments.contains(&request_lookup_id.to_string()) {
798            return Err(payment::Error::InvoicePaymentPending);
799        }
800
801        Ok(MakePaymentResponse {
802            payment_proof: Some("".to_string()),
803            payment_lookup_id: request_lookup_id.clone(),
804            status,
805            total_spent,
806            unit: CurrencyUnit::Msat,
807        })
808    }
809}
810
811/// Create fake invoice
812#[instrument]
813pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice {
814    let private_key = SecretKey::from_slice(
815        &[
816            0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, 0xe2,
817            0x06, 0xbe, 0x71, 0x50, 0x00, 0xf9, 0x4d, 0xac, 0x06, 0x7d, 0x1c, 0x04, 0xa8, 0xca,
818            0x3b, 0x2d, 0xb7, 0x34,
819        ][..],
820    )
821    .unwrap();
822
823    use bitcoin::secp256k1::rand::rngs::OsRng;
824    use bitcoin::secp256k1::rand::Rng;
825    let mut rng = OsRng;
826    let mut random_bytes = [0u8; 32];
827    rng.fill(&mut random_bytes);
828
829    let payment_hash = sha256::Hash::from_slice(&random_bytes).unwrap();
830    let payment_secret = PaymentSecret([42u8; 32]);
831
832    InvoiceBuilder::new(Currency::Bitcoin)
833        .description(description)
834        .payment_hash(payment_hash)
835        .payment_secret(payment_secret)
836        .amount_milli_satoshis(amount_msat)
837        .current_timestamp()
838        .min_final_cltv_expiry_delta(144)
839        .build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))
840        .unwrap()
841}