fedimint_lnv2_client/
receive_sm.rs1use 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)]
48impl 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}