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::OutPoint;
5use fedimint_core::core::OperationId;
6use fedimint_core::encoding::{Decodable, Encodable};
7use fedimint_core::module::Amounts;
8use fedimint_core::secp256k1::Keypair;
9use fedimint_lnv2_common::contracts::IncomingContract;
10use fedimint_lnv2_common::{LightningInput, LightningInputV0};
11use fedimint_logging::LOG_CLIENT_MODULE_LNV2;
12use tpe::AggregateDecryptionKey;
13use tracing::instrument;
14
15use crate::LightningClientContext;
16use crate::api::LightningFederationApi;
17
18#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
19pub struct ReceiveStateMachine {
20    pub common: ReceiveSMCommon,
21    pub state: ReceiveSMState,
22}
23
24impl ReceiveStateMachine {
25    pub fn update(&self, state: ReceiveSMState) -> Self {
26        Self {
27            common: self.common.clone(),
28            state,
29        }
30    }
31}
32
33#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
34pub struct ReceiveSMCommon {
35    pub operation_id: OperationId,
36    pub contract: IncomingContract,
37    pub claim_keypair: Keypair,
38    pub agg_decryption_key: AggregateDecryptionKey,
39}
40
41#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
42pub enum ReceiveSMState {
43    Pending,
44    Claiming(Vec<OutPoint>),
45    Expired,
46}
47
48#[cfg_attr(doc, aquamarine::aquamarine)]
49/// State machine that waits on the receipt of a Lightning payment.
50///
51/// ```mermaid
52/// graph LR
53/// classDef virtual fill:#fff,stroke-dasharray: 5 5
54///
55///     Pending -- incoming contract is confirmed --> Claiming
56///     Pending -- decryption contract expires --> Expired
57/// ```
58impl State for ReceiveStateMachine {
59    type ModuleContext = LightningClientContext;
60
61    fn transitions(
62        &self,
63        _context: &Self::ModuleContext,
64        global_context: &DynGlobalClientContext,
65    ) -> Vec<StateTransition<Self>> {
66        let gc = global_context.clone();
67
68        match &self.state {
69            ReceiveSMState::Pending => {
70                vec![StateTransition::new(
71                    Self::await_incoming_contract(self.common.contract.clone(), gc.clone()),
72                    move |dbtx, contract_confirmed, old_state| {
73                        Box::pin(Self::transition_incoming_contract(
74                            dbtx,
75                            old_state,
76                            gc.clone(),
77                            contract_confirmed,
78                        ))
79                    },
80                )]
81            }
82            ReceiveSMState::Claiming(..) | ReceiveSMState::Expired => {
83                vec![]
84            }
85        }
86    }
87
88    fn operation_id(&self) -> OperationId {
89        self.common.operation_id
90    }
91}
92
93impl ReceiveStateMachine {
94    #[instrument(target = LOG_CLIENT_MODULE_LNV2, skip(global_context))]
95    async fn await_incoming_contract(
96        contract: IncomingContract,
97        global_context: DynGlobalClientContext,
98    ) -> Option<OutPoint> {
99        global_context
100            .module_api()
101            .await_incoming_contract(&contract.contract_id(), contract.commitment.expiration)
102            .await
103    }
104
105    async fn transition_incoming_contract(
106        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
107        old_state: ReceiveStateMachine,
108        global_context: DynGlobalClientContext,
109        outpoint: Option<OutPoint>,
110    ) -> ReceiveStateMachine {
111        let Some(outpoint) = outpoint else {
112            return old_state.update(ReceiveSMState::Expired);
113        };
114
115        let client_input = ClientInput::<LightningInput> {
116            input: LightningInput::V0(LightningInputV0::Incoming(
117                outpoint,
118                old_state.common.agg_decryption_key,
119            )),
120            amounts: Amounts::new_bitcoin(old_state.common.contract.commitment.amount),
121            keys: vec![old_state.common.claim_keypair],
122        };
123
124        let change_range = global_context
125            .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
126            .await
127            .expect("Cannot claim input, additional funding needed");
128
129        old_state.update(ReceiveSMState::Claiming(change_range.into_iter().collect()))
130    }
131}