Skip to main content

fedimint_lnv2_client/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::must_use_candidate)]
6
7pub use fedimint_lnv2_common as common;
8
9mod api;
10#[cfg(feature = "cli")]
11mod cli;
12pub mod db;
13pub mod events;
14pub mod htlc;
15mod receive_sm;
16mod send_sm;
17
18use std::collections::{BTreeMap, BTreeSet};
19use std::sync::Arc;
20
21use async_stream::stream;
22use bitcoin::hashes::{Hash, sha256};
23use bitcoin::secp256k1;
24use db::{DbKeyPrefix, GatewayKey, IncomingContractStreamIndexKey};
25use fedimint_api_client::api::DynModuleApi;
26use fedimint_client_module::error::{
27    ClientModuleError, InsufficientBalanceError, OperationLookupError, TransactionSubmitError,
28};
29use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
30use fedimint_client_module::module::recovery::NoModuleBackup;
31use fedimint_client_module::module::{ClientContext, ClientModule, OutPointRange};
32use fedimint_client_module::oplog::UpdateStreamOrOutcome;
33use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
34use fedimint_client_module::transaction::{
35    ClientOutput, ClientOutputBundle, ClientOutputSM, FeeQuote, FeeQuoteRequest,
36    TransactionBuilder, max_affordable_send_amount,
37};
38use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
39use fedimint_core::config::FederationId;
40use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
41use fedimint_core::db::{DatabaseTransaction, IDatabaseTransactionOpsCoreTyped};
42use fedimint_core::encoding::{Decodable, Encodable};
43use fedimint_core::module::{
44    Amounts, ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
45};
46use fedimint_core::secp256k1::SECP256K1;
47use fedimint_core::task::TaskGroup;
48use fedimint_core::time::duration_since_epoch;
49use fedimint_core::util::{FmtCompact as _, SafeUrl};
50use fedimint_core::{Amount, PeerId, apply, async_trait_maybe_send, runtime};
51use fedimint_derive_secret::{ChildId, DerivableSecret};
52use fedimint_lnv2_common::config::LightningClientConfig;
53use fedimint_lnv2_common::contracts::{IncomingContract, OutgoingContract, PaymentImage};
54use fedimint_lnv2_common::gateway_api::{
55    GatewayConnection, MAX_INVOICE_EXPIRY_SECS, PaymentFee, RealGatewayConnection, RoutingInfo,
56};
57use fedimint_lnv2_common::{
58    Bolt11InvoiceDescription, GatewayApi, KIND, LightningCommonInit, LightningInvoice,
59    LightningModuleTypes, LightningOutput, LightningOutputV0, lnurl, tweak,
60};
61use fedimint_logging::LOG_CLIENT_MODULE_LNV2;
62use futures::StreamExt;
63use lightning_invoice::{Bolt11Invoice, Currency};
64use secp256k1::{Keypair, Scalar, SecretKey, ecdh};
65use serde::{Deserialize, Serialize};
66use serde_json::Value;
67use strum::IntoEnumIterator as _;
68use thiserror::Error;
69use tpe::{AggregateDecryptionKey, AggregatePublicKey, derive_agg_dk};
70use tracing::warn;
71
72use crate::api::LightningFederationApi;
73use crate::events::SendPaymentEvent;
74use crate::htlc::{CreateHtlcOperationMeta, SpendHtlcOperationMeta};
75use crate::receive_sm::{ReceiveSMCommon, ReceiveSMState, ReceiveStateMachine};
76use crate::send_sm::{SendSMCommon, SendSMState, SendStateMachine};
77
78/// Number of blocks until outgoing lightning contracts times out and user
79/// client can refund it unilaterally
80const EXPIRATION_DELTA_LIMIT: u64 = 1440;
81
82/// A two hour buffer in case either the client or gateway go offline
83const CONTRACT_CONFIRMATION_BUFFER: u64 = 12;
84
85#[allow(clippy::large_enum_variant)]
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub enum LightningOperationMeta {
88    Send(SendOperationMeta),
89    Receive(ReceiveOperationMeta),
90    LnurlReceive(LnurlReceiveOperationMeta),
91    CreateHtlc(CreateHtlcOperationMeta),
92    ClaimHtlc(SpendHtlcOperationMeta),
93    RefundHtlc(SpendHtlcOperationMeta),
94    CancelHtlc(SpendHtlcOperationMeta),
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct SendOperationMeta {
99    pub change_outpoint_range: OutPointRange,
100    pub gateway: SafeUrl,
101    pub contract: OutgoingContract,
102    pub invoice: LightningInvoice,
103    pub custom_meta: Value,
104}
105
106impl SendOperationMeta {
107    /// Calculate the absolute fee paid to the gateway on success.
108    pub fn gateway_fee(&self) -> Amount {
109        match &self.invoice {
110            LightningInvoice::Bolt11(invoice) => self.contract.amount.saturating_sub(
111                Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount")),
112            ),
113        }
114    }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct ReceiveOperationMeta {
119    pub gateway: SafeUrl,
120    pub contract: IncomingContract,
121    pub invoice: LightningInvoice,
122    pub custom_meta: Value,
123}
124
125impl ReceiveOperationMeta {
126    /// Calculate the absolute fee paid to the gateway on success.
127    pub fn gateway_fee(&self) -> Amount {
128        match &self.invoice {
129            LightningInvoice::Bolt11(invoice) => {
130                Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount"))
131                    .saturating_sub(self.contract.commitment.amount)
132            }
133        }
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct LnurlReceiveOperationMeta {
139    pub contract: IncomingContract,
140    pub custom_meta: Value,
141}
142
143/// The state of an operation sending a payment over lightning.
144///
145/// ```mermaid
146/// graph LR
147/// classDef virtual fill:#fff,stroke-dasharray: 5 5
148///
149///     Funding -- funding transaction is rejected --> Rejected
150///     Funding -- funding transaction is accepted --> Funded
151///     Funded -- payment is confirmed  --> Success
152///     Funded -- payment attempt expires --> Refunding
153///     Funded -- gateway cancels payment attempt --> Refunding
154///     Refunding -- payment is confirmed --> Success
155///     Refunding -- ecash is minted --> Refunded
156///     Refunding -- minting ecash fails --> Failure
157/// ```
158/// The transition from Refunding to Success is only possible if the gateway
159/// misbehaves.
160#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
161pub enum SendOperationState {
162    /// We are funding the contract to incentivize the gateway.
163    Funding,
164    /// We are waiting for the gateway to complete the payment.
165    Funded,
166    /// The payment was successful.
167    Success([u8; 32]),
168    /// The payment has failed and we are refunding the contract.
169    Refunding,
170    /// The payment has been refunded.
171    Refunded,
172    /// Either a programming error has occurred or the federation is malicious.
173    Failure,
174}
175
176/// The final state of an operation sending a payment over lightning.
177#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
178pub enum FinalSendOperationState {
179    /// The payment was successful. Carries the payment preimage proving
180    /// the gateway settled the invoice, serialized as a lowercase hex string.
181    Success(#[serde(with = "fedimint_core::hex::serde")] [u8; 32]),
182    /// The payment has been refunded.
183    Refunded,
184    /// Either a programming error has occurred or the federation is malicious.
185    Failure,
186}
187
188pub type SendResult = Result<OperationId, SendPaymentError>;
189
190/// The state of an operation receiving a payment over lightning.
191///
192/// ```mermaid
193/// graph LR
194/// classDef virtual fill:#fff,stroke-dasharray: 5 5
195///
196///     Pending -- payment is confirmed --> Claiming
197///     Pending -- invoice expires --> Expired
198///     Claiming -- ecash is minted --> Claimed
199///     Claiming -- minting ecash fails --> Failure
200/// ```
201#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
202pub enum ReceiveOperationState {
203    /// We are waiting for the payment.
204    Pending,
205    /// The payment request has expired.
206    Expired,
207    /// The payment has been confirmed and we are issuing the ecash.
208    Claiming,
209    /// The payment has been successful.
210    Claimed,
211    /// Either a programming error has occurred or the federation is malicious.
212    Failure,
213    /// The contract is worth less than the federation charges to claim it, so
214    /// it was left unclaimed. Reachable without any payment request of ours:
215    /// anyone can address an incoming contract to a published lnurl key.
216    Uneconomical,
217}
218
219/// The final state of an operation receiving a payment over lightning.
220#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
221pub enum FinalReceiveOperationState {
222    /// The payment request has expired.
223    Expired,
224    /// The payment has been successful.
225    Claimed,
226    /// Either a programming error has occurred or the federation is malicious.
227    Failure,
228    /// The contract is worth less than the federation charges to claim it, so
229    /// it was left unclaimed.
230    Uneconomical,
231}
232
233pub type ReceiveResult = Result<(Bolt11Invoice, OperationId), ReceiveError>;
234
235#[derive(Clone)]
236pub struct LightningClientInit {
237    pub gateway_conn: Option<Arc<dyn GatewayConnection + Send + Sync>>,
238    pub custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
239}
240
241impl std::fmt::Debug for LightningClientInit {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        f.debug_struct("LightningClientInit")
244            .field("gateway_conn", &self.gateway_conn)
245            .field("custom_meta_fn", &"<function>")
246            .finish()
247    }
248}
249
250impl Default for LightningClientInit {
251    fn default() -> Self {
252        LightningClientInit {
253            gateway_conn: None,
254            custom_meta_fn: Arc::new(|| Value::Null),
255        }
256    }
257}
258
259impl ModuleInit for LightningClientInit {
260    type Common = LightningCommonInit;
261
262    async fn dump_database(
263        &self,
264        _dbtx: &mut DatabaseTransaction<'_>,
265        _prefix_names: Vec<String>,
266    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
267        Box::new(BTreeMap::new().into_iter())
268    }
269}
270
271#[apply(async_trait_maybe_send!)]
272impl ClientModuleInit for LightningClientInit {
273    type Module = LightningClientModule;
274
275    fn supported_api_versions(&self) -> MultiApiVersion {
276        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
277            .expect("no version conflicts")
278    }
279
280    async fn init(
281        &self,
282        args: &ClientModuleInitArgs<Self>,
283    ) -> Result<Self::Module, ClientModuleError> {
284        let gateway_conn = if let Some(gateway_conn) = self.gateway_conn.clone() {
285            gateway_conn
286        } else {
287            let api = GatewayApi::new(None, args.connector_registry.clone());
288            Arc::new(RealGatewayConnection { api })
289        };
290        Ok(LightningClientModule::new(
291            *args.federation_id(),
292            args.cfg().clone(),
293            args.notifier().clone(),
294            args.context(),
295            args.module_api().clone(),
296            args.module_root_secret(),
297            gateway_conn,
298            self.custom_meta_fn.clone(),
299            args.admin_auth().cloned(),
300            args.task_group(),
301            args.client_span(),
302        ))
303    }
304
305    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
306        Some(
307            DbKeyPrefix::iter()
308                .map(|p| p as u8)
309                .chain(
310                    DbKeyPrefix::ExternalReservedStart as u8
311                        ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
312                )
313                .collect(),
314        )
315    }
316}
317
318#[derive(Debug, Clone)]
319pub struct LightningClientContext {
320    federation_id: FederationId,
321    gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
322    pub(crate) client_ctx: ClientContext<LightningClientModule>,
323}
324
325impl Context for LightningClientContext {
326    const KIND: Option<ModuleKind> = Some(KIND);
327}
328
329#[derive(Debug, Clone)]
330pub struct LightningClientModule {
331    federation_id: FederationId,
332    cfg: LightningClientConfig,
333    notifier: ModuleNotifier<LightningClientStateMachines>,
334    client_ctx: ClientContext<Self>,
335    module_api: DynModuleApi,
336    keypair: Keypair,
337    lnurl_keypair: Keypair,
338    gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
339    #[allow(unused)] // The field is only used by the cli feature
340    admin_auth: Option<ApiAuth>,
341}
342
343#[apply(async_trait_maybe_send!)]
344impl ClientModule for LightningClientModule {
345    type Init = LightningClientInit;
346    type Common = LightningModuleTypes;
347    type Backup = NoModuleBackup;
348    type ModuleStateMachineContext = LightningClientContext;
349    type States = LightningClientStateMachines;
350
351    fn context(&self) -> Self::ModuleStateMachineContext {
352        LightningClientContext {
353            federation_id: self.federation_id,
354            gateway_conn: self.gateway_conn.clone(),
355            client_ctx: self.client_ctx.clone(),
356        }
357    }
358
359    fn input_fee(
360        &self,
361        amounts: &Amounts,
362        _input: &<Self::Common as ModuleCommon>::Input,
363    ) -> Option<Amounts> {
364        Some(Amounts::new_bitcoin(
365            self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
366        ))
367    }
368
369    fn output_fee(
370        &self,
371        amounts: &Amounts,
372        _output: &<Self::Common as ModuleCommon>::Output,
373    ) -> Option<Amounts> {
374        Some(Amounts::new_bitcoin(
375            self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
376        ))
377    }
378
379    #[cfg(feature = "cli")]
380    async fn handle_cli_command(
381        &self,
382        args: &[std::ffi::OsString],
383    ) -> Result<serde_json::Value, ClientModuleError> {
384        cli::handle_cli_command(self, args)
385            .await
386            .map_err(ClientModuleError::other)
387    }
388}
389
390impl LightningClientModule {
391    #[allow(clippy::too_many_arguments)]
392    fn new(
393        federation_id: FederationId,
394        cfg: LightningClientConfig,
395        notifier: ModuleNotifier<LightningClientStateMachines>,
396        client_ctx: ClientContext<Self>,
397        module_api: DynModuleApi,
398        module_root_secret: &DerivableSecret,
399        gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
400        custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
401        admin_auth: Option<ApiAuth>,
402        task_group: &TaskGroup,
403        client_span: &tracing::Span,
404    ) -> Self {
405        let module = Self {
406            federation_id,
407            cfg,
408            notifier,
409            client_ctx,
410            module_api,
411            keypair: module_root_secret
412                .child_key(ChildId(0))
413                .to_secp_key(SECP256K1),
414            lnurl_keypair: module_root_secret
415                .child_key(ChildId(1))
416                .to_secp_key(SECP256K1),
417            gateway_conn,
418            admin_auth,
419        };
420
421        module.spawn_receive_lnurl_task(custom_meta_fn, task_group, client_span);
422
423        module.spawn_gateway_map_update_task(task_group, client_span);
424
425        module
426    }
427
428    fn spawn_gateway_map_update_task(&self, task_group: &TaskGroup, client_span: &tracing::Span) {
429        let module = self.clone();
430        let api = self.module_api.clone();
431
432        task_group.spawn_cancellable_with_span(
433            client_span.clone(),
434            "gateway_map_update_task",
435            async move {
436                api.wait_for_initialized_connections().await;
437                module.update_gateway_map().await;
438            },
439        );
440    }
441
442    async fn update_gateway_map(&self) {
443        // Update the mapping from lightning node public keys to gateway api
444        // endpoints maintained in the module database. When paying an invoice this
445        // enables the client to select the gateway that has created the invoice,
446        // if possible, such that the payment does not go over lightning, reducing
447        // fees and latency.
448
449        if let Ok(gateways) = self.module_api.gateways().await {
450            let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
451
452            for gateway in gateways {
453                if let Ok(Some(routing_info)) = self
454                    .gateway_conn
455                    .routing_info(gateway.clone(), &self.federation_id)
456                    .await
457                {
458                    dbtx.insert_entry(&GatewayKey(routing_info.lightning_public_key), &gateway)
459                        .await;
460                }
461            }
462
463            if let Err(e) = dbtx.commit_tx_result().await {
464                warn!("Failed to commit the updated gateway mapping to the database: {e}");
465            }
466        }
467    }
468
469    /// Selects an available gateway by querying the federation's registered
470    /// gateways, checking if one of them match the invoice's payee public
471    /// key, then queries the gateway for `RoutingInfo` to determine if it is
472    /// online.
473    pub async fn select_gateway(
474        &self,
475        invoice: Option<Bolt11Invoice>,
476    ) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
477        let gateways = self
478            .module_api
479            .gateways()
480            .await
481            .map_err(|e| SelectGatewayError::FailedToRequestGateways(e.to_string()))?;
482
483        if gateways.is_empty() {
484            return Err(SelectGatewayError::NoGatewaysAvailable);
485        }
486
487        if let Some(invoice) = invoice
488            && let Some(gateway) = self
489                .client_ctx
490                .module_db()
491                .begin_transaction_nc()
492                .await
493                .get_value(&GatewayKey(invoice.recover_payee_pub_key()))
494                .await
495                .filter(|gateway| gateways.contains(gateway))
496            && let Ok(Some(routing_info)) = self.routing_info(&gateway).await
497        {
498            return Ok((gateway, routing_info));
499        }
500
501        for gateway in gateways {
502            if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
503                return Ok((gateway, routing_info));
504            }
505        }
506
507        Err(SelectGatewayError::GatewaysUnresponsive)
508    }
509
510    /// Sends a request to each peer for their registered gateway list and
511    /// returns a `Vec<SafeUrl` of all registered gateways to the client.
512    pub async fn list_gateways(
513        &self,
514        peer: Option<PeerId>,
515    ) -> Result<Vec<SafeUrl>, ListGatewaysError> {
516        if let Some(peer) = peer {
517            self.module_api
518                .gateways_from_peer(peer)
519                .await
520                .map_err(|_| ListGatewaysError::FailedToListGateways)
521        } else {
522            self.module_api
523                .gateways()
524                .await
525                .map_err(|_| ListGatewaysError::FailedToListGateways)
526        }
527    }
528
529    /// Requests the `RoutingInfo`, including fee information, from the gateway
530    /// available at the `SafeUrl`.
531    pub async fn routing_info(
532        &self,
533        gateway: &SafeUrl,
534    ) -> Result<Option<RoutingInfo>, RoutingInfoError> {
535        self.gateway_conn
536            .routing_info(gateway.clone(), &self.federation_id)
537            .await
538            .map_err(|_| RoutingInfoError::FailedToRequestRoutingInfo)
539    }
540
541    /// Pay an invoice. For testing you can optionally specify a gateway to
542    /// route with, otherwise a gateway will be selected automatically. If the
543    /// invoice was created by a gateway connected to our federation, the same
544    /// gateway will be selected to allow for a direct ecash swap. Otherwise we
545    /// select a random online gateway.
546    ///
547    /// The fee for this payment may depend on the selected gateway but
548    /// will be limited to one and a half percent plus one hundred satoshis.
549    /// This fee accounts for the fee charged by the gateway as well as
550    /// the additional fee required to reliably route this payment over
551    /// lightning if necessary. Since the gateway has been vetted by at least
552    /// one guardian we trust it to set a reasonable fee and only enforce a
553    /// rather high limit.
554    ///
555    /// The absolute fee for a payment can be calculated from the operation meta
556    /// to be shown to the user in the transaction history.
557    pub async fn send(
558        &self,
559        invoice: Bolt11Invoice,
560        gateway: Option<SafeUrl>,
561        custom_meta: Value,
562    ) -> Result<OperationId, SendPaymentError> {
563        let (amount, operation_id) = self.validate_send_invoice(&invoice).await?;
564
565        let (gateway_api, routing_info) = self.resolve_send_gateway(&invoice, gateway).await?;
566
567        self.fund_outgoing_contract(
568            invoice,
569            amount,
570            operation_id,
571            gateway_api,
572            routing_info,
573            custom_meta,
574        )
575        .await
576    }
577
578    /// Pays `invoice` through `gateway` at the terms the caller already
579    /// checked.
580    ///
581    /// [`Self::send`] reads the gateway's terms itself, so a caller that showed
582    /// the user a fee and asked for approval has no guarantee the funded
583    /// contract carries that fee: the gateway may change its schedule between
584    /// the two reads. This variant takes the `send_fee` and `expiration_delta`
585    /// the caller obtained from [`RoutingInfo::send_parameters`] and refuses
586    /// with [`SendWithTermsError::TermsChanged`], before funding anything, if
587    /// the gateway currently reports different terms. A cheaper fee counts
588    /// as changed too, so the funded contract always matches what was
589    /// approved. This binds the funded contract to the approved terms, not
590    /// the gateway's acceptance: a gateway that raises its schedule after the
591    /// check can still refuse the contract, in which case the payment is
592    /// refunded. The error carries the current terms for re-quoting.
593    pub async fn send_with_terms(
594        &self,
595        invoice: Bolt11Invoice,
596        gateway: SafeUrl,
597        send_fee: PaymentFee,
598        expiration_delta: u64,
599        custom_meta: Value,
600    ) -> Result<OperationId, SendWithTermsError> {
601        let (amount, operation_id) = self.validate_send_invoice(&invoice).await?;
602
603        let (gateway_api, routing_info) =
604            self.resolve_send_gateway(&invoice, Some(gateway)).await?;
605
606        let current = routing_info.send_parameters(&invoice);
607
608        if current != (send_fee, expiration_delta) {
609            return Err(SendWithTermsError::TermsChanged {
610                send_fee: current.0,
611                expiration_delta: current.1,
612            });
613        }
614
615        let operation_id = self
616            .fund_outgoing_contract(
617                invoice,
618                amount,
619                operation_id,
620                gateway_api,
621                routing_info,
622                custom_meta,
623            )
624            .await?;
625
626        Ok(operation_id)
627    }
628
629    /// Checks that `invoice` is payable by this client and has not been
630    /// attempted before, returning its amount in millisatoshis and the
631    /// operation id a payment of it uses.
632    async fn validate_send_invoice(
633        &self,
634        invoice: &Bolt11Invoice,
635    ) -> Result<(u64, OperationId), SendPaymentError> {
636        let amount = invoice
637            .amount_milli_satoshis()
638            .ok_or(SendPaymentError::InvoiceMissingAmount)?;
639
640        if invoice.is_expired() {
641            return Err(SendPaymentError::InvoiceExpired);
642        }
643
644        if self.cfg.network != invoice.currency().into() {
645            return Err(SendPaymentError::WrongCurrency {
646                invoice_currency: invoice.currency(),
647                federation_currency: self.cfg.network.into(),
648            });
649        }
650
651        // The attempt index is fixed at `0` so the operation id matches the one
652        // older clients derived for the first payment attempt, ensuring an
653        // already-paid or in-flight invoice is still detected after an upgrade.
654        let operation_id = OperationId::from_encodable(&(invoice.clone(), 0u64));
655
656        if self.client_ctx.operation_exists(operation_id).await {
657            return Err(SendPaymentError::DuplicatePaymentAttempt(operation_id));
658        }
659
660        Ok((amount, operation_id))
661    }
662
663    /// Resolves the gateway to pay `invoice` through and its current routing
664    /// info: the given one, or an automatically selected one when `None`.
665    async fn resolve_send_gateway(
666        &self,
667        invoice: &Bolt11Invoice,
668        gateway: Option<SafeUrl>,
669    ) -> Result<(SafeUrl, RoutingInfo), SendPaymentError> {
670        match gateway {
671            Some(gateway_api) => Ok((
672                gateway_api.clone(),
673                self.routing_info(&gateway_api)
674                    .await
675                    .map_err(|e| SendPaymentError::FailedToConnectToGateway(e.to_string()))?
676                    .ok_or(SendPaymentError::FederationNotSupported)?,
677            )),
678            None => self
679                .select_gateway(Some(invoice.clone()))
680                .await
681                .map_err(SendPaymentError::SelectGateway),
682        }
683    }
684
685    /// Funds an outgoing contract for `invoice` at the terms in `routing_info`
686    /// and starts the state machine that hands it to `gateway_api`.
687    #[allow(clippy::too_many_lines)]
688    async fn fund_outgoing_contract(
689        &self,
690        invoice: Bolt11Invoice,
691        amount: u64,
692        operation_id: OperationId,
693        gateway_api: SafeUrl,
694        routing_info: RoutingInfo,
695        custom_meta: Value,
696    ) -> Result<OperationId, SendPaymentError> {
697        let (ephemeral_tweak, ephemeral_pk) = tweak::generate(self.keypair.public_key());
698
699        let refund_keypair = SecretKey::from_slice(&ephemeral_tweak)
700            .expect("32 bytes, within curve order")
701            .keypair(secp256k1::SECP256K1);
702
703        let (send_fee, expiration_delta) = routing_info.send_parameters(&invoice);
704
705        if !send_fee.is_within(&PaymentFee::SEND_FEE_LIMIT) {
706            return Err(SendPaymentError::GatewayFeeExceedsLimit);
707        }
708
709        if EXPIRATION_DELTA_LIMIT < expiration_delta {
710            return Err(SendPaymentError::GatewayExpirationExceedsLimit);
711        }
712
713        let consensus_block_count = self
714            .module_api
715            .consensus_block_count()
716            .await
717            .map_err(|e| SendPaymentError::FailedToRequestBlockCount(e.to_string()))?;
718
719        let contract = OutgoingContract {
720            payment_image: PaymentImage::Hash(*invoice.payment_hash()),
721            amount: send_fee.add_to(amount),
722            expiration: consensus_block_count + expiration_delta + CONTRACT_CONFIRMATION_BUFFER,
723            claim_pk: routing_info.module_public_key,
724            refund_pk: refund_keypair.public_key(),
725            ephemeral_pk,
726        };
727
728        let contract_clone = contract.clone();
729        let gateway_api_clone = gateway_api.clone();
730        let invoice_clone = invoice.clone();
731
732        let client_output = ClientOutput::<LightningOutput> {
733            output: LightningOutput::V0(LightningOutputV0::Outgoing(contract.clone())),
734            amounts: Amounts::new_bitcoin(contract.amount),
735        };
736
737        let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
738            state_machines: Arc::new(move |range: OutPointRange| {
739                vec![LightningClientStateMachines::Send(SendStateMachine {
740                    common: SendSMCommon {
741                        operation_id,
742                        outpoint: range.into_iter().next().unwrap(),
743                        contract: contract_clone.clone(),
744                        gateway_api: Some(gateway_api_clone.clone()),
745                        invoice: Some(LightningInvoice::Bolt11(invoice_clone.clone())),
746                        refund_keypair,
747                    },
748                    state: SendSMState::Funding,
749                })]
750            }),
751        };
752
753        let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
754            vec![client_output],
755            vec![client_output_sm],
756        ));
757
758        let transaction = TransactionBuilder::new().with_outputs(client_output);
759
760        self.client_ctx
761            .finalize_and_submit_transaction(
762                operation_id,
763                LightningCommonInit::KIND.as_str(),
764                move |change_outpoint_range| {
765                    LightningOperationMeta::Send(SendOperationMeta {
766                        change_outpoint_range,
767                        gateway: gateway_api.clone(),
768                        contract: contract.clone(),
769                        invoice: LightningInvoice::Bolt11(invoice.clone()),
770                        custom_meta: custom_meta.clone(),
771                    })
772                },
773                transaction,
774            )
775            .await
776            .map_err(|e| match e {
777                TransactionSubmitError::InsufficientFunds(e) => {
778                    SendPaymentError::InsufficientFunds(e)
779                }
780                e => SendPaymentError::FailedToFundPayment(e.fmt_compact().to_string()),
781            })?;
782
783        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
784
785        self.client_ctx
786            .log_event(
787                &mut dbtx,
788                SendPaymentEvent {
789                    operation_id,
790                    amount: Amount::from_msats(amount),
791                    fee: send_fee.fee(amount),
792                },
793            )
794            .await;
795
796        dbtx.commit_tx().await;
797
798        Ok(operation_id)
799    }
800
801    /// The status of a previous [`Self::send`] for this invoice.
802    ///
803    /// Callers deciding whether an invoice is safe to pay through another
804    /// mechanism must treat anything but [`InvoiceSendStatus::NotAttempted`]
805    /// as a potential double payment.
806    pub async fn get_invoice_send_status(
807        &self,
808        invoice: &Bolt11Invoice,
809    ) -> Result<InvoiceSendStatus, OperationLookupError> {
810        // Send only creates attempt index 0 nowadays, but older clients
811        // allocated a fresh index per retry, so scan for the latest attempt.
812        // No new attempt was ever allocated after a success, so the latest
813        // attempt alone determines the status.
814        let mut last_op = None;
815
816        for attempt in 0_u64.. {
817            let operation_id = OperationId::from_encodable(&(invoice.clone(), attempt));
818
819            if !self.client_ctx.operation_exists(operation_id).await {
820                break;
821            }
822
823            last_op = Some(operation_id);
824        }
825
826        let Some(operation_id) = last_op else {
827            return Ok(InvoiceSendStatus::NotAttempted);
828        };
829
830        if self.client_ctx.has_active_states(operation_id).await {
831            return Ok(InvoiceSendStatus::InFlight(operation_id));
832        }
833
834        // The operation is finished; replaying its (already terminated) update
835        // stream yields the cached outcome without blocking.
836        let mut stream = self
837            .subscribe_send_operation_state_updates(operation_id)
838            .await?
839            .into_stream();
840
841        while let Some(state) = stream.next().await {
842            if let SendOperationState::Success(_) = state {
843                return Ok(InvoiceSendStatus::Succeeded(operation_id));
844            }
845        }
846
847        Ok(InvoiceSendStatus::Failed(operation_id))
848    }
849
850    /// Subscribe to all state updates of the send operation.
851    pub async fn subscribe_send_operation_state_updates(
852        &self,
853        operation_id: OperationId,
854    ) -> Result<UpdateStreamOrOutcome<SendOperationState>, OperationLookupError> {
855        let operation = self.client_ctx.get_operation(operation_id).await?;
856        let mut stream = self.notifier.subscribe(operation_id).await;
857        let client_ctx = self.client_ctx.clone();
858        let module_api = self.module_api.clone();
859
860        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
861                SendOperationState::Funding
862                | SendOperationState::Funded
863                | SendOperationState::Refunding => false,
864                SendOperationState::Success(_)
865                | SendOperationState::Refunded
866                | SendOperationState::Failure => true,
867            }, move || {
868            stream! {
869                loop {
870                    if let Some(LightningClientStateMachines::Send(state)) = stream.next().await {
871                        match state.state {
872                            SendSMState::Funding => yield SendOperationState::Funding,
873                            SendSMState::Funded => yield SendOperationState::Funded,
874                            SendSMState::Success(preimage) => {
875                                // the preimage has been verified by the state machine previously
876                                assert!(state.common.contract.verify_preimage(&preimage));
877
878                                yield SendOperationState::Success(preimage);
879                                return;
880                            },
881                            SendSMState::Refunding(out_points) => {
882                                yield SendOperationState::Refunding;
883
884                                if client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await.is_ok() {
885                                    yield SendOperationState::Refunded;
886                                    return;
887                                }
888
889                                // The gateway may have incorrectly claimed the outgoing contract thereby causing
890                                // our refund transaction to be rejected. Therefore, we check one last time if
891                                // the preimage is available before we enter the failure state.
892                                if let Some(preimage) = module_api.await_preimage(
893                                    state.common.outpoint,
894                                    0
895                                ).await
896                                    && state.common.contract.verify_preimage(&preimage) {
897                                        yield SendOperationState::Success(preimage);
898                                        return;
899                                    }
900
901                                yield SendOperationState::Failure;
902                                return;
903                            },
904                            SendSMState::Rejected(..) => {
905                                yield SendOperationState::Failure;
906                                return;
907                            },
908                        }
909                    }
910                }
911            }
912        }))
913    }
914
915    /// Await the final state of the send operation.
916    pub async fn await_final_send_operation_state(
917        &self,
918        operation_id: OperationId,
919    ) -> Result<FinalSendOperationState, OperationLookupError> {
920        let mut stream = self
921            .subscribe_send_operation_state_updates(operation_id)
922            .await?
923            .into_stream();
924
925        let mut final_state = None;
926
927        while let Some(state) = stream.next().await {
928            match state {
929                SendOperationState::Success(preimage) => {
930                    final_state = Some(FinalSendOperationState::Success(preimage));
931                }
932                SendOperationState::Refunded => {
933                    final_state = Some(FinalSendOperationState::Refunded);
934                }
935                SendOperationState::Failure => final_state = Some(FinalSendOperationState::Failure),
936                _ => {}
937            }
938        }
939
940        Ok(final_state.expect("Stream contains one final state"))
941    }
942
943    /// Request an invoice. For testing you can optionally specify a gateway to
944    /// generate the invoice, otherwise a random online gateway will be selected
945    /// automatically.
946    ///
947    /// The total fee for this payment may depend on the chosen gateway but
948    /// will be limited to half of one percent plus fifty satoshis. Since the
949    /// selected gateway has been vetted by at least one guardian we trust it to
950    /// set a reasonable fee and only enforce a rather high limit.
951    ///
952    /// The absolute fee for a payment can be calculated from the operation meta
953    /// to be shown to the user in the transaction history.
954    pub async fn receive(
955        &self,
956        amount: Amount,
957        expiry_secs: u32,
958        description: Bolt11InvoiceDescription,
959        gateway: Option<SafeUrl>,
960        custom_meta: Value,
961    ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
962        if expiry_secs > MAX_INVOICE_EXPIRY_SECS {
963            return Err(ReceiveError::InvoiceExpiryTooLong);
964        }
965
966        let (gateway, routing_info) = self.resolve_receive_gateway(gateway).await?;
967
968        self.receive_with_routing_info(
969            amount,
970            expiry_secs,
971            description,
972            gateway,
973            routing_info,
974            custom_meta,
975        )
976        .await
977    }
978
979    /// Requests an invoice from `gateway` at the receive fee the caller
980    /// already checked.
981    ///
982    /// [`Self::receive`] reads the gateway's fee itself, so a caller that
983    /// showed the user a fee has no guarantee the incoming contract is
984    /// created at that fee: the gateway may change its schedule between the
985    /// two reads. This variant takes the `receive_fee` the caller obtained
986    /// from [`RoutingInfo::receive_fee`] and refuses with
987    /// [`ReceiveWithTermsError::TermsChanged`], before creating anything, if
988    /// the gateway currently reports a different fee. A cheaper fee counts as
989    /// changed too, so the contract always matches what was approved; the
990    /// error carries the current fee for re-quoting.
991    pub async fn receive_with_terms(
992        &self,
993        amount: Amount,
994        expiry_secs: u32,
995        description: Bolt11InvoiceDescription,
996        gateway: SafeUrl,
997        receive_fee: PaymentFee,
998        custom_meta: Value,
999    ) -> Result<(Bolt11Invoice, OperationId), ReceiveWithTermsError> {
1000        if expiry_secs > MAX_INVOICE_EXPIRY_SECS {
1001            return Err(ReceiveError::InvoiceExpiryTooLong.into());
1002        }
1003
1004        let (gateway, routing_info) = self.resolve_receive_gateway(Some(gateway)).await?;
1005
1006        if routing_info.receive_fee != receive_fee {
1007            return Err(ReceiveWithTermsError::TermsChanged {
1008                receive_fee: routing_info.receive_fee,
1009            });
1010        }
1011
1012        let (invoice, operation_id) = self
1013            .receive_with_routing_info(
1014                amount,
1015                expiry_secs,
1016                description,
1017                gateway,
1018                routing_info,
1019                custom_meta,
1020            )
1021            .await?;
1022
1023        Ok((invoice, operation_id))
1024    }
1025
1026    /// Resolves the gateway to request an invoice from and its current
1027    /// routing info: the given one, or an automatically selected one when
1028    /// `None`.
1029    async fn resolve_receive_gateway(
1030        &self,
1031        gateway: Option<SafeUrl>,
1032    ) -> Result<(SafeUrl, RoutingInfo), ReceiveError> {
1033        match gateway {
1034            Some(gateway) => {
1035                let routing_info = self
1036                    .routing_info(&gateway)
1037                    .await
1038                    .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?
1039                    .ok_or(ReceiveError::FederationNotSupported)?;
1040
1041                if !routing_info.receive_enabled {
1042                    return Err(ReceiveError::ReceiveDisabled);
1043                }
1044
1045                Ok((gateway, routing_info))
1046            }
1047            None => self
1048                .select_receive_gateway()
1049                .await
1050                .map_err(ReceiveError::SelectGateway),
1051        }
1052    }
1053
1054    /// Selects the first registered gateway that is online and currently
1055    /// accepts incoming payments for this federation. A gateway that answers
1056    /// but has turned receives off is skipped rather than handed an invoice
1057    /// request it would refuse.
1058    async fn select_receive_gateway(&self) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
1059        let gateways = self
1060            .module_api
1061            .gateways()
1062            .await
1063            .map_err(|e| SelectGatewayError::FailedToRequestGateways(e.to_string()))?;
1064
1065        if gateways.is_empty() {
1066            return Err(SelectGatewayError::NoGatewaysAvailable);
1067        }
1068
1069        let mut any_responded = false;
1070
1071        for gateway in gateways {
1072            if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
1073                any_responded = true;
1074
1075                if routing_info.receive_enabled {
1076                    return Ok((gateway, routing_info));
1077                }
1078            }
1079        }
1080
1081        if any_responded {
1082            Err(SelectGatewayError::NoGatewayAcceptsReceives)
1083        } else {
1084            Err(SelectGatewayError::GatewaysUnresponsive)
1085        }
1086    }
1087
1088    /// Creates the incoming contract locked to a public key derived from our
1089    /// static module public key, fetches its invoice from `gateway` at the
1090    /// terms in `routing_info`, then starts the receive operation.
1091    async fn receive_with_routing_info(
1092        &self,
1093        amount: Amount,
1094        expiry_secs: u32,
1095        description: Bolt11InvoiceDescription,
1096        gateway: SafeUrl,
1097        routing_info: RoutingInfo,
1098        custom_meta: Value,
1099    ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
1100        let recipient_static_pk = self.keypair.public_key();
1101
1102        let (ephemeral_tweak, ephemeral_pk) = tweak::generate(recipient_static_pk);
1103
1104        let encryption_seed = ephemeral_tweak
1105            .consensus_hash::<sha256::Hash>()
1106            .to_byte_array();
1107
1108        let preimage = encryption_seed
1109            .consensus_hash::<sha256::Hash>()
1110            .to_byte_array();
1111
1112        if !routing_info
1113            .receive_fee
1114            .is_within(&PaymentFee::RECEIVE_FEE_LIMIT)
1115        {
1116            return Err(ReceiveError::GatewayFeeExceedsLimit);
1117        }
1118
1119        let contract_amount = routing_info.receive_fee.subtract_from(amount.msats);
1120
1121        // Quoting the claim against this federation's fee consensus is exact, where a
1122        // fixed floor is either too permissive or too strict depending on how the
1123        // federation is configured.
1124        if !self.is_worth_claiming(contract_amount).await {
1125            return Err(ReceiveError::AmountTooSmall);
1126        }
1127
1128        let expiration = duration_since_epoch()
1129            .as_secs()
1130            .saturating_add(u64::from(expiry_secs));
1131
1132        let claim_pk = recipient_static_pk
1133            .mul_tweak(
1134                secp256k1::SECP256K1,
1135                &Scalar::from_be_bytes(ephemeral_tweak).expect("Within curve order"),
1136            )
1137            .expect("Tweak is valid");
1138
1139        let contract = IncomingContract::new(
1140            self.cfg.tpe_agg_pk,
1141            encryption_seed,
1142            preimage,
1143            PaymentImage::Hash(preimage.consensus_hash()),
1144            contract_amount,
1145            expiration,
1146            claim_pk,
1147            routing_info.module_public_key,
1148            ephemeral_pk,
1149        );
1150
1151        let invoice = self
1152            .gateway_conn
1153            .bolt11_invoice(
1154                gateway.clone(),
1155                self.federation_id,
1156                contract.clone(),
1157                amount,
1158                description,
1159                expiry_secs,
1160            )
1161            .await
1162            .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?;
1163
1164        if invoice.payment_hash() != &preimage.consensus_hash() {
1165            return Err(ReceiveError::InvalidInvoice);
1166        }
1167
1168        if invoice.amount_milli_satoshis() != Some(amount.msats) {
1169            return Err(ReceiveError::IncorrectInvoiceAmount);
1170        }
1171
1172        let operation_id = self
1173            .receive_incoming_contract(
1174                self.keypair.secret_key(),
1175                contract.clone(),
1176                LightningOperationMeta::Receive(ReceiveOperationMeta {
1177                    gateway,
1178                    contract,
1179                    invoice: LightningInvoice::Bolt11(invoice.clone()),
1180                    custom_meta,
1181                }),
1182            )
1183            .await
1184            .expect("The contract has been generated with our public key");
1185
1186        Ok((invoice, operation_id))
1187    }
1188
1189    /// Computes the federation fee a `receive` of `amount` would incur, without
1190    /// submitting anything.
1191    ///
1192    /// When the incoming contract is claimed, the client submits a transaction
1193    /// with a single Lightning input worth the contract amount; the primary
1194    /// module balances it by minting the change credited to the wallet. This
1195    /// quotes the fee of that transaction — the Lightning input fee, the mint
1196    /// output fees, and any sub-denomination dust — via the shared,
1197    /// module-agnostic fee quote.
1198    ///
1199    /// The gateway's off-chain Lightning fee is deliberately excluded: this is
1200    /// only the fee of the on-federation transaction. For that reason the quote
1201    /// is taken on `amount` directly (rather than the gateway-reduced contract
1202    /// amount), and no gateway round-trip is needed.
1203    pub async fn receive_fee_quote(
1204        &self,
1205        amount: Amount,
1206    ) -> Result<FeeQuote, TransactionSubmitError> {
1207        self.client_ctx
1208            .fee_quote(
1209                OperationId::new_random(),
1210                FeeQuoteRequest {
1211                    input_amount: Amounts::new_bitcoin(amount),
1212                    output_amount: Amounts::ZERO,
1213                    input_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
1214                    output_fee: Amounts::ZERO,
1215                },
1216            )
1217            .await
1218    }
1219
1220    /// Whether an incoming contract worth `amount` is worth claiming, i.e.
1221    /// whether claiming it would leave the wallet better off than leaving it.
1222    ///
1223    /// This is the computed counterpart of a fixed dust limit: it prices the
1224    /// actual claim - the lightning input fee at this federation's fee
1225    /// consensus, the change the primary module has to mint, and the
1226    /// sub-denomination remainder that cannot be minted at all - rather than
1227    /// assuming a floor. A quote that fails outright is the same verdict: the
1228    /// claim spends the contract as the transaction's only input, so a fee
1229    /// larger than the contract leaves a transaction that cannot be balanced.
1230    async fn is_worth_claiming(&self, amount: Amount) -> bool {
1231        match self.receive_fee_quote(amount).await {
1232            Ok(quote) => quote.total().get_bitcoin() < amount,
1233            Err(_) => false,
1234        }
1235    }
1236
1237    /// Computes the federation fee a `send` funding an outgoing contract worth
1238    /// `amount` would incur, without submitting anything.
1239    ///
1240    /// When a payment is sent, the client submits a transaction with a single
1241    /// Lightning output (the outgoing contract) worth `amount`; the primary
1242    /// module balances it by spending ecash to fund the contract and minting
1243    /// any change. This quotes the fee of that transaction — the Lightning
1244    /// output fee, the mint input fees on the funding notes, any mint change
1245    /// output fees, and sub-denomination dust — via the shared, module-agnostic
1246    /// fee quote.
1247    ///
1248    /// The gateway's off-chain Lightning fee is deliberately excluded: it is
1249    /// part of the contract `amount` the gateway claims, not the on-federation
1250    /// transaction fee. So `amount` is the full outgoing contract value
1251    /// (`send_fee.add_to(invoice_amount)`).
1252    pub async fn send_fee_quote(&self, amount: Amount) -> Result<FeeQuote, TransactionSubmitError> {
1253        self.client_ctx
1254            .fee_quote(
1255                OperationId::new_random(),
1256                FeeQuoteRequest {
1257                    input_amount: Amounts::ZERO,
1258                    output_amount: Amounts::new_bitcoin(amount),
1259                    input_fee: Amounts::ZERO,
1260                    output_fee: Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)),
1261                },
1262            )
1263            .await
1264    }
1265
1266    /// Computes the largest invoice amount the client can pay in full out of
1267    /// `balance`, i.e. the amount to request an invoice for in order to spend
1268    /// (close to) the entire balance.
1269    ///
1270    /// Paying an invoice deducts two kinds of fee from the balance:
1271    /// - the *gateway* fee, which is added on top of the invoice amount to form
1272    ///   the outgoing contract (`send_fee.add_to(invoice_amount)`), and
1273    /// - the *federation* fee of funding that contract — the Lightning output
1274    ///   fee, the mint input fees on the funding notes, the mint output fees on
1275    ///   any change, and sub-denomination dust — as quoted by
1276    ///   [`Self::send_fee_quote`].
1277    ///
1278    /// `balance` is the client's current Bitcoin balance (e.g. from
1279    /// `Client::get_balance_for_btc`). `gateway` optionally pins the gateway
1280    /// whose fee schedule to use; if `None` one is selected automatically, the
1281    /// same way [`Self::send`] does when no gateway is given. The gateway's
1282    /// *default* send fee is used — the higher of its two send fees, applied to
1283    /// a Lightning swap rather than a direct fedimint-to-fedimint swap — so the
1284    /// returned amount stays payable even when the eventual invoice is routed
1285    /// over Lightning.
1286    ///
1287    /// The maximum payable amount is found by binary search over the real fee
1288    /// quote (see [`max_affordable_send_amount`]) rather than a closed form,
1289    /// because the federation fee is stepwise in the amount. The quote is
1290    /// point-in-time and moves with the balance, exactly like
1291    /// [`Self::send_fee_quote`]; the eventual [`Self::send`] remains the source
1292    /// of truth and may still fail if balance or gateway state changes in
1293    /// between.
1294    ///
1295    /// Returns [`SpendableAmountError::BalanceTooLow`] if the balance cannot
1296    /// cover even the smallest payable amount plus fees, and
1297    /// [`SpendableAmountError::Quote`] if the fee probe itself failed; only
1298    /// the first of the two says the wallet is short. Any LNURL
1299    /// `minSendable`/`maxSendable` bounds are the caller's responsibility to
1300    /// apply. The remaining variants of [`SpendableAmountError`] are
1301    /// gateway-side, and none of them means the wallet is short.
1302    pub async fn spendable_amount(
1303        &self,
1304        balance: Amount,
1305        gateway: Option<SafeUrl>,
1306    ) -> Result<Amount, SpendableAmountError> {
1307        let routing_info = match gateway {
1308            Some(gateway) => self
1309                .routing_info(&gateway)
1310                .await?
1311                .ok_or(SpendableAmountError::FederationNotSupported)?,
1312            None => self.select_gateway(None).await?.1,
1313        };
1314
1315        // The default (Lightning-swap) send fee is the higher of the gateway's
1316        // two send fees, so using it keeps the result payable even if the
1317        // eventual invoice is routed over Lightning instead of settled by a
1318        // direct swap.
1319        let send_fee = routing_info.send_fee_default;
1320
1321        if !send_fee.is_within(&PaymentFee::SEND_FEE_LIMIT) {
1322            return Err(SpendableAmountError::SendFeeExceedsLimit {
1323                fee: send_fee,
1324                limit: PaymentFee::SEND_FEE_LIMIT,
1325            });
1326        }
1327
1328        max_affordable_send_amount(
1329            balance,
1330            Amount::from_msats(1),
1331            balance,
1332            |invoice_amount: Amount| send_fee.add_to(invoice_amount.msats),
1333            |contract_amount: Amount| self.send_fee_quote(contract_amount),
1334        )
1335        .await
1336        .map_err(SpendableAmountError::Quote)?
1337        .ok_or(SpendableAmountError::BalanceTooLow { balance })
1338    }
1339
1340    // Receive an incoming contract locked to a public key derived from our
1341    // static module public key.
1342    async fn receive_incoming_contract(
1343        &self,
1344        sk: SecretKey,
1345        contract: IncomingContract,
1346        operation_meta: LightningOperationMeta,
1347    ) -> Option<OperationId> {
1348        let operation_id = OperationId::from_encodable(&contract.clone());
1349
1350        let tpe_agg_pk = self.cfg.tpe_agg_pk;
1351
1352        let (claim_keypair, agg_decryption_key) = runtime::spawn_blocking({
1353            let contract = contract.clone();
1354
1355            move || recover_contract_keys(sk, &tpe_agg_pk, &contract)
1356        })
1357        .await?;
1358
1359        let receive_sm = LightningClientStateMachines::Receive(ReceiveStateMachine {
1360            common: ReceiveSMCommon {
1361                operation_id,
1362                contract: contract.clone(),
1363                claim_keypair,
1364                agg_decryption_key,
1365            },
1366            state: ReceiveSMState::Pending,
1367        });
1368
1369        // this may only fail if the operation id is already in use, in which case we
1370        // ignore the error such that the method is idempotent
1371        self.client_ctx
1372            .manual_operation_start(
1373                operation_id,
1374                LightningCommonInit::KIND.as_str(),
1375                operation_meta,
1376                vec![self.client_ctx.make_dyn_state(receive_sm)],
1377            )
1378            .await
1379            .ok();
1380
1381        Some(operation_id)
1382    }
1383
1384    /// Subscribe to all state updates of the receive operation.
1385    pub async fn subscribe_receive_operation_state_updates(
1386        &self,
1387        operation_id: OperationId,
1388    ) -> Result<UpdateStreamOrOutcome<ReceiveOperationState>, OperationLookupError> {
1389        let operation = self.client_ctx.get_operation(operation_id).await?;
1390        let mut stream = self.notifier.subscribe(operation_id).await;
1391        let client_ctx = self.client_ctx.clone();
1392
1393        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
1394                ReceiveOperationState::Pending | ReceiveOperationState::Claiming => false,
1395                ReceiveOperationState::Expired
1396                | ReceiveOperationState::Claimed
1397                | ReceiveOperationState::Failure
1398                | ReceiveOperationState::Uneconomical => true,
1399            }, move || {
1400            stream! {
1401                loop {
1402                    if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
1403                        match state.state {
1404                            ReceiveSMState::Pending => yield ReceiveOperationState::Pending,
1405                            ReceiveSMState::Claiming(out_points) => {
1406                                yield ReceiveOperationState::Claiming;
1407
1408                                if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
1409                                    yield ReceiveOperationState::Claimed;
1410                                } else {
1411                                    yield ReceiveOperationState::Failure;
1412                                }
1413                                return;
1414                            },
1415                            ReceiveSMState::Expired => {
1416                                yield ReceiveOperationState::Expired;
1417                                return;
1418                            }
1419                            ReceiveSMState::Uneconomical => {
1420                                yield ReceiveOperationState::Uneconomical;
1421                                return;
1422                            }
1423                        }
1424                    }
1425                }
1426            }
1427        }))
1428    }
1429
1430    /// Await the final state of the receive operation.
1431    pub async fn await_final_receive_operation_state(
1432        &self,
1433        operation_id: OperationId,
1434    ) -> Result<FinalReceiveOperationState, OperationLookupError> {
1435        let mut stream = self
1436            .subscribe_receive_operation_state_updates(operation_id)
1437            .await?
1438            .into_stream();
1439
1440        let mut final_state = None;
1441
1442        while let Some(state) = stream.next().await {
1443            match state {
1444                ReceiveOperationState::Expired => {
1445                    final_state = Some(FinalReceiveOperationState::Expired);
1446                }
1447                ReceiveOperationState::Claimed => {
1448                    final_state = Some(FinalReceiveOperationState::Claimed);
1449                }
1450                ReceiveOperationState::Failure => {
1451                    final_state = Some(FinalReceiveOperationState::Failure);
1452                }
1453                ReceiveOperationState::Uneconomical => {
1454                    final_state = Some(FinalReceiveOperationState::Uneconomical);
1455                }
1456                _ => {}
1457            }
1458        }
1459
1460        Ok(final_state.expect("Stream contains one final state"))
1461    }
1462
1463    /// Generate an lnurl for the client. You can optionally specify a gateway
1464    /// to use for testing purposes.
1465    pub async fn generate_lnurl(
1466        &self,
1467        recurringd: SafeUrl,
1468        gateway: Option<SafeUrl>,
1469    ) -> Result<String, GenerateLnurlError> {
1470        let gateways = if let Some(gateway) = gateway {
1471            vec![gateway]
1472        } else {
1473            let gateways = self
1474                .module_api
1475                .gateways()
1476                .await
1477                .map_err(|e| GenerateLnurlError::FailedToRequestGateways(e.to_string()))?;
1478
1479            if gateways.is_empty() {
1480                return Err(GenerateLnurlError::NoGatewaysAvailable);
1481            }
1482
1483            gateways
1484        };
1485
1486        let payload = fedimint_core::base32::encode_prefixed(
1487            fedimint_core::base32::FEDIMINT_PREFIX,
1488            &lnurl::LnurlRequest {
1489                federation_id: self.federation_id,
1490                recipient_pk: self.lnurl_keypair.public_key(),
1491                aggregate_pk: self.cfg.tpe_agg_pk,
1492                gateways,
1493            },
1494        );
1495
1496        Ok(fedimint_lnurl::encode_lnurl(&format!(
1497            "{recurringd}pay/{payload}"
1498        )))
1499    }
1500
1501    fn spawn_receive_lnurl_task(
1502        &self,
1503        custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
1504        task_group: &TaskGroup,
1505        client_span: &tracing::Span,
1506    ) {
1507        let module = self.clone();
1508        let api = self.module_api.clone();
1509
1510        task_group.spawn_cancellable_with_span(
1511            client_span.clone(),
1512            "receive_lnurl_task",
1513            async move {
1514                api.wait_for_initialized_connections().await;
1515                loop {
1516                    module.receive_lnurl(custom_meta_fn()).await;
1517                }
1518            },
1519        );
1520    }
1521
1522    async fn receive_lnurl(&self, custom_meta: Value) {
1523        // Read the stream cursor with a short-lived transaction. It must NOT stay open
1524        // across the long-poll below: RocksDB's optimistic transactions validate a
1525        // commit against bounded memtable history, so a transaction held open
1526        // for minutes fails with `DatabaseError::SnapshotTooOld` once enough
1527        // concurrent writes flush that history — not because anything conflicted,
1528        // but because the backend can no longer tell — and the panicking
1529        // `commit_tx()` then killed this task permanently, silently
1530        // stalling every future receive for the lifetime of the process. Long-lived
1531        // clients (daemons) hit this reproducibly under concurrent lnv2 activity.
1532        // See <https://github.com/fedimint/fedimint/issues/8872>.
1533        let stream_index = self
1534            .client_ctx
1535            .module_db()
1536            .begin_transaction_nc()
1537            .await
1538            .get_value(&IncomingContractStreamIndexKey)
1539            .await
1540            .unwrap_or(0);
1541
1542        let (contracts, next_index) = self
1543            .module_api
1544            .await_incoming_contracts(stream_index, 128)
1545            .await;
1546
1547        for contract in &contracts {
1548            // The stream carries every incoming contract the federation funded, and
1549            // anyone can address one to a published lnurl key. Check that it is ours
1550            // before quoting - recovering the keys is local arithmetic, the quote
1551            // reads the wallet - and skip the contracts the claim fee would swallow,
1552            // so that unsolicited dust never becomes an operation in the first place.
1553            let keys = runtime::spawn_blocking({
1554                let sk = self.lnurl_keypair.secret_key();
1555                let tpe_agg_pk = self.cfg.tpe_agg_pk;
1556                let contract = contract.clone();
1557
1558                move || recover_contract_keys(sk, &tpe_agg_pk, &contract)
1559            })
1560            .await;
1561
1562            if keys.is_none() {
1563                continue;
1564            }
1565
1566            if !self.is_worth_claiming(contract.commitment.amount).await {
1567                warn!(
1568                    target: LOG_CLIENT_MODULE_LNV2,
1569                    amount = %contract.commitment.amount,
1570                    "Ignoring incoming contract, its amount does not cover the claim fee"
1571                );
1572
1573                continue;
1574            }
1575
1576            if let Some(operation_id) = self
1577                .receive_incoming_contract(
1578                    self.lnurl_keypair.secret_key(),
1579                    contract.clone(),
1580                    LightningOperationMeta::LnurlReceive(LnurlReceiveOperationMeta {
1581                        contract: contract.clone(),
1582                        custom_meta: custom_meta.clone(),
1583                    }),
1584                )
1585                .await
1586            {
1587                self.await_final_receive_operation_state(operation_id)
1588                    .await
1589                    .ok();
1590            }
1591        }
1592
1593        // Advance the cursor in its own short transaction. This is the only writer of
1594        // this key and it runs in a single sequential loop, so there is no concurrent
1595        // writer to guard against; and because this transaction is short-lived — opened
1596        // after the long-poll and committed immediately — it cannot hit the
1597        // `SnapshotTooOld` failure that a transaction held open across the long-poll
1598        // would, so a plain `commit_tx()` is safe. Ordering is unchanged: the cursor
1599        // only moves after the batch above was processed, and a crash in between
1600        // re-fetches the same batch on the next iteration.
1601        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1602
1603        dbtx.insert_entry(&IncomingContractStreamIndexKey, &next_index)
1604            .await;
1605
1606        dbtx.commit_tx().await;
1607    }
1608}
1609
1610#[derive(Error, Debug, Clone, Eq, PartialEq)]
1611pub enum SelectGatewayError {
1612    #[error("Failed to request gateways")]
1613    FailedToRequestGateways(String),
1614    #[error("No gateways are available")]
1615    NoGatewaysAvailable,
1616    #[error("All gateways failed to respond")]
1617    GatewaysUnresponsive,
1618    #[error("No online gateway currently accepts incoming payments for this federation")]
1619    NoGatewayAcceptsReceives,
1620}
1621
1622/// The status of the latest send attempt for an invoice, derived from the
1623/// operation log.
1624#[derive(Debug, Clone, Eq, PartialEq)]
1625pub enum InvoiceSendStatus {
1626    /// No payment operation exists for this invoice.
1627    NotAttempted,
1628    /// A payment operation is still being driven to completion.
1629    InFlight(OperationId),
1630    /// The payment succeeded; paying the invoice again would be a double
1631    /// payment.
1632    Succeeded(OperationId),
1633    /// The payment failed and any funds were refunded.
1634    Failed(OperationId),
1635}
1636
1637#[derive(Error, Debug, Clone, Eq, PartialEq)]
1638pub enum SendPaymentError {
1639    #[error("Invoice is missing an amount")]
1640    InvoiceMissingAmount,
1641    #[error("Invoice has expired")]
1642    InvoiceExpired,
1643    #[error("Payment attempt is duplicate")]
1644    DuplicatePaymentAttempt(OperationId),
1645    #[error(transparent)]
1646    SelectGateway(SelectGatewayError),
1647    #[error("Failed to connect to gateway")]
1648    FailedToConnectToGateway(String),
1649    #[error("Gateway does not support this federation")]
1650    FederationNotSupported,
1651    #[error("Gateway fee exceeds the allowed limit")]
1652    GatewayFeeExceedsLimit,
1653    #[error("Gateway expiration time exceeds the allowed limit")]
1654    GatewayExpirationExceedsLimit,
1655    #[error("Failed to request block count")]
1656    FailedToRequestBlockCount(String),
1657    /// The client's balance cannot fund the payment and its fees.
1658    #[error("The client does not have sufficient funds to send the payment")]
1659    InsufficientFunds(#[source] InsufficientBalanceError),
1660    /// The funding transaction could not be submitted for a reason that is
1661    /// not about the balance.
1662    #[error("Failed to fund the payment")]
1663    FailedToFundPayment(String),
1664    #[error("Invoice is for a different currency")]
1665    WrongCurrency {
1666        invoice_currency: Currency,
1667        federation_currency: Currency,
1668    },
1669}
1670
1671/// A failure of [`LightningClientModule::send_with_terms`].
1672#[derive(Error, Debug, Clone, Eq, PartialEq)]
1673#[non_exhaustive]
1674pub enum SendWithTermsError {
1675    /// The gateway's current send terms differ from the ones the caller
1676    /// checked. Carries the current terms so the caller can re-quote.
1677    #[error("Gateway's send terms changed since they were checked")]
1678    TermsChanged {
1679        send_fee: PaymentFee,
1680        expiration_delta: u64,
1681    },
1682    /// Any failure [`LightningClientModule::send`] can report.
1683    #[error(transparent)]
1684    Send(#[from] SendPaymentError),
1685}
1686
1687#[derive(Error, Debug, Clone, Eq, PartialEq)]
1688pub enum ReceiveError {
1689    #[error(transparent)]
1690    SelectGateway(SelectGatewayError),
1691    #[error("Failed to connect to gateway")]
1692    FailedToConnectToGateway(String),
1693    #[error("Gateway does not support this federation")]
1694    FederationNotSupported,
1695    #[error("Gateway does not currently accept incoming payments for this federation")]
1696    ReceiveDisabled,
1697    #[error("Gateway fee exceeds the allowed limit")]
1698    GatewayFeeExceedsLimit,
1699    #[error("Amount is too small to cover fees")]
1700    AmountTooSmall,
1701    #[error("Gateway returned an invalid invoice")]
1702    InvalidInvoice,
1703    #[error("Gateway returned an invoice with incorrect amount")]
1704    IncorrectInvoiceAmount,
1705    #[error("Requested invoice expiry exceeds the maximum of one day")]
1706    InvoiceExpiryTooLong,
1707}
1708
1709/// A failure of [`LightningClientModule::receive_with_terms`].
1710#[derive(Error, Debug, Clone, Eq, PartialEq)]
1711#[non_exhaustive]
1712pub enum ReceiveWithTermsError {
1713    /// The gateway's current receive fee differs from the one the caller
1714    /// checked. Carries the current fee so the caller can re-quote.
1715    #[error("Gateway's receive fee changed since it was checked")]
1716    TermsChanged { receive_fee: PaymentFee },
1717    /// Any failure [`LightningClientModule::receive`] can report.
1718    #[error(transparent)]
1719    Receive(#[from] ReceiveError),
1720}
1721
1722#[derive(Error, Debug, Clone, Eq, PartialEq)]
1723pub enum GenerateLnurlError {
1724    #[error("No gateways are available")]
1725    NoGatewaysAvailable,
1726    #[error("Failed to request gateways")]
1727    FailedToRequestGateways(String),
1728}
1729
1730#[derive(Error, Debug, Clone, Eq, PartialEq)]
1731pub enum ListGatewaysError {
1732    #[error("Failed to request gateways")]
1733    FailedToListGateways,
1734}
1735
1736#[derive(Error, Debug, Clone, Eq, PartialEq)]
1737pub enum RoutingInfoError {
1738    #[error("Failed to request routing info")]
1739    FailedToRequestRoutingInfo,
1740}
1741
1742/// A failure to work out the largest invoice amount the client could pay in
1743/// full.
1744///
1745/// The answer depends on a gateway's fee schedule as well as on the balance,
1746/// so it can be missing either because no schedule could be obtained or
1747/// because the balance does not reach the smallest payable amount once both
1748/// fees are applied.
1749#[derive(Debug, Error)]
1750#[non_exhaustive]
1751pub enum SpendableAmountError {
1752    /// The caller named a gateway and its routing information could not be
1753    /// fetched, so there is no fee schedule to compute against.
1754    #[error(transparent)]
1755    RoutingInfo(#[from] RoutingInfoError),
1756
1757    /// The caller named a gateway and it answered, but it does not serve this
1758    /// federation.
1759    #[error("The gateway does not support this federation")]
1760    FederationNotSupported,
1761
1762    /// The caller named no gateway and none could be picked automatically, so
1763    /// there is no fee schedule to compute against.
1764    #[error(transparent)]
1765    SelectGateway(#[from] SelectGatewayError),
1766
1767    /// The gateway's default send fee is above the limit this module accepts,
1768    /// so an amount computed against it would not be payable through it.
1769    #[error("The gateway's default send fee {fee} exceeds the limit {limit}")]
1770    SendFeeExceedsLimit {
1771        /// The default send fee the gateway announced.
1772        fee: PaymentFee,
1773        /// The largest default send fee this module accepts.
1774        limit: PaymentFee,
1775    },
1776
1777    /// The fee probe failed for a reason unrelated to the balance.
1778    #[error("The fee quote for the payment failed")]
1779    Quote(#[source] TransactionSubmitError),
1780
1781    /// The balance cannot cover the smallest payable amount plus the gateway
1782    /// and federation fees, so there is no amount to send.
1783    #[error("The balance {balance} is too low to send any amount after fees")]
1784    BalanceTooLow {
1785        /// The balance the answer was computed against.
1786        balance: Amount,
1787    },
1788}
1789
1790#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1791pub enum LightningClientStateMachines {
1792    Send(SendStateMachine),
1793    Receive(ReceiveStateMachine),
1794}
1795
1796impl IntoDynInstance for LightningClientStateMachines {
1797    type DynType = DynState;
1798
1799    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1800        DynState::from_typed(instance_id, self)
1801    }
1802}
1803
1804impl State for LightningClientStateMachines {
1805    type ModuleContext = LightningClientContext;
1806
1807    fn transitions(
1808        &self,
1809        context: &Self::ModuleContext,
1810        global_context: &DynGlobalClientContext,
1811    ) -> Vec<StateTransition<Self>> {
1812        match self {
1813            LightningClientStateMachines::Send(state) => {
1814                sm_enum_variant_translation!(
1815                    state.transitions(context, global_context),
1816                    LightningClientStateMachines::Send
1817                )
1818            }
1819            LightningClientStateMachines::Receive(state) => {
1820                sm_enum_variant_translation!(
1821                    state.transitions(context, global_context),
1822                    LightningClientStateMachines::Receive
1823                )
1824            }
1825        }
1826    }
1827
1828    fn operation_id(&self) -> OperationId {
1829        match self {
1830            LightningClientStateMachines::Send(state) => state.operation_id(),
1831            LightningClientStateMachines::Receive(state) => state.operation_id(),
1832        }
1833    }
1834}
1835
1836/// The claim keypair and aggregate decryption key of an incoming contract
1837/// addressed to `sk`, or none when it is not ours.
1838///
1839/// Verifying the key is four pairings, so callers run this off the async
1840/// workers.
1841fn recover_contract_keys(
1842    sk: SecretKey,
1843    tpe_agg_pk: &AggregatePublicKey,
1844    contract: &IncomingContract,
1845) -> Option<(Keypair, AggregateDecryptionKey)> {
1846    let tweak = ecdh::SharedSecret::new(&contract.commitment.ephemeral_pk, &sk);
1847
1848    let encryption_seed = tweak
1849        .secret_bytes()
1850        .consensus_hash::<sha256::Hash>()
1851        .to_byte_array();
1852
1853    let claim_keypair = sk
1854        .mul_tweak(&Scalar::from_be_bytes(tweak.secret_bytes()).expect("Within curve order"))
1855        .expect("Tweak is valid")
1856        .keypair(secp256k1::SECP256K1);
1857
1858    if claim_keypair.public_key() != contract.commitment.claim_pk {
1859        return None; // The claim key is not derived from our pk
1860    }
1861
1862    let agg_decryption_key = derive_agg_dk(tpe_agg_pk, &encryption_seed);
1863
1864    if !contract.verify_agg_decryption_key(tpe_agg_pk, &agg_decryption_key) {
1865        return None; // The decryption key is not derived from our pk
1866    }
1867
1868    contract.decrypt_preimage(&agg_decryption_key)?;
1869
1870    Some((claim_keypair, agg_decryption_key))
1871}