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