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