1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::must_use_candidate)]
6
7use core::cmp::Ordering;
8use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use db::{DbKeyPrefix, DummyClientFundsKey, DummyClientFundsKeyPrefixAll};
12use fedimint_client_module::db::ClientModuleMigrationFn;
13use fedimint_client_module::error::{ClientModuleError, InsufficientBalanceError};
14use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
15use fedimint_client_module::module::recovery::NoModuleBackup;
16use fedimint_client_module::module::{
17 ClientContext, ClientModule, OutPointRange, PrimaryModulePriority, PrimaryModuleSupport,
18};
19use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
20use fedimint_client_module::transaction::{
21 ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle, ClientOutputSM,
22};
23use fedimint_client_module::{DynGlobalClientContext, sm_enum_variant_translation};
24use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
25use fedimint_core::db::{
26 Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
27};
28use fedimint_core::encoding::{Decodable, Encodable};
29use fedimint_core::module::{
30 AmountUnit, Amounts, ApiVersion, ModuleCommon, ModuleInit, MultiApiVersion,
31};
32use fedimint_core::secp256k1::{Keypair, Secp256k1};
33use fedimint_core::util::BoxStream;
34use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send, push_db_pair_items};
35pub use fedimint_dummy_common as common;
36use fedimint_dummy_common::{DummyCommonInit, DummyInput, DummyModuleTypes, DummyOutput};
37use futures::StreamExt;
38use strum::IntoEnumIterator;
39use tokio::sync::watch;
40
41pub mod db;
42mod input_sm;
43mod output_sm;
44
45use input_sm::{DummyInputSMCommon, DummyInputSMState, DummyInputStateMachine};
46use output_sm::{DummyOutputSMCommon, DummyOutputSMState, DummyOutputStateMachine};
47
48#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
50pub enum DummyStateMachine {
51 Input(DummyInputStateMachine),
52 Output(DummyOutputStateMachine),
53}
54
55impl State for DummyStateMachine {
56 type ModuleContext = DummyClientContext;
57
58 fn transitions(
59 &self,
60 context: &Self::ModuleContext,
61 global_context: &DynGlobalClientContext,
62 ) -> Vec<StateTransition<Self>> {
63 match self {
64 DummyStateMachine::Input(sm) => {
65 sm_enum_variant_translation!(
66 sm.transitions(context, global_context),
67 DummyStateMachine::Input
68 )
69 }
70 DummyStateMachine::Output(sm) => {
71 sm_enum_variant_translation!(
72 sm.transitions(context, global_context),
73 DummyStateMachine::Output
74 )
75 }
76 }
77 }
78
79 fn operation_id(&self) -> OperationId {
80 match self {
81 DummyStateMachine::Input(sm) => sm.operation_id(),
82 DummyStateMachine::Output(sm) => sm.operation_id(),
83 }
84 }
85}
86
87impl IntoDynInstance for DummyStateMachine {
88 type DynType = DynState;
89
90 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
91 DynState::from_typed(instance_id, self)
92 }
93}
94
95pub struct DummyClientModule {
96 key: Keypair,
97 db: Database,
98 notifier: ModuleNotifier<DummyStateMachine>,
99 client_ctx: ClientContext<Self>,
100 balance_update_sender: watch::Sender<()>,
101}
102
103impl std::fmt::Debug for DummyClientModule {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("DummyClientModule").finish_non_exhaustive()
106 }
107}
108
109#[derive(Clone)]
111pub struct DummyClientContext {
112 pub balance_update_sender: watch::Sender<()>,
113}
114
115impl std::fmt::Debug for DummyClientContext {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.debug_struct("DummyClientContext").finish_non_exhaustive()
118 }
119}
120
121impl Context for DummyClientContext {
122 const KIND: Option<ModuleKind> = None;
123}
124
125#[apply(async_trait_maybe_send!)]
126impl ClientModule for DummyClientModule {
127 type Init = DummyClientInit;
128 type Common = DummyModuleTypes;
129 type Backup = NoModuleBackup;
130 type ModuleStateMachineContext = DummyClientContext;
131 type States = DummyStateMachine;
132
133 fn context(&self) -> Self::ModuleStateMachineContext {
134 DummyClientContext {
135 balance_update_sender: self.balance_update_sender.clone(),
136 }
137 }
138
139 fn input_fee(
140 &self,
141 _amount: &Amounts,
142 _input: &<Self::Common as ModuleCommon>::Input,
143 ) -> Option<Amounts> {
144 Some(Amounts::ZERO)
145 }
146
147 fn output_fee(
148 &self,
149 _amount: &Amounts,
150 _output: &<Self::Common as ModuleCommon>::Output,
151 ) -> Option<Amounts> {
152 Some(Amounts::ZERO)
153 }
154
155 fn supports_being_primary(&self) -> PrimaryModuleSupport {
156 PrimaryModuleSupport::Any {
157 priority: PrimaryModulePriority::LOW,
158 }
159 }
160
161 async fn create_final_inputs_and_outputs(
162 &self,
163 dbtx: &mut DatabaseTransaction<'_>,
164 operation_id: OperationId,
165 unit: AmountUnit,
166 input_amount: Amount,
167 output_amount: Amount,
168 ) -> Result<
169 (
170 ClientInputBundle<DummyInput, DummyStateMachine>,
171 ClientOutputBundle<DummyOutput, DummyStateMachine>,
172 ),
173 ClientModuleError,
174 > {
175 dbtx.ensure_isolated().expect("must be isolated");
176
177 match input_amount.cmp(&output_amount) {
178 Ordering::Less => {
179 let missing_input_amount = output_amount.saturating_sub(input_amount);
181
182 let our_funds = get_funds(dbtx, unit).await;
183
184 if our_funds < missing_input_amount {
185 return Err(InsufficientBalanceError {
186 requested_amount: missing_input_amount,
187 total_amount: our_funds,
188 }
189 .into());
190 }
191
192 let updated = our_funds.saturating_sub(missing_input_amount);
193
194 dbtx.insert_entry(&DummyClientFundsKey(unit), &updated)
195 .await;
196
197 let sender = self.balance_update_sender.clone();
198
199 dbtx.on_commit(move || sender.send_replace(()));
200
201 let input = ClientInput {
202 input: DummyInput {
203 amount: missing_input_amount,
204 unit,
205 pub_key: self.key.public_key(),
206 },
207 amounts: Amounts::new_custom(unit, missing_input_amount),
208 keys: vec![self.key],
209 };
210
211 let input_sm = ClientInputSM {
212 state_machines: Arc::new(move |out_point_range: OutPointRange| {
213 out_point_range
214 .into_iter()
215 .map(|out_point| {
216 DummyStateMachine::Input(DummyInputStateMachine {
217 common: DummyInputSMCommon {
218 operation_id,
219 out_point,
220 amount: missing_input_amount,
221 unit,
222 },
223 state: DummyInputSMState::Created,
224 })
225 })
226 .collect()
227 }),
228 };
229
230 Ok((
231 ClientInputBundle::new(vec![input], vec![input_sm]),
232 ClientOutputBundle::new(vec![], vec![]),
233 ))
234 }
235 Ordering::Equal => Ok((
236 ClientInputBundle::new(vec![], vec![]),
237 ClientOutputBundle::new(vec![], vec![]),
238 )),
239 Ordering::Greater => {
240 let missing_output_amount = input_amount.saturating_sub(output_amount);
242
243 let output = ClientOutput {
244 output: DummyOutput {
245 amount: missing_output_amount,
246 unit,
247 },
248 amounts: Amounts::new_custom(unit, missing_output_amount),
249 };
250
251 let output_sm = ClientOutputSM {
252 state_machines: Arc::new(move |out_point_range: OutPointRange| {
253 out_point_range
254 .into_iter()
255 .map(|out_point| {
256 DummyStateMachine::Output(DummyOutputStateMachine {
257 common: DummyOutputSMCommon {
258 operation_id,
259 out_point,
260 amount: missing_output_amount,
261 unit,
262 },
263 state: DummyOutputSMState::Created,
264 })
265 })
266 .collect()
267 }),
268 };
269
270 Ok((
271 ClientInputBundle::new(vec![], vec![]),
272 ClientOutputBundle::new(vec![output], vec![output_sm]),
273 ))
274 }
275 }
276 }
277
278 async fn await_primary_module_output(
279 &self,
280 operation_id: OperationId,
281 out_point: OutPoint,
282 ) -> Result<(), ClientModuleError> {
283 let mut stream = self.notifier.subscribe(operation_id).await;
284
285 loop {
286 let DummyStateMachine::Output(output_sm) = stream
287 .next()
288 .await
289 .expect("Stream should not end before reaching final state")
290 else {
291 continue;
292 };
293
294 if output_sm.common.out_point != out_point {
295 continue;
296 }
297
298 match output_sm.state {
299 DummyOutputSMState::Created => {}
300 DummyOutputSMState::Accepted => return Ok(()),
301 DummyOutputSMState::Rejected => {
302 return Err(ClientModuleError::other("Transaction was rejected"));
303 }
304 }
305 }
306 }
307
308 async fn get_balance(&self, dbtc: &mut DatabaseTransaction<'_>, unit: AmountUnit) -> Amount {
309 get_funds(dbtc, unit).await
310 }
311
312 async fn get_balances(&self, dbtx: &mut DatabaseTransaction<'_>) -> Amounts {
313 get_funds_all(dbtx).await
314 }
315
316 async fn subscribe_balance_changes(&self) -> BoxStream<'static, ()> {
317 Box::pin(tokio_stream::wrappers::WatchStream::new(
318 self.balance_update_sender.subscribe(),
319 ))
320 }
321}
322
323impl DummyClientModule {
324 pub fn create_input(&self, amount: Amount) -> ClientInputBundle {
327 let keypair = Keypair::new(&Secp256k1::new(), &mut rand::rngs::OsRng);
328
329 let client_input = ClientInput {
330 input: DummyInput {
331 amount,
332 unit: AmountUnit::BITCOIN,
333 pub_key: keypair.public_key(),
334 },
335 amounts: Amounts::new_bitcoin(amount),
336 keys: vec![keypair],
337 };
338
339 self.client_ctx
340 .make_client_inputs(ClientInputBundle::new_no_sm(vec![client_input]))
341 }
342
343 pub async fn mock_receive(&self, amount: Amount, unit: AmountUnit) {
345 let mut dbtx = self.db.begin_transaction().await;
346
347 let current = dbtx
348 .get_value(&DummyClientFundsKey(unit))
349 .await
350 .unwrap_or(Amount::ZERO);
351
352 dbtx.insert_entry(&DummyClientFundsKey(unit), &(current + amount))
353 .await;
354
355 dbtx.commit_tx().await;
356 }
357}
358
359async fn get_funds(dbtx: &mut DatabaseTransaction<'_>, unit: AmountUnit) -> Amount {
360 dbtx.get_value(&DummyClientFundsKey(unit))
361 .await
362 .unwrap_or(Amount::ZERO)
363}
364
365async fn get_funds_all(dbtx: &mut DatabaseTransaction<'_>) -> Amounts {
366 dbtx.find_by_prefix(&DummyClientFundsKeyPrefixAll)
367 .await
368 .fold(Amounts::ZERO, |acc, (key, amount)| async move {
369 acc.checked_add_unit(amount, key.0).expect("can't overflow")
370 })
371 .await
372}
373
374#[derive(Debug, Clone)]
375pub struct DummyClientInit;
376
377impl ModuleInit for DummyClientInit {
378 type Common = DummyCommonInit;
379
380 async fn dump_database(
381 &self,
382 dbtx: &mut DatabaseTransaction<'_>,
383 prefix_names: Vec<String>,
384 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
385 let mut items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
386 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
387 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
388 });
389
390 for table in filtered_prefixes {
391 match table {
392 DbKeyPrefix::ClientFunds => {
393 push_db_pair_items!(
394 dbtx,
395 DummyClientFundsKeyPrefixAll,
396 DummyClientFundsKey,
397 Amount,
398 items,
399 "Dummy Funds"
400 );
401 }
402 DbKeyPrefix::ExternalReservedStart
403 | DbKeyPrefix::CoreInternalReservedStart
404 | DbKeyPrefix::CoreInternalReservedEnd => {}
405 }
406 }
407
408 Box::new(items.into_iter())
409 }
410}
411
412#[apply(async_trait_maybe_send!)]
414impl ClientModuleInit for DummyClientInit {
415 type Module = DummyClientModule;
416
417 fn supported_api_versions(&self) -> MultiApiVersion {
418 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
419 .expect("no version conflicts")
420 }
421
422 async fn init(
423 &self,
424 args: &ClientModuleInitArgs<Self>,
425 ) -> Result<Self::Module, ClientModuleError> {
426 Ok(DummyClientModule {
427 key: args
428 .module_root_secret()
429 .clone()
430 .to_secp_key(&Secp256k1::new()),
431 db: args.db().clone(),
432 notifier: args.notifier().clone(),
433 client_ctx: args.context(),
434 balance_update_sender: watch::channel(()).0,
435 })
436 }
437
438 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
439 BTreeMap::new()
440 }
441}