Skip to main content

fedimint_lnv2_client/
receive_sm.rs

1use fedimint_client_module::DynGlobalClientContext;
2use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
3use fedimint_client_module::transaction::{ClientInput, ClientInputBundle};
4use fedimint_core::core::OperationId;
5use fedimint_core::encoding::{Decodable, Encodable};
6use fedimint_core::module::Amounts;
7use fedimint_core::secp256k1::Keypair;
8use fedimint_core::{Amount, OutPoint};
9use fedimint_lnv2_common::contracts::{IncomingContract, fee_from_expiration};
10use fedimint_lnv2_common::{LightningInput, LightningInputV0};
11use fedimint_logging::LOG_CLIENT_MODULE_LNV2;
12use tpe::AggregateDecryptionKey;
13use tracing::instrument;
14
15use crate::api::LightningFederationApi;
16use crate::events::ReceivePaymentEvent;
17use crate::{LightningClientContext, LightningOperationMeta};
18
19#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
20pub struct ReceiveStateMachine {
21    pub common: ReceiveSMCommon,
22    pub state: ReceiveSMState,
23}
24
25impl ReceiveStateMachine {
26    pub fn update(&self, state: ReceiveSMState) -> Self {
27        Self {
28            common: self.common.clone(),
29            state,
30        }
31    }
32}
33
34#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
35pub struct ReceiveSMCommon {
36    pub operation_id: OperationId,
37    pub contract: IncomingContract,
38    pub claim_keypair: Keypair,
39    pub agg_decryption_key: AggregateDecryptionKey,
40}
41
42#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
43pub enum ReceiveSMState {
44    Pending,
45    Claiming(Vec<OutPoint>),
46    Expired,
47}
48
49#[cfg_attr(doc, aquamarine::aquamarine)]
50/// State machine that waits on the receipt of a Lightning payment.
51///
52/// ```mermaid
53/// graph LR
54/// classDef virtual fill:#fff,stroke-dasharray: 5 5
55///
56///     Pending -- incoming contract is confirmed --> Claiming
57///     Pending -- decryption contract expires --> Expired
58/// ```
59impl State for ReceiveStateMachine {
60    type ModuleContext = LightningClientContext;
61
62    fn transitions(
63        &self,
64        context: &Self::ModuleContext,
65        global_context: &DynGlobalClientContext,
66    ) -> Vec<StateTransition<Self>> {
67        let gc = global_context.clone();
68        let ctx = context.clone();
69
70        match &self.state {
71            ReceiveSMState::Pending => {
72                vec![StateTransition::new(
73                    Self::await_incoming_contract(self.common.contract.clone(), gc.clone()),
74                    move |dbtx, contract_confirmed, old_state| {
75                        Box::pin(Self::transition_incoming_contract(
76                            dbtx,
77                            old_state,
78                            ctx.clone(),
79                            gc.clone(),
80                            contract_confirmed,
81                        ))
82                    },
83                )]
84            }
85            ReceiveSMState::Claiming(..) | ReceiveSMState::Expired => {
86                vec![]
87            }
88        }
89    }
90
91    fn operation_id(&self) -> OperationId {
92        self.common.operation_id
93    }
94}
95
96impl ReceiveStateMachine {
97    #[instrument(target = LOG_CLIENT_MODULE_LNV2, skip(global_context))]
98    async fn await_incoming_contract(
99        contract: IncomingContract,
100        global_context: DynGlobalClientContext,
101    ) -> Option<OutPoint> {
102        global_context
103            .module_api()
104            .await_incoming_contract(
105                &contract.contract_id(),
106                contract.commitment.expiration_or_fee,
107            )
108            .await
109    }
110
111    async fn transition_incoming_contract(
112        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
113        old_state: ReceiveStateMachine,
114        context: LightningClientContext,
115        global_context: DynGlobalClientContext,
116        outpoint: Option<OutPoint>,
117    ) -> ReceiveStateMachine {
118        let Some(outpoint) = outpoint else {
119            return old_state.update(ReceiveSMState::Expired);
120        };
121
122        let client_input = ClientInput::<LightningInput> {
123            input: LightningInput::V0(LightningInputV0::Incoming(
124                outpoint,
125                old_state.common.agg_decryption_key,
126            )),
127            amounts: Amounts::new_bitcoin(old_state.common.contract.commitment.amount),
128            keys: vec![old_state.common.claim_keypair],
129        };
130
131        let change_range = global_context
132            .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
133            .await
134            .expect("Cannot claim input, additional funding needed");
135
136        // The event reports the invoice amount and the gateway fee separately.
137        // Manual receives carry the invoice in their operation meta, so the fee
138        // is the difference between invoice and contract amount. Lnurl receives
139        // do not have an invoice on the client, so the fee is recovered from the
140        // fee-encoded contract expiration set by the recurring daemon instead.
141        let fee = match context
142            .client_ctx
143            .get_operation(old_state.common.operation_id)
144            .await
145            .map(|operation| operation.meta::<LightningOperationMeta>())
146        {
147            // A receive operation meta is only recorded for manually created
148            // invoices; lnurl receives have no invoice on the client (and no
149            // operation), so the fee is recovered from the fee-encoded expiration.
150            Ok(LightningOperationMeta::Receive(meta)) => meta.gateway_fee(),
151            _ => Amount::from_msats(fee_from_expiration(
152                old_state.common.contract.commitment.expiration_or_fee,
153            )),
154        };
155
156        // Log event when receive completes successfully
157        context
158            .client_ctx
159            .log_event(
160                &mut dbtx.module_tx(),
161                ReceivePaymentEvent {
162                    operation_id: old_state.common.operation_id,
163                    amount: old_state.common.contract.commitment.amount + fee,
164                    fee,
165                },
166            )
167            .await;
168
169        old_state.update(ReceiveSMState::Claiming(change_range.into_iter().collect()))
170    }
171}