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