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 #[cfg(feature = "interactive")]
163 async fn cashu_send_quote(
164 &self,
165 _wallet: &str,
166 _amount: &Amount,
167 ) -> Result<CashuSendQuoteInfo, PayError> {
168 Err(PayError::NotImplemented(
169 "cashu_send_quote not supported".to_string(),
170 ))
171 }
172 async fn cashu_send(
173 &self,
174 wallet: &str,
175 amount: Amount,
176 onchain_memo: Option<&str>,
177 mints: Option<&[String]>,
178 ) -> Result<CashuSendResult, PayError>;
179 async fn cashu_receive(
180 &self,
181 wallet: &str,
182 token: &str,
183 ) -> Result<CashuReceiveResult, PayError>;
184 async fn send(
185 &self,
186 wallet: &str,
187 to: &str,
188 onchain_memo: Option<&str>,
189 mints: Option<&[String]>,
190 ) -> Result<SendResult, PayError>;
191
192 async fn send_quote(
193 &self,
194 _wallet: &str,
195 _to: &str,
196 _mints: Option<&[String]>,
197 ) -> Result<SendQuoteInfo, PayError> {
198 Err(PayError::NotImplemented(
199 "send_quote not supported".to_string(),
200 ))
201 }
202
203 async fn history_list(
204 &self,
205 wallet: &str,
206 limit: usize,
207 offset: usize,
208 ) -> Result<Vec<HistoryRecord>, PayError>;
209 async fn history_status(&self, transaction_id: &str) -> Result<HistoryStatusInfo, PayError>;
210 async fn history_onchain_memo(
213 &self,
214 _wallet: &str,
215 _transaction_id: &str,
216 ) -> Result<Option<String>, PayError> {
217 Ok(None)
218 }
219 async fn history_sync(&self, wallet: &str, limit: usize) -> Result<HistorySyncStats, PayError> {
220 let items = self.history_list(wallet, limit, 0).await?;
221 Ok(HistorySyncStats {
222 records_scanned: items.len(),
223 records_added: 0,
224 records_updated: 0,
225 })
226 }
227}
228
229pub struct StubProvider {
234 #[allow(dead_code)]
235 network: Network,
236}
237
238impl StubProvider {
239 pub fn new(network: Network) -> Self {
240 Self { network }
241 }
242}
243
244#[async_trait]
245impl PayProvider for StubProvider {
246 fn network(&self) -> Network {
247 self.network
248 }
249
250 async fn create_wallet(&self, _request: &WalletCreateRequest) -> Result<WalletInfo, PayError> {
251 Err(PayError::NotImplemented("network not enabled".to_string()))
252 }
253
254 async fn create_ln_wallet(
255 &self,
256 _request: LnWalletCreateRequest,
257 ) -> Result<WalletInfo, PayError> {
258 Err(PayError::NotImplemented("network not enabled".to_string()))
259 }
260
261 async fn close_wallet(&self, _wallet: &str) -> Result<(), PayError> {
262 Err(PayError::NotImplemented("network not enabled".to_string()))
263 }
264
265 async fn list_wallets(&self) -> Result<Vec<WalletSummary>, PayError> {
266 Err(PayError::NotImplemented("network not enabled".to_string()))
267 }
268
269 async fn balance(&self, _wallet: &str) -> Result<BalanceInfo, PayError> {
270 Err(PayError::NotImplemented("network not enabled".to_string()))
271 }
272
273 async fn balance_all(&self) -> Result<Vec<WalletBalanceItem>, PayError> {
274 Err(PayError::NotImplemented("network not enabled".to_string()))
275 }
276
277 async fn receive_info(
278 &self,
279 _wallet: &str,
280 _amount: Option<Amount>,
281 ) -> Result<ReceiveInfo, PayError> {
282 Err(PayError::NotImplemented("network not enabled".to_string()))
283 }
284
285 async fn receive_claim(&self, _wallet: &str, _quote_id: &str) -> Result<u64, PayError> {
286 Err(PayError::NotImplemented("network not enabled".to_string()))
287 }
288
289 async fn cashu_send(
290 &self,
291 _wallet: &str,
292 _amount: Amount,
293 _onchain_memo: Option<&str>,
294 _mints: Option<&[String]>,
295 ) -> Result<CashuSendResult, PayError> {
296 Err(PayError::NotImplemented("network not enabled".to_string()))
297 }
298
299 async fn cashu_receive(
300 &self,
301 _wallet: &str,
302 _token: &str,
303 ) -> Result<CashuReceiveResult, PayError> {
304 Err(PayError::NotImplemented("network not enabled".to_string()))
305 }
306
307 async fn send(
308 &self,
309 _wallet: &str,
310 _to: &str,
311 _onchain_memo: Option<&str>,
312 _mints: Option<&[String]>,
313 ) -> Result<SendResult, PayError> {
314 Err(PayError::NotImplemented("network not enabled".to_string()))
315 }
316
317 async fn history_list(
318 &self,
319 _wallet: &str,
320 _limit: usize,
321 _offset: usize,
322 ) -> Result<Vec<HistoryRecord>, PayError> {
323 Err(PayError::NotImplemented("network not enabled".to_string()))
324 }
325
326 async fn history_status(&self, _transaction_id: &str) -> Result<HistoryStatusInfo, PayError> {
327 Err(PayError::NotImplemented("network not enabled".to_string()))
328 }
329}