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;
10use std::time::Duration;
11
12use anyhow::{anyhow, format_err, Context as _};
13use common::broken_fed_key_pair;
14use db::{migrate_to_v1, DbKeyPrefix, DummyClientFundsKeyV1, DummyClientNameKey};
15use fedimint_client::db::{migrate_state, ClientMigrationFn};
16use fedimint_client::module::init::{ClientModuleInit, ClientModuleInitArgs};
17use fedimint_client::module::recovery::NoModuleBackup;
18use fedimint_client::module::{ClientContext, ClientModule, IClientModule};
19use fedimint_client::sm::{Context, ModuleNotifier};
20use fedimint_client::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};
28use fedimint_core::module::{
29 ApiVersion, CommonModuleInit, ModuleCommon, ModuleInit, MultiApiVersion,
30};
31use fedimint_core::secp256k1::{Keypair, PublicKey, Secp256k1};
32use fedimint_core::util::{BoxStream, NextOrPending};
33use fedimint_core::{apply, async_trait_maybe_send, Amount, OutPoint};
34pub use fedimint_dummy_common as common;
35use fedimint_dummy_common::config::DummyClientConfig;
36use fedimint_dummy_common::{
37 fed_key_pair, DummyCommonInit, DummyInput, DummyModuleTypes, DummyOutput, DummyOutputOutcome,
38 KIND,
39};
40use futures::{pin_mut, StreamExt};
41use states::DummyStateMachine;
42use strum::IntoEnumIterator;
43
44pub mod api;
45pub mod db;
46pub mod states;
47
48#[derive(Debug)]
49pub struct DummyClientModule {
50 cfg: DummyClientConfig,
51 key: Keypair,
52 notifier: ModuleNotifier<DummyStateMachine>,
53 client_ctx: ClientContext<Self>,
54 db: Database,
55}
56
57#[derive(Debug, Clone)]
59pub struct DummyClientContext {
60 pub dummy_decoder: Decoder,
61}
62
63impl Context for DummyClientContext {
65 const KIND: Option<ModuleKind> = None;
66}
67
68#[apply(async_trait_maybe_send!)]
69impl ClientModule for DummyClientModule {
70 type Init = DummyClientInit;
71 type Common = DummyModuleTypes;
72 type Backup = NoModuleBackup;
73 type ModuleStateMachineContext = DummyClientContext;
74 type States = DummyStateMachine;
75
76 fn context(&self) -> Self::ModuleStateMachineContext {
77 DummyClientContext {
78 dummy_decoder: self.decoder(),
79 }
80 }
81
82 fn input_fee(
83 &self,
84 _amount: Amount,
85 _input: &<Self::Common as ModuleCommon>::Input,
86 ) -> Option<Amount> {
87 Some(self.cfg.tx_fee)
88 }
89
90 fn output_fee(
91 &self,
92 _amount: Amount,
93 _output: &<Self::Common as ModuleCommon>::Output,
94 ) -> Option<Amount> {
95 Some(self.cfg.tx_fee)
96 }
97
98 fn supports_being_primary(&self) -> bool {
99 true
100 }
101
102 async fn create_final_inputs_and_outputs(
103 &self,
104 dbtx: &mut DatabaseTransaction<'_>,
105 operation_id: OperationId,
106 input_amount: Amount,
107 output_amount: Amount,
108 ) -> anyhow::Result<(
109 ClientInputBundle<DummyInput, DummyStateMachine>,
110 ClientOutputBundle<DummyOutput, DummyStateMachine>,
111 )> {
112 dbtx.ensure_isolated().expect("must be isolated");
113
114 match input_amount.cmp(&output_amount) {
115 Ordering::Less => {
116 let missing_input_amount = output_amount - input_amount;
117
118 let our_funds = get_funds(dbtx).await;
120
121 if our_funds < missing_input_amount {
122 return Err(format_err!("Insufficient funds"));
123 }
124
125 let updated = our_funds - missing_input_amount;
126
127 dbtx.insert_entry(&DummyClientFundsKeyV1, &updated).await;
128
129 let input = ClientInput {
130 input: DummyInput {
131 amount: missing_input_amount,
132 account: self.key.public_key(),
133 },
134 amount: missing_input_amount,
135 keys: vec![self.key],
136 };
137 let input_sm = ClientInputSM {
138 state_machines: Arc::new(move |out_point_range| {
139 vec![DummyStateMachine::Input(
140 missing_input_amount,
141 out_point_range.txid(),
142 operation_id,
143 )]
144 }),
145 };
146
147 Ok((
148 ClientInputBundle::new(vec![input], vec![input_sm]),
149 ClientOutputBundle::new(vec![], vec![]),
150 ))
151 }
152 Ordering::Equal => Ok((
153 ClientInputBundle::new(vec![], vec![]),
154 ClientOutputBundle::new(vec![], vec![]),
155 )),
156 Ordering::Greater => {
157 let missing_output_amount = input_amount - output_amount;
158 let output = ClientOutput {
159 output: DummyOutput {
160 amount: missing_output_amount,
161 account: self.key.public_key(),
162 },
163 amount: missing_output_amount,
164 };
165
166 let output_sm = ClientOutputSM {
167 state_machines: Arc::new(move |out_point_range| {
168 vec![DummyStateMachine::Output(
169 missing_output_amount,
170 out_point_range.txid(),
171 operation_id,
172 )]
173 }),
174 };
175
176 Ok((
177 ClientInputBundle::new(vec![], vec![]),
178 ClientOutputBundle::new(vec![output], vec![output_sm]),
179 ))
180 }
181 }
182 }
183
184 async fn await_primary_module_output(
185 &self,
186 operation_id: OperationId,
187 out_point: OutPoint,
188 ) -> anyhow::Result<()> {
189 let stream = self
190 .notifier
191 .subscribe(operation_id)
192 .await
193 .filter_map(|state| async move {
194 match state {
195 DummyStateMachine::OutputDone(_, txid, _) => {
196 if txid != out_point.txid {
197 return None;
198 }
199 Some(Ok(()))
200 }
201 DummyStateMachine::Refund(_) => Some(Err(anyhow::anyhow!(
202 "Error occurred processing the dummy transaction"
203 ))),
204 _ => None,
205 }
206 });
207
208 pin_mut!(stream);
209
210 stream.next_or_pending().await
211 }
212
213 async fn get_balance(&self, dbtc: &mut DatabaseTransaction<'_>) -> Amount {
214 get_funds(dbtc).await
215 }
216
217 async fn subscribe_balance_changes(&self) -> BoxStream<'static, ()> {
218 Box::pin(
219 self.notifier
220 .subscribe_all_operations()
221 .filter_map(|state| async move {
222 match state {
223 DummyStateMachine::OutputDone(_, _, _)
224 | DummyStateMachine::Input { .. }
225 | DummyStateMachine::Refund(_) => Some(()),
226 _ => None,
227 }
228 }),
229 )
230 }
231}
232
233impl DummyClientModule {
234 pub async fn print_using_account(
235 &self,
236 amount: Amount,
237 account_kp: Keypair,
238 ) -> anyhow::Result<(OperationId, OutPoint)> {
239 let op_id = OperationId(rand::random());
240
241 let input = ClientInput {
244 input: DummyInput {
245 amount,
246 account: account_kp.public_key(),
247 },
248 amount,
249 keys: vec![account_kp],
250 };
251
252 let tx = TransactionBuilder::new().with_inputs(
255 self.client_ctx
256 .make_client_inputs(ClientInputBundle::new_no_sm(vec![input])),
257 );
258 let outpoint = |txid, _| OutPoint { txid, out_idx: 0 };
259 let (_, change) = self
260 .client_ctx
261 .finalize_and_submit_transaction(op_id, KIND.as_str(), outpoint, tx)
262 .await?;
263
264 self.client_ctx
266 .await_primary_module_outputs(op_id, change.clone())
267 .await
268 .context("Waiting for the output of print_using_account")?;
269
270 Ok((op_id, change[0]))
271 }
272
273 pub async fn print_money(&self, amount: Amount) -> anyhow::Result<(OperationId, OutPoint)> {
275 self.print_using_account(amount, fed_key_pair()).await
276 }
277
278 pub async fn print_liability(&self, amount: Amount) -> anyhow::Result<(OperationId, OutPoint)> {
281 self.print_using_account(amount, broken_fed_key_pair())
282 .await
283 }
284
285 pub async fn send_money(&self, account: PublicKey, amount: Amount) -> anyhow::Result<OutPoint> {
287 self.db.ensure_isolated().expect("must be isolated");
288
289 let op_id = OperationId(rand::random());
290
291 let output = ClientOutput {
293 output: DummyOutput { amount, account },
294 amount,
295 };
296
297 let tx = TransactionBuilder::new().with_outputs(
299 self.client_ctx
300 .make_client_outputs(ClientOutputBundle::new_no_sm(vec![output])),
301 );
302
303 let outpoint = |txid, _| OutPoint { txid, out_idx: 0 };
304 let (txid, _) = self
305 .client_ctx
306 .finalize_and_submit_transaction(op_id, DummyCommonInit::KIND.as_str(), outpoint, tx)
307 .await?;
308
309 let tx_subscription = self.client_ctx.transaction_updates(op_id).await;
310
311 tx_subscription
312 .await_tx_accepted(txid)
313 .await
314 .map_err(|e| anyhow!(e))?;
315
316 Ok(OutPoint { txid, out_idx: 0 })
317 }
318
319 pub async fn receive_money(&self, outpoint: OutPoint) -> anyhow::Result<()> {
321 let mut dbtx = self.db.begin_transaction().await;
322 let DummyOutputOutcome(new_balance, account) = self
323 .client_ctx
324 .global_api()
325 .await_output_outcome(outpoint, Duration::from_secs(10), &self.decoder())
326 .await?;
327
328 if account != self.key.public_key() {
329 return Err(format_err!("Wrong account id"));
330 }
331
332 dbtx.insert_entry(&DummyClientFundsKeyV1, &new_balance)
333 .await;
334 dbtx.commit_tx().await;
335 Ok(())
336 }
337
338 pub fn account(&self) -> PublicKey {
340 self.key.public_key()
341 }
342}
343
344async fn get_funds(dbtx: &mut DatabaseTransaction<'_>) -> Amount {
345 let funds = dbtx.get_value(&DummyClientFundsKeyV1).await;
346 funds.unwrap_or(Amount::ZERO)
347}
348
349#[derive(Debug, Clone)]
350pub struct DummyClientInit;
351
352impl ModuleInit for DummyClientInit {
354 type Common = DummyCommonInit;
355
356 async fn dump_database(
357 &self,
358 dbtx: &mut DatabaseTransaction<'_>,
359 prefix_names: Vec<String>,
360 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
361 let mut items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
362 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
363 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
364 });
365
366 for table in filtered_prefixes {
367 match table {
368 DbKeyPrefix::ClientFunds => {
369 if let Some(funds) = dbtx.get_value(&DummyClientFundsKeyV1).await {
370 items.insert("Dummy Funds".to_string(), Box::new(funds));
371 }
372 }
373 DbKeyPrefix::ClientName => {
374 if let Some(name) = dbtx.get_value(&DummyClientNameKey).await {
375 items.insert("Dummy Name".to_string(), Box::new(name));
376 }
377 }
378 }
379 }
380
381 Box::new(items.into_iter())
382 }
383}
384
385#[apply(async_trait_maybe_send!)]
387impl ClientModuleInit for DummyClientInit {
388 type Module = DummyClientModule;
389
390 fn supported_api_versions(&self) -> MultiApiVersion {
391 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
392 .expect("no version conflicts")
393 }
394
395 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
396 Ok(DummyClientModule {
397 cfg: args.cfg().clone(),
398 key: args
399 .module_root_secret()
400 .clone()
401 .to_secp_key(&Secp256k1::new()),
402
403 notifier: args.notifier().clone(),
404 client_ctx: args.context(),
405 db: args.db().clone(),
406 })
407 }
408
409 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientMigrationFn> {
410 let mut migrations: BTreeMap<DatabaseVersion, ClientMigrationFn> = BTreeMap::new();
411 migrations.insert(DatabaseVersion(0), |dbtx, _, _| {
412 Box::pin(migrate_to_v1(dbtx))
413 });
414
415 migrations.insert(DatabaseVersion(1), |_, active_states, inactive_states| {
416 Box::pin(async {
417 migrate_state(active_states, inactive_states, db::get_v1_migrated_state)
418 })
419 });
420
421 migrations
422 }
423}