Skip to main content

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::util::FmtCompact as _;
14use fedimint_core::{Amount, OutPoint, TransactionId, secp256k1};
15use fedimint_lightning::{LightningRpcError, PayInvoiceResponse};
16use fedimint_ln_client::api::LnFederationApi;
17use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData};
18use fedimint_ln_common::config::FeeToAmount;
19use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
20use fedimint_ln_common::contracts::{ContractId, FundedContract, IdentifiableContract, Preimage};
21use fedimint_ln_common::{LightningInput, LightningOutput};
22use futures::future;
23use lightning_invoice::RoutingFees;
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26use tokio_stream::StreamExt;
27use tracing::{Instrument, debug, error, info, warn};
28
29use super::{GatewayClientContext, GatewayExtReceiveStates};
30use crate::events::{OutgoingPaymentFailed, OutgoingPaymentSucceeded};
31use crate::{GatewayClientModule, SwapParameters};
32
33const TIMELOCK_DELTA: u64 = 10;
34
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    #[error("The invoice amount plus the gateway fee overflows")]
167    InvoiceAmountTooLarge,
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    #[error("A payment for this payment hash was already accepted")]
189    PaymentHashAlreadyClaimed,
190}
191
192#[derive(
193    Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
194)]
195pub struct OutgoingPaymentError {
196    pub error_type: OutgoingPaymentErrorType,
197    pub contract_id: ContractId,
198    pub contract: Option<OutgoingContractAccount>,
199}
200
201impl Display for OutgoingPaymentError {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(f, "OutgoingContractError: {}", self.error_type)
204    }
205}
206
207#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
208pub struct GatewayPayInvoice {
209    pub pay_invoice_payload: PayInvoicePayload,
210}
211
212impl GatewayPayInvoice {
213    fn transitions(
214        &self,
215        global_context: DynGlobalClientContext,
216        context: &GatewayClientContext,
217        common: &GatewayPayCommon,
218    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
219        let payload = self.pay_invoice_payload.clone();
220        vec![StateTransition::new(
221            Self::fetch_parameters_and_pay(
222                global_context,
223                payload,
224                context.clone(),
225                common.clone(),
226            ),
227            |_dbtx, result, _old_state| Box::pin(futures::future::ready(result)),
228        )]
229    }
230
231    async fn fetch_parameters_and_pay(
232        global_context: DynGlobalClientContext,
233        pay_invoice_payload: PayInvoicePayload,
234        context: GatewayClientContext,
235        common: GatewayPayCommon,
236    ) -> GatewayPayStateMachine {
237        match Self::await_get_payment_parameters(
238            global_context,
239            context.clone(),
240            pay_invoice_payload.contract_id,
241            pay_invoice_payload.payment_data.clone(),
242            pay_invoice_payload.federation_id,
243        )
244        .await
245        {
246            Ok((contract, payment_parameters)) => {
247                Self::buy_preimage(
248                    context.clone(),
249                    contract.clone(),
250                    payment_parameters.clone(),
251                    common.clone(),
252                    pay_invoice_payload.clone(),
253                )
254                .await
255            }
256            Err(e) => {
257                warn!("Failed to get payment parameters: {e:?}");
258                match e.contract.clone() {
259                    Some(contract) => GatewayPayStateMachine {
260                        common,
261                        state: GatewayPayStates::CancelContract(Box::new(
262                            GatewayPayCancelContract { contract, error: e },
263                        )),
264                    },
265                    None => GatewayPayStateMachine {
266                        common,
267                        state: GatewayPayStates::OfferDoesNotExist(e.contract_id),
268                    },
269                }
270            }
271        }
272    }
273
274    /// Checks the gateway's database to determine if the current gateway
275    /// generated the invoice using the LNv2 protocol. If it did, the
276    /// gateway can buy the preimage and use it to claim the LNv1
277    /// `OutgoingContract`.
278    async fn buy_lnv2_preimage(
279        context: &GatewayClientContext,
280        contract: OutgoingContractAccount,
281        swap_parameters: SwapParameters,
282        common: GatewayPayCommon,
283        fresh_dispatch_refusal: Option<OutgoingContractError>,
284    ) -> Option<GatewayPayStateMachine> {
285        let amount = swap_parameters.amount_msat;
286        if let Ok(Some((lnv2_incoming_contract, client))) = context
287            .lightning_manager
288            .is_lnv2_direct_swap(swap_parameters.payment_hash, amount)
289            .await
290        {
291            let state = match client
292                .get_first_module::<fedimint_gwv2_client::GatewayClientModuleV2>()
293                .expect("Must have client module")
294                .relay_direct_swap(
295                    lnv2_incoming_contract,
296                    amount.msats,
297                    fresh_dispatch_refusal.is_none(),
298                )
299                .await
300            {
301                Ok(Some(final_receive_state)) => match final_receive_state {
302                    fedimint_gwv2_client::FinalReceiveState::Success(preimage) => {
303                        GatewayPayStateMachine {
304                            common,
305                            state: GatewayPayStates::ClaimOutgoingContract(Box::new(
306                                GatewayPayClaimOutgoingContract {
307                                    contract,
308                                    preimage: Preimage(preimage),
309                                },
310                            )),
311                        }
312                    }
313                    state => GatewayPayStateMachine {
314                        common,
315                        state: GatewayPayStates::CancelContract(Box::new(
316                            GatewayPayCancelContract {
317                                contract: contract.clone(),
318                                error: OutgoingPaymentError {
319                                    contract_id: contract.contract.contract_id(),
320                                    contract: Some(contract.clone()),
321                                    error_type: OutgoingPaymentErrorType::SwapFailed {
322                                        swap_error: format!(
323                                            "Failed to initiate LNv1 -> LNv2 swap. LNv2 state: {state:?}"
324                                        ),
325                                    },
326                                },
327                            },
328                        )),
329                    },
330                },
331                Ok(None) => {
332                    let error = fresh_dispatch_refusal
333                        .expect("the relay only refuses a fresh dispatch when one was denied");
334                    GatewayPayStateMachine {
335                        common,
336                        state: GatewayPayStates::CancelContract(Box::new(
337                            GatewayPayCancelContract {
338                                contract: contract.clone(),
339                                error: OutgoingPaymentError {
340                                    contract_id: contract.contract.contract_id(),
341                                    contract: Some(contract.clone()),
342                                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
343                                        error,
344                                    },
345                                },
346                            },
347                        )),
348                    }
349                }
350                Err(err) => GatewayPayStateMachine {
351                    common,
352                    state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
353                        contract: contract.clone(),
354                        error: OutgoingPaymentError {
355                            contract_id: contract.contract.contract_id(),
356                            contract: Some(contract.clone()),
357                            error_type: OutgoingPaymentErrorType::SwapFailed {
358                                swap_error: format!(
359                                    "Failed to initiate LNv1 -> LNv2 swap. Err: {}",
360                                    err.fmt_compact()
361                                ),
362                            },
363                        },
364                    })),
365                },
366            };
367
368            return Some(state);
369        }
370
371        None
372    }
373
374    async fn buy_preimage(
375        context: GatewayClientContext,
376        contract: OutgoingContractAccount,
377        payment_parameters: PaymentParameters,
378        common: GatewayPayCommon,
379        payload: PayInvoicePayload,
380    ) -> GatewayPayStateMachine {
381        debug!("Buying preimage contract {contract:?}");
382        // Verify that this client is authorized to receive the preimage.
383        if let Err(err) = context
384            .lightning_manager
385            .verify_preimage_authentication(
386                payload.payment_data.payment_hash(),
387                payload.preimage_auth,
388                contract.clone(),
389            )
390            .await
391        {
392            warn!("Preimage authentication failed: {err} for contract {contract:?}");
393            return GatewayPayStateMachine {
394                common,
395                state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
396                    contract,
397                    error: err,
398                })),
399            };
400        }
401
402        // Not all clients support LNv2 yet, so here we check if we are trying to pay an
403        // LNv2 invoice. If this gateway also supports LNv2, the gateway can do
404        // a swap between LNv1 `OutgoingContract` and an
405        // LNv2 `IncomingContract`.
406        if let Ok(swap_parameters) =
407            SwapParameters::try_from(payment_parameters.payment_data.clone())
408            && let Some(new_state) = Self::buy_lnv2_preimage(
409                &context,
410                contract.clone(),
411                swap_parameters,
412                common.clone(),
413                payment_parameters.fresh_dispatch.clone().err(),
414            )
415            .await
416        {
417            return new_state;
418        }
419
420        match context
421            .lightning_manager
422            .get_client_for_invoice(payment_parameters.payment_data.clone())
423            .await
424        {
425            Some(client) => {
426                client
427                    .with(|client| {
428                        Self::buy_preimage_via_direct_swap(
429                            client,
430                            payment_parameters.payment_data.clone(),
431                            contract.clone(),
432                            common.clone(),
433                            payment_parameters.fresh_dispatch.clone().err(),
434                        )
435                    })
436                    .await
437            }
438            _ => {
439                Self::buy_preimage_over_lightning(
440                    context,
441                    payment_parameters,
442                    contract.clone(),
443                    common.clone(),
444                )
445                .await
446            }
447        }
448    }
449
450    async fn await_get_payment_parameters(
451        global_context: DynGlobalClientContext,
452        context: GatewayClientContext,
453        contract_id: ContractId,
454        payment_data: PaymentData,
455        federation_id: FederationId,
456    ) -> Result<(OutgoingContractAccount, PaymentParameters), OutgoingPaymentError> {
457        debug!("Await payment parameters for outgoing contract {contract_id:?}");
458        let account = global_context
459            .module_api()
460            .await_contract(contract_id)
461            .await;
462
463        if let FundedContract::Outgoing(contract) = account.contract {
464            let outgoing_contract_account = OutgoingContractAccount {
465                amount: account.amount,
466                contract,
467            };
468
469            let consensus_block_count = global_context
470                .module_api()
471                .fetch_consensus_block_count()
472                .await
473                .map_err(|_| OutgoingPaymentError {
474                    contract_id,
475                    contract: Some(outgoing_contract_account.clone()),
476                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
477                        error: OutgoingContractError::TimeoutTooClose,
478                    },
479                })?;
480
481            debug!(
482                "Consensus block count: {consensus_block_count:?} for outgoing contract {contract_id:?}"
483            );
484            if consensus_block_count.is_none() {
485                return Err(OutgoingPaymentError {
486                    contract_id,
487                    contract: Some(outgoing_contract_account.clone()),
488                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
489                        error: OutgoingContractError::MissingContractData,
490                    },
491                });
492            }
493
494            let routing_fees = context
495                .lightning_manager
496                .get_routing_fees(federation_id)
497                .await
498                .ok_or(OutgoingPaymentError {
499                    error_type: OutgoingPaymentErrorType::InvalidFederationConfiguration,
500                    contract_id,
501                    contract: Some(outgoing_contract_account.clone()),
502                })?;
503
504            let payment_parameters = Self::validate_outgoing_account(
505                &outgoing_contract_account,
506                context.redeem_key,
507                consensus_block_count.unwrap(),
508                &payment_data,
509                routing_fees,
510            )
511            .map_err(|e| {
512                warn!("Invalid outgoing contract: {e:?}");
513                OutgoingPaymentError {
514                    contract_id,
515                    contract: Some(outgoing_contract_account.clone()),
516                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract { error: e },
517                }
518            })?;
519            debug!("Got payment parameters: {payment_parameters:?} for contract {contract_id:?}");
520            return Ok((outgoing_contract_account, payment_parameters));
521        }
522
523        error!("Contract {contract_id:?} is not an outgoing contract");
524        Err(OutgoingPaymentError {
525            contract_id,
526            contract: None,
527            error_type: OutgoingPaymentErrorType::OutgoingContractDoesNotExist { contract_id },
528        })
529    }
530
531    async fn buy_preimage_over_lightning(
532        context: GatewayClientContext,
533        buy_preimage: PaymentParameters,
534        contract: OutgoingContractAccount,
535        common: GatewayPayCommon,
536    ) -> GatewayPayStateMachine {
537        debug!("Buying preimage over lightning for contract {contract:?}");
538
539        // A payment the node already knows resolves through `pay`'s
540        // idempotent resume path, which never re-dispatches, so a drifted
541        // timelock or expiry gate only refuses a dispatch that never happened.
542        if let Err(error) = &buy_preimage.fresh_dispatch
543            && !context
544                .lightning_manager
545                .outbound_payment_exists(buy_preimage.payment_data.payment_hash())
546                .await
547        {
548            warn!(
549                ?contract,
550                err = %error.fmt_compact(),
551                "Refusing fresh lightning dispatch"
552            );
553            return GatewayPayStateMachine {
554                common,
555                state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
556                    contract: contract.clone(),
557                    error: OutgoingPaymentError {
558                        contract_id: contract.contract.contract_id(),
559                        contract: Some(contract),
560                        error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
561                            error: error.clone(),
562                        },
563                    },
564                })),
565            };
566        }
567
568        // On the resume path `pay` consults the node's own payment record
569        // before the budget, so `0` is a fail-closed sentinel: should that
570        // record have vanished in between, LND rejects a zero CLTV limit and
571        // LDK finds no route, rather than dispatching under a stale budget.
572        let max_delay = buy_preimage.fresh_dispatch.clone().unwrap_or(0);
573        let max_fee = buy_preimage.max_send_amount.saturating_sub(
574            buy_preimage
575                .payment_data
576                .amount()
577                .expect("We already checked that an amount was supplied"),
578        );
579
580        let payment_result = context
581            .lightning_manager
582            .pay(buy_preimage.payment_data, max_delay, max_fee)
583            .await;
584
585        match payment_result {
586            Ok(PayInvoiceResponse { preimage, .. }) => {
587                debug!("Preimage received for contract {contract:?}");
588                GatewayPayStateMachine {
589                    common,
590                    state: GatewayPayStates::ClaimOutgoingContract(Box::new(
591                        GatewayPayClaimOutgoingContract { contract, preimage },
592                    )),
593                }
594            }
595            Err(error) => Self::gateway_pay_cancel_contract(error, contract, common),
596        }
597    }
598
599    fn gateway_pay_cancel_contract(
600        error: LightningRpcError,
601        contract: OutgoingContractAccount,
602        common: GatewayPayCommon,
603    ) -> GatewayPayStateMachine {
604        warn!("Failed to buy preimage with {error} for contract {contract:?}");
605        let outgoing_error = OutgoingPaymentError {
606            contract_id: contract.contract.contract_id(),
607            contract: Some(contract.clone()),
608            error_type: OutgoingPaymentErrorType::LightningPayError {
609                lightning_error: error,
610            },
611        };
612        GatewayPayStateMachine {
613            common,
614            state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
615                contract,
616                error: outgoing_error,
617            })),
618        }
619    }
620
621    async fn buy_preimage_via_direct_swap(
622        client: ClientHandleArc,
623        payment_data: PaymentData,
624        contract: OutgoingContractAccount,
625        common: GatewayPayCommon,
626        fresh_dispatch_refusal: Option<OutgoingContractError>,
627    ) -> GatewayPayStateMachine {
628        debug!("Buying preimage via direct swap for contract {contract:?}");
629        match payment_data.try_into() {
630            Ok(swap_params) => match client
631                .get_first_module::<GatewayClientModule>()
632                .expect("Must have client module")
633                .gateway_handle_direct_swap(swap_params, fresh_dispatch_refusal.is_none())
634                .await
635            {
636                Ok(Some(operation_id)) => {
637                    debug!("Direct swap initiated for contract {contract:?}");
638                    GatewayPayStateMachine {
639                        common,
640                        state: GatewayPayStates::WaitForSwapPreimage(Box::new(
641                            GatewayPayWaitForSwapPreimage {
642                                contract,
643                                federation_id: client.federation_id(),
644                                operation_id,
645                            },
646                        )),
647                    }
648                }
649                Ok(None) => {
650                    let error = fresh_dispatch_refusal
651                        .expect("the relay only refuses a fresh dispatch when one was denied");
652                    GatewayPayStateMachine {
653                        common,
654                        state: GatewayPayStates::CancelContract(Box::new(
655                            GatewayPayCancelContract {
656                                contract: contract.clone(),
657                                error: OutgoingPaymentError {
658                                    contract_id: contract.contract.contract_id(),
659                                    contract: Some(contract.clone()),
660                                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
661                                        error,
662                                    },
663                                },
664                            },
665                        )),
666                    }
667                }
668                Err(e) => {
669                    info!(
670                        err = %e.fmt_compact(),
671                        "Failed to initiate direct swap for contract {contract:?}"
672                    );
673                    let outgoing_payment_error = OutgoingPaymentError {
674                        contract_id: contract.contract.contract_id(),
675                        contract: Some(contract.clone()),
676                        error_type: OutgoingPaymentErrorType::SwapFailed {
677                            swap_error: format!(
678                                "Failed to initiate direct swap: {}",
679                                e.fmt_compact()
680                            ),
681                        },
682                    };
683                    GatewayPayStateMachine {
684                        common,
685                        state: GatewayPayStates::CancelContract(Box::new(
686                            GatewayPayCancelContract {
687                                contract: contract.clone(),
688                                error: outgoing_payment_error,
689                            },
690                        )),
691                    }
692                }
693            },
694            Err(e) => {
695                info!(
696                    err = %e.fmt_compact(),
697                    "Failed to initiate direct swap for contract {contract:?}"
698                );
699                let outgoing_payment_error = OutgoingPaymentError {
700                    contract_id: contract.contract.contract_id(),
701                    contract: Some(contract.clone()),
702                    error_type: OutgoingPaymentErrorType::SwapFailed {
703                        swap_error: format!("Failed to initiate direct swap: {}", e.fmt_compact()),
704                    },
705                };
706                GatewayPayStateMachine {
707                    common,
708                    state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
709                        contract: contract.clone(),
710                        error: outgoing_payment_error,
711                    })),
712                }
713            }
714        }
715    }
716
717    fn validate_outgoing_account(
718        account: &OutgoingContractAccount,
719        redeem_key: bitcoin::key::Keypair,
720        consensus_block_count: u64,
721        payment_data: &PaymentData,
722        routing_fees: RoutingFees,
723    ) -> Result<PaymentParameters, OutgoingContractError> {
724        let our_pub_key = secp256k1::PublicKey::from_keypair(&redeem_key);
725
726        if account.contract.cancelled {
727            return Err(OutgoingContractError::CancelledContract);
728        }
729
730        if account.contract.gateway_key != our_pub_key {
731            return Err(OutgoingContractError::NotOurKey);
732        }
733
734        // The contract id and the payment data reach us as independent fields of
735        // `PayInvoicePayload`, and an outgoing contract carries no invoice, so nothing
736        // ties the two together implicitly. Without this check we would pay an invoice
737        // whose preimage cannot satisfy the contract we are being paid from.
738        if account.contract.hash != payment_data.payment_hash() {
739            return Err(OutgoingContractError::InvalidOutgoingContract {
740                contract_id: account.contract.contract_id(),
741            });
742        }
743
744        let payment_amount = payment_data
745            .amount()
746            .ok_or(OutgoingContractError::InvoiceMissingAmount)?;
747
748        // A pruned invoice carries a raw, caller-controlled amount. Add the fee
749        // with checked arithmetic so a huge amount cannot overflow `u64` and
750        // wrap the underfunding check below into passing against a near-empty
751        // contract.
752        let gateway_fee = routing_fees.to_amount(&payment_amount);
753        let necessary_contract_amount = payment_amount
754            .checked_add(gateway_fee)
755            .ok_or(OutgoingContractError::InvoiceAmountTooLarge)?;
756        if account.amount < necessary_contract_amount {
757            return Err(OutgoingContractError::Underfunded(
758                necessary_contract_amount,
759                account.amount,
760            ));
761        }
762
763        // `max_delay` becomes the lightning node's CLTV limit, and LND treats
764        // a limit of zero as "unset", enforcing its `--max-cltv-expiry`
765        // default instead. That would let the HTLC outlive the contract
766        // timelock, so zero must fail closed just like the underflow case.
767        let max_delay = u64::from(account.contract.timelock)
768            .checked_sub(consensus_block_count.saturating_sub(1))
769            .and_then(|delta| delta.checked_sub(TIMELOCK_DELTA))
770            .filter(|max_delay| *max_delay > 0);
771
772        // The timelock budget and invoice expiry drift with the chain tip and
773        // the wall clock, and this validation re-runs from scratch whenever
774        // the state machine restarts. Failing validation outright would
775        // cancel a payment that may have been dispatched before a crash and
776        // still be in flight -- one that settles or fails regardless of
777        // either gate -- returning the escrow while the payment can still
778        // claim it. They are therefore recorded as a refusal that each rail
779        // consults only before dispatching fresh; anything already started
780        // resumes unconditionally.
781        let fresh_dispatch = match max_delay {
782            None => Err(OutgoingContractError::TimeoutTooClose),
783            Some(_) if payment_data.is_expired() => Err(OutgoingContractError::InvoiceExpired(
784                payment_data.expiry_timestamp(),
785            )),
786            Some(max_delay) => Ok(max_delay),
787        };
788
789        Ok(PaymentParameters {
790            fresh_dispatch,
791            max_send_amount: account.amount,
792            payment_data: payment_data.clone(),
793        })
794    }
795}
796
797#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable, Serialize, Deserialize)]
798struct PaymentParameters {
799    /// `Ok` carries the CLTV budget a fresh dispatch must respect. `Err` means
800    /// a drifted pre-dispatch gate (timelock budget or invoice expiry) forbids
801    /// starting one. Each rail consults this only before initiating; a
802    /// dispatch that already exists resumes unconditionally.
803    fresh_dispatch: Result<u64, OutgoingContractError>,
804    max_send_amount: Amount,
805    payment_data: PaymentData,
806}
807
808#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
809pub struct GatewayPayClaimOutgoingContract {
810    contract: OutgoingContractAccount,
811    preimage: Preimage,
812}
813
814impl GatewayPayClaimOutgoingContract {
815    fn transitions(
816        &self,
817        global_context: DynGlobalClientContext,
818        context: GatewayClientContext,
819        common: GatewayPayCommon,
820    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
821        let contract = self.contract.clone();
822        let preimage = self.preimage.clone();
823        vec![StateTransition::new(
824            future::ready(()),
825            move |dbtx, (), _| {
826                Box::pin(Self::transition_claim_outgoing_contract(
827                    dbtx,
828                    global_context.clone(),
829                    context.clone(),
830                    common.clone(),
831                    contract.clone(),
832                    preimage.clone(),
833                ))
834            },
835        )]
836    }
837
838    async fn transition_claim_outgoing_contract(
839        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
840        global_context: DynGlobalClientContext,
841        context: GatewayClientContext,
842        common: GatewayPayCommon,
843        contract: OutgoingContractAccount,
844        preimage: Preimage,
845    ) -> GatewayPayStateMachine {
846        debug!("Claiming outgoing contract {contract:?}");
847
848        context
849            .client_ctx
850            .log_event(
851                &mut dbtx.module_tx(),
852                OutgoingPaymentSucceeded {
853                    outgoing_contract: contract.clone(),
854                    contract_id: contract.contract.contract_id(),
855                    preimage: preimage.consensus_encode_to_hex(),
856                },
857            )
858            .await;
859
860        let claim_input = contract.claim(preimage.clone());
861        let client_input = ClientInput::<LightningInput> {
862            input: claim_input,
863            amounts: Amounts::new_bitcoin(contract.amount),
864            keys: vec![context.redeem_key],
865        };
866
867        let out_points = global_context
868            .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
869            .await
870            .expect("Cannot claim input, additional funding needed")
871            .into_iter()
872            .collect();
873        debug!("Claimed outgoing contract {contract:?} with out points {out_points:?}");
874        GatewayPayStateMachine {
875            common,
876            state: GatewayPayStates::Preimage(out_points, preimage),
877        }
878    }
879}
880
881#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
882pub struct GatewayPayWaitForSwapPreimage {
883    contract: OutgoingContractAccount,
884    federation_id: FederationId,
885    operation_id: OperationId,
886}
887
888impl GatewayPayWaitForSwapPreimage {
889    fn transitions(
890        &self,
891        context: GatewayClientContext,
892        common: GatewayPayCommon,
893    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
894        let federation_id = self.federation_id;
895        let operation_id = self.operation_id;
896        let contract = self.contract.clone();
897        vec![StateTransition::new(
898            Self::await_preimage(context, federation_id, operation_id, contract.clone()),
899            move |_dbtx, result, _old_state| {
900                let common = common.clone();
901                let contract = contract.clone();
902                Box::pin(async {
903                    Self::transition_claim_outgoing_contract(common, result, contract)
904                })
905            },
906        )]
907    }
908
909    async fn await_preimage(
910        context: GatewayClientContext,
911        federation_id: FederationId,
912        operation_id: OperationId,
913        contract: OutgoingContractAccount,
914    ) -> Result<Preimage, OutgoingPaymentError> {
915        debug!("Waiting preimage for contract {contract:?}");
916
917        let client = context
918            .lightning_manager
919            .get_client(&federation_id)
920            .await
921            .ok_or(OutgoingPaymentError {
922                contract_id: contract.contract.contract_id(),
923                contract: Some(contract.clone()),
924                error_type: OutgoingPaymentErrorType::SwapFailed {
925                    swap_error: "Federation client not found".to_string(),
926                },
927            })?;
928
929        async {
930            let mut stream = client
931                .value()
932                .get_first_module::<GatewayClientModule>()
933                .expect("Must have client module")
934                .gateway_subscribe_ln_receive(operation_id)
935                .await
936                .map_err(|e| {
937                    let contract_id = contract.contract.contract_id();
938                    warn!(
939                        ?contract_id,
940                        err = %e.fmt_compact(),
941                        "Failed to subscribe to ln receive of direct swap"
942                    );
943                    OutgoingPaymentError {
944                        contract_id,
945                        contract: Some(contract.clone()),
946                        error_type: OutgoingPaymentErrorType::SwapFailed {
947                            swap_error: format!(
948                                "Failed to subscribe to ln receive of direct swap: {}",
949                                e.fmt_compact()
950                            ),
951                        },
952                    }
953                })?
954                .into_stream();
955
956            loop {
957                debug!("Waiting next state of preimage buy for contract {contract:?}");
958                if let Some(state) = stream.next().await {
959                    match state {
960                        GatewayExtReceiveStates::Funding => {
961                            debug!(?contract, "Funding");
962                            continue;
963                        }
964                        GatewayExtReceiveStates::Preimage(preimage) => {
965                            debug!(?contract, "Received preimage");
966                            return Ok(preimage);
967                        }
968                        other => {
969                            warn!(?contract, "Got state {other:?}");
970                            return Err(OutgoingPaymentError {
971                                contract_id: contract.contract.contract_id(),
972                                contract: Some(contract),
973                                error_type: OutgoingPaymentErrorType::SwapFailed {
974                                    swap_error: "Failed to receive preimage".to_string(),
975                                },
976                            });
977                        }
978                    }
979                }
980            }
981        }
982        .instrument(client.span())
983        .await
984    }
985
986    fn transition_claim_outgoing_contract(
987        common: GatewayPayCommon,
988        result: Result<Preimage, OutgoingPaymentError>,
989        contract: OutgoingContractAccount,
990    ) -> GatewayPayStateMachine {
991        match result {
992            Ok(preimage) => GatewayPayStateMachine {
993                common,
994                state: GatewayPayStates::ClaimOutgoingContract(Box::new(
995                    GatewayPayClaimOutgoingContract { contract, preimage },
996                )),
997            },
998            Err(e) => GatewayPayStateMachine {
999                common,
1000                state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
1001                    contract,
1002                    error: e,
1003                })),
1004            },
1005        }
1006    }
1007}
1008
1009#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
1010pub struct GatewayPayCancelContract {
1011    contract: OutgoingContractAccount,
1012    error: OutgoingPaymentError,
1013}
1014
1015impl GatewayPayCancelContract {
1016    pub(super) fn for_claimed_hash(contract: OutgoingContractAccount) -> Self {
1017        Self {
1018            error: OutgoingPaymentError {
1019                error_type: OutgoingPaymentErrorType::PaymentHashAlreadyClaimed,
1020                contract_id: contract.contract.contract_id(),
1021                contract: Some(contract.clone()),
1022            },
1023            contract,
1024        }
1025    }
1026
1027    fn transitions(
1028        &self,
1029        global_context: DynGlobalClientContext,
1030        context: GatewayClientContext,
1031        common: GatewayPayCommon,
1032    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
1033        let contract = self.contract.clone();
1034        let error = self.error.clone();
1035        vec![StateTransition::new(
1036            future::ready(()),
1037            move |dbtx, (), _| {
1038                Box::pin(Self::transition_canceled(
1039                    dbtx,
1040                    contract.clone(),
1041                    global_context.clone(),
1042                    context.clone(),
1043                    common.clone(),
1044                    error.clone(),
1045                ))
1046            },
1047        )]
1048    }
1049
1050    async fn transition_canceled(
1051        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
1052        contract: OutgoingContractAccount,
1053        global_context: DynGlobalClientContext,
1054        context: GatewayClientContext,
1055        common: GatewayPayCommon,
1056        error: OutgoingPaymentError,
1057    ) -> GatewayPayStateMachine {
1058        info!("Canceling outgoing contract {contract:?}");
1059
1060        context
1061            .client_ctx
1062            .log_event(
1063                &mut dbtx.module_tx(),
1064                OutgoingPaymentFailed {
1065                    outgoing_contract: contract.clone(),
1066                    contract_id: contract.contract.contract_id(),
1067                    error: error.clone(),
1068                },
1069            )
1070            .await;
1071
1072        let cancel_signature = context.secp.sign_schnorr(
1073            &bitcoin::secp256k1::Message::from_digest(
1074                *contract.contract.cancellation_message().as_ref(),
1075            ),
1076            &context.redeem_key,
1077        );
1078        let cancel_output = LightningOutput::new_v0_cancel_outgoing(
1079            contract.contract.contract_id(),
1080            cancel_signature,
1081        );
1082        let client_output = ClientOutput::<LightningOutput> {
1083            output: cancel_output,
1084            amounts: Amounts::ZERO,
1085        };
1086
1087        match global_context
1088            .fund_output(dbtx, ClientOutputBundle::new_no_sm(vec![client_output]))
1089            .await
1090        {
1091            Ok(change_range) => {
1092                info!(
1093                    "Canceled outgoing contract {contract:?} with txid {:?}",
1094                    change_range.txid()
1095                );
1096                GatewayPayStateMachine {
1097                    common,
1098                    state: GatewayPayStates::Canceled {
1099                        txid: change_range.txid(),
1100                        contract_id: contract.contract.contract_id(),
1101                        error,
1102                    },
1103                }
1104            }
1105            Err(e) => {
1106                warn!("Failed to cancel outgoing contract {contract:?}: {e:?}");
1107                GatewayPayStateMachine {
1108                    common,
1109                    state: GatewayPayStates::Failed {
1110                        error,
1111                        error_message: format!(
1112                            "Failed to submit refund transaction to federation {e:?}"
1113                        ),
1114                    },
1115                }
1116            }
1117        }
1118    }
1119}
1120
1121#[cfg(test)]
1122mod tests;