Skip to main content

fedimint_gwv2_client/
lib.rs

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
62/// LNv2 CLTV Delta in blocks
63pub 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/// Identifies the role of an LNv2 gateway operation.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(untagged)]
135pub enum GatewayOperationMetaV2 {
136    /// Metadata written before operation roles were distinguished.
137    Legacy(()),
138    /// Metadata written for a role-specific operation.
139    Role {
140        /// Role used when recovering active operations.
141        role: GatewayOperationRoleV2,
142    },
143}
144
145/// Role of an LNv2 gateway operation.
146#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum GatewayOperationRoleV2 {
149    /// An outgoing payment operation.
150    Send,
151    /// A federation receive operation.
152    Receive,
153    /// An incoming Lightning circuit completion operation.
154    CircuitCompletion,
155}
156
157impl GatewayOperationMetaV2 {
158    /// Constructs metadata for `role`.
159    pub fn role(role: GatewayOperationRoleV2) -> Self {
160        Self::Role { role }
161    }
162
163    /// Returns whether shutdown must await Lightning circuit completion.
164    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    /// Legacy completion state retained to decode and resume combined receive
286    /// operations created by older clients.
287    Complete(CompleteStateMachine),
288    /// Completes one incoming circuit independently of other circuits carrying
289    /// the same payment hash and amount.
290    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        // The operation id is equal to the contract id which also doubles as the
382        // message signed by the gateway via the forfeit signature to forfeit
383        // the gateways claim to a contract in case of cancellation. We only create a
384        // forfeit signature after we have started the send state machine to
385        // prevent replay attacks with a previously cancelled outgoing contract
386        let operation_id = OperationId::from_encodable(&payload.contract.clone());
387
388        // Since the following checks may only fail due to client side
389        // programming error we do not have to enable cancellation and can check
390        // them before we start the state machine.
391        ensure!(
392            payload.contract.claim_pk == self.keypair.public_key(),
393            "The outgoing contract is keyed to another gateway"
394        );
395
396        // This prevents DOS attacks where an attacker submits a different invoice.
397        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        // The operation id is derived from the contract, which is public in the
411        // funding transaction, and joining the operation yields its preimage. So
412        // the join belongs behind the signature above.
413        if self.client_ctx.operation_exists(operation_id).await {
414            return Ok(self.subscribe_send(operation_id).await);
415        }
416
417        // We need to check that the contract has been confirmed by the federation
418        // before we start the state machine to prevent DOS attacks.
419        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                        // The preimage is proof the payment succeeded, so return it to
501                        // the sender as soon as it is available rather than waiting for
502                        // an additional ordering. The gateway's claim of the outgoing
503                        // contract has already been submitted by the send state machine
504                        // and finalizes in the background.
505                        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    /// Waits for a legacy combined receive operation or a circuit-completion
763    /// operation to finish.
764    ///
765    /// `operation_id` must identify either a legacy operation containing
766    /// [`GatewayClientStateMachinesV2::Complete`] or a role-specific operation
767    /// containing [`GatewayClientStateMachinesV2::CircuitComplete`]. Both
768    /// successful completion and a durable permanent outcome conflict terminate
769    /// the wait.
770    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/// An interface between module implementation and the general `Gateway`
812///
813/// To abstract away and decouple the core gateway from the modules, the
814/// interface between the is expressed as a trait. The core gateway handles
815/// LNv2 operations that require access to the database or lightning node.
816#[async_trait]
817pub trait IGatewayClientV2: Debug + Send + Sync {
818    /// Uses the gateway's Lightning node to complete a payment.
819    ///
820    /// Implementations must absorb and retry every transient node or
821    /// connectivity failure. They return `Err` only when Lightning has reached
822    /// a permanent state that makes the requested outcome impossible. The
823    /// future may block while retrying and must remain cancellation-safe.
824    /// Completion state machines persist any returned error as terminal
825    /// `CompletionFailed`.
826    async fn complete_htlc(
827        &self,
828        htlc_response: InterceptPaymentResponse,
829    ) -> Result<(), LightningRpcError>;
830
831    /// Determines if the payment can be completed using a direct swap to
832    /// another federation.
833    ///
834    /// A direct swap is determined by checking the gateway's connected
835    /// lightning node against the invoice's payee lightning node. If they
836    /// are the same, then the gateway can use another client to complete
837    /// the payment be swapping ecash instead of a payment over the
838    /// Lightning network.
839    async fn is_direct_swap(
840        &self,
841        invoice: &Bolt11Invoice,
842    ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>>;
843
844    /// Initiates a payment over the Lightning network.
845    async fn pay(
846        &self,
847        invoice: Bolt11Invoice,
848        max_delay: u64,
849        max_fee: Amount,
850    ) -> Result<[u8; 32], LightningRpcError>;
851
852    /// Computes the minimum contract amount necessary for making an outgoing
853    /// payment.
854    ///
855    /// The minimum contract amount must contain transaction fees to cover the
856    /// gateway's transaction fee and optionally additional fee to cover the
857    /// gateway's Lightning fee if the payment goes over the Lightning
858    /// network.
859    async fn min_contract_amount(
860        &self,
861        federation_id: &FederationId,
862        amount: u64,
863    ) -> anyhow::Result<Amount>;
864
865    /// Check if this invoice was created using LNv1 and if the gateway is
866    /// connected to the target federation.
867    async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>>;
868
869    /// Perform a swap from an LNv2 `OutgoingContract` to an LNv1
870    /// `IncomingContract`
871    async fn relay_lnv1_swap(
872        &self,
873        client: &ClientHandleArc,
874        invoice: &Bolt11Invoice,
875    ) -> anyhow::Result<FinalReceiveState>;
876
877    /// Claims the given payment image for `operation_id` in the gateway's
878    /// global database, returning `true` if this operation may claim the
879    /// outgoing contract (the image was unclaimed, or already claimed by
880    /// this same operation) and `false` if another operation already
881    /// claimed it.
882    ///
883    /// A single Lightning payment yields a single preimage, so the gateway may
884    /// claim at most one outgoing contract per payment image. Unlike the
885    /// per-federation module database, this spans all of the gateway's
886    /// federations, so two clients in different federations paying the same
887    /// invoice cannot both be claimed.
888    async fn claim_payment_image(
889        &self,
890        payment_image: &PaymentImage,
891        operation_id: OperationId,
892    ) -> bool;
893}
894
895#[cfg(test)]
896mod tests;