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