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#[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 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 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 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 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 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 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 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 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 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 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 fn contract_account(hash: sha256::Hash) -> OutgoingContractAccount {
1125 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 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 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 #[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 #[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 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 #[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 #[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 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 #[test]
1310 fn rejects_invoice_amount_that_would_overflow_the_underfunding_check() {
1311 let hash = sha256::Hash::hash(b"preimage");
1312
1313 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 assert_eq!(
1333 validate_amount(Amount::from_msats(1), Amount::from_msats(u64::MAX)),
1334 Err(OutgoingContractError::InvoiceAmountTooLarge)
1335 );
1336
1337 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}