breez_sdk_core/
test_utils.rs

1use std::collections::{HashMap, HashSet};
2use std::pin::Pin;
3use std::time::{Duration, SystemTime};
4use std::{mem, vec};
5
6use anyhow::{Error, Result};
7use chrono::{SecondsFormat, Utc};
8use rand::distributions::uniform::{SampleRange, SampleUniform};
9use rand::distributions::{Alphanumeric, DistString, Standard};
10use rand::rngs::OsRng;
11use rand::{random, Rng};
12use sdk_common::grpc::{
13    self, CreateSwapResponse, PaySwapResponse, RefundSwapResponse, SwapParameters,
14};
15use sdk_common::prelude::{FiatAPI, FiatCurrency, Rate};
16use secp256k1::musig::MusigKeyAggCache;
17use serde_json::{json, Value};
18use tokio::sync::{mpsc, watch, Mutex};
19use tokio::time::sleep;
20use tokio_stream::Stream;
21use tokio_stream::StreamExt;
22use tonic::Status;
23
24use crate::backup::{BackupState, BackupTransport};
25use crate::bitcoin::bip32::{ChildNumber, ExtendedPrivKey};
26use crate::bitcoin::blockdata::opcodes::all::{
27    OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CSV, OP_EQUALVERIFY, OP_HASH160,
28};
29use crate::bitcoin::blockdata::script;
30use crate::bitcoin::hashes::ripemd160;
31use crate::bitcoin::hashes::{sha256, Hash};
32use crate::bitcoin::secp256k1::ecdsa::RecoverableSignature;
33use crate::bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1};
34use crate::bitcoin::taproot::{TaprootBuilder, TaprootSpendInfo};
35use crate::bitcoin::{key::XOnlyPublicKey, Address, Network, ScriptBuf, Sequence};
36use crate::breez_services::{OpenChannelParams, Receiver};
37use crate::buy::BuyBitcoinApi;
38use crate::chain::{ChainService, OnchainTx, Outspend, RecommendedFees, TxStatus};
39use crate::error::{ReceivePaymentError, SdkError, SdkResult};
40use crate::invoice::{InvoiceError, InvoiceResult};
41use crate::lightning::ln::PaymentSecret;
42use crate::lightning_invoice::{Currency, InvoiceBuilder, RawBolt11Invoice};
43use crate::lsp::LspInformation;
44use crate::models::{
45    LnPaymentDetails, LspAPI, NodeState, Payment, PaymentDetails, PaymentStatus, PaymentType,
46    ReverseSwapServiceAPI, SwapperAPI, SyncResponse, TlvEntry,
47};
48use crate::node_api::{
49    CreateInvoiceRequest, FetchBolt11Result, IncomingPayment, NodeAPI, NodeError, NodeResult,
50};
51use crate::swap_in::TaprootSwapperAPI;
52use crate::swap_out::boltzswap::{BoltzApiCreateReverseSwapResponse, BoltzApiReverseSwapStatus};
53use crate::swap_out::error::{ReverseSwapError, ReverseSwapResult};
54use crate::{
55    parse_invoice, BuyBitcoinProvider, Config, CustomMessage, LNInvoice, MaxChannelAmount,
56    NodeCredentials, OpeningFeeParamsMenu, PaymentResponse, PrepareRedeemOnchainFundsRequest,
57    PrepareRedeemOnchainFundsResponse, ReceivePaymentRequest, ReverseSwapPairInfo, RouteHint,
58    RouteHintHop, SwapInfo,
59};
60
61pub const MOCK_REVERSE_SWAP_MIN: u64 = 50_000;
62pub const MOCK_REVERSE_SWAP_MAX: u64 = 1_000_000;
63
64pub struct MockBackupTransport {
65    pub num_pushed: std::sync::Mutex<u32>,
66    pub num_pulled: std::sync::Mutex<u32>,
67    pub remote_version: std::sync::Mutex<Option<u64>>,
68    pub state: std::sync::Mutex<Option<BackupState>>,
69}
70
71impl MockBackupTransport {
72    pub fn new() -> Self {
73        MockBackupTransport {
74            num_pushed: std::sync::Mutex::new(0),
75            num_pulled: std::sync::Mutex::new(0),
76            remote_version: std::sync::Mutex::new(None),
77            state: std::sync::Mutex::new(None),
78        }
79    }
80    pub fn pushed(&self) -> u32 {
81        *self.num_pushed.lock().unwrap()
82    }
83    pub fn pulled(&self) -> u32 {
84        *self.num_pulled.lock().unwrap()
85    }
86}
87
88#[tonic::async_trait]
89impl BackupTransport for MockBackupTransport {
90    async fn pull(&self) -> SdkResult<Option<BackupState>> {
91        sleep(Duration::from_millis(10)).await;
92        *self.num_pulled.lock().unwrap() += 1;
93        let current_state = self.state.lock().unwrap();
94
95        match current_state.clone() {
96            Some(state) => Ok(Some(state)),
97            None => Ok(None),
98        }
99    }
100    async fn push(&self, version: Option<u64>, data: Vec<u8>) -> SdkResult<u64> {
101        sleep(Duration::from_millis(10)).await;
102        let mut remote_version = self.remote_version.lock().unwrap();
103        let mut numpushed = self.num_pushed.lock().unwrap();
104        *numpushed += 1;
105
106        if !remote_version.is_none() && *remote_version != version {
107            return Err(SdkError::Generic {
108                err: "version mismatch".into(),
109            });
110        }
111        let next_version = match version {
112            Some(v) => v + 1,
113            None => 1,
114        };
115        *remote_version = Some(next_version);
116        *self.state.lock().unwrap() = Some(BackupState {
117            generation: next_version,
118            data,
119        });
120        Ok(next_version)
121    }
122}
123
124pub struct MockSwapperAPI {}
125
126#[tonic::async_trait]
127impl SwapperAPI for MockSwapperAPI {
128    async fn complete_swap(&self, _bolt11: String) -> Result<()> {
129        Ok(())
130    }
131}
132
133pub struct MockReverseSwapperAPI {}
134
135#[tonic::async_trait]
136impl ReverseSwapServiceAPI for MockReverseSwapperAPI {
137    async fn fetch_reverse_swap_fees(&self) -> ReverseSwapResult<ReverseSwapPairInfo> {
138        Ok(ReverseSwapPairInfo {
139            min: MOCK_REVERSE_SWAP_MIN,
140            max: MOCK_REVERSE_SWAP_MAX,
141            fees_hash: rand_string(5),
142            fees_percentage: 0.5,
143            fees_lockup: 3_000 + rand_int_in_range(1..1_000),
144            fees_claim: 3_000 + rand_int_in_range(1..1_000),
145            total_fees: None,
146        })
147    }
148
149    async fn create_reverse_swap_on_remote(
150        &self,
151        _amount_sat: u64,
152        _preimage_hash_hex: String,
153        _claim_pubkey: String,
154        _pair_hash: String,
155        _routing_node: String,
156    ) -> ReverseSwapResult<BoltzApiCreateReverseSwapResponse> {
157        Err(ReverseSwapError::generic("Not implemented"))
158    }
159
160    async fn get_boltz_status(&self, _id: String) -> ReverseSwapResult<BoltzApiReverseSwapStatus> {
161        Err(ReverseSwapError::generic("Not implemented"))
162    }
163
164    async fn get_route_hints(&self, _routing_node_id: String) -> ReverseSwapResult<Vec<RouteHint>> {
165        Err(ReverseSwapError::generic("Not implemented"))
166    }
167}
168
169#[derive(Clone)]
170pub struct MockChainService {
171    pub tip: u32,
172    pub recommended_fees: RecommendedFees,
173    pub address_to_transactions: HashMap<String, Vec<OnchainTx>>,
174}
175
176impl Default for MockChainService {
177    fn default() -> Self {
178        let recommended_fees: RecommendedFees = serde_json::from_str(
179            r#"{
180               "fastestFee": 1,
181               "halfHourFee": 1,
182               "hourFee": 1,
183               "economyFee": 1,
184               "minimumFee": 1
185             }"#,
186        )
187        .unwrap();
188
189        let txs: Vec<OnchainTx> = serde_json::from_str(
190                r#"[{"txid":"a418e856bb22b6345868dc0b1ac1dd7a6b7fae1d231b275b74172f9584fa0bdf","version":1,"locktime":0,"vin":[{"txid":"ec901bcab07df7d475d98fff2933dcb56d57bbdaa029c4142aed93462b6928fe","vout":0,"prevout":{"scriptpubkey":"0014b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qkd9hm2qwvck3mvlul035kl6v4nz04s6dmryeq5","value":197497253},"scriptsig":"","scriptsig_asm":"","witness":["304502210089933e46614114e060d3d681c54af71e3d47f8be8131d9310ef8fe231c060f3302204103910a6790e3a678964df6f0f9ae2107666a91e777bd87f9172a28653e374701","0356f385879fefb8c52758126f6e7b9ac57374c2f73f2ee9047b4c61df0ba390b9"],"is_coinbase":false,"sequence":4294967293},{"txid":"fda3ce37f5fb849502e2027958d51efebd1841cb43bbfdd5f3d354c93a551ef9","vout":0,"prevout":{"scriptpubkey":"00145c7f3b6ceb79d03d5a5397df83f2334394ebdd2c","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 5c7f3b6ceb79d03d5a5397df83f2334394ebdd2c","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt3lnkm8t08gr6kjnjl0c8u3ngw2whhfvzwsxrg","value":786885},"scriptsig":"","scriptsig_asm":"","witness":["304402200ae5465efe824609f7faf1094cce0195763df52e5409dd9ae0526568bf3bcaa20220103749041a87e082cf95bf1e12c5174881e5e4c55e75ab2db29a68538dbabbad01","03dfd8cc1f72f46d259dc0afc6d756bce551fce2fbf58a9ad36409a1b82a17e64f"],"is_coinbase":false,"sequence":4294967293}],"vout":[{"scriptpubkey":"a9141df45814863edfd6d87457e8f8bd79607a116a8f87","scriptpubkey_asm":"OP_HASH160 OP_PUSHBYTES_20 1df45814863edfd6d87457e8f8bd79607a116a8f OP_EQUAL","scriptpubkey_type":"p2sh","scriptpubkey_address":"34RQERthXaruAXtW6q1bvrGTeUbqi2Sm1i","value":26087585},{"scriptpubkey":"001479001aa5f4b981a0b654c3f834d0573595b0ed53","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 79001aa5f4b981a0b654c3f834d0573595b0ed53","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q0yqp4f05hxq6pdj5c0urf5zhxk2mpm2ndx85za","value":171937413}],"size":372,"weight":837,"fee":259140,"status":{"confirmed":true,"block_height":767637,"block_hash":"000000000000000000077769f3b2e6a28b9ed688f0d773f9ff2d73c622a2cfac","block_time":1671174562}},{"txid":"ec901bcab07df7d475d98fff2933dcb56d57bbdaa029c4142aed93462b6928fe","version":1,"locktime":767636,"vin":[{"txid":"d4344fc9e7f66b3a1a50d1d76836a157629ba0c6ede093e94f1c809d334c9146","vout":0,"prevout":{"scriptpubkey":"0014cab22290b7adc75f861de820baa97d319c1110a6","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 cab22290b7adc75f861de820baa97d319c1110a6","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qe2ez9y9h4hr4lpsaaqst42taxxwpzy9xlzqt8k","value":209639471},"scriptsig":"","scriptsig_asm":"","witness":["304402202e914c35b75da798f0898c7cfe6ead207aaee41219afd77124fd56971f05d9030220123ce5d124f4635171b7622995dae35e00373a5fbf8117bfdca5e5080ad6554101","02122fa6d20413bb5da5c7e3fb42228be5436b1bd84e29b294bfc200db5eac460e"],"is_coinbase":false,"sequence":4294967293}],"vout":[{"scriptpubkey":"0014b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qkd9hm2qwvck3mvlul035kl6v4nz04s6dmryeq5","value":197497253},{"scriptpubkey":"0014f0e2a057d0e60411ac3d7218e29bf9489a59df18","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0e2a057d0e60411ac3d7218e29bf9489a59df18","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7r32q47suczprtpawgvw9xlefzd9nhccyatxvu","value":12140465}],"size":222,"weight":561,"fee":1753,"status":{"confirmed":true,"block_height":767637,"block_hash":"000000000000000000077769f3b2e6a28b9ed688f0d773f9ff2d73c622a2cfac","block_time":1671174562}}]"#,
191            ).unwrap();
192        Self {
193            tip: 767640,
194            recommended_fees,
195            address_to_transactions: HashMap::from([(
196                "bc1qkd9hm2qwvck3mvlul035kl6v4nz04s6dmryeq5".to_string(),
197                txs,
198            )]),
199        }
200    }
201}
202
203#[tonic::async_trait]
204impl ChainService for MockChainService {
205    async fn recommended_fees(&self) -> SdkResult<RecommendedFees> {
206        Ok(self.recommended_fees.clone())
207    }
208
209    async fn address_transactions(&self, address: String) -> SdkResult<Vec<OnchainTx>> {
210        Ok(self
211            .address_to_transactions
212            .get(&address)
213            .unwrap_or(&Vec::<OnchainTx>::new())
214            .clone())
215    }
216
217    async fn current_tip(&self) -> SdkResult<u32> {
218        Ok(self.tip)
219    }
220
221    async fn transaction_outspends(&self, _txid: String) -> SdkResult<Vec<Outspend>> {
222        Ok(vec![Outspend {
223            spent: true,
224            txid: Some("test-tx-id".into()),
225            vin: Some(0),
226            status: Some(TxStatus {
227                confirmed: true,
228                block_height: Some(123),
229                block_hash: Some("test-hash".into()),
230                block_time: Some(123),
231            }),
232        }])
233    }
234
235    async fn broadcast_transaction(&self, _tx: Vec<u8>) -> SdkResult<String> {
236        let mut array = [0; 32];
237        rand::thread_rng().fill(&mut array);
238        Ok(hex::encode(array))
239    }
240}
241
242impl TryFrom<Payment> for crate::models::PaymentResponse {
243    type Error = anyhow::Error;
244
245    fn try_from(payment: Payment) -> std::result::Result<Self, Self::Error> {
246        let payment_hash: String = match payment.details.clone() {
247            crate::models::PaymentDetails::Ln { data } => data.payment_hash,
248            _ => "".into(),
249        };
250        let payment_preimage: String = match payment.details.clone() {
251            crate::models::PaymentDetails::Ln { data } => data.payment_preimage,
252            _ => "".into(),
253        };
254        Ok(crate::models::PaymentResponse {
255            payment_time: payment.payment_time,
256            amount_msat: payment.amount_msat,
257            fee_msat: payment.fee_msat,
258            payment_hash,
259            payment_preimage,
260        })
261    }
262}
263
264pub struct MockReceiver {
265    pub bolt11: String,
266}
267
268impl Default for MockReceiver {
269    fn default() -> Self {
270        MockReceiver { bolt11: "lnbc500u1p3eerl2dq8w3jhxaqpp5w3w4z63erts5usxtkvpwdy356l29xfd43mnzlq6x2d69kqhjtepsxqyjw5qsp5an4vlkhp8cgahvamrdkn2uzmmcd5neq7yq3j6a8v0sc0q9rlde5s9qrsgqcqpxrzjqwk7573qcyfskzw33jnvs0shq9tzy28sd86naqlgkdga9p8z74fsyzancsqqvpsqqqqqqqlgqqqqqzsqygrzjqwk7573qcyfskzw33jnvs0shq9tzy28sd86naqlgkdga9p8z74fsyqqqqyqqqqqqqqqqqqlgqqqqqzsqjqacpq7rd5rf7ssza0lps93ehylrwtjhdlk44g0llwp039f8uqxsck52ccr69djxs59mmwqkvvglylpg0cdzaqusg9m9cyju92t7kjpfsqma2lmf".to_string() }
271    }
272}
273
274#[tonic::async_trait]
275impl Receiver for MockReceiver {
276    fn open_channel_needed(&self, _amount_msat: u64) -> Result<bool, ReceivePaymentError> {
277        Ok(true)
278    }
279    async fn receive_payment(
280        &self,
281        _request: ReceivePaymentRequest,
282    ) -> Result<crate::ReceivePaymentResponse, ReceivePaymentError> {
283        Ok(crate::ReceivePaymentResponse {
284            ln_invoice: parse_invoice(&self.bolt11)?,
285            opening_fee_params: _request.opening_fee_params,
286            opening_fee_msat: None,
287        })
288    }
289    async fn wrap_node_invoice(
290        &self,
291        invoice: &str,
292        _params: Option<OpenChannelParams>,
293        _lsp_info: Option<LspInformation>,
294    ) -> Result<String, ReceivePaymentError> {
295        Ok(String::from(invoice))
296    }
297}
298
299pub struct MockNodeAPI {
300    /// Simulated repository of confirmed new outgoing payments.
301    ///
302    /// Each call to [MockNodeAPI::add_dummy_payment_for] will add the new payment here such that
303    /// [NodeAPI::pull_changed], which is called in [BreezServices::sync], always retrieves the newly
304    /// added test payments
305    cloud_payments: Mutex<Vec<Payment>>,
306    node_state: NodeState,
307    on_send_custom_message: Box<dyn Fn(CustomMessage) -> NodeResult<()> + Sync + Send>,
308    on_stream_custom_messages: Mutex<mpsc::Receiver<CustomMessage>>,
309}
310
311#[tonic::async_trait]
312impl NodeAPI for MockNodeAPI {
313    async fn node_credentials(&self) -> NodeResult<Option<NodeCredentials>> {
314        Err(NodeError::Generic("Not implemented".to_string()))
315    }
316
317    async fn configure_node(&self, _close_to_address: Option<String>) -> NodeResult<()> {
318        Ok(())
319    }
320
321    async fn create_invoice(&self, request: CreateInvoiceRequest) -> NodeResult<String> {
322        let invoice = create_invoice(
323            request.description,
324            request.amount_msat,
325            vec![],
326            request.preimage,
327        );
328        Ok(invoice.bolt11)
329    }
330
331    async fn delete_invoice(&self, _bolt11: String) -> NodeResult<()> {
332        Ok(())
333    }
334
335    async fn pull_changed(
336        &self,
337        _sync_state: Option<Value>,
338        _match_local_balance: bool,
339    ) -> NodeResult<SyncResponse> {
340        Ok(SyncResponse {
341            sync_state: Value::Null,
342            node_state: self.node_state.clone(),
343            payments: self
344                .cloud_payments
345                .lock()
346                .await
347                .iter()
348                .cloned()
349                .flat_map(TryInto::try_into)
350                .collect(),
351            channels: Vec::new(),
352        })
353    }
354
355    async fn send_pay(&self, _bolt11: String, _max_hops: u32) -> NodeResult<PaymentResponse> {
356        Err(NodeError::Generic("Not implemented".to_string()))
357    }
358
359    async fn send_payment(
360        &self,
361        bolt11: String,
362        _amount_msat: Option<u64>,
363        _label: Option<String>,
364    ) -> NodeResult<Payment> {
365        let payment = self.add_dummy_payment_for(bolt11, None, None).await?;
366        Ok(payment)
367    }
368
369    async fn send_trampoline_payment(
370        &self,
371        bolt11: String,
372        _amount_msat: u64,
373        _label: Option<String>,
374        _trampoline_id: Vec<u8>,
375    ) -> NodeResult<Payment> {
376        let payment = self.add_dummy_payment_for(bolt11, None, None).await?;
377        Ok(payment)
378    }
379
380    async fn send_spontaneous_payment(
381        &self,
382        _node_id: String,
383        _amount_msat: u64,
384        _extra_tlvs: Option<Vec<TlvEntry>>,
385        _label: Option<String>,
386    ) -> NodeResult<Payment> {
387        let payment = self.add_dummy_payment_rand().await?;
388        Ok(payment)
389    }
390
391    async fn node_id(&self) -> NodeResult<String> {
392        Ok("".to_string())
393    }
394
395    async fn redeem_onchain_funds(
396        &self,
397        _to_address: String,
398        _sat_per_vbyte: u32,
399    ) -> NodeResult<Vec<u8>> {
400        Ok(rand_vec_u8(32))
401    }
402
403    async fn prepare_redeem_onchain_funds(
404        &self,
405        _req: PrepareRedeemOnchainFundsRequest,
406    ) -> NodeResult<PrepareRedeemOnchainFundsResponse> {
407        Err(NodeError::Generic("Not implemented".to_string()))
408    }
409
410    async fn start(&self, _shutdown: mpsc::Receiver<()>) {}
411
412    async fn start_keep_alive(&self, _shutdown: watch::Receiver<()>) {}
413
414    async fn connect_peer(&self, _node_id: String, _addr: String) -> NodeResult<()> {
415        Ok(())
416    }
417
418    async fn sign_message(&self, _message: &str) -> NodeResult<String> {
419        Ok("".to_string())
420    }
421
422    async fn check_message(
423        &self,
424        _message: &str,
425        _pubkey: &str,
426        _signature: &str,
427    ) -> NodeResult<bool> {
428        Ok(true)
429    }
430
431    async fn sign_invoice(&self, invoice: RawBolt11Invoice) -> NodeResult<String> {
432        Ok(sign_invoice(invoice))
433    }
434
435    async fn close_peer_channels(&self, _node_id: String) -> NodeResult<Vec<String>> {
436        Ok(vec![])
437    }
438    async fn stream_incoming_payments(
439        &self,
440    ) -> NodeResult<Pin<Box<dyn Stream<Item = IncomingPayment> + Send>>> {
441        Err(NodeError::Generic("Not implemented".to_string()))
442    }
443
444    async fn stream_log_messages(&self) -> NodeResult<Pin<Box<dyn Stream<Item = String> + Send>>> {
445        Err(NodeError::Generic("Not implemented".to_string()))
446    }
447
448    async fn static_backup(&self) -> NodeResult<Vec<String>> {
449        Ok(Vec::new())
450    }
451
452    async fn execute_command(&self, _command: String) -> NodeResult<Value> {
453        Err(NodeError::Generic("Not implemented".to_string()))
454    }
455
456    async fn generate_diagnostic_data(&self) -> NodeResult<Value> {
457        Ok(json!({}))
458    }
459
460    async fn max_sendable_amount<'a>(
461        &self,
462        _payee_node_id: Option<Vec<u8>>,
463        _max_hops: u32,
464        _last_hop: Option<&'a RouteHintHop>,
465    ) -> NodeResult<Vec<MaxChannelAmount>> {
466        Err(NodeError::Generic("Not implemented".to_string()))
467    }
468
469    async fn derive_bip32_key(&self, _path: Vec<ChildNumber>) -> NodeResult<ExtendedPrivKey> {
470        Ok(ExtendedPrivKey::new_master(Network::Bitcoin, &[])?)
471    }
472
473    async fn legacy_derive_bip32_key(
474        &self,
475        _path: Vec<ChildNumber>,
476    ) -> NodeResult<ExtendedPrivKey> {
477        Ok(ExtendedPrivKey::new_master(Network::Bitcoin, &[])?)
478    }
479
480    async fn send_custom_message(&self, message: CustomMessage) -> NodeResult<()> {
481        (self.on_send_custom_message)(message)
482    }
483
484    async fn stream_custom_messages(
485        &self,
486    ) -> NodeResult<Pin<Box<dyn Stream<Item = core::result::Result<CustomMessage, Error>> + Send>>>
487    {
488        let (_, next_rx) = mpsc::channel(1);
489        let mut guard = self.on_stream_custom_messages.lock().await;
490        let rx = mem::replace(&mut *guard, next_rx);
491        Ok(Box::pin(
492            tokio_stream::wrappers::ReceiverStream::new(rx).map(Ok),
493        ))
494    }
495
496    async fn get_routing_hints(
497        &self,
498        _lsp_info: &LspInformation,
499    ) -> NodeResult<(Vec<RouteHint>, bool)> {
500        Ok((vec![], false))
501    }
502
503    async fn fetch_bolt11(&self, _payment_hash: Vec<u8>) -> NodeResult<Option<FetchBolt11Result>> {
504        Ok(None)
505    }
506
507    async fn get_open_peers(&self) -> NodeResult<HashSet<Vec<u8>>> {
508        Ok(HashSet::new())
509    }
510}
511
512impl MockNodeAPI {
513    pub fn new(node_state: NodeState) -> Self {
514        Self {
515            cloud_payments: Mutex::new(Vec::new()),
516            node_state,
517            on_send_custom_message: Box::new(|_| Ok(())),
518            on_stream_custom_messages: {
519                let (_, rx) = mpsc::channel(1);
520                Mutex::new(rx)
521            },
522        }
523    }
524    /// Creates a (simulated) payment for the specified BOLT11 and adds it to a test-specific
525    /// global state.
526    ///
527    /// This payment and its details are retrieved and stored within [crate::BreezServices::sync]
528    /// by a combination of [NodeAPI::pull_changed] and [crate::persist::db::SqliteStorage::insert_or_update_payments].
529    pub(crate) async fn add_dummy_payment_for(
530        &self,
531        bolt11: String,
532        preimage: Option<sha256::Hash>,
533        status: Option<PaymentStatus>,
534    ) -> NodeResult<Payment> {
535        let inv = bolt11
536            .parse::<crate::lightning_invoice::Bolt11Invoice>()
537            .map_err(|e| NodeError::Generic(e.to_string()))?;
538
539        Ok(self.add_dummy_payment(inv, preimage, status).await)
540    }
541
542    /// Adds a dummy payment with random attributes.
543    pub(crate) async fn add_dummy_payment_rand(&self) -> NodeResult<Payment> {
544        let preimage = sha256::Hash::hash(&rand_vec_u8(10));
545        let inv = rand_invoice_with_description_hash_and_preimage("test".into(), preimage)?;
546
547        Ok(self.add_dummy_payment(inv, Some(preimage), None).await)
548    }
549
550    /// Adds a dummy payment.
551    pub(crate) async fn add_dummy_payment(
552        &self,
553        inv: crate::lightning_invoice::Bolt11Invoice,
554        preimage: Option<sha256::Hash>,
555        status: Option<PaymentStatus>,
556    ) -> Payment {
557        let preimage = match preimage {
558            Some(preimage) => hex::encode(&preimage),
559            None => hex::encode(rand_vec_u8(32)),
560        };
561        let payment = Payment {
562            id: hex::encode(inv.payment_hash()),
563            payment_type: PaymentType::Sent,
564            payment_time: random::<i64>(),
565            amount_msat: inv.amount_milli_satoshis().unwrap_or_default(),
566            fee_msat: 0,
567            status: status.unwrap_or(PaymentStatus::Complete),
568            error: None,
569            description: None,
570            details: PaymentDetails::Ln {
571                data: LnPaymentDetails {
572                    payment_hash: hex::encode(inv.payment_hash()),
573                    label: String::new(),
574                    destination_pubkey: hex::encode(rand_vec_u8(32)),
575                    payment_preimage: preimage,
576                    keysend: false,
577                    bolt11: inv.to_string(),
578                    lnurl_success_action: None,
579                    lnurl_pay_domain: None,
580                    lnurl_pay_comment: None,
581                    lnurl_metadata: None,
582                    ln_address: None,
583                    lnurl_withdraw_endpoint: None,
584                    swap_info: None,
585                    reverse_swap_info: None,
586                    pending_expiration_block: None,
587                    open_channel_bolt11: None,
588                },
589            },
590            metadata: None,
591        };
592
593        self.save_payment_for_future_sync_updates(payment).await
594    }
595
596    /// Include payment in the result of [MockNodeAPI::pull_changed].
597    async fn save_payment_for_future_sync_updates(&self, payment: Payment) -> Payment {
598        let mut cloud_payments = self.cloud_payments.lock().await;
599
600        // Only store it if a payment with the same ID doesn't already exist
601        // This allows us to initialize a MockBreezServer with a list of known payments using
602        // breez_services::tests::breez_services_with(vec), but not replace them when
603        // send_payment is called in tests for those payments.
604        match cloud_payments.iter().find(|p| p.id == payment.id) {
605            None => {
606                // If payment is not already known, add it to the list and return it
607                cloud_payments.push(payment.clone());
608                payment
609            }
610            Some(p) => {
611                // If a payment already exists (by ID), then do not replace it and return it
612                // The existing version is returned, because that's initialized with the preimage
613                // on mock breez service init
614                p.clone()
615            }
616        }
617    }
618
619    pub fn set_on_send_custom_message(
620        &mut self,
621        f: Box<dyn Fn(CustomMessage) -> NodeResult<()> + Sync + Send>,
622    ) {
623        self.on_send_custom_message = f;
624    }
625
626    pub async fn set_on_stream_custom_messages(&mut self, f: mpsc::Receiver<CustomMessage>) {
627        *self.on_stream_custom_messages.lock().await = f;
628    }
629}
630
631pub struct MockBreezServer {}
632
633impl MockBreezServer {
634    pub(crate) fn lsp_pub_key(&self) -> String {
635        "02d4e7e420d9dcf6f0206c27ecc69c400cc269b1f5f5ec856d8c9d1fc7e6d910d6".to_string()
636    }
637    pub(crate) fn lsp_id(&self) -> String {
638        "1".to_string()
639    }
640}
641
642#[tonic::async_trait]
643impl LspAPI for MockBreezServer {
644    async fn list_lsps(&self, _node_pubkey: String) -> SdkResult<Vec<LspInformation>> {
645        Ok(vec![LspInformation {
646            id: self.lsp_id(),
647            name: "test lsp".to_string(),
648            widget_url: "".to_string(),
649            pubkey: self.lsp_pub_key(),
650            host: "localhost".to_string(),
651            base_fee_msat: 1,
652            fee_rate: 1.0,
653            time_lock_delta: 32,
654            min_htlc_msat: 1000,
655            lsp_pubkey: hex::decode(self.lsp_pub_key()).unwrap(),
656            // Initialize menu with one Fee Param that is valid for >48h
657            // This way, it can be used in both kinds of tests (those that need the cheapest fee,
658            // as well as those with the longest valid fee)
659            opening_fee_params_list: OpeningFeeParamsMenu::try_from(vec![get_test_ofp_48h(
660                10, 12,
661            )])?,
662        }])
663    }
664
665    async fn list_used_lsps(&self, _node_pubkey: String) -> SdkResult<Vec<LspInformation>> {
666        Ok(vec![])
667    }
668
669    async fn register_payment_notifications(
670        &self,
671        _lsp_id: String,
672        _lsp_pubkey: Vec<u8>,
673        _webhook_url: String,
674        _webhook_url_signature: String,
675    ) -> SdkResult<grpc::RegisterPaymentNotificationResponse> {
676        Ok(grpc::RegisterPaymentNotificationResponse {})
677    }
678
679    async fn unregister_payment_notifications(
680        &self,
681        _lsp_id: String,
682        _lsp_pubkey: Vec<u8>,
683        _webhook_url: String,
684        _webhook_url_signature: String,
685    ) -> SdkResult<grpc::RemovePaymentNotificationResponse> {
686        Ok(grpc::RemovePaymentNotificationResponse {})
687    }
688
689    async fn register_payment(
690        &self,
691        _lsp_id: String,
692        _lsp_pubkey: Vec<u8>,
693        _payment_info: grpc::PaymentInformation,
694    ) -> SdkResult<grpc::RegisterPaymentReply> {
695        Ok(grpc::RegisterPaymentReply {})
696    }
697}
698
699#[tonic::async_trait]
700impl FiatAPI for MockBreezServer {
701    async fn list_fiat_currencies(&self) -> Result<Vec<FiatCurrency>> {
702        Ok(vec![])
703    }
704
705    async fn fetch_fiat_rates(&self) -> Result<Vec<Rate>> {
706        Ok(vec![Rate {
707            coin: "USD".to_string(),
708            value: 20_000.00,
709        }])
710    }
711}
712
713pub struct MockBuyBitcoinService {}
714
715#[tonic::async_trait]
716impl BuyBitcoinApi for MockBuyBitcoinService {
717    async fn buy_bitcoin(
718        &self,
719        _provider: BuyBitcoinProvider,
720        swap_info: &SwapInfo,
721        _redirect_url: Option<String>,
722    ) -> Result<String> {
723        Ok(format!(
724            "https://mock.moonpay?wa={}&ma={}",
725            swap_info.bitcoin_address.as_str(),
726            format!("{:.8}", swap_info.max_allowed_deposit as f64 / 100000000.0).as_str(),
727        ))
728    }
729}
730
731pub(crate) fn rand_invoice_with_description_hash(
732    expected_desc: String,
733) -> InvoiceResult<crate::lightning_invoice::Bolt11Invoice> {
734    let preimage = sha256::Hash::hash(&rand_vec_u8(10));
735
736    rand_invoice_with_description_hash_and_preimage(expected_desc, preimage)
737}
738
739pub(crate) fn rand_invoice_with_description_hash_and_preimage(
740    expected_desc: String,
741    preimage: sha256::Hash,
742) -> InvoiceResult<crate::lightning_invoice::Bolt11Invoice> {
743    let expected_desc_hash = Hash::hash(expected_desc.as_bytes());
744
745    let hashed_preimage = Message::from_hashed_data::<sha256::Hash>(&preimage[..]);
746    let payment_hash = hashed_preimage.as_ref();
747
748    let payment_secret = PaymentSecret([42u8; 32]);
749
750    let secp = Secp256k1::new();
751    let key_pair = KeyPair::new(&secp, &mut rand::thread_rng());
752    let private_key = key_pair.secret_key();
753
754    Ok(InvoiceBuilder::new(Currency::Bitcoin)
755        .description_hash(expected_desc_hash)
756        .amount_milli_satoshis(50 * 1000)
757        .payment_hash(
758            Hash::from_slice(payment_hash).map_err(|e| InvoiceError::Generic(e.to_string()))?,
759        )
760        .payment_secret(payment_secret)
761        .current_timestamp()
762        .min_final_cltv_expiry_delta(144)
763        .build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))?)
764}
765
766pub fn rand_string(len: usize) -> String {
767    Alphanumeric.sample_string(&mut rand::thread_rng(), len)
768}
769
770pub fn rand_vec_u8(len: usize) -> Vec<u8> {
771    rand::thread_rng().sample_iter(Standard).take(len).collect()
772}
773
774pub fn rand_int_in_range<T, R>(range: R) -> T
775where
776    T: SampleUniform,
777    R: SampleRange<T>,
778{
779    rand::thread_rng().gen_range(range)
780}
781
782pub fn create_test_config() -> crate::models::Config {
783    let mut conf = Config {
784        default_lsp_id: Some(String::from("03cea51f-b654-4fb0-8e82-eca137f236a0")),
785        chainnotifier_url: "http://test-chainnotifier.local".to_string(),
786        ..Config::production(
787            "".into(),
788            crate::NodeConfig::Greenlight {
789                config: crate::GreenlightNodeConfig {
790                    partner_credentials: None,
791                    invite_code: None,
792                },
793            },
794        )
795    };
796    conf.working_dir = get_test_working_dir();
797    conf
798}
799
800pub(crate) fn create_test_persister(
801    config: crate::models::Config,
802) -> crate::persist::db::SqliteStorage {
803    println!("create_test_persister {}", config.working_dir);
804    crate::persist::db::SqliteStorage::new(config.working_dir)
805}
806
807pub fn get_test_working_dir() -> String {
808    let mut rng = rand::thread_rng();
809    let s = std::env::temp_dir().to_str().unwrap().to_string();
810    let dir = format!("{}/{}", s, rng.gen::<u32>());
811    std::fs::create_dir_all(dir.clone()).unwrap();
812    dir
813}
814
815pub fn create_invoice(
816    description: String,
817    amount_msat: u64,
818    hints: Vec<RouteHint>,
819    invoice_preimage: Option<Vec<u8>>,
820) -> LNInvoice {
821    let preimage = invoice_preimage.unwrap_or(rand::thread_rng().gen::<[u8; 32]>().to_vec());
822    let hashed = Message::from_hashed_data::<sha256::Hash>(&preimage[..]);
823    let hash = hashed.as_ref();
824
825    let mut invoice_builder = InvoiceBuilder::new(Currency::Bitcoin)
826        .description(description)
827        .payment_hash(sha256::Hash::hash(hash))
828        .timestamp(SystemTime::now())
829        .amount_milli_satoshis(amount_msat)
830        .expiry_time(Duration::new(3600, 0))
831        .payment_secret(PaymentSecret(rand::thread_rng().gen::<[u8; 32]>()))
832        .min_final_cltv_expiry_delta(32);
833
834    for hint in hints {
835        invoice_builder = invoice_builder.private_route(hint.to_ldk_hint().unwrap());
836    }
837
838    let raw_invoice = invoice_builder.build_raw().unwrap();
839    parse_invoice(&sign_invoice(raw_invoice)).unwrap()
840}
841
842fn sign_invoice(invoice: RawBolt11Invoice) -> String {
843    let secp = Secp256k1::new();
844    let (secret_key, _) = secp.generate_keypair(&mut OsRng);
845    invoice
846        .sign(|m| -> Result<RecoverableSignature, anyhow::Error> {
847            Ok(secp.sign_ecdsa_recoverable(m, &secret_key))
848        })
849        .unwrap()
850        .to_string()
851}
852
853/// [OpeningFeeParams] that are valid for more than 48h
854pub(crate) fn get_test_ofp_48h(min_msat: u64, proportional: u32) -> grpc::OpeningFeeParams {
855    get_test_ofp_generic(min_msat, proportional, true, chrono::Duration::days(10))
856}
857
858/// [OpeningFeeParams] with 1 minute in the future or the past
859pub(crate) fn get_test_ofp(
860    min_msat: u64,
861    proportional: u32,
862    future_or_past: bool,
863) -> grpc::OpeningFeeParams {
864    get_test_ofp_generic(
865        min_msat,
866        proportional,
867        future_or_past,
868        chrono::Duration::seconds(60),
869    )
870}
871
872pub(crate) fn get_test_ofp_generic(
873    min_msat: u64,
874    proportional: u32,
875    future_or_past: bool,
876    duration: chrono::Duration,
877) -> grpc::OpeningFeeParams {
878    let now = Utc::now();
879    let date_time = match future_or_past {
880        true => now.checked_add_signed(duration).unwrap(),
881        false => now.checked_sub_signed(duration).unwrap(),
882    };
883    let formatted = date_time.to_rfc3339_opts(SecondsFormat::Millis, true);
884
885    grpc::OpeningFeeParams {
886        min_msat,
887        proportional,
888        valid_until: formatted,
889        max_idle_time: 0,
890        max_client_to_self_delay: 0,
891        promise: "".to_string(),
892    }
893    .into()
894}
895
896#[tonic::async_trait]
897impl TaprootSwapperAPI for MockBreezServer {
898    async fn create_swap(
899        &self,
900        hash: Vec<u8>,
901        refund_pubkey: Vec<u8>,
902    ) -> SdkResult<CreateSwapResponse> {
903        let lock_time = 1008;
904        let claim_pubkey_bytes =
905            hex::decode("0207121ffeda98fb0b28258d06efaa97623d1b4298dce269630432b9c12f4f6c84")
906                .unwrap();
907        let claim_pubkey = PublicKey::from_slice(&claim_pubkey_bytes).unwrap();
908        let refund_pubkey = PublicKey::from_slice(&refund_pubkey).unwrap();
909        let (x_only_claim_pubkey, _) = claim_pubkey.x_only_public_key();
910        let (x_only_refund_pubkey, _) = refund_pubkey.x_only_public_key();
911        let claim_script = claim_script(&x_only_claim_pubkey, &hash);
912        let refund_script = refund_script(&x_only_refund_pubkey, lock_time);
913
914        let taproot_spend_info = taproot_spend_info(
915            &claim_pubkey.serialize(),
916            &refund_pubkey.serialize(),
917            claim_script,
918            refund_script,
919        )?;
920        let address =
921            Address::p2tr_tweaked(taproot_spend_info.output_key(), Network::Bitcoin).to_string();
922
923        Ok(CreateSwapResponse {
924            address: address.to_string(),
925            claim_pubkey: claim_pubkey_bytes,
926            lock_time,
927            parameters: Some(SwapParameters {
928                max_swap_amount_sat: 1_000_000,
929                min_swap_amount_sat: 1_000,
930                min_utxo_amount_sat: 1_000,
931            }),
932        })
933    }
934    async fn pay_swap(&self, _payment_request: String) -> Result<PaySwapResponse, Status> {
935        Ok(PaySwapResponse::default())
936    }
937    async fn refund_swap(
938        &self,
939        _address: String,
940        _input_index: u32,
941        _pub_nonce: Vec<u8>,
942        _transaction: Vec<u8>,
943    ) -> SdkResult<RefundSwapResponse> {
944        Ok(RefundSwapResponse::default())
945    }
946    async fn swap_parameters(&self) -> SdkResult<Option<SwapParameters>> {
947        Ok(Some(SwapParameters::default()))
948    }
949}
950
951fn claim_script(x_only_claim_pubkey: &XOnlyPublicKey, hash: &[u8]) -> ScriptBuf {
952    script::Builder::new()
953        .push_opcode(OP_HASH160)
954        .push_slice(&ripemd160::Hash::hash(hash).as_byte_array())
955        .push_opcode(OP_EQUALVERIFY)
956        .push_x_only_key(x_only_claim_pubkey)
957        .push_opcode(OP_CHECKSIG)
958        .into_script()
959}
960
961fn refund_script(x_only_refund_pubkey: &XOnlyPublicKey, lock_time: u32) -> ScriptBuf {
962    script::Builder::new()
963        .push_x_only_key(x_only_refund_pubkey)
964        .push_opcode(OP_CHECKSIGVERIFY)
965        .push_int(Sequence::from_height(lock_time as u16).to_consensus_u32() as i64)
966        .push_opcode(OP_CSV)
967        .into_script()
968}
969
970fn key_agg_cache(claim_pubkey: &[u8], refund_pubkey: &[u8]) -> Result<MusigKeyAggCache> {
971    let cp = secp256k1::PublicKey::from_slice(claim_pubkey)?;
972    let rp = secp256k1::PublicKey::from_slice(refund_pubkey)?;
973    Ok(MusigKeyAggCache::new(
974        &secp256k1::Secp256k1::new(),
975        &[&cp, &rp],
976    ))
977}
978
979fn taproot_spend_info(
980    claim_pubkey: &[u8],
981    refund_pubkey: &[u8],
982    claim_script: ScriptBuf,
983    refund_script: ScriptBuf,
984) -> Result<TaprootSpendInfo> {
985    let m = key_agg_cache(claim_pubkey, refund_pubkey)?;
986    let internal_key = m.agg_pk();
987
988    // Convert from one secp256k1 crate to the other.
989    let internal_key = XOnlyPublicKey::from_slice(&internal_key.serialize())?;
990
991    // claim and refund scripts go in a taptree.
992    Ok(TaprootBuilder::new()
993        .add_leaf(1, claim_script)?
994        .add_leaf(1, refund_script)?
995        .finalize(&Secp256k1::new(), internal_key)
996        .map_err(|_| anyhow::anyhow!("taproot builder error"))?)
997}