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