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
65    // Migration log
66    fn drain_migration_log(&self) -> Vec<MigrationLog>;
67}
68
69/// Storage backend enum dispatching to the active variant.
70#[derive(Clone)]
71pub enum StorageBackend {
72    #[cfg(feature = "redb")]
73    Redb(redb_store::RedbStore),
74    #[cfg(feature = "postgres")]
75    Postgres(postgres_store::PostgresStore),
76    /// Uninhabited variant ensuring the enum is valid when no backend features
77    /// are enabled. Cannot be constructed at runtime.
78    #[doc(hidden)]
79    _None(std::convert::Infallible),
80}
81
82impl PayStore for StorageBackend {
83    fn save_wallet_metadata(&self, meta: &WalletMetadata) -> Result<(), PayError> {
84        match self {
85            #[cfg(feature = "redb")]
86            Self::Redb(s) => s.save_wallet_metadata(meta),
87            #[cfg(feature = "postgres")]
88            Self::Postgres(s) => s.save_wallet_metadata(meta),
89            Self::_None(n) => match *n {},
90        }
91    }
92
93    fn load_wallet_metadata(&self, wallet_id: &str) -> Result<WalletMetadata, PayError> {
94        match self {
95            #[cfg(feature = "redb")]
96            Self::Redb(s) => s.load_wallet_metadata(wallet_id),
97            #[cfg(feature = "postgres")]
98            Self::Postgres(s) => s.load_wallet_metadata(wallet_id),
99            Self::_None(n) => match *n {},
100        }
101    }
102
103    fn list_wallet_metadata(
104        &self,
105        network: Option<Network>,
106    ) -> Result<Vec<WalletMetadata>, PayError> {
107        match self {
108            #[cfg(feature = "redb")]
109            Self::Redb(s) => s.list_wallet_metadata(network),
110            #[cfg(feature = "postgres")]
111            Self::Postgres(s) => s.list_wallet_metadata(network),
112            Self::_None(n) => match *n {},
113        }
114    }
115
116    fn delete_wallet_metadata(&self, wallet_id: &str) -> Result<(), PayError> {
117        match self {
118            #[cfg(feature = "redb")]
119            Self::Redb(s) => s.delete_wallet_metadata(wallet_id),
120            #[cfg(feature = "postgres")]
121            Self::Postgres(s) => s.delete_wallet_metadata(wallet_id),
122            Self::_None(n) => match *n {},
123        }
124    }
125
126    fn wallet_directory_path(&self, wallet_id: &str) -> Result<PathBuf, PayError> {
127        match self {
128            #[cfg(feature = "redb")]
129            Self::Redb(s) => s.wallet_directory_path(wallet_id),
130            #[cfg(feature = "postgres")]
131            Self::Postgres(s) => s.wallet_directory_path(wallet_id),
132            Self::_None(n) => match *n {},
133        }
134    }
135
136    fn wallet_data_directory_path(&self, wallet_id: &str) -> Result<PathBuf, PayError> {
137        match self {
138            #[cfg(feature = "redb")]
139            Self::Redb(s) => s.wallet_data_directory_path(wallet_id),
140            #[cfg(feature = "postgres")]
141            Self::Postgres(s) => s.wallet_data_directory_path(wallet_id),
142            Self::_None(n) => match *n {},
143        }
144    }
145
146    fn wallet_data_directory_path_for_meta(&self, meta: &WalletMetadata) -> PathBuf {
147        match self {
148            #[cfg(feature = "redb")]
149            Self::Redb(s) => s.wallet_data_directory_path_for_meta(meta),
150            #[cfg(feature = "postgres")]
151            Self::Postgres(s) => s.wallet_data_directory_path_for_meta(meta),
152            Self::_None(n) => match *n {},
153        }
154    }
155
156    fn resolve_wallet_id(&self, id_or_label: &str) -> Result<String, PayError> {
157        match self {
158            #[cfg(feature = "redb")]
159            Self::Redb(s) => s.resolve_wallet_id(id_or_label),
160            #[cfg(feature = "postgres")]
161            Self::Postgres(s) => s.resolve_wallet_id(id_or_label),
162            Self::_None(n) => match *n {},
163        }
164    }
165
166    fn append_transaction_record(&self, record: &HistoryRecord) -> Result<(), PayError> {
167        match self {
168            #[cfg(feature = "redb")]
169            Self::Redb(s) => s.append_transaction_record(record),
170            #[cfg(feature = "postgres")]
171            Self::Postgres(s) => s.append_transaction_record(record),
172            Self::_None(n) => match *n {},
173        }
174    }
175
176    fn load_wallet_transaction_records(
177        &self,
178        wallet_id: &str,
179    ) -> Result<Vec<HistoryRecord>, PayError> {
180        match self {
181            #[cfg(feature = "redb")]
182            Self::Redb(s) => s.load_wallet_transaction_records(wallet_id),
183            #[cfg(feature = "postgres")]
184            Self::Postgres(s) => s.load_wallet_transaction_records(wallet_id),
185            Self::_None(n) => match *n {},
186        }
187    }
188
189    fn find_transaction_record_by_id(
190        &self,
191        tx_id: &str,
192    ) -> Result<Option<HistoryRecord>, PayError> {
193        match self {
194            #[cfg(feature = "redb")]
195            Self::Redb(s) => s.find_transaction_record_by_id(tx_id),
196            #[cfg(feature = "postgres")]
197            Self::Postgres(s) => s.find_transaction_record_by_id(tx_id),
198            Self::_None(n) => match *n {},
199        }
200    }
201
202    fn update_transaction_record_memo(
203        &self,
204        tx_id: &str,
205        memo: Option<&BTreeMap<String, String>>,
206    ) -> Result<(), PayError> {
207        match self {
208            #[cfg(feature = "redb")]
209            Self::Redb(s) => s.update_transaction_record_memo(tx_id, memo),
210            #[cfg(feature = "postgres")]
211            Self::Postgres(s) => s.update_transaction_record_memo(tx_id, memo),
212            Self::_None(n) => match *n {},
213        }
214    }
215
216    fn update_transaction_record_fee(
217        &self,
218        tx_id: &str,
219        fee_value: u64,
220        fee_unit: &str,
221    ) -> Result<(), PayError> {
222        match self {
223            #[cfg(feature = "redb")]
224            Self::Redb(s) => s.update_transaction_record_fee(tx_id, fee_value, fee_unit),
225            #[cfg(feature = "postgres")]
226            Self::Postgres(s) => s.update_transaction_record_fee(tx_id, fee_value, fee_unit),
227            Self::_None(n) => match *n {},
228        }
229    }
230
231    fn drain_migration_log(&self) -> Vec<MigrationLog> {
232        match self {
233            #[cfg(feature = "redb")]
234            Self::Redb(s) => s.drain_migration_log(),
235            #[cfg(feature = "postgres")]
236            Self::Postgres(s) => s.drain_migration_log(),
237            Self::_None(n) => match *n {},
238        }
239    }
240}
241
242/// Create a storage backend based on config and enabled features.
243/// Returns None if no storage backend is available (frontend-only mode).
244/// For postgres, performs async connection via `block_in_place`.
245pub fn create_storage_backend(config: &crate::types::RuntimeConfig) -> Option<StorageBackend> {
246    let requested = config.storage_backend.as_deref().unwrap_or("redb");
247
248    match requested {
249        #[cfg(feature = "redb")]
250        "redb" => Some(StorageBackend::Redb(redb_store::RedbStore::new(
251            &config.data_dir,
252        ))),
253        #[cfg(feature = "postgres")]
254        "postgres" => {
255            let url = config.postgres_url_secret.as_deref()?;
256            let data_dir = config.data_dir.clone();
257            tokio::task::block_in_place(|| {
258                tokio::runtime::Handle::current().block_on(async {
259                    match postgres_store::PostgresStore::connect(url, &data_dir).await {
260                        Ok(store) => Some(StorageBackend::Postgres(store)),
261                        Err(_) => None,
262                    }
263                })
264            })
265        }
266        _ => None,
267    }
268}
269
270/// Create a postgres storage backend asynchronously.
271#[cfg(feature = "postgres")]
272#[allow(dead_code)]
273pub async fn create_postgres_backend(
274    config: &crate::types::RuntimeConfig,
275) -> Result<StorageBackend, String> {
276    let url = config.postgres_url_secret.as_deref().ok_or_else(|| {
277        "postgres_url_secret is required when storage_backend = postgres".to_string()
278    })?;
279    let store = postgres_store::PostgresStore::connect(url, &config.data_dir)
280        .await
281        .map_err(|e| format!("postgres connection failed: {e}"))?;
282    Ok(StorageBackend::Postgres(store))
283}