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::FmtCompactAnyhow;
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
55#[cfg_attr(doc, aquamarine::aquamarine)]
56impl State for ReceiveStateMachine {
67 type ModuleContext = LightningClientContext;
68
69 fn transitions(
70 &self,
71 context: &Self::ModuleContext,
72 global_context: &DynGlobalClientContext,
73 ) -> Vec<StateTransition<Self>> {
74 let gc = global_context.clone();
75 let ctx = context.clone();
76
77 match &self.state {
78 ReceiveSMState::Pending => {
79 vec![StateTransition::new(
80 Self::await_incoming_contract(self.common.contract.clone(), gc.clone()),
81 move |dbtx, contract_confirmed, old_state| {
82 Box::pin(Self::transition_incoming_contract(
83 dbtx,
84 old_state,
85 ctx.clone(),
86 gc.clone(),
87 contract_confirmed,
88 ))
89 },
90 )]
91 }
92 ReceiveSMState::Claiming(..)
93 | ReceiveSMState::Expired
94 | ReceiveSMState::Uneconomical => {
95 vec![]
96 }
97 }
98 }
99
100 fn operation_id(&self) -> OperationId {
101 self.common.operation_id
102 }
103}
104
105impl ReceiveStateMachine {
106 #[instrument(target = LOG_CLIENT_MODULE_LNV2, skip(global_context))]
107 async fn await_incoming_contract(
108 contract: IncomingContract,
109 global_context: DynGlobalClientContext,
110 ) -> Option<OutPoint> {
111 global_context
112 .module_api()
113 .await_incoming_contract(
114 &contract.contract_id(),
115 contract.commitment.expiration_or_fee,
116 )
117 .await
118 }
119
120 async fn transition_incoming_contract(
121 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
122 old_state: ReceiveStateMachine,
123 context: LightningClientContext,
124 global_context: DynGlobalClientContext,
125 outpoint: Option<OutPoint>,
126 ) -> ReceiveStateMachine {
127 let Some(outpoint) = outpoint else {
128 return old_state.update(ReceiveSMState::Expired);
129 };
130
131 let client_input = ClientInput::<LightningInput> {
132 input: LightningInput::V0(LightningInputV0::Incoming(
133 outpoint,
134 old_state.common.agg_decryption_key,
135 )),
136 amounts: Amounts::new_bitcoin(old_state.common.contract.commitment.amount),
137 keys: vec![old_state.common.claim_keypair],
138 };
139
140 let change_range = match global_context
141 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
142 .await
143 {
144 Ok(change_range) => change_range,
145 Err(err) => {
153 warn!(
154 target: LOG_CLIENT_MODULE_LNV2,
155 err = %err.fmt_compact_anyhow(),
156 amount = %old_state.common.contract.commitment.amount,
157 "Not claiming incoming contract, its amount does not cover the claim fee"
158 );
159
160 return old_state.update(ReceiveSMState::Uneconomical);
161 }
162 };
163
164 let fee = match context
170 .client_ctx
171 .get_operation(old_state.common.operation_id)
172 .await
173 .map(|operation| operation.meta::<LightningOperationMeta>())
174 {
175 Ok(LightningOperationMeta::Receive(meta)) => meta.gateway_fee(),
179 _ => Amount::from_msats(fee_from_expiration(
180 old_state.common.contract.commitment.expiration_or_fee,
181 )),
182 };
183
184 context
186 .client_ctx
187 .log_event(
188 &mut dbtx.module_tx(),
189 ReceivePaymentEvent {
190 operation_id: old_state.common.operation_id,
191 amount: old_state.common.contract.commitment.amount + fee,
192 fee,
193 },
194 )
195 .await;
196
197 old_state.update(ReceiveSMState::Claiming(change_range.into_iter().collect()))
198 }
199}