lexe/unstable/
wallet_db.rs

1//! Lexe wallet database.
2
3use anyhow::Context;
4use lexe_node_client::client::NodeClient;
5use tracing::{debug, warn};
6
7use super::{
8    ffs::{DiskFs, fsext},
9    payments_db::{self, PaymentsDb},
10};
11use crate::{config::WalletUserDbConfig, types::command::PaymentSyncSummary};
12
13/// Persistent wallet database which can be used with [`LexeWallet`].
14///
15/// [`LexeWallet`]: crate::wallet::LexeWallet
16pub struct WalletDb<F> {
17    #[allow(dead_code)] // TODO(max): Remove once stable
18    user_db_config: WalletUserDbConfig,
19
20    payments_db: PaymentsDb<F>,
21    payment_sync_lock: tokio::sync::Mutex<()>,
22}
23
24// TODO(max): Rework Ffs so this impl can be generic across all Ffs impls.
25// The user should just be able to give us a Ffs impl set to some base path
26// which is the lexe_data_dir. From there we should be able to create sub-Ffs's
27// for the wallet env, user, payments db, etc. Probably instead of one level of
28// directory it should be prefix-based. As the Ffs is currently designed, end
29// users have to manually create all the subdivisions, all the way down to e.g.
30// `payments_db`, which is tedious and error-prone. Also, it might need to be
31// renamed, since it won't be flat anymore.
32impl WalletDb<DiskFs> {
33    /// Create a fresh [`WalletDb`], deleting any existing data for this user.
34    pub fn fresh(user_db_config: WalletUserDbConfig) -> anyhow::Result<Self> {
35        let payments_ffs =
36            DiskFs::create_clean_dir_all(user_db_config.payments_db_dir())
37                .context("Could not create payments ffs")?;
38
39        // Delete the old payments_db dir just in case it exists.
40        for old_dir in user_db_config.old_payment_db_dirs() {
41            match fsext::remove_dir_all_idempotent(&old_dir) {
42                Ok(true) => debug!("Deleted old payments_db dir: {old_dir:?}"),
43                Ok(false) => (),
44                Err(e) => warn!(?old_dir, "Couldn't delete old dir: {e:#}"),
45            }
46        }
47
48        Ok(Self {
49            user_db_config,
50            payments_db: PaymentsDb::empty(payments_ffs),
51            payment_sync_lock: tokio::sync::Mutex::new(()),
52        })
53    }
54
55    /// Load an existing [`WalletDb`]. Returns [`None`] if no local data exists.
56    pub fn load(
57        user_db_config: WalletUserDbConfig,
58    ) -> anyhow::Result<Option<Self>> {
59        if !user_db_config.user_db_dir().exists() {
60            return Ok(None);
61        }
62
63        let payments_ffs =
64            DiskFs::create_dir_all(user_db_config.payments_db_dir())
65                .context("Could not create payments ffs")?;
66
67        let payments_db = PaymentsDb::read(payments_ffs)
68            .context("Failed to load payments db")?;
69
70        // If the payments_db contains 0 payments, the user may have just
71        // upgraded to the latest format. Delete the old dirs just in case.
72        let num_payments = payments_db.num_payments();
73        if num_payments == 0 {
74            for old_dir in user_db_config.old_payment_db_dirs() {
75                match fsext::remove_dir_all_idempotent(&old_dir) {
76                    Ok(true) =>
77                        debug!("Deleted old payments_db dir: {old_dir:?}"),
78                    Ok(false) => (),
79                    Err(e) => warn!(?old_dir, "Couldn't delete old dir: {e:#}"),
80                }
81            }
82        }
83
84        // Try to delete old provision_db since provision history is now on the
85        // backend.
86        let old_provision_db_dir = user_db_config.old_provision_db_dir();
87        match fsext::remove_dir_all_idempotent(&old_provision_db_dir) {
88            Ok(true) =>
89                debug!("Deleted old provision_db dir: {old_provision_db_dir:?}"),
90            Ok(false) => (),
91            Err(e) =>
92                warn!(?old_provision_db_dir, "Couldn't delete old dir: {e:#}"),
93        }
94
95        let num_pending = payments_db.num_pending();
96        let latest_updated_index = payments_db.latest_updated_index();
97        let last_synced_at = payments_db.last_synced_at();
98        debug!(
99            %num_payments, %num_pending, ?latest_updated_index, ?last_synced_at,
100            "Loaded WalletDb."
101        );
102
103        Ok(Some(Self {
104            user_db_config,
105            payments_db,
106            payment_sync_lock: tokio::sync::Mutex::new(()),
107        }))
108    }
109
110    /// Load an existing [`WalletDb`], or create a fresh one if none exists.
111    pub fn load_or_fresh(
112        user_db_config: WalletUserDbConfig,
113    ) -> anyhow::Result<Self> {
114        let maybe_db = Self::load(user_db_config.clone())
115            .context("Failed to load wallet db")?;
116
117        let db = match maybe_db {
118            Some(d) => d,
119            None => Self::fresh(user_db_config)
120                .context("Failed to create fresh wallet db")?,
121        };
122
123        Ok(db)
124    }
125
126    /// Get the user database configuration.
127    #[allow(dead_code)] // TODO(max): Remove once stable
128    pub fn user_db_config(&self) -> &WalletUserDbConfig {
129        &self.user_db_config
130    }
131
132    /// Get a reference to the payments database.
133    pub fn payments_db(&self) -> &PaymentsDb<DiskFs> {
134        &self.payments_db
135    }
136
137    /// Sync payments from the node to the local payments database.
138    ///
139    /// If another sync is already in progress, waits for it to complete
140    /// before starting a new one.
141    pub async fn sync_payments(
142        &self,
143        node_client: &NodeClient,
144        batch_size: u16,
145    ) -> anyhow::Result<PaymentSyncSummary> {
146        let _lock = self.payment_sync_lock.lock().await;
147        payments_db::sync_payments(&self.payments_db, node_client, batch_size)
148            .await
149    }
150}