Skip to main content

fedimint_ln_client/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::module_name_repetitions)]
6#![allow(clippy::must_use_candidate)]
7#![allow(clippy::too_many_lines)]
8
9pub use fedimint_ln_common as common;
10
11pub mod api;
12#[cfg(feature = "cli")]
13pub mod cli;
14pub mod db;
15pub mod events;
16pub mod incoming;
17pub mod pay;
18pub mod receive;
19/// Implements recurring payment codes (e.g. LNURL, BOLT12)
20pub mod recurring;
21
22use std::collections::{BTreeMap, BTreeSet};
23use std::iter::once;
24use std::str::FromStr;
25use std::sync::Arc;
26use std::time::Duration;
27
28use anyhow::{Context, anyhow, bail, ensure, format_err};
29use api::LnFederationApi;
30use async_stream::{stream, try_stream};
31use bitcoin::Network;
32use bitcoin::hashes::{Hash, HashEngine, Hmac, HmacEngine, sha256};
33use db::{
34    DbKeyPrefix, LightningGatewayKey, LightningGatewayKeyPrefix, PaymentResult, PaymentResultKey,
35    RecurringPaymentCodeKeyPrefix,
36};
37use fedimint_api_client::api::{DynModuleApi, ServerError};
38use fedimint_client_module::db::{ClientModuleMigrationFn, migrate_state};
39use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
40use fedimint_client_module::module::recovery::NoModuleBackup;
41use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
42use fedimint_client_module::oplog::UpdateStreamOrOutcome;
43use fedimint_client_module::sm::{DynState, ModuleNotifier, State, StateTransition};
44use fedimint_client_module::transaction::{
45    ClientInput, ClientInputBundle, ClientOutput, ClientOutputBundle, ClientOutputSM, FeeQuote,
46    FeeQuoteRequest, TransactionBuilder,
47};
48use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
49use fedimint_core::config::FederationId;
50use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
51use fedimint_core::db::{DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped};
52use fedimint_core::encoding::{Decodable, Encodable};
53use fedimint_core::module::{
54    Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
55};
56use fedimint_core::secp256k1::{
57    All, Keypair, PublicKey, Scalar, Secp256k1, SecretKey, Signing, Verification,
58};
59use fedimint_core::task::{MaybeSend, MaybeSync, timeout};
60use fedimint_core::util::update_merge::UpdateMerge;
61use fedimint_core::util::{BoxStream, FmtCompactAnyhow as _, backoff_util, retry};
62use fedimint_core::{
63    Amount, OutPoint, apply, async_trait_maybe_send, push_db_pair_items, runtime, secp256k1,
64};
65use fedimint_derive_secret::{ChildId, DerivableSecret};
66use fedimint_ln_common::client::GatewayApi;
67use fedimint_ln_common::config::{FeeToAmount, LightningClientConfig};
68use fedimint_ln_common::contracts::incoming::{IncomingContract, IncomingContractOffer};
69use fedimint_ln_common::contracts::outgoing::{
70    OutgoingContract, OutgoingContractAccount, OutgoingContractData,
71};
72use fedimint_ln_common::contracts::{
73    Contract, ContractId, DecryptedPreimage, EncryptedPreimage, IdentifiableContract, Preimage,
74    PreimageKey,
75};
76use fedimint_ln_common::gateway_endpoint_constants::{
77    GET_GATEWAY_ID_ENDPOINT, PAY_INVOICE_ENDPOINT,
78};
79use fedimint_ln_common::{
80    ContractOutput, KIND, LightningCommonInit, LightningGateway, LightningGatewayAnnouncement,
81    LightningGatewayRegistration, LightningInput, LightningModuleTypes, LightningOutput,
82    LightningOutputV0,
83};
84use fedimint_logging::LOG_CLIENT_MODULE_LN;
85use futures::{Future, StreamExt};
86use incoming::IncomingSmError;
87use itertools::Itertools;
88use lightning_invoice::{
89    Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret, RouteHint, RouteHintHop, RoutingFees,
90};
91use pay::PayInvoicePayload;
92use rand::rngs::OsRng;
93use rand::seq::IteratorRandom as _;
94use rand::{CryptoRng, Rng, RngCore};
95use reqwest::Method;
96use serde::{Deserialize, Serialize};
97use strum::IntoEnumIterator;
98use tokio::sync::Notify;
99use tracing::{debug, error, info};
100
101use crate::db::PaymentResultPrefix;
102use crate::incoming::{
103    FundingOfferState, IncomingSmCommon, IncomingSmStates, IncomingStateMachine,
104};
105use crate::pay::lightningpay::LightningPayStates;
106use crate::pay::{
107    GatewayPayError, LightningPayCommon, LightningPayCreatedOutgoingLnContract,
108    LightningPayStateMachine,
109};
110use crate::receive::{
111    LightningReceiveConfirmedInvoice, LightningReceiveError, LightningReceiveStateMachine,
112    LightningReceiveStates, LightningReceiveSubmittedOffer, get_incoming_contract,
113};
114use crate::recurring::RecurringPaymentCodeEntry;
115
116/// Number of blocks until outgoing lightning contracts times out and user
117/// client can get refund
118const OUTGOING_LN_CONTRACT_TIMELOCK: u64 = 500;
119
120// 24 hours. Many wallets default to 1 hour, but it's a bad user experience if
121// invoices expire too quickly
122const DEFAULT_INVOICE_EXPIRY_TIME: Duration = Duration::from_hours(24);
123
124#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
125#[serde(rename_all = "snake_case")]
126pub enum PayType {
127    // Payment from this client to another user within the federation
128    Internal(OperationId),
129    // Payment from this client to another user, facilitated by a gateway
130    Lightning(OperationId),
131}
132
133impl PayType {
134    pub fn operation_id(&self) -> OperationId {
135        match self {
136            PayType::Internal(operation_id) | PayType::Lightning(operation_id) => *operation_id,
137        }
138    }
139
140    pub fn payment_type(&self) -> String {
141        match self {
142            PayType::Internal(_) => "internal",
143            PayType::Lightning(_) => "lightning",
144        }
145        .into()
146    }
147}
148
149/// Where to receive the payment to, either to ourselves or to another user
150#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
151pub enum ReceivingKey {
152    /// The keypair used to receive payments for ourselves, we will use this to
153    /// sweep to our own ecash wallet on success
154    Personal(Keypair),
155    /// A public key of another user, the lightning payment will be locked to
156    /// this key for them to claim on success
157    External(PublicKey),
158}
159
160impl ReceivingKey {
161    /// The public key of the receiving key
162    pub fn public_key(&self) -> PublicKey {
163        match self {
164            ReceivingKey::Personal(keypair) => keypair.public_key(),
165            ReceivingKey::External(public_key) => *public_key,
166        }
167    }
168}
169
170#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
171pub enum LightningPaymentOutcome {
172    Success { preimage: String },
173    Failure { error_message: String },
174}
175
176/// The high-level state of an pay operation internal to the federation,
177/// started with [`LightningClientModule::pay_bolt11_invoice`].
178#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
179#[serde(rename_all = "snake_case")]
180pub enum InternalPayState {
181    Funding,
182    Preimage(Preimage),
183    RefundSuccess {
184        out_points: Vec<OutPoint>,
185        error: IncomingSmError,
186    },
187    RefundError {
188        error_message: String,
189        error: IncomingSmError,
190    },
191    FundingFailed {
192        error: IncomingSmError,
193    },
194    UnexpectedError(String),
195}
196
197/// The high-level state of a pay operation over lightning,
198/// started with [`LightningClientModule::pay_bolt11_invoice`].
199#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum LnPayState {
202    Created,
203    Canceled,
204    Funded { block_height: u32 },
205    WaitingForRefund { error_reason: String },
206    AwaitingChange,
207    Success { preimage: String },
208    Refunded { gateway_error: GatewayPayError },
209    UnexpectedError { error_message: String },
210}
211
212/// The high-level state of a reissue operation started with
213/// [`LightningClientModule::create_bolt11_invoice`].
214#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
215#[serde(rename_all = "snake_case")]
216pub enum LnReceiveState {
217    Created,
218    WaitingForPayment { invoice: String, timeout: Duration },
219    Canceled { reason: LightningReceiveError },
220    Funded,
221    AwaitingFunds,
222    Claimed,
223}
224
225fn invoice_has_internal_payment_markers(
226    invoice: &Bolt11Invoice,
227    markers: (fedimint_core::secp256k1::PublicKey, u64),
228) -> bool {
229    // Asserts that the invoice src_node_id and short_channel_id match known
230    // values used as internal payment markers
231    invoice
232        .route_hints()
233        .first()
234        .and_then(|rh| rh.0.last())
235        .map(|hop| (hop.src_node_id, hop.short_channel_id))
236        == Some(markers)
237}
238
239fn invoice_routes_back_to_federation(
240    invoice: &Bolt11Invoice,
241    gateways: Vec<LightningGateway>,
242) -> bool {
243    gateways.into_iter().any(|gateway| {
244        invoice
245            .route_hints()
246            .first()
247            .and_then(|rh| rh.0.last())
248            .map(|hop| (hop.src_node_id, hop.short_channel_id))
249            == Some((gateway.node_pub_key, gateway.federation_index))
250    })
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(rename_all = "snake_case")]
255pub struct LightningOperationMetaPay {
256    pub out_point: OutPoint,
257    pub invoice: Bolt11Invoice,
258    pub fee: Amount,
259    pub change: Vec<OutPoint>,
260    pub is_internal_payment: bool,
261    pub contract_id: ContractId,
262    pub gateway_id: Option<secp256k1::PublicKey>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct LightningOperationMeta {
267    pub variant: LightningOperationMetaVariant,
268    pub extra_meta: serde_json::Value,
269}
270
271pub use deprecated_variant_hack::LightningOperationMetaVariant;
272
273/// This is a hack to allow us to use the deprecated variant in the database
274/// without the serde derived implementation throwing warnings.
275///
276/// See <https://github.com/serde-rs/serde/issues/2195>
277#[allow(deprecated)]
278mod deprecated_variant_hack {
279    use super::{
280        Bolt11Invoice, Deserialize, LightningOperationMetaPay, OperationId, OutPoint, Serialize,
281        secp256k1,
282    };
283    use crate::recurring::ReurringPaymentReceiveMeta;
284
285    #[derive(Debug, Clone, Serialize, Deserialize)]
286    #[serde(rename_all = "snake_case")]
287    pub enum LightningOperationMetaVariant {
288        Pay(LightningOperationMetaPay),
289        Receive {
290            out_point: OutPoint,
291            invoice: Bolt11Invoice,
292            gateway_id: Option<secp256k1::PublicKey>,
293        },
294        ReceiveReclaim {
295            original_operation_id: OperationId,
296            invoice: Bolt11Invoice,
297            gateway_id: Option<secp256k1::PublicKey>,
298        },
299        #[deprecated(
300            since = "0.7.0",
301            note = "Use recurring payment functionality instead instead"
302        )]
303        Claim {
304            out_points: Vec<OutPoint>,
305        },
306        RecurringPaymentReceive(ReurringPaymentReceiveMeta),
307    }
308}
309
310#[derive(Debug, Clone, Default)]
311pub struct LightningClientInit {
312    pub gateway_conn: Option<Arc<dyn GatewayConnection + Send + Sync>>,
313}
314
315impl ModuleInit for LightningClientInit {
316    type Common = LightningCommonInit;
317
318    async fn dump_database(
319        &self,
320        dbtx: &mut DatabaseTransaction<'_>,
321        prefix_names: Vec<String>,
322    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
323        let mut ln_client_items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> =
324            BTreeMap::new();
325        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
326            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
327        });
328
329        for table in filtered_prefixes {
330            #[allow(clippy::match_same_arms)]
331            match table {
332                DbKeyPrefix::ActiveGateway | DbKeyPrefix::MetaOverridesDeprecated => {
333                    // Deprecated
334                }
335                DbKeyPrefix::PaymentResult => {
336                    push_db_pair_items!(
337                        dbtx,
338                        PaymentResultPrefix,
339                        PaymentResultKey,
340                        PaymentResult,
341                        ln_client_items,
342                        "Payment Result"
343                    );
344                }
345                DbKeyPrefix::LightningGateway => {
346                    push_db_pair_items!(
347                        dbtx,
348                        LightningGatewayKeyPrefix,
349                        LightningGatewayKey,
350                        LightningGatewayRegistration,
351                        ln_client_items,
352                        "Lightning Gateways"
353                    );
354                }
355                DbKeyPrefix::RecurringPaymentKey => {
356                    push_db_pair_items!(
357                        dbtx,
358                        RecurringPaymentCodeKeyPrefix,
359                        RecurringPaymentCodeKey,
360                        RecurringPaymentCodeEntry,
361                        ln_client_items,
362                        "Recurring Payment Code"
363                    );
364                }
365                DbKeyPrefix::ExternalReservedStart
366                | DbKeyPrefix::CoreInternalReservedStart
367                | DbKeyPrefix::CoreInternalReservedEnd => {}
368            }
369        }
370
371        Box::new(ln_client_items.into_iter())
372    }
373}
374
375#[derive(Debug)]
376#[repr(u64)]
377pub enum LightningChildKeys {
378    RedeemKey = 0,
379    PreimageAuthentication = 1,
380    RecurringPaymentCodeSecret = 2,
381}
382
383#[apply(async_trait_maybe_send!)]
384impl ClientModuleInit for LightningClientInit {
385    type Module = LightningClientModule;
386
387    fn supported_api_versions(&self) -> MultiApiVersion {
388        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
389            .expect("no version conflicts")
390    }
391
392    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
393        let gateway_conn = if let Some(gateway_conn) = self.gateway_conn.clone() {
394            gateway_conn
395        } else {
396            let api = GatewayApi::new(None, args.connector_registry.clone());
397            Arc::new(RealGatewayConnection { api })
398        };
399        Ok(LightningClientModule::new(args, gateway_conn))
400    }
401
402    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
403        let mut migrations: BTreeMap<DatabaseVersion, ClientModuleMigrationFn> = BTreeMap::new();
404        migrations.insert(DatabaseVersion(0), |dbtx, _, _| {
405            Box::pin(async {
406                dbtx.remove_entry(&crate::db::ActiveGatewayKey).await;
407                Ok(None)
408            })
409        });
410
411        migrations.insert(DatabaseVersion(1), |_, active_states, inactive_states| {
412            Box::pin(async {
413                migrate_state(active_states, inactive_states, db::get_v1_migrated_state)
414            })
415        });
416
417        migrations.insert(DatabaseVersion(2), |_, active_states, inactive_states| {
418            Box::pin(async {
419                migrate_state(active_states, inactive_states, db::get_v2_migrated_state)
420            })
421        });
422
423        migrations.insert(DatabaseVersion(3), |_, active_states, inactive_states| {
424            Box::pin(async {
425                migrate_state(active_states, inactive_states, db::get_v3_migrated_state)
426            })
427        });
428
429        migrations
430    }
431
432    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
433        Some(
434            DbKeyPrefix::iter()
435                .map(|p| p as u8)
436                .chain(
437                    DbKeyPrefix::ExternalReservedStart as u8
438                        ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
439                )
440                .collect(),
441        )
442    }
443}
444
445/// Client side lightning module
446///
447/// Note that lightning gateways use a different version
448/// of client side module.
449#[derive(Debug)]
450pub struct LightningClientModule {
451    pub cfg: LightningClientConfig,
452    notifier: ModuleNotifier<LightningClientStateMachines>,
453    redeem_key: Keypair,
454    recurring_payment_code_secret: DerivableSecret,
455    secp: Secp256k1<All>,
456    module_api: DynModuleApi,
457    preimage_auth: Keypair,
458    client_ctx: ClientContext<Self>,
459    update_gateway_cache_merge: UpdateMerge,
460    gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
461    new_recurring_payment_code: Arc<Notify>,
462}
463
464#[apply(async_trait_maybe_send!)]
465impl ClientModule for LightningClientModule {
466    type Init = LightningClientInit;
467    type Common = LightningModuleTypes;
468    type Backup = NoModuleBackup;
469    type ModuleStateMachineContext = LightningClientContext;
470    type States = LightningClientStateMachines;
471
472    fn context(&self) -> Self::ModuleStateMachineContext {
473        LightningClientContext {
474            ln_decoder: self.decoder(),
475            redeem_key: self.redeem_key,
476            gateway_conn: self.gateway_conn.clone(),
477            client_ctx: Some(self.client_ctx.clone()),
478        }
479    }
480
481    fn input_fee(
482        &self,
483        _amount: &Amounts,
484        _input: &<Self::Common as ModuleCommon>::Input,
485    ) -> Option<Amounts> {
486        Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input))
487    }
488
489    fn output_fee(
490        &self,
491        _amount: &Amounts,
492        output: &<Self::Common as ModuleCommon>::Output,
493    ) -> Option<Amounts> {
494        match output.maybe_v0_ref()? {
495            LightningOutputV0::Contract(_) => {
496                Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_output))
497            }
498            LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
499                Some(Amounts::ZERO)
500            }
501        }
502    }
503
504    #[cfg(feature = "cli")]
505    async fn handle_cli_command(
506        &self,
507        args: &[std::ffi::OsString],
508    ) -> anyhow::Result<serde_json::Value> {
509        cli::handle_cli_command(self, args).await
510    }
511
512    async fn handle_rpc(
513        &self,
514        method: String,
515        payload: serde_json::Value,
516    ) -> BoxStream<'_, anyhow::Result<serde_json::Value>> {
517        Box::pin(try_stream! {
518            match method.as_str() {
519                "create_bolt11_invoice" => {
520                    let req: CreateBolt11InvoiceRequest = serde_json::from_value(payload)?;
521                    let (op, invoice, _) = self
522                        .create_bolt11_invoice(
523                            req.amount,
524                            lightning_invoice::Bolt11InvoiceDescription::Direct(
525                                lightning_invoice::Description::new(req.description)?,
526                            ),
527                            req.expiry_time,
528                            req.extra_meta,
529                            req.gateway,
530                        )
531                        .await?;
532                    yield serde_json::json!({
533                        "operation_id": op,
534                        "invoice": invoice,
535                    });
536                }
537                "pay_bolt11_invoice" => {
538                    let req: PayBolt11InvoiceRequest = serde_json::from_value(payload)?;
539                    let outgoing_payment = self
540                        .pay_bolt11_invoice(req.maybe_gateway, req.invoice, req.extra_meta)
541                        .await?;
542                    yield serde_json::to_value(outgoing_payment)?;
543                }
544                "select_available_gateway" => {
545                    let req: SelectAvailableGatewayRequest = serde_json::from_value(payload)?;
546                    let gateway = self.select_available_gateway(req.maybe_gateway,req.maybe_invoice).await?;
547                    yield serde_json::to_value(gateway)?;
548                }
549                "subscribe_ln_pay" => {
550                    let req: SubscribeLnPayRequest = serde_json::from_value(payload)?;
551                    for await state in self.subscribe_ln_pay(req.operation_id).await?.into_stream() {
552                        yield serde_json::to_value(state)?;
553                    }
554                }
555                "subscribe_internal_pay" => {
556                    let req: SubscribeInternalPayRequest = serde_json::from_value(payload)?;
557                    for await state in self.subscribe_internal_pay(req.operation_id).await?.into_stream() {
558                        yield serde_json::to_value(state)?;
559                    }
560                }
561                "subscribe_ln_receive" => {
562                    let req: SubscribeLnReceiveRequest = serde_json::from_value(payload)?;
563                    for await state in self.subscribe_ln_receive(req.operation_id).await?.into_stream()
564                    {
565                        yield serde_json::to_value(state)?;
566                    }
567                }
568                "reclaim_ln_receive" => {
569                    let req: ReclaimLnReceiveRequest = serde_json::from_value(payload)?;
570                    let operation_id = self.reclaim_ln_receive(req.original_operation_id).await?;
571                    yield serde_json::json!({
572                        "operation_id": operation_id,
573                    });
574                }
575                "create_bolt11_invoice_for_user_tweaked" => {
576                    let req: CreateBolt11InvoiceForUserTweakedRequest = serde_json::from_value(payload)?;
577                    let (op, invoice, _) = self
578                        .create_bolt11_invoice_for_user_tweaked(
579                            req.amount,
580                            lightning_invoice::Bolt11InvoiceDescription::Direct(
581                                lightning_invoice::Description::new(req.description)?,
582                            ),
583                            req.expiry_time,
584                            req.user_key,
585                            req.index,
586                            req.extra_meta,
587                            req.gateway,
588                        )
589                        .await?;
590                    yield serde_json::json!({
591                        "operation_id": op,
592                        "invoice": invoice,
593                    });
594                }
595                #[allow(deprecated)]
596                "scan_receive_for_user_tweaked" => {
597                    let req: ScanReceiveForUserTweakedRequest = serde_json::from_value(payload)?;
598                    let keypair = Keypair::from_secret_key(&self.secp, &req.user_key);
599                    let operation_ids = self.scan_receive_for_user_tweaked(keypair, req.indices, req.extra_meta).await;
600                    yield serde_json::to_value(operation_ids)?;
601                }
602                #[allow(deprecated)]
603                "subscribe_ln_claim" => {
604                    let req: SubscribeLnClaimRequest = serde_json::from_value(payload)?;
605                    for await state in self.subscribe_ln_claim(req.operation_id).await?.into_stream() {
606                        yield serde_json::to_value(state)?;
607                    }
608                }
609                "get_gateway" => {
610                    let req: GetGatewayRequest = serde_json::from_value(payload)?;
611                    let gateway = self.get_gateway(req.gateway_id, req.force_internal).await?;
612                    yield serde_json::to_value(gateway)?;
613                }
614                "list_gateways" => {
615                    let gateways = self.list_gateways().await;
616                    yield serde_json::to_value(gateways)?;
617                }
618                "update_gateway_cache" => {
619                    self.update_gateway_cache().await?;
620                    yield serde_json::Value::Null;
621                }
622                "pay_lightning_address" => {
623                    let req: PayLightningAddressRequest = serde_json::from_value(payload)?;
624                    let invoice = get_invoice(&req.address, Some(Amount::from_msats(req.amount)), None).await?;
625                    let gateway = self.get_gateway(None, false).await?;
626                    let output = self.pay_bolt11_invoice(gateway, invoice, ()).await?;
627
628                    yield serde_json::to_value(output)?;
629                }
630                _ => {
631                    Err(anyhow::format_err!("Unknown method: {method}"))?;
632                    unreachable!()
633                },
634            }
635        })
636    }
637}
638
639#[derive(Deserialize)]
640struct CreateBolt11InvoiceRequest {
641    amount: Amount,
642    description: String,
643    expiry_time: Option<u64>,
644    extra_meta: serde_json::Value,
645    gateway: Option<LightningGateway>,
646}
647
648#[derive(Deserialize)]
649struct PayBolt11InvoiceRequest {
650    maybe_gateway: Option<LightningGateway>,
651    invoice: Bolt11Invoice,
652    extra_meta: Option<serde_json::Value>,
653}
654
655#[derive(Deserialize)]
656struct SubscribeLnPayRequest {
657    operation_id: OperationId,
658}
659
660#[derive(Deserialize)]
661struct SubscribeInternalPayRequest {
662    operation_id: OperationId,
663}
664
665#[derive(Deserialize)]
666struct SubscribeLnReceiveRequest {
667    operation_id: OperationId,
668}
669
670#[derive(Deserialize)]
671struct ReclaimLnReceiveRequest {
672    original_operation_id: OperationId,
673}
674
675#[derive(Debug, Serialize, Deserialize)]
676pub struct SelectAvailableGatewayRequest {
677    maybe_gateway: Option<LightningGateway>,
678    maybe_invoice: Option<Bolt11Invoice>,
679}
680
681#[derive(Deserialize)]
682struct CreateBolt11InvoiceForUserTweakedRequest {
683    amount: Amount,
684    description: String,
685    expiry_time: Option<u64>,
686    user_key: PublicKey,
687    index: u64,
688    extra_meta: serde_json::Value,
689    gateway: Option<LightningGateway>,
690}
691
692#[derive(Deserialize)]
693struct ScanReceiveForUserTweakedRequest {
694    user_key: SecretKey,
695    indices: Vec<u64>,
696    extra_meta: serde_json::Value,
697}
698
699#[derive(Deserialize)]
700struct SubscribeLnClaimRequest {
701    operation_id: OperationId,
702}
703
704#[derive(Deserialize)]
705struct GetGatewayRequest {
706    gateway_id: Option<secp256k1::PublicKey>,
707    force_internal: bool,
708}
709
710#[derive(Deserialize)]
711struct PayLightningAddressRequest {
712    address: String,
713    amount: u64,
714}
715
716#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
717pub enum GatewayStatus {
718    OnlineVetted,
719    OnlineNonVetted,
720}
721
722#[derive(thiserror::Error, Debug, Clone)]
723pub enum PayBolt11InvoiceError {
724    #[error("Previous payment attempt({}) still in progress", .operation_id.fmt_full())]
725    PreviousPaymentAttemptStillInProgress { operation_id: OperationId },
726    #[error("No LN gateway available")]
727    NoLnGatewayAvailable,
728    #[error("Funded contract already exists: {}", .contract_id)]
729    FundedContractAlreadyExists { contract_id: ContractId },
730}
731
732impl LightningClientModule {
733    fn new(
734        args: &ClientModuleInitArgs<LightningClientInit>,
735        gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
736    ) -> Self {
737        let secp = Secp256k1::new();
738
739        let new_recurring_payment_code = Arc::new(Notify::new());
740        args.spawn_cancellable(
741            "Recurring payment sync",
742            Self::scan_recurring_payment_code_invoices(
743                args.context(),
744                new_recurring_payment_code.clone(),
745            ),
746        );
747
748        Self {
749            cfg: args.cfg().clone(),
750            notifier: args.notifier().clone(),
751            redeem_key: args
752                .module_root_secret()
753                .child_key(ChildId(LightningChildKeys::RedeemKey as u64))
754                .to_secp_key(&secp),
755            recurring_payment_code_secret: args.module_root_secret().child_key(ChildId(
756                LightningChildKeys::RecurringPaymentCodeSecret as u64,
757            )),
758            module_api: args.module_api().clone(),
759            preimage_auth: args
760                .module_root_secret()
761                .child_key(ChildId(LightningChildKeys::PreimageAuthentication as u64))
762                .to_secp_key(&secp),
763            secp,
764            client_ctx: args.context(),
765            update_gateway_cache_merge: UpdateMerge::default(),
766            gateway_conn,
767            new_recurring_payment_code,
768        }
769    }
770
771    pub async fn get_prev_payment_result(
772        &self,
773        payment_hash: &sha256::Hash,
774        dbtx: &mut DatabaseTransaction<'_>,
775    ) -> PaymentResult {
776        let prev_result = dbtx
777            .get_value(&PaymentResultKey {
778                payment_hash: *payment_hash,
779            })
780            .await;
781        prev_result.unwrap_or(PaymentResult {
782            index: 0,
783            completed_payment: None,
784        })
785    }
786
787    fn get_payment_operation_id(payment_hash: &sha256::Hash, index: u16) -> OperationId {
788        // Copy the 32 byte payment hash and a 2 byte index to make every payment
789        // attempt have a unique `OperationId`
790        let mut bytes = [0; 34];
791        bytes[0..32].copy_from_slice(&payment_hash.to_byte_array());
792        bytes[32..34].copy_from_slice(&index.to_le_bytes());
793        let hash: sha256::Hash = Hash::hash(&bytes);
794        OperationId(hash.to_byte_array())
795    }
796
797    /// Hashes the client's preimage authentication secret with the provided
798    /// `payment_hash`. The resulting hash is used when contacting the
799    /// gateway to determine if this client is allowed to be shown the
800    /// preimage.
801    fn get_preimage_authentication(&self, payment_hash: &sha256::Hash) -> sha256::Hash {
802        let mut bytes = [0; 64];
803        bytes[0..32].copy_from_slice(&payment_hash.to_byte_array());
804        bytes[32..64].copy_from_slice(&self.preimage_auth.secret_bytes());
805        Hash::hash(&bytes)
806    }
807
808    /// Create an output that incentivizes a Lightning gateway to pay an invoice
809    /// for us. It has time till the block height defined by `timelock`,
810    /// after that we can claim our money back.
811    async fn create_outgoing_output<'a, 'b>(
812        &'a self,
813        operation_id: OperationId,
814        invoice: Bolt11Invoice,
815        gateway: LightningGateway,
816        fed_id: FederationId,
817        mut rng: impl RngCore + CryptoRng + 'a,
818    ) -> anyhow::Result<(
819        ClientOutput<LightningOutputV0>,
820        ClientOutputSM<LightningClientStateMachines>,
821        ContractId,
822    )> {
823        let federation_currency: Currency = self.cfg.network.0.into();
824        let invoice_currency = invoice.currency();
825        ensure!(
826            federation_currency == invoice_currency,
827            "Invalid invoice currency: expected={federation_currency:?}, got={invoice_currency:?}"
828        );
829
830        // Do not create the funding transaction if the gateway is not currently
831        // available
832        self.gateway_conn
833            .verify_gateway_availability(&gateway)
834            .await?;
835
836        let consensus_count = self
837            .module_api
838            .fetch_consensus_block_count()
839            .await?
840            .ok_or(format_err!("Cannot get consensus block count"))?;
841
842        // Add the timelock to the current block count and the invoice's
843        // `min_cltv_delta`
844        let min_final_cltv = invoice.min_final_cltv_expiry_delta();
845        let absolute_timelock =
846            consensus_count + min_final_cltv + OUTGOING_LN_CONTRACT_TIMELOCK - 1;
847
848        // Compute amount to lock in the outgoing contract
849        let invoice_amount = Amount::from_msats(
850            invoice
851                .amount_milli_satoshis()
852                .context("MissingInvoiceAmount")?,
853        );
854
855        let gateway_fee = gateway.fees.to_amount(&invoice_amount);
856        let contract_amount = invoice_amount + gateway_fee;
857
858        let user_sk = Keypair::new(&self.secp, &mut rng);
859
860        let payment_hash = *invoice.payment_hash();
861        let preimage_auth = self.get_preimage_authentication(&payment_hash);
862        let contract = OutgoingContract {
863            hash: payment_hash,
864            gateway_key: gateway.gateway_redeem_key,
865            timelock: absolute_timelock as u32,
866            user_key: user_sk.public_key(),
867            cancelled: false,
868        };
869
870        let outgoing_payment = OutgoingContractData {
871            recovery_key: user_sk,
872            contract_account: OutgoingContractAccount {
873                amount: contract_amount,
874                contract: contract.clone(),
875            },
876        };
877
878        let contract_id = contract.contract_id();
879        let sm_gen = Arc::new(move |out_point_range: OutPointRange| {
880            vec![LightningClientStateMachines::LightningPay(
881                LightningPayStateMachine {
882                    common: LightningPayCommon {
883                        operation_id,
884                        federation_id: fed_id,
885                        contract: outgoing_payment.clone(),
886                        gateway_fee,
887                        preimage_auth,
888                        invoice: invoice.clone(),
889                    },
890                    state: LightningPayStates::CreatedOutgoingLnContract(
891                        LightningPayCreatedOutgoingLnContract {
892                            funding_txid: out_point_range.txid(),
893                            contract_id,
894                            gateway: gateway.clone(),
895                        },
896                    ),
897                },
898            )]
899        });
900
901        let ln_output = LightningOutputV0::Contract(ContractOutput {
902            amount: contract_amount,
903            contract: Contract::Outgoing(contract),
904        });
905
906        Ok((
907            ClientOutput {
908                output: ln_output,
909                amounts: Amounts::new_bitcoin(contract_amount),
910            },
911            ClientOutputSM {
912                state_machines: sm_gen,
913            },
914            contract_id,
915        ))
916    }
917
918    /// Create an output that funds an incoming contract within the federation
919    /// This directly completes a transaction between users, without involving a
920    /// gateway
921    async fn create_incoming_output(
922        &self,
923        operation_id: OperationId,
924        invoice: Bolt11Invoice,
925    ) -> anyhow::Result<(
926        ClientOutput<LightningOutputV0>,
927        ClientOutputSM<LightningClientStateMachines>,
928        ContractId,
929    )> {
930        let payment_hash = *invoice.payment_hash();
931        let invoice_amount = Amount {
932            msats: invoice
933                .amount_milli_satoshis()
934                .ok_or(IncomingSmError::AmountError {
935                    invoice: invoice.clone(),
936                })?,
937        };
938
939        let (incoming_output, amount, contract_id) = create_incoming_contract_output(
940            &self.module_api,
941            payment_hash,
942            invoice_amount,
943            &self.redeem_key,
944        )
945        .await?;
946
947        let client_output = ClientOutput::<LightningOutputV0> {
948            output: incoming_output,
949            amounts: Amounts::new_bitcoin(amount),
950        };
951
952        let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
953            state_machines: Arc::new(move |out_point_range| {
954                vec![LightningClientStateMachines::InternalPay(
955                    IncomingStateMachine {
956                        common: IncomingSmCommon {
957                            operation_id,
958                            contract_id,
959                            payment_hash,
960                        },
961                        state: IncomingSmStates::FundingOffer(FundingOfferState {
962                            txid: out_point_range.txid(),
963                        }),
964                    },
965                )]
966            }),
967        };
968
969        Ok((client_output, client_output_sm, contract_id))
970    }
971
972    async fn await_receive_success(
973        &self,
974        operation_id: OperationId,
975    ) -> Result<(), LightningReceiveError> {
976        let mut stream = self.notifier.subscribe(operation_id).await;
977        loop {
978            if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
979                match state.state {
980                    LightningReceiveStates::Success(_) => return Ok(()),
981                    LightningReceiveStates::Canceled(e) => {
982                        return Err(e);
983                    }
984                    _ => {}
985                }
986            }
987        }
988    }
989
990    async fn await_claim_acceptance(
991        &self,
992        operation_id: OperationId,
993    ) -> Result<Vec<OutPoint>, LightningReceiveError> {
994        let mut stream = self.notifier.subscribe(operation_id).await;
995        loop {
996            if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
997                match state.state {
998                    LightningReceiveStates::Success(out_points) => return Ok(out_points),
999                    LightningReceiveStates::Canceled(e) => {
1000                        return Err(e);
1001                    }
1002                    _ => {}
1003                }
1004            }
1005        }
1006    }
1007
1008    #[allow(clippy::too_many_arguments)]
1009    #[allow(clippy::type_complexity)]
1010    fn create_lightning_receive_output<'a>(
1011        &'a self,
1012        amount: Amount,
1013        description: lightning_invoice::Bolt11InvoiceDescription,
1014        receiving_key: ReceivingKey,
1015        mut rng: impl RngCore + CryptoRng + 'a,
1016        expiry_time: Option<u64>,
1017        src_node_id: secp256k1::PublicKey,
1018        short_channel_id: u64,
1019        route_hints: &[fedimint_ln_common::route_hints::RouteHint],
1020        network: Network,
1021    ) -> anyhow::Result<(
1022        OperationId,
1023        Bolt11Invoice,
1024        ClientOutputBundle<LightningOutput, LightningClientStateMachines>,
1025        [u8; 32],
1026    )> {
1027        let preimage_key: [u8; 33] = receiving_key.public_key().serialize();
1028        let preimage = sha256::Hash::hash(&preimage_key);
1029        let payment_hash = sha256::Hash::hash(&preimage.to_byte_array());
1030
1031        // Temporary lightning node pubkey
1032        let (node_secret_key, node_public_key) = self.secp.generate_keypair(&mut rng);
1033
1034        // Route hint instructing payer how to route to gateway
1035        let route_hint_last_hop = RouteHintHop {
1036            src_node_id,
1037            short_channel_id,
1038            fees: RoutingFees {
1039                base_msat: 0,
1040                proportional_millionths: 0,
1041            },
1042            cltv_expiry_delta: 30,
1043            htlc_minimum_msat: None,
1044            htlc_maximum_msat: None,
1045        };
1046        let mut final_route_hints = vec![RouteHint(vec![route_hint_last_hop.clone()])];
1047        if !route_hints.is_empty() {
1048            let mut two_hop_route_hints: Vec<RouteHint> = route_hints
1049                .iter()
1050                .map(|rh| {
1051                    RouteHint(
1052                        rh.to_ldk_route_hint()
1053                            .0
1054                            .iter()
1055                            .cloned()
1056                            .chain(once(route_hint_last_hop.clone()))
1057                            .collect(),
1058                    )
1059                })
1060                .collect();
1061            final_route_hints.append(&mut two_hop_route_hints);
1062        }
1063
1064        let duration_since_epoch = fedimint_core::time::duration_since_epoch();
1065
1066        let mut invoice_builder = InvoiceBuilder::new(network.into())
1067            .amount_milli_satoshis(amount.msats)
1068            .invoice_description(description)
1069            .payment_hash(payment_hash)
1070            .payment_secret(PaymentSecret(rng.r#gen()))
1071            .duration_since_epoch(duration_since_epoch)
1072            .min_final_cltv_expiry_delta(18)
1073            .payee_pub_key(node_public_key)
1074            .expiry_time(Duration::from_secs(
1075                expiry_time.unwrap_or(DEFAULT_INVOICE_EXPIRY_TIME.as_secs()),
1076            ));
1077
1078        for rh in final_route_hints {
1079            invoice_builder = invoice_builder.private_route(rh);
1080        }
1081
1082        let invoice = invoice_builder
1083            .build_signed(|msg| self.secp.sign_ecdsa_recoverable(msg, &node_secret_key))?;
1084
1085        let operation_id = OperationId(*invoice.payment_hash().as_ref());
1086
1087        let sm_invoice = invoice.clone();
1088        let sm_gen = Arc::new(move |out_point_range: OutPointRange| {
1089            vec![LightningClientStateMachines::Receive(
1090                LightningReceiveStateMachine {
1091                    operation_id,
1092                    state: LightningReceiveStates::SubmittedOffer(LightningReceiveSubmittedOffer {
1093                        offer_txid: out_point_range.txid(),
1094                        invoice: sm_invoice.clone(),
1095                        receiving_key,
1096                    }),
1097                },
1098            )]
1099        });
1100
1101        let ln_output = LightningOutput::new_v0_offer(IncomingContractOffer {
1102            amount,
1103            hash: payment_hash,
1104            encrypted_preimage: EncryptedPreimage::new(
1105                &PreimageKey(preimage_key),
1106                &self.cfg.threshold_pub_key,
1107            ),
1108            expiry_time,
1109        });
1110
1111        Ok((
1112            operation_id,
1113            invoice,
1114            ClientOutputBundle::new(
1115                vec![ClientOutput {
1116                    output: ln_output,
1117                    amounts: Amounts::ZERO,
1118                }],
1119                vec![ClientOutputSM {
1120                    state_machines: sm_gen,
1121                }],
1122            ),
1123            *preimage.as_ref(),
1124        ))
1125    }
1126
1127    pub async fn select_available_gateway(
1128        &self,
1129        maybe_gateway: Option<LightningGateway>,
1130        maybe_invoice: Option<Bolt11Invoice>,
1131    ) -> anyhow::Result<LightningGateway> {
1132        if let Some(gw) = maybe_gateway {
1133            let gw_id = gw.gateway_id;
1134            if self
1135                .gateway_conn
1136                .verify_gateway_availability(&gw)
1137                .await
1138                .is_ok()
1139            {
1140                return Ok(gw);
1141            }
1142            return Err(anyhow::anyhow!("Specified gateway is offline: {gw_id}"));
1143        }
1144
1145        let gateways: Vec<LightningGatewayAnnouncement> = self.list_gateways().await;
1146        if gateways.is_empty() {
1147            return Err(anyhow::anyhow!("No gateways available"));
1148        }
1149
1150        let gateways_with_status =
1151            futures::future::join_all(gateways.into_iter().map(|gw| async {
1152                let online = self
1153                    .gateway_conn
1154                    .verify_gateway_availability(&gw.info)
1155                    .await
1156                    .is_ok();
1157                (gw, online)
1158            }))
1159            .await;
1160
1161        let sorted_gateways: Vec<(LightningGatewayAnnouncement, GatewayStatus)> =
1162            gateways_with_status
1163                .into_iter()
1164                .filter_map(|(ann, online)| {
1165                    if online {
1166                        let status = if ann.vetted {
1167                            GatewayStatus::OnlineVetted
1168                        } else {
1169                            GatewayStatus::OnlineNonVetted
1170                        };
1171                        Some((ann, status))
1172                    } else {
1173                        None
1174                    }
1175                })
1176                .collect();
1177
1178        if sorted_gateways.is_empty() {
1179            return Err(anyhow::anyhow!("No Lightning Gateway was reachable"));
1180        }
1181
1182        let amount_msat = maybe_invoice.and_then(|inv| inv.amount_milli_satoshis());
1183        let sorted_gateways = sorted_gateways
1184            .into_iter()
1185            .sorted_by_key(|(ann, status)| {
1186                let total_fee_msat: u64 =
1187                    amount_msat.map_or(u64::from(ann.info.fees.base_msat), |amt| {
1188                        u64::from(ann.info.fees.base_msat)
1189                            + ((u128::from(amt)
1190                                * u128::from(ann.info.fees.proportional_millionths))
1191                                / 1_000_000) as u64
1192                    });
1193                (status.clone(), total_fee_msat)
1194            })
1195            .collect::<Vec<_>>();
1196
1197        Ok(sorted_gateways[0].0.info.clone())
1198    }
1199
1200    /// Selects a Lightning Gateway from a given `gateway_id` from the gateway
1201    /// cache.
1202    pub async fn select_gateway(
1203        &self,
1204        gateway_id: &secp256k1::PublicKey,
1205    ) -> Option<LightningGateway> {
1206        let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
1207        let gateways = dbtx
1208            .find_by_prefix(&LightningGatewayKeyPrefix)
1209            .await
1210            .map(|(_, gw)| gw.info)
1211            .collect::<Vec<_>>()
1212            .await;
1213        gateways.into_iter().find(|g| &g.gateway_id == gateway_id)
1214    }
1215
1216    /// Updates the gateway cache by fetching the latest registered gateways
1217    /// from the federation.
1218    ///
1219    /// See also [`Self::update_gateway_cache_continuously`].
1220    pub async fn update_gateway_cache(&self) -> anyhow::Result<()> {
1221        self.update_gateway_cache_merge
1222            .merge(async {
1223                let gateways = self.module_api.fetch_gateways().await?;
1224                let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1225
1226                // Remove all previous gateway entries
1227                dbtx.remove_by_prefix(&LightningGatewayKeyPrefix).await;
1228
1229                for gw in &gateways {
1230                    dbtx.insert_entry(
1231                        &LightningGatewayKey(gw.info.gateway_id),
1232                        &gw.clone().anchor(),
1233                    )
1234                    .await;
1235                }
1236
1237                dbtx.commit_tx().await;
1238
1239                Ok(())
1240            })
1241            .await
1242    }
1243
1244    /// Continuously update the gateway cache whenever a gateway expires.
1245    ///
1246    /// The gateways returned by `gateway_filters` are checked for expiry.
1247    /// Client integrators are expected to call this function in a spawned task.
1248    pub async fn update_gateway_cache_continuously<Fut>(
1249        &self,
1250        gateways_filter: impl Fn(Vec<LightningGatewayAnnouncement>) -> Fut,
1251    ) -> !
1252    where
1253        Fut: Future<Output = Vec<LightningGatewayAnnouncement>>,
1254    {
1255        const ABOUT_TO_EXPIRE: Duration = Duration::from_secs(30);
1256        const EMPTY_GATEWAY_SLEEP: Duration = Duration::from_mins(10);
1257
1258        let mut first_time = true;
1259
1260        loop {
1261            let gateways = self.list_gateways().await;
1262            let sleep_time = gateways_filter(gateways)
1263                .await
1264                .into_iter()
1265                .map(|x| x.ttl.saturating_sub(ABOUT_TO_EXPIRE))
1266                .min()
1267                .unwrap_or(if first_time {
1268                    // retry immediately first time
1269                    Duration::ZERO
1270                } else {
1271                    EMPTY_GATEWAY_SLEEP
1272                });
1273            runtime::sleep(sleep_time).await;
1274
1275            // should never fail with usize::MAX attempts.
1276            let _ = retry(
1277                "update_gateway_cache",
1278                backoff_util::background_backoff(),
1279                || self.update_gateway_cache(),
1280            )
1281            .await;
1282            first_time = false;
1283        }
1284    }
1285
1286    /// Returns all gateways that are currently in the gateway cache.
1287    pub async fn list_gateways(&self) -> Vec<LightningGatewayAnnouncement> {
1288        let mut dbtx = self.client_ctx.module_db().begin_transaction_nc().await;
1289        dbtx.find_by_prefix(&LightningGatewayKeyPrefix)
1290            .await
1291            .map(|(_, gw)| gw.unanchor())
1292            .collect::<Vec<_>>()
1293            .await
1294    }
1295
1296    /// Pays a LN invoice with our available funds using the supplied `gateway`
1297    /// if one was provided and the invoice is not an internal one. If none is
1298    /// supplied only internal payments are possible.
1299    ///
1300    /// The `gateway` can be acquired by calling
1301    /// [`LightningClientModule::select_gateway`].
1302    ///
1303    /// Can return error of type [`PayBolt11InvoiceError`]
1304    pub async fn pay_bolt11_invoice<M: Serialize + MaybeSend + MaybeSync>(
1305        &self,
1306        maybe_gateway: Option<LightningGateway>,
1307        invoice: Bolt11Invoice,
1308        extra_meta: M,
1309    ) -> anyhow::Result<OutgoingLightningPayment> {
1310        if let Some(expires_at) = invoice.expires_at() {
1311            ensure!(
1312                expires_at.as_secs() > fedimint_core::time::duration_since_epoch().as_secs(),
1313                "Invoice has expired"
1314            );
1315        }
1316
1317        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1318        let maybe_gateway_id = maybe_gateway.as_ref().map(|g| g.gateway_id);
1319        let prev_payment_result = self
1320            .get_prev_payment_result(invoice.payment_hash(), &mut dbtx.to_ref_nc())
1321            .await;
1322
1323        if let Some(completed_payment) = prev_payment_result.completed_payment {
1324            return Ok(completed_payment);
1325        }
1326
1327        // Verify that no previous payment attempt is still running
1328        let prev_operation_id = LightningClientModule::get_payment_operation_id(
1329            invoice.payment_hash(),
1330            prev_payment_result.index,
1331        );
1332        if self.client_ctx.has_active_states(prev_operation_id).await {
1333            bail!(
1334                PayBolt11InvoiceError::PreviousPaymentAttemptStillInProgress {
1335                    operation_id: prev_operation_id
1336                }
1337            )
1338        }
1339
1340        let next_index = prev_payment_result.index + 1;
1341        let operation_id =
1342            LightningClientModule::get_payment_operation_id(invoice.payment_hash(), next_index);
1343
1344        let new_payment_result = PaymentResult {
1345            index: next_index,
1346            completed_payment: None,
1347        };
1348
1349        dbtx.insert_entry(
1350            &PaymentResultKey {
1351                payment_hash: *invoice.payment_hash(),
1352            },
1353            &new_payment_result,
1354        )
1355        .await;
1356
1357        let markers = self.client_ctx.get_internal_payment_markers()?;
1358
1359        let mut is_internal_payment = invoice_has_internal_payment_markers(&invoice, markers);
1360        if !is_internal_payment {
1361            let gateways = dbtx
1362                .find_by_prefix(&LightningGatewayKeyPrefix)
1363                .await
1364                .map(|(_, gw)| gw.info)
1365                .collect::<Vec<_>>()
1366                .await;
1367            is_internal_payment = invoice_routes_back_to_federation(&invoice, gateways);
1368        }
1369
1370        let (pay_type, client_output, client_output_sm, contract_id) = if is_internal_payment {
1371            let (output, output_sm, contract_id) = self
1372                .create_incoming_output(operation_id, invoice.clone())
1373                .await?;
1374            (
1375                PayType::Internal(operation_id),
1376                output,
1377                output_sm,
1378                contract_id,
1379            )
1380        } else {
1381            let gateway = maybe_gateway.context(PayBolt11InvoiceError::NoLnGatewayAvailable)?;
1382            let (output, output_sm, contract_id) = self
1383                .create_outgoing_output(
1384                    operation_id,
1385                    invoice.clone(),
1386                    gateway,
1387                    self.client_ctx
1388                        .get_config()
1389                        .await
1390                        .global
1391                        .calculate_federation_id(),
1392                    rand::rngs::OsRng,
1393                )
1394                .await?;
1395            (
1396                PayType::Lightning(operation_id),
1397                output,
1398                output_sm,
1399                contract_id,
1400            )
1401        };
1402
1403        // Verify that no other outgoing contract exists or the value is empty
1404        if let Ok(Some(contract)) = self.module_api.fetch_contract(contract_id).await
1405            && contract.amount.msats != 0
1406        {
1407            bail!(PayBolt11InvoiceError::FundedContractAlreadyExists { contract_id });
1408        }
1409
1410        let amount_msat = invoice
1411            .amount_milli_satoshis()
1412            .ok_or(anyhow!("MissingInvoiceAmount"))?;
1413
1414        // TODO: return fee from create_outgoing_output or even let user supply
1415        // it/bounds for it
1416        let fee = match &client_output.output {
1417            LightningOutputV0::Contract(contract) => {
1418                let fee_msat = contract
1419                    .amount
1420                    .msats
1421                    .checked_sub(amount_msat)
1422                    .expect("Contract amount should be greater or equal than invoice amount");
1423                Amount::from_msats(fee_msat)
1424            }
1425            _ => unreachable!("User client will only create contract outputs on spend"),
1426        };
1427
1428        let output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
1429            vec![ClientOutput {
1430                output: LightningOutput::V0(client_output.output),
1431                amounts: client_output.amounts,
1432            }],
1433            vec![client_output_sm],
1434        ));
1435
1436        let tx = TransactionBuilder::new().with_outputs(output);
1437        let extra_meta =
1438            serde_json::to_value(extra_meta).context("Failed to serialize extra meta")?;
1439        let operation_meta_gen = move |change_range: OutPointRange| LightningOperationMeta {
1440            variant: LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
1441                out_point: OutPoint {
1442                    txid: change_range.txid(),
1443                    out_idx: 0,
1444                },
1445                invoice: invoice.clone(),
1446                fee,
1447                change: change_range.into_iter().collect(),
1448                is_internal_payment,
1449                contract_id,
1450                gateway_id: maybe_gateway_id,
1451            }),
1452            extra_meta: extra_meta.clone(),
1453        };
1454
1455        // Write the new payment index into the database, fail the payment if the commit
1456        // to the database fails.
1457        dbtx.commit_tx_result().await?;
1458
1459        self.client_ctx
1460            .finalize_and_submit_transaction(
1461                operation_id,
1462                LightningCommonInit::KIND.as_str(),
1463                operation_meta_gen,
1464                tx,
1465            )
1466            .await?;
1467
1468        let mut event_dbtx = self.client_ctx.module_db().begin_transaction().await;
1469
1470        self.client_ctx
1471            .log_event(
1472                &mut event_dbtx,
1473                events::SendPaymentEvent {
1474                    operation_id,
1475                    amount: Amount::from_msats(amount_msat),
1476                    fee,
1477                },
1478            )
1479            .await;
1480
1481        event_dbtx.commit_tx().await;
1482
1483        Ok(OutgoingLightningPayment {
1484            payment_type: pay_type,
1485            contract_id,
1486            fee,
1487        })
1488    }
1489
1490    pub async fn get_ln_pay_details_for(
1491        &self,
1492        operation_id: OperationId,
1493    ) -> anyhow::Result<LightningOperationMetaPay> {
1494        let operation = self.client_ctx.get_operation(operation_id).await?;
1495        let LightningOperationMetaVariant::Pay(pay) =
1496            operation.meta::<LightningOperationMeta>().variant
1497        else {
1498            anyhow::bail!("Operation is not a lightning payment")
1499        };
1500        Ok(pay)
1501    }
1502
1503    pub async fn subscribe_internal_pay(
1504        &self,
1505        operation_id: OperationId,
1506    ) -> anyhow::Result<UpdateStreamOrOutcome<InternalPayState>> {
1507        let operation = self.client_ctx.get_operation(operation_id).await?;
1508
1509        let LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
1510            out_point: _,
1511            invoice: _,
1512            change: _, // FIXME: why isn't this used here?
1513            is_internal_payment,
1514            ..
1515        }) = operation.meta::<LightningOperationMeta>().variant
1516        else {
1517            bail!("Operation is not a lightning payment")
1518        };
1519
1520        ensure!(
1521            is_internal_payment,
1522            "Subscribing to an external LN payment, expected internal LN payment"
1523        );
1524
1525        let mut stream = self.notifier.subscribe(operation_id).await;
1526        let client_ctx = self.client_ctx.clone();
1527
1528        Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1529            stream! {
1530                yield InternalPayState::Funding;
1531
1532                let state = loop {
1533                    match stream.next().await { Some(LightningClientStateMachines::InternalPay(state)) => {
1534                        match state.state {
1535                            IncomingSmStates::Preimage(preimage) => break InternalPayState::Preimage(preimage),
1536                            IncomingSmStates::RefundSubmitted{ out_points, error } => {
1537                                match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
1538                                    Ok(()) => break InternalPayState::RefundSuccess { out_points, error },
1539                                    Err(e) => break InternalPayState::RefundError{ error_message: e.to_string(), error },
1540                                }
1541                            },
1542                            IncomingSmStates::FundingFailed { error } => break InternalPayState::FundingFailed{ error },
1543                            _ => {}
1544                        }
1545                    } _ => {
1546                        break InternalPayState::UnexpectedError("Unexpected State! Expected an InternalPay state".to_string())
1547                    }}
1548                };
1549                yield state;
1550            }
1551        }))
1552    }
1553
1554    /// Subscribes to a stream of updates about a particular external Lightning
1555    /// payment operation specified by the `operation_id`.
1556    pub async fn subscribe_ln_pay(
1557        &self,
1558        operation_id: OperationId,
1559    ) -> anyhow::Result<UpdateStreamOrOutcome<LnPayState>> {
1560        async fn get_next_pay_state(
1561            stream: &mut BoxStream<'_, LightningClientStateMachines>,
1562        ) -> Option<LightningPayStates> {
1563            match stream.next().await {
1564                Some(LightningClientStateMachines::LightningPay(state)) => Some(state.state),
1565                Some(event) => {
1566                    // nosemgrep: use-err-formatting
1567                    error!(event = ?event, "Operation is not a lightning payment");
1568                    debug_assert!(false, "Operation is not a lightning payment: {event:?}");
1569                    None
1570                }
1571                None => None,
1572            }
1573        }
1574
1575        let operation = self.client_ctx.get_operation(operation_id).await?;
1576        let LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
1577            out_point: _,
1578            invoice: _,
1579            change,
1580            is_internal_payment,
1581            ..
1582        }) = operation.meta::<LightningOperationMeta>().variant
1583        else {
1584            bail!("Operation is not a lightning payment")
1585        };
1586
1587        ensure!(
1588            !is_internal_payment,
1589            "Subscribing to an internal LN payment, expected external LN payment"
1590        );
1591
1592        let client_ctx = self.client_ctx.clone();
1593
1594        Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1595            stream! {
1596                let self_ref = client_ctx.self_ref();
1597
1598                let mut stream = self_ref.notifier.subscribe(operation_id).await;
1599                let state = get_next_pay_state(&mut stream).await;
1600                match state {
1601                    Some(LightningPayStates::CreatedOutgoingLnContract(_)) => {
1602                        yield LnPayState::Created;
1603                    }
1604                    Some(LightningPayStates::FundingRejected) => {
1605                        yield LnPayState::Canceled;
1606                        return;
1607                    }
1608                    Some(state) => {
1609                        yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1610                        return;
1611                    }
1612                    None => {
1613                        error!("Unexpected end of lightning pay state machine");
1614                        return;
1615                    }
1616                }
1617
1618                let state = get_next_pay_state(&mut stream).await;
1619                match state {
1620                    Some(LightningPayStates::Funded(funded)) => {
1621                        yield LnPayState::Funded { block_height: funded.timelock }
1622                    }
1623                    Some(state) => {
1624                        yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1625                        return;
1626                    }
1627                    _ => {
1628                        error!("Unexpected end of lightning pay state machine");
1629                        return;
1630                    }
1631                }
1632
1633                let state = get_next_pay_state(&mut stream).await;
1634                match state {
1635                    Some(LightningPayStates::Success(preimage)) => {
1636                        if change.is_empty() {
1637                            yield LnPayState::Success { preimage };
1638                        } else {
1639                            yield LnPayState::AwaitingChange;
1640                            match client_ctx.await_primary_module_outputs(operation_id, change.clone()).await {
1641                                Ok(()) => {
1642                                    yield LnPayState::Success { preimage };
1643                                }
1644                                Err(e) => {
1645                                    yield LnPayState::UnexpectedError { error_message: format!("Error occurred while waiting for the change: {e:?}") };
1646                                }
1647                            }
1648                        }
1649                    }
1650                    Some(LightningPayStates::Refund(refund)) => {
1651                        yield LnPayState::WaitingForRefund {
1652                            error_reason: refund.error_reason.clone(),
1653                        };
1654
1655                        match client_ctx.await_primary_module_outputs(operation_id, refund.out_points).await {
1656                            Ok(()) => {
1657                                let gateway_error = GatewayPayError::GatewayInternalError { error_code: Some(500), error_message: refund.error_reason };
1658                                yield LnPayState::Refunded { gateway_error };
1659                            }
1660                            Err(e) => {
1661                                yield LnPayState::UnexpectedError {
1662                                    error_message: format!("Error occurred trying to get refund. Refund was not successful: {e:?}"),
1663                                };
1664                            }
1665                        }
1666                    }
1667                    Some(state) => {
1668                        yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1669                    }
1670                    None => {
1671                        error!("Unexpected end of lightning pay state machine");
1672                        yield LnPayState::UnexpectedError { error_message: "Unexpected end of lightning pay state machine".to_string() };
1673                    }
1674                }
1675            }
1676        }))
1677    }
1678
1679    /// Scan unspent incoming contracts for a payment hash that matches a
1680    /// tweaked keys in the `indices` vector
1681    #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1682    #[allow(deprecated)]
1683    pub async fn scan_receive_for_user_tweaked<M: Serialize + Send + Sync + Clone>(
1684        &self,
1685        key_pair: Keypair,
1686        indices: Vec<u64>,
1687        extra_meta: M,
1688    ) -> Vec<OperationId> {
1689        let mut claims = Vec::new();
1690        for i in indices {
1691            let key_pair_tweaked = tweak_user_secret_key(&self.secp, key_pair, i);
1692            match self
1693                .scan_receive_for_user(key_pair_tweaked, extra_meta.clone())
1694                .await
1695            {
1696                Ok(operation_id) => claims.push(operation_id),
1697                Err(err) => {
1698                    error!(err = %err.fmt_compact_anyhow(), %i, "Failed to scan tweaked key at index i");
1699                }
1700            }
1701        }
1702
1703        claims
1704    }
1705
1706    /// Scan unspent incoming contracts for a payment hash that matches a public
1707    /// key and claim the incoming contract
1708    #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1709    #[allow(deprecated)]
1710    pub async fn scan_receive_for_user<M: Serialize + Send + Sync>(
1711        &self,
1712        key_pair: Keypair,
1713        extra_meta: M,
1714    ) -> anyhow::Result<OperationId> {
1715        let preimage_key: [u8; 33] = key_pair.public_key().serialize();
1716        let preimage = sha256::Hash::hash(&preimage_key);
1717        let contract_id = ContractId::from_raw_hash(sha256::Hash::hash(&preimage.to_byte_array()));
1718        self.claim_funded_incoming_contract(key_pair, contract_id, extra_meta)
1719            .await
1720    }
1721
1722    /// Claim the funded, unspent incoming contract by submitting a transaction
1723    /// to the federation and awaiting the primary module's outputs
1724    #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1725    #[allow(deprecated)]
1726    pub async fn claim_funded_incoming_contract<M: Serialize + Send + Sync>(
1727        &self,
1728        key_pair: Keypair,
1729        contract_id: ContractId,
1730        extra_meta: M,
1731    ) -> anyhow::Result<OperationId> {
1732        let incoming_contract_account = get_incoming_contract(self.module_api.clone(), contract_id)
1733            .await?
1734            .ok_or(anyhow!("No contract account found"))
1735            .with_context(|| format!("No contract found for {contract_id:?}"))?;
1736
1737        let input = incoming_contract_account.claim();
1738        let client_input = ClientInput::<LightningInput> {
1739            input,
1740            amounts: Amounts::new_bitcoin(incoming_contract_account.amount),
1741            keys: vec![key_pair],
1742        };
1743
1744        let tx = TransactionBuilder::new().with_inputs(
1745            self.client_ctx
1746                .make_client_inputs(ClientInputBundle::new_no_sm(vec![client_input])),
1747        );
1748        let extra_meta = serde_json::to_value(extra_meta).expect("extra_meta is serializable");
1749        let operation_meta_gen = move |change_range: OutPointRange| LightningOperationMeta {
1750            variant: LightningOperationMetaVariant::Claim {
1751                out_points: change_range.into_iter().collect(),
1752            },
1753            extra_meta: extra_meta.clone(),
1754        };
1755        let operation_id = OperationId::new_random();
1756        self.client_ctx
1757            .finalize_and_submit_transaction(
1758                operation_id,
1759                LightningCommonInit::KIND.as_str(),
1760                operation_meta_gen,
1761                tx,
1762            )
1763            .await?;
1764        Ok(operation_id)
1765    }
1766
1767    /// Receive over LN with a new invoice
1768    /// Computes the federation fee receiving `amount` over Lightning would
1769    /// incur, without submitting anything.
1770    ///
1771    /// When the incoming contract is claimed, the client submits a transaction
1772    /// with a single Lightning input worth the contract amount; the primary
1773    /// module balances it by minting the change credited to the wallet. This
1774    /// quotes the fee of that transaction — the Lightning input fee, the mint
1775    /// output fees, and any sub-denomination dust — via the shared,
1776    /// module-agnostic fee quote.
1777    ///
1778    /// The gateway's off-chain Lightning fee is deliberately excluded: this is
1779    /// only the fee of the on-federation transaction. For that reason the quote
1780    /// is taken on `amount` directly (rather than the gateway-reduced contract
1781    /// amount), and no gateway round-trip is needed.
1782    pub async fn receive_fee_quote(&self, amount: Amount) -> anyhow::Result<FeeQuote> {
1783        self.client_ctx
1784            .fee_quote(
1785                OperationId::new_random(),
1786                FeeQuoteRequest {
1787                    input_amount: Amounts::new_bitcoin(amount),
1788                    output_amount: Amounts::ZERO,
1789                    input_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input),
1790                    output_fee: Amounts::ZERO,
1791                },
1792            )
1793            .await
1794    }
1795
1796    /// Computes the federation fee a `pay` funding an outgoing contract worth
1797    /// `amount` would incur, without submitting anything.
1798    ///
1799    /// When a payment is sent, the client submits a transaction with a single
1800    /// Lightning output (the outgoing contract) worth `amount`; the primary
1801    /// module balances it by spending ecash to fund the contract and minting
1802    /// any change. This quotes the fee of that transaction — the Lightning
1803    /// output fee, the mint input fees on the funding notes, any mint change
1804    /// output fees, and sub-denomination dust — via the shared, module-agnostic
1805    /// fee quote.
1806    ///
1807    /// The gateway's off-chain Lightning fee is deliberately excluded: it is
1808    /// part of the contract `amount` the gateway claims, not the on-federation
1809    /// transaction fee. So `amount` is the full outgoing contract value.
1810    pub async fn send_fee_quote(&self, amount: Amount) -> anyhow::Result<FeeQuote> {
1811        self.client_ctx
1812            .fee_quote(
1813                OperationId::new_random(),
1814                FeeQuoteRequest {
1815                    input_amount: Amounts::ZERO,
1816                    output_amount: Amounts::new_bitcoin(amount),
1817                    input_fee: Amounts::ZERO,
1818                    output_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.contract_output),
1819                },
1820            )
1821            .await
1822    }
1823
1824    pub async fn create_bolt11_invoice<M: Serialize + Send + Sync>(
1825        &self,
1826        amount: Amount,
1827        description: lightning_invoice::Bolt11InvoiceDescription,
1828        expiry_time: Option<u64>,
1829        extra_meta: M,
1830        gateway: Option<LightningGateway>,
1831    ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1832        let receiving_key =
1833            ReceivingKey::Personal(Keypair::new(&self.secp, &mut rand::rngs::OsRng));
1834        self.create_bolt11_invoice_internal(
1835            amount,
1836            description,
1837            expiry_time,
1838            receiving_key,
1839            extra_meta,
1840            gateway,
1841        )
1842        .await
1843    }
1844
1845    /// Receive over LN with a new invoice for another user, tweaking their key
1846    /// by the given index
1847    #[allow(clippy::too_many_arguments)]
1848    pub async fn create_bolt11_invoice_for_user_tweaked<M: Serialize + Send + Sync>(
1849        &self,
1850        amount: Amount,
1851        description: lightning_invoice::Bolt11InvoiceDescription,
1852        expiry_time: Option<u64>,
1853        user_key: PublicKey,
1854        index: u64,
1855        extra_meta: M,
1856        gateway: Option<LightningGateway>,
1857    ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1858        let tweaked_key = tweak_user_key(&self.secp, user_key, index);
1859        self.create_bolt11_invoice_for_user(
1860            amount,
1861            description,
1862            expiry_time,
1863            tweaked_key,
1864            extra_meta,
1865            gateway,
1866        )
1867        .await
1868    }
1869
1870    /// Receive over LN with a new invoice for another user
1871    pub async fn create_bolt11_invoice_for_user<M: Serialize + Send + Sync>(
1872        &self,
1873        amount: Amount,
1874        description: lightning_invoice::Bolt11InvoiceDescription,
1875        expiry_time: Option<u64>,
1876        user_key: PublicKey,
1877        extra_meta: M,
1878        gateway: Option<LightningGateway>,
1879    ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1880        let receiving_key = ReceivingKey::External(user_key);
1881        self.create_bolt11_invoice_internal(
1882            amount,
1883            description,
1884            expiry_time,
1885            receiving_key,
1886            extra_meta,
1887            gateway,
1888        )
1889        .await
1890    }
1891
1892    /// Receive over LN with a new invoice
1893    async fn create_bolt11_invoice_internal<M: Serialize + Send + Sync>(
1894        &self,
1895        amount: Amount,
1896        description: lightning_invoice::Bolt11InvoiceDescription,
1897        expiry_time: Option<u64>,
1898        receiving_key: ReceivingKey,
1899        extra_meta: M,
1900        gateway: Option<LightningGateway>,
1901    ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1902        let gateway_id = gateway.as_ref().map(|g| g.gateway_id);
1903        let (src_node_id, short_channel_id, route_hints) = if let Some(current_gateway) = gateway {
1904            (
1905                current_gateway.node_pub_key,
1906                current_gateway.federation_index,
1907                current_gateway.route_hints,
1908            )
1909        } else {
1910            // If no gateway is provided, this is assumed to be an internal payment.
1911            let markers = self.client_ctx.get_internal_payment_markers()?;
1912            (markers.0, markers.1, vec![])
1913        };
1914
1915        debug!(target: LOG_CLIENT_MODULE_LN, ?gateway_id, %amount, "Selected LN gateway for invoice generation");
1916
1917        let (operation_id, invoice, output, preimage) = self.create_lightning_receive_output(
1918            amount,
1919            description,
1920            receiving_key,
1921            rand::rngs::OsRng,
1922            expiry_time,
1923            src_node_id,
1924            short_channel_id,
1925            &route_hints,
1926            self.cfg.network.0,
1927        )?;
1928
1929        let tx =
1930            TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(output));
1931        let extra_meta = serde_json::to_value(extra_meta).expect("extra_meta is serializable");
1932        let operation_meta_gen = {
1933            let invoice = invoice.clone();
1934            move |change_range: OutPointRange| LightningOperationMeta {
1935                variant: LightningOperationMetaVariant::Receive {
1936                    out_point: OutPoint {
1937                        txid: change_range.txid(),
1938                        out_idx: 0,
1939                    },
1940                    invoice: invoice.clone(),
1941                    gateway_id,
1942                },
1943                extra_meta: extra_meta.clone(),
1944            }
1945        };
1946        let change_range = self
1947            .client_ctx
1948            .finalize_and_submit_transaction(
1949                operation_id,
1950                LightningCommonInit::KIND.as_str(),
1951                operation_meta_gen,
1952                tx,
1953            )
1954            .await?;
1955
1956        debug!(target: LOG_CLIENT_MODULE_LN, txid = ?change_range.txid(), ?operation_id, "Waiting for LN invoice to be confirmed");
1957
1958        // Wait for the transaction to be accepted by the federation, otherwise the
1959        // invoice will not be able to be paid
1960        self.client_ctx
1961            .transaction_updates(operation_id)
1962            .await
1963            .await_tx_accepted(change_range.txid())
1964            .await
1965            .map_err(|e| anyhow!("Offer transaction was not accepted: {e:?}"))?;
1966
1967        debug!(target: LOG_CLIENT_MODULE_LN, %invoice, "Invoice confirmed");
1968
1969        Ok((operation_id, invoice, preimage))
1970    }
1971
1972    /// Starts a new state machine that retries claiming a previously paid
1973    /// invoice.
1974    ///
1975    /// This is a local state-history recovery tool: it requires the client DB
1976    /// to still contain a historical `SubmittedOffer` or `ConfirmedInvoice`
1977    /// state for the original operation. It does not recover seed-only
1978    /// restores where that local state-machine history is unavailable.
1979    ///
1980    /// Repeated calls start independent reclaim attempts. This is intentional:
1981    /// this is a manual break-glass recovery path, and concurrent attempts race
1982    /// through the normal federation transaction validation.
1983    ///
1984    /// # Errors
1985    ///
1986    /// Returns an error if the original operation is not a reclaimable
1987    /// lightning receive, if it is still active, or if the original receiving
1988    /// key cannot be recovered from state history.
1989    pub async fn reclaim_ln_receive(
1990        &self,
1991        original_operation_id: OperationId,
1992    ) -> anyhow::Result<OperationId> {
1993        let operation = self.client_ctx.get_operation(original_operation_id).await?;
1994        let LightningOperationMeta {
1995            variant,
1996            extra_meta,
1997        } = operation
1998            .try_meta::<LightningOperationMeta>()
1999            .context("Invalid lightning operation metadata")?;
2000
2001        let (invoice, gateway_id) = match variant {
2002            LightningOperationMetaVariant::Receive {
2003                invoice,
2004                gateway_id,
2005                ..
2006            } => (invoice, gateway_id),
2007            LightningOperationMetaVariant::RecurringPaymentReceive(meta) => (meta.invoice, None),
2008            _ => bail!("Operation is not a reclaimable lightning receive"),
2009        };
2010
2011        let active_states = self
2012            .client_ctx
2013            .get_own_operation_active_states(original_operation_id)
2014            .await;
2015        ensure!(
2016            !active_states
2017                .iter()
2018                .any(|(state, _)| matches!(state, LightningClientStateMachines::Receive(_))),
2019            "Cannot reclaim an active lightning receive"
2020        );
2021
2022        let inactive_states = self
2023            .client_ctx
2024            .get_own_operation_inactive_states(original_operation_id)
2025            .await;
2026
2027        let receiving_key = inactive_states
2028            .iter()
2029            .find_map(|(state, _)| Self::ln_receive_key_from_state(state))
2030            .ok_or_else(|| {
2031                anyhow!("Cannot reclaim LN receive because the original receive key is unavailable")
2032            })?;
2033        let db = self.client_ctx.module_db();
2034        let mut dbtx = db.begin_transaction().await;
2035        let reclaim_operation_id = OperationId::new_random();
2036        let operation_meta = LightningOperationMeta {
2037            variant: LightningOperationMetaVariant::ReceiveReclaim {
2038                original_operation_id,
2039                invoice: invoice.clone(),
2040                gateway_id,
2041            },
2042            extra_meta,
2043        };
2044        let state = LightningClientStateMachines::Receive(LightningReceiveStateMachine {
2045            operation_id: reclaim_operation_id,
2046            state: LightningReceiveStates::ConfirmedInvoice(LightningReceiveConfirmedInvoice {
2047                invoice,
2048                receiving_key,
2049            }),
2050        });
2051
2052        self.client_ctx
2053            .manual_operation_start_dbtx(
2054                &mut dbtx.to_ref_nc(),
2055                reclaim_operation_id,
2056                LightningCommonInit::KIND.as_str(),
2057                operation_meta,
2058                vec![self.client_ctx.make_dyn_state(state)],
2059            )
2060            .await?;
2061
2062        dbtx.commit_tx().await;
2063
2064        Ok(reclaim_operation_id)
2065    }
2066
2067    fn ln_receive_key_from_state(state: &LightningClientStateMachines) -> Option<ReceivingKey> {
2068        match state {
2069            LightningClientStateMachines::Receive(receive) => match &receive.state {
2070                LightningReceiveStates::SubmittedOffer(submitted_offer) => {
2071                    Some(submitted_offer.receiving_key)
2072                }
2073                LightningReceiveStates::ConfirmedInvoice(confirmed_invoice) => {
2074                    Some(confirmed_invoice.receiving_key)
2075                }
2076                LightningReceiveStates::Canceled(_)
2077                | LightningReceiveStates::Funded(_)
2078                | LightningReceiveStates::Success(_) => None,
2079            },
2080            LightningClientStateMachines::InternalPay(_)
2081            | LightningClientStateMachines::LightningPay(_) => None,
2082        }
2083    }
2084
2085    #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
2086    #[allow(deprecated)]
2087    pub async fn subscribe_ln_claim(
2088        &self,
2089        operation_id: OperationId,
2090    ) -> anyhow::Result<UpdateStreamOrOutcome<LnReceiveState>> {
2091        let operation = self.client_ctx.get_operation(operation_id).await?;
2092        let LightningOperationMetaVariant::Claim { out_points } =
2093            operation.meta::<LightningOperationMeta>().variant
2094        else {
2095            bail!("Operation is not a lightning claim")
2096        };
2097
2098        let client_ctx = self.client_ctx.clone();
2099
2100        Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
2101            stream! {
2102                yield LnReceiveState::AwaitingFunds;
2103
2104                if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
2105                    yield LnReceiveState::Claimed;
2106                } else {
2107                    yield LnReceiveState::Canceled { reason: LightningReceiveError::ClaimRejected }
2108                }
2109            }
2110        }))
2111    }
2112
2113    pub async fn subscribe_ln_receive(
2114        &self,
2115        operation_id: OperationId,
2116    ) -> anyhow::Result<UpdateStreamOrOutcome<LnReceiveState>> {
2117        let operation = self.client_ctx.get_operation(operation_id).await?;
2118        let (invoice, tx_accepted_future) = match operation.meta::<LightningOperationMeta>().variant
2119        {
2120            LightningOperationMetaVariant::Receive {
2121                out_point, invoice, ..
2122            } => {
2123                let tx_accepted_future = self
2124                    .client_ctx
2125                    .transaction_updates(operation_id)
2126                    .await
2127                    .await_tx_accepted(out_point.txid);
2128                (invoice, Some(tx_accepted_future))
2129            }
2130            LightningOperationMetaVariant::ReceiveReclaim { invoice, .. } => (invoice, None),
2131            _ => bail!("Operation is not a lightning receive"),
2132        };
2133
2134        let client_ctx = self.client_ctx.clone();
2135
2136        Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
2137            stream! {
2138
2139                let self_ref = client_ctx.self_ref();
2140
2141                yield LnReceiveState::Created;
2142
2143                let tx_rejected = match tx_accepted_future {
2144                    Some(tx_accepted_future) => tx_accepted_future.await.is_err(),
2145                    None => false,
2146                };
2147                if tx_rejected {
2148                    yield LnReceiveState::Canceled { reason: LightningReceiveError::Rejected };
2149                    return;
2150                }
2151                yield LnReceiveState::WaitingForPayment { invoice: invoice.to_string(), timeout: invoice.expiry_time() };
2152
2153                match self_ref.await_receive_success(operation_id).await {
2154                    Ok(()) => {
2155
2156                        yield LnReceiveState::Funded;
2157
2158                        match self_ref.await_claim_acceptance(operation_id).await {
2159                            Ok(out_points) => {
2160                                yield LnReceiveState::AwaitingFunds;
2161
2162                                if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
2163                                    yield LnReceiveState::Claimed;
2164                                    return;
2165                                }
2166
2167                                // The claim transaction was accepted, but its outputs were not
2168                                // confirmed by the primary module. The incoming contract is already
2169                                // spent, so this is not reclaimable as a rejected claim.
2170                                yield LnReceiveState::Canceled { reason: LightningReceiveError::Rejected };
2171                            }
2172                            Err(e) => {
2173                                yield LnReceiveState::Canceled { reason: e };
2174                            }
2175                        }
2176                    }
2177                    Err(e) => {
2178                        yield LnReceiveState::Canceled { reason: e };
2179                    }
2180                }
2181            }
2182        }))
2183    }
2184
2185    /// Returns a gateway to be used for a lightning operation. If
2186    /// `force_internal` is true and no `gateway_id` is specified, no
2187    /// gateway will be selected.
2188    pub async fn get_gateway(
2189        &self,
2190        gateway_id: Option<secp256k1::PublicKey>,
2191        force_internal: bool,
2192    ) -> anyhow::Result<Option<LightningGateway>> {
2193        match gateway_id {
2194            Some(gateway_id) => {
2195                if let Some(gw) = self.select_gateway(&gateway_id).await {
2196                    Ok(Some(gw))
2197                } else {
2198                    // Refresh the gateway cache in case the target gateway was registered since the
2199                    // last update.
2200                    self.update_gateway_cache().await?;
2201                    Ok(self.select_gateway(&gateway_id).await)
2202                }
2203            }
2204            None if !force_internal => {
2205                // Refresh the gateway cache to find a random gateway to select from.
2206                self.update_gateway_cache().await?;
2207                let gateways = self.list_gateways().await;
2208                let gw = gateways.into_iter().choose(&mut OsRng).map(|gw| gw.info);
2209                if let Some(gw) = gw {
2210                    let gw_id = gw.gateway_id;
2211                    info!(%gw_id, "Using random gateway");
2212                    Ok(Some(gw))
2213                } else {
2214                    Err(anyhow!(
2215                        "No gateways exist in gateway cache and `force_internal` is false"
2216                    ))
2217                }
2218            }
2219            None => Ok(None),
2220        }
2221    }
2222
2223    /// Subscribes to either a internal or external lightning payment and
2224    /// returns `LightningPaymentOutcome` that indicates if the payment was
2225    /// successful or not.
2226    pub async fn await_outgoing_payment(
2227        &self,
2228        operation_id: OperationId,
2229    ) -> anyhow::Result<LightningPaymentOutcome> {
2230        let operation = self.client_ctx.get_operation(operation_id).await?;
2231        let variant = operation.meta::<LightningOperationMeta>().variant;
2232        let LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
2233            is_internal_payment,
2234            ..
2235        }) = variant
2236        else {
2237            bail!("Operation is not a lightning payment")
2238        };
2239
2240        let mut final_state = None;
2241
2242        // First check if the outgoing payment is an internal payment
2243        if is_internal_payment {
2244            let updates = self.subscribe_internal_pay(operation_id).await?;
2245            let mut stream = updates.into_stream();
2246            while let Some(update) = stream.next().await {
2247                match update {
2248                    InternalPayState::Preimage(preimage) => {
2249                        final_state = Some(LightningPaymentOutcome::Success {
2250                            preimage: preimage.0.consensus_encode_to_hex(),
2251                        });
2252                    }
2253                    InternalPayState::RefundSuccess {
2254                        out_points: _,
2255                        error,
2256                    } => {
2257                        final_state = Some(LightningPaymentOutcome::Failure {
2258                            error_message: format!("LNv1 internal payment was refunded: {error:?}"),
2259                        });
2260                    }
2261                    InternalPayState::FundingFailed { error } => {
2262                        final_state = Some(LightningPaymentOutcome::Failure {
2263                            error_message: format!(
2264                                "LNv1 internal payment funding failed: {error:?}"
2265                            ),
2266                        });
2267                    }
2268                    InternalPayState::RefundError {
2269                        error_message,
2270                        error,
2271                    } => {
2272                        final_state = Some(LightningPaymentOutcome::Failure {
2273                            error_message: format!(
2274                                "LNv1 refund failed: {error_message}: {error:?}"
2275                            ),
2276                        });
2277                    }
2278                    InternalPayState::UnexpectedError(error) => {
2279                        final_state = Some(LightningPaymentOutcome::Failure {
2280                            error_message: error,
2281                        });
2282                    }
2283                    InternalPayState::Funding => {}
2284                }
2285            }
2286        } else {
2287            let updates = self.subscribe_ln_pay(operation_id).await?;
2288            let mut stream = updates.into_stream();
2289            while let Some(update) = stream.next().await {
2290                match update {
2291                    LnPayState::Success { preimage } => {
2292                        final_state = Some(LightningPaymentOutcome::Success { preimage });
2293                    }
2294                    LnPayState::Refunded { gateway_error } => {
2295                        final_state = Some(LightningPaymentOutcome::Failure {
2296                            error_message: format!(
2297                                "LNv1 external payment was refunded: {gateway_error:?}"
2298                            ),
2299                        });
2300                    }
2301                    LnPayState::UnexpectedError { error_message } => {
2302                        final_state = Some(LightningPaymentOutcome::Failure { error_message });
2303                    }
2304                    _ => {}
2305                }
2306            }
2307        }
2308
2309        final_state.ok_or(anyhow!(
2310            "Internal or external outgoing lightning payment did not reach a final state"
2311        ))
2312    }
2313}
2314
2315// TODO: move to appropriate module (cli?)
2316// some refactoring here needed
2317#[derive(Debug, Clone, Serialize, Deserialize)]
2318#[serde(rename_all = "snake_case")]
2319pub struct PayInvoiceResponse {
2320    operation_id: OperationId,
2321    contract_id: ContractId,
2322    preimage: String,
2323}
2324
2325#[allow(clippy::large_enum_variant)]
2326#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
2327pub enum LightningClientStateMachines {
2328    InternalPay(IncomingStateMachine),
2329    LightningPay(LightningPayStateMachine),
2330    Receive(LightningReceiveStateMachine),
2331}
2332
2333impl IntoDynInstance for LightningClientStateMachines {
2334    type DynType = DynState;
2335
2336    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
2337        DynState::from_typed(instance_id, self)
2338    }
2339}
2340
2341impl State for LightningClientStateMachines {
2342    type ModuleContext = LightningClientContext;
2343
2344    fn transitions(
2345        &self,
2346        context: &Self::ModuleContext,
2347        global_context: &DynGlobalClientContext,
2348    ) -> Vec<StateTransition<Self>> {
2349        match self {
2350            LightningClientStateMachines::InternalPay(internal_pay_state) => {
2351                sm_enum_variant_translation!(
2352                    internal_pay_state.transitions(context, global_context),
2353                    LightningClientStateMachines::InternalPay
2354                )
2355            }
2356            LightningClientStateMachines::LightningPay(lightning_pay_state) => {
2357                sm_enum_variant_translation!(
2358                    lightning_pay_state.transitions(context, global_context),
2359                    LightningClientStateMachines::LightningPay
2360                )
2361            }
2362            LightningClientStateMachines::Receive(receive_state) => {
2363                sm_enum_variant_translation!(
2364                    receive_state.transitions(context, global_context),
2365                    LightningClientStateMachines::Receive
2366                )
2367            }
2368        }
2369    }
2370
2371    fn operation_id(&self) -> OperationId {
2372        match self {
2373            LightningClientStateMachines::InternalPay(internal_pay_state) => {
2374                internal_pay_state.operation_id()
2375            }
2376            LightningClientStateMachines::LightningPay(lightning_pay_state) => {
2377                lightning_pay_state.operation_id()
2378            }
2379            LightningClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
2380        }
2381    }
2382}
2383
2384async fn fetch_and_validate_offer(
2385    module_api: &DynModuleApi,
2386    payment_hash: sha256::Hash,
2387    amount_msat: Amount,
2388) -> anyhow::Result<IncomingContractOffer, IncomingSmError> {
2389    let offer = timeout(Duration::from_secs(5), module_api.fetch_offer(payment_hash))
2390        .await
2391        .map_err(|_| IncomingSmError::TimeoutFetchingOffer { payment_hash })?
2392        .map_err(|e| IncomingSmError::FetchContractError {
2393            payment_hash,
2394            error_message: e.to_string(),
2395        })?;
2396
2397    if offer.amount > amount_msat {
2398        return Err(IncomingSmError::ViolatedFeePolicy {
2399            offer_amount: offer.amount,
2400            payment_amount: amount_msat,
2401        });
2402    }
2403    if offer.hash != payment_hash {
2404        return Err(IncomingSmError::InvalidOffer {
2405            offer_hash: offer.hash,
2406            payment_hash,
2407        });
2408    }
2409    Ok(offer)
2410}
2411
2412pub async fn create_incoming_contract_output(
2413    module_api: &DynModuleApi,
2414    payment_hash: sha256::Hash,
2415    amount_msat: Amount,
2416    redeem_key: &Keypair,
2417) -> Result<(LightningOutputV0, Amount, ContractId), IncomingSmError> {
2418    let offer = fetch_and_validate_offer(module_api, payment_hash, amount_msat).await?;
2419    let our_pub_key = secp256k1::PublicKey::from_keypair(redeem_key);
2420    let contract = IncomingContract {
2421        hash: offer.hash,
2422        encrypted_preimage: offer.encrypted_preimage.clone(),
2423        decrypted_preimage: DecryptedPreimage::Pending,
2424        gateway_key: our_pub_key,
2425    };
2426    let contract_id = contract.contract_id();
2427    let incoming_output = LightningOutputV0::Contract(ContractOutput {
2428        amount: offer.amount,
2429        contract: Contract::Incoming(contract),
2430    });
2431
2432    Ok((incoming_output, offer.amount, contract_id))
2433}
2434
2435#[derive(Debug, Encodable, Decodable, Serialize)]
2436pub struct OutgoingLightningPayment {
2437    pub payment_type: PayType,
2438    pub contract_id: ContractId,
2439    pub fee: Amount,
2440}
2441
2442async fn set_payment_result(
2443    dbtx: &mut DatabaseTransaction<'_>,
2444    payment_hash: sha256::Hash,
2445    payment_type: PayType,
2446    contract_id: ContractId,
2447    fee: Amount,
2448) {
2449    if let Some(mut payment_result) = dbtx.get_value(&PaymentResultKey { payment_hash }).await {
2450        payment_result.completed_payment = Some(OutgoingLightningPayment {
2451            payment_type,
2452            contract_id,
2453            fee,
2454        });
2455        dbtx.insert_entry(&PaymentResultKey { payment_hash }, &payment_result)
2456            .await;
2457    }
2458}
2459
2460/// Tweak a user key with an index, this is used to generate a new key for each
2461/// invoice. This is done to not be able to link invoices to the same user.
2462pub fn tweak_user_key<Ctx: Verification + Signing>(
2463    secp: &Secp256k1<Ctx>,
2464    user_key: PublicKey,
2465    index: u64,
2466) -> PublicKey {
2467    let mut hasher = HmacEngine::<sha256::Hash>::new(&user_key.serialize()[..]);
2468    hasher.input(&index.to_be_bytes());
2469    let tweak = Hmac::from_engine(hasher).to_byte_array();
2470
2471    user_key
2472        .add_exp_tweak(secp, &Scalar::from_be_bytes(tweak).expect("can't fail"))
2473        .expect("tweak is always 32 bytes, other failure modes are negligible")
2474}
2475
2476/// Tweak a secret key with an index, this is used to claim an unspent incoming
2477/// contract.
2478fn tweak_user_secret_key<Ctx: Verification + Signing>(
2479    secp: &Secp256k1<Ctx>,
2480    key_pair: Keypair,
2481    index: u64,
2482) -> Keypair {
2483    let public_key = key_pair.public_key();
2484    let mut hasher = HmacEngine::<sha256::Hash>::new(&public_key.serialize()[..]);
2485    hasher.input(&index.to_be_bytes());
2486    let tweak = Hmac::from_engine(hasher).to_byte_array();
2487
2488    let secret_key = key_pair.secret_key();
2489    let sk_tweaked = secret_key
2490        .add_tweak(&Scalar::from_be_bytes(tweak).expect("Cant fail"))
2491        .expect("Cant fail");
2492    Keypair::from_secret_key(secp, &sk_tweaked)
2493}
2494
2495/// Get LN invoice with given settings
2496pub async fn get_invoice(
2497    info: &str,
2498    amount: Option<Amount>,
2499    lnurl_comment: Option<String>,
2500) -> anyhow::Result<Bolt11Invoice> {
2501    let info = info.trim();
2502    match lightning_invoice::Bolt11Invoice::from_str(info) {
2503        Ok(invoice) => {
2504            debug!("Parsed parameter as bolt11 invoice: {invoice}");
2505            match (invoice.amount_milli_satoshis(), amount) {
2506                (Some(_), Some(_)) => {
2507                    bail!("Amount specified in both invoice and command line")
2508                }
2509                (None, _) => {
2510                    bail!("We don't support invoices without an amount")
2511                }
2512                _ => {}
2513            }
2514            Ok(invoice)
2515        }
2516        Err(e) => {
2517            let lnurl = if info.to_lowercase().starts_with("lnurl") {
2518                lnurl::lnurl::LnUrl::from_str(info)?
2519            } else if info.contains('@') {
2520                lnurl::lightning_address::LightningAddress::from_str(info)?.lnurl()
2521            } else {
2522                bail!("Invalid invoice or lnurl: {e:?}");
2523            };
2524            debug!("Parsed parameter as lnurl: {lnurl:?}");
2525            let amount = amount.context("When using a lnurl, an amount must be specified")?;
2526            let async_client = lnurl::AsyncClient::from_client(reqwest::Client::new());
2527            let response = async_client.make_request(&lnurl.url).await?;
2528            match response {
2529                lnurl::LnUrlResponse::LnUrlPayResponse(response) => {
2530                    let invoice = async_client
2531                        .get_invoice(&response, amount.msats, None, lnurl_comment.as_deref())
2532                        .await?;
2533                    let invoice = Bolt11Invoice::from_str(invoice.invoice())?;
2534                    let invoice_amount = invoice.amount_milli_satoshis();
2535                    ensure!(
2536                        invoice_amount == Some(amount.msats),
2537                        "the amount generated by the lnurl ({invoice_amount:?}) is different from the requested amount ({amount}), try again using a different amount"
2538                    );
2539                    Ok(invoice)
2540                }
2541                other => {
2542                    bail!("Unexpected response from lnurl: {other:?}");
2543                }
2544            }
2545        }
2546    }
2547}
2548
2549#[derive(Debug, Clone)]
2550pub struct LightningClientContext {
2551    pub ln_decoder: Decoder,
2552    pub redeem_key: Keypair,
2553    pub gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
2554    /// Set to `None` for the gateway since it does not emit the client events.
2555    pub client_ctx: Option<ClientContext<LightningClientModule>>,
2556}
2557
2558impl fedimint_client_module::sm::Context for LightningClientContext {
2559    const KIND: Option<ModuleKind> = Some(KIND);
2560}
2561
2562#[apply(async_trait_maybe_send!)]
2563pub trait GatewayConnection: std::fmt::Debug {
2564    // Ping gateway endpoint to verify that it is available before locking funds in
2565    // OutgoingContract
2566    async fn verify_gateway_availability(
2567        &self,
2568        gateway: &LightningGateway,
2569    ) -> Result<(), ServerError>;
2570
2571    // Request the gateway to pay a BOLT11 invoice
2572    async fn pay_invoice(
2573        &self,
2574        gateway: LightningGateway,
2575        payload: PayInvoicePayload,
2576    ) -> Result<String, GatewayPayError>;
2577}
2578
2579#[derive(Debug)]
2580pub struct RealGatewayConnection {
2581    pub api: GatewayApi,
2582}
2583
2584#[apply(async_trait_maybe_send!)]
2585impl GatewayConnection for RealGatewayConnection {
2586    async fn verify_gateway_availability(
2587        &self,
2588        gateway: &LightningGateway,
2589    ) -> Result<(), ServerError> {
2590        self.api
2591            .request::<PublicKey, serde_json::Value>(
2592                &gateway.api,
2593                Method::GET,
2594                GET_GATEWAY_ID_ENDPOINT,
2595                None,
2596            )
2597            .await?;
2598        Ok(())
2599    }
2600
2601    async fn pay_invoice(
2602        &self,
2603        gateway: LightningGateway,
2604        payload: PayInvoicePayload,
2605    ) -> Result<String, GatewayPayError> {
2606        let preimage: String = self
2607            .api
2608            .request(
2609                &gateway.api,
2610                Method::POST,
2611                PAY_INVOICE_ENDPOINT,
2612                Some(payload),
2613            )
2614            .await
2615            .map_err(|e| GatewayPayError::GatewayInternalError {
2616                error_code: None,
2617                error_message: e.to_string(),
2618            })?;
2619        let length = preimage.len();
2620        Ok(preimage[1..length - 1].to_string())
2621    }
2622}
2623
2624#[derive(Debug)]
2625pub struct MockGatewayConnection;
2626
2627#[apply(async_trait_maybe_send!)]
2628impl GatewayConnection for MockGatewayConnection {
2629    async fn verify_gateway_availability(
2630        &self,
2631        _gateway: &LightningGateway,
2632    ) -> Result<(), ServerError> {
2633        Ok(())
2634    }
2635
2636    async fn pay_invoice(
2637        &self,
2638        _gateway: LightningGateway,
2639        _payload: PayInvoicePayload,
2640    ) -> Result<String, GatewayPayError> {
2641        // Just return a fake preimage to indicate success
2642        Ok("00000000".to_string())
2643    }
2644}