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