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