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