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