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::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;
17use crate::events::ReceivePaymentEvent;
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)]
50impl 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(&contract.contract_id(), contract.commitment.expiration)
105 .await
106 }
107
108 async fn transition_incoming_contract(
109 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
110 old_state: ReceiveStateMachine,
111 context: LightningClientContext,
112 global_context: DynGlobalClientContext,
113 outpoint: Option<OutPoint>,
114 ) -> ReceiveStateMachine {
115 let Some(outpoint) = outpoint else {
116 return old_state.update(ReceiveSMState::Expired);
117 };
118
119 let client_input = ClientInput::<LightningInput> {
120 input: LightningInput::V0(LightningInputV0::Incoming(
121 outpoint,
122 old_state.common.agg_decryption_key,
123 )),
124 amounts: Amounts::new_bitcoin(old_state.common.contract.commitment.amount),
125 keys: vec![old_state.common.claim_keypair],
126 };
127
128 let change_range = global_context
129 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
130 .await
131 .expect("Cannot claim input, additional funding needed");
132
133 context
135 .client_ctx
136 .log_event(
137 &mut dbtx.module_tx(),
138 ReceivePaymentEvent {
139 operation_id: old_state.common.operation_id,
140 amount: old_state.common.contract.commitment.amount,
141 },
142 )
143 .await;
144
145 old_state.update(ReceiveSMState::Claiming(change_range.into_iter().collect()))
146 }
147}