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