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