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::core::OperationId;
5use fedimint_core::encoding::{Decodable, Encodable};
6use fedimint_core::module::Amounts;
7use fedimint_core::secp256k1::Keypair;
8use fedimint_core::util::FmtCompact;
9use fedimint_core::{Amount, OutPoint};
10use fedimint_lnv2_common::contracts::{IncomingContract, fee_from_expiration};
11use fedimint_lnv2_common::{LightningInput, LightningInputV0};
12use fedimint_logging::LOG_CLIENT_MODULE_LNV2;
13use tpe::AggregateDecryptionKey;
14use tracing::{instrument, warn};
15
16use crate::api::LightningFederationApi;
17use crate::events::ReceivePaymentEvent;
18use crate::{LightningClientContext, LightningOperationMeta};
19
20#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
21pub struct ReceiveStateMachine {
22 pub common: ReceiveSMCommon,
23 pub state: ReceiveSMState,
24}
25
26impl ReceiveStateMachine {
27 pub fn update(&self, state: ReceiveSMState) -> Self {
28 Self {
29 common: self.common.clone(),
30 state,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
36pub struct ReceiveSMCommon {
37 pub operation_id: OperationId,
38 pub contract: IncomingContract,
39 pub claim_keypair: Keypair,
40 pub agg_decryption_key: AggregateDecryptionKey,
41}
42
43#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
44pub enum ReceiveSMState {
45 Pending,
46 Claiming(Vec<OutPoint>),
47 Expired,
48 Uneconomical,
53}
54
55impl State for ReceiveStateMachine {
66 type ModuleContext = LightningClientContext;
67
68 fn transitions(
69 &self,
70 context: &Self::ModuleContext,
71 global_context: &DynGlobalClientContext,
72 ) -> Vec<StateTransition<Self>> {
73 let gc = global_context.clone();
74 let ctx = context.clone();
75
76 match &self.state {
77 ReceiveSMState::Pending => {
78 vec![StateTransition::new(
79 Self::await_incoming_contract(self.common.contract.clone(), gc.clone()),
80 move |dbtx, contract_confirmed, old_state| {
81 Box::pin(Self::transition_incoming_contract(
82 dbtx,
83 old_state,
84 ctx.clone(),
85 gc.clone(),
86 contract_confirmed,
87 ))
88 },
89 )]
90 }
91 ReceiveSMState::Claiming(..)
92 | ReceiveSMState::Expired
93 | ReceiveSMState::Uneconomical => {
94 vec![]
95 }
96 }
97 }
98
99 fn operation_id(&self) -> OperationId {
100 self.common.operation_id
101 }
102}
103
104impl ReceiveStateMachine {
105 #[instrument(target = LOG_CLIENT_MODULE_LNV2, skip(global_context))]
106 async fn await_incoming_contract(
107 contract: IncomingContract,
108 global_context: DynGlobalClientContext,
109 ) -> Option<OutPoint> {
110 global_context
111 .module_api()
112 .await_incoming_contract(
113 &contract.contract_id(),
114 contract.commitment.expiration_or_fee,
115 )
116 .await
117 }
118
119 async fn transition_incoming_contract(
120 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
121 old_state: ReceiveStateMachine,
122 context: LightningClientContext,
123 global_context: DynGlobalClientContext,
124 outpoint: Option<OutPoint>,
125 ) -> ReceiveStateMachine {
126 let Some(outpoint) = outpoint else {
127 return old_state.update(ReceiveSMState::Expired);
128 };
129
130 let client_input = ClientInput::<LightningInput> {
131 input: LightningInput::V0(LightningInputV0::Incoming(
132 outpoint,
133 old_state.common.agg_decryption_key,
134 )),
135 amounts: Amounts::new_bitcoin(old_state.common.contract.commitment.amount),
136 keys: vec![old_state.common.claim_keypair],
137 };
138
139 let change_range = match global_context
140 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
141 .await
142 {
143 Ok(change_range) => change_range,
144 Err(err) => {
152 warn!(
153 target: LOG_CLIENT_MODULE_LNV2,
154 err = %err.fmt_compact(),
155 amount = %old_state.common.contract.commitment.amount,
156 "Not claiming incoming contract, its amount does not cover the claim fee"
157 );
158
159 return old_state.update(ReceiveSMState::Uneconomical);
160 }
161 };
162
163 let fee = match context
169 .client_ctx
170 .get_operation(old_state.common.operation_id)
171 .await
172 .map(|operation| operation.meta::<LightningOperationMeta>())
173 {
174 Ok(LightningOperationMeta::Receive(meta)) => meta.gateway_fee(),
178 _ => Amount::from_msats(fee_from_expiration(
179 old_state.common.contract.commitment.expiration_or_fee,
180 )),
181 };
182
183 context
185 .client_ctx
186 .log_event(
187 &mut dbtx.module_tx(),
188 ReceivePaymentEvent {
189 operation_id: old_state.common.operation_id,
190 amount: old_state.common.contract.commitment.amount + fee,
191 fee,
192 },
193 )
194 .await;
195
196 old_state.update(ReceiveSMState::Claiming(change_range.into_iter().collect()))
197 }
198}