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::GatewayClientModule;
29use crate::events::{OutgoingPaymentFailed, OutgoingPaymentSucceeded};
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_preimage(
270 context: GatewayClientContext,
271 contract: OutgoingContractAccount,
272 payment_parameters: PaymentParameters,
273 common: GatewayPayCommon,
274 payload: PayInvoicePayload,
275 ) -> GatewayPayStateMachine {
276 debug!("Buying preimage contract {contract:?}");
277 if let Err(err) = context
279 .lightning_manager
280 .verify_preimage_authentication(
281 payload.payment_data.payment_hash(),
282 payload.preimage_auth,
283 contract.clone(),
284 )
285 .await
286 {
287 warn!("Preimage authentication failed: {err} for contract {contract:?}");
288 return GatewayPayStateMachine {
289 common,
290 state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
291 contract,
292 error: err,
293 })),
294 };
295 }
296
297 match context
298 .lightning_manager
299 .get_client_for_invoice(payment_parameters.payment_data.clone())
300 .await
301 {
302 Some(client) => {
303 client
304 .with(|client| {
305 Self::buy_preimage_via_direct_swap(
306 client,
307 payment_parameters.payment_data.clone(),
308 contract.clone(),
309 common.clone(),
310 )
311 })
312 .await
313 }
314 _ => {
315 Self::buy_preimage_over_lightning(
316 context,
317 payment_parameters,
318 contract.clone(),
319 common.clone(),
320 )
321 .await
322 }
323 }
324 }
325
326 async fn await_get_payment_parameters(
327 global_context: DynGlobalClientContext,
328 context: GatewayClientContext,
329 contract_id: ContractId,
330 payment_data: PaymentData,
331 federation_id: FederationId,
332 ) -> Result<(OutgoingContractAccount, PaymentParameters), OutgoingPaymentError> {
333 debug!("Await payment parameters for outgoing contract {contract_id:?}");
334 let account = global_context
335 .module_api()
336 .await_contract(contract_id)
337 .await;
338
339 if let FundedContract::Outgoing(contract) = account.contract {
340 let outgoing_contract_account = OutgoingContractAccount {
341 amount: account.amount,
342 contract,
343 };
344
345 let consensus_block_count = global_context
346 .module_api()
347 .fetch_consensus_block_count()
348 .await
349 .map_err(|_| OutgoingPaymentError {
350 contract_id,
351 contract: Some(outgoing_contract_account.clone()),
352 error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
353 error: OutgoingContractError::TimeoutTooClose,
354 },
355 })?;
356
357 debug!(
358 "Consensus block count: {consensus_block_count:?} for outgoing contract {contract_id:?}"
359 );
360 if consensus_block_count.is_none() {
361 return Err(OutgoingPaymentError {
362 contract_id,
363 contract: Some(outgoing_contract_account.clone()),
364 error_type: OutgoingPaymentErrorType::InvalidOutgoingContract {
365 error: OutgoingContractError::MissingContractData,
366 },
367 });
368 }
369
370 let routing_fees = context
371 .lightning_manager
372 .get_routing_fees(federation_id)
373 .await
374 .ok_or(OutgoingPaymentError {
375 error_type: OutgoingPaymentErrorType::InvalidFederationConfiguration,
376 contract_id,
377 contract: Some(outgoing_contract_account.clone()),
378 })?;
379
380 let payment_parameters = Self::validate_outgoing_account(
381 &outgoing_contract_account,
382 context.redeem_key,
383 consensus_block_count.unwrap(),
384 &payment_data,
385 routing_fees,
386 )
387 .map_err(|e| {
388 warn!("Invalid outgoing contract: {e:?}");
389 OutgoingPaymentError {
390 contract_id,
391 contract: Some(outgoing_contract_account.clone()),
392 error_type: OutgoingPaymentErrorType::InvalidOutgoingContract { error: e },
393 }
394 })?;
395 debug!("Got payment parameters: {payment_parameters:?} for contract {contract_id:?}");
396 return Ok((outgoing_contract_account, payment_parameters));
397 }
398
399 error!("Contract {contract_id:?} is not an outgoing contract");
400 Err(OutgoingPaymentError {
401 contract_id,
402 contract: None,
403 error_type: OutgoingPaymentErrorType::OutgoingContractDoesNotExist { contract_id },
404 })
405 }
406
407 async fn buy_preimage_over_lightning(
408 context: GatewayClientContext,
409 buy_preimage: PaymentParameters,
410 contract: OutgoingContractAccount,
411 common: GatewayPayCommon,
412 ) -> GatewayPayStateMachine {
413 debug!("Buying preimage over lightning for contract {contract:?}");
414
415 let max_delay = buy_preimage.max_delay;
416 let max_fee = buy_preimage.max_send_amount.saturating_sub(
417 buy_preimage
418 .payment_data
419 .amount()
420 .expect("We already checked that an amount was supplied"),
421 );
422
423 let payment_result = context
424 .lightning_manager
425 .pay(buy_preimage.payment_data, max_delay, max_fee)
426 .await;
427
428 match payment_result {
429 Ok(PayInvoiceResponse { preimage, .. }) => {
430 debug!("Preimage received for contract {contract:?}");
431 GatewayPayStateMachine {
432 common,
433 state: GatewayPayStates::ClaimOutgoingContract(Box::new(
434 GatewayPayClaimOutgoingContract { contract, preimage },
435 )),
436 }
437 }
438 Err(error) => Self::gateway_pay_cancel_contract(error, contract, common),
439 }
440 }
441
442 fn gateway_pay_cancel_contract(
443 error: LightningRpcError,
444 contract: OutgoingContractAccount,
445 common: GatewayPayCommon,
446 ) -> GatewayPayStateMachine {
447 warn!("Failed to buy preimage with {error} for contract {contract:?}");
448 let outgoing_error = OutgoingPaymentError {
449 contract_id: contract.contract.contract_id(),
450 contract: Some(contract.clone()),
451 error_type: OutgoingPaymentErrorType::LightningPayError {
452 lightning_error: error,
453 },
454 };
455 GatewayPayStateMachine {
456 common,
457 state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
458 contract,
459 error: outgoing_error,
460 })),
461 }
462 }
463
464 async fn buy_preimage_via_direct_swap(
465 client: ClientHandleArc,
466 payment_data: PaymentData,
467 contract: OutgoingContractAccount,
468 common: GatewayPayCommon,
469 ) -> GatewayPayStateMachine {
470 debug!("Buying preimage via direct swap for contract {contract:?}");
471 match payment_data.try_into() {
472 Ok(swap_params) => match client
473 .get_first_module::<GatewayClientModule>()
474 .expect("Must have client module")
475 .gateway_handle_direct_swap(swap_params)
476 .await
477 {
478 Ok(operation_id) => {
479 debug!("Direct swap initiated for contract {contract:?}");
480 GatewayPayStateMachine {
481 common,
482 state: GatewayPayStates::WaitForSwapPreimage(Box::new(
483 GatewayPayWaitForSwapPreimage {
484 contract,
485 federation_id: client.federation_id(),
486 operation_id,
487 },
488 )),
489 }
490 }
491 Err(e) => {
492 info!("Failed to initiate direct swap: {e:?} for contract {contract:?}");
493 let outgoing_payment_error = OutgoingPaymentError {
494 contract_id: contract.contract.contract_id(),
495 contract: Some(contract.clone()),
496 error_type: OutgoingPaymentErrorType::SwapFailed {
497 swap_error: format!("Failed to initiate direct swap: {e}"),
498 },
499 };
500 GatewayPayStateMachine {
501 common,
502 state: GatewayPayStates::CancelContract(Box::new(
503 GatewayPayCancelContract {
504 contract: contract.clone(),
505 error: outgoing_payment_error,
506 },
507 )),
508 }
509 }
510 },
511 Err(e) => {
512 info!("Failed to initiate direct swap: {e:?} for contract {contract:?}");
513 let outgoing_payment_error = OutgoingPaymentError {
514 contract_id: contract.contract.contract_id(),
515 contract: Some(contract.clone()),
516 error_type: OutgoingPaymentErrorType::SwapFailed {
517 swap_error: format!("Failed to initiate direct swap: {e}"),
518 },
519 };
520 GatewayPayStateMachine {
521 common,
522 state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
523 contract: contract.clone(),
524 error: outgoing_payment_error,
525 })),
526 }
527 }
528 }
529 }
530
531 fn validate_outgoing_account(
532 account: &OutgoingContractAccount,
533 redeem_key: bitcoin::key::Keypair,
534 consensus_block_count: u64,
535 payment_data: &PaymentData,
536 routing_fees: RoutingFees,
537 ) -> Result<PaymentParameters, OutgoingContractError> {
538 let our_pub_key = secp256k1::PublicKey::from_keypair(&redeem_key);
539
540 if account.contract.cancelled {
541 return Err(OutgoingContractError::CancelledContract);
542 }
543
544 if account.contract.gateway_key != our_pub_key {
545 return Err(OutgoingContractError::NotOurKey);
546 }
547
548 let payment_amount = payment_data
549 .amount()
550 .ok_or(OutgoingContractError::InvoiceMissingAmount)?;
551
552 let gateway_fee = routing_fees.to_amount(&payment_amount);
553 let necessary_contract_amount = payment_amount + gateway_fee;
554 if account.amount < necessary_contract_amount {
555 return Err(OutgoingContractError::Underfunded(
556 necessary_contract_amount,
557 account.amount,
558 ));
559 }
560
561 let max_delay = u64::from(account.contract.timelock)
562 .checked_sub(consensus_block_count.saturating_sub(1))
563 .and_then(|delta| delta.checked_sub(TIMELOCK_DELTA));
564 if max_delay.is_none() {
565 return Err(OutgoingContractError::TimeoutTooClose);
566 }
567
568 if payment_data.is_expired() {
569 return Err(OutgoingContractError::InvoiceExpired(
570 payment_data.expiry_timestamp(),
571 ));
572 }
573
574 Ok(PaymentParameters {
575 max_delay: max_delay.unwrap(),
576 max_send_amount: account.amount,
577 payment_data: payment_data.clone(),
578 })
579 }
580}
581
582#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable, Serialize, Deserialize)]
583struct PaymentParameters {
584 max_delay: u64,
585 max_send_amount: Amount,
586 payment_data: PaymentData,
587}
588
589#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
590pub struct GatewayPayClaimOutgoingContract {
591 contract: OutgoingContractAccount,
592 preimage: Preimage,
593}
594
595impl GatewayPayClaimOutgoingContract {
596 fn transitions(
597 &self,
598 global_context: DynGlobalClientContext,
599 context: GatewayClientContext,
600 common: GatewayPayCommon,
601 ) -> Vec<StateTransition<GatewayPayStateMachine>> {
602 let contract = self.contract.clone();
603 let preimage = self.preimage.clone();
604 vec![StateTransition::new(
605 future::ready(()),
606 move |dbtx, (), _| {
607 Box::pin(Self::transition_claim_outgoing_contract(
608 dbtx,
609 global_context.clone(),
610 context.clone(),
611 common.clone(),
612 contract.clone(),
613 preimage.clone(),
614 ))
615 },
616 )]
617 }
618
619 async fn transition_claim_outgoing_contract(
620 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
621 global_context: DynGlobalClientContext,
622 context: GatewayClientContext,
623 common: GatewayPayCommon,
624 contract: OutgoingContractAccount,
625 preimage: Preimage,
626 ) -> GatewayPayStateMachine {
627 debug!("Claiming outgoing contract {contract:?}");
628
629 context
630 .client_ctx
631 .log_event(
632 &mut dbtx.module_tx(),
633 OutgoingPaymentSucceeded {
634 outgoing_contract: contract.clone(),
635 contract_id: contract.contract.contract_id(),
636 preimage: preimage.consensus_encode_to_hex(),
637 },
638 )
639 .await;
640
641 let claim_input = contract.claim(preimage.clone());
642 let client_input = ClientInput::<LightningInput> {
643 input: claim_input,
644 amount: contract.amount,
645 keys: vec![context.redeem_key],
646 };
647
648 let out_points = global_context
649 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
650 .await
651 .expect("Cannot claim input, additional funding needed")
652 .into_iter()
653 .collect();
654 debug!("Claimed outgoing contract {contract:?} with out points {out_points:?}");
655 GatewayPayStateMachine {
656 common,
657 state: GatewayPayStates::Preimage(out_points, preimage),
658 }
659 }
660}
661
662#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
663pub struct GatewayPayWaitForSwapPreimage {
664 contract: OutgoingContractAccount,
665 federation_id: FederationId,
666 operation_id: OperationId,
667}
668
669impl GatewayPayWaitForSwapPreimage {
670 fn transitions(
671 &self,
672 context: GatewayClientContext,
673 common: GatewayPayCommon,
674 ) -> Vec<StateTransition<GatewayPayStateMachine>> {
675 let federation_id = self.federation_id;
676 let operation_id = self.operation_id;
677 let contract = self.contract.clone();
678 vec![StateTransition::new(
679 Self::await_preimage(context, federation_id, operation_id, contract.clone()),
680 move |_dbtx, result, _old_state| {
681 let common = common.clone();
682 let contract = contract.clone();
683 Box::pin(async {
684 Self::transition_claim_outgoing_contract(common, result, contract)
685 })
686 },
687 )]
688 }
689
690 async fn await_preimage(
691 context: GatewayClientContext,
692 federation_id: FederationId,
693 operation_id: OperationId,
694 contract: OutgoingContractAccount,
695 ) -> Result<Preimage, OutgoingPaymentError> {
696 debug!("Waiting preimage for contract {contract:?}");
697
698 let client = context
699 .lightning_manager
700 .get_client(&federation_id)
701 .await
702 .ok_or(OutgoingPaymentError {
703 contract_id: contract.contract.contract_id(),
704 contract: Some(contract.clone()),
705 error_type: OutgoingPaymentErrorType::SwapFailed {
706 swap_error: "Federation client not found".to_string(),
707 },
708 })?;
709
710 async {
711 let mut stream = client
712 .value()
713 .get_first_module::<GatewayClientModule>()
714 .expect("Must have client module")
715 .gateway_subscribe_ln_receive(operation_id)
716 .await
717 .map_err(|e| {
718 let contract_id = contract.contract.contract_id();
719 warn!(
720 ?contract_id,
721 "Failed to subscribe to ln receive of direct swap: {e:?}"
722 );
723 OutgoingPaymentError {
724 contract_id,
725 contract: Some(contract.clone()),
726 error_type: OutgoingPaymentErrorType::SwapFailed {
727 swap_error: format!(
728 "Failed to subscribe to ln receive of direct swap: {e}"
729 ),
730 },
731 }
732 })?
733 .into_stream();
734
735 loop {
736 debug!("Waiting next state of preimage buy for contract {contract:?}");
737 if let Some(state) = stream.next().await {
738 match state {
739 GatewayExtReceiveStates::Funding => {
740 debug!(?contract, "Funding");
741 continue;
742 }
743 GatewayExtReceiveStates::Preimage(preimage) => {
744 debug!(?contract, "Received preimage");
745 return Ok(preimage);
746 }
747 other => {
748 warn!(?contract, "Got state {other:?}");
749 return Err(OutgoingPaymentError {
750 contract_id: contract.contract.contract_id(),
751 contract: Some(contract),
752 error_type: OutgoingPaymentErrorType::SwapFailed {
753 swap_error: "Failed to receive preimage".to_string(),
754 },
755 });
756 }
757 }
758 }
759 }
760 }
761 .instrument(client.span())
762 .await
763 }
764
765 fn transition_claim_outgoing_contract(
766 common: GatewayPayCommon,
767 result: Result<Preimage, OutgoingPaymentError>,
768 contract: OutgoingContractAccount,
769 ) -> GatewayPayStateMachine {
770 match result {
771 Ok(preimage) => GatewayPayStateMachine {
772 common,
773 state: GatewayPayStates::ClaimOutgoingContract(Box::new(
774 GatewayPayClaimOutgoingContract { contract, preimage },
775 )),
776 },
777 Err(e) => GatewayPayStateMachine {
778 common,
779 state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
780 contract,
781 error: e,
782 })),
783 },
784 }
785 }
786}
787
788#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
789pub struct GatewayPayCancelContract {
790 contract: OutgoingContractAccount,
791 error: OutgoingPaymentError,
792}
793
794impl GatewayPayCancelContract {
795 fn transitions(
796 &self,
797 global_context: DynGlobalClientContext,
798 context: GatewayClientContext,
799 common: GatewayPayCommon,
800 ) -> Vec<StateTransition<GatewayPayStateMachine>> {
801 let contract = self.contract.clone();
802 let error = self.error.clone();
803 vec![StateTransition::new(
804 future::ready(()),
805 move |dbtx, (), _| {
806 Box::pin(Self::transition_canceled(
807 dbtx,
808 contract.clone(),
809 global_context.clone(),
810 context.clone(),
811 common.clone(),
812 error.clone(),
813 ))
814 },
815 )]
816 }
817
818 async fn transition_canceled(
819 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
820 contract: OutgoingContractAccount,
821 global_context: DynGlobalClientContext,
822 context: GatewayClientContext,
823 common: GatewayPayCommon,
824 error: OutgoingPaymentError,
825 ) -> GatewayPayStateMachine {
826 info!("Canceling outgoing contract {contract:?}");
827
828 context
829 .client_ctx
830 .log_event(
831 &mut dbtx.module_tx(),
832 OutgoingPaymentFailed {
833 outgoing_contract: contract.clone(),
834 contract_id: contract.contract.contract_id(),
835 error: error.clone(),
836 },
837 )
838 .await;
839
840 let cancel_signature = context.secp.sign_schnorr(
841 &bitcoin::secp256k1::Message::from_digest(
842 *contract.contract.cancellation_message().as_ref(),
843 ),
844 &context.redeem_key,
845 );
846 let cancel_output = LightningOutput::new_v0_cancel_outgoing(
847 contract.contract.contract_id(),
848 cancel_signature,
849 );
850 let client_output = ClientOutput::<LightningOutput> {
851 output: cancel_output,
852 amount: Amount::ZERO,
853 };
854
855 match global_context
856 .fund_output(dbtx, ClientOutputBundle::new_no_sm(vec![client_output]))
857 .await
858 {
859 Ok(change_range) => {
860 info!(
861 "Canceled outgoing contract {contract:?} with txid {:?}",
862 change_range.txid()
863 );
864 GatewayPayStateMachine {
865 common,
866 state: GatewayPayStates::Canceled {
867 txid: change_range.txid(),
868 contract_id: contract.contract.contract_id(),
869 error,
870 },
871 }
872 }
873 Err(e) => {
874 warn!("Failed to cancel outgoing contract {contract:?}: {e:?}");
875 GatewayPayStateMachine {
876 common,
877 state: GatewayPayStates::Failed {
878 error,
879 error_message: format!(
880 "Failed to submit refund transaction to federation {e:?}"
881 ),
882 },
883 }
884 }
885 }
886 }
887}