fedimint_gw_client/
pay.rs

1use std::fmt::{self, Display};
2
3use fedimint_client::ClientHandleArc;
4use fedimint_client_module::DynGlobalClientContext;
5use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
6use fedimint_client_module::transaction::{
7    ClientInput, ClientInputBundle, ClientOutput, ClientOutputBundle,
8};
9use fedimint_core::config::FederationId;
10use fedimint_core::core::OperationId;
11use fedimint_core::encoding::{Decodable, Encodable};
12use fedimint_core::{Amount, OutPoint, TransactionId, secp256k1};
13use fedimint_lightning::{LightningRpcError, PayInvoiceResponse};
14use fedimint_ln_client::api::LnFederationApi;
15use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData};
16use fedimint_ln_common::config::FeeToAmount;
17use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
18use fedimint_ln_common::contracts::{ContractId, FundedContract, IdentifiableContract, Preimage};
19use fedimint_ln_common::{LightningInput, LightningOutput};
20use futures::future;
21use lightning_invoice::RoutingFees;
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24use tokio_stream::StreamExt;
25use tracing::{Instrument, debug, error, info, warn};
26
27use super::{GatewayClientContext, GatewayExtReceiveStates};
28use crate::events::{OutgoingPaymentFailed, OutgoingPaymentSucceeded};
29use crate::{GatewayClientModule, SwapParameters};
30
31const TIMELOCK_DELTA: u64 = 10;
32
33#[cfg_attr(doc, aquamarine::aquamarine)]
34/// State machine that executes the Lightning payment on behalf of
35/// the fedimint user that requested an invoice to be paid.
36///
37/// ```mermaid
38/// graph LR
39/// classDef virtual fill:#fff,stroke-dasharray: 5 5
40///
41///    PayInvoice -- fetch contract failed --> Canceled
42///    PayInvoice -- validate contract failed --> CancelContract
43///    PayInvoice -- pay invoice unsuccessful --> CancelContract
44///    PayInvoice -- pay invoice over Lightning successful --> ClaimOutgoingContract
45///    PayInvoice -- pay invoice via direct swap successful --> WaitForSwapPreimage
46///    WaitForSwapPreimage -- received preimage --> ClaimOutgoingContract
47///    WaitForSwapPreimage -- wait for preimge failed --> Canceled
48///    ClaimOutgoingContract -- claim tx submission --> Preimage
49///    CancelContract -- cancel tx submission successful --> Canceled
50///    CancelContract -- cancel tx submission unsuccessful --> Failed
51/// ```
52#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
53pub enum GatewayPayStates {
54    PayInvoice(GatewayPayInvoice),
55    CancelContract(Box<GatewayPayCancelContract>),
56    Preimage(Vec<OutPoint>, Preimage),
57    OfferDoesNotExist(ContractId),
58    Canceled {
59        txid: TransactionId,
60        contract_id: ContractId,
61        error: OutgoingPaymentError,
62    },
63    WaitForSwapPreimage(Box<GatewayPayWaitForSwapPreimage>),
64    ClaimOutgoingContract(Box<GatewayPayClaimOutgoingContract>),
65    Failed {
66        error: OutgoingPaymentError,
67        error_message: String,
68    },
69}
70
71impl fmt::Display for GatewayPayStates {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            GatewayPayStates::PayInvoice(_) => write!(f, "PayInvoice"),
75            GatewayPayStates::CancelContract(_) => write!(f, "CancelContract"),
76            GatewayPayStates::Preimage(..) => write!(f, "Preimage"),
77            GatewayPayStates::OfferDoesNotExist(_) => write!(f, "OfferDoesNotExist"),
78            GatewayPayStates::Canceled { .. } => write!(f, "Canceled"),
79            GatewayPayStates::WaitForSwapPreimage(_) => write!(f, "WaitForSwapPreimage"),
80            GatewayPayStates::ClaimOutgoingContract(_) => write!(f, "ClaimOutgoingContract"),
81            GatewayPayStates::Failed { .. } => write!(f, "Failed"),
82        }
83    }
84}
85
86#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
87pub struct GatewayPayCommon {
88    pub operation_id: OperationId,
89}
90
91#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
92pub struct GatewayPayStateMachine {
93    pub common: GatewayPayCommon,
94    pub state: GatewayPayStates,
95}
96
97impl fmt::Display for GatewayPayStateMachine {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        write!(
100            f,
101            "Gateway Pay State Machine Operation ID: {:?} State: {}",
102            self.common.operation_id, self.state
103        )
104    }
105}
106
107impl State for GatewayPayStateMachine {
108    type ModuleContext = GatewayClientContext;
109
110    fn transitions(
111        &self,
112        context: &Self::ModuleContext,
113        global_context: &DynGlobalClientContext,
114    ) -> Vec<StateTransition<Self>> {
115        match &self.state {
116            GatewayPayStates::PayInvoice(gateway_pay_invoice) => {
117                gateway_pay_invoice.transitions(global_context.clone(), context, &self.common)
118            }
119            GatewayPayStates::WaitForSwapPreimage(gateway_pay_wait_for_swap_preimage) => {
120                gateway_pay_wait_for_swap_preimage.transitions(context.clone(), self.common.clone())
121            }
122            GatewayPayStates::ClaimOutgoingContract(gateway_pay_claim_outgoing_contract) => {
123                gateway_pay_claim_outgoing_contract.transitions(
124                    global_context.clone(),
125                    context.clone(),
126                    self.common.clone(),
127                )
128            }
129            GatewayPayStates::CancelContract(gateway_pay_cancel) => gateway_pay_cancel.transitions(
130                global_context.clone(),
131                context.clone(),
132                self.common.clone(),
133            ),
134            _ => {
135                vec![]
136            }
137        }
138    }
139
140    fn operation_id(&self) -> fedimint_core::core::OperationId {
141        self.common.operation_id
142    }
143}
144
145#[derive(
146    Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
147)]
148pub enum OutgoingContractError {
149    #[error("Invalid OutgoingContract {contract_id}")]
150    InvalidOutgoingContract { contract_id: ContractId },
151    #[error("The contract is already cancelled and can't be processed by the gateway")]
152    CancelledContract,
153    #[error("The Account or offer is keyed to another gateway")]
154    NotOurKey,
155    #[error("Invoice is missing amount")]
156    InvoiceMissingAmount,
157    #[error("Outgoing contract is underfunded, wants us to pay {0}, but only contains {1}")]
158    Underfunded(Amount, Amount),
159    #[error("The contract's timeout is in the past or does not allow for a safety margin")]
160    TimeoutTooClose,
161    #[error("Gateway could not retrieve metadata about the contract.")]
162    MissingContractData,
163    #[error("The invoice is expired. Expiry happened at timestamp: {0}")]
164    InvoiceExpired(u64),
165}
166
167#[derive(
168    Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
169)]
170pub enum OutgoingPaymentErrorType {
171    #[error("OutgoingContract does not exist {contract_id}")]
172    OutgoingContractDoesNotExist { contract_id: ContractId },
173    #[error("An error occurred while paying the lightning invoice.")]
174    LightningPayError { lightning_error: LightningRpcError },
175    #[error("An invalid contract was specified.")]
176    InvalidOutgoingContract { error: OutgoingContractError },
177    #[error("An error occurred while attempting direct swap between federations.")]
178    SwapFailed { swap_error: String },
179    #[error("Invoice has already been paid")]
180    InvoiceAlreadyPaid,
181    #[error("No federation configuration")]
182    InvalidFederationConfiguration,
183    #[error("Invalid invoice preimage")]
184    InvalidInvoicePreimage,
185}
186
187#[derive(
188    Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
189)]
190pub struct OutgoingPaymentError {
191    pub error_type: OutgoingPaymentErrorType,
192    pub contract_id: ContractId,
193    pub contract: Option<OutgoingContractAccount>,
194}
195
196impl Display for OutgoingPaymentError {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        write!(f, "OutgoingContractError: {}", self.error_type)
199    }
200}
201
202#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
203pub struct GatewayPayInvoice {
204    pub pay_invoice_payload: PayInvoicePayload,
205}
206
207impl GatewayPayInvoice {
208    fn transitions(
209        &self,
210        global_context: DynGlobalClientContext,
211        context: &GatewayClientContext,
212        common: &GatewayPayCommon,
213    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
214        let payload = self.pay_invoice_payload.clone();
215        vec![StateTransition::new(
216            Self::fetch_parameters_and_pay(
217                global_context,
218                payload,
219                context.clone(),
220                common.clone(),
221            ),
222            |_dbtx, result, _old_state| Box::pin(futures::future::ready(result)),
223        )]
224    }
225
226    async fn fetch_parameters_and_pay(
227        global_context: DynGlobalClientContext,
228        pay_invoice_payload: PayInvoicePayload,
229        context: GatewayClientContext,
230        common: GatewayPayCommon,
231    ) -> GatewayPayStateMachine {
232        match Self::await_get_payment_parameters(
233            global_context,
234            context.clone(),
235            pay_invoice_payload.contract_id,
236            pay_invoice_payload.payment_data.clone(),
237            pay_invoice_payload.federation_id,
238        )
239        .await
240        {
241            Ok((contract, payment_parameters)) => {
242                Self::buy_preimage(
243                    context.clone(),
244                    contract.clone(),
245                    payment_parameters.clone(),
246                    common.clone(),
247                    pay_invoice_payload.clone(),
248                )
249                .await
250            }
251            Err(e) => {
252                warn!("Failed to get payment parameters: {e:?}");
253                match e.contract.clone() {
254                    Some(contract) => GatewayPayStateMachine {
255                        common,
256                        state: GatewayPayStates::CancelContract(Box::new(
257                            GatewayPayCancelContract { contract, error: e },
258                        )),
259                    },
260                    None => GatewayPayStateMachine {
261                        common,
262                        state: GatewayPayStates::OfferDoesNotExist(e.contract_id),
263                    },
264                }
265            }
266        }
267    }
268
269    /// Checks the gateway's database to determine if the current gateway
270    /// generated the invoice using the LNv2 protocol. If it did, the
271    /// gateway can buy the preimage and use it to claim the LNv1
272    /// `OutgoingContract`.
273    async fn buy_lnv2_preimage(
274        context: &GatewayClientContext,
275        contract: OutgoingContractAccount,
276        swap_parameters: SwapParameters,
277        common: GatewayPayCommon,
278    ) -> Option<GatewayPayStateMachine> {
279        let amount = swap_parameters.amount_msat;
280        if let Ok(Some((lnv2_incoming_contract, client))) = context
281            .lightning_manager
282            .is_lnv2_direct_swap(swap_parameters.payment_hash, amount)
283            .await
284        {
285            let state = match client
286                .get_first_module::<fedimint_gwv2_client::GatewayClientModuleV2>()
287                .expect("Must have client module")
288                .relay_direct_swap(lnv2_incoming_contract, amount.msats)
289                .await
290            {
291                Ok(final_receive_state) => match final_receive_state {
292                    fedimint_gwv2_client::FinalReceiveState::Success(preimage) => {
293                        GatewayPayStateMachine {
294                            common,
295                            state: GatewayPayStates::ClaimOutgoingContract(Box::new(
296                                GatewayPayClaimOutgoingContract {
297                                    contract,
298                                    preimage: Preimage(preimage),
299                                },
300                            )),
301                        }
302                    }
303                    state => GatewayPayStateMachine {
304                        common,
305                        state: GatewayPayStates::CancelContract(Box::new(
306                            GatewayPayCancelContract {
307                                contract: contract.clone(),
308                                error: OutgoingPaymentError {
309                                    contract_id: contract.contract.contract_id(),
310                                    contract: Some(contract.clone()),
311                                    error_type: OutgoingPaymentErrorType::SwapFailed {
312                                        swap_error: format!(
313                                            "Failed to initiate LNv1 -> LNv2 swap. LNv2 state: {state:?}"
314                                        ),
315                                    },
316                                },
317                            },
318                        )),
319                    },
320                },
321                Err(err) => GatewayPayStateMachine {
322                    common,
323                    state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
324                        contract: contract.clone(),
325                        error: OutgoingPaymentError {
326                            contract_id: contract.contract.contract_id(),
327                            contract: Some(contract.clone()),
328                            error_type: OutgoingPaymentErrorType::SwapFailed {
329                                swap_error: format!(
330                                    "Failed to initiate LNv1 -> LNv2 swap. Err: {err}"
331                                ),
332                            },
333                        },
334                    })),
335                },
336            };
337
338            return Some(state);
339        }
340
341        None
342    }
343
344    async fn buy_preimage(
345        context: GatewayClientContext,
346        contract: OutgoingContractAccount,
347        payment_parameters: PaymentParameters,
348        common: GatewayPayCommon,
349        payload: PayInvoicePayload,
350    ) -> GatewayPayStateMachine {
351        debug!("Buying preimage contract {contract:?}");
352        // Verify that this client is authorized to receive the preimage.
353        if let Err(err) = context
354            .lightning_manager
355            .verify_preimage_authentication(
356                payload.payment_data.payment_hash(),
357                payload.preimage_auth,
358                contract.clone(),
359            )
360            .await
361        {
362            warn!("Preimage authentication failed: {err} for contract {contract:?}");
363            return GatewayPayStateMachine {
364                common,
365                state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
366                    contract,
367                    error: err,
368                })),
369            };
370        }
371
372        // Not all clients support LNv2 yet, so here we check if we are trying to pay an
373        // LNv2 invoice. If this gateway also supports LNv2, the gateway can do
374        // a swap between LNv1 `OutgoingContract` and an
375        // LNv2 `IncomingContract`.
376        let swap_parameters: anyhow::Result<SwapParameters> =
377            payment_parameters.payment_data.clone().try_into();
378        if let Ok(swap_parameters) = swap_parameters
379            && let Some(new_state) =
380                Self::buy_lnv2_preimage(&context, contract.clone(), swap_parameters, common.clone())
381                    .await
382        {
383            return new_state;
384        }
385
386        match context
387            .lightning_manager
388            .get_client_for_invoice(payment_parameters.payment_data.clone())
389            .await
390        {
391            Some(client) => {
392                client
393                    .with(|client| {
394                        Self::buy_preimage_via_direct_swap(
395                            client,
396                            payment_parameters.payment_data.clone(),
397                            contract.clone(),
398                            common.clone(),
399                        )
400                    })
401                    .await
402            }
403            _ => {
404                Self::buy_preimage_over_lightning(
405                    context,
406                    payment_parameters,
407                    contract.clone(),
408                    common.clone(),
409                )
410                .await
411            }
412        }
413    }
414
415    async fn await_get_payment_parameters(
416        global_context: DynGlobalClientContext,
417        context: GatewayClientContext,
418        contract_id: ContractId,
419        payment_data: PaymentData,
420        federation_id: FederationId,
421    ) -> Result<(OutgoingContractAccount, PaymentParameters), OutgoingPaymentError> {
422        debug!("Await payment parameters for outgoing contract {contract_id:?}");
423        let account = global_context
424            .module_api()
425            .await_contract(contract_id)
426            .await;
427
428        if let FundedContract::Outgoing(contract) = account.contract {
429            let outgoing_contract_account = OutgoingContractAccount {
430                amount: account.amount,
431                contract,
432            };
433
434            let consensus_block_count = global_context
435                .module_api()
436                .fetch_consensus_block_count()
437                .await
438                .map_err(|_| OutgoingPaymentError {
439                    contract_id,
440                    contract: Some(outgoing_contract_account.clone()),
441                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
442                        error: OutgoingContractError::TimeoutTooClose,
443                    },
444                })?;
445
446            debug!(
447                "Consensus block count: {consensus_block_count:?} for outgoing contract {contract_id:?}"
448            );
449            if consensus_block_count.is_none() {
450                return Err(OutgoingPaymentError {
451                    contract_id,
452                    contract: Some(outgoing_contract_account.clone()),
453                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
454                        error: OutgoingContractError::MissingContractData,
455                    },
456                });
457            }
458
459            let routing_fees = context
460                .lightning_manager
461                .get_routing_fees(federation_id)
462                .await
463                .ok_or(OutgoingPaymentError {
464                    error_type: OutgoingPaymentErrorType::InvalidFederationConfiguration,
465                    contract_id,
466                    contract: Some(outgoing_contract_account.clone()),
467                })?;
468
469            let payment_parameters = Self::validate_outgoing_account(
470                &outgoing_contract_account,
471                context.redeem_key,
472                consensus_block_count.unwrap(),
473                &payment_data,
474                routing_fees,
475            )
476            .map_err(|e| {
477                warn!("Invalid outgoing contract: {e:?}");
478                OutgoingPaymentError {
479                    contract_id,
480                    contract: Some(outgoing_contract_account.clone()),
481                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract { error: e },
482                }
483            })?;
484            debug!("Got payment parameters: {payment_parameters:?} for contract {contract_id:?}");
485            return Ok((outgoing_contract_account, payment_parameters));
486        }
487
488        error!("Contract {contract_id:?} is not an outgoing contract");
489        Err(OutgoingPaymentError {
490            contract_id,
491            contract: None,
492            error_type: OutgoingPaymentErrorType::OutgoingContractDoesNotExist { contract_id },
493        })
494    }
495
496    async fn buy_preimage_over_lightning(
497        context: GatewayClientContext,
498        buy_preimage: PaymentParameters,
499        contract: OutgoingContractAccount,
500        common: GatewayPayCommon,
501    ) -> GatewayPayStateMachine {
502        debug!("Buying preimage over lightning for contract {contract:?}");
503
504        let max_delay = buy_preimage.max_delay;
505        let max_fee = buy_preimage.max_send_amount.saturating_sub(
506            buy_preimage
507                .payment_data
508                .amount()
509                .expect("We already checked that an amount was supplied"),
510        );
511
512        let payment_result = context
513            .lightning_manager
514            .pay(buy_preimage.payment_data, max_delay, max_fee)
515            .await;
516
517        match payment_result {
518            Ok(PayInvoiceResponse { preimage, .. }) => {
519                debug!("Preimage received for contract {contract:?}");
520                GatewayPayStateMachine {
521                    common,
522                    state: GatewayPayStates::ClaimOutgoingContract(Box::new(
523                        GatewayPayClaimOutgoingContract { contract, preimage },
524                    )),
525                }
526            }
527            Err(error) => Self::gateway_pay_cancel_contract(error, contract, common),
528        }
529    }
530
531    fn gateway_pay_cancel_contract(
532        error: LightningRpcError,
533        contract: OutgoingContractAccount,
534        common: GatewayPayCommon,
535    ) -> GatewayPayStateMachine {
536        warn!("Failed to buy preimage with {error} for contract {contract:?}");
537        let outgoing_error = OutgoingPaymentError {
538            contract_id: contract.contract.contract_id(),
539            contract: Some(contract.clone()),
540            error_type: OutgoingPaymentErrorType::LightningPayError {
541                lightning_error: error,
542            },
543        };
544        GatewayPayStateMachine {
545            common,
546            state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
547                contract,
548                error: outgoing_error,
549            })),
550        }
551    }
552
553    async fn buy_preimage_via_direct_swap(
554        client: ClientHandleArc,
555        payment_data: PaymentData,
556        contract: OutgoingContractAccount,
557        common: GatewayPayCommon,
558    ) -> GatewayPayStateMachine {
559        debug!("Buying preimage via direct swap for contract {contract:?}");
560        match payment_data.try_into() {
561            Ok(swap_params) => match client
562                .get_first_module::<GatewayClientModule>()
563                .expect("Must have client module")
564                .gateway_handle_direct_swap(swap_params)
565                .await
566            {
567                Ok(operation_id) => {
568                    debug!("Direct swap initiated for contract {contract:?}");
569                    GatewayPayStateMachine {
570                        common,
571                        state: GatewayPayStates::WaitForSwapPreimage(Box::new(
572                            GatewayPayWaitForSwapPreimage {
573                                contract,
574                                federation_id: client.federation_id(),
575                                operation_id,
576                            },
577                        )),
578                    }
579                }
580                Err(e) => {
581                    info!("Failed to initiate direct swap: {e:?} for contract {contract:?}");
582                    let outgoing_payment_error = OutgoingPaymentError {
583                        contract_id: contract.contract.contract_id(),
584                        contract: Some(contract.clone()),
585                        error_type: OutgoingPaymentErrorType::SwapFailed {
586                            swap_error: format!("Failed to initiate direct swap: {e}"),
587                        },
588                    };
589                    GatewayPayStateMachine {
590                        common,
591                        state: GatewayPayStates::CancelContract(Box::new(
592                            GatewayPayCancelContract {
593                                contract: contract.clone(),
594                                error: outgoing_payment_error,
595                            },
596                        )),
597                    }
598                }
599            },
600            Err(e) => {
601                info!("Failed to initiate direct swap: {e:?} for contract {contract:?}");
602                let outgoing_payment_error = OutgoingPaymentError {
603                    contract_id: contract.contract.contract_id(),
604                    contract: Some(contract.clone()),
605                    error_type: OutgoingPaymentErrorType::SwapFailed {
606                        swap_error: format!("Failed to initiate direct swap: {e}"),
607                    },
608                };
609                GatewayPayStateMachine {
610                    common,
611                    state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
612                        contract: contract.clone(),
613                        error: outgoing_payment_error,
614                    })),
615                }
616            }
617        }
618    }
619
620    fn validate_outgoing_account(
621        account: &OutgoingContractAccount,
622        redeem_key: bitcoin::key::Keypair,
623        consensus_block_count: u64,
624        payment_data: &PaymentData,
625        routing_fees: RoutingFees,
626    ) -> Result<PaymentParameters, OutgoingContractError> {
627        let our_pub_key = secp256k1::PublicKey::from_keypair(&redeem_key);
628
629        if account.contract.cancelled {
630            return Err(OutgoingContractError::CancelledContract);
631        }
632
633        if account.contract.gateway_key != our_pub_key {
634            return Err(OutgoingContractError::NotOurKey);
635        }
636
637        let payment_amount = payment_data
638            .amount()
639            .ok_or(OutgoingContractError::InvoiceMissingAmount)?;
640
641        let gateway_fee = routing_fees.to_amount(&payment_amount);
642        let necessary_contract_amount = payment_amount + gateway_fee;
643        if account.amount < necessary_contract_amount {
644            return Err(OutgoingContractError::Underfunded(
645                necessary_contract_amount,
646                account.amount,
647            ));
648        }
649
650        let max_delay = u64::from(account.contract.timelock)
651            .checked_sub(consensus_block_count.saturating_sub(1))
652            .and_then(|delta| delta.checked_sub(TIMELOCK_DELTA));
653        if max_delay.is_none() {
654            return Err(OutgoingContractError::TimeoutTooClose);
655        }
656
657        if payment_data.is_expired() {
658            return Err(OutgoingContractError::InvoiceExpired(
659                payment_data.expiry_timestamp(),
660            ));
661        }
662
663        Ok(PaymentParameters {
664            max_delay: max_delay.unwrap(),
665            max_send_amount: account.amount,
666            payment_data: payment_data.clone(),
667        })
668    }
669}
670
671#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable, Serialize, Deserialize)]
672struct PaymentParameters {
673    max_delay: u64,
674    max_send_amount: Amount,
675    payment_data: PaymentData,
676}
677
678#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
679pub struct GatewayPayClaimOutgoingContract {
680    contract: OutgoingContractAccount,
681    preimage: Preimage,
682}
683
684impl GatewayPayClaimOutgoingContract {
685    fn transitions(
686        &self,
687        global_context: DynGlobalClientContext,
688        context: GatewayClientContext,
689        common: GatewayPayCommon,
690    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
691        let contract = self.contract.clone();
692        let preimage = self.preimage.clone();
693        vec![StateTransition::new(
694            future::ready(()),
695            move |dbtx, (), _| {
696                Box::pin(Self::transition_claim_outgoing_contract(
697                    dbtx,
698                    global_context.clone(),
699                    context.clone(),
700                    common.clone(),
701                    contract.clone(),
702                    preimage.clone(),
703                ))
704            },
705        )]
706    }
707
708    async fn transition_claim_outgoing_contract(
709        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
710        global_context: DynGlobalClientContext,
711        context: GatewayClientContext,
712        common: GatewayPayCommon,
713        contract: OutgoingContractAccount,
714        preimage: Preimage,
715    ) -> GatewayPayStateMachine {
716        debug!("Claiming outgoing contract {contract:?}");
717
718        context
719            .client_ctx
720            .log_event(
721                &mut dbtx.module_tx(),
722                OutgoingPaymentSucceeded {
723                    outgoing_contract: contract.clone(),
724                    contract_id: contract.contract.contract_id(),
725                    preimage: preimage.consensus_encode_to_hex(),
726                },
727            )
728            .await;
729
730        let claim_input = contract.claim(preimage.clone());
731        let client_input = ClientInput::<LightningInput> {
732            input: claim_input,
733            amount: contract.amount,
734            keys: vec![context.redeem_key],
735        };
736
737        let out_points = global_context
738            .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
739            .await
740            .expect("Cannot claim input, additional funding needed")
741            .into_iter()
742            .collect();
743        debug!("Claimed outgoing contract {contract:?} with out points {out_points:?}");
744        GatewayPayStateMachine {
745            common,
746            state: GatewayPayStates::Preimage(out_points, preimage),
747        }
748    }
749}
750
751#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
752pub struct GatewayPayWaitForSwapPreimage {
753    contract: OutgoingContractAccount,
754    federation_id: FederationId,
755    operation_id: OperationId,
756}
757
758impl GatewayPayWaitForSwapPreimage {
759    fn transitions(
760        &self,
761        context: GatewayClientContext,
762        common: GatewayPayCommon,
763    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
764        let federation_id = self.federation_id;
765        let operation_id = self.operation_id;
766        let contract = self.contract.clone();
767        vec![StateTransition::new(
768            Self::await_preimage(context, federation_id, operation_id, contract.clone()),
769            move |_dbtx, result, _old_state| {
770                let common = common.clone();
771                let contract = contract.clone();
772                Box::pin(async {
773                    Self::transition_claim_outgoing_contract(common, result, contract)
774                })
775            },
776        )]
777    }
778
779    async fn await_preimage(
780        context: GatewayClientContext,
781        federation_id: FederationId,
782        operation_id: OperationId,
783        contract: OutgoingContractAccount,
784    ) -> Result<Preimage, OutgoingPaymentError> {
785        debug!("Waiting preimage for contract {contract:?}");
786
787        let client = context
788            .lightning_manager
789            .get_client(&federation_id)
790            .await
791            .ok_or(OutgoingPaymentError {
792                contract_id: contract.contract.contract_id(),
793                contract: Some(contract.clone()),
794                error_type: OutgoingPaymentErrorType::SwapFailed {
795                    swap_error: "Federation client not found".to_string(),
796                },
797            })?;
798
799        async {
800            let mut stream = client
801                .value()
802                .get_first_module::<GatewayClientModule>()
803                .expect("Must have client module")
804                .gateway_subscribe_ln_receive(operation_id)
805                .await
806                .map_err(|e| {
807                    let contract_id = contract.contract.contract_id();
808                    warn!(
809                        ?contract_id,
810                        "Failed to subscribe to ln receive of direct swap: {e:?}"
811                    );
812                    OutgoingPaymentError {
813                        contract_id,
814                        contract: Some(contract.clone()),
815                        error_type: OutgoingPaymentErrorType::SwapFailed {
816                            swap_error: format!(
817                                "Failed to subscribe to ln receive of direct swap: {e}"
818                            ),
819                        },
820                    }
821                })?
822                .into_stream();
823
824            loop {
825                debug!("Waiting next state of preimage buy for contract {contract:?}");
826                if let Some(state) = stream.next().await {
827                    match state {
828                        GatewayExtReceiveStates::Funding => {
829                            debug!(?contract, "Funding");
830                            continue;
831                        }
832                        GatewayExtReceiveStates::Preimage(preimage) => {
833                            debug!(?contract, "Received preimage");
834                            return Ok(preimage);
835                        }
836                        other => {
837                            warn!(?contract, "Got state {other:?}");
838                            return Err(OutgoingPaymentError {
839                                contract_id: contract.contract.contract_id(),
840                                contract: Some(contract),
841                                error_type: OutgoingPaymentErrorType::SwapFailed {
842                                    swap_error: "Failed to receive preimage".to_string(),
843                                },
844                            });
845                        }
846                    }
847                }
848            }
849        }
850        .instrument(client.span())
851        .await
852    }
853
854    fn transition_claim_outgoing_contract(
855        common: GatewayPayCommon,
856        result: Result<Preimage, OutgoingPaymentError>,
857        contract: OutgoingContractAccount,
858    ) -> GatewayPayStateMachine {
859        match result {
860            Ok(preimage) => GatewayPayStateMachine {
861                common,
862                state: GatewayPayStates::ClaimOutgoingContract(Box::new(
863                    GatewayPayClaimOutgoingContract { contract, preimage },
864                )),
865            },
866            Err(e) => GatewayPayStateMachine {
867                common,
868                state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
869                    contract,
870                    error: e,
871                })),
872            },
873        }
874    }
875}
876
877#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
878pub struct GatewayPayCancelContract {
879    contract: OutgoingContractAccount,
880    error: OutgoingPaymentError,
881}
882
883impl GatewayPayCancelContract {
884    fn transitions(
885        &self,
886        global_context: DynGlobalClientContext,
887        context: GatewayClientContext,
888        common: GatewayPayCommon,
889    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
890        let contract = self.contract.clone();
891        let error = self.error.clone();
892        vec![StateTransition::new(
893            future::ready(()),
894            move |dbtx, (), _| {
895                Box::pin(Self::transition_canceled(
896                    dbtx,
897                    contract.clone(),
898                    global_context.clone(),
899                    context.clone(),
900                    common.clone(),
901                    error.clone(),
902                ))
903            },
904        )]
905    }
906
907    async fn transition_canceled(
908        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
909        contract: OutgoingContractAccount,
910        global_context: DynGlobalClientContext,
911        context: GatewayClientContext,
912        common: GatewayPayCommon,
913        error: OutgoingPaymentError,
914    ) -> GatewayPayStateMachine {
915        info!("Canceling outgoing contract {contract:?}");
916
917        context
918            .client_ctx
919            .log_event(
920                &mut dbtx.module_tx(),
921                OutgoingPaymentFailed {
922                    outgoing_contract: contract.clone(),
923                    contract_id: contract.contract.contract_id(),
924                    error: error.clone(),
925                },
926            )
927            .await;
928
929        let cancel_signature = context.secp.sign_schnorr(
930            &bitcoin::secp256k1::Message::from_digest(
931                *contract.contract.cancellation_message().as_ref(),
932            ),
933            &context.redeem_key,
934        );
935        let cancel_output = LightningOutput::new_v0_cancel_outgoing(
936            contract.contract.contract_id(),
937            cancel_signature,
938        );
939        let client_output = ClientOutput::<LightningOutput> {
940            output: cancel_output,
941            amount: Amount::ZERO,
942        };
943
944        match global_context
945            .fund_output(dbtx, ClientOutputBundle::new_no_sm(vec![client_output]))
946            .await
947        {
948            Ok(change_range) => {
949                info!(
950                    "Canceled outgoing contract {contract:?} with txid {:?}",
951                    change_range.txid()
952                );
953                GatewayPayStateMachine {
954                    common,
955                    state: GatewayPayStates::Canceled {
956                        txid: change_range.txid(),
957                        contract_id: contract.contract.contract_id(),
958                        error,
959                    },
960                }
961            }
962            Err(e) => {
963                warn!("Failed to cancel outgoing contract {contract:?}: {e:?}");
964                GatewayPayStateMachine {
965                    common,
966                    state: GatewayPayStates::Failed {
967                        error,
968                        error_message: format!(
969                            "Failed to submit refund transaction to federation {e:?}"
970                        ),
971                    },
972                }
973            }
974        }
975    }
976}