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::registry::ModuleDecoderRegistry;
23use fedimint_core::module::{ApiAuth, ApiVersion};
24use fedimint_core::task::{MaybeSend, MaybeSync};
25use fedimint_core::util::{BoxStream, NextOrPending};
26use fedimint_core::{
27 Amount, PeerId, TransactionId, apply, async_trait_maybe_send, dyn_newtype_define,
28 maybe_add_send_sync,
29};
30use fedimint_eventlog::{Event, EventKind, EventPersistence};
31use fedimint_logging::LOG_CLIENT;
32use futures::StreamExt;
33use module::OutPointRange;
34use serde::{Deserialize, Serialize};
35use tracing::debug;
36use transaction::{
37 ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputSM, TxSubmissionStatesSM,
38};
39
40pub use crate::error::{
41 AddStateMachinesError, ApiVersionDiscoveryError, ClientModuleError, MetaFetchError,
42 ModuleLookupError, OperationAlreadyExistsError, OperationLookupError, OperationNotFoundError,
43 TransactionSubmitError,
44};
45pub use crate::module::{ClientModule, StateGenerator};
46use crate::sm::executor::ContextGen;
47use crate::sm::{ClientSMDatabaseTransaction, DynState, IState, State};
48use crate::transaction::{ClientInput, ClientOutputBundle, TxSubmissionStates};
49
50pub mod api;
51
52pub mod db;
53
54pub mod error;
56
57pub mod backup;
58pub mod envs;
60pub mod meta;
61pub mod module;
63pub mod oplog;
65pub mod secret;
67pub mod sm;
69pub mod transaction;
71
72pub mod api_version_discovery;
73
74#[derive(Serialize, Deserialize)]
75pub struct TxCreatedEvent {
76 pub txid: TransactionId,
77 pub operation_id: OperationId,
78}
79
80impl Event for TxCreatedEvent {
81 const MODULE: Option<ModuleKind> = None;
82 const KIND: EventKind = EventKind::from_static("tx-created");
83 const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
84}
85
86#[derive(Serialize, Deserialize)]
87pub struct TxAcceptedEvent {
88 txid: TransactionId,
89 operation_id: OperationId,
90}
91
92impl Event for TxAcceptedEvent {
93 const MODULE: Option<ModuleKind> = None;
94 const KIND: EventKind = EventKind::from_static("tx-accepted");
95 const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
96}
97
98#[derive(Serialize, Deserialize)]
99pub struct TxRejectedEvent {
100 txid: TransactionId,
101 error: String,
102 operation_id: OperationId,
103}
104impl Event for TxRejectedEvent {
105 const MODULE: Option<ModuleKind> = None;
106 const KIND: EventKind = EventKind::from_static("tx-rejected");
107 const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
108}
109
110#[derive(Serialize, Deserialize)]
129pub struct TxSubmissionStalledEvent {
130 pub txid: TransactionId,
131 pub operation_id: OperationId,
132 pub attempt: u64,
134 pub elapsed_s: u64,
136}
137
138impl Event for TxSubmissionStalledEvent {
139 const MODULE: Option<ModuleKind> = None;
140 const KIND: EventKind = EventKind::from_static("tx-submission-stalled");
141 const PERSISTENCE: EventPersistence = EventPersistence::Transient;
142}
143
144#[derive(Serialize, Deserialize)]
145pub struct ModuleRecoveryStarted {
146 module_id: ModuleInstanceId,
147}
148
149impl ModuleRecoveryStarted {
150 pub fn new(module_id: ModuleInstanceId) -> Self {
151 Self { module_id }
152 }
153}
154
155impl Event for ModuleRecoveryStarted {
156 const MODULE: Option<ModuleKind> = None;
157 const KIND: EventKind = EventKind::from_static("module-recovery-started");
158 const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
159}
160
161#[derive(Serialize, Deserialize)]
162pub struct ModuleRecoveryCompleted {
163 pub module_id: ModuleInstanceId,
164 #[serde(default)]
171 pub kind: Option<ModuleKind>,
172 #[serde(default)]
182 pub amount: Option<Amount>,
183}
184
185impl Event for ModuleRecoveryCompleted {
186 const MODULE: Option<ModuleKind> = None;
187 const KIND: EventKind = EventKind::from_static("module-recovery-completed");
188 const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
189}
190
191pub type InstancelessDynClientInput = ClientInput<Box<maybe_add_send_sync!(dyn IInput + 'static)>>;
192
193pub type InstancelessDynClientInputSM =
194 ClientInputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
195
196pub type InstancelessDynClientInputBundle = ClientInputBundle<
197 Box<maybe_add_send_sync!(dyn IInput + 'static)>,
198 Box<maybe_add_send_sync!(dyn IState + 'static)>,
199>;
200
201pub type InstancelessDynClientOutput =
202 ClientOutput<Box<maybe_add_send_sync!(dyn IOutput + 'static)>>;
203
204pub type InstancelessDynClientOutputSM =
205 ClientOutputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
206pub type InstancelessDynClientOutputBundle = ClientOutputBundle<
207 Box<maybe_add_send_sync!(dyn IOutput + 'static)>,
208 Box<maybe_add_send_sync!(dyn IState + 'static)>,
209>;
210
211pub type AddStateMachinesResult = Result<(), AddStateMachinesError>;
212
213#[apply(async_trait_maybe_send!)]
214pub trait IGlobalClientContext: Debug + MaybeSend + MaybeSync + 'static {
215 fn module_api(&self) -> DynModuleApi;
218
219 async fn client_config(&self) -> ClientConfig;
220
221 fn api(&self) -> &DynGlobalApi;
228
229 fn decoders(&self) -> &ModuleDecoderRegistry;
230
231 async fn claim_inputs_dyn(
236 &self,
237 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
238 inputs: InstancelessDynClientInputBundle,
239 ) -> Result<OutPointRange, TransactionSubmitError>;
240
241 async fn fund_output_dyn(
246 &self,
247 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
248 outputs: InstancelessDynClientOutputBundle,
249 ) -> Result<OutPointRange, TransactionSubmitError>;
250
251 async fn add_state_machine_dyn(
253 &self,
254 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
255 sm: Box<maybe_add_send_sync!(dyn IState)>,
256 ) -> AddStateMachinesResult;
257
258 async fn log_event_json(
259 &self,
260 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
261 kind: EventKind,
262 module: Option<(ModuleKind, ModuleInstanceId)>,
263 payload: serde_json::Value,
264 persist: EventPersistence,
265 );
266
267 async fn log_event_json_no_dbtx(
274 &self,
275 kind: EventKind,
276 module_kind: Option<ModuleKind>,
277 payload: serde_json::Value,
278 persist: EventPersistence,
279 );
280
281 async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM>;
282
283 async fn core_api_version(&self) -> ApiVersion;
285}
286
287#[apply(async_trait_maybe_send!)]
288impl IGlobalClientContext for () {
289 fn module_api(&self) -> DynModuleApi {
290 unimplemented!("fake implementation, only for tests");
291 }
292
293 async fn client_config(&self) -> ClientConfig {
294 unimplemented!("fake implementation, only for tests");
295 }
296
297 fn api(&self) -> &DynGlobalApi {
298 unimplemented!("fake implementation, only for tests");
299 }
300
301 fn decoders(&self) -> &ModuleDecoderRegistry {
302 unimplemented!("fake implementation, only for tests");
303 }
304
305 async fn claim_inputs_dyn(
306 &self,
307 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
308 _input: InstancelessDynClientInputBundle,
309 ) -> Result<OutPointRange, TransactionSubmitError> {
310 unimplemented!("fake implementation, only for tests");
311 }
312
313 async fn fund_output_dyn(
314 &self,
315 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
316 _outputs: InstancelessDynClientOutputBundle,
317 ) -> Result<OutPointRange, TransactionSubmitError> {
318 unimplemented!("fake implementation, only for tests");
319 }
320
321 async fn add_state_machine_dyn(
322 &self,
323 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
324 _sm: Box<maybe_add_send_sync!(dyn IState)>,
325 ) -> AddStateMachinesResult {
326 unimplemented!("fake implementation, only for tests");
327 }
328
329 async fn log_event_json(
330 &self,
331 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
332 _kind: EventKind,
333 _module: Option<(ModuleKind, ModuleInstanceId)>,
334 _payload: serde_json::Value,
335 _persist: EventPersistence,
336 ) {
337 unimplemented!("fake implementation, only for tests");
338 }
339
340 async fn log_event_json_no_dbtx(
341 &self,
342 _kind: EventKind,
343 _module_kind: Option<ModuleKind>,
344 _payload: serde_json::Value,
345 _persist: EventPersistence,
346 ) {
347 unimplemented!("fake implementation, only for tests");
348 }
349
350 async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM> {
351 unimplemented!("fake implementation, only for tests");
352 }
353
354 async fn core_api_version(&self) -> ApiVersion {
355 unimplemented!("fake implementation, only for tests");
356 }
357}
358
359dyn_newtype_define! {
360 #[derive(Clone)]
363 pub DynGlobalClientContext(Arc<IGlobalClientContext>)
364}
365
366impl DynGlobalClientContext {
367 pub fn new_fake() -> Self {
368 DynGlobalClientContext::from(())
369 }
370
371 pub async fn await_tx_accepted(&self, query_txid: TransactionId) -> Result<(), String> {
372 self.transaction_update_stream()
373 .await
374 .filter_map(|tx_update| {
375 std::future::ready(match tx_update.state {
376 TxSubmissionStates::Accepted(txid) if txid == query_txid => Some(Ok(())),
377 TxSubmissionStates::Rejected(txid, submit_error) if txid == query_txid => {
378 Some(Err(submit_error))
379 }
380 _ => None,
381 })
382 })
383 .next_or_pending()
384 .await
385 }
386
387 pub async fn claim_inputs<I, S>(
388 &self,
389 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
390 inputs: ClientInputBundle<I, S>,
391 ) -> Result<OutPointRange, TransactionSubmitError>
392 where
393 I: IInput + MaybeSend + MaybeSync + 'static,
394 S: IState + MaybeSend + MaybeSync + 'static,
395 {
396 self.claim_inputs_dyn(dbtx, inputs.into_instanceless())
397 .await
398 }
399
400 pub async fn fund_output<O, S>(
409 &self,
410 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
411 outputs: ClientOutputBundle<O, S>,
412 ) -> Result<OutPointRange, TransactionSubmitError>
413 where
414 O: IOutput + MaybeSend + MaybeSync + 'static,
415 S: IState + MaybeSend + MaybeSync + 'static,
416 {
417 self.fund_output_dyn(dbtx, outputs.into_instanceless())
418 .await
419 }
420
421 pub async fn add_state_machine<S>(
425 &self,
426 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
427 sm: S,
428 ) -> AddStateMachinesResult
429 where
430 S: State + MaybeSend + MaybeSync + 'static,
431 {
432 self.add_state_machine_dyn(dbtx, box_up_state(sm)).await
433 }
434
435 async fn log_event<E>(&self, dbtx: &mut ClientSMDatabaseTransaction<'_, '_>, event: E)
436 where
437 E: Event + Send,
438 {
439 self.log_event_json(
440 dbtx,
441 E::KIND,
442 E::MODULE.map(|m| (m, dbtx.module_id())),
443 serde_json::to_value(&event).expect("Payload serialization can't fail"),
444 <E as Event>::PERSISTENCE,
445 )
446 .await;
447 }
448
449 async fn log_event_no_dbtx<E>(&self, event: E)
454 where
455 E: Event + Send,
456 {
457 self.log_event_json_no_dbtx(
458 E::KIND,
459 E::MODULE,
460 serde_json::to_value(&event).expect("Payload serialization can't fail"),
461 <E as Event>::PERSISTENCE,
462 )
463 .await;
464 }
465}
466
467fn states_to_instanceless_dyn<S: IState + MaybeSend + MaybeSync + 'static>(
468 state_gen: StateGenerator<S>,
469) -> StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>> {
470 Arc::new(move |out_point_range| {
471 let states: Vec<S> = state_gen(out_point_range);
472 states
473 .into_iter()
474 .map(|state| box_up_state(state))
475 .collect()
476 })
477}
478
479fn box_up_state(state: impl IState + 'static) -> Box<maybe_add_send_sync!(dyn IState + 'static)> {
482 Box::new(state)
483}
484
485impl<T> From<Arc<T>> for DynGlobalClientContext
486where
487 T: IGlobalClientContext,
488{
489 fn from(inner: Arc<T>) -> Self {
490 DynGlobalClientContext { inner }
491 }
492}
493
494fn states_add_instance(
495 module_instance_id: ModuleInstanceId,
496 state_gen: StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>>,
497) -> StateGenerator<DynState> {
498 Arc::new(move |out_point_range| {
499 let states = state_gen(out_point_range);
500 Iterator::collect(
501 states
502 .into_iter()
503 .map(|state| DynState::from_parts(module_instance_id, state)),
504 )
505 })
506}
507
508pub type ModuleGlobalContextGen = ContextGen;
509
510pub struct ClientModuleInstance<'m, M: ClientModule> {
512 pub id: ModuleInstanceId,
514 pub db: Database,
516 pub api: DynModuleApi,
518
519 pub module: &'m M,
520}
521
522impl<'m, M: ClientModule> ClientModuleInstance<'m, M> {
523 pub fn inner(&self) -> &'m M {
525 self.module
526 }
527}
528
529impl<M> ops::Deref for ClientModuleInstance<'_, M>
530where
531 M: ClientModule,
532{
533 type Target = M;
534
535 fn deref(&self) -> &Self::Target {
536 self.module
537 }
538}
539#[derive(Deserialize)]
540pub struct GetInviteCodeRequest {
541 pub peer: PeerId,
542}
543
544pub struct TransactionUpdates {
545 pub update_stream: BoxStream<'static, TxSubmissionStatesSM>,
546}
547
548impl TransactionUpdates {
549 pub async fn await_tx_accepted(self, await_txid: TransactionId) -> Result<(), String> {
552 debug!(target: LOG_CLIENT, %await_txid, "Await tx accepted");
553 self.update_stream
554 .filter_map(|tx_update| {
555 std::future::ready(match tx_update.state {
556 TxSubmissionStates::Accepted(txid) if txid == await_txid => Some(Ok(())),
557 TxSubmissionStates::Rejected(txid, submit_error) if txid == await_txid => {
558 Some(Err(submit_error))
559 }
560 _ => None,
561 })
562 })
563 .next_or_pending()
564 .await?;
565 debug!(target: LOG_CLIENT, %await_txid, "Tx accepted");
566 Ok(())
567 }
568}
569
570pub struct AdminCreds {
572 pub peer_id: PeerId,
574 pub auth: ApiAuth,
576}