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;
12mod db;
13pub mod events;
14mod receive_sm;
15mod send_sm;
16
17use std::collections::{BTreeMap, BTreeSet};
18use std::sync::Arc;
19
20use async_stream::stream;
21use bitcoin::hashes::{Hash, sha256};
22use bitcoin::secp256k1;
23use db::{DbKeyPrefix, GatewayKey, IncomingContractStreamIndexKey};
24use fedimint_api_client::api::DynModuleApi;
25use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
26use fedimint_client_module::module::recovery::NoModuleBackup;
27use fedimint_client_module::module::{ClientContext, ClientModule, OutPointRange};
28use fedimint_client_module::oplog::UpdateStreamOrOutcome;
29use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
30use fedimint_client_module::transaction::{
31    ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
32};
33use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
34use fedimint_core::config::FederationId;
35use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
36use fedimint_core::db::{DatabaseTransaction, IDatabaseTransactionOpsCoreTyped};
37use fedimint_core::encoding::{Decodable, Encodable};
38use fedimint_core::module::{
39    Amounts, ApiAuth, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
40};
41use fedimint_core::secp256k1::SECP256K1;
42use fedimint_core::task::TaskGroup;
43use fedimint_core::time::duration_since_epoch;
44use fedimint_core::util::SafeUrl;
45use fedimint_core::{Amount, PeerId, apply, async_trait_maybe_send};
46use fedimint_derive_secret::{ChildId, DerivableSecret};
47use fedimint_lnv2_common::config::LightningClientConfig;
48use fedimint_lnv2_common::contracts::{IncomingContract, OutgoingContract, PaymentImage};
49use fedimint_lnv2_common::gateway_api::{
50    GatewayConnection, PaymentFee, RealGatewayConnection, RoutingInfo,
51};
52use fedimint_lnv2_common::{
53    Bolt11InvoiceDescription, GatewayApi, KIND, LightningCommonInit, LightningInvoice,
54    LightningModuleTypes, LightningOutput, LightningOutputV0, MINIMUM_INCOMING_CONTRACT_AMOUNT,
55    lnurl, tweak,
56};
57use futures::StreamExt;
58use lightning_invoice::{Bolt11Invoice, Currency};
59use secp256k1::{Keypair, PublicKey, Scalar, SecretKey, ecdh};
60use serde::{Deserialize, Serialize};
61use serde_json::Value;
62use strum::IntoEnumIterator as _;
63use thiserror::Error;
64use tpe::{AggregateDecryptionKey, derive_agg_dk};
65use tracing::warn;
66
67use crate::api::LightningFederationApi;
68use crate::events::SendPaymentEvent;
69use crate::receive_sm::{ReceiveSMCommon, ReceiveSMState, ReceiveStateMachine};
70use crate::send_sm::{SendSMCommon, SendSMState, SendStateMachine};
71
72/// Number of blocks until outgoing lightning contracts times out and user
73/// client can refund it unilaterally
74const EXPIRATION_DELTA_LIMIT: u64 = 1440;
75
76/// A two hour buffer in case either the client or gateway go offline
77const CONTRACT_CONFIRMATION_BUFFER: u64 = 12;
78
79#[allow(clippy::large_enum_variant)]
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum LightningOperationMeta {
82    Send(SendOperationMeta),
83    Receive(ReceiveOperationMeta),
84    LnurlReceive(LnurlReceiveOperationMeta),
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SendOperationMeta {
89    pub change_outpoint_range: OutPointRange,
90    pub gateway: SafeUrl,
91    pub contract: OutgoingContract,
92    pub invoice: LightningInvoice,
93    pub custom_meta: Value,
94}
95
96impl SendOperationMeta {
97    /// Calculate the absolute fee paid to the gateway on success.
98    pub fn gateway_fee(&self) -> Amount {
99        match &self.invoice {
100            LightningInvoice::Bolt11(invoice) => self.contract.amount.saturating_sub(
101                Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount")),
102            ),
103        }
104    }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ReceiveOperationMeta {
109    pub gateway: SafeUrl,
110    pub contract: IncomingContract,
111    pub invoice: LightningInvoice,
112    pub custom_meta: Value,
113}
114
115impl ReceiveOperationMeta {
116    /// Calculate the absolute fee paid to the gateway on success.
117    pub fn gateway_fee(&self) -> Amount {
118        match &self.invoice {
119            LightningInvoice::Bolt11(invoice) => {
120                Amount::from_msats(invoice.amount_milli_satoshis().expect("Invoice has amount"))
121                    .saturating_sub(self.contract.commitment.amount)
122            }
123        }
124    }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct LnurlReceiveOperationMeta {
129    pub contract: IncomingContract,
130    pub custom_meta: Value,
131}
132
133#[cfg_attr(doc, aquamarine::aquamarine)]
134/// The state of an operation sending a payment over lightning.
135///
136/// ```mermaid
137/// graph LR
138/// classDef virtual fill:#fff,stroke-dasharray: 5 5
139///
140///     Funding -- funding transaction is rejected --> Rejected
141///     Funding -- funding transaction is accepted --> Funded
142///     Funded -- payment is confirmed  --> Success
143///     Funded -- payment attempt expires --> Refunding
144///     Funded -- gateway cancels payment attempt --> Refunding
145///     Refunding -- payment is confirmed --> Success
146///     Refunding -- ecash is minted --> Refunded
147///     Refunding -- minting ecash fails --> Failure
148/// ```
149/// The transition from Refunding to Success is only possible if the gateway
150/// misbehaves.
151#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
152pub enum SendOperationState {
153    /// We are funding the contract to incentivize the gateway.
154    Funding,
155    /// We are waiting for the gateway to complete the payment.
156    Funded,
157    /// The payment was successful.
158    Success([u8; 32]),
159    /// The payment has failed and we are refunding the contract.
160    Refunding,
161    /// The payment has been refunded.
162    Refunded,
163    /// Either a programming error has occurred or the federation is malicious.
164    Failure,
165}
166
167/// The final state of an operation sending a payment over lightning.
168#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
169pub enum FinalSendOperationState {
170    /// The payment was successful.
171    Success,
172    /// The payment has been refunded.
173    Refunded,
174    /// Either a programming error has occurred or the federation is malicious.
175    Failure,
176}
177
178pub type SendResult = Result<OperationId, SendPaymentError>;
179
180#[cfg_attr(doc, aquamarine::aquamarine)]
181/// The state of an operation receiving a payment over lightning.
182///
183/// ```mermaid
184/// graph LR
185/// classDef virtual fill:#fff,stroke-dasharray: 5 5
186///
187///     Pending -- payment is confirmed --> Claiming
188///     Pending -- invoice expires --> Expired
189///     Claiming -- ecash is minted --> Claimed
190///     Claiming -- minting ecash fails --> Failure
191/// ```
192#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
193pub enum ReceiveOperationState {
194    /// We are waiting for the payment.
195    Pending,
196    /// The payment request has expired.
197    Expired,
198    /// The payment has been confirmed and we are issuing the ecash.
199    Claiming,
200    /// The payment has been successful.
201    Claimed,
202    /// Either a programming error has occurred or the federation is malicious.
203    Failure,
204}
205
206/// The final state of an operation receiving a payment over lightning.
207#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
208pub enum FinalReceiveOperationState {
209    /// The payment request has expired.
210    Expired,
211    /// The payment has been successful.
212    Claimed,
213    /// Either a programming error has occurred or the federation is malicious.
214    Failure,
215}
216
217pub type ReceiveResult = Result<(Bolt11Invoice, OperationId), ReceiveError>;
218
219#[derive(Clone)]
220pub struct LightningClientInit {
221    pub gateway_conn: Option<Arc<dyn GatewayConnection + Send + Sync>>,
222    pub custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
223}
224
225impl std::fmt::Debug for LightningClientInit {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        f.debug_struct("LightningClientInit")
228            .field("gateway_conn", &self.gateway_conn)
229            .field("custom_meta_fn", &"<function>")
230            .finish()
231    }
232}
233
234impl Default for LightningClientInit {
235    fn default() -> Self {
236        LightningClientInit {
237            gateway_conn: None,
238            custom_meta_fn: Arc::new(|| Value::Null),
239        }
240    }
241}
242
243impl ModuleInit for LightningClientInit {
244    type Common = LightningCommonInit;
245
246    async fn dump_database(
247        &self,
248        _dbtx: &mut DatabaseTransaction<'_>,
249        _prefix_names: Vec<String>,
250    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
251        Box::new(BTreeMap::new().into_iter())
252    }
253}
254
255#[apply(async_trait_maybe_send!)]
256impl ClientModuleInit for LightningClientInit {
257    type Module = LightningClientModule;
258
259    fn supported_api_versions(&self) -> MultiApiVersion {
260        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
261            .expect("no version conflicts")
262    }
263
264    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
265        let gateway_conn = if let Some(gateway_conn) = self.gateway_conn.clone() {
266            gateway_conn
267        } else {
268            let api = GatewayApi::new(None, args.connector_registry.clone());
269            Arc::new(RealGatewayConnection { api })
270        };
271        Ok(LightningClientModule::new(
272            *args.federation_id(),
273            args.cfg().clone(),
274            args.notifier().clone(),
275            args.context(),
276            args.module_api().clone(),
277            args.module_root_secret(),
278            gateway_conn,
279            self.custom_meta_fn.clone(),
280            args.admin_auth().cloned(),
281            args.task_group(),
282        ))
283    }
284
285    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
286        Some(
287            DbKeyPrefix::iter()
288                .map(|p| p as u8)
289                .chain(
290                    DbKeyPrefix::ExternalReservedStart as u8
291                        ..=DbKeyPrefix::CoreInternalReservedEnd as u8,
292                )
293                .collect(),
294        )
295    }
296}
297
298#[derive(Debug, Clone)]
299pub struct LightningClientContext {
300    federation_id: FederationId,
301    gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
302    pub(crate) client_ctx: ClientContext<LightningClientModule>,
303}
304
305impl Context for LightningClientContext {
306    const KIND: Option<ModuleKind> = Some(KIND);
307}
308
309#[derive(Debug, Clone)]
310pub struct LightningClientModule {
311    federation_id: FederationId,
312    cfg: LightningClientConfig,
313    notifier: ModuleNotifier<LightningClientStateMachines>,
314    client_ctx: ClientContext<Self>,
315    module_api: DynModuleApi,
316    keypair: Keypair,
317    lnurl_keypair: Keypair,
318    gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
319    #[allow(unused)] // The field is only used by the cli feature
320    admin_auth: Option<ApiAuth>,
321}
322
323#[apply(async_trait_maybe_send!)]
324impl ClientModule for LightningClientModule {
325    type Init = LightningClientInit;
326    type Common = LightningModuleTypes;
327    type Backup = NoModuleBackup;
328    type ModuleStateMachineContext = LightningClientContext;
329    type States = LightningClientStateMachines;
330
331    fn context(&self) -> Self::ModuleStateMachineContext {
332        LightningClientContext {
333            federation_id: self.federation_id,
334            gateway_conn: self.gateway_conn.clone(),
335            client_ctx: self.client_ctx.clone(),
336        }
337    }
338
339    fn input_fee(
340        &self,
341        amounts: &Amounts,
342        _input: &<Self::Common as ModuleCommon>::Input,
343    ) -> Option<Amounts> {
344        Some(Amounts::new_bitcoin(
345            self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
346        ))
347    }
348
349    fn output_fee(
350        &self,
351        amounts: &Amounts,
352        _output: &<Self::Common as ModuleCommon>::Output,
353    ) -> Option<Amounts> {
354        Some(Amounts::new_bitcoin(
355            self.cfg.fee_consensus.fee(amounts.expect_only_bitcoin()),
356        ))
357    }
358
359    #[cfg(feature = "cli")]
360    async fn handle_cli_command(
361        &self,
362        args: &[std::ffi::OsString],
363    ) -> anyhow::Result<serde_json::Value> {
364        cli::handle_cli_command(self, args).await
365    }
366}
367
368impl LightningClientModule {
369    #[allow(clippy::too_many_arguments)]
370    fn new(
371        federation_id: FederationId,
372        cfg: LightningClientConfig,
373        notifier: ModuleNotifier<LightningClientStateMachines>,
374        client_ctx: ClientContext<Self>,
375        module_api: DynModuleApi,
376        module_root_secret: &DerivableSecret,
377        gateway_conn: Arc<dyn GatewayConnection + Send + Sync>,
378        custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
379        admin_auth: Option<ApiAuth>,
380        task_group: &TaskGroup,
381    ) -> Self {
382        let module = Self {
383            federation_id,
384            cfg,
385            notifier,
386            client_ctx,
387            module_api,
388            keypair: module_root_secret
389                .child_key(ChildId(0))
390                .to_secp_key(SECP256K1),
391            lnurl_keypair: module_root_secret
392                .child_key(ChildId(1))
393                .to_secp_key(SECP256K1),
394            gateway_conn,
395            admin_auth,
396        };
397
398        module.spawn_receive_lnurl_task(custom_meta_fn, task_group);
399
400        module.spawn_gateway_map_update_task(task_group);
401
402        module
403    }
404
405    fn spawn_gateway_map_update_task(&self, task_group: &TaskGroup) {
406        let module = self.clone();
407        let api = self.module_api.clone();
408
409        task_group.spawn_cancellable("gateway_map_update_task", async move {
410            api.wait_for_initialized_connections().await;
411            module.update_gateway_map().await;
412        });
413    }
414
415    async fn update_gateway_map(&self) {
416        // Update the mapping from lightning node public keys to gateway api
417        // endpoints maintained in the module database. When paying an invoice this
418        // enables the client to select the gateway that has created the invoice,
419        // if possible, such that the payment does not go over lightning, reducing
420        // fees and latency.
421
422        if let Ok(gateways) = self.module_api.gateways().await {
423            let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
424
425            for gateway in gateways {
426                if let Ok(Some(routing_info)) = self
427                    .gateway_conn
428                    .routing_info(gateway.clone(), &self.federation_id)
429                    .await
430                {
431                    dbtx.insert_entry(&GatewayKey(routing_info.lightning_public_key), &gateway)
432                        .await;
433                }
434            }
435
436            if let Err(e) = dbtx.commit_tx_result().await {
437                warn!("Failed to commit the updated gateway mapping to the database: {e}");
438            }
439        }
440    }
441
442    /// Selects an available gateway by querying the federation's registered
443    /// gateways, checking if one of them match the invoice's payee public
444    /// key, then queries the gateway for `RoutingInfo` to determine if it is
445    /// online.
446    pub async fn select_gateway(
447        &self,
448        invoice: Option<Bolt11Invoice>,
449    ) -> Result<(SafeUrl, RoutingInfo), SelectGatewayError> {
450        let gateways = self
451            .module_api
452            .gateways()
453            .await
454            .map_err(|e| SelectGatewayError::FailedToRequestGateways(e.to_string()))?;
455
456        if gateways.is_empty() {
457            return Err(SelectGatewayError::NoGatewaysAvailable);
458        }
459
460        if let Some(invoice) = invoice
461            && let Some(gateway) = self
462                .client_ctx
463                .module_db()
464                .begin_transaction_nc()
465                .await
466                .get_value(&GatewayKey(invoice.recover_payee_pub_key()))
467                .await
468                .filter(|gateway| gateways.contains(gateway))
469            && let Ok(Some(routing_info)) = self.routing_info(&gateway).await
470        {
471            return Ok((gateway, routing_info));
472        }
473
474        for gateway in gateways {
475            if let Ok(Some(routing_info)) = self.routing_info(&gateway).await {
476                return Ok((gateway, routing_info));
477            }
478        }
479
480        Err(SelectGatewayError::GatewaysUnresponsive)
481    }
482
483    /// Sends a request to each peer for their registered gateway list and
484    /// returns a `Vec<SafeUrl` of all registered gateways to the client.
485    pub async fn list_gateways(
486        &self,
487        peer: Option<PeerId>,
488    ) -> Result<Vec<SafeUrl>, ListGatewaysError> {
489        if let Some(peer) = peer {
490            self.module_api
491                .gateways_from_peer(peer)
492                .await
493                .map_err(|_| ListGatewaysError::FailedToListGateways)
494        } else {
495            self.module_api
496                .gateways()
497                .await
498                .map_err(|_| ListGatewaysError::FailedToListGateways)
499        }
500    }
501
502    /// Requests the `RoutingInfo`, including fee information, from the gateway
503    /// available at the `SafeUrl`.
504    pub async fn routing_info(
505        &self,
506        gateway: &SafeUrl,
507    ) -> Result<Option<RoutingInfo>, RoutingInfoError> {
508        self.gateway_conn
509            .routing_info(gateway.clone(), &self.federation_id)
510            .await
511            .map_err(|_| RoutingInfoError::FailedToRequestRoutingInfo)
512    }
513
514    /// Pay an invoice. For testing you can optionally specify a gateway to
515    /// route with, otherwise a gateway will be selected automatically. If the
516    /// invoice was created by a gateway connected to our federation, the same
517    /// gateway will be selected to allow for a direct ecash swap. Otherwise we
518    /// select a random online gateway.
519    ///
520    /// The fee for this payment may depend on the selected gateway but
521    /// will be limited to one and a half percent plus one hundred satoshis.
522    /// This fee accounts for the fee charged by the gateway as well as
523    /// the additional fee required to reliably route this payment over
524    /// lightning if necessary. Since the gateway has been vetted by at least
525    /// one guardian we trust it to set a reasonable fee and only enforce a
526    /// rather high limit.
527    ///
528    /// The absolute fee for a payment can be calculated from the operation meta
529    /// to be shown to the user in the transaction history.
530    #[allow(clippy::too_many_lines)]
531    pub async fn send(
532        &self,
533        invoice: Bolt11Invoice,
534        gateway: Option<SafeUrl>,
535        custom_meta: Value,
536    ) -> Result<OperationId, SendPaymentError> {
537        let amount = invoice
538            .amount_milli_satoshis()
539            .ok_or(SendPaymentError::InvoiceMissingAmount)?;
540
541        if invoice.is_expired() {
542            return Err(SendPaymentError::InvoiceExpired);
543        }
544
545        if self.cfg.network != invoice.currency().into() {
546            return Err(SendPaymentError::WrongCurrency {
547                invoice_currency: invoice.currency(),
548                federation_currency: self.cfg.network.into(),
549            });
550        }
551
552        let operation_id = self.get_next_operation_id(&invoice).await?;
553
554        let (ephemeral_tweak, ephemeral_pk) = tweak::generate(self.keypair.public_key());
555
556        let refund_keypair = SecretKey::from_slice(&ephemeral_tweak)
557            .expect("32 bytes, within curve order")
558            .keypair(secp256k1::SECP256K1);
559
560        let (gateway_api, routing_info) = match gateway {
561            Some(gateway_api) => (
562                gateway_api.clone(),
563                self.routing_info(&gateway_api)
564                    .await
565                    .map_err(|e| SendPaymentError::FailedToConnectToGateway(e.to_string()))?
566                    .ok_or(SendPaymentError::FederationNotSupported)?,
567            ),
568            None => self
569                .select_gateway(Some(invoice.clone()))
570                .await
571                .map_err(SendPaymentError::SelectGateway)?,
572        };
573
574        let (send_fee, expiration_delta) = routing_info.send_parameters(&invoice);
575
576        if !send_fee.le(&PaymentFee::SEND_FEE_LIMIT) {
577            return Err(SendPaymentError::GatewayFeeExceedsLimit);
578        }
579
580        if EXPIRATION_DELTA_LIMIT < expiration_delta {
581            return Err(SendPaymentError::GatewayExpirationExceedsLimit);
582        }
583
584        let consensus_block_count = self
585            .module_api
586            .consensus_block_count()
587            .await
588            .map_err(|e| SendPaymentError::FailedToRequestBlockCount(e.to_string()))?;
589
590        let contract = OutgoingContract {
591            payment_image: PaymentImage::Hash(*invoice.payment_hash()),
592            amount: send_fee.add_to(amount),
593            expiration: consensus_block_count + expiration_delta + CONTRACT_CONFIRMATION_BUFFER,
594            claim_pk: routing_info.module_public_key,
595            refund_pk: refund_keypair.public_key(),
596            ephemeral_pk,
597        };
598
599        let contract_clone = contract.clone();
600        let gateway_api_clone = gateway_api.clone();
601        let invoice_clone = invoice.clone();
602
603        let client_output = ClientOutput::<LightningOutput> {
604            output: LightningOutput::V0(LightningOutputV0::Outgoing(contract.clone())),
605            amounts: Amounts::new_bitcoin(contract.amount),
606        };
607
608        let client_output_sm = ClientOutputSM::<LightningClientStateMachines> {
609            state_machines: Arc::new(move |range: OutPointRange| {
610                vec![LightningClientStateMachines::Send(SendStateMachine {
611                    common: SendSMCommon {
612                        operation_id,
613                        outpoint: range.into_iter().next().unwrap(),
614                        contract: contract_clone.clone(),
615                        gateway_api: Some(gateway_api_clone.clone()),
616                        invoice: Some(LightningInvoice::Bolt11(invoice_clone.clone())),
617                        refund_keypair,
618                    },
619                    state: SendSMState::Funding,
620                })]
621            }),
622        };
623
624        let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
625            vec![client_output],
626            vec![client_output_sm],
627        ));
628
629        let transaction = TransactionBuilder::new().with_outputs(client_output);
630
631        self.client_ctx
632            .finalize_and_submit_transaction(
633                operation_id,
634                LightningCommonInit::KIND.as_str(),
635                move |change_outpoint_range| {
636                    LightningOperationMeta::Send(SendOperationMeta {
637                        change_outpoint_range,
638                        gateway: gateway_api.clone(),
639                        contract: contract.clone(),
640                        invoice: LightningInvoice::Bolt11(invoice.clone()),
641                        custom_meta: custom_meta.clone(),
642                    })
643                },
644                transaction,
645            )
646            .await
647            .map_err(|e| SendPaymentError::FailedToFundPayment(e.to_string()))?;
648
649        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
650
651        self.client_ctx
652            .log_event(
653                &mut dbtx,
654                SendPaymentEvent {
655                    operation_id,
656                    amount: send_fee.add_to(amount),
657                    fee: send_fee.fee(amount),
658                },
659            )
660            .await;
661
662        dbtx.commit_tx().await;
663
664        Ok(operation_id)
665    }
666
667    async fn get_next_operation_id(
668        &self,
669        invoice: &Bolt11Invoice,
670    ) -> Result<OperationId, SendPaymentError> {
671        for payment_attempt in 0..u64::MAX {
672            let operation_id = OperationId::from_encodable(&(invoice.clone(), payment_attempt));
673
674            if !self.client_ctx.operation_exists(operation_id).await {
675                return Ok(operation_id);
676            }
677
678            if self.client_ctx.has_active_states(operation_id).await {
679                return Err(SendPaymentError::PaymentInProgress(operation_id));
680            }
681
682            let mut stream = self
683                .subscribe_send_operation_state_updates(operation_id)
684                .await
685                .expect("operation_id exists")
686                .into_stream();
687
688            // This will not block since we checked for active states and there were none,
689            // so by definition a final state has to have been assumed already.
690            while let Some(state) = stream.next().await {
691                if let SendOperationState::Success(_) = state {
692                    return Err(SendPaymentError::InvoiceAlreadyPaid(operation_id));
693                }
694            }
695        }
696
697        panic!("We could not find an unused operation id for sending a lightning payment");
698    }
699
700    /// Subscribe to all state updates of the send operation.
701    pub async fn subscribe_send_operation_state_updates(
702        &self,
703        operation_id: OperationId,
704    ) -> anyhow::Result<UpdateStreamOrOutcome<SendOperationState>> {
705        let operation = self.client_ctx.get_operation(operation_id).await?;
706        let mut stream = self.notifier.subscribe(operation_id).await;
707        let client_ctx = self.client_ctx.clone();
708        let module_api = self.module_api.clone();
709
710        Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
711            stream! {
712                loop {
713                    if let Some(LightningClientStateMachines::Send(state)) = stream.next().await {
714                        match state.state {
715                            SendSMState::Funding => yield SendOperationState::Funding,
716                            SendSMState::Funded => yield SendOperationState::Funded,
717                            SendSMState::Success(preimage) => {
718                                // the preimage has been verified by the state machine previously
719                                assert!(state.common.contract.verify_preimage(&preimage));
720
721                                yield SendOperationState::Success(preimage);
722                                return;
723                            },
724                            SendSMState::Refunding(out_points) => {
725                                yield SendOperationState::Refunding;
726
727                                if client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await.is_ok() {
728                                    yield SendOperationState::Refunded;
729                                    return;
730                                }
731
732                                // The gateway may have incorrectly claimed the outgoing contract thereby causing
733                                // our refund transaction to be rejected. Therefore, we check one last time if
734                                // the preimage is available before we enter the failure state.
735                                if let Some(preimage) = module_api.await_preimage(
736                                    state.common.outpoint,
737                                    0
738                                ).await
739                                    && state.common.contract.verify_preimage(&preimage) {
740                                        yield SendOperationState::Success(preimage);
741                                        return;
742                                    }
743
744                                yield SendOperationState::Failure;
745                                return;
746                            },
747                            SendSMState::Rejected(..) => {
748                                yield SendOperationState::Failure;
749                                return;
750                            },
751                        }
752                    }
753                }
754            }
755        }))
756    }
757
758    /// Await the final state of the send operation.
759    pub async fn await_final_send_operation_state(
760        &self,
761        operation_id: OperationId,
762    ) -> anyhow::Result<FinalSendOperationState> {
763        let mut stream = self
764            .subscribe_send_operation_state_updates(operation_id)
765            .await?
766            .into_stream();
767
768        let mut final_state = None;
769
770        while let Some(state) = stream.next().await {
771            match state {
772                SendOperationState::Success(_) => {
773                    final_state = Some(FinalSendOperationState::Success);
774                }
775                SendOperationState::Refunded => {
776                    final_state = Some(FinalSendOperationState::Refunded);
777                }
778                SendOperationState::Failure => final_state = Some(FinalSendOperationState::Failure),
779                _ => {}
780            }
781        }
782
783        Ok(final_state.expect("Stream contains one final state"))
784    }
785
786    /// Request an invoice. For testing you can optionally specify a gateway to
787    /// generate the invoice, otherwise a random online gateway will be selected
788    /// automatically.
789    ///
790    /// The total fee for this payment may depend on the chosen gateway but
791    /// will be limited to half of one percent plus fifty satoshis. Since the
792    /// selected gateway has been vetted by at least one guardian we trust it to
793    /// set a reasonable fee and only enforce a rather high limit.
794    ///
795    /// The absolute fee for a payment can be calculated from the operation meta
796    /// to be shown to the user in the transaction history.
797    pub async fn receive(
798        &self,
799        amount: Amount,
800        expiry_secs: u32,
801        description: Bolt11InvoiceDescription,
802        gateway: Option<SafeUrl>,
803        custom_meta: Value,
804    ) -> Result<(Bolt11Invoice, OperationId), ReceiveError> {
805        let (gateway, contract, invoice) = self
806            .create_contract_and_fetch_invoice(
807                self.keypair.public_key(),
808                amount,
809                expiry_secs,
810                description,
811                gateway,
812            )
813            .await?;
814
815        let operation_id = self
816            .receive_incoming_contract(
817                self.keypair.secret_key(),
818                contract.clone(),
819                LightningOperationMeta::Receive(ReceiveOperationMeta {
820                    gateway,
821                    contract,
822                    invoice: LightningInvoice::Bolt11(invoice.clone()),
823                    custom_meta,
824                }),
825            )
826            .await
827            .expect("The contract has been generated with our public key");
828
829        Ok((invoice, operation_id))
830    }
831
832    /// Create an incoming contract locked to a public key derived from the
833    /// recipient's static module public key and fetches the corresponding
834    /// invoice.
835    async fn create_contract_and_fetch_invoice(
836        &self,
837        recipient_static_pk: PublicKey,
838        amount: Amount,
839        expiry_secs: u32,
840        description: Bolt11InvoiceDescription,
841        gateway: Option<SafeUrl>,
842    ) -> Result<(SafeUrl, IncomingContract, Bolt11Invoice), ReceiveError> {
843        let (ephemeral_tweak, ephemeral_pk) = tweak::generate(recipient_static_pk);
844
845        let encryption_seed = ephemeral_tweak
846            .consensus_hash::<sha256::Hash>()
847            .to_byte_array();
848
849        let preimage = encryption_seed
850            .consensus_hash::<sha256::Hash>()
851            .to_byte_array();
852
853        let (gateway, routing_info) = match gateway {
854            Some(gateway) => (
855                gateway.clone(),
856                self.routing_info(&gateway)
857                    .await
858                    .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?
859                    .ok_or(ReceiveError::FederationNotSupported)?,
860            ),
861            None => self
862                .select_gateway(None)
863                .await
864                .map_err(ReceiveError::SelectGateway)?,
865        };
866
867        if !routing_info.receive_fee.le(&PaymentFee::RECEIVE_FEE_LIMIT) {
868            return Err(ReceiveError::GatewayFeeExceedsLimit);
869        }
870
871        let contract_amount = routing_info.receive_fee.subtract_from(amount.msats);
872
873        if contract_amount < MINIMUM_INCOMING_CONTRACT_AMOUNT {
874            return Err(ReceiveError::AmountTooSmall);
875        }
876
877        let expiration = duration_since_epoch()
878            .as_secs()
879            .saturating_add(u64::from(expiry_secs));
880
881        let claim_pk = recipient_static_pk
882            .mul_tweak(
883                secp256k1::SECP256K1,
884                &Scalar::from_be_bytes(ephemeral_tweak).expect("Within curve order"),
885            )
886            .expect("Tweak is valid");
887
888        let contract = IncomingContract::new(
889            self.cfg.tpe_agg_pk,
890            encryption_seed,
891            preimage,
892            PaymentImage::Hash(preimage.consensus_hash()),
893            contract_amount,
894            expiration,
895            claim_pk,
896            routing_info.module_public_key,
897            ephemeral_pk,
898        );
899
900        let invoice = self
901            .gateway_conn
902            .bolt11_invoice(
903                gateway.clone(),
904                self.federation_id,
905                contract.clone(),
906                amount,
907                description,
908                expiry_secs,
909            )
910            .await
911            .map_err(|e| ReceiveError::FailedToConnectToGateway(e.to_string()))?;
912
913        if invoice.payment_hash() != &preimage.consensus_hash() {
914            return Err(ReceiveError::InvalidInvoice);
915        }
916
917        if invoice.amount_milli_satoshis() != Some(amount.msats) {
918            return Err(ReceiveError::IncorrectInvoiceAmount);
919        }
920
921        Ok((gateway, contract, invoice))
922    }
923
924    // Receive an incoming contract locked to a public key derived from our
925    // static module public key.
926    async fn receive_incoming_contract(
927        &self,
928        sk: SecretKey,
929        contract: IncomingContract,
930        operation_meta: LightningOperationMeta,
931    ) -> Option<OperationId> {
932        let operation_id = OperationId::from_encodable(&contract.clone());
933
934        let (claim_keypair, agg_decryption_key) = self.recover_contract_keys(sk, &contract)?;
935
936        let receive_sm = LightningClientStateMachines::Receive(ReceiveStateMachine {
937            common: ReceiveSMCommon {
938                operation_id,
939                contract: contract.clone(),
940                claim_keypair,
941                agg_decryption_key,
942            },
943            state: ReceiveSMState::Pending,
944        });
945
946        // this may only fail if the operation id is already in use, in which case we
947        // ignore the error such that the method is idempotent
948        self.client_ctx
949            .manual_operation_start(
950                operation_id,
951                LightningCommonInit::KIND.as_str(),
952                operation_meta,
953                vec![self.client_ctx.make_dyn_state(receive_sm)],
954            )
955            .await
956            .ok();
957
958        Some(operation_id)
959    }
960
961    fn recover_contract_keys(
962        &self,
963        sk: SecretKey,
964        contract: &IncomingContract,
965    ) -> Option<(Keypair, AggregateDecryptionKey)> {
966        let tweak = ecdh::SharedSecret::new(&contract.commitment.ephemeral_pk, &sk);
967
968        let encryption_seed = tweak
969            .secret_bytes()
970            .consensus_hash::<sha256::Hash>()
971            .to_byte_array();
972
973        let claim_keypair = sk
974            .mul_tweak(&Scalar::from_be_bytes(tweak.secret_bytes()).expect("Within curve order"))
975            .expect("Tweak is valid")
976            .keypair(secp256k1::SECP256K1);
977
978        if claim_keypair.public_key() != contract.commitment.claim_pk {
979            return None; // The claim key is not derived from our pk
980        }
981
982        let agg_decryption_key = derive_agg_dk(&self.cfg.tpe_agg_pk, &encryption_seed);
983
984        if !contract.verify_agg_decryption_key(&self.cfg.tpe_agg_pk, &agg_decryption_key) {
985            return None; // The decryption key is not derived from our pk
986        }
987
988        contract.decrypt_preimage(&agg_decryption_key)?;
989
990        Some((claim_keypair, agg_decryption_key))
991    }
992
993    /// Subscribe to all state updates of the receive operation.
994    pub async fn subscribe_receive_operation_state_updates(
995        &self,
996        operation_id: OperationId,
997    ) -> anyhow::Result<UpdateStreamOrOutcome<ReceiveOperationState>> {
998        let operation = self.client_ctx.get_operation(operation_id).await?;
999        let mut stream = self.notifier.subscribe(operation_id).await;
1000        let client_ctx = self.client_ctx.clone();
1001
1002        Ok(self.client_ctx.outcome_or_updates(operation, operation_id, move || {
1003            stream! {
1004                loop {
1005                    if let Some(LightningClientStateMachines::Receive(state)) = stream.next().await {
1006                        match state.state {
1007                            ReceiveSMState::Pending => yield ReceiveOperationState::Pending,
1008                            ReceiveSMState::Claiming(out_points) => {
1009                                yield ReceiveOperationState::Claiming;
1010
1011                                if client_ctx.await_primary_module_outputs(operation_id, out_points).await.is_ok() {
1012                                    yield ReceiveOperationState::Claimed;
1013                                } else {
1014                                    yield ReceiveOperationState::Failure;
1015                                }
1016                                return;
1017                            },
1018                            ReceiveSMState::Expired => {
1019                                yield ReceiveOperationState::Expired;
1020                                return;
1021                            }
1022                        }
1023                    }
1024                }
1025            }
1026        }))
1027    }
1028
1029    /// Await the final state of the receive operation.
1030    pub async fn await_final_receive_operation_state(
1031        &self,
1032        operation_id: OperationId,
1033    ) -> anyhow::Result<FinalReceiveOperationState> {
1034        let mut stream = self
1035            .subscribe_receive_operation_state_updates(operation_id)
1036            .await?
1037            .into_stream();
1038
1039        let mut final_state = None;
1040
1041        while let Some(state) = stream.next().await {
1042            match state {
1043                ReceiveOperationState::Expired => {
1044                    final_state = Some(FinalReceiveOperationState::Expired);
1045                }
1046                ReceiveOperationState::Claimed => {
1047                    final_state = Some(FinalReceiveOperationState::Claimed);
1048                }
1049                ReceiveOperationState::Failure => {
1050                    final_state = Some(FinalReceiveOperationState::Failure);
1051                }
1052                _ => {}
1053            }
1054        }
1055
1056        Ok(final_state.expect("Stream contains one final state"))
1057    }
1058
1059    /// Generate an lnurl for the client. You can optionally specify a gateway
1060    /// to use for testing purposes.
1061    pub async fn generate_lnurl(
1062        &self,
1063        recurringd: SafeUrl,
1064        gateway: Option<SafeUrl>,
1065    ) -> Result<String, GenerateLnurlError> {
1066        let gateways = if let Some(gateway) = gateway {
1067            vec![gateway]
1068        } else {
1069            let gateways = self
1070                .module_api
1071                .gateways()
1072                .await
1073                .map_err(|e| GenerateLnurlError::FailedToRequestGateways(e.to_string()))?;
1074
1075            if gateways.is_empty() {
1076                return Err(GenerateLnurlError::NoGatewaysAvailable);
1077            }
1078
1079            gateways
1080        };
1081
1082        let payload = fedimint_core::base32::encode_prefixed(
1083            fedimint_core::base32::FEDIMINT_PREFIX,
1084            &lnurl::LnurlRequest {
1085                federation_id: self.federation_id,
1086                recipient_pk: self.lnurl_keypair.public_key(),
1087                aggregate_pk: self.cfg.tpe_agg_pk,
1088                gateways,
1089            },
1090        );
1091
1092        Ok(fedimint_lnurl::encode_lnurl(&format!(
1093            "{recurringd}pay/{payload}"
1094        )))
1095    }
1096
1097    fn spawn_receive_lnurl_task(
1098        &self,
1099        custom_meta_fn: Arc<dyn Fn() -> Value + Send + Sync>,
1100        task_group: &TaskGroup,
1101    ) {
1102        let module = self.clone();
1103        let api = self.module_api.clone();
1104
1105        task_group.spawn_cancellable("receive_lnurl_task", async move {
1106            api.wait_for_initialized_connections().await;
1107            loop {
1108                module.receive_lnurl(custom_meta_fn()).await;
1109            }
1110        });
1111    }
1112
1113    async fn receive_lnurl(&self, custom_meta: Value) {
1114        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
1115
1116        let stream_index = dbtx
1117            .get_value(&IncomingContractStreamIndexKey)
1118            .await
1119            .unwrap_or(0);
1120
1121        let (contracts, next_index) = self
1122            .module_api
1123            .await_incoming_contracts(stream_index, 128)
1124            .await;
1125
1126        for contract in &contracts {
1127            if let Some(operation_id) = self
1128                .receive_incoming_contract(
1129                    self.lnurl_keypair.secret_key(),
1130                    contract.clone(),
1131                    LightningOperationMeta::LnurlReceive(LnurlReceiveOperationMeta {
1132                        contract: contract.clone(),
1133                        custom_meta: custom_meta.clone(),
1134                    }),
1135                )
1136                .await
1137            {
1138                self.await_final_receive_operation_state(operation_id)
1139                    .await
1140                    .ok();
1141            }
1142        }
1143
1144        dbtx.insert_entry(&IncomingContractStreamIndexKey, &next_index)
1145            .await;
1146
1147        dbtx.commit_tx().await;
1148    }
1149}
1150
1151#[derive(Error, Debug, Clone, Eq, PartialEq)]
1152pub enum SelectGatewayError {
1153    #[error("Failed to request gateways")]
1154    FailedToRequestGateways(String),
1155    #[error("No gateways are available")]
1156    NoGatewaysAvailable,
1157    #[error("All gateways failed to respond")]
1158    GatewaysUnresponsive,
1159}
1160
1161#[derive(Error, Debug, Clone, Eq, PartialEq)]
1162pub enum SendPaymentError {
1163    #[error("Invoice is missing an amount")]
1164    InvoiceMissingAmount,
1165    #[error("Invoice has expired")]
1166    InvoiceExpired,
1167    #[error("A payment for this invoice is already in progress")]
1168    PaymentInProgress(OperationId),
1169    #[error("This invoice has already been paid")]
1170    InvoiceAlreadyPaid(OperationId),
1171    #[error(transparent)]
1172    SelectGateway(SelectGatewayError),
1173    #[error("Failed to connect to gateway")]
1174    FailedToConnectToGateway(String),
1175    #[error("Gateway does not support this federation")]
1176    FederationNotSupported,
1177    #[error("Gateway fee exceeds the allowed limit")]
1178    GatewayFeeExceedsLimit,
1179    #[error("Gateway expiration time exceeds the allowed limit")]
1180    GatewayExpirationExceedsLimit,
1181    #[error("Failed to request block count")]
1182    FailedToRequestBlockCount(String),
1183    #[error("Failed to fund the payment")]
1184    FailedToFundPayment(String),
1185    #[error("Invoice is for a different currency")]
1186    WrongCurrency {
1187        invoice_currency: Currency,
1188        federation_currency: Currency,
1189    },
1190}
1191
1192#[derive(Error, Debug, Clone, Eq, PartialEq)]
1193pub enum ReceiveError {
1194    #[error(transparent)]
1195    SelectGateway(SelectGatewayError),
1196    #[error("Failed to connect to gateway")]
1197    FailedToConnectToGateway(String),
1198    #[error("Gateway does not support this federation")]
1199    FederationNotSupported,
1200    #[error("Gateway fee exceeds the allowed limit")]
1201    GatewayFeeExceedsLimit,
1202    #[error("Amount is too small to cover fees")]
1203    AmountTooSmall,
1204    #[error("Gateway returned an invalid invoice")]
1205    InvalidInvoice,
1206    #[error("Gateway returned an invoice with incorrect amount")]
1207    IncorrectInvoiceAmount,
1208}
1209
1210#[derive(Error, Debug, Clone, Eq, PartialEq)]
1211pub enum GenerateLnurlError {
1212    #[error("No gateways are available")]
1213    NoGatewaysAvailable,
1214    #[error("Failed to request gateways")]
1215    FailedToRequestGateways(String),
1216}
1217
1218#[derive(Error, Debug, Clone, Eq, PartialEq)]
1219pub enum ListGatewaysError {
1220    #[error("Failed to request gateways")]
1221    FailedToListGateways,
1222}
1223
1224#[derive(Error, Debug, Clone, Eq, PartialEq)]
1225pub enum RoutingInfoError {
1226    #[error("Failed to request routing info")]
1227    FailedToRequestRoutingInfo,
1228}
1229
1230#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1231pub enum LightningClientStateMachines {
1232    Send(SendStateMachine),
1233    Receive(ReceiveStateMachine),
1234}
1235
1236impl IntoDynInstance for LightningClientStateMachines {
1237    type DynType = DynState;
1238
1239    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1240        DynState::from_typed(instance_id, self)
1241    }
1242}
1243
1244impl State for LightningClientStateMachines {
1245    type ModuleContext = LightningClientContext;
1246
1247    fn transitions(
1248        &self,
1249        context: &Self::ModuleContext,
1250        global_context: &DynGlobalClientContext,
1251    ) -> Vec<StateTransition<Self>> {
1252        match self {
1253            LightningClientStateMachines::Send(state) => {
1254                sm_enum_variant_translation!(
1255                    state.transitions(context, global_context),
1256                    LightningClientStateMachines::Send
1257                )
1258            }
1259            LightningClientStateMachines::Receive(state) => {
1260                sm_enum_variant_translation!(
1261                    state.transitions(context, global_context),
1262                    LightningClientStateMachines::Receive
1263                )
1264            }
1265        }
1266    }
1267
1268    fn operation_id(&self) -> OperationId {
1269        match self {
1270            LightningClientStateMachines::Send(state) => state.operation_id(),
1271            LightningClientStateMachines::Receive(state) => state.operation_id(),
1272        }
1273    }
1274}