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