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