1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::doc_markdown)]
4#![allow(clippy::explicit_deref_methods)]
5#![allow(clippy::missing_errors_doc)]
6#![allow(clippy::missing_panics_doc)]
7#![allow(clippy::module_name_repetitions)]
8#![allow(clippy::must_use_candidate)]
9#![allow(clippy::needless_lifetimes)]
10#![allow(clippy::return_self_not_must_use)]
11#![allow(clippy::too_many_lines)]
12#![allow(clippy::type_complexity)]
13
14use std::fmt::Debug;
15use std::ops::{self};
16use std::sync::Arc;
17
18use fedimint_api_client::api::{DynGlobalApi, DynModuleApi};
19use fedimint_core::config::ClientConfig;
20pub use fedimint_core::core::{IInput, IOutput, ModuleInstanceId, ModuleKind, OperationId};
21use fedimint_core::db::Database;
22use fedimint_core::module::ApiAuth;
23use fedimint_core::module::registry::ModuleDecoderRegistry;
24use fedimint_core::task::{MaybeSend, MaybeSync};
25use fedimint_core::util::{BoxStream, NextOrPending};
26use fedimint_core::{
27 PeerId, TransactionId, apply, async_trait_maybe_send, dyn_newtype_define, maybe_add_send_sync,
28};
29use fedimint_eventlog::{Event, EventKind};
30use fedimint_logging::LOG_CLIENT;
31use futures::StreamExt;
32use module::OutPointRange;
33use serde::{Deserialize, Serialize};
34use thiserror::Error;
35use tracing::debug;
36use transaction::{
37 ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputSM, TxSubmissionStatesSM,
38};
39
40pub use crate::module::{ClientModule, StateGenerator};
41use crate::sm::executor::ContextGen;
42use crate::sm::{ClientSMDatabaseTransaction, DynState, IState, State};
43use crate::transaction::{ClientInput, ClientOutputBundle, TxSubmissionStates};
44
45pub mod api;
46
47pub mod db;
48
49pub mod backup;
50pub mod envs;
52pub mod meta;
53pub mod module;
55pub mod oplog;
57pub mod secret;
59pub mod sm;
61pub mod transaction;
63
64pub mod api_version_discovery;
65
66#[derive(Serialize, Deserialize)]
67pub struct TxCreatedEvent {
68 pub txid: TransactionId,
69 pub operation_id: OperationId,
70}
71
72impl Event for TxCreatedEvent {
73 const MODULE: Option<ModuleKind> = None;
74
75 const KIND: EventKind = EventKind::from_static("tx-created");
76}
77
78#[derive(Serialize, Deserialize)]
79pub struct TxAcceptedEvent {
80 txid: TransactionId,
81 operation_id: OperationId,
82}
83
84impl Event for TxAcceptedEvent {
85 const MODULE: Option<ModuleKind> = None;
86
87 const KIND: EventKind = EventKind::from_static("tx-accepted");
88}
89
90#[derive(Serialize, Deserialize)]
91pub struct TxRejectedEvent {
92 txid: TransactionId,
93 error: String,
94 operation_id: OperationId,
95}
96impl Event for TxRejectedEvent {
97 const MODULE: Option<ModuleKind> = None;
98
99 const KIND: EventKind = EventKind::from_static("tx-rejected");
100}
101
102#[derive(Serialize, Deserialize)]
103pub struct ModuleRecoveryStarted {
104 module_id: ModuleInstanceId,
105}
106
107impl ModuleRecoveryStarted {
108 pub fn new(module_id: ModuleInstanceId) -> Self {
109 Self { module_id }
110 }
111}
112
113impl Event for ModuleRecoveryStarted {
114 const MODULE: Option<ModuleKind> = None;
115
116 const KIND: EventKind = EventKind::from_static("module-recovery-started");
117}
118
119#[derive(Serialize, Deserialize)]
120pub struct ModuleRecoveryCompleted {
121 pub module_id: ModuleInstanceId,
122}
123
124impl Event for ModuleRecoveryCompleted {
125 const MODULE: Option<ModuleKind> = None;
126
127 const KIND: EventKind = EventKind::from_static("module-recovery-completed");
128}
129
130pub type InstancelessDynClientInput = ClientInput<Box<maybe_add_send_sync!(dyn IInput + 'static)>>;
131
132pub type InstancelessDynClientInputSM =
133 ClientInputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
134
135pub type InstancelessDynClientInputBundle = ClientInputBundle<
136 Box<maybe_add_send_sync!(dyn IInput + 'static)>,
137 Box<maybe_add_send_sync!(dyn IState + 'static)>,
138>;
139
140pub type InstancelessDynClientOutput =
141 ClientOutput<Box<maybe_add_send_sync!(dyn IOutput + 'static)>>;
142
143pub type InstancelessDynClientOutputSM =
144 ClientOutputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
145pub type InstancelessDynClientOutputBundle = ClientOutputBundle<
146 Box<maybe_add_send_sync!(dyn IOutput + 'static)>,
147 Box<maybe_add_send_sync!(dyn IState + 'static)>,
148>;
149
150#[derive(Debug, Error)]
151pub enum AddStateMachinesError {
152 #[error("State already exists in database")]
153 StateAlreadyExists,
154 #[error("Got {0}")]
155 Other(#[from] anyhow::Error),
156}
157
158pub type AddStateMachinesResult = Result<(), AddStateMachinesError>;
159
160#[apply(async_trait_maybe_send!)]
161pub trait IGlobalClientContext: Debug + MaybeSend + MaybeSync + 'static {
162 fn module_api(&self) -> DynModuleApi;
165
166 async fn client_config(&self) -> ClientConfig;
167
168 fn api(&self) -> &DynGlobalApi;
175
176 fn decoders(&self) -> &ModuleDecoderRegistry;
177
178 async fn claim_inputs_dyn(
183 &self,
184 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
185 inputs: InstancelessDynClientInputBundle,
186 ) -> anyhow::Result<OutPointRange>;
187
188 async fn fund_output_dyn(
193 &self,
194 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
195 outputs: InstancelessDynClientOutputBundle,
196 ) -> anyhow::Result<OutPointRange>;
197
198 async fn add_state_machine_dyn(
200 &self,
201 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
202 sm: Box<maybe_add_send_sync!(dyn IState)>,
203 ) -> AddStateMachinesResult;
204
205 async fn log_event_json(
206 &self,
207 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
208 kind: EventKind,
209 module: Option<(ModuleKind, ModuleInstanceId)>,
210 payload: serde_json::Value,
211 persist: bool,
212 );
213
214 async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM>;
215}
216
217#[apply(async_trait_maybe_send!)]
218impl IGlobalClientContext for () {
219 fn module_api(&self) -> DynModuleApi {
220 unimplemented!("fake implementation, only for tests");
221 }
222
223 async fn client_config(&self) -> ClientConfig {
224 unimplemented!("fake implementation, only for tests");
225 }
226
227 fn api(&self) -> &DynGlobalApi {
228 unimplemented!("fake implementation, only for tests");
229 }
230
231 fn decoders(&self) -> &ModuleDecoderRegistry {
232 unimplemented!("fake implementation, only for tests");
233 }
234
235 async fn claim_inputs_dyn(
236 &self,
237 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
238 _input: InstancelessDynClientInputBundle,
239 ) -> anyhow::Result<OutPointRange> {
240 unimplemented!("fake implementation, only for tests");
241 }
242
243 async fn fund_output_dyn(
244 &self,
245 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
246 _outputs: InstancelessDynClientOutputBundle,
247 ) -> anyhow::Result<OutPointRange> {
248 unimplemented!("fake implementation, only for tests");
249 }
250
251 async fn add_state_machine_dyn(
252 &self,
253 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
254 _sm: Box<maybe_add_send_sync!(dyn IState)>,
255 ) -> AddStateMachinesResult {
256 unimplemented!("fake implementation, only for tests");
257 }
258
259 async fn log_event_json(
260 &self,
261 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
262 _kind: EventKind,
263 _module: Option<(ModuleKind, ModuleInstanceId)>,
264 _payload: serde_json::Value,
265 _persist: bool,
266 ) {
267 unimplemented!("fake implementation, only for tests");
268 }
269
270 async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM> {
271 unimplemented!("fake implementation, only for tests");
272 }
273}
274
275dyn_newtype_define! {
276 #[derive(Clone)]
279 pub DynGlobalClientContext(Arc<IGlobalClientContext>)
280}
281
282impl DynGlobalClientContext {
283 pub fn new_fake() -> Self {
284 DynGlobalClientContext::from(())
285 }
286
287 pub async fn await_tx_accepted(&self, query_txid: TransactionId) -> Result<(), String> {
288 self.transaction_update_stream()
289 .await
290 .filter_map(|tx_update| {
291 std::future::ready(match tx_update.state {
292 TxSubmissionStates::Accepted(txid) if txid == query_txid => Some(Ok(())),
293 TxSubmissionStates::Rejected(txid, submit_error) if txid == query_txid => {
294 Some(Err(submit_error))
295 }
296 _ => None,
297 })
298 })
299 .next_or_pending()
300 .await
301 }
302
303 pub async fn claim_inputs<I, S>(
304 &self,
305 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
306 inputs: ClientInputBundle<I, S>,
307 ) -> anyhow::Result<OutPointRange>
308 where
309 I: IInput + MaybeSend + MaybeSync + 'static,
310 S: IState + MaybeSend + MaybeSync + 'static,
311 {
312 self.claim_inputs_dyn(dbtx, inputs.into_instanceless())
313 .await
314 }
315
316 pub async fn fund_output<O, S>(
325 &self,
326 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
327 outputs: ClientOutputBundle<O, S>,
328 ) -> anyhow::Result<OutPointRange>
329 where
330 O: IOutput + MaybeSend + MaybeSync + 'static,
331 S: IState + MaybeSend + MaybeSync + 'static,
332 {
333 self.fund_output_dyn(dbtx, outputs.into_instanceless())
334 .await
335 }
336
337 pub async fn add_state_machine<S>(
341 &self,
342 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
343 sm: S,
344 ) -> AddStateMachinesResult
345 where
346 S: State + MaybeSend + MaybeSync + 'static,
347 {
348 self.add_state_machine_dyn(dbtx, box_up_state(sm)).await
349 }
350
351 async fn log_event<E>(&self, dbtx: &mut ClientSMDatabaseTransaction<'_, '_>, event: E)
352 where
353 E: Event + Send,
354 {
355 self.log_event_json(
356 dbtx,
357 E::KIND,
358 E::MODULE.map(|m| (m, dbtx.module_id())),
359 serde_json::to_value(&event).expect("Payload serialization can't fail"),
360 <E as Event>::PERSIST,
361 )
362 .await;
363 }
364}
365
366fn states_to_instanceless_dyn<S: IState + MaybeSend + MaybeSync + 'static>(
367 state_gen: StateGenerator<S>,
368) -> StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>> {
369 Arc::new(move |out_point_range| {
370 let states: Vec<S> = state_gen(out_point_range);
371 states
372 .into_iter()
373 .map(|state| box_up_state(state))
374 .collect()
375 })
376}
377
378fn box_up_state(state: impl IState + 'static) -> Box<maybe_add_send_sync!(dyn IState + 'static)> {
381 Box::new(state)
382}
383
384impl<T> From<Arc<T>> for DynGlobalClientContext
385where
386 T: IGlobalClientContext,
387{
388 fn from(inner: Arc<T>) -> Self {
389 DynGlobalClientContext { inner }
390 }
391}
392
393fn states_add_instance(
394 module_instance_id: ModuleInstanceId,
395 state_gen: StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>>,
396) -> StateGenerator<DynState> {
397 Arc::new(move |out_point_range| {
398 let states = state_gen(out_point_range);
399 Iterator::collect(
400 states
401 .into_iter()
402 .map(|state| DynState::from_parts(module_instance_id, state)),
403 )
404 })
405}
406
407pub type ModuleGlobalContextGen = ContextGen;
408
409pub struct ClientModuleInstance<'m, M: ClientModule> {
411 pub id: ModuleInstanceId,
413 pub db: Database,
415 pub api: DynModuleApi,
417
418 pub module: &'m M,
419}
420
421impl<'m, M: ClientModule> ClientModuleInstance<'m, M> {
422 pub fn inner(&self) -> &'m M {
424 self.module
425 }
426}
427
428impl<'m, M> ops::Deref for ClientModuleInstance<'m, M>
429where
430 M: ClientModule,
431{
432 type Target = M;
433
434 fn deref(&self) -> &Self::Target {
435 self.module
436 }
437}
438#[derive(Deserialize)]
439pub struct GetInviteCodeRequest {
440 pub peer: PeerId,
441}
442
443pub struct TransactionUpdates {
444 pub update_stream: BoxStream<'static, TxSubmissionStatesSM>,
445}
446
447impl TransactionUpdates {
448 pub async fn await_tx_accepted(self, await_txid: TransactionId) -> Result<(), String> {
451 debug!(target: LOG_CLIENT, %await_txid, "Await tx accepted");
452 self.update_stream
453 .filter_map(|tx_update| {
454 std::future::ready(match tx_update.state {
455 TxSubmissionStates::Accepted(txid) if txid == await_txid => Some(Ok(())),
456 TxSubmissionStates::Rejected(txid, submit_error) if txid == await_txid => {
457 Some(Err(submit_error))
458 }
459 _ => None,
460 })
461 })
462 .next_or_pending()
463 .await?;
464 debug!(target: LOG_CLIENT, %await_txid, "Tx accepted");
465 Ok(())
466 }
467}
468
469pub struct AdminCreds {
471 pub peer_id: PeerId,
473 pub auth: ApiAuth,
475}