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 if account.contract.hash != payment_data.payment_hash() {
643 return Err(OutgoingContractError::InvalidOutgoingContract {
644 contract_id: account.contract.contract_id(),
645 });
646 }
647
648 let payment_amount = payment_data
649 .amount()
650 .ok_or(OutgoingContractError::InvoiceMissingAmount)?;
651
652 let gateway_fee = routing_fees.to_amount(&payment_amount);
653 let necessary_contract_amount = payment_amount + gateway_fee;
654 if account.amount < necessary_contract_amount {
655 return Err(OutgoingContractError::Underfunded(
656 necessary_contract_amount,
657 account.amount,
658 ));
659 }
660
661 let max_delay = u64::from(account.contract.timelock)
662 .checked_sub(consensus_block_count.saturating_sub(1))
663 .and_then(|delta| delta.checked_sub(TIMELOCK_DELTA));
664 if max_delay.is_none() {
665 return Err(OutgoingContractError::TimeoutTooClose);
666 }
667
668 if payment_data.is_expired() {
669 return Err(OutgoingContractError::InvoiceExpired(
670 payment_data.expiry_timestamp(),
671 ));
672 }
673
674 Ok(PaymentParameters {
675 max_delay: max_delay.unwrap(),
676 max_send_amount: account.amount,
677 payment_data: payment_data.clone(),
678 })
679 }
680}
681
682#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable, Serialize, Deserialize)]
683struct PaymentParameters {
684 max_delay: u64,
685 max_send_amount: Amount,
686 payment_data: PaymentData,
687}
688
689#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
690pub struct GatewayPayClaimOutgoingContract {
691 contract: OutgoingContractAccount,
692 preimage: Preimage,
693}
694
695impl GatewayPayClaimOutgoingContract {
696 fn transitions(
697 &self,
698 global_context: DynGlobalClientContext,
699 context: GatewayClientContext,
700 common: GatewayPayCommon,
701 ) -> Vec<StateTransition<GatewayPayStateMachine>> {
702 let contract = self.contract.clone();
703 let preimage = self.preimage.clone();
704 vec![StateTransition::new(
705 future::ready(()),
706 move |dbtx, (), _| {
707 Box::pin(Self::transition_claim_outgoing_contract(
708 dbtx,
709 global_context.clone(),
710 context.clone(),
711 common.clone(),
712 contract.clone(),
713 preimage.clone(),
714 ))
715 },
716 )]
717 }
718
719 async fn transition_claim_outgoing_contract(
720 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
721 global_context: DynGlobalClientContext,
722 context: GatewayClientContext,
723 common: GatewayPayCommon,
724 contract: OutgoingContractAccount,
725 preimage: Preimage,
726 ) -> GatewayPayStateMachine {
727 debug!("Claiming outgoing contract {contract:?}");
728
729 context
730 .client_ctx
731 .log_event(
732 &mut dbtx.module_tx(),
733 OutgoingPaymentSucceeded {
734 outgoing_contract: contract.clone(),
735 contract_id: contract.contract.contract_id(),
736 preimage: preimage.consensus_encode_to_hex(),
737 },
738 )
739 .await;
740
741 let claim_input = contract.claim(preimage.clone());
742 let client_input = ClientInput::<LightningInput> {
743 input: claim_input,
744 amounts: Amounts::new_bitcoin(contract.amount),
745 keys: vec![context.redeem_key],
746 };
747
748 let out_points = global_context
749 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
750 .await
751 .expect("Cannot claim input, additional funding needed")
752 .into_iter()
753 .collect();
754 debug!("Claimed outgoing contract {contract:?} with out points {out_points:?}");
755 GatewayPayStateMachine {
756 common,
757 state: GatewayPayStates::Preimage(out_points, preimage),
758 }
759 }
760}
761
762#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
763pub struct GatewayPayWaitForSwapPreimage {
764 contract: OutgoingContractAccount,
765 federation_id: FederationId,
766 operation_id: OperationId,
767}
768
769impl GatewayPayWaitForSwapPreimage {
770 fn transitions(
771 &self,
772 context: GatewayClientContext,
773 common: GatewayPayCommon,
774 ) -> Vec<StateTransition<GatewayPayStateMachine>> {
775 let federation_id = self.federation_id;
776 let operation_id = self.operation_id;
777 let contract = self.contract.clone();
778 vec![StateTransition::new(
779 Self::await_preimage(context, federation_id, operation_id, contract.clone()),
780 move |_dbtx, result, _old_state| {
781 let common = common.clone();
782 let contract = contract.clone();
783 Box::pin(async {
784 Self::transition_claim_outgoing_contract(common, result, contract)
785 })
786 },
787 )]
788 }
789
790 async fn await_preimage(
791 context: GatewayClientContext,
792 federation_id: FederationId,
793 operation_id: OperationId,
794 contract: OutgoingContractAccount,
795 ) -> Result<Preimage, OutgoingPaymentError> {
796 debug!("Waiting preimage for contract {contract:?}");
797
798 let client = context
799 .lightning_manager
800 .get_client(&federation_id)
801 .await
802 .ok_or(OutgoingPaymentError {
803 contract_id: contract.contract.contract_id(),
804 contract: Some(contract.clone()),
805 error_type: OutgoingPaymentErrorType::SwapFailed {
806 swap_error: "Federation client not found".to_string(),
807 },
808 })?;
809
810 async {
811 let mut stream = client
812 .value()
813 .get_first_module::<GatewayClientModule>()
814 .expect("Must have client module")
815 .gateway_subscribe_ln_receive(operation_id)
816 .await
817 .map_err(|e| {
818 let contract_id = contract.contract.contract_id();
819 warn!(
820 ?contract_id,
821 "Failed to subscribe to ln receive of direct swap: {e:?}"
822 );
823 OutgoingPaymentError {
824 contract_id,
825 contract: Some(contract.clone()),
826 error_type: OutgoingPaymentErrorType::SwapFailed {
827 swap_error: format!(
828 "Failed to subscribe to ln receive of direct swap: {e}"
829 ),
830 },
831 }
832 })?
833 .into_stream();
834
835 loop {
836 debug!("Waiting next state of preimage buy for contract {contract:?}");
837 if let Some(state) = stream.next().await {
838 match state {
839 GatewayExtReceiveStates::Funding => {
840 debug!(?contract, "Funding");
841 continue;
842 }
843 GatewayExtReceiveStates::Preimage(preimage) => {
844 debug!(?contract, "Received preimage");
845 return Ok(preimage);
846 }
847 other => {
848 warn!(?contract, "Got state {other:?}");
849 return Err(OutgoingPaymentError {
850 contract_id: contract.contract.contract_id(),
851 contract: Some(contract),
852 error_type: OutgoingPaymentErrorType::SwapFailed {
853 swap_error: "Failed to receive preimage".to_string(),
854 },
855 });
856 }
857 }
858 }
859 }
860 }
861 .instrument(client.span())
862 .await
863 }
864
865 fn transition_claim_outgoing_contract(
866 common: GatewayPayCommon,
867 result: Result<Preimage, OutgoingPaymentError>,
868 contract: OutgoingContractAccount,
869 ) -> GatewayPayStateMachine {
870 match result {
871 Ok(preimage) => GatewayPayStateMachine {
872 common,
873 state: GatewayPayStates::ClaimOutgoingContract(Box::new(
874 GatewayPayClaimOutgoingContract { contract, preimage },
875 )),
876 },
877 Err(e) => GatewayPayStateMachine {
878 common,
879 state: GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract {
880 contract,
881 error: e,
882 })),
883 },
884 }
885 }
886}
887
888#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable, Serialize, Deserialize)]
889pub struct GatewayPayCancelContract {
890 contract: OutgoingContractAccount,
891 error: OutgoingPaymentError,
892}
893
894impl GatewayPayCancelContract {
895 fn transitions(
896 &self,
897 global_context: DynGlobalClientContext,
898 context: GatewayClientContext,
899 common: GatewayPayCommon,
900 ) -> Vec<StateTransition<GatewayPayStateMachine>> {
901 let contract = self.contract.clone();
902 let error = self.error.clone();
903 vec![StateTransition::new(
904 future::ready(()),
905 move |dbtx, (), _| {
906 Box::pin(Self::transition_canceled(
907 dbtx,
908 contract.clone(),
909 global_context.clone(),
910 context.clone(),
911 common.clone(),
912 error.clone(),
913 ))
914 },
915 )]
916 }
917
918 async fn transition_canceled(
919 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
920 contract: OutgoingContractAccount,
921 global_context: DynGlobalClientContext,
922 context: GatewayClientContext,
923 common: GatewayPayCommon,
924 error: OutgoingPaymentError,
925 ) -> GatewayPayStateMachine {
926 info!("Canceling outgoing contract {contract:?}");
927
928 context
929 .client_ctx
930 .log_event(
931 &mut dbtx.module_tx(),
932 OutgoingPaymentFailed {
933 outgoing_contract: contract.clone(),
934 contract_id: contract.contract.contract_id(),
935 error: error.clone(),
936 },
937 )
938 .await;
939
940 let cancel_signature = context.secp.sign_schnorr(
941 &bitcoin::secp256k1::Message::from_digest(
942 *contract.contract.cancellation_message().as_ref(),
943 ),
944 &context.redeem_key,
945 );
946 let cancel_output = LightningOutput::new_v0_cancel_outgoing(
947 contract.contract.contract_id(),
948 cancel_signature,
949 );
950 let client_output = ClientOutput::<LightningOutput> {
951 output: cancel_output,
952 amounts: Amounts::ZERO,
953 };
954
955 match global_context
956 .fund_output(dbtx, ClientOutputBundle::new_no_sm(vec![client_output]))
957 .await
958 {
959 Ok(change_range) => {
960 info!(
961 "Canceled outgoing contract {contract:?} with txid {:?}",
962 change_range.txid()
963 );
964 GatewayPayStateMachine {
965 common,
966 state: GatewayPayStates::Canceled {
967 txid: change_range.txid(),
968 contract_id: contract.contract.contract_id(),
969 error,
970 },
971 }
972 }
973 Err(e) => {
974 warn!("Failed to cancel outgoing contract {contract:?}: {e:?}");
975 GatewayPayStateMachine {
976 common,
977 state: GatewayPayStates::Failed {
978 error,
979 error_message: format!(
980 "Failed to submit refund transaction to federation {e:?}"
981 ),
982 },
983 }
984 }
985 }
986 }
987}
988
989#[cfg(test)]
990mod tests {
991 use bitcoin::hashes::{Hash as _, sha256};
992 use bitcoin::key::Keypair;
993 use fedimint_core::Amount;
994 use fedimint_core::secp256k1::{self, SecretKey};
995 use fedimint_ln_client::pay::PaymentData;
996 use fedimint_ln_common::PrunedInvoice;
997 use fedimint_ln_common::contracts::IdentifiableContract as _;
998 use fedimint_ln_common::contracts::outgoing::{OutgoingContract, OutgoingContractAccount};
999 use lightning_invoice::RoutingFees;
1000
1001 use super::{GatewayPayInvoice, OutgoingContractError};
1002
1003 const CONSENSUS_BLOCK_COUNT: u64 = 1;
1004 const INVOICE_AMOUNT: Amount = Amount::from_msats(1000);
1005
1006 fn gateway_keypair() -> Keypair {
1007 Keypair::from_secret_key(
1008 secp256k1::SECP256K1,
1009 &SecretKey::from_slice(&[1; 32]).expect("Valid secret key"),
1010 )
1011 }
1012
1013 fn contract_account(hash: sha256::Hash) -> OutgoingContractAccount {
1016 let gateway_key = secp256k1::PublicKey::from_keypair(&gateway_keypair());
1017
1018 OutgoingContractAccount {
1019 amount: INVOICE_AMOUNT,
1020 contract: OutgoingContract {
1021 hash,
1022 gateway_key,
1023 timelock: 100,
1025 user_key: gateway_key,
1026 cancelled: false,
1027 },
1028 }
1029 }
1030
1031 fn payment_data(payment_hash: sha256::Hash) -> PaymentData {
1032 PaymentData::PrunedInvoice(PrunedInvoice {
1033 amount: INVOICE_AMOUNT,
1034 destination: secp256k1::PublicKey::from_keypair(&gateway_keypair()),
1035 destination_features: vec![],
1036 payment_hash,
1037 payment_secret: [0; 32],
1038 route_hints: vec![],
1039 min_final_cltv_delta: 0,
1040 expiry_timestamp: u64::MAX,
1041 })
1042 }
1043
1044 fn validate(
1045 contract_hash: sha256::Hash,
1046 invoice_hash: sha256::Hash,
1047 ) -> Result<(), OutgoingContractError> {
1048 GatewayPayInvoice::validate_outgoing_account(
1049 &contract_account(contract_hash),
1050 gateway_keypair(),
1051 CONSENSUS_BLOCK_COUNT,
1052 &payment_data(invoice_hash),
1053 RoutingFees {
1054 base_msat: 0,
1055 proportional_millionths: 0,
1056 },
1057 )
1058 .map(|_| ())
1059 }
1060
1061 #[test]
1064 fn accepts_contract_matching_the_invoice() {
1065 let hash = sha256::Hash::hash(b"preimage");
1066
1067 assert_eq!(validate(hash, hash), Ok(()));
1068 }
1069
1070 #[test]
1071 fn rejects_contract_not_committing_to_the_invoice() {
1072 let contract_hash = sha256::Hash::hash(b"contract preimage");
1077 let invoice_hash = sha256::Hash::hash(b"unrelated invoice preimage");
1078
1079 assert_eq!(
1080 validate(contract_hash, invoice_hash),
1081 Err(OutgoingContractError::InvalidOutgoingContract {
1082 contract_id: contract_account(contract_hash).contract.contract_id(),
1083 })
1084 );
1085 }
1086}