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