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 anyhow::{Context as _, anyhow, format_err};
12use common::broken_fed_key_pair;
13use db::{
14 DbKeyPrefix, DummyClientFundsKey, DummyClientFundsKeyV1, DummyClientFundsKeyV2PrefixAll,
15 DummyClientNameKey, migrate_to_v1,
16};
17use fedimint_api_client::api::{FederationApiExt, SerdeOutputOutcome, deserialize_outcome};
18use fedimint_client_module::db::{ClientModuleMigrationFn, migrate_state};
19use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
20use fedimint_client_module::module::recovery::NoModuleBackup;
21use fedimint_client_module::module::{
22 ClientContext, ClientModule, IClientModule, OutPointRange, PrimaryModulePriority,
23 PrimaryModuleSupport,
24};
25use fedimint_client_module::sm::{Context, ModuleNotifier};
26use fedimint_client_module::transaction::{
27 ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle,
28 ClientOutputSM, TransactionBuilder,
29};
30use fedimint_core::core::{Decoder, ModuleKind, OperationId};
31use fedimint_core::db::{
32 Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
33};
34#[allow(deprecated)]
35use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
36use fedimint_core::module::{
37 AmountUnit, Amounts, ApiRequestErased, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit,
38 MultiApiVersion,
39};
40use fedimint_core::secp256k1::{Keypair, PublicKey, Secp256k1};
41use fedimint_core::util::{BoxStream, NextOrPending};
42use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send};
43pub use fedimint_dummy_common as common;
44use fedimint_dummy_common::config::DummyClientConfig;
45use fedimint_dummy_common::{
46 DummyCommonInit, DummyInput, DummyInputV1, DummyModuleTypes, DummyOutput, DummyOutputOutcome,
47 DummyOutputV1, KIND, fed_key_pair,
48};
49use futures::{StreamExt, pin_mut};
50use states::DummyStateMachine;
51use strum::IntoEnumIterator;
52
53pub mod api;
54pub mod db;
55pub mod states;
56
57#[derive(Debug)]
58pub struct DummyClientModule {
59 cfg: DummyClientConfig,
60 key: Keypair,
61 notifier: ModuleNotifier<DummyStateMachine>,
62 client_ctx: ClientContext<Self>,
63 db: Database,
64}
65
66#[derive(Debug, Clone)]
68pub struct DummyClientContext {
69 pub dummy_decoder: Decoder,
70}
71
72impl Context for DummyClientContext {
74 const KIND: Option<ModuleKind> = None;
75}
76
77#[apply(async_trait_maybe_send!)]
78impl ClientModule for DummyClientModule {
79 type Init = DummyClientInit;
80 type Common = DummyModuleTypes;
81 type Backup = NoModuleBackup;
82 type ModuleStateMachineContext = DummyClientContext;
83 type States = DummyStateMachine;
84
85 fn context(&self) -> Self::ModuleStateMachineContext {
86 DummyClientContext {
87 dummy_decoder: self.decoder(),
88 }
89 }
90
91 fn input_fee(
92 &self,
93 _amount: &Amounts,
94 _input: &<Self::Common as ModuleCommon>::Input,
95 ) -> Option<Amounts> {
96 Some(Amounts::new_bitcoin(self.cfg.tx_fee))
97 }
98
99 fn output_fee(
100 &self,
101 _amount: &Amounts,
102 _output: &<Self::Common as ModuleCommon>::Output,
103 ) -> Option<Amounts> {
104 Some(Amounts::new_bitcoin(self.cfg.tx_fee))
105 }
106
107 fn supports_being_primary(&self) -> PrimaryModuleSupport {
108 PrimaryModuleSupport::Any {
109 priority: PrimaryModulePriority::LOW,
110 }
111 }
112
113 async fn create_final_inputs_and_outputs(
114 &self,
115 dbtx: &mut DatabaseTransaction<'_>,
116 operation_id: OperationId,
117 unit: AmountUnit,
118 input_amount: Amount,
119 output_amount: Amount,
120 ) -> anyhow::Result<(
121 ClientInputBundle<DummyInput, DummyStateMachine>,
122 ClientOutputBundle<DummyOutput, DummyStateMachine>,
123 )> {
124 dbtx.ensure_isolated().expect("must be isolated");
125
126 match input_amount.cmp(&output_amount) {
127 Ordering::Less => {
128 let missing_input_amount = output_amount.saturating_sub(input_amount);
129
130 let our_funds = get_funds(dbtx, unit).await;
132
133 if our_funds < missing_input_amount {
134 return Err(format_err!("Insufficient funds"));
135 }
136
137 let updated = our_funds.saturating_sub(missing_input_amount);
138 dbtx.insert_entry(&DummyClientFundsKey { unit }, &updated)
139 .await;
140
141 let input = ClientInput {
142 input: DummyInputV1 {
143 amount: missing_input_amount,
144 unit,
145 account: self.key.public_key(),
146 }
147 .into(),
148 amounts: Amounts::new_custom(unit, missing_input_amount),
149 keys: vec![self.key],
150 };
151 let input_sm = ClientInputSM {
152 state_machines: Arc::new(move |out_point_range| {
153 vec![DummyStateMachine::Input(
154 missing_input_amount,
155 unit,
156 out_point_range.txid(),
157 operation_id,
158 )]
159 }),
160 };
161
162 Ok((
163 ClientInputBundle::new(vec![input], vec![input_sm]),
164 ClientOutputBundle::new(vec![], vec![]),
165 ))
166 }
167 Ordering::Equal => Ok((
168 ClientInputBundle::new(vec![], vec![]),
169 ClientOutputBundle::new(vec![], vec![]),
170 )),
171 Ordering::Greater => {
172 let missing_output_amount = input_amount.saturating_sub(output_amount);
173 let output = ClientOutput {
174 output: DummyOutputV1 {
175 amount: missing_output_amount,
176 unit,
177 account: self.key.public_key(),
178 }
179 .into(),
180 amounts: Amounts::new_custom(unit, missing_output_amount),
181 };
182
183 let output_sm = ClientOutputSM {
184 state_machines: Arc::new(move |out_point_range| {
185 vec![DummyStateMachine::Output(
186 missing_output_amount,
187 unit,
188 out_point_range.txid(),
189 operation_id,
190 )]
191 }),
192 };
193
194 Ok((
195 ClientInputBundle::new(vec![], vec![]),
196 ClientOutputBundle::new(vec![output], vec![output_sm]),
197 ))
198 }
199 }
200 }
201
202 async fn await_primary_module_output(
203 &self,
204 operation_id: OperationId,
205 out_point: OutPoint,
206 ) -> anyhow::Result<()> {
207 let stream = self
208 .notifier
209 .subscribe(operation_id)
210 .await
211 .filter_map(|state| async move {
212 match state {
213 DummyStateMachine::OutputDone(_, _, txid, _) => {
214 if txid != out_point.txid {
215 return None;
216 }
217 Some(Ok(()))
218 }
219 DummyStateMachine::Refund(_) => Some(Err(anyhow::anyhow!(
220 "Error occurred processing the dummy transaction"
221 ))),
222 _ => None,
223 }
224 });
225
226 pin_mut!(stream);
227
228 stream.next_or_pending().await
229 }
230
231 async fn get_balance(&self, dbtc: &mut DatabaseTransaction<'_>, unit: AmountUnit) -> Amount {
232 get_funds(dbtc, unit).await
233 }
234
235 async fn get_balances(&self, dbtx: &mut DatabaseTransaction<'_>) -> Amounts {
236 get_funds_all(dbtx).await
237 }
238
239 async fn subscribe_balance_changes(&self) -> BoxStream<'static, ()> {
240 Box::pin(
241 self.notifier
242 .subscribe_all_operations()
243 .filter_map(|state| async move {
244 match state {
245 DummyStateMachine::OutputDone(_, _, _, _)
246 | DummyStateMachine::Input { .. }
247 | DummyStateMachine::Refund(_) => Some(()),
248 _ => None,
249 }
250 }),
251 )
252 }
253}
254
255impl DummyClientModule {
256 pub async fn print_money_units(
257 &self,
258 amount: Amount,
259 unit: AmountUnit,
260 account_kp: Keypair,
261 ) -> anyhow::Result<(OperationId, OutPoint)> {
262 let op_id = OperationId(rand::random());
263
264 let input = ClientInput::<DummyInput> {
267 input: DummyInputV1 {
268 amount,
269 unit,
270 account: account_kp.public_key(),
271 }
272 .into(),
273 amounts: Amounts::new_custom(unit, amount),
274 keys: vec![account_kp],
275 };
276
277 let tx = TransactionBuilder::new().with_inputs(
280 self.client_ctx
281 .make_client_inputs(ClientInputBundle::new_no_sm(vec![input])),
282 );
283 let meta_gen = |change_range: OutPointRange| OutPoint {
284 txid: change_range.txid(),
285 out_idx: 0,
286 };
287 let change_range = self
288 .client_ctx
289 .finalize_and_submit_transaction(op_id, KIND.as_str(), meta_gen, tx)
290 .await?;
291
292 self.client_ctx
294 .await_primary_module_outputs(op_id, change_range.into_iter().collect())
295 .await
296 .context("Waiting for the output of print_using_account")?;
297
298 Ok((
299 op_id,
300 change_range
301 .into_iter()
302 .next()
303 .expect("At least one output"),
304 ))
305 }
306
307 pub async fn print_money(&self, amount: Amount) -> anyhow::Result<(OperationId, OutPoint)> {
309 self.print_money_units(amount, AmountUnit::BITCOIN, fed_key_pair())
310 .await
311 }
312
313 pub async fn print_liability(&self, amount: Amount) -> anyhow::Result<(OperationId, OutPoint)> {
316 self.print_money_units(amount, AmountUnit::BITCOIN, broken_fed_key_pair())
317 .await
318 }
319
320 pub async fn send_money(
322 &self,
323 account: PublicKey,
324 amount: Amount,
325 unit: AmountUnit,
326 ) -> anyhow::Result<OutPoint> {
327 self.db.ensure_isolated().expect("must be isolated");
328
329 let op_id = OperationId(rand::random());
330
331 let output = ClientOutput::<DummyOutput> {
333 output: DummyOutputV1 {
334 amount,
335 unit,
336 account,
337 }
338 .into(),
339 amounts: Amounts::new_custom(unit, amount),
340 };
341
342 let tx = TransactionBuilder::new().with_outputs(
344 self.client_ctx
345 .make_client_outputs(ClientOutputBundle::new_no_sm(vec![output])),
346 );
347
348 let meta_gen = |change_range: OutPointRange| OutPoint {
349 txid: change_range.txid(),
350 out_idx: 0,
351 };
352 let change_range = self
353 .client_ctx
354 .finalize_and_submit_transaction(op_id, DummyCommonInit::KIND.as_str(), meta_gen, tx)
355 .await?;
356
357 let tx_subscription = self.client_ctx.transaction_updates(op_id).await;
358
359 tx_subscription
360 .await_tx_accepted(change_range.txid())
361 .await
362 .map_err(|e| anyhow!(e))?;
363
364 Ok(OutPoint {
365 txid: change_range.txid(),
366 out_idx: 0,
367 })
368 }
369
370 pub async fn receive_money_hack(&self, outpoint: OutPoint) -> anyhow::Result<()> {
372 let mut dbtx = self.db.begin_transaction().await;
373
374 #[allow(deprecated)]
375 let outcome = self
376 .client_ctx
377 .global_api()
378 .request_current_consensus::<SerdeOutputOutcome>(
379 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
380 ApiRequestErased::new(outpoint),
381 )
382 .await?;
383
384 let outcome = deserialize_outcome::<DummyOutputOutcome>(&outcome, &self.decoder())?;
385
386 if outcome.2 != self.key.public_key() {
387 return Err(format_err!("Wrong account id"));
388 }
389
390 dbtx.insert_entry(&DummyClientFundsKey { unit: outcome.1 }, &outcome.0)
396 .await;
397
398 dbtx.commit_tx().await;
399
400 Ok(())
401 }
402
403 pub fn account(&self) -> PublicKey {
405 self.key.public_key()
406 }
407
408 pub async fn get_balance(&self, unit: AmountUnit) -> anyhow::Result<Amount> {
410 let mut dbtx = self.db.begin_transaction_nc().await;
411 Ok(get_funds(&mut dbtx, unit).await)
412 }
413}
414
415async fn get_funds(dbtx: &mut DatabaseTransaction<'_>, unit: AmountUnit) -> Amount {
416 let funds = dbtx.get_value(&DummyClientFundsKey { unit }).await;
417 funds.unwrap_or(Amount::ZERO)
418}
419
420async fn get_funds_all(dbtx: &mut DatabaseTransaction<'_>) -> Amounts {
421 use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
422
423 let funds_entries = dbtx
424 .find_by_prefix(&DummyClientFundsKeyV2PrefixAll)
425 .await
426 .collect::<Vec<_>>()
427 .await;
428
429 let mut result = Amounts::ZERO;
430 for (key, amount) in funds_entries {
431 if amount > Amount::ZERO {
432 result = result
433 .checked_add_unit(amount, key.unit)
434 .expect("We can't overfolow here");
435 }
436 }
437
438 result
439}
440
441#[derive(Debug, Clone)]
442pub struct DummyClientInit;
443
444impl ModuleInit for DummyClientInit {
446 type Common = DummyCommonInit;
447
448 async fn dump_database(
449 &self,
450 dbtx: &mut DatabaseTransaction<'_>,
451 prefix_names: Vec<String>,
452 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
453 let mut items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
454 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
455 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
456 });
457
458 for table in filtered_prefixes {
459 match table {
460 DbKeyPrefix::ClientFunds => {
461 if let Some(funds) = dbtx.get_value(&DummyClientFundsKeyV1).await {
462 items.insert("Dummy Funds".to_string(), Box::new(funds));
463 }
464 }
465 DbKeyPrefix::ClientName => {
466 if let Some(name) = dbtx.get_value(&DummyClientNameKey).await {
467 items.insert("Dummy Name".to_string(), Box::new(name));
468 }
469 }
470 DbKeyPrefix::ExternalReservedStart
471 | DbKeyPrefix::CoreInternalReservedStart
472 | DbKeyPrefix::CoreInternalReservedEnd => {}
473 }
474 }
475
476 Box::new(items.into_iter())
477 }
478}
479
480#[apply(async_trait_maybe_send!)]
482impl ClientModuleInit for DummyClientInit {
483 type Module = DummyClientModule;
484
485 fn supported_api_versions(&self) -> MultiApiVersion {
486 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
487 .expect("no version conflicts")
488 }
489
490 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
491 Ok(DummyClientModule {
492 cfg: args.cfg().clone(),
493 key: args
494 .module_root_secret()
495 .clone()
496 .to_secp_key(&Secp256k1::new()),
497
498 notifier: args.notifier().clone(),
499 client_ctx: args.context(),
500 db: args.db().clone(),
501 })
502 }
503
504 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
505 let mut migrations: BTreeMap<DatabaseVersion, ClientModuleMigrationFn> = BTreeMap::new();
506 migrations.insert(DatabaseVersion(0), |dbtx, _, _| {
507 Box::pin(migrate_to_v1(dbtx))
508 });
509
510 migrations.insert(DatabaseVersion(1), |_, active_states, inactive_states| {
511 Box::pin(async {
512 migrate_state(active_states, inactive_states, db::get_v1_migrated_state)
513 })
514 });
515
516 migrations.insert(DatabaseVersion(2), |_, active_states, inactive_states| {
517 Box::pin(async {
518 migrate_state(active_states, inactive_states, db::get_v2_migrated_state)
519 })
520 });
521 migrations
522 }
523}