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)]
107pub struct ModuleRecoveryStarted {
108 module_id: ModuleInstanceId,
109}
110
111impl ModuleRecoveryStarted {
112 pub fn new(module_id: ModuleInstanceId) -> Self {
113 Self { module_id }
114 }
115}
116
117impl Event for ModuleRecoveryStarted {
118 const MODULE: Option<ModuleKind> = None;
119 const KIND: EventKind = EventKind::from_static("module-recovery-started");
120 const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
121}
122
123#[derive(Serialize, Deserialize)]
124pub struct ModuleRecoveryCompleted {
125 pub module_id: ModuleInstanceId,
126 #[serde(default)]
133 pub kind: Option<ModuleKind>,
134 #[serde(default)]
144 pub amount: Option<Amount>,
145}
146
147impl Event for ModuleRecoveryCompleted {
148 const MODULE: Option<ModuleKind> = None;
149 const KIND: EventKind = EventKind::from_static("module-recovery-completed");
150 const PERSISTENCE: EventPersistence = EventPersistence::Persistent;
151}
152
153pub type InstancelessDynClientInput = ClientInput<Box<maybe_add_send_sync!(dyn IInput + 'static)>>;
154
155pub type InstancelessDynClientInputSM =
156 ClientInputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
157
158pub type InstancelessDynClientInputBundle = ClientInputBundle<
159 Box<maybe_add_send_sync!(dyn IInput + 'static)>,
160 Box<maybe_add_send_sync!(dyn IState + 'static)>,
161>;
162
163pub type InstancelessDynClientOutput =
164 ClientOutput<Box<maybe_add_send_sync!(dyn IOutput + 'static)>>;
165
166pub type InstancelessDynClientOutputSM =
167 ClientOutputSM<Box<maybe_add_send_sync!(dyn IState + 'static)>>;
168pub type InstancelessDynClientOutputBundle = ClientOutputBundle<
169 Box<maybe_add_send_sync!(dyn IOutput + 'static)>,
170 Box<maybe_add_send_sync!(dyn IState + 'static)>,
171>;
172
173#[derive(Debug, Error)]
174pub enum AddStateMachinesError {
175 #[error("State already exists in database")]
176 StateAlreadyExists,
177 #[error("Got {0}")]
178 Other(#[from] anyhow::Error),
179}
180
181pub type AddStateMachinesResult = Result<(), AddStateMachinesError>;
182
183#[apply(async_trait_maybe_send!)]
184pub trait IGlobalClientContext: Debug + MaybeSend + MaybeSync + 'static {
185 fn module_api(&self) -> DynModuleApi;
188
189 async fn client_config(&self) -> ClientConfig;
190
191 fn api(&self) -> &DynGlobalApi;
198
199 fn decoders(&self) -> &ModuleDecoderRegistry;
200
201 async fn claim_inputs_dyn(
206 &self,
207 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
208 inputs: InstancelessDynClientInputBundle,
209 ) -> anyhow::Result<OutPointRange>;
210
211 async fn fund_output_dyn(
216 &self,
217 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
218 outputs: InstancelessDynClientOutputBundle,
219 ) -> anyhow::Result<OutPointRange>;
220
221 async fn add_state_machine_dyn(
223 &self,
224 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
225 sm: Box<maybe_add_send_sync!(dyn IState)>,
226 ) -> AddStateMachinesResult;
227
228 async fn log_event_json(
229 &self,
230 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
231 kind: EventKind,
232 module: Option<(ModuleKind, ModuleInstanceId)>,
233 payload: serde_json::Value,
234 persist: EventPersistence,
235 );
236
237 async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM>;
238
239 async fn core_api_version(&self) -> ApiVersion;
241}
242
243#[apply(async_trait_maybe_send!)]
244impl IGlobalClientContext for () {
245 fn module_api(&self) -> DynModuleApi {
246 unimplemented!("fake implementation, only for tests");
247 }
248
249 async fn client_config(&self) -> ClientConfig {
250 unimplemented!("fake implementation, only for tests");
251 }
252
253 fn api(&self) -> &DynGlobalApi {
254 unimplemented!("fake implementation, only for tests");
255 }
256
257 fn decoders(&self) -> &ModuleDecoderRegistry {
258 unimplemented!("fake implementation, only for tests");
259 }
260
261 async fn claim_inputs_dyn(
262 &self,
263 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
264 _input: InstancelessDynClientInputBundle,
265 ) -> anyhow::Result<OutPointRange> {
266 unimplemented!("fake implementation, only for tests");
267 }
268
269 async fn fund_output_dyn(
270 &self,
271 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
272 _outputs: InstancelessDynClientOutputBundle,
273 ) -> anyhow::Result<OutPointRange> {
274 unimplemented!("fake implementation, only for tests");
275 }
276
277 async fn add_state_machine_dyn(
278 &self,
279 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
280 _sm: Box<maybe_add_send_sync!(dyn IState)>,
281 ) -> AddStateMachinesResult {
282 unimplemented!("fake implementation, only for tests");
283 }
284
285 async fn log_event_json(
286 &self,
287 _dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
288 _kind: EventKind,
289 _module: Option<(ModuleKind, ModuleInstanceId)>,
290 _payload: serde_json::Value,
291 _persist: EventPersistence,
292 ) {
293 unimplemented!("fake implementation, only for tests");
294 }
295
296 async fn transaction_update_stream(&self) -> BoxStream<TxSubmissionStatesSM> {
297 unimplemented!("fake implementation, only for tests");
298 }
299
300 async fn core_api_version(&self) -> ApiVersion {
301 unimplemented!("fake implementation, only for tests");
302 }
303}
304
305dyn_newtype_define! {
306 #[derive(Clone)]
309 pub DynGlobalClientContext(Arc<IGlobalClientContext>)
310}
311
312impl DynGlobalClientContext {
313 pub fn new_fake() -> Self {
314 DynGlobalClientContext::from(())
315 }
316
317 pub async fn await_tx_accepted(&self, query_txid: TransactionId) -> Result<(), String> {
318 self.transaction_update_stream()
319 .await
320 .filter_map(|tx_update| {
321 std::future::ready(match tx_update.state {
322 TxSubmissionStates::Accepted(txid) if txid == query_txid => Some(Ok(())),
323 TxSubmissionStates::Rejected(txid, submit_error) if txid == query_txid => {
324 Some(Err(submit_error))
325 }
326 _ => None,
327 })
328 })
329 .next_or_pending()
330 .await
331 }
332
333 pub async fn claim_inputs<I, S>(
334 &self,
335 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
336 inputs: ClientInputBundle<I, S>,
337 ) -> anyhow::Result<OutPointRange>
338 where
339 I: IInput + MaybeSend + MaybeSync + 'static,
340 S: IState + MaybeSend + MaybeSync + 'static,
341 {
342 self.claim_inputs_dyn(dbtx, inputs.into_instanceless())
343 .await
344 }
345
346 pub async fn fund_output<O, S>(
355 &self,
356 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
357 outputs: ClientOutputBundle<O, S>,
358 ) -> anyhow::Result<OutPointRange>
359 where
360 O: IOutput + MaybeSend + MaybeSync + 'static,
361 S: IState + MaybeSend + MaybeSync + 'static,
362 {
363 self.fund_output_dyn(dbtx, outputs.into_instanceless())
364 .await
365 }
366
367 pub async fn add_state_machine<S>(
371 &self,
372 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
373 sm: S,
374 ) -> AddStateMachinesResult
375 where
376 S: State + MaybeSend + MaybeSync + 'static,
377 {
378 self.add_state_machine_dyn(dbtx, box_up_state(sm)).await
379 }
380
381 async fn log_event<E>(&self, dbtx: &mut ClientSMDatabaseTransaction<'_, '_>, event: E)
382 where
383 E: Event + Send,
384 {
385 self.log_event_json(
386 dbtx,
387 E::KIND,
388 E::MODULE.map(|m| (m, dbtx.module_id())),
389 serde_json::to_value(&event).expect("Payload serialization can't fail"),
390 <E as Event>::PERSISTENCE,
391 )
392 .await;
393 }
394}
395
396fn states_to_instanceless_dyn<S: IState + MaybeSend + MaybeSync + 'static>(
397 state_gen: StateGenerator<S>,
398) -> StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>> {
399 Arc::new(move |out_point_range| {
400 let states: Vec<S> = state_gen(out_point_range);
401 states
402 .into_iter()
403 .map(|state| box_up_state(state))
404 .collect()
405 })
406}
407
408fn box_up_state(state: impl IState + 'static) -> Box<maybe_add_send_sync!(dyn IState + 'static)> {
411 Box::new(state)
412}
413
414impl<T> From<Arc<T>> for DynGlobalClientContext
415where
416 T: IGlobalClientContext,
417{
418 fn from(inner: Arc<T>) -> Self {
419 DynGlobalClientContext { inner }
420 }
421}
422
423fn states_add_instance(
424 module_instance_id: ModuleInstanceId,
425 state_gen: StateGenerator<Box<maybe_add_send_sync!(dyn IState + 'static)>>,
426) -> StateGenerator<DynState> {
427 Arc::new(move |out_point_range| {
428 let states = state_gen(out_point_range);
429 Iterator::collect(
430 states
431 .into_iter()
432 .map(|state| DynState::from_parts(module_instance_id, state)),
433 )
434 })
435}
436
437pub type ModuleGlobalContextGen = ContextGen;
438
439pub struct ClientModuleInstance<'m, M: ClientModule> {
441 pub id: ModuleInstanceId,
443 pub db: Database,
445 pub api: DynModuleApi,
447
448 pub module: &'m M,
449}
450
451impl<'m, M: ClientModule> ClientModuleInstance<'m, M> {
452 pub fn inner(&self) -> &'m M {
454 self.module
455 }
456}
457
458impl<M> ops::Deref for ClientModuleInstance<'_, M>
459where
460 M: ClientModule,
461{
462 type Target = M;
463
464 fn deref(&self) -> &Self::Target {
465 self.module
466 }
467}
468#[derive(Deserialize)]
469pub struct GetInviteCodeRequest {
470 pub peer: PeerId,
471}
472
473pub struct TransactionUpdates {
474 pub update_stream: BoxStream<'static, TxSubmissionStatesSM>,
475}
476
477impl TransactionUpdates {
478 pub async fn await_tx_accepted(self, await_txid: TransactionId) -> Result<(), String> {
481 debug!(target: LOG_CLIENT, %await_txid, "Await tx accepted");
482 self.update_stream
483 .filter_map(|tx_update| {
484 std::future::ready(match tx_update.state {
485 TxSubmissionStates::Accepted(txid) if txid == await_txid => Some(Ok(())),
486 TxSubmissionStates::Rejected(txid, submit_error) if txid == await_txid => {
487 Some(Err(submit_error))
488 }
489 _ => None,
490 })
491 })
492 .next_or_pending()
493 .await?;
494 debug!(target: LOG_CLIENT, %await_txid, "Tx accepted");
495 Ok(())
496 }
497}
498
499pub struct AdminCreds {
501 pub peer_id: PeerId,
503 pub auth: ApiAuth,
505}