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#[allow(dead_code)]
31pub trait PayStore: Send + Sync {
32 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 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 fn drain_migration_log(&self) -> Vec<MigrationLog>;
73}
74
75#[derive(Clone)]
77pub enum StorageBackend {
78 #[cfg(feature = "redb")]
79 Redb(redb_store::RedbStore),
80 #[cfg(feature = "postgres")]
81 Postgres(postgres_store::PostgresStore),
82 #[doc(hidden)]
85 _None(std::convert::Infallible),
86}
87
88macro_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
202pub 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#[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}