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