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