Skip to main content

agent_first_pay/store/
mod.rs

1#[cfg(feature = "redb")]
2pub mod lock;
3pub mod wallet;
4
5#[cfg(feature = "redb")]
6pub mod db;
7#[cfg(feature = "redb")]
8pub mod redb_store;
9#[cfg(feature = "redb")]
10pub mod transaction;
11
12#[cfg(feature = "postgres")]
13pub mod postgres_store;
14
15use crate::provider::PayError;
16use crate::types::{HistoryRecord, Network};
17use serde::Serialize;
18use std::collections::BTreeMap;
19use std::path::PathBuf;
20use wallet::WalletMetadata;
21
22#[derive(Debug, Clone, Serialize)]
23pub struct MigrationLog {
24    pub database: String,
25    pub from_version: u64,
26    pub to_version: u64,
27}
28
29/// Trait abstracting wallet + transaction storage operations.
30#[allow(dead_code)]
31pub trait PayStore: Send + Sync {
32    // Wallet
33    fn save_wallet_metadata(&self, meta: &WalletMetadata) -> Result<(), PayError>;
34    fn load_wallet_metadata(&self, wallet_id: &str) -> Result<WalletMetadata, PayError>;
35    fn list_wallet_metadata(
36        &self,
37        network: Option<Network>,
38    ) -> Result<Vec<WalletMetadata>, PayError>;
39    fn delete_wallet_metadata(&self, wallet_id: &str) -> Result<(), PayError>;
40    fn wallet_directory_path(&self, wallet_id: &str) -> Result<PathBuf, PayError>;
41    fn wallet_data_directory_path(&self, wallet_id: &str) -> Result<PathBuf, PayError>;
42    fn wallet_data_directory_path_for_meta(&self, meta: &WalletMetadata) -> PathBuf;
43    fn resolve_wallet_id(&self, id_or_label: &str) -> Result<String, PayError>;
44
45    // Transaction
46    fn append_transaction_record(&self, record: &HistoryRecord) -> Result<(), PayError>;
47    fn load_wallet_transaction_records(
48        &self,
49        wallet_id: &str,
50    ) -> Result<Vec<HistoryRecord>, PayError>;
51    fn find_transaction_record_by_id(&self, tx_id: &str)
52        -> Result<Option<HistoryRecord>, PayError>;
53    fn update_transaction_record_memo(
54        &self,
55        tx_id: &str,
56        memo: Option<&BTreeMap<String, String>>,
57    ) -> Result<(), PayError>;
58    fn update_transaction_record_fee(
59        &self,
60        tx_id: &str,
61        fee_value: u64,
62        fee_unit: &str,
63    ) -> Result<(), PayError>;
64    fn update_transaction_record_status(
65        &self,
66        tx_id: &str,
67        status: crate::types::TxStatus,
68        confirmed_at_epoch_s: Option<u64>,
69    ) -> Result<(), PayError>;
70
71    // Migration log
72    fn drain_migration_log(&self) -> Vec<MigrationLog>;
73}
74
75/// Storage backend enum dispatching to the active variant.
76#[derive(Clone)]
77pub enum StorageBackend {
78    #[cfg(feature = "redb")]
79    Redb(redb_store::RedbStore),
80    #[cfg(feature = "postgres")]
81    Postgres(postgres_store::PostgresStore),
82    /// Uninhabited variant ensuring the enum is valid when no backend features
83    /// are enabled. Cannot be constructed at runtime.
84    #[doc(hidden)]
85    _None(std::convert::Infallible),
86}
87
88/// Dispatch a method call to the active storage backend variant.
89macro_rules! dispatch_storage {
90    ($self:expr, $method:ident $(, $arg:expr)*) => {
91        match $self {
92            #[cfg(feature = "redb")]
93            Self::Redb(s) => s.$method($($arg),*),
94            #[cfg(feature = "postgres")]
95            Self::Postgres(s) => s.$method($($arg),*),
96            Self::_None(n) => match *n {},
97        }
98    }
99}
100
101#[cfg_attr(
102    not(any(feature = "redb", feature = "postgres")),
103    allow(unused_variables)
104)]
105impl PayStore for StorageBackend {
106    fn save_wallet_metadata(&self, meta: &WalletMetadata) -> Result<(), PayError> {
107        dispatch_storage!(self, save_wallet_metadata, meta)
108    }
109
110    fn load_wallet_metadata(&self, wallet_id: &str) -> Result<WalletMetadata, PayError> {
111        dispatch_storage!(self, load_wallet_metadata, wallet_id)
112    }
113
114    fn list_wallet_metadata(
115        &self,
116        network: Option<Network>,
117    ) -> Result<Vec<WalletMetadata>, PayError> {
118        dispatch_storage!(self, list_wallet_metadata, network)
119    }
120
121    fn delete_wallet_metadata(&self, wallet_id: &str) -> Result<(), PayError> {
122        dispatch_storage!(self, delete_wallet_metadata, wallet_id)
123    }
124
125    fn wallet_directory_path(&self, wallet_id: &str) -> Result<PathBuf, PayError> {
126        dispatch_storage!(self, wallet_directory_path, wallet_id)
127    }
128
129    fn wallet_data_directory_path(&self, wallet_id: &str) -> Result<PathBuf, PayError> {
130        dispatch_storage!(self, wallet_data_directory_path, wallet_id)
131    }
132
133    fn wallet_data_directory_path_for_meta(&self, meta: &WalletMetadata) -> PathBuf {
134        dispatch_storage!(self, wallet_data_directory_path_for_meta, meta)
135    }
136
137    fn resolve_wallet_id(&self, id_or_label: &str) -> Result<String, PayError> {
138        dispatch_storage!(self, resolve_wallet_id, id_or_label)
139    }
140
141    fn append_transaction_record(&self, record: &HistoryRecord) -> Result<(), PayError> {
142        dispatch_storage!(self, append_transaction_record, record)
143    }
144
145    fn load_wallet_transaction_records(
146        &self,
147        wallet_id: &str,
148    ) -> Result<Vec<HistoryRecord>, PayError> {
149        dispatch_storage!(self, load_wallet_transaction_records, wallet_id)
150    }
151
152    fn find_transaction_record_by_id(
153        &self,
154        tx_id: &str,
155    ) -> Result<Option<HistoryRecord>, PayError> {
156        dispatch_storage!(self, find_transaction_record_by_id, tx_id)
157    }
158
159    fn update_transaction_record_memo(
160        &self,
161        tx_id: &str,
162        memo: Option<&BTreeMap<String, String>>,
163    ) -> Result<(), PayError> {
164        dispatch_storage!(self, update_transaction_record_memo, tx_id, memo)
165    }
166
167    fn update_transaction_record_fee(
168        &self,
169        tx_id: &str,
170        fee_value: u64,
171        fee_unit: &str,
172    ) -> Result<(), PayError> {
173        dispatch_storage!(
174            self,
175            update_transaction_record_fee,
176            tx_id,
177            fee_value,
178            fee_unit
179        )
180    }
181
182    fn update_transaction_record_status(
183        &self,
184        tx_id: &str,
185        status: crate::types::TxStatus,
186        confirmed_at_epoch_s: Option<u64>,
187    ) -> Result<(), PayError> {
188        dispatch_storage!(
189            self,
190            update_transaction_record_status,
191            tx_id,
192            status,
193            confirmed_at_epoch_s
194        )
195    }
196
197    fn drain_migration_log(&self) -> Vec<MigrationLog> {
198        dispatch_storage!(self, drain_migration_log)
199    }
200}
201
202/// Create a storage backend based on config and enabled features.
203/// Returns None if no storage backend is available (frontend-only mode).
204/// For postgres, performs async connection via `block_in_place`.
205pub fn create_storage_backend(config: &crate::types::RuntimeConfig) -> Option<StorageBackend> {
206    let requested = config.storage_backend.as_deref().unwrap_or("redb");
207
208    match requested {
209        #[cfg(feature = "redb")]
210        "redb" => Some(StorageBackend::Redb(redb_store::RedbStore::new(
211            &config.data_dir,
212        ))),
213        #[cfg(feature = "postgres")]
214        "postgres" => {
215            let url = config.postgres_url_secret.as_deref()?;
216            let data_dir = config.data_dir.clone();
217            tokio::task::block_in_place(|| {
218                tokio::runtime::Handle::current().block_on(async {
219                    match postgres_store::PostgresStore::connect(url, &data_dir).await {
220                        Ok(store) => Some(StorageBackend::Postgres(store)),
221                        Err(_) => None,
222                    }
223                })
224            })
225        }
226        _ => None,
227    }
228}
229
230/// Create a postgres storage backend asynchronously.
231#[cfg(feature = "postgres")]
232#[allow(dead_code)]
233pub async fn create_postgres_backend(
234    config: &crate::types::RuntimeConfig,
235) -> Result<StorageBackend, String> {
236    let url = config.postgres_url_secret.as_deref().ok_or_else(|| {
237        "postgres_url_secret is required when storage_backend = postgres".to_string()
238    })?;
239    let store = postgres_store::PostgresStore::connect(url, &config.data_dir)
240        .await
241        .map_err(|e| format!("postgres connection failed: {e}"))?;
242    Ok(StorageBackend::Postgres(store))
243}