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::{Amount, OutPoint};
9use fedimint_lnv2_common::contracts::{IncomingContract, fee_from_expiration};
10use fedimint_lnv2_common::{LightningInput, LightningInputV0};
11use fedimint_logging::LOG_CLIENT_MODULE_LNV2;
12use tpe::AggregateDecryptionKey;
13use tracing::instrument;
14
15use crate::api::LightningFederationApi;
16use crate::events::ReceivePaymentEvent;
17use crate::{LightningClientContext, LightningOperationMeta};
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(
105 &contract.contract_id(),
106 contract.commitment.expiration_or_fee,
107 )
108 .await
109 }
110
111 async fn transition_incoming_contract(
112 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
113 old_state: ReceiveStateMachine,
114 context: LightningClientContext,
115 global_context: DynGlobalClientContext,
116 outpoint: Option<OutPoint>,
117 ) -> ReceiveStateMachine {
118 let Some(outpoint) = outpoint else {
119 return old_state.update(ReceiveSMState::Expired);
120 };
121
122 let client_input = ClientInput::<LightningInput> {
123 input: LightningInput::V0(LightningInputV0::Incoming(
124 outpoint,
125 old_state.common.agg_decryption_key,
126 )),
127 amounts: Amounts::new_bitcoin(old_state.common.contract.commitment.amount),
128 keys: vec![old_state.common.claim_keypair],
129 };
130
131 let change_range = global_context
132 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
133 .await
134 .expect("Cannot claim input, additional funding needed");
135
136 let fee = match context
142 .client_ctx
143 .get_operation(old_state.common.operation_id)
144 .await
145 .map(|operation| operation.meta::<LightningOperationMeta>())
146 {
147 Ok(LightningOperationMeta::Receive(meta)) => meta.gateway_fee(),
151 _ => Amount::from_msats(fee_from_expiration(
152 old_state.common.contract.commitment.expiration_or_fee,
153 )),
154 };
155
156 context
158 .client_ctx
159 .log_event(
160 &mut dbtx.module_tx(),
161 ReceivePaymentEvent {
162 operation_id: old_state.common.operation_id,
163 amount: old_state.common.contract.commitment.amount + fee,
164 fee,
165 },
166 )
167 .await;
168
169 old_state.update(ReceiveSMState::Claiming(change_range.into_iter().collect()))
170 }
171}