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