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
101impl PayStore for StorageBackend {
102    fn save_wallet_metadata(&self, meta: &WalletMetadata) -> Result<(), PayError> {
103        dispatch_storage!(self, save_wallet_metadata, meta)
104    }
105
106    fn load_wallet_metadata(&self, wallet_id: &str) -> Result<WalletMetadata, PayError> {
107        dispatch_storage!(self, load_wallet_metadata, wallet_id)
108    }
109
110    fn list_wallet_metadata(
111        &self,
112        network: Option<Network>,
113    ) -> Result<Vec<WalletMetadata>, PayError> {
114        dispatch_storage!(self, list_wallet_metadata, network)
115    }
116
117    fn delete_wallet_metadata(&self, wallet_id: &str) -> Result<(), PayError> {
118        dispatch_storage!(self, delete_wallet_metadata, wallet_id)
119    }
120
121    fn wallet_directory_path(&self, wallet_id: &str) -> Result<PathBuf, PayError> {
122        dispatch_storage!(self, wallet_directory_path, wallet_id)
123    }
124
125    fn wallet_data_directory_path(&self, wallet_id: &str) -> Result<PathBuf, PayError> {
126        dispatch_storage!(self, wallet_data_directory_path, wallet_id)
127    }
128
129    fn wallet_data_directory_path_for_meta(&self, meta: &WalletMetadata) -> PathBuf {
130        dispatch_storage!(self, wallet_data_directory_path_for_meta, meta)
131    }
132
133    fn resolve_wallet_id(&self, id_or_label: &str) -> Result<String, PayError> {
134        dispatch_storage!(self, resolve_wallet_id, id_or_label)
135    }
136
137    fn append_transaction_record(&self, record: &HistoryRecord) -> Result<(), PayError> {
138        dispatch_storage!(self, append_transaction_record, record)
139    }
140
141    fn load_wallet_transaction_records(
142        &self,
143        wallet_id: &str,
144    ) -> Result<Vec<HistoryRecord>, PayError> {
145        dispatch_storage!(self, load_wallet_transaction_records, wallet_id)
146    }
147
148    fn find_transaction_record_by_id(
149        &self,
150        tx_id: &str,
151    ) -> Result<Option<HistoryRecord>, PayError> {
152        dispatch_storage!(self, find_transaction_record_by_id, tx_id)
153    }
154
155    fn update_transaction_record_memo(
156        &self,
157        tx_id: &str,
158        memo: Option<&BTreeMap<String, String>>,
159    ) -> Result<(), PayError> {
160        dispatch_storage!(self, update_transaction_record_memo, tx_id, memo)
161    }
162
163    fn update_transaction_record_fee(
164        &self,
165        tx_id: &str,
166        fee_value: u64,
167        fee_unit: &str,
168    ) -> Result<(), PayError> {
169        dispatch_storage!(
170            self,
171            update_transaction_record_fee,
172            tx_id,
173            fee_value,
174            fee_unit
175        )
176    }
177
178    fn update_transaction_record_status(
179        &self,
180        tx_id: &str,
181        status: crate::types::TxStatus,
182        confirmed_at_epoch_s: Option<u64>,
183    ) -> Result<(), PayError> {
184        dispatch_storage!(
185            self,
186            update_transaction_record_status,
187            tx_id,
188            status,
189            confirmed_at_epoch_s
190        )
191    }
192
193    fn drain_migration_log(&self) -> Vec<MigrationLog> {
194        dispatch_storage!(self, drain_migration_log)
195    }
196}
197
198/// Create a storage backend based on config and enabled features.
199/// Returns None if no storage backend is available (frontend-only mode).
200/// For postgres, performs async connection via `block_in_place`.
201pub fn create_storage_backend(config: &crate::types::RuntimeConfig) -> Option<StorageBackend> {
202    let requested = config.storage_backend.as_deref().unwrap_or("redb");
203
204    match requested {
205        #[cfg(feature = "redb")]
206        "redb" => Some(StorageBackend::Redb(redb_store::RedbStore::new(
207            &config.data_dir,
208        ))),
209        #[cfg(feature = "postgres")]
210        "postgres" => {
211            let url = config.postgres_url_secret.as_deref()?;
212            let data_dir = config.data_dir.clone();
213            tokio::task::block_in_place(|| {
214                tokio::runtime::Handle::current().block_on(async {
215                    match postgres_store::PostgresStore::connect(url, &data_dir).await {
216                        Ok(store) => Some(StorageBackend::Postgres(store)),
217                        Err(_) => None,
218                    }
219                })
220            })
221        }
222        _ => None,
223    }
224}
225
226/// Create a postgres storage backend asynchronously.
227#[cfg(feature = "postgres")]
228#[allow(dead_code)]
229pub async fn create_postgres_backend(
230    config: &crate::types::RuntimeConfig,
231) -> Result<StorageBackend, String> {
232    let url = config.postgres_url_secret.as_deref().ok_or_else(|| {
233        "postgres_url_secret is required when storage_backend = postgres".to_string()
234    })?;
235    let store = postgres_store::PostgresStore::connect(url, &config.data_dir)
236        .await
237        .map_err(|e| format!("postgres connection failed: {e}"))?;
238    Ok(StorageBackend::Postgres(store))
239}