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