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