fedimint_dummy_client/
lib.rs

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/// Data needed by the state machine
58#[derive(Debug, Clone)]
59pub struct DummyClientContext {
60    pub dummy_decoder: Decoder,
61}
62
63// TODO: Boiler-plate
64impl 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                // Check and subtract from our funds
119                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        // TODO: Building a tx could be easier
242        // Create input using the fed's account
243        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        // Build and send tx to the fed
253        // Will output to our primary client module
254        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        // Wait for the output of the primary module
265        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    /// Request the federation prints money for us
274    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    /// Use a broken printer to print a liability instead of money
279    /// If the federation is honest, should always fail
280    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    /// Send money to another user
286    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        // Create output using another account
292        let output = ClientOutput {
293            output: DummyOutput { amount, account },
294            amount,
295        };
296
297        // Build and send tx to the fed
298        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    /// Wait to receive money at an outpoint
320    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    /// Return our account
339    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
352// TODO: Boilerplate-code
353impl 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/// Generates the client module
386#[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}