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    #[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    /// Optional provider-specific on-chain memo decoding for a transaction.
211    /// Returns `Ok(None)` when memo cannot be decoded or is absent.
212    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
229// ═══════════════════════════════════════════
230// StubProvider
231// ═══════════════════════════════════════════
232
233pub 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}