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::{DbKeyPrefix, DummyClientFundsKeyV1, DummyClientNameKey, migrate_to_v1};
14use fedimint_api_client::api::{FederationApiExt, SerdeOutputOutcome, deserialize_outcome};
15use fedimint_client_module::db::{ClientMigrationFn, migrate_state};
16use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
17use fedimint_client_module::module::recovery::NoModuleBackup;
18use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
19use fedimint_client_module::sm::{Context, ModuleNotifier};
20use fedimint_client_module::transaction::{
21 ClientInput, ClientInputBundle, ClientInputSM, ClientOutput, ClientOutputBundle,
22 ClientOutputSM, TransactionBuilder,
23};
24use fedimint_core::core::{Decoder, ModuleKind, OperationId};
25use fedimint_core::db::{
26 Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
27};
28#[allow(deprecated)]
29use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
30use fedimint_core::module::{
31 ApiRequestErased, ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
32};
33use fedimint_core::secp256k1::{Keypair, PublicKey, Secp256k1};
34use fedimint_core::util::{BoxStream, NextOrPending};
35use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send};
36pub use fedimint_dummy_common as common;
37use fedimint_dummy_common::config::DummyClientConfig;
38use fedimint_dummy_common::{
39 DummyCommonInit, DummyInput, DummyModuleTypes, DummyOutput, DummyOutputOutcome, KIND,
40 fed_key_pair,
41};
42use futures::{StreamExt, pin_mut};
43use states::DummyStateMachine;
44use strum::IntoEnumIterator;
45
46pub mod api;
47pub mod db;
48pub mod states;
49
50#[derive(Debug)]
51pub struct DummyClientModule {
52 cfg: DummyClientConfig,
53 key: Keypair,
54 notifier: ModuleNotifier<DummyStateMachine>,
55 client_ctx: ClientContext<Self>,
56 db: Database,
57}
58
59#[derive(Debug, Clone)]
61pub struct DummyClientContext {
62 pub dummy_decoder: Decoder,
63}
64
65impl Context for DummyClientContext {
67 const KIND: Option<ModuleKind> = None;
68}
69
70#[apply(async_trait_maybe_send!)]
71impl ClientModule for DummyClientModule {
72 type Init = DummyClientInit;
73 type Common = DummyModuleTypes;
74 type Backup = NoModuleBackup;
75 type ModuleStateMachineContext = DummyClientContext;
76 type States = DummyStateMachine;
77
78 fn context(&self) -> Self::ModuleStateMachineContext {
79 DummyClientContext {
80 dummy_decoder: self.decoder(),
81 }
82 }
83
84 fn input_fee(
85 &self,
86 _amount: Amount,
87 _input: &<Self::Common as ModuleCommon>::Input,
88 ) -> Option<Amount> {
89 Some(self.cfg.tx_fee)
90 }
91
92 fn output_fee(
93 &self,
94 _amount: Amount,
95 _output: &<Self::Common as ModuleCommon>::Output,
96 ) -> Option<Amount> {
97 Some(self.cfg.tx_fee)
98 }
99
100 fn supports_being_primary(&self) -> bool {
101 true
102 }
103
104 async fn create_final_inputs_and_outputs(
105 &self,
106 dbtx: &mut DatabaseTransaction<'_>,
107 operation_id: OperationId,
108 input_amount: Amount,
109 output_amount: Amount,
110 ) -> anyhow::Result<(
111 ClientInputBundle<DummyInput, DummyStateMachine>,
112 ClientOutputBundle<DummyOutput, DummyStateMachine>,
113 )> {
114 dbtx.ensure_isolated().expect("must be isolated");
115
116 match input_amount.cmp(&output_amount) {
117 Ordering::Less => {
118 let missing_input_amount = output_amount.saturating_sub(input_amount);
119
120 let our_funds = get_funds(dbtx).await;
122
123 if our_funds < missing_input_amount {
124 return Err(format_err!("Insufficient funds"));
125 }
126
127 let updated = our_funds.saturating_sub(missing_input_amount);
128
129 dbtx.insert_entry(&DummyClientFundsKeyV1, &updated).await;
130
131 let input = ClientInput {
132 input: DummyInput {
133 amount: missing_input_amount,
134 account: self.key.public_key(),
135 },
136 amount: missing_input_amount,
137 keys: vec![self.key],
138 };
139 let input_sm = ClientInputSM {
140 state_machines: Arc::new(move |out_point_range| {
141 vec![DummyStateMachine::Input(
142 missing_input_amount,
143 out_point_range.txid(),
144 operation_id,
145 )]
146 }),
147 };
148
149 Ok((
150 ClientInputBundle::new(vec![input], vec![input_sm]),
151 ClientOutputBundle::new(vec![], vec![]),
152 ))
153 }
154 Ordering::Equal => Ok((
155 ClientInputBundle::new(vec![], vec![]),
156 ClientOutputBundle::new(vec![], vec![]),
157 )),
158 Ordering::Greater => {
159 let missing_output_amount = input_amount.saturating_sub(output_amount);
160 let output = ClientOutput {
161 output: DummyOutput {
162 amount: missing_output_amount,
163 account: self.key.public_key(),
164 },
165 amount: missing_output_amount,
166 };
167
168 let output_sm = ClientOutputSM {
169 state_machines: Arc::new(move |out_point_range| {
170 vec![DummyStateMachine::Output(
171 missing_output_amount,
172 out_point_range.txid(),
173 operation_id,
174 )]
175 }),
176 };
177
178 Ok((
179 ClientInputBundle::new(vec![], vec![]),
180 ClientOutputBundle::new(vec![output], vec![output_sm]),
181 ))
182 }
183 }
184 }
185
186 async fn await_primary_module_output(
187 &self,
188 operation_id: OperationId,
189 out_point: OutPoint,
190 ) -> anyhow::Result<()> {
191 let stream = self
192 .notifier
193 .subscribe(operation_id)
194 .await
195 .filter_map(|state| async move {
196 match state {
197 DummyStateMachine::OutputDone(_, txid, _) => {
198 if txid != out_point.txid {
199 return None;
200 }
201 Some(Ok(()))
202 }
203 DummyStateMachine::Refund(_) => Some(Err(anyhow::anyhow!(
204 "Error occurred processing the dummy transaction"
205 ))),
206 _ => None,
207 }
208 });
209
210 pin_mut!(stream);
211
212 stream.next_or_pending().await
213 }
214
215 async fn get_balance(&self, dbtc: &mut DatabaseTransaction<'_>) -> Amount {
216 get_funds(dbtc).await
217 }
218
219 async fn subscribe_balance_changes(&self) -> BoxStream<'static, ()> {
220 Box::pin(
221 self.notifier
222 .subscribe_all_operations()
223 .filter_map(|state| async move {
224 match state {
225 DummyStateMachine::OutputDone(_, _, _)
226 | DummyStateMachine::Input { .. }
227 | DummyStateMachine::Refund(_) => Some(()),
228 _ => None,
229 }
230 }),
231 )
232 }
233}
234
235impl DummyClientModule {
236 pub async fn print_using_account(
237 &self,
238 amount: Amount,
239 account_kp: Keypair,
240 ) -> anyhow::Result<(OperationId, OutPoint)> {
241 let op_id = OperationId(rand::random());
242
243 let input = ClientInput {
246 input: DummyInput {
247 amount,
248 account: account_kp.public_key(),
249 },
250 amount,
251 keys: vec![account_kp],
252 };
253
254 let tx = TransactionBuilder::new().with_inputs(
257 self.client_ctx
258 .make_client_inputs(ClientInputBundle::new_no_sm(vec![input])),
259 );
260 let meta_gen = |change_range: OutPointRange| OutPoint {
261 txid: change_range.txid(),
262 out_idx: 0,
263 };
264 let change_range = self
265 .client_ctx
266 .finalize_and_submit_transaction(op_id, KIND.as_str(), meta_gen, tx)
267 .await?;
268
269 self.client_ctx
271 .await_primary_module_outputs(op_id, change_range.into_iter().collect())
272 .await
273 .context("Waiting for the output of print_using_account")?;
274
275 Ok((
276 op_id,
277 change_range
278 .into_iter()
279 .next()
280 .expect("At least one output"),
281 ))
282 }
283
284 pub async fn print_money(&self, amount: Amount) -> anyhow::Result<(OperationId, OutPoint)> {
286 self.print_using_account(amount, fed_key_pair()).await
287 }
288
289 pub async fn print_liability(&self, amount: Amount) -> anyhow::Result<(OperationId, OutPoint)> {
292 self.print_using_account(amount, broken_fed_key_pair())
293 .await
294 }
295
296 pub async fn send_money(&self, account: PublicKey, amount: Amount) -> anyhow::Result<OutPoint> {
298 self.db.ensure_isolated().expect("must be isolated");
299
300 let op_id = OperationId(rand::random());
301
302 let output = ClientOutput {
304 output: DummyOutput { amount, account },
305 amount,
306 };
307
308 let tx = TransactionBuilder::new().with_outputs(
310 self.client_ctx
311 .make_client_outputs(ClientOutputBundle::new_no_sm(vec![output])),
312 );
313
314 let meta_gen = |change_range: OutPointRange| OutPoint {
315 txid: change_range.txid(),
316 out_idx: 0,
317 };
318 let change_range = self
319 .client_ctx
320 .finalize_and_submit_transaction(op_id, DummyCommonInit::KIND.as_str(), meta_gen, tx)
321 .await?;
322
323 let tx_subscription = self.client_ctx.transaction_updates(op_id).await;
324
325 tx_subscription
326 .await_tx_accepted(change_range.txid())
327 .await
328 .map_err(|e| anyhow!(e))?;
329
330 Ok(OutPoint {
331 txid: change_range.txid(),
332 out_idx: 0,
333 })
334 }
335
336 pub async fn receive_money(&self, outpoint: OutPoint) -> anyhow::Result<()> {
338 let mut dbtx = self.db.begin_transaction().await;
339
340 #[allow(deprecated)]
341 let outcome = self
342 .client_ctx
343 .global_api()
344 .request_current_consensus::<SerdeOutputOutcome>(
345 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
346 ApiRequestErased::new(outpoint),
347 )
348 .await?;
349
350 let outcome = deserialize_outcome::<DummyOutputOutcome>(&outcome, &self.decoder())?;
351
352 if outcome.1 != self.key.public_key() {
353 return Err(format_err!("Wrong account id"));
354 }
355
356 dbtx.insert_entry(&DummyClientFundsKeyV1, &outcome.0).await;
357 dbtx.commit_tx().await;
358
359 Ok(())
360 }
361
362 pub fn account(&self) -> PublicKey {
364 self.key.public_key()
365 }
366}
367
368async fn get_funds(dbtx: &mut DatabaseTransaction<'_>) -> Amount {
369 let funds = dbtx.get_value(&DummyClientFundsKeyV1).await;
370 funds.unwrap_or(Amount::ZERO)
371}
372
373#[derive(Debug, Clone)]
374pub struct DummyClientInit;
375
376impl 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 if let Some(funds) = dbtx.get_value(&DummyClientFundsKeyV1).await {
394 items.insert("Dummy Funds".to_string(), Box::new(funds));
395 }
396 }
397 DbKeyPrefix::ClientName => {
398 if let Some(name) = dbtx.get_value(&DummyClientNameKey).await {
399 items.insert("Dummy Name".to_string(), Box::new(name));
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(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
423 Ok(DummyClientModule {
424 cfg: args.cfg().clone(),
425 key: args
426 .module_root_secret()
427 .clone()
428 .to_secp_key(&Secp256k1::new()),
429
430 notifier: args.notifier().clone(),
431 client_ctx: args.context(),
432 db: args.db().clone(),
433 })
434 }
435
436 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientMigrationFn> {
437 let mut migrations: BTreeMap<DatabaseVersion, ClientMigrationFn> = BTreeMap::new();
438 migrations.insert(DatabaseVersion(0), |dbtx, _, _| {
439 Box::pin(migrate_to_v1(dbtx))
440 });
441
442 migrations.insert(DatabaseVersion(1), |_, active_states, inactive_states| {
443 Box::pin(async {
444 migrate_state(active_states, inactive_states, db::get_v1_migrated_state)
445 })
446 });
447
448 migrations
449 }
450}