ln_gateway/state_machine/
pay.rs

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