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}
190
191#[derive(
192    Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
193)]
194pub struct OutgoingPaymentError {
195    pub error_type: OutgoingPaymentErrorType,
196    pub contract_id: ContractId,
197    pub contract: Option<OutgoingContractAccount>,
198}
199
200impl Display for OutgoingPaymentError {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        write!(f, "OutgoingContractError: {}", self.error_type)
203    }
204}
205
206#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
207pub struct GatewayPayInvoice {
208    pub pay_invoice_payload: PayInvoicePayload,
209}
210
211impl GatewayPayInvoice {
212    fn transitions(
213        &self,
214        global_context: DynGlobalClientContext,
215        context: &GatewayClientContext,
216        common: &GatewayPayCommon,
217    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
218        let payload = self.pay_invoice_payload.clone();
219        vec![StateTransition::new(
220            Self::fetch_parameters_and_pay(
221                global_context,
222                payload,
223                context.clone(),
224                common.clone(),
225            ),
226            |_dbtx, result, _old_state| Box::pin(futures::future::ready(result)),
227        )]
228    }
229
230    async fn fetch_parameters_and_pay(
231        global_context: DynGlobalClientContext,
232        pay_invoice_payload: PayInvoicePayload,
233        context: GatewayClientContext,
234        common: GatewayPayCommon,
235    ) -> GatewayPayStateMachine {
236        match Self::await_get_payment_parameters(
237            global_context,
238            context.clone(),
239            pay_invoice_payload.contract_id,
240            pay_invoice_payload.payment_data.clone(),
241            pay_invoice_payload.federation_id,
242        )
243        .await
244        {
245            Ok((contract, payment_parameters)) => {
246                Self::buy_preimage(
247                    context.clone(),
248                    contract.clone(),
249                    payment_parameters.clone(),
250                    common.clone(),
251                    pay_invoice_payload.clone(),
252                )
253                .await
254            }
255            Err(e) => {
256                warn!("Failed to get payment parameters: {e:?}");
257                match e.contract.clone() {
258                    Some(contract) => GatewayPayStateMachine {
259                        common,
260                        state: GatewayPayStates::CancelContract(Box::new(
261                            GatewayPayCancelContract { contract, error: e },
262                        )),
263                    },
264                    None => GatewayPayStateMachine {
265                        common,
266                        state: GatewayPayStates::OfferDoesNotExist(e.contract_id),
267                    },
268                }
269            }
270        }
271    }
272
273    /// Checks the gateway's database to determine if the current gateway
274    /// generated the invoice using the LNv2 protocol. If it did, the
275    /// gateway can buy the preimage and use it to claim the LNv1
276    /// `OutgoingContract`.
277    async fn buy_lnv2_preimage(
278        context: &GatewayClientContext,
279        contract: OutgoingContractAccount,
280        swap_parameters: SwapParameters,
281        common: GatewayPayCommon,
282        fresh_dispatch_refusal: Option<OutgoingContractError>,
283    ) -> Option<GatewayPayStateMachine> {
284        let amount = swap_parameters.amount_msat;
285        if let Ok(Some((lnv2_incoming_contract, client))) = context
286            .lightning_manager
287            .is_lnv2_direct_swap(swap_parameters.payment_hash, amount)
288            .await
289        {
290            let state = match client
291                .get_first_module::<fedimint_gwv2_client::GatewayClientModuleV2>()
292                .expect("Must have client module")
293                .relay_direct_swap(
294                    lnv2_incoming_contract,
295                    amount.msats,
296                    fresh_dispatch_refusal.is_none(),
297                )
298                .await
299            {
300                Ok(Some(final_receive_state)) => match final_receive_state {
301                    fedimint_gwv2_client::FinalReceiveState::Success(preimage) => {
302                        GatewayPayStateMachine {
303                            common,
304                            state: GatewayPayStates::ClaimOutgoingContract(Box::new(
305                                GatewayPayClaimOutgoingContract {
306                                    contract,
307                                    preimage: Preimage(preimage),
308                                },
309                            )),
310                        }
311                    }
312                    state => GatewayPayStateMachine {
313                        common,
314                        state: GatewayPayStates::CancelContract(Box::new(
315                            GatewayPayCancelContract {
316                                contract: contract.clone(),
317                                error: OutgoingPaymentError {
318                                    contract_id: contract.contract.contract_id(),
319                                    contract: Some(contract.clone()),
320                                    error_type: OutgoingPaymentErrorType::SwapFailed {
321                                        swap_error: format!(
322                                            "Failed to initiate LNv1 -> LNv2 swap. LNv2 state: {state:?}"
323                                        ),
324                                    },
325                                },
326                            },
327                        )),
328                    },
329                },
330                Ok(None) => {
331                    let error = fresh_dispatch_refusal
332                        .expect("the relay only refuses a fresh dispatch when one was denied");
333                    GatewayPayStateMachine {
334                        common,
335                        state: GatewayPayStates::CancelContract(Box::new(
336                            GatewayPayCancelContract {
337                                contract: contract.clone(),
338                                error: OutgoingPaymentError {
339                                    contract_id: contract.contract.contract_id(),
340                                    contract: Some(contract.clone()),
341                                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
342                                        error,
343                                    },
344                                },
345                            },
346                        )),
347                    }
348                }
349                Err(err) => GatewayPayStateMachine {
350                    common,
351                    state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
352                        contract: contract.clone(),
353                        error: OutgoingPaymentError {
354                            contract_id: contract.contract.contract_id(),
355                            contract: Some(contract.clone()),
356                            error_type: OutgoingPaymentErrorType::SwapFailed {
357                                swap_error: format!(
358                                    "Failed to initiate LNv1 -> LNv2 swap. Err: {err}"
359                                ),
360                            },
361                        },
362                    })),
363                },
364            };
365
366            return Some(state);
367        }
368
369        None
370    }
371
372    async fn buy_preimage(
373        context: GatewayClientContext,
374        contract: OutgoingContractAccount,
375        payment_parameters: PaymentParameters,
376        common: GatewayPayCommon,
377        payload: PayInvoicePayload,
378    ) -> GatewayPayStateMachine {
379        debug!("Buying preimage contract {contract:?}");
380        // Verify that this client is authorized to receive the preimage.
381        if let Err(err) = context
382            .lightning_manager
383            .verify_preimage_authentication(
384                payload.payment_data.payment_hash(),
385                payload.preimage_auth,
386                contract.clone(),
387            )
388            .await
389        {
390            warn!("Preimage authentication failed: {err} for contract {contract:?}");
391            return GatewayPayStateMachine {
392                common,
393                state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
394                    contract,
395                    error: err,
396                })),
397            };
398        }
399
400        // Not all clients support LNv2 yet, so here we check if we are trying to pay an
401        // LNv2 invoice. If this gateway also supports LNv2, the gateway can do
402        // a swap between LNv1 `OutgoingContract` and an
403        // LNv2 `IncomingContract`.
404        let swap_parameters: anyhow::Result<SwapParameters> =
405            payment_parameters.payment_data.clone().try_into();
406        if let Ok(swap_parameters) = swap_parameters
407            && let Some(new_state) = Self::buy_lnv2_preimage(
408                &context,
409                contract.clone(),
410                swap_parameters,
411                common.clone(),
412                payment_parameters.fresh_dispatch.clone().err(),
413            )
414            .await
415        {
416            return new_state;
417        }
418
419        match context
420            .lightning_manager
421            .get_client_for_invoice(payment_parameters.payment_data.clone())
422            .await
423        {
424            Some(client) => {
425                client
426                    .with(|client| {
427                        Self::buy_preimage_via_direct_swap(
428                            client,
429                            payment_parameters.payment_data.clone(),
430                            contract.clone(),
431                            common.clone(),
432                            payment_parameters.fresh_dispatch.clone().err(),
433                        )
434                    })
435                    .await
436            }
437            _ => {
438                Self::buy_preimage_over_lightning(
439                    context,
440                    payment_parameters,
441                    contract.clone(),
442                    common.clone(),
443                )
444                .await
445            }
446        }
447    }
448
449    async fn await_get_payment_parameters(
450        global_context: DynGlobalClientContext,
451        context: GatewayClientContext,
452        contract_id: ContractId,
453        payment_data: PaymentData,
454        federation_id: FederationId,
455    ) -> Result<(OutgoingContractAccount, PaymentParameters), OutgoingPaymentError> {
456        debug!("Await payment parameters for outgoing contract {contract_id:?}");
457        let account = global_context
458            .module_api()
459            .await_contract(contract_id)
460            .await;
461
462        if let FundedContract::Outgoing(contract) = account.contract {
463            let outgoing_contract_account = OutgoingContractAccount {
464                amount: account.amount,
465                contract,
466            };
467
468            let consensus_block_count = global_context
469                .module_api()
470                .fetch_consensus_block_count()
471                .await
472                .map_err(|_| OutgoingPaymentError {
473                    contract_id,
474                    contract: Some(outgoing_contract_account.clone()),
475                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
476                        error: OutgoingContractError::TimeoutTooClose,
477                    },
478                })?;
479
480            debug!(
481                "Consensus block count: {consensus_block_count:?} for outgoing contract {contract_id:?}"
482            );
483            if consensus_block_count.is_none() {
484                return Err(OutgoingPaymentError {
485                    contract_id,
486                    contract: Some(outgoing_contract_account.clone()),
487                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
488                        error: OutgoingContractError::MissingContractData,
489                    },
490                });
491            }
492
493            let routing_fees = context
494                .lightning_manager
495                .get_routing_fees(federation_id)
496                .await
497                .ok_or(OutgoingPaymentError {
498                    error_type: OutgoingPaymentErrorType::InvalidFederationConfiguration,
499                    contract_id,
500                    contract: Some(outgoing_contract_account.clone()),
501                })?;
502
503            let payment_parameters = Self::validate_outgoing_account(
504                &outgoing_contract_account,
505                context.redeem_key,
506                consensus_block_count.unwrap(),
507                &payment_data,
508                routing_fees,
509            )
510            .map_err(|e| {
511                warn!("Invalid outgoing contract: {e:?}");
512                OutgoingPaymentError {
513                    contract_id,
514                    contract: Some(outgoing_contract_account.clone()),
515                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract { error: e },
516                }
517            })?;
518            debug!("Got payment parameters: {payment_parameters:?} for contract {contract_id:?}");
519            return Ok((outgoing_contract_account, payment_parameters));
520        }
521
522        error!("Contract {contract_id:?} is not an outgoing contract");
523        Err(OutgoingPaymentError {
524            contract_id,
525            contract: None,
526            error_type: OutgoingPaymentErrorType::OutgoingContractDoesNotExist { contract_id },
527        })
528    }
529
530    async fn buy_preimage_over_lightning(
531        context: GatewayClientContext,
532        buy_preimage: PaymentParameters,
533        contract: OutgoingContractAccount,
534        common: GatewayPayCommon,
535    ) -> GatewayPayStateMachine {
536        debug!("Buying preimage over lightning for contract {contract:?}");
537
538        // A payment the node already knows resolves through `pay`'s
539        // idempotent resume path, which never re-dispatches, so a drifted
540        // timelock or expiry gate only refuses a dispatch that never happened.
541        if let Err(error) = &buy_preimage.fresh_dispatch
542            && !context
543                .lightning_manager
544                .outbound_payment_exists(buy_preimage.payment_data.payment_hash())
545                .await
546        {
547            warn!(
548                ?contract,
549                err = %error.fmt_compact(),
550                "Refusing fresh lightning dispatch"
551            );
552            return GatewayPayStateMachine {
553                common,
554                state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
555                    contract: contract.clone(),
556                    error: OutgoingPaymentError {
557                        contract_id: contract.contract.contract_id(),
558                        contract: Some(contract),
559                        error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
560                            error: error.clone(),
561                        },
562                    },
563                })),
564            };
565        }
566
567        // On the resume path `pay` consults the node's own payment record
568        // before the budget, so `0` is a fail-closed sentinel: should that
569        // record have vanished in between, LND rejects a zero CLTV limit and
570        // LDK finds no route, rather than dispatching under a stale budget.
571        let max_delay = buy_preimage.fresh_dispatch.clone().unwrap_or(0);
572        let max_fee = buy_preimage.max_send_amount.saturating_sub(
573            buy_preimage
574                .payment_data
575                .amount()
576                .expect("We already checked that an amount was supplied"),
577        );
578
579        let payment_result = context
580            .lightning_manager
581            .pay(buy_preimage.payment_data, max_delay, max_fee)
582            .await;
583
584        match payment_result {
585            Ok(PayInvoiceResponse { preimage, .. }) => {
586                debug!("Preimage received for contract {contract:?}");
587                GatewayPayStateMachine {
588                    common,
589                    state: GatewayPayStates::ClaimOutgoingContract(Box::new(
590                        GatewayPayClaimOutgoingContract { contract, preimage },
591                    )),
592                }
593            }
594            Err(error) => Self::gateway_pay_cancel_contract(error, contract, common),
595        }
596    }
597
598    fn gateway_pay_cancel_contract(
599        error: LightningRpcError,
600        contract: OutgoingContractAccount,
601        common: GatewayPayCommon,
602    ) -> GatewayPayStateMachine {
603        warn!("Failed to buy preimage with {error} for contract {contract:?}");
604        let outgoing_error = OutgoingPaymentError {
605            contract_id: contract.contract.contract_id(),
606            contract: Some(contract.clone()),
607            error_type: OutgoingPaymentErrorType::LightningPayError {
608                lightning_error: error,
609            },
610        };
611        GatewayPayStateMachine {
612            common,
613            state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
614                contract,
615                error: outgoing_error,
616            })),
617        }
618    }
619
620    async fn buy_preimage_via_direct_swap(
621        client: ClientHandleArc,
622        payment_data: PaymentData,
623        contract: OutgoingContractAccount,
624        common: GatewayPayCommon,
625        fresh_dispatch_refusal: Option<OutgoingContractError>,
626    ) -> GatewayPayStateMachine {
627        debug!("Buying preimage via direct swap for contract {contract:?}");
628        match payment_data.try_into() {
629            Ok(swap_params) => match client
630                .get_first_module::<GatewayClientModule>()
631                .expect("Must have client module")
632                .gateway_handle_direct_swap(swap_params, fresh_dispatch_refusal.is_none())
633                .await
634            {
635                Ok(Some(operation_id)) => {
636                    debug!("Direct swap initiated for contract {contract:?}");
637                    GatewayPayStateMachine {
638                        common,
639                        state: GatewayPayStates::WaitForSwapPreimage(Box::new(
640                            GatewayPayWaitForSwapPreimage {
641                                contract,
642                                federation_id: client.federation_id(),
643                                operation_id,
644                            },
645                        )),
646                    }
647                }
648                Ok(None) => {
649                    let error = fresh_dispatch_refusal
650                        .expect("the relay only refuses a fresh dispatch when one was denied");
651                    GatewayPayStateMachine {
652                        common,
653                        state: GatewayPayStates::CancelContract(Box::new(
654                            GatewayPayCancelContract {
655                                contract: contract.clone(),
656                                error: OutgoingPaymentError {
657                                    contract_id: contract.contract.contract_id(),
658                                    contract: Some(contract.clone()),
659                                    error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
660                                        error,
661                                    },
662                                },
663                            },
664                        )),
665                    }
666                }
667                Err(e) => {
668                    info!("Failed to initiate direct swap: {e:?} for contract {contract:?}");
669                    let outgoing_payment_error = OutgoingPaymentError {
670                        contract_id: contract.contract.contract_id(),
671                        contract: Some(contract.clone()),
672                        error_type: OutgoingPaymentErrorType::SwapFailed {
673                            swap_error: format!("Failed to initiate direct swap: {e}"),
674                        },
675                    };
676                    GatewayPayStateMachine {
677                        common,
678                        state: GatewayPayStates::CancelContract(Box::new(
679                            GatewayPayCancelContract {
680                                contract: contract.clone(),
681                                error: outgoing_payment_error,
682                            },
683                        )),
684                    }
685                }
686            },
687            Err(e) => {
688                info!("Failed to initiate direct swap: {e:?} for contract {contract:?}");
689                let outgoing_payment_error = OutgoingPaymentError {
690                    contract_id: contract.contract.contract_id(),
691                    contract: Some(contract.clone()),
692                    error_type: OutgoingPaymentErrorType::SwapFailed {
693                        swap_error: format!("Failed to initiate direct swap: {e}"),
694                    },
695                };
696                GatewayPayStateMachine {
697                    common,
698                    state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
699                        contract: contract.clone(),
700                        error: outgoing_payment_error,
701                    })),
702                }
703            }
704        }
705    }
706
707    fn validate_outgoing_account(
708        account: &OutgoingContractAccount,
709        redeem_key: bitcoin::key::Keypair,
710        consensus_block_count: u64,
711        payment_data: &PaymentData,
712        routing_fees: RoutingFees,
713    ) -> Result<PaymentParameters, OutgoingContractError> {
714        let our_pub_key = secp256k1::PublicKey::from_keypair(&redeem_key);
715
716        if account.contract.cancelled {
717            return Err(OutgoingContractError::CancelledContract);
718        }
719
720        if account.contract.gateway_key != our_pub_key {
721            return Err(OutgoingContractError::NotOurKey);
722        }
723
724        // The contract id and the payment data reach us as independent fields of
725        // `PayInvoicePayload`, and an outgoing contract carries no invoice, so nothing
726        // ties the two together implicitly. Without this check we would pay an invoice
727        // whose preimage cannot satisfy the contract we are being paid from.
728        if account.contract.hash != payment_data.payment_hash() {
729            return Err(OutgoingContractError::InvalidOutgoingContract {
730                contract_id: account.contract.contract_id(),
731            });
732        }
733
734        let payment_amount = payment_data
735            .amount()
736            .ok_or(OutgoingContractError::InvoiceMissingAmount)?;
737
738        // A pruned invoice carries a raw, caller-controlled amount. Add the fee
739        // with checked arithmetic so a huge amount cannot overflow `u64` and
740        // wrap the underfunding check below into passing against a near-empty
741        // contract.
742        let gateway_fee = routing_fees.to_amount(&payment_amount);
743        let necessary_contract_amount = payment_amount
744            .checked_add(gateway_fee)
745            .ok_or(OutgoingContractError::InvoiceAmountTooLarge)?;
746        if account.amount < necessary_contract_amount {
747            return Err(OutgoingContractError::Underfunded(
748                necessary_contract_amount,
749                account.amount,
750            ));
751        }
752
753        // `max_delay` becomes the lightning node's CLTV limit, and LND treats
754        // a limit of zero as "unset", enforcing its `--max-cltv-expiry`
755        // default instead. That would let the HTLC outlive the contract
756        // timelock, so zero must fail closed just like the underflow case.
757        let max_delay = u64::from(account.contract.timelock)
758            .checked_sub(consensus_block_count.saturating_sub(1))
759            .and_then(|delta| delta.checked_sub(TIMELOCK_DELTA))
760            .filter(|max_delay| *max_delay > 0);
761
762        // The timelock budget and invoice expiry drift with the chain tip and
763        // the wall clock, and this validation re-runs from scratch whenever
764        // the state machine restarts. Failing validation outright would
765        // cancel a payment that may have been dispatched before a crash and
766        // still be in flight -- one that settles or fails regardless of
767        // either gate -- returning the escrow while the payment can still
768        // claim it. They are therefore recorded as a refusal that each rail
769        // consults only before dispatching fresh; anything already started
770        // resumes unconditionally.
771        let fresh_dispatch = match max_delay {
772            None => Err(OutgoingContractError::TimeoutTooClose),
773            Some(_) if payment_data.is_expired() => Err(OutgoingContractError::InvoiceExpired(
774                payment_data.expiry_timestamp(),
775            )),
776            Some(max_delay) => Ok(max_delay),
777        };
778
779        Ok(PaymentParameters {
780            fresh_dispatch,
781            max_send_amount: account.amount,
782            payment_data: payment_data.clone(),
783        })
784    }
785}
786
787#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable, Serialize, Deserialize)]
788struct PaymentParameters {
789    /// `Ok` carries the CLTV budget a fresh dispatch must respect. `Err` means
790    /// a drifted pre-dispatch gate (timelock budget or invoice expiry) forbids
791    /// starting one. Each rail consults this only before initiating; a
792    /// dispatch that already exists resumes unconditionally.
793    fresh_dispatch: Result<u64, OutgoingContractError>,
794    max_send_amount: Amount,
795    payment_data: PaymentData,
796}
797
798#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
799pub struct GatewayPayClaimOutgoingContract {
800    contract: OutgoingContractAccount,
801    preimage: Preimage,
802}
803
804impl GatewayPayClaimOutgoingContract {
805    fn transitions(
806        &self,
807        global_context: DynGlobalClientContext,
808        context: GatewayClientContext,
809        common: GatewayPayCommon,
810    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
811        let contract = self.contract.clone();
812        let preimage = self.preimage.clone();
813        vec![StateTransition::new(
814            future::ready(()),
815            move |dbtx, (), _| {
816                Box::pin(Self::transition_claim_outgoing_contract(
817                    dbtx,
818                    global_context.clone(),
819                    context.clone(),
820                    common.clone(),
821                    contract.clone(),
822                    preimage.clone(),
823                ))
824            },
825        )]
826    }
827
828    async fn transition_claim_outgoing_contract(
829        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
830        global_context: DynGlobalClientContext,
831        context: GatewayClientContext,
832        common: GatewayPayCommon,
833        contract: OutgoingContractAccount,
834        preimage: Preimage,
835    ) -> GatewayPayStateMachine {
836        debug!("Claiming outgoing contract {contract:?}");
837
838        context
839            .client_ctx
840            .log_event(
841                &mut dbtx.module_tx(),
842                OutgoingPaymentSucceeded {
843                    outgoing_contract: contract.clone(),
844                    contract_id: contract.contract.contract_id(),
845                    preimage: preimage.consensus_encode_to_hex(),
846                },
847            )
848            .await;
849
850        let claim_input = contract.claim(preimage.clone());
851        let client_input = ClientInput::<LightningInput> {
852            input: claim_input,
853            amounts: Amounts::new_bitcoin(contract.amount),
854            keys: vec![context.redeem_key],
855        };
856
857        let out_points = global_context
858            .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
859            .await
860            .expect("Cannot claim input, additional funding needed")
861            .into_iter()
862            .collect();
863        debug!("Claimed outgoing contract {contract:?} with out points {out_points:?}");
864        GatewayPayStateMachine {
865            common,
866            state: GatewayPayStates::Preimage(out_points, preimage),
867        }
868    }
869}
870
871#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
872pub struct GatewayPayWaitForSwapPreimage {
873    contract: OutgoingContractAccount,
874    federation_id: FederationId,
875    operation_id: OperationId,
876}
877
878impl GatewayPayWaitForSwapPreimage {
879    fn transitions(
880        &self,
881        context: GatewayClientContext,
882        common: GatewayPayCommon,
883    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
884        let federation_id = self.federation_id;
885        let operation_id = self.operation_id;
886        let contract = self.contract.clone();
887        vec![StateTransition::new(
888            Self::await_preimage(context, federation_id, operation_id, contract.clone()),
889            move |_dbtx, result, _old_state| {
890                let common = common.clone();
891                let contract = contract.clone();
892                Box::pin(async {
893                    Self::transition_claim_outgoing_contract(common, result, contract)
894                })
895            },
896        )]
897    }
898
899    async fn await_preimage(
900        context: GatewayClientContext,
901        federation_id: FederationId,
902        operation_id: OperationId,
903        contract: OutgoingContractAccount,
904    ) -> Result<Preimage, OutgoingPaymentError> {
905        debug!("Waiting preimage for contract {contract:?}");
906
907        let client = context
908            .lightning_manager
909            .get_client(&federation_id)
910            .await
911            .ok_or(OutgoingPaymentError {
912                contract_id: contract.contract.contract_id(),
913                contract: Some(contract.clone()),
914                error_type: OutgoingPaymentErrorType::SwapFailed {
915                    swap_error: "Federation client not found".to_string(),
916                },
917            })?;
918
919        async {
920            let mut stream = client
921                .value()
922                .get_first_module::<GatewayClientModule>()
923                .expect("Must have client module")
924                .gateway_subscribe_ln_receive(operation_id)
925                .await
926                .map_err(|e| {
927                    let contract_id = contract.contract.contract_id();
928                    warn!(
929                        ?contract_id,
930                        "Failed to subscribe to ln receive of direct swap: {e:?}"
931                    );
932                    OutgoingPaymentError {
933                        contract_id,
934                        contract: Some(contract.clone()),
935                        error_type: OutgoingPaymentErrorType::SwapFailed {
936                            swap_error: format!(
937                                "Failed to subscribe to ln receive of direct swap: {e}"
938                            ),
939                        },
940                    }
941                })?
942                .into_stream();
943
944            loop {
945                debug!("Waiting next state of preimage buy for contract {contract:?}");
946                if let Some(state) = stream.next().await {
947                    match state {
948                        GatewayExtReceiveStates::Funding => {
949                            debug!(?contract, "Funding");
950                            continue;
951                        }
952                        GatewayExtReceiveStates::Preimage(preimage) => {
953                            debug!(?contract, "Received preimage");
954                            return Ok(preimage);
955                        }
956                        other => {
957                            warn!(?contract, "Got state {other:?}");
958                            return Err(OutgoingPaymentError {
959                                contract_id: contract.contract.contract_id(),
960                                contract: Some(contract),
961                                error_type: OutgoingPaymentErrorType::SwapFailed {
962                                    swap_error: "Failed to receive preimage".to_string(),
963                                },
964                            });
965                        }
966                    }
967                }
968            }
969        }
970        .instrument(client.span())
971        .await
972    }
973
974    fn transition_claim_outgoing_contract(
975        common: GatewayPayCommon,
976        result: Result<Preimage, OutgoingPaymentError>,
977        contract: OutgoingContractAccount,
978    ) -> GatewayPayStateMachine {
979        match result {
980            Ok(preimage) => GatewayPayStateMachine {
981                common,
982                state: GatewayPayStates::ClaimOutgoingContract(Box::new(
983                    GatewayPayClaimOutgoingContract { contract, preimage },
984                )),
985            },
986            Err(e) => GatewayPayStateMachine {
987                common,
988                state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
989                    contract,
990                    error: e,
991                })),
992            },
993        }
994    }
995}
996
997#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
998pub struct GatewayPayCancelContract {
999    contract: OutgoingContractAccount,
1000    error: OutgoingPaymentError,
1001}
1002
1003impl GatewayPayCancelContract {
1004    fn transitions(
1005        &self,
1006        global_context: DynGlobalClientContext,
1007        context: GatewayClientContext,
1008        common: GatewayPayCommon,
1009    ) -> Vec<StateTransition<GatewayPayStateMachine>> {
1010        let contract = self.contract.clone();
1011        let error = self.error.clone();
1012        vec![StateTransition::new(
1013            future::ready(()),
1014            move |dbtx, (), _| {
1015                Box::pin(Self::transition_canceled(
1016                    dbtx,
1017                    contract.clone(),
1018                    global_context.clone(),
1019                    context.clone(),
1020                    common.clone(),
1021                    error.clone(),
1022                ))
1023            },
1024        )]
1025    }
1026
1027    async fn transition_canceled(
1028        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
1029        contract: OutgoingContractAccount,
1030        global_context: DynGlobalClientContext,
1031        context: GatewayClientContext,
1032        common: GatewayPayCommon,
1033        error: OutgoingPaymentError,
1034    ) -> GatewayPayStateMachine {
1035        info!("Canceling outgoing contract {contract:?}");
1036
1037        context
1038            .client_ctx
1039            .log_event(
1040                &mut dbtx.module_tx(),
1041                OutgoingPaymentFailed {
1042                    outgoing_contract: contract.clone(),
1043                    contract_id: contract.contract.contract_id(),
1044                    error: error.clone(),
1045                },
1046            )
1047            .await;
1048
1049        let cancel_signature = context.secp.sign_schnorr(
1050            &bitcoin::secp256k1::Message::from_digest(
1051                *contract.contract.cancellation_message().as_ref(),
1052            ),
1053            &context.redeem_key,
1054        );
1055        let cancel_output = LightningOutput::new_v0_cancel_outgoing(
1056            contract.contract.contract_id(),
1057            cancel_signature,
1058        );
1059        let client_output = ClientOutput::<LightningOutput> {
1060            output: cancel_output,
1061            amounts: Amounts::ZERO,
1062        };
1063
1064        match global_context
1065            .fund_output(dbtx, ClientOutputBundle::new_no_sm(vec![client_output]))
1066            .await
1067        {
1068            Ok(change_range) => {
1069                info!(
1070                    "Canceled outgoing contract {contract:?} with txid {:?}",
1071                    change_range.txid()
1072                );
1073                GatewayPayStateMachine {
1074                    common,
1075                    state: GatewayPayStates::Canceled {
1076                        txid: change_range.txid(),
1077                        contract_id: contract.contract.contract_id(),
1078                        error,
1079                    },
1080                }
1081            }
1082            Err(e) => {
1083                warn!("Failed to cancel outgoing contract {contract:?}: {e:?}");
1084                GatewayPayStateMachine {
1085                    common,
1086                    state: GatewayPayStates::Failed {
1087                        error,
1088                        error_message: format!(
1089                            "Failed to submit refund transaction to federation {e:?}"
1090                        ),
1091                    },
1092                }
1093            }
1094        }
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use bitcoin::hashes::{Hash as _, sha256};
1101    use bitcoin::key::Keypair;
1102    use fedimint_core::Amount;
1103    use fedimint_core::secp256k1::{self, SecretKey};
1104    use fedimint_ln_client::pay::PaymentData;
1105    use fedimint_ln_common::PrunedInvoice;
1106    use fedimint_ln_common::contracts::IdentifiableContract as _;
1107    use fedimint_ln_common::contracts::outgoing::{OutgoingContract, OutgoingContractAccount};
1108    use lightning_invoice::RoutingFees;
1109
1110    use super::{GatewayPayInvoice, OutgoingContractError, TIMELOCK_DELTA};
1111
1112    const CONSENSUS_BLOCK_COUNT: u64 = 1;
1113    const INVOICE_AMOUNT: Amount = Amount::from_msats(1000);
1114
1115    fn gateway_keypair() -> Keypair {
1116        Keypair::from_secret_key(
1117            secp256k1::SECP256K1,
1118            &SecretKey::from_slice(&[1; 32]).expect("Valid secret key"),
1119        )
1120    }
1121
1122    /// An account that is valid in every respect other than the payment hash,
1123    /// which the caller chooses so a mismatch can be tested in isolation.
1124    fn contract_account(hash: sha256::Hash) -> OutgoingContractAccount {
1125        // Comfortably beyond `CONSENSUS_BLOCK_COUNT + TIMELOCK_DELTA`
1126        contract_account_with_timelock(hash, 100)
1127    }
1128
1129    fn contract_account_with_timelock(
1130        hash: sha256::Hash,
1131        timelock: u32,
1132    ) -> OutgoingContractAccount {
1133        contract_account_with(hash, INVOICE_AMOUNT, timelock)
1134    }
1135
1136    fn contract_account_with(
1137        hash: sha256::Hash,
1138        amount: Amount,
1139        timelock: u32,
1140    ) -> OutgoingContractAccount {
1141        let gateway_key = secp256k1::PublicKey::from_keypair(&gateway_keypair());
1142
1143        OutgoingContractAccount {
1144            amount,
1145            contract: OutgoingContract {
1146                hash,
1147                gateway_key,
1148                timelock,
1149                user_key: gateway_key,
1150                cancelled: false,
1151            },
1152        }
1153    }
1154
1155    fn payment_data(payment_hash: sha256::Hash) -> PaymentData {
1156        pruned_payment_data(payment_hash, INVOICE_AMOUNT)
1157    }
1158
1159    fn pruned_payment_data(payment_hash: sha256::Hash, amount: Amount) -> PaymentData {
1160        PaymentData::PrunedInvoice(PrunedInvoice {
1161            amount,
1162            destination: secp256k1::PublicKey::from_keypair(&gateway_keypair()),
1163            destination_features: vec![],
1164            payment_hash,
1165            payment_secret: [0; 32],
1166            route_hints: vec![],
1167            min_final_cltv_delta: 0,
1168            expiry_timestamp: u64::MAX,
1169        })
1170    }
1171
1172    fn validate(
1173        contract_hash: sha256::Hash,
1174        invoice_hash: sha256::Hash,
1175    ) -> Result<(), OutgoingContractError> {
1176        validate_account(&contract_account(contract_hash), invoice_hash)
1177    }
1178
1179    /// Surfaces a recorded fresh-dispatch refusal as an error so tests can
1180    /// assert on the drifting gates alongside the hard validation errors.
1181    fn validate_account(
1182        account: &OutgoingContractAccount,
1183        invoice_hash: sha256::Hash,
1184    ) -> Result<(), OutgoingContractError> {
1185        validate_payment_data(account, &payment_data(invoice_hash))
1186    }
1187
1188    fn validate_payment_data(
1189        account: &OutgoingContractAccount,
1190        payment_data: &PaymentData,
1191    ) -> Result<(), OutgoingContractError> {
1192        GatewayPayInvoice::validate_outgoing_account(
1193            account,
1194            gateway_keypair(),
1195            CONSENSUS_BLOCK_COUNT,
1196            payment_data,
1197            RoutingFees {
1198                base_msat: 0,
1199                proportional_millionths: 0,
1200            },
1201        )
1202        .and_then(|parameters| parameters.fresh_dispatch.map(|_| ()))
1203    }
1204
1205    /// Payment data whose invoice expired at the unix epoch.
1206    fn expired_payment_data(payment_hash: sha256::Hash) -> PaymentData {
1207        match payment_data(payment_hash) {
1208            PaymentData::PrunedInvoice(mut invoice) => {
1209                invoice.expiry_timestamp = 0;
1210                PaymentData::PrunedInvoice(invoice)
1211            }
1212            PaymentData::Invoice(..) => unreachable!("the fixture builds a pruned invoice"),
1213        }
1214    }
1215
1216    /// Guards against the fixture being invalid for some unrelated reason,
1217    /// which would make the rejection test below pass vacuously.
1218    #[test]
1219    fn accepts_contract_matching_the_invoice() {
1220        let hash = sha256::Hash::hash(b"preimage");
1221
1222        assert_eq!(validate(hash, hash), Ok(()));
1223    }
1224
1225    /// A timelock close enough to the consensus height that `max_delay`
1226    /// computes to zero must refuse a fresh dispatch: LND treats a CLTV limit
1227    /// of zero as "unset" and substitutes its `--max-cltv-expiry` default,
1228    /// which would let the HTLC outlive the contract timelock and the user
1229    /// refund the contract while the payment is still in flight. The refusal
1230    /// is recorded rather than failing validation so a payment dispatched
1231    /// before a restart can still resume.
1232    #[test]
1233    fn rejects_timelock_yielding_a_max_delay_of_zero() {
1234        let hash = sha256::Hash::hash(b"preimage");
1235        let zero_delay_timelock =
1236            u32::try_from(CONSENSUS_BLOCK_COUNT - 1 + TIMELOCK_DELTA).expect("small constant");
1237
1238        let validate_with_timelock =
1239            |timelock| validate_account(&contract_account_with_timelock(hash, timelock), hash);
1240
1241        // The smallest acceptable timelock, asserted so this test pins the
1242        // boundary rather than passing against a check that rejects
1243        // everything.
1244        assert_eq!(validate_with_timelock(zero_delay_timelock + 1), Ok(()));
1245
1246        assert_eq!(
1247            validate_with_timelock(zero_delay_timelock),
1248            Err(OutgoingContractError::TimeoutTooClose)
1249        );
1250        assert_eq!(
1251            validate_with_timelock(zero_delay_timelock - 1),
1252            Err(OutgoingContractError::TimeoutTooClose)
1253        );
1254    }
1255
1256    /// An expired invoice must refuse a fresh dispatch. Like the timelock
1257    /// gate, the refusal is recorded rather than failing validation, so a
1258    /// payment dispatched before a restart can still resume past it.
1259    #[test]
1260    fn records_refusal_for_an_expired_invoice() {
1261        let hash = sha256::Hash::hash(b"preimage");
1262
1263        assert_eq!(
1264            validate_payment_data(&contract_account(hash), &expired_payment_data(hash)),
1265            Err(OutgoingContractError::InvoiceExpired(0))
1266        );
1267    }
1268
1269    /// When both drifting gates fail, the timelock refusal is reported: with
1270    /// no timelock budget left the payment cannot be dispatched at all, so
1271    /// expiry never gets a say. Pinned so error reporting stays stable.
1272    #[test]
1273    fn timelock_refusal_takes_precedence_over_expiry() {
1274        let hash = sha256::Hash::hash(b"preimage");
1275        let zero_delay_timelock =
1276            u32::try_from(CONSENSUS_BLOCK_COUNT - 1 + TIMELOCK_DELTA).expect("small constant");
1277
1278        assert_eq!(
1279            validate_payment_data(
1280                &contract_account_with_timelock(hash, zero_delay_timelock),
1281                &expired_payment_data(hash),
1282            ),
1283            Err(OutgoingContractError::TimeoutTooClose)
1284        );
1285    }
1286
1287    #[test]
1288    fn rejects_contract_not_committing_to_the_invoice() {
1289        // A client picks the contract id and the invoice independently, so a
1290        // contract funded against an unrelated hash must not authorize paying
1291        // this invoice: the preimage we would obtain cannot claim the contract,
1292        // leaving the gateway out of pocket with no way to recover.
1293        let contract_hash = sha256::Hash::hash(b"contract preimage");
1294        let invoice_hash = sha256::Hash::hash(b"unrelated invoice preimage");
1295
1296        assert_eq!(
1297            validate(contract_hash, invoice_hash),
1298            Err(OutgoingContractError::InvalidOutgoingContract {
1299                contract_id: contract_account(contract_hash).contract.contract_id(),
1300            })
1301        );
1302    }
1303
1304    /// A pruned invoice's amount is a raw, caller-supplied `u64`. An amount so
1305    /// large that `payment_amount + fee` overflows must be rejected: otherwise
1306    /// the sum wraps to a small value, the underfunding check passes against a
1307    /// near-empty contract, and the gateway pays out real funds it can never
1308    /// reclaim.
1309    #[test]
1310    fn rejects_invoice_amount_that_would_overflow_the_underfunding_check() {
1311        let hash = sha256::Hash::hash(b"preimage");
1312
1313        // A one-millisatoshi base fee makes `u64::MAX + fee` wrap to zero, so
1314        // before the fix the underfunding check passed against any contract.
1315        let fees = RoutingFees {
1316            base_msat: 1,
1317            proportional_millionths: 0,
1318        };
1319
1320        let validate_amount = |contract_amount: Amount, invoice_amount: Amount| {
1321            GatewayPayInvoice::validate_outgoing_account(
1322                &contract_account_with(hash, contract_amount, 100),
1323                gateway_keypair(),
1324                CONSENSUS_BLOCK_COUNT,
1325                &pruned_payment_data(hash, invoice_amount),
1326                fees,
1327            )
1328            .map(|_| ())
1329        };
1330
1331        // The attack: a wrapping invoice amount against a near-empty contract.
1332        assert_eq!(
1333            validate_amount(Amount::from_msats(1), Amount::from_msats(u64::MAX)),
1334            Err(OutgoingContractError::InvoiceAmountTooLarge)
1335        );
1336
1337        // The largest amount whose sum with the fee still fits is accepted when
1338        // the contract funds it, so the guard rejects exactly the overflow and
1339        // nothing else.
1340        let largest_representable = Amount::from_msats(u64::MAX - u64::from(fees.base_msat));
1341        assert_eq!(
1342            validate_amount(Amount::from_msats(u64::MAX), largest_representable),
1343            Ok(())
1344        );
1345    }
1346}