Skip to main content

agent_first_pay/provider/
mod.rs

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// ═══════════════════════════════════════════
22// PayError
23// ═══════════════════════════════════════════
24
25#[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        /// Which node rejected: None = local, Some(endpoint) = remote.
42        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// ═══════════════════════════════════════════
107// PayProvider Trait
108// ═══════════════════════════════════════════
109
110#[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    /// Whether this provider writes to local disk (needs data-dir lock).
123    fn writes_locally(&self) -> bool {
124        false
125    }
126
127    /// Connectivity check. Remote providers ping the RPC endpoint; local providers no-op.
128    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    /// Optional provider-specific on-chain memo decoding for a transaction.
210    /// Returns `Ok(None)` when memo cannot be decoded or is absent.
211    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
228// ═══════════════════════════════════════════
229// StubProvider
230// ═══════════════════════════════════════════
231
232pub 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}