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