1#[cfg(any(
2 feature = "btc-esplora",
3 feature = "btc-core",
4 feature = "btc-electrum"
5))]
6pub mod btc;
7#[cfg(feature = "cashu")]
8pub mod cashu;
9#[cfg(feature = "evm")]
10pub mod evm;
11#[cfg(any(feature = "ln-nwc", feature = "ln-phoenixd", feature = "ln-lnbits"))]
12pub mod ln;
13pub mod remote;
14#[cfg(feature = "sol")]
15pub mod sol;
16
17use crate::types::*;
18use async_trait::async_trait;
19use std::fmt;
20
21#[derive(Debug)]
26#[allow(dead_code)]
27pub enum PayError {
28 NotImplemented(String),
29 WalletNotFound(String),
30 InvalidAmount(String),
31 NetworkError(String),
32 InternalError(String),
33 LimitExceeded {
34 rule_id: String,
35 scope: SpendScope,
36 scope_key: String,
37 spent: u64,
38 max_spend: u64,
39 token: Option<String>,
40 remaining_s: u64,
41 origin: Option<String>,
43 },
44}
45
46impl fmt::Display for PayError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Self::NotImplemented(msg) => write!(f, "{msg}"),
50 Self::WalletNotFound(msg) => write!(f, "{msg}"),
51 Self::InvalidAmount(msg) => write!(f, "{msg}"),
52 Self::NetworkError(msg) => write!(f, "{msg}"),
53 Self::InternalError(msg) => write!(f, "{msg}"),
54 Self::LimitExceeded {
55 scope,
56 scope_key,
57 spent,
58 max_spend,
59 token,
60 origin,
61 ..
62 } => {
63 let token_str = token.as_deref().unwrap_or("base-units");
64 if let Some(node) = origin {
65 write!(
66 f,
67 "spend limit exceeded at {node} ({scope:?}:{scope_key}): spent {spent} of {max_spend} {token_str}"
68 )
69 } else {
70 write!(
71 f,
72 "spend limit exceeded ({scope:?}:{scope_key}): spent {spent} of {max_spend} {token_str}"
73 )
74 }
75 }
76 }
77 }
78}
79
80impl PayError {
81 pub fn error_code(&self) -> &'static str {
82 match self {
83 Self::NotImplemented(_) => "not_implemented",
84 Self::WalletNotFound(_) => "wallet_not_found",
85 Self::InvalidAmount(_) => "invalid_amount",
86 Self::NetworkError(_) => "network_error",
87 Self::InternalError(_) => "internal_error",
88 Self::LimitExceeded { .. } => "limit_exceeded",
89 }
90 }
91
92 pub fn retryable(&self) -> bool {
93 matches!(self, Self::NetworkError(_))
94 }
95
96 pub fn hint(&self) -> Option<String> {
97 match self {
98 Self::NotImplemented(_) => Some("enable redb or postgres storage backend".to_string()),
99 Self::WalletNotFound(_) => Some("list wallets with: afpay wallet list".to_string()),
100 Self::LimitExceeded { .. } => Some("check limits with: afpay limit list".to_string()),
101 _ => None,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, Default)]
111pub struct HistorySyncStats {
112 pub records_scanned: usize,
113 pub records_added: usize,
114 pub records_updated: usize,
115}
116
117#[async_trait]
118pub trait PayProvider: Send + Sync {
119 #[allow(dead_code)]
120 fn network(&self) -> Network;
121
122 fn writes_locally(&self) -> bool {
124 false
125 }
126
127 async fn ping(&self) -> Result<(), PayError> {
129 Ok(())
130 }
131
132 async fn create_wallet(&self, request: &WalletCreateRequest) -> Result<WalletInfo, PayError>;
133 async fn create_ln_wallet(
134 &self,
135 _request: LnWalletCreateRequest,
136 ) -> Result<WalletInfo, PayError> {
137 Err(PayError::NotImplemented(
138 "ln wallet creation not supported".to_string(),
139 ))
140 }
141 async fn close_wallet(&self, wallet: &str) -> Result<(), PayError>;
142 async fn list_wallets(&self) -> Result<Vec<WalletSummary>, PayError>;
143 async fn balance(&self, wallet: &str) -> Result<BalanceInfo, PayError>;
144 async fn check_balance(&self, _wallet: &str) -> Result<BalanceInfo, PayError> {
145 Err(PayError::NotImplemented(
146 "check_balance not supported".to_string(),
147 ))
148 }
149 async fn restore(&self, _wallet: &str) -> Result<RestoreResult, PayError> {
150 Err(PayError::NotImplemented(
151 "restore not supported".to_string(),
152 ))
153 }
154 async fn balance_all(&self) -> Result<Vec<WalletBalanceItem>, PayError>;
155 async fn receive_info(
156 &self,
157 wallet: &str,
158 amount: Option<Amount>,
159 ) -> Result<ReceiveInfo, PayError>;
160 async fn receive_claim(&self, wallet: &str, quote_id: &str) -> Result<u64, PayError>;
161
162 async fn cashu_send_quote(
163 &self,
164 _wallet: &str,
165 _amount: &Amount,
166 ) -> Result<CashuSendQuoteInfo, PayError> {
167 Err(PayError::NotImplemented(
168 "cashu_send_quote not supported".to_string(),
169 ))
170 }
171 async fn cashu_send(
172 &self,
173 wallet: &str,
174 amount: Amount,
175 onchain_memo: Option<&str>,
176 mints: Option<&[String]>,
177 ) -> Result<CashuSendResult, PayError>;
178 async fn cashu_receive(
179 &self,
180 wallet: &str,
181 token: &str,
182 ) -> Result<CashuReceiveResult, PayError>;
183 async fn send(
184 &self,
185 wallet: &str,
186 to: &str,
187 onchain_memo: Option<&str>,
188 mints: Option<&[String]>,
189 ) -> Result<SendResult, PayError>;
190
191 async fn send_quote(
192 &self,
193 _wallet: &str,
194 _to: &str,
195 _mints: Option<&[String]>,
196 ) -> Result<SendQuoteInfo, PayError> {
197 Err(PayError::NotImplemented(
198 "send_quote not supported".to_string(),
199 ))
200 }
201
202 async fn history_list(
203 &self,
204 wallet: &str,
205 limit: usize,
206 offset: usize,
207 ) -> Result<Vec<HistoryRecord>, PayError>;
208 async fn history_status(&self, transaction_id: &str) -> Result<HistoryStatusInfo, PayError>;
209 async fn history_onchain_memo(
212 &self,
213 _wallet: &str,
214 _transaction_id: &str,
215 ) -> Result<Option<String>, PayError> {
216 Ok(None)
217 }
218 async fn history_sync(&self, wallet: &str, limit: usize) -> Result<HistorySyncStats, PayError> {
219 let items = self.history_list(wallet, limit, 0).await?;
220 Ok(HistorySyncStats {
221 records_scanned: items.len(),
222 records_added: 0,
223 records_updated: 0,
224 })
225 }
226}
227
228pub struct StubProvider {
233 #[allow(dead_code)]
234 network: Network,
235}
236
237impl StubProvider {
238 pub fn new(network: Network) -> Self {
239 Self { network }
240 }
241}
242
243#[async_trait]
244impl PayProvider for StubProvider {
245 fn network(&self) -> Network {
246 self.network
247 }
248
249 async fn create_wallet(&self, _request: &WalletCreateRequest) -> Result<WalletInfo, PayError> {
250 Err(PayError::NotImplemented("network not enabled".to_string()))
251 }
252
253 async fn create_ln_wallet(
254 &self,
255 _request: LnWalletCreateRequest,
256 ) -> Result<WalletInfo, PayError> {
257 Err(PayError::NotImplemented("network not enabled".to_string()))
258 }
259
260 async fn close_wallet(&self, _wallet: &str) -> Result<(), PayError> {
261 Err(PayError::NotImplemented("network not enabled".to_string()))
262 }
263
264 async fn list_wallets(&self) -> Result<Vec<WalletSummary>, PayError> {
265 Err(PayError::NotImplemented("network not enabled".to_string()))
266 }
267
268 async fn balance(&self, _wallet: &str) -> Result<BalanceInfo, PayError> {
269 Err(PayError::NotImplemented("network not enabled".to_string()))
270 }
271
272 async fn balance_all(&self) -> Result<Vec<WalletBalanceItem>, PayError> {
273 Err(PayError::NotImplemented("network not enabled".to_string()))
274 }
275
276 async fn receive_info(
277 &self,
278 _wallet: &str,
279 _amount: Option<Amount>,
280 ) -> Result<ReceiveInfo, PayError> {
281 Err(PayError::NotImplemented("network not enabled".to_string()))
282 }
283
284 async fn receive_claim(&self, _wallet: &str, _quote_id: &str) -> Result<u64, PayError> {
285 Err(PayError::NotImplemented("network not enabled".to_string()))
286 }
287
288 async fn cashu_send(
289 &self,
290 _wallet: &str,
291 _amount: Amount,
292 _onchain_memo: Option<&str>,
293 _mints: Option<&[String]>,
294 ) -> Result<CashuSendResult, PayError> {
295 Err(PayError::NotImplemented("network not enabled".to_string()))
296 }
297
298 async fn cashu_receive(
299 &self,
300 _wallet: &str,
301 _token: &str,
302 ) -> Result<CashuReceiveResult, PayError> {
303 Err(PayError::NotImplemented("network not enabled".to_string()))
304 }
305
306 async fn send(
307 &self,
308 _wallet: &str,
309 _to: &str,
310 _onchain_memo: Option<&str>,
311 _mints: Option<&[String]>,
312 ) -> Result<SendResult, PayError> {
313 Err(PayError::NotImplemented("network not enabled".to_string()))
314 }
315
316 async fn history_list(
317 &self,
318 _wallet: &str,
319 _limit: usize,
320 _offset: usize,
321 ) -> Result<Vec<HistoryRecord>, PayError> {
322 Err(PayError::NotImplemented("network not enabled".to_string()))
323 }
324
325 async fn history_status(&self, _transaction_id: &str) -> Result<HistoryStatusInfo, PayError> {
326 Err(PayError::NotImplemented("network not enabled".to_string()))
327 }
328}