1mod api;
2mod complete_sm;
3pub mod events;
4mod receive_sm;
5mod send_sm;
6
7use std::collections::BTreeMap;
8use std::fmt;
9use std::fmt::Debug;
10use std::sync::Arc;
11
12use anyhow::{anyhow, ensure};
13use async_trait::async_trait;
14use bitcoin::hashes::sha256;
15use bitcoin::secp256k1::Message;
16use events::{IncomingPaymentStarted, OutgoingPaymentStarted};
17use fedimint_api_client::api::DynModuleApi;
18use fedimint_client::ClientHandleArc;
19use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
20use fedimint_client_module::module::recovery::NoModuleBackup;
21use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
22use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
23use fedimint_client_module::transaction::{
24 ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
25};
26use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
27use fedimint_core::config::FederationId;
28use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
29use fedimint_core::db::DatabaseTransaction;
30use fedimint_core::encoding::{Decodable, Encodable};
31use fedimint_core::module::{
32 Amounts, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
33};
34use fedimint_core::secp256k1::Keypair;
35use fedimint_core::time::now;
36use fedimint_core::util::Spanned;
37use fedimint_core::{Amount, PeerId, apply, async_trait_maybe_send, secp256k1};
38use fedimint_lightning::{InterceptPaymentResponse, LightningRpcError};
39use fedimint_lnv2_common::config::LightningClientConfig;
40use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
41use fedimint_lnv2_common::gateway_api::SendPaymentPayload;
42use fedimint_lnv2_common::{
43 LightningCommonInit, LightningInvoice, LightningModuleTypes, LightningOutput, LightningOutputV0,
44};
45use futures::StreamExt;
46use lightning_invoice::Bolt11Invoice;
47use receive_sm::{ReceiveSMState, ReceiveStateMachine};
48use secp256k1::schnorr::Signature;
49use send_sm::{SendSMState, SendStateMachine};
50use serde::{Deserialize, Serialize};
51use tpe::{AggregatePublicKey, PublicKeyShare};
52use tracing::{info, warn};
53
54use crate::api::GatewayFederationApi;
55pub use crate::complete_sm::IncomingCircuitKey;
56use crate::complete_sm::{
57 CircuitCompleteSMCommon, CircuitCompleteStateMachine, CompleteSMState, CompleteStateMachine,
58};
59use crate::receive_sm::ReceiveSMCommon;
60use crate::send_sm::SendSMCommon;
61
62pub const EXPIRATION_DELTA_MINIMUM_V2: u64 = 144;
64
65fn incoming_circuit_operation_id(
66 receive_operation_id: OperationId,
67 circuit: IncomingCircuitKey,
68) -> OperationId {
69 OperationId::from_encodable(&(
70 "gateway-lnv2-incoming-circuit",
71 receive_operation_id,
72 circuit,
73 ))
74}
75
76fn is_legacy_completion_for_circuit(
77 state: &GatewayClientStateMachinesV2,
78 circuit: IncomingCircuitKey,
79) -> bool {
80 let IncomingCircuitKey {
81 incoming_chan_id,
82 htlc_id,
83 } = circuit;
84 matches!(
85 state,
86 GatewayClientStateMachinesV2::Complete(CompleteStateMachine {
87 common,
88 ..
89 }) if common.incoming_chan_id == incoming_chan_id && common.htlc_id == htlc_id
90 )
91}
92
93fn legacy_completion_in_states(
94 active: &[GatewayClientStateMachinesV2],
95 inactive: &[GatewayClientStateMachinesV2],
96 circuit: IncomingCircuitKey,
97) -> bool {
98 active
99 .iter()
100 .chain(inactive)
101 .any(|state| is_legacy_completion_for_circuit(state, circuit))
102}
103
104#[derive(Debug, Clone, Copy, Eq, PartialEq)]
105enum IncomingRelayPlan {
106 Replay,
107 AddCompletion,
108 CreateReceiveAndCompletion,
109}
110
111fn incoming_relay_plan(
112 receive_exists: bool,
113 completion_exists: bool,
114 legacy_completion_exists: bool,
115) -> IncomingRelayPlan {
116 if completion_exists || legacy_completion_exists {
117 IncomingRelayPlan::Replay
118 } else if receive_exists {
119 IncomingRelayPlan::AddCompletion
120 } else {
121 IncomingRelayPlan::CreateReceiveAndCompletion
122 }
123}
124
125fn operation_creation_failed_permanently(
126 creation_failed: bool,
127 operation_exists_after_failure: bool,
128) -> bool {
129 creation_failed && !operation_exists_after_failure
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(untagged)]
135pub enum GatewayOperationMetaV2 {
136 Legacy(()),
138 Role {
140 role: GatewayOperationRoleV2,
142 },
143}
144
145#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum GatewayOperationRoleV2 {
149 Send,
151 Receive,
153 CircuitCompletion,
155}
156
157impl GatewayOperationMetaV2 {
158 pub fn role(role: GatewayOperationRoleV2) -> Self {
160 Self::Role { role }
161 }
162
163 pub fn waits_for_completion(&self) -> bool {
165 matches!(
166 self,
167 Self::Legacy(())
168 | Self::Role {
169 role: GatewayOperationRoleV2::CircuitCompletion
170 }
171 )
172 }
173}
174
175#[derive(Debug, Clone)]
176pub struct GatewayClientInitV2 {
177 pub gateway: Arc<dyn IGatewayClientV2>,
178}
179
180impl ModuleInit for GatewayClientInitV2 {
181 type Common = LightningCommonInit;
182
183 async fn dump_database(
184 &self,
185 _dbtx: &mut DatabaseTransaction<'_>,
186 _prefix_names: Vec<String>,
187 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
188 Box::new(vec![].into_iter())
189 }
190}
191
192#[apply(async_trait_maybe_send!)]
193impl ClientModuleInit for GatewayClientInitV2 {
194 type Module = GatewayClientModuleV2;
195
196 fn supported_api_versions(&self) -> MultiApiVersion {
197 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
198 .expect("no version conflicts")
199 }
200
201 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
202 Ok(GatewayClientModuleV2 {
203 federation_id: *args.federation_id(),
204 cfg: args.cfg().clone(),
205 notifier: args.notifier().clone(),
206 client_ctx: args.context(),
207 module_api: args.module_api().clone(),
208 keypair: args
209 .module_root_secret()
210 .clone()
211 .to_secp_key(fedimint_core::secp256k1::SECP256K1),
212 gateway: self.gateway.clone(),
213 })
214 }
215}
216
217#[derive(Debug, Clone)]
218pub struct GatewayClientModuleV2 {
219 pub federation_id: FederationId,
220 pub cfg: LightningClientConfig,
221 pub notifier: ModuleNotifier<GatewayClientStateMachinesV2>,
222 pub client_ctx: ClientContext<Self>,
223 pub module_api: DynModuleApi,
224 pub keypair: Keypair,
225 pub gateway: Arc<dyn IGatewayClientV2>,
226}
227
228#[derive(Debug, Clone)]
229pub struct GatewayClientContextV2 {
230 pub module: GatewayClientModuleV2,
231 pub decoder: Decoder,
232 pub tpe_agg_pk: AggregatePublicKey,
233 pub tpe_pks: BTreeMap<PeerId, PublicKeyShare>,
234 pub gateway: Arc<dyn IGatewayClientV2>,
235}
236
237impl Context for GatewayClientContextV2 {
238 const KIND: Option<ModuleKind> = Some(fedimint_lnv2_common::KIND);
239}
240
241impl ClientModule for GatewayClientModuleV2 {
242 type Init = GatewayClientInitV2;
243 type Common = LightningModuleTypes;
244 type Backup = NoModuleBackup;
245 type ModuleStateMachineContext = GatewayClientContextV2;
246 type States = GatewayClientStateMachinesV2;
247
248 fn context(&self) -> Self::ModuleStateMachineContext {
249 GatewayClientContextV2 {
250 module: self.clone(),
251 decoder: self.decoder(),
252 tpe_agg_pk: self.cfg.tpe_agg_pk,
253 tpe_pks: self.cfg.tpe_pks.clone(),
254 gateway: self.gateway.clone(),
255 }
256 }
257 fn input_fee(
258 &self,
259 amount: &Amounts,
260 _input: &<Self::Common as ModuleCommon>::Input,
261 ) -> Option<Amounts> {
262 Some(Amounts::new_bitcoin(
263 self.cfg.fee_consensus.fee(amount.expect_only_bitcoin()),
264 ))
265 }
266
267 fn output_fee(
268 &self,
269 _amount: &Amounts,
270 output: &<Self::Common as ModuleCommon>::Output,
271 ) -> Option<Amounts> {
272 let amount = match output.ensure_v0_ref().ok()? {
273 LightningOutputV0::Outgoing(contract) => contract.amount,
274 LightningOutputV0::Incoming(contract) => contract.commitment.amount,
275 };
276
277 Some(Amounts::new_bitcoin(self.cfg.fee_consensus.fee(amount)))
278 }
279}
280
281#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
282pub enum GatewayClientStateMachinesV2 {
283 Send(SendStateMachine),
284 Receive(ReceiveStateMachine),
285 Complete(CompleteStateMachine),
288 CircuitComplete(CircuitCompleteStateMachine),
291}
292
293impl fmt::Display for GatewayClientStateMachinesV2 {
294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295 match self {
296 GatewayClientStateMachinesV2::Send(send) => {
297 write!(f, "{send}")
298 }
299 GatewayClientStateMachinesV2::Receive(receive) => {
300 write!(f, "{receive}")
301 }
302 GatewayClientStateMachinesV2::Complete(complete) => {
303 write!(f, "{complete}")
304 }
305 GatewayClientStateMachinesV2::CircuitComplete(complete) => {
306 write!(f, "{complete}")
307 }
308 }
309 }
310}
311
312impl IntoDynInstance for GatewayClientStateMachinesV2 {
313 type DynType = DynState;
314
315 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
316 DynState::from_typed(instance_id, self)
317 }
318}
319
320impl State for GatewayClientStateMachinesV2 {
321 type ModuleContext = GatewayClientContextV2;
322
323 fn transitions(
324 &self,
325 context: &Self::ModuleContext,
326 global_context: &DynGlobalClientContext,
327 ) -> Vec<StateTransition<Self>> {
328 match self {
329 GatewayClientStateMachinesV2::Send(state) => {
330 sm_enum_variant_translation!(
331 state.transitions(context, global_context),
332 GatewayClientStateMachinesV2::Send
333 )
334 }
335 GatewayClientStateMachinesV2::Receive(state) => {
336 sm_enum_variant_translation!(
337 state.transitions(context, global_context),
338 GatewayClientStateMachinesV2::Receive
339 )
340 }
341 GatewayClientStateMachinesV2::Complete(state) => {
342 sm_enum_variant_translation!(
343 state.transitions(context, global_context),
344 GatewayClientStateMachinesV2::Complete
345 )
346 }
347 GatewayClientStateMachinesV2::CircuitComplete(state) => {
348 sm_enum_variant_translation!(
349 state.transitions(context, global_context),
350 GatewayClientStateMachinesV2::CircuitComplete
351 )
352 }
353 }
354 }
355
356 fn operation_id(&self) -> OperationId {
357 match self {
358 GatewayClientStateMachinesV2::Send(state) => state.operation_id(),
359 GatewayClientStateMachinesV2::Receive(state) => state.operation_id(),
360 GatewayClientStateMachinesV2::Complete(state) => state.operation_id(),
361 GatewayClientStateMachinesV2::CircuitComplete(state) => state.operation_id(),
362 }
363 }
364}
365
366#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)]
367pub enum FinalReceiveState {
368 Rejected,
369 Success([u8; 32]),
370 Refunded,
371 Failure,
372}
373
374impl GatewayClientModuleV2 {
375 pub async fn send_payment(
376 &self,
377 payload: SendPaymentPayload,
378 ) -> anyhow::Result<Result<[u8; 32], Signature>> {
379 let operation_start = now();
380
381 let operation_id = OperationId::from_encodable(&payload.contract.clone());
387
388 ensure!(
392 payload.contract.claim_pk == self.keypair.public_key(),
393 "The outgoing contract is keyed to another gateway"
394 );
395
396 ensure!(
398 secp256k1::SECP256K1
399 .verify_schnorr(
400 &payload.auth,
401 &Message::from_digest(
402 *payload.invoice.consensus_hash::<sha256::Hash>().as_ref()
403 ),
404 &payload.contract.refund_pk.x_only_public_key().0,
405 )
406 .is_ok(),
407 "Invalid auth signature for the invoice data"
408 );
409
410 if self.client_ctx.operation_exists(operation_id).await {
414 return Ok(self.subscribe_send(operation_id).await);
415 }
416
417 let (contract_id, expiration) = self
420 .module_api
421 .outgoing_contract_expiration(payload.outpoint)
422 .await
423 .map_err(|_| anyhow!("The gateway can not reach the federation"))?
424 .ok_or(anyhow!("The outgoing contract has not yet been confirmed"))?;
425
426 ensure!(
427 contract_id == payload.contract.contract_id(),
428 "Contract Id returned by the federation does not match contract in request"
429 );
430
431 let (payment_hash, amount) = match &payload.invoice {
432 LightningInvoice::Bolt11(invoice) => (
433 invoice.payment_hash(),
434 invoice
435 .amount_milli_satoshis()
436 .ok_or(anyhow!("Invoice is missing amount"))?,
437 ),
438 };
439
440 ensure!(
441 PaymentImage::Hash(*payment_hash) == payload.contract.payment_image,
442 "The invoices payment hash does not match the contracts payment hash"
443 );
444
445 let min_contract_amount = self
446 .gateway
447 .min_contract_amount(&payload.federation_id, amount)
448 .await?;
449
450 let send_sm = GatewayClientStateMachinesV2::Send(SendStateMachine {
451 common: SendSMCommon {
452 operation_id,
453 outpoint: payload.outpoint,
454 contract: payload.contract.clone(),
455 max_delay: expiration.saturating_sub(EXPIRATION_DELTA_MINIMUM_V2),
456 min_contract_amount,
457 invoice: payload.invoice,
458 claim_keypair: self.keypair,
459 },
460 state: SendSMState::Sending,
461 });
462
463 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
464 self.client_ctx
465 .manual_operation_start_dbtx(
466 &mut dbtx.to_ref_nc(),
467 operation_id,
468 LightningCommonInit::KIND.as_str(),
469 GatewayOperationMetaV2::role(GatewayOperationRoleV2::Send),
470 vec![self.client_ctx.make_dyn_state(send_sm)],
471 )
472 .await
473 .ok();
474
475 self.client_ctx
476 .log_event(
477 &mut dbtx,
478 OutgoingPaymentStarted {
479 operation_start,
480 outgoing_contract: payload.contract.clone(),
481 min_contract_amount,
482 invoice_amount: Amount::from_msats(amount),
483 max_delay: expiration.saturating_sub(EXPIRATION_DELTA_MINIMUM_V2),
484 },
485 )
486 .await;
487 dbtx.commit_tx().await;
488
489 Ok(self.subscribe_send(operation_id).await)
490 }
491
492 pub async fn subscribe_send(&self, operation_id: OperationId) -> Result<[u8; 32], Signature> {
493 let mut stream = self.notifier.subscribe(operation_id).await;
494
495 loop {
496 if let Some(GatewayClientStateMachinesV2::Send(state)) = stream.next().await {
497 match state.state {
498 SendSMState::Sending => {}
499 SendSMState::Claiming(claiming) => {
500 return Ok(claiming.preimage);
506 }
507 SendSMState::Cancelled(cancelled) => {
508 warn!("Outgoing lightning payment is cancelled {:?}", cancelled);
509
510 let signature = self
511 .keypair
512 .sign_schnorr(state.common.contract.forfeit_message());
513
514 assert!(state.common.contract.verify_forfeit_signature(&signature));
515
516 return Err(signature);
517 }
518 }
519 }
520 }
521 }
522
523 async fn legacy_completion_exists(
524 &self,
525 operation_id: OperationId,
526 circuit: IncomingCircuitKey,
527 ) -> bool {
528 let active = self
529 .client_ctx
530 .get_own_operation_active_states(operation_id)
531 .await
532 .into_iter()
533 .map(|(state, _)| state)
534 .collect::<Vec<_>>();
535 let inactive = self
536 .client_ctx
537 .get_own_operation_inactive_states(operation_id)
538 .await
539 .into_iter()
540 .map(|(state, _)| state)
541 .collect::<Vec<_>>();
542
543 legacy_completion_in_states(&active, &inactive, circuit)
544 }
545
546 pub async fn relay_incoming_htlc(
547 &self,
548 payment_hash: sha256::Hash,
549 incoming_chan_id: u64,
550 htlc_id: u64,
551 contract: IncomingContract,
552 amount_msat: u64,
553 ) -> anyhow::Result<()> {
554 let operation_start = now();
555 let receive_operation_id = OperationId::from_encodable(&contract);
556 let circuit = IncomingCircuitKey {
557 incoming_chan_id,
558 htlc_id,
559 };
560 let completion_operation_id = incoming_circuit_operation_id(receive_operation_id, circuit);
561 let receive_exists = self.client_ctx.operation_exists(receive_operation_id).await;
562 let completion_exists = self
563 .client_ctx
564 .operation_exists(completion_operation_id)
565 .await;
566 let legacy_completion_exists = self
567 .legacy_completion_exists(receive_operation_id, circuit)
568 .await;
569 let plan = incoming_relay_plan(receive_exists, completion_exists, legacy_completion_exists);
570 if plan == IncomingRelayPlan::Replay {
571 return Ok(());
572 }
573
574 let commitment = contract.commitment.clone();
575 if plan == IncomingRelayPlan::CreateReceiveAndCompletion {
576 let refund_keypair = self.keypair;
577 let client_output = ClientOutput::<LightningOutput> {
578 output: LightningOutput::V0(LightningOutputV0::Incoming(contract.clone())),
579 amounts: Amounts::new_bitcoin(contract.commitment.amount),
580 };
581 let client_output_sm = ClientOutputSM::<GatewayClientStateMachinesV2> {
582 state_machines: Arc::new(move |range: OutPointRange| {
583 assert_eq!(range.count(), 1);
584
585 vec![GatewayClientStateMachinesV2::Receive(ReceiveStateMachine {
586 common: ReceiveSMCommon {
587 operation_id: receive_operation_id,
588 contract: contract.clone(),
589 outpoint: range.into_iter().next().unwrap(),
590 refund_keypair,
591 },
592 state: ReceiveSMState::Funding,
593 })]
594 }),
595 };
596
597 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
598 vec![client_output],
599 vec![client_output_sm],
600 ));
601 let transaction = TransactionBuilder::new().with_outputs(client_output);
602
603 let creation_result = self
604 .client_ctx
605 .finalize_and_submit_transaction(
606 receive_operation_id,
607 LightningCommonInit::KIND.as_str(),
608 |_| GatewayOperationMetaV2::role(GatewayOperationRoleV2::Receive),
609 transaction,
610 )
611 .await;
612 if let Err(error) = creation_result {
613 let operation_exists = self.client_ctx.operation_exists(receive_operation_id).await;
614 if operation_creation_failed_permanently(true, operation_exists) {
615 return Err(error);
616 }
617 } else {
618 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
619 self.client_ctx
620 .log_event(
621 &mut dbtx,
622 IncomingPaymentStarted {
623 operation_start,
624 incoming_contract_commitment: commitment,
625 invoice_amount: Amount::from_msats(amount_msat),
626 },
627 )
628 .await;
629 dbtx.commit_tx().await;
630 }
631 }
632
633 let completion =
634 GatewayClientStateMachinesV2::CircuitComplete(CircuitCompleteStateMachine {
635 common: CircuitCompleteSMCommon {
636 operation_id: completion_operation_id,
637 receive_operation_id,
638 payment_hash,
639 circuit,
640 },
641 state: CompleteSMState::Pending,
642 });
643 let creation_result = self
644 .client_ctx
645 .manual_operation_start(
646 completion_operation_id,
647 LightningCommonInit::KIND.as_str(),
648 GatewayOperationMetaV2::role(GatewayOperationRoleV2::CircuitCompletion),
649 vec![self.client_ctx.make_dyn_state(completion)],
650 )
651 .await;
652 if let Err(error) = creation_result {
653 let operation_exists = self
654 .client_ctx
655 .operation_exists(completion_operation_id)
656 .await;
657 if operation_creation_failed_permanently(true, operation_exists) {
658 return Err(error);
659 }
660 }
661
662 Ok(())
663 }
664
665 pub async fn relay_direct_swap(
666 &self,
667 contract: IncomingContract,
668 amount_msat: u64,
669 ) -> anyhow::Result<FinalReceiveState> {
670 let operation_start = now();
671
672 let operation_id = OperationId::from_encodable(&contract);
673
674 if self.client_ctx.operation_exists(operation_id).await {
675 return Ok(self.await_receive(operation_id).await);
676 }
677
678 let refund_keypair = self.keypair;
679
680 let client_output = ClientOutput::<LightningOutput> {
681 output: LightningOutput::V0(LightningOutputV0::Incoming(contract.clone())),
682 amounts: Amounts::new_bitcoin(contract.commitment.amount),
683 };
684 let commitment = contract.commitment.clone();
685 let client_output_sm = ClientOutputSM::<GatewayClientStateMachinesV2> {
686 state_machines: Arc::new(move |range| {
687 assert_eq!(range.count(), 1);
688
689 vec![GatewayClientStateMachinesV2::Receive(ReceiveStateMachine {
690 common: ReceiveSMCommon {
691 operation_id,
692 contract: contract.clone(),
693 outpoint: range.into_iter().next().unwrap(),
694 refund_keypair,
695 },
696 state: ReceiveSMState::Funding,
697 })]
698 }),
699 };
700
701 let client_output = self.client_ctx.make_client_outputs(ClientOutputBundle::new(
702 vec![client_output],
703 vec![client_output_sm],
704 ));
705
706 let transaction = TransactionBuilder::new().with_outputs(client_output);
707
708 self.client_ctx
709 .finalize_and_submit_transaction(
710 operation_id,
711 LightningCommonInit::KIND.as_str(),
712 |_| GatewayOperationMetaV2::role(GatewayOperationRoleV2::Receive),
713 transaction,
714 )
715 .await?;
716
717 let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
718 self.client_ctx
719 .log_event(
720 &mut dbtx,
721 IncomingPaymentStarted {
722 operation_start,
723 incoming_contract_commitment: commitment,
724 invoice_amount: Amount::from_msats(amount_msat),
725 },
726 )
727 .await;
728 dbtx.commit_tx().await;
729
730 Ok(self.await_receive(operation_id).await)
731 }
732
733 pub async fn await_receive(&self, operation_id: OperationId) -> FinalReceiveState {
734 let mut stream = self.notifier.subscribe(operation_id).await;
735
736 loop {
737 if let Some(GatewayClientStateMachinesV2::Receive(state)) = stream.next().await {
738 match state.state {
739 ReceiveSMState::Funding => {}
740 ReceiveSMState::Rejected(..) => return FinalReceiveState::Rejected,
741 ReceiveSMState::Success(preimage) => {
742 return FinalReceiveState::Success(preimage);
743 }
744 ReceiveSMState::Refunding(out_points) => {
745 if self
746 .client_ctx
747 .await_primary_module_outputs(operation_id, out_points)
748 .await
749 .is_err()
750 {
751 return FinalReceiveState::Failure;
752 }
753
754 return FinalReceiveState::Refunded;
755 }
756 ReceiveSMState::Failure => return FinalReceiveState::Failure,
757 }
758 }
759 }
760 }
761
762 pub async fn await_completion(&self, operation_id: OperationId) {
771 let mut stream = self.notifier.subscribe(operation_id).await;
772
773 loop {
774 match stream.next().await {
775 Some(GatewayClientStateMachinesV2::Complete(state)) => {
776 if matches!(
777 state.state,
778 CompleteSMState::Completed | CompleteSMState::CompletionFailed(_)
779 ) {
780 info!(%state, "LNv2 completion state machine finished");
781 return;
782 }
783
784 info!(%state, "Waiting for LNv2 completion state machine");
785 }
786 Some(GatewayClientStateMachinesV2::Receive(state)) => {
787 info!(%state, "Waiting for LNv2 completion state machine");
788 continue;
789 }
790 Some(GatewayClientStateMachinesV2::CircuitComplete(state)) => {
791 if matches!(
792 state.state,
793 CompleteSMState::Completed | CompleteSMState::CompletionFailed(_)
794 ) {
795 info!(%state, "LNv2 circuit completion state machine finished");
796 return;
797 }
798
799 info!(%state, "Waiting for LNv2 circuit completion state machine");
800 }
801 Some(state) => {
802 warn!(%state, "Operation is not an LNv2 completion state machine");
803 return;
804 }
805 None => return,
806 }
807 }
808 }
809}
810
811#[async_trait]
817pub trait IGatewayClientV2: Debug + Send + Sync {
818 async fn complete_htlc(
827 &self,
828 htlc_response: InterceptPaymentResponse,
829 ) -> Result<(), LightningRpcError>;
830
831 async fn is_direct_swap(
840 &self,
841 invoice: &Bolt11Invoice,
842 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>>;
843
844 async fn pay(
846 &self,
847 invoice: Bolt11Invoice,
848 max_delay: u64,
849 max_fee: Amount,
850 ) -> Result<[u8; 32], LightningRpcError>;
851
852 async fn min_contract_amount(
860 &self,
861 federation_id: &FederationId,
862 amount: u64,
863 ) -> anyhow::Result<Amount>;
864
865 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>>;
868
869 async fn relay_lnv1_swap(
872 &self,
873 client: &ClientHandleArc,
874 invoice: &Bolt11Invoice,
875 ) -> anyhow::Result<FinalReceiveState>;
876
877 async fn claim_payment_image(
889 &self,
890 payment_image: &PaymentImage,
891 operation_id: OperationId,
892 ) -> bool;
893}
894
895#[cfg(test)]
896mod tests;