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, |state| match state {
1548                InternalPayState::Funding => false,
1549                InternalPayState::Preimage(_)
1550                | InternalPayState::RefundSuccess { .. }
1551                | InternalPayState::RefundError { .. }
1552                | InternalPayState::FundingFailed { .. }
1553                | InternalPayState::UnexpectedError(_) => true,
1554            }, move || {
1555            stream! {
1556                yield InternalPayState::Funding;
1557
1558                let state = loop {
1559                    match stream.next().await { Some(LightningClientStateMachines::InternalPay(state)) => {
1560                        match state.state {
1561                            IncomingSmStates::Preimage(preimage) => break InternalPayState::Preimage(preimage),
1562                            IncomingSmStates::RefundSubmitted{ out_points, error } => {
1563                                match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
1564                                    Ok(()) => break InternalPayState::RefundSuccess { out_points, error },
1565                                    Err(e) => break InternalPayState::RefundError{ error_message: e.to_string(), error },
1566                                }
1567                            },
1568                            IncomingSmStates::FundingFailed { error } => break InternalPayState::FundingFailed{ error },
1569                            _ => {}
1570                        }
1571                    } _ => {
1572                        break InternalPayState::UnexpectedError("Unexpected State! Expected an InternalPay state".to_string())
1573                    }}
1574                };
1575                yield state;
1576            }
1577        }))
1578    }
1579
1580    /// Subscribes to a stream of updates about a particular external Lightning
1581    /// payment operation specified by the `operation_id`.
1582    pub async fn subscribe_ln_pay(
1583        &self,
1584        operation_id: OperationId,
1585    ) -> anyhow::Result<UpdateStreamOrOutcome<LnPayState>> {
1586        async fn get_next_pay_state(
1587            stream: &mut BoxStream<'_, LightningClientStateMachines>,
1588        ) -> Option<LightningPayStates> {
1589            match stream.next().await {
1590                Some(LightningClientStateMachines::LightningPay(state)) => Some(state.state),
1591                Some(event) => {
1592                    // nosemgrep: use-err-formatting
1593                    error!(event = ?event, "Operation is not a lightning payment");
1594                    debug_assert!(false, "Operation is not a lightning payment: {event:?}");
1595                    None
1596                }
1597                None => None,
1598            }
1599        }
1600
1601        let operation = self.client_ctx.get_operation(operation_id).await?;
1602        let LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
1603            out_point: _,
1604            invoice: _,
1605            change,
1606            is_internal_payment,
1607            ..
1608        }) = operation.meta::<LightningOperationMeta>().variant
1609        else {
1610            bail!("Operation is not a lightning payment")
1611        };
1612
1613        ensure!(
1614            !is_internal_payment,
1615            "Subscribing to an internal LN payment, expected external LN payment"
1616        );
1617
1618        let client_ctx = self.client_ctx.clone();
1619
1620        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
1621                LnPayState::Created
1622                | LnPayState::Funded { .. }
1623                | LnPayState::WaitingForRefund { .. }
1624                | LnPayState::AwaitingChange => false,
1625                LnPayState::Success { .. }
1626                | LnPayState::Canceled
1627                | LnPayState::Refunded { .. }
1628                | LnPayState::UnexpectedError { .. } => true,
1629            }, move || {
1630            stream! {
1631                let self_ref = client_ctx.self_ref();
1632
1633                let mut stream = self_ref.notifier.subscribe(operation_id).await;
1634                let state = get_next_pay_state(&mut stream).await;
1635                match state {
1636                    Some(LightningPayStates::CreatedOutgoingLnContract(_)) => {
1637                        yield LnPayState::Created;
1638                    }
1639                    Some(LightningPayStates::FundingRejected) => {
1640                        yield LnPayState::Canceled;
1641                        return;
1642                    }
1643                    Some(state) => {
1644                        yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1645                        return;
1646                    }
1647                    None => {
1648                        error!("Unexpected end of lightning pay state machine");
1649                        return;
1650                    }
1651                }
1652
1653                let state = get_next_pay_state(&mut stream).await;
1654                match state {
1655                    Some(LightningPayStates::Funded(funded)) => {
1656                        yield LnPayState::Funded { block_height: funded.timelock }
1657                    }
1658                    Some(state) => {
1659                        yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1660                        return;
1661                    }
1662                    _ => {
1663                        error!("Unexpected end of lightning pay state machine");
1664                        return;
1665                    }
1666                }
1667
1668                let state = get_next_pay_state(&mut stream).await;
1669                match state {
1670                    Some(LightningPayStates::Success(preimage)) => {
1671                        if change.is_empty() {
1672                            yield LnPayState::Success { preimage };
1673                        } else {
1674                            yield LnPayState::AwaitingChange;
1675                            match client_ctx.await_primary_module_outputs(operation_id, change.clone()).await {
1676                                Ok(()) => {
1677                                    yield LnPayState::Success { preimage };
1678                                }
1679                                Err(e) => {
1680                                    yield LnPayState::UnexpectedError { error_message: format!("Error occurred while waiting for the change: {e:?}") };
1681                                }
1682                            }
1683                        }
1684                    }
1685                    Some(LightningPayStates::Refund(refund)) => {
1686                        yield LnPayState::WaitingForRefund {
1687                            error_reason: refund.error_reason.clone(),
1688                        };
1689
1690                        match client_ctx.await_primary_module_outputs(operation_id, refund.out_points).await {
1691                            Ok(()) => {
1692                                let gateway_error = GatewayPayError::GatewayInternalError { error_code: Some(500), error_message: refund.error_reason };
1693                                yield LnPayState::Refunded { gateway_error };
1694                            }
1695                            Err(e) => {
1696                                yield LnPayState::UnexpectedError {
1697                                    error_message: format!("Error occurred trying to get refund. Refund was not successful: {e:?}"),
1698                                };
1699                            }
1700                        }
1701                    }
1702                    Some(state) => {
1703                        yield LnPayState::UnexpectedError { error_message: format!("Found unexpected state during lightning payment: {state:?}") };
1704                    }
1705                    None => {
1706                        error!("Unexpected end of lightning pay state machine");
1707                        yield LnPayState::UnexpectedError { error_message: "Unexpected end of lightning pay state machine".to_string() };
1708                    }
1709                }
1710            }
1711        }))
1712    }
1713
1714    /// Scan unspent incoming contracts for a payment hash that matches a
1715    /// tweaked keys in the `indices` vector
1716    #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1717    #[allow(deprecated)]
1718    pub async fn scan_receive_for_user_tweaked<M: Serialize + Send + Sync + Clone>(
1719        &self,
1720        key_pair: Keypair,
1721        indices: Vec<u64>,
1722        extra_meta: M,
1723    ) -> Vec<OperationId> {
1724        let mut claims = Vec::new();
1725        for i in indices {
1726            let key_pair_tweaked = tweak_user_secret_key(&self.secp, key_pair, i);
1727            match self
1728                .scan_receive_for_user(key_pair_tweaked, extra_meta.clone())
1729                .await
1730            {
1731                Ok(operation_id) => claims.push(operation_id),
1732                Err(err) => {
1733                    error!(err = %err.fmt_compact_anyhow(), %i, "Failed to scan tweaked key at index i");
1734                }
1735            }
1736        }
1737
1738        claims
1739    }
1740
1741    /// Scan unspent incoming contracts for a payment hash that matches a public
1742    /// key and claim the incoming contract
1743    #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1744    #[allow(deprecated)]
1745    pub async fn scan_receive_for_user<M: Serialize + Send + Sync>(
1746        &self,
1747        key_pair: Keypair,
1748        extra_meta: M,
1749    ) -> anyhow::Result<OperationId> {
1750        let preimage_key: [u8; 33] = key_pair.public_key().serialize();
1751        let preimage = sha256::Hash::hash(&preimage_key);
1752        let contract_id = ContractId::from_raw_hash(sha256::Hash::hash(&preimage.to_byte_array()));
1753        self.claim_funded_incoming_contract(key_pair, contract_id, extra_meta)
1754            .await
1755    }
1756
1757    /// Claim the funded, unspent incoming contract by submitting a transaction
1758    /// to the federation and awaiting the primary module's outputs
1759    #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
1760    #[allow(deprecated)]
1761    pub async fn claim_funded_incoming_contract<M: Serialize + Send + Sync>(
1762        &self,
1763        key_pair: Keypair,
1764        contract_id: ContractId,
1765        extra_meta: M,
1766    ) -> anyhow::Result<OperationId> {
1767        let incoming_contract_account = get_incoming_contract(self.module_api.clone(), contract_id)
1768            .await?
1769            .ok_or(anyhow!("No contract account found"))
1770            .with_context(|| format!("No contract found for {contract_id:?}"))?;
1771
1772        let input = incoming_contract_account.claim();
1773        let client_input = ClientInput::<LightningInput> {
1774            input,
1775            amounts: Amounts::new_bitcoin(incoming_contract_account.amount),
1776            keys: vec![key_pair],
1777        };
1778
1779        let tx = TransactionBuilder::new().with_inputs(
1780            self.client_ctx
1781                .make_client_inputs(ClientInputBundle::new_no_sm(vec![client_input])),
1782        );
1783        let extra_meta = serde_json::to_value(extra_meta).expect("extra_meta is serializable");
1784        let operation_meta_gen = move |change_range: OutPointRange| LightningOperationMeta {
1785            variant: LightningOperationMetaVariant::Claim {
1786                out_points: change_range.into_iter().collect(),
1787            },
1788            extra_meta: extra_meta.clone(),
1789        };
1790        let operation_id = OperationId::new_random();
1791        self.client_ctx
1792            .finalize_and_submit_transaction(
1793                operation_id,
1794                LightningCommonInit::KIND.as_str(),
1795                operation_meta_gen,
1796                tx,
1797            )
1798            .await?;
1799        Ok(operation_id)
1800    }
1801
1802    /// Receive over LN with a new invoice
1803    /// Computes the federation fee receiving `amount` over Lightning would
1804    /// incur, without submitting anything.
1805    ///
1806    /// When the incoming contract is claimed, the client submits a transaction
1807    /// with a single Lightning input worth the contract amount; the primary
1808    /// module balances it by minting the change credited to the wallet. This
1809    /// quotes the fee of that transaction — the Lightning input fee, the mint
1810    /// output fees, and any sub-denomination dust — via the shared,
1811    /// module-agnostic fee quote.
1812    ///
1813    /// The gateway's off-chain Lightning fee is deliberately excluded: this is
1814    /// only the fee of the on-federation transaction. For that reason the quote
1815    /// is taken on `amount` directly (rather than the gateway-reduced contract
1816    /// amount), and no gateway round-trip is needed.
1817    pub async fn receive_fee_quote(&self, amount: Amount) -> anyhow::Result<FeeQuote> {
1818        self.client_ctx
1819            .fee_quote(
1820                OperationId::new_random(),
1821                FeeQuoteRequest {
1822                    input_amount: Amounts::new_bitcoin(amount),
1823                    output_amount: Amounts::ZERO,
1824                    input_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input),
1825                    output_fee: Amounts::ZERO,
1826                },
1827            )
1828            .await
1829    }
1830
1831    /// Computes the federation fee a `pay` funding an outgoing contract worth
1832    /// `amount` would incur, without submitting anything.
1833    ///
1834    /// When a payment is sent, the client submits a transaction with a single
1835    /// Lightning output (the outgoing contract) worth `amount`; the primary
1836    /// module balances it by spending ecash to fund the contract and minting
1837    /// any change. This quotes the fee of that transaction — the Lightning
1838    /// output fee, the mint input fees on the funding notes, any mint change
1839    /// output fees, and sub-denomination dust — via the shared, module-agnostic
1840    /// fee quote.
1841    ///
1842    /// The gateway's off-chain Lightning fee is deliberately excluded: it is
1843    /// part of the contract `amount` the gateway claims, not the on-federation
1844    /// transaction fee. So `amount` is the full outgoing contract value.
1845    pub async fn send_fee_quote(&self, amount: Amount) -> anyhow::Result<FeeQuote> {
1846        self.client_ctx
1847            .fee_quote(
1848                OperationId::new_random(),
1849                FeeQuoteRequest {
1850                    input_amount: Amounts::ZERO,
1851                    output_amount: Amounts::new_bitcoin(amount),
1852                    input_fee: Amounts::ZERO,
1853                    output_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.contract_output),
1854                },
1855            )
1856            .await
1857    }
1858
1859    /// Computes the largest invoice amount the client can pay in full out of
1860    /// `balance`, i.e. the amount to request an invoice for in order to spend
1861    /// (close to) the entire balance.
1862    ///
1863    /// Paying an invoice deducts two kinds of fee from the balance:
1864    /// - the *gateway* routing fee, which is added on top of the invoice amount
1865    ///   to form the outgoing contract (`invoice_amount + gateway.fees`), and
1866    /// - the *federation* fee of funding that contract — the Lightning output
1867    ///   fee, the mint input fees on the funding notes, the mint output fees on
1868    ///   any change, and sub-denomination dust — as quoted by
1869    ///   [`Self::send_fee_quote`].
1870    ///
1871    /// `balance` is the client's current Bitcoin balance (e.g. from
1872    /// `Client::get_balance_for_btc`). `gateway` optionally pins the gateway to
1873    /// use; pass the same [`LightningGateway`] you intend to hand to
1874    /// [`Self::pay_bolt11_invoice`] so the fee schedules match. If `None`, a
1875    /// registered gateway is selected at random, the same way
1876    /// [`Self::get_gateway`] does for an external payment.
1877    ///
1878    /// The maximum payable amount is found by binary search over the real fee
1879    /// quote (see [`max_affordable_send_amount`]) rather than a closed form,
1880    /// because the federation fee is stepwise in the amount. The quote is
1881    /// point-in-time and moves with the balance; the eventual
1882    /// [`Self::pay_bolt11_invoice`] remains the source of truth and may still
1883    /// fail if balance or gateway state changes in between.
1884    ///
1885    /// Returns an error if no gateway is available or if the balance cannot
1886    /// cover even the smallest payable amount plus fees. Any LNURL
1887    /// `minSendable`/`maxSendable` bounds are the caller's responsibility to
1888    /// apply.
1889    pub async fn spendable_amount(
1890        &self,
1891        balance: Amount,
1892        gateway: Option<LightningGateway>,
1893    ) -> anyhow::Result<Amount> {
1894        let gateway = match gateway {
1895            Some(gateway) => gateway,
1896            None => self
1897                .get_gateway(None, false)
1898                .await?
1899                .ok_or_else(|| anyhow!("No gateway available to send the payment"))?,
1900        };
1901
1902        max_affordable_send_amount(
1903            balance,
1904            Amount::from_msats(1),
1905            balance,
1906            |invoice_amount: Amount| invoice_amount + gateway.fees.to_amount(&invoice_amount),
1907            |contract_amount: Amount| self.send_fee_quote(contract_amount),
1908        )
1909        .await
1910        .ok_or_else(|| anyhow!("Balance is too low to send any amount after fees"))
1911    }
1912
1913    pub async fn create_bolt11_invoice<M: Serialize + Send + Sync>(
1914        &self,
1915        amount: Amount,
1916        description: lightning_invoice::Bolt11InvoiceDescription,
1917        expiry_time: Option<u64>,
1918        extra_meta: M,
1919        gateway: Option<LightningGateway>,
1920    ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1921        let receiving_key =
1922            ReceivingKey::Personal(Keypair::new(&self.secp, &mut rand::rngs::OsRng));
1923        self.create_bolt11_invoice_internal(
1924            amount,
1925            description,
1926            expiry_time,
1927            receiving_key,
1928            extra_meta,
1929            gateway,
1930        )
1931        .await
1932    }
1933
1934    /// Receive over LN with a new invoice for another user, tweaking their key
1935    /// by the given index
1936    #[allow(clippy::too_many_arguments)]
1937    pub async fn create_bolt11_invoice_for_user_tweaked<M: Serialize + Send + Sync>(
1938        &self,
1939        amount: Amount,
1940        description: lightning_invoice::Bolt11InvoiceDescription,
1941        expiry_time: Option<u64>,
1942        user_key: PublicKey,
1943        index: u64,
1944        extra_meta: M,
1945        gateway: Option<LightningGateway>,
1946    ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1947        let tweaked_key = tweak_user_key(&self.secp, user_key, index);
1948        self.create_bolt11_invoice_for_user(
1949            amount,
1950            description,
1951            expiry_time,
1952            tweaked_key,
1953            extra_meta,
1954            gateway,
1955        )
1956        .await
1957    }
1958
1959    /// Receive over LN with a new invoice for another user
1960    pub async fn create_bolt11_invoice_for_user<M: Serialize + Send + Sync>(
1961        &self,
1962        amount: Amount,
1963        description: lightning_invoice::Bolt11InvoiceDescription,
1964        expiry_time: Option<u64>,
1965        user_key: PublicKey,
1966        extra_meta: M,
1967        gateway: Option<LightningGateway>,
1968    ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1969        let receiving_key = ReceivingKey::External(user_key);
1970        self.create_bolt11_invoice_internal(
1971            amount,
1972            description,
1973            expiry_time,
1974            receiving_key,
1975            extra_meta,
1976            gateway,
1977        )
1978        .await
1979    }
1980
1981    /// Receive over LN with a new invoice
1982    async fn create_bolt11_invoice_internal<M: Serialize + Send + Sync>(
1983        &self,
1984        amount: Amount,
1985        description: lightning_invoice::Bolt11InvoiceDescription,
1986        expiry_time: Option<u64>,
1987        receiving_key: ReceivingKey,
1988        extra_meta: M,
1989        gateway: Option<LightningGateway>,
1990    ) -> anyhow::Result<(OperationId, Bolt11Invoice, [u8; 32])> {
1991        let gateway_id = gateway.as_ref().map(|g| g.gateway_id);
1992        let (src_node_id, short_channel_id, route_hints) = if let Some(current_gateway) = gateway {
1993            (
1994                current_gateway.node_pub_key,
1995                current_gateway.federation_index,
1996                current_gateway.route_hints,
1997            )
1998        } else {
1999            // If no gateway is provided, this is assumed to be an internal payment.
2000            let markers = self.client_ctx.get_internal_payment_markers()?;
2001            (markers.0, markers.1, vec![])
2002        };
2003
2004        debug!(target: LOG_CLIENT_MODULE_LN, ?gateway_id, %amount, "Selected LN gateway for invoice generation");
2005
2006        let (operation_id, invoice, output, preimage) = self.create_lightning_receive_output(
2007            amount,
2008            description,
2009            receiving_key,
2010            rand::rngs::OsRng,
2011            expiry_time,
2012            src_node_id,
2013            short_channel_id,
2014            &route_hints,
2015            self.cfg.network.0,
2016        )?;
2017
2018        let tx =
2019            TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(output));
2020        let extra_meta = serde_json::to_value(extra_meta).expect("extra_meta is serializable");
2021        let operation_meta_gen = {
2022            let invoice = invoice.clone();
2023            move |change_range: OutPointRange| LightningOperationMeta {
2024                variant: LightningOperationMetaVariant::Receive {
2025                    out_point: OutPoint {
2026                        txid: change_range.txid(),
2027                        out_idx: 0,
2028                    },
2029                    invoice: invoice.clone(),
2030                    gateway_id,
2031                },
2032                extra_meta: extra_meta.clone(),
2033            }
2034        };
2035        let change_range = self
2036            .client_ctx
2037            .finalize_and_submit_transaction(
2038                operation_id,
2039                LightningCommonInit::KIND.as_str(),
2040                operation_meta_gen,
2041                tx,
2042            )
2043            .await?;
2044
2045        debug!(target: LOG_CLIENT_MODULE_LN, txid = ?change_range.txid(), ?operation_id, "Waiting for LN invoice to be confirmed");
2046
2047        // Wait for the transaction to be accepted by the federation, otherwise the
2048        // invoice will not be able to be paid
2049        self.client_ctx
2050            .transaction_updates(operation_id)
2051            .await
2052            .await_tx_accepted(change_range.txid())
2053            .await
2054            .map_err(|e| anyhow!("Offer transaction was not accepted: {e:?}"))?;
2055
2056        debug!(target: LOG_CLIENT_MODULE_LN, %invoice, "Invoice confirmed");
2057
2058        Ok((operation_id, invoice, preimage))
2059    }
2060
2061    /// Starts a new state machine that retries claiming a previously paid
2062    /// invoice.
2063    ///
2064    /// This is a local state-history recovery tool: it requires the client DB
2065    /// to still contain a historical `SubmittedOffer` or `ConfirmedInvoice`
2066    /// state for the original operation. It does not recover seed-only
2067    /// restores where that local state-machine history is unavailable.
2068    ///
2069    /// Repeated calls start independent reclaim attempts. This is intentional:
2070    /// this is a manual break-glass recovery path, and concurrent attempts race
2071    /// through the normal federation transaction validation.
2072    ///
2073    /// # Errors
2074    ///
2075    /// Returns an error if the original operation is not a reclaimable
2076    /// lightning receive, if it is still active, or if the original receiving
2077    /// key cannot be recovered from state history.
2078    pub async fn reclaim_ln_receive(
2079        &self,
2080        original_operation_id: OperationId,
2081    ) -> anyhow::Result<OperationId> {
2082        let operation = self.client_ctx.get_operation(original_operation_id).await?;
2083        let LightningOperationMeta {
2084            variant,
2085            extra_meta,
2086        } = operation
2087            .try_meta::<LightningOperationMeta>()
2088            .context("Invalid lightning operation metadata")?;
2089
2090        let (invoice, gateway_id) = match variant {
2091            LightningOperationMetaVariant::Receive {
2092                invoice,
2093                gateway_id,
2094                ..
2095            } => (invoice, gateway_id),
2096            LightningOperationMetaVariant::RecurringPaymentReceive(meta) => (meta.invoice, None),
2097            _ => bail!("Operation is not a reclaimable lightning receive"),
2098        };
2099
2100        let active_states = self
2101            .client_ctx
2102            .get_own_operation_active_states(original_operation_id)
2103            .await;
2104        ensure!(
2105            !active_states
2106                .iter()
2107                .any(|(state, _)| matches!(state, LightningClientStateMachines::Receive(_))),
2108            "Cannot reclaim an active lightning receive"
2109        );
2110
2111        let inactive_states = self
2112            .client_ctx
2113            .get_own_operation_inactive_states(original_operation_id)
2114            .await;
2115
2116        let receiving_key = inactive_states
2117            .iter()
2118            .find_map(|(state, _)| Self::ln_receive_key_from_state(state))
2119            .ok_or_else(|| {
2120                anyhow!("Cannot reclaim LN receive because the original receive key is unavailable")
2121            })?;
2122        let db = self.client_ctx.module_db();
2123        let mut dbtx = db.begin_transaction().await;
2124        let reclaim_operation_id = OperationId::new_random();
2125        let operation_meta = LightningOperationMeta {
2126            variant: LightningOperationMetaVariant::ReceiveReclaim {
2127                original_operation_id,
2128                invoice: invoice.clone(),
2129                gateway_id,
2130            },
2131            extra_meta,
2132        };
2133        let state = LightningClientStateMachines::Receive(LightningReceiveStateMachine {
2134            operation_id: reclaim_operation_id,
2135            state: LightningReceiveStates::ConfirmedInvoice(LightningReceiveConfirmedInvoice {
2136                invoice,
2137                receiving_key,
2138            }),
2139        });
2140
2141        self.client_ctx
2142            .manual_operation_start_dbtx(
2143                &mut dbtx.to_ref_nc(),
2144                reclaim_operation_id,
2145                LightningCommonInit::KIND.as_str(),
2146                operation_meta,
2147                vec![self.client_ctx.make_dyn_state(state)],
2148            )
2149            .await?;
2150
2151        dbtx.commit_tx().await;
2152
2153        Ok(reclaim_operation_id)
2154    }
2155
2156    fn ln_receive_key_from_state(state: &LightningClientStateMachines) -> Option<ReceivingKey> {
2157        match state {
2158            LightningClientStateMachines::Receive(receive) => match &receive.state {
2159                LightningReceiveStates::SubmittedOffer(submitted_offer) => {
2160                    Some(submitted_offer.receiving_key)
2161                }
2162                LightningReceiveStates::ConfirmedInvoice(confirmed_invoice) => {
2163                    Some(confirmed_invoice.receiving_key)
2164                }
2165                LightningReceiveStates::Canceled(_)
2166                | LightningReceiveStates::Funded(_)
2167                | LightningReceiveStates::Success(_) => None,
2168            },
2169            LightningClientStateMachines::InternalPay(_)
2170            | LightningClientStateMachines::LightningPay(_) => None,
2171        }
2172    }
2173
2174    #[deprecated(since = "0.7.0", note = "Use recurring payment functionality instead")]
2175    #[allow(deprecated)]
2176    pub async fn subscribe_ln_claim(
2177        &self,
2178        operation_id: OperationId,
2179    ) -> anyhow::Result<UpdateStreamOrOutcome<LnReceiveState>> {
2180        let operation = self.client_ctx.get_operation(operation_id).await?;
2181        let LightningOperationMetaVariant::Claim { out_points } =
2182            operation.meta::<LightningOperationMeta>().variant
2183        else {
2184            bail!("Operation is not a lightning claim")
2185        };
2186
2187        let client_ctx = self.client_ctx.clone();
2188
2189        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
2190                LnReceiveState::Created
2191                | LnReceiveState::WaitingForPayment { .. }
2192                | LnReceiveState::Funded
2193                | LnReceiveState::AwaitingFunds => false,
2194                LnReceiveState::Canceled { .. } | LnReceiveState::Claimed => true,
2195            }, move || {
2196            stream! {
2197                yield LnReceiveState::AwaitingFunds;
2198
2199                if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
2200                    yield LnReceiveState::Claimed;
2201                } else {
2202                    yield LnReceiveState::Canceled { reason: LightningReceiveError::ClaimRejected }
2203                }
2204            }
2205        }))
2206    }
2207
2208    pub async fn subscribe_ln_receive(
2209        &self,
2210        operation_id: OperationId,
2211    ) -> anyhow::Result<UpdateStreamOrOutcome<LnReceiveState>> {
2212        let operation = self.client_ctx.get_operation(operation_id).await?;
2213        let (invoice, tx_accepted_future) = match operation.meta::<LightningOperationMeta>().variant
2214        {
2215            LightningOperationMetaVariant::Receive {
2216                out_point, invoice, ..
2217            } => {
2218                let tx_accepted_future = self
2219                    .client_ctx
2220                    .transaction_updates(operation_id)
2221                    .await
2222                    .await_tx_accepted(out_point.txid);
2223                (invoice, Some(tx_accepted_future))
2224            }
2225            LightningOperationMetaVariant::ReceiveReclaim { invoice, .. } => (invoice, None),
2226            _ => bail!("Operation is not a lightning receive"),
2227        };
2228
2229        let client_ctx = self.client_ctx.clone();
2230
2231        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
2232                LnReceiveState::Created
2233                | LnReceiveState::WaitingForPayment { .. }
2234                | LnReceiveState::Funded
2235                | LnReceiveState::AwaitingFunds => false,
2236                LnReceiveState::Canceled { .. } | LnReceiveState::Claimed => true,
2237            }, move || {
2238            stream! {
2239
2240                let self_ref = client_ctx.self_ref();
2241
2242                yield LnReceiveState::Created;
2243
2244                let tx_rejected = match tx_accepted_future {
2245                    Some(tx_accepted_future) => tx_accepted_future.await.is_err(),
2246                    None => false,
2247                };
2248                if tx_rejected {
2249                    yield LnReceiveState::Canceled { reason: LightningReceiveError::Rejected };
2250                    return;
2251                }
2252                yield LnReceiveState::WaitingForPayment { invoice: invoice.to_string(), timeout: invoice.expiry_time() };
2253
2254                match self_ref.await_receive_success(operation_id).await {
2255                    Ok(()) => {
2256
2257                        yield LnReceiveState::Funded;
2258
2259                        match self_ref.await_claim_acceptance(operation_id).await {
2260                            Ok(out_points) => {
2261                                yield LnReceiveState::AwaitingFunds;
2262
2263                                if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
2264                                    yield LnReceiveState::Claimed;
2265                                    return;
2266                                }
2267
2268                                // The claim transaction was accepted, but its outputs were not
2269                                // confirmed by the primary module. The incoming contract is already
2270                                // spent, so this is not reclaimable as a rejected claim.
2271                                yield LnReceiveState::Canceled { reason: LightningReceiveError::Rejected };
2272                            }
2273                            Err(e) => {
2274                                yield LnReceiveState::Canceled { reason: e };
2275                            }
2276                        }
2277                    }
2278                    Err(e) => {
2279                        yield LnReceiveState::Canceled { reason: e };
2280                    }
2281                }
2282            }
2283        }))
2284    }
2285
2286    /// Returns a gateway to be used for a lightning operation. If
2287    /// `force_internal` is true and no `gateway_id` is specified, no
2288    /// gateway will be selected.
2289    pub async fn get_gateway(
2290        &self,
2291        gateway_id: Option<secp256k1::PublicKey>,
2292        force_internal: bool,
2293    ) -> anyhow::Result<Option<LightningGateway>> {
2294        match gateway_id {
2295            Some(gateway_id) => {
2296                if let Some(gw) = self.select_gateway(&gateway_id).await {
2297                    Ok(Some(gw))
2298                } else {
2299                    // Refresh the gateway cache in case the target gateway was registered since the
2300                    // last update.
2301                    self.update_gateway_cache().await?;
2302                    Ok(self.select_gateway(&gateway_id).await)
2303                }
2304            }
2305            None if !force_internal => {
2306                // Refresh the gateway cache to find a random gateway to select from.
2307                self.update_gateway_cache().await?;
2308                let gateways = self.list_gateways().await;
2309                let gw = gateways.into_iter().choose(&mut OsRng).map(|gw| gw.info);
2310                if let Some(gw) = gw {
2311                    let gw_id = gw.gateway_id;
2312                    info!(%gw_id, "Using random gateway");
2313                    Ok(Some(gw))
2314                } else {
2315                    Err(anyhow!(
2316                        "No gateways exist in gateway cache and `force_internal` is false"
2317                    ))
2318                }
2319            }
2320            None => Ok(None),
2321        }
2322    }
2323
2324    /// Subscribes to either a internal or external lightning payment and
2325    /// returns `LightningPaymentOutcome` that indicates if the payment was
2326    /// successful or not.
2327    pub async fn await_outgoing_payment(
2328        &self,
2329        operation_id: OperationId,
2330    ) -> anyhow::Result<LightningPaymentOutcome> {
2331        let operation = self.client_ctx.get_operation(operation_id).await?;
2332        let variant = operation.meta::<LightningOperationMeta>().variant;
2333        let LightningOperationMetaVariant::Pay(LightningOperationMetaPay {
2334            is_internal_payment,
2335            ..
2336        }) = variant
2337        else {
2338            bail!("Operation is not a lightning payment")
2339        };
2340
2341        let mut final_state = None;
2342
2343        // First check if the outgoing payment is an internal payment
2344        if is_internal_payment {
2345            let updates = self.subscribe_internal_pay(operation_id).await?;
2346            let mut stream = updates.into_stream();
2347            while let Some(update) = stream.next().await {
2348                match update {
2349                    InternalPayState::Preimage(preimage) => {
2350                        final_state = Some(LightningPaymentOutcome::Success {
2351                            preimage: preimage.0.consensus_encode_to_hex(),
2352                        });
2353                    }
2354                    InternalPayState::RefundSuccess {
2355                        out_points: _,
2356                        error,
2357                    } => {
2358                        final_state = Some(LightningPaymentOutcome::Failure {
2359                            error_message: format!("LNv1 internal payment was refunded: {error:?}"),
2360                        });
2361                    }
2362                    InternalPayState::FundingFailed { error } => {
2363                        final_state = Some(LightningPaymentOutcome::Failure {
2364                            error_message: format!(
2365                                "LNv1 internal payment funding failed: {error:?}"
2366                            ),
2367                        });
2368                    }
2369                    InternalPayState::RefundError {
2370                        error_message,
2371                        error,
2372                    } => {
2373                        final_state = Some(LightningPaymentOutcome::Failure {
2374                            error_message: format!(
2375                                "LNv1 refund failed: {error_message}: {error:?}"
2376                            ),
2377                        });
2378                    }
2379                    InternalPayState::UnexpectedError(error) => {
2380                        final_state = Some(LightningPaymentOutcome::Failure {
2381                            error_message: error,
2382                        });
2383                    }
2384                    InternalPayState::Funding => {}
2385                }
2386            }
2387        } else {
2388            let updates = self.subscribe_ln_pay(operation_id).await?;
2389            let mut stream = updates.into_stream();
2390            while let Some(update) = stream.next().await {
2391                match update {
2392                    LnPayState::Success { preimage } => {
2393                        final_state = Some(LightningPaymentOutcome::Success { preimage });
2394                    }
2395                    LnPayState::Refunded { gateway_error } => {
2396                        final_state = Some(LightningPaymentOutcome::Failure {
2397                            error_message: format!(
2398                                "LNv1 external payment was refunded: {gateway_error:?}"
2399                            ),
2400                        });
2401                    }
2402                    LnPayState::UnexpectedError { error_message } => {
2403                        final_state = Some(LightningPaymentOutcome::Failure { error_message });
2404                    }
2405                    _ => {}
2406                }
2407            }
2408        }
2409
2410        final_state.ok_or(anyhow!(
2411            "Internal or external outgoing lightning payment did not reach a final state"
2412        ))
2413    }
2414}
2415
2416// TODO: move to appropriate module (cli?)
2417// some refactoring here needed
2418#[derive(Debug, Clone, Serialize, Deserialize)]
2419#[serde(rename_all = "snake_case")]
2420pub struct PayInvoiceResponse {
2421    operation_id: OperationId,
2422    contract_id: ContractId,
2423    preimage: String,
2424}
2425
2426#[allow(clippy::large_enum_variant)]
2427#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
2428pub enum LightningClientStateMachines {
2429    InternalPay(IncomingStateMachine),
2430    LightningPay(LightningPayStateMachine),
2431    Receive(LightningReceiveStateMachine),
2432}
2433
2434impl IntoDynInstance for LightningClientStateMachines {
2435    type DynType = DynState;
2436
2437    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
2438        DynState::from_typed(instance_id, self)
2439    }
2440}
2441
2442impl State for LightningClientStateMachines {
2443    type ModuleContext = LightningClientContext;
2444
2445    fn transitions(
2446        &self,
2447        context: &Self::ModuleContext,
2448        global_context: &DynGlobalClientContext,
2449    ) -> Vec<StateTransition<Self>> {
2450        match self {
2451            LightningClientStateMachines::InternalPay(internal_pay_state) => {
2452                sm_enum_variant_translation!(
2453                    internal_pay_state.transitions(context, global_context),
2454                    LightningClientStateMachines::InternalPay
2455                )
2456            }
2457            LightningClientStateMachines::LightningPay(lightning_pay_state) => {
2458                sm_enum_variant_translation!(
2459                    lightning_pay_state.transitions(context, global_context),
2460                    LightningClientStateMachines::LightningPay
2461                )
2462            }
2463            LightningClientStateMachines::Receive(receive_state) => {
2464                sm_enum_variant_translation!(
2465                    receive_state.transitions(context, global_context),
2466                    LightningClientStateMachines::Receive
2467                )
2468            }
2469        }
2470    }
2471
2472    fn operation_id(&self) -> OperationId {
2473        match self {
2474            LightningClientStateMachines::InternalPay(internal_pay_state) => {
2475                internal_pay_state.operation_id()
2476            }
2477            LightningClientStateMachines::LightningPay(lightning_pay_state) => {
2478                lightning_pay_state.operation_id()
2479            }
2480            LightningClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
2481        }
2482    }
2483}
2484
2485async fn fetch_and_validate_offer(
2486    module_api: &DynModuleApi,
2487    payment_hash: sha256::Hash,
2488    amount_msat: Amount,
2489) -> anyhow::Result<IncomingContractOffer, IncomingSmError> {
2490    let offer = timeout(Duration::from_secs(5), module_api.fetch_offer(payment_hash))
2491        .await
2492        .map_err(|_| IncomingSmError::TimeoutFetchingOffer { payment_hash })?
2493        .map_err(|e| IncomingSmError::FetchContractError {
2494            payment_hash,
2495            error_message: e.to_string(),
2496        })?;
2497
2498    if offer.amount > amount_msat {
2499        return Err(IncomingSmError::ViolatedFeePolicy {
2500            offer_amount: offer.amount,
2501            payment_amount: amount_msat,
2502        });
2503    }
2504    if offer.hash != payment_hash {
2505        return Err(IncomingSmError::InvalidOffer {
2506            offer_hash: offer.hash,
2507            payment_hash,
2508        });
2509    }
2510    Ok(offer)
2511}
2512
2513pub async fn create_incoming_contract_output(
2514    module_api: &DynModuleApi,
2515    payment_hash: sha256::Hash,
2516    amount_msat: Amount,
2517    redeem_key: &Keypair,
2518) -> Result<(LightningOutputV0, Amount, ContractId), IncomingSmError> {
2519    let offer = fetch_and_validate_offer(module_api, payment_hash, amount_msat).await?;
2520    let our_pub_key = secp256k1::PublicKey::from_keypair(redeem_key);
2521    let contract = IncomingContract {
2522        hash: offer.hash,
2523        encrypted_preimage: offer.encrypted_preimage.clone(),
2524        decrypted_preimage: DecryptedPreimage::Pending,
2525        gateway_key: our_pub_key,
2526    };
2527    let contract_id = contract.contract_id();
2528    let incoming_output = LightningOutputV0::Contract(ContractOutput {
2529        amount: offer.amount,
2530        contract: Contract::Incoming(contract),
2531    });
2532
2533    Ok((incoming_output, offer.amount, contract_id))
2534}
2535
2536#[derive(Debug, Encodable, Decodable, Serialize, Deserialize)]
2537#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2538pub struct OutgoingLightningPayment {
2539    pub payment_type: PayType,
2540    pub contract_id: ContractId,
2541    pub fee: Amount,
2542}
2543
2544async fn set_payment_result(
2545    dbtx: &mut DatabaseTransaction<'_>,
2546    payment_hash: sha256::Hash,
2547    payment_type: PayType,
2548    contract_id: ContractId,
2549    fee: Amount,
2550) {
2551    if let Some(mut payment_result) = dbtx.get_value(&PaymentResultKey { payment_hash }).await {
2552        payment_result.completed_payment = Some(OutgoingLightningPayment {
2553            payment_type,
2554            contract_id,
2555            fee,
2556        });
2557        dbtx.insert_entry(&PaymentResultKey { payment_hash }, &payment_result)
2558            .await;
2559    }
2560}
2561
2562/// Tweak a user key with an index, this is used to generate a new key for each
2563/// invoice. This is done to not be able to link invoices to the same user.
2564pub fn tweak_user_key<Ctx: Verification + Signing>(
2565    secp: &Secp256k1<Ctx>,
2566    user_key: PublicKey,
2567    index: u64,
2568) -> PublicKey {
2569    let mut hasher = HmacEngine::<sha256::Hash>::new(&user_key.serialize()[..]);
2570    hasher.input(&index.to_be_bytes());
2571    let tweak = Hmac::from_engine(hasher).to_byte_array();
2572
2573    user_key
2574        .add_exp_tweak(secp, &Scalar::from_be_bytes(tweak).expect("can't fail"))
2575        .expect("tweak is always 32 bytes, other failure modes are negligible")
2576}
2577
2578/// Tweak a secret key with an index, this is used to claim an unspent incoming
2579/// contract.
2580fn tweak_user_secret_key<Ctx: Verification + Signing>(
2581    secp: &Secp256k1<Ctx>,
2582    key_pair: Keypair,
2583    index: u64,
2584) -> Keypair {
2585    let public_key = key_pair.public_key();
2586    let mut hasher = HmacEngine::<sha256::Hash>::new(&public_key.serialize()[..]);
2587    hasher.input(&index.to_be_bytes());
2588    let tweak = Hmac::from_engine(hasher).to_byte_array();
2589
2590    let secret_key = key_pair.secret_key();
2591    let sk_tweaked = secret_key
2592        .add_tweak(&Scalar::from_be_bytes(tweak).expect("Cant fail"))
2593        .expect("Cant fail");
2594    Keypair::from_secret_key(secp, &sk_tweaked)
2595}
2596
2597/// A payment target parsed from user input: either a bolt11 invoice or the
2598/// pay parameters resolved from an LNURL/lightning address.
2599#[derive(Debug, Clone)]
2600pub enum PaymentInfo {
2601    Bolt11(Bolt11Invoice),
2602    Lnurl(lnurl::pay::PayResponse),
2603}
2604
2605impl PaymentInfo {
2606    /// Parse `info` as a bolt11 invoice, or resolve it as an LNURL/lightning
2607    /// address by fetching the endpoint's pay parameters.
2608    pub async fn parse(info: &str) -> anyhow::Result<Self> {
2609        let info = info.trim();
2610        match lightning_invoice::Bolt11Invoice::from_str(info) {
2611            Ok(invoice) => {
2612                debug!("Parsed parameter as bolt11 invoice: {invoice}");
2613                Ok(Self::Bolt11(invoice))
2614            }
2615            Err(e) => {
2616                let lnurl = if info.to_lowercase().starts_with("lnurl") {
2617                    lnurl::lnurl::LnUrl::from_str(info)?
2618                } else if info.contains('@') {
2619                    lnurl::lightning_address::LightningAddress::from_str(info)?.lnurl()
2620                } else {
2621                    bail!("Invalid invoice or lnurl: {e:?}");
2622                };
2623                debug!("Parsed parameter as lnurl: {lnurl:?}");
2624                let async_client = lnurl::AsyncClient::from_client(reqwest::Client::new());
2625                let response = async_client.make_request(&lnurl.url).await?;
2626                match response {
2627                    lnurl::LnUrlResponse::LnUrlPayResponse(response) => Ok(Self::Lnurl(response)),
2628                    other => {
2629                        bail!("Unexpected response from lnurl: {other:?}");
2630                    }
2631                }
2632            }
2633        }
2634    }
2635
2636    /// Produce the bolt11 invoice to pay: the parsed invoice itself, or one
2637    /// requested from the LNURL endpoint for `amount`.
2638    pub async fn get_invoice(
2639        self,
2640        amount: Option<Amount>,
2641        lnurl_comment: Option<String>,
2642    ) -> anyhow::Result<Bolt11Invoice> {
2643        match self {
2644            Self::Bolt11(invoice) => {
2645                match (invoice.amount_milli_satoshis(), amount) {
2646                    (Some(_), Some(_)) => {
2647                        bail!("Amount specified in both invoice and command line")
2648                    }
2649                    (None, _) => {
2650                        bail!("We don't support invoices without an amount")
2651                    }
2652                    _ => {}
2653                }
2654                Ok(invoice)
2655            }
2656            Self::Lnurl(response) => {
2657                let amount = amount.context("When using a lnurl, an amount must be specified")?;
2658                let async_client = lnurl::AsyncClient::from_client(reqwest::Client::new());
2659                let invoice = async_client
2660                    .get_invoice(&response, amount.msats, None, lnurl_comment.as_deref())
2661                    .await?;
2662                let invoice = Bolt11Invoice::from_str(invoice.invoice())?;
2663                let invoice_amount = invoice.amount_milli_satoshis();
2664                ensure!(
2665                    invoice_amount == Some(amount.msats),
2666                    "the amount generated by the lnurl ({invoice_amount:?}) is different from the requested amount ({amount}), try again using a different amount"
2667                );
2668                Ok(invoice)
2669            }
2670        }
2671    }
2672}
2673
2674/// Get LN invoice with given settings
2675pub async fn get_invoice(
2676    info: &str,
2677    amount: Option<Amount>,
2678    lnurl_comment: Option<String>,
2679) -> anyhow::Result<Bolt11Invoice> {
2680    PaymentInfo::parse(info)
2681        .await?
2682        .get_invoice(amount, lnurl_comment)
2683        .await
2684}
2685
2686#[derive(Debug, Clone)]
2687pub struct LightningClientContext {
2688    pub ln_decoder: Decoder,
2689    pub redeem_key: Keypair,
2690    pub gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
2691    /// Set to `None` for the gateway since it does not emit the client events.
2692    pub client_ctx: Option<ClientContext<LightningClientModule>>,
2693}
2694
2695impl fedimint_client_module::sm::Context for LightningClientContext {
2696    const KIND: Option<ModuleKind> = Some(KIND);
2697}
2698
2699#[apply(async_trait_maybe_send!)]
2700pub trait GatewayConnection: std::fmt::Debug {
2701    // Ping gateway endpoint to verify that it is available before locking funds in
2702    // OutgoingContract
2703    async fn verify_gateway_availability(
2704        &self,
2705        gateway: &LightningGateway,
2706    ) -> Result<(), ServerError>;
2707
2708    // Request the gateway to pay a BOLT11 invoice
2709    async fn pay_invoice(
2710        &self,
2711        gateway: LightningGateway,
2712        payload: PayInvoicePayload,
2713    ) -> Result<String, GatewayPayError>;
2714}
2715
2716#[derive(Debug)]
2717pub struct RealGatewayConnection {
2718    pub api: GatewayApi,
2719}
2720
2721#[apply(async_trait_maybe_send!)]
2722impl GatewayConnection for RealGatewayConnection {
2723    async fn verify_gateway_availability(
2724        &self,
2725        gateway: &LightningGateway,
2726    ) -> Result<(), ServerError> {
2727        self.api
2728            .request::<PublicKey, serde_json::Value>(
2729                &gateway.api,
2730                Method::GET,
2731                GET_GATEWAY_ID_ENDPOINT,
2732                None,
2733            )
2734            .await?;
2735        Ok(())
2736    }
2737
2738    async fn pay_invoice(
2739        &self,
2740        gateway: LightningGateway,
2741        payload: PayInvoicePayload,
2742    ) -> Result<String, GatewayPayError> {
2743        let preimage: String = self
2744            .api
2745            .request(
2746                &gateway.api,
2747                Method::POST,
2748                PAY_INVOICE_ENDPOINT,
2749                Some(payload),
2750            )
2751            .await
2752            .map_err(|e| GatewayPayError::GatewayInternalError {
2753                error_code: None,
2754                error_message: e.to_string(),
2755            })?;
2756        let length = preimage.len();
2757        Ok(preimage[1..length - 1].to_string())
2758    }
2759}
2760
2761#[derive(Debug)]
2762pub struct MockGatewayConnection;
2763
2764#[apply(async_trait_maybe_send!)]
2765impl GatewayConnection for MockGatewayConnection {
2766    async fn verify_gateway_availability(
2767        &self,
2768        _gateway: &LightningGateway,
2769    ) -> Result<(), ServerError> {
2770        Ok(())
2771    }
2772
2773    async fn pay_invoice(
2774        &self,
2775        _gateway: LightningGateway,
2776        _payload: PayInvoicePayload,
2777    ) -> Result<String, GatewayPayError> {
2778        // Just return a fake preimage to indicate success
2779        Ok("00000000".to_string())
2780    }
2781}