Skip to main content

agent_first_pay/types/
domain.rs

1use super::limits::SpendDebit;
2use serde::{Deserialize, Deserializer, Serialize};
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum Network {
8    Ln,
9    Sol,
10    Evm,
11    Cashu,
12    Btc,
13}
14
15impl std::fmt::Display for Network {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            Self::Ln => write!(f, "ln"),
19            Self::Sol => write!(f, "sol"),
20            Self::Evm => write!(f, "evm"),
21            Self::Cashu => write!(f, "cashu"),
22            Self::Btc => write!(f, "btc"),
23        }
24    }
25}
26
27impl std::str::FromStr for Network {
28    type Err = String;
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        match s {
31            "ln" => Ok(Self::Ln),
32            "sol" => Ok(Self::Sol),
33            "evm" => Ok(Self::Evm),
34            "cashu" => Ok(Self::Cashu),
35            "btc" => Ok(Self::Btc),
36            _ => Err(format!(
37                "unknown network '{s}'; expected: cashu, ln, sol, evm, btc"
38            )),
39        }
40    }
41}
42
43#[derive(Clone, Serialize, Deserialize)]
44pub struct WalletCreateRequest {
45    pub label: String,
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub mint_url: Option<String>,
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub rpc_endpoints: Vec<String>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub chain_id: Option<u64>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub mnemonic_secret: Option<String>,
54    /// Esplora API URL for BTC (btc only).
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub btc_esplora_url: Option<String>,
57    /// BTC sub-network: "mainnet" or "signet" (btc only).
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub btc_network: Option<String>,
60    /// BTC address type: "taproot" or "segwit" (btc only).
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub btc_address_type: Option<String>,
63    /// BTC chain-source backend: esplora (default), core-rpc, electrum.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub btc_backend: Option<BtcBackend>,
66    /// Bitcoin Core RPC URL (btc core-rpc backend only).
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub btc_core_url: Option<String>,
69    /// Bitcoin Core RPC auth "user:pass" (btc core-rpc backend only).
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub btc_core_auth_secret: Option<String>,
72    /// Electrum server URL (btc electrum backend only).
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub btc_electrum_url: Option<String>,
75}
76
77impl std::fmt::Debug for WalletCreateRequest {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("WalletCreateRequest")
80            .field("label", &self.label)
81            .field("mint_url", &self.mint_url)
82            .field("rpc_endpoints", &self.rpc_endpoints)
83            .field("chain_id", &self.chain_id)
84            .field(
85                "mnemonic_secret",
86                &self.mnemonic_secret.as_ref().map(|_| "***"),
87            )
88            .field("btc_esplora_url", &self.btc_esplora_url)
89            .field("btc_network", &self.btc_network)
90            .field("btc_address_type", &self.btc_address_type)
91            .field("btc_backend", &self.btc_backend)
92            .field("btc_core_url", &self.btc_core_url)
93            .field(
94                "btc_core_auth_secret",
95                &self.btc_core_auth_secret.as_ref().map(|_| "***"),
96            )
97            .field("btc_electrum_url", &self.btc_electrum_url)
98            .finish()
99    }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "lowercase")]
104pub enum Direction {
105    Send,
106    Receive,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "lowercase")]
111pub enum TxStatus {
112    Pending,
113    Confirmed,
114    Failed,
115}
116
117// ═══════════════════════════════════════════
118// Value Types
119// ═══════════════════════════════════════════
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct Amount {
123    pub value: u64,
124    pub token: String,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum LnWalletBackend {
130    Nwc,
131    Phoenixd,
132    Lnbits,
133}
134
135impl LnWalletBackend {
136    #[cfg_attr(
137        not(any(feature = "ln-nwc", feature = "ln-phoenixd", feature = "ln-lnbits")),
138        allow(dead_code)
139    )]
140    pub fn as_str(self) -> &'static str {
141        match self {
142            Self::Nwc => "nwc",
143            Self::Phoenixd => "phoenixd",
144            Self::Lnbits => "lnbits",
145        }
146    }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "kebab-case")]
151pub enum BtcBackend {
152    Esplora,
153    CoreRpc,
154    Electrum,
155}
156
157impl BtcBackend {
158    #[cfg_attr(
159        not(any(
160            feature = "btc-esplora",
161            feature = "btc-core",
162            feature = "btc-electrum"
163        )),
164        allow(dead_code)
165    )]
166    pub fn as_str(self) -> &'static str {
167        match self {
168            Self::Esplora => "esplora",
169            Self::CoreRpc => "core-rpc",
170            Self::Electrum => "electrum",
171        }
172    }
173}
174
175#[derive(Clone, Serialize, Deserialize)]
176pub struct LnWalletCreateRequest {
177    pub backend: LnWalletBackend,
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub label: Option<String>,
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub nwc_uri_secret: Option<String>,
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub endpoint: Option<String>,
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub password_secret: Option<String>,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub admin_key_secret: Option<String>,
188}
189
190impl std::fmt::Debug for LnWalletCreateRequest {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        f.debug_struct("LnWalletCreateRequest")
193            .field("backend", &self.backend)
194            .field("label", &self.label)
195            .field(
196                "nwc_uri_secret",
197                &self.nwc_uri_secret.as_ref().map(|_| "***"),
198            )
199            .field("endpoint", &self.endpoint)
200            .field(
201                "password_secret",
202                &self.password_secret.as_ref().map(|_| "***"),
203            )
204            .field(
205                "admin_key_secret",
206                &self.admin_key_secret.as_ref().map(|_| "***"),
207            )
208            .finish()
209    }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct WalletInfo {
214    pub id: String,
215    pub network: Network,
216    pub address: String,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub label: Option<String>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub mnemonic: Option<String>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct WalletSummary {
225    pub id: String,
226    pub network: Network,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub label: Option<String>,
229    pub address: String,
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub backend: Option<String>,
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub mint_url: Option<String>,
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub rpc_endpoints: Option<Vec<String>>,
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub chain_id: Option<u64>,
238    pub created_at_epoch_s: u64,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct BalanceInfo {
243    pub confirmed: u64,
244    pub pending: u64,
245    /// Native unit name: "sats", "lamports", "gwei", "token-units".
246    pub unit: String,
247    /// Provider-specific extra balance categories.
248    /// Example: `fee_credit_sats` for phoenixd.
249    #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
250    pub additional: BTreeMap<String, u64>,
251}
252
253impl BalanceInfo {
254    #[allow(dead_code)]
255    pub fn new(confirmed: u64, pending: u64, unit: impl Into<String>) -> Self {
256        Self {
257            confirmed,
258            pending,
259            unit: unit.into(),
260            additional: BTreeMap::new(),
261        }
262    }
263
264    #[cfg_attr(not(feature = "ln-phoenixd"), allow(dead_code))]
265    pub fn with_additional(mut self, key: impl Into<String>, value: u64) -> Self {
266        self.additional.insert(key.into(), value);
267        self
268    }
269
270    #[cfg_attr(
271        not(any(
272            feature = "ln-nwc",
273            feature = "ln-phoenixd",
274            feature = "ln-lnbits",
275            feature = "sol",
276            feature = "evm"
277        )),
278        allow(dead_code)
279    )]
280    pub fn non_zero_components(&self) -> Vec<(String, u64)> {
281        let mut components = Vec::new();
282        if self.confirmed > 0 {
283            components.push((format!("confirmed_{}", self.unit), self.confirmed));
284        }
285        if self.pending > 0 {
286            components.push((format!("pending_{}", self.unit), self.pending));
287        }
288        for (key, value) in &self.additional {
289            if *value > 0 {
290                components.push((key.clone(), *value));
291            }
292        }
293        components
294    }
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct WalletBalanceItem {
299    #[serde(flatten)]
300    pub wallet: WalletSummary,
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub balance: Option<BalanceInfo>,
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub error: Option<String>,
305}
306
307/// Per-network balance summary aggregated from individual wallets.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct NetworkBalanceSummary {
310    pub network: Network,
311    pub wallet_count: usize,
312    pub confirmed: u64,
313    pub pending: u64,
314    pub unit: String,
315    pub errors: usize,
316}
317
318impl NetworkBalanceSummary {
319    /// Build summaries grouped by (network, unit) from a list of wallet balances.
320    pub fn from_wallets(wallets: &[WalletBalanceItem]) -> Vec<Self> {
321        use std::collections::BTreeMap;
322        let mut groups: BTreeMap<(String, String), Self> = BTreeMap::new();
323        for item in wallets {
324            let network = item.wallet.network;
325            let (unit, confirmed, pending) = match &item.balance {
326                Some(b) => (b.unit.clone(), b.confirmed, b.pending),
327                None => ("unknown".to_string(), 0, 0),
328            };
329            let has_error = item.error.is_some() || item.balance.is_none();
330            let key = (network.to_string(), unit.clone());
331            let entry = groups.entry(key).or_insert(Self {
332                network,
333                wallet_count: 0,
334                confirmed: 0,
335                pending: 0,
336                unit,
337                errors: 0,
338            });
339            entry.wallet_count += 1;
340            entry.confirmed += confirmed;
341            entry.pending += pending;
342            if has_error {
343                entry.errors += 1;
344            }
345        }
346        groups.into_values().collect()
347    }
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct ReceiveInfo {
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub address: Option<String>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub invoice: Option<String>,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub quote_id: Option<String>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct HistoryRecord {
362    pub transaction_id: String,
363    pub wallet: String,
364    pub network: Network,
365    pub direction: Direction,
366    pub amount: Amount,
367    pub status: TxStatus,
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub onchain_memo: Option<String>,
370    #[serde(
371        default,
372        skip_serializing_if = "Option::is_none",
373        deserialize_with = "deserialize_local_memo"
374    )]
375    pub local_memo: Option<BTreeMap<String, String>>,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub remote_addr: Option<String>,
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub preimage: Option<String>,
380    pub created_at_epoch_s: u64,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub confirmed_at_epoch_s: Option<u64>,
383    #[serde(default, skip_serializing_if = "Option::is_none")]
384    pub fee: Option<Amount>,
385    /// Reference keys found in the transaction (sol only, per strain-payment-method-solana).
386    #[serde(default, skip_serializing_if = "Option::is_none")]
387    pub reference_keys: Option<Vec<String>>,
388}
389
390#[derive(Debug, Clone, Serialize)]
391pub struct CashuSendResult {
392    pub wallet: String,
393    pub transaction_id: String,
394    pub status: TxStatus,
395    pub fee: Option<Amount>,
396    pub token: String,
397}
398
399#[derive(Debug, Clone, Serialize)]
400pub struct CashuReceiveResult {
401    pub wallet: String,
402    pub amount: Amount,
403    pub memo: Option<String>,
404}
405
406#[derive(Debug, Clone, Serialize)]
407pub struct RestoreResult {
408    pub wallet: String,
409    pub unspent: u64,
410    pub spent: u64,
411    pub pending: u64,
412    pub unit: String,
413}
414
415#[cfg(feature = "interactive")]
416#[derive(Debug, Clone, Serialize)]
417pub struct CashuSendQuoteInfo {
418    pub wallet: String,
419    pub amount_native: u64,
420    pub fee_native: u64,
421    pub fee_unit: String,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct SendQuoteInfo {
426    pub wallet: String,
427    pub amount_native: u64,
428    pub fee_estimate_native: u64,
429    pub fee_unit: String,
430    #[serde(default, skip_serializing_if = "Vec::is_empty")]
431    pub spend_debits: Vec<SpendDebit>,
432}
433
434#[derive(Debug, Clone, Serialize)]
435pub struct SendResult {
436    pub wallet: String,
437    pub transaction_id: String,
438    pub amount: Amount,
439    pub fee: Option<Amount>,
440    pub preimage: Option<String>,
441}
442
443#[derive(Debug, Clone, Serialize)]
444pub struct HistoryStatusInfo {
445    pub transaction_id: String,
446    pub status: TxStatus,
447    pub confirmations: Option<u32>,
448    pub preimage: Option<String>,
449    pub item: Option<HistoryRecord>,
450}
451
452/// Deserializes `local_memo` with backward compatibility.
453/// Accepts: null → None, "string" → Some({"note": "string"}), {object} → Some(object).
454pub(crate) fn deserialize_local_memo<'de, D>(
455    d: D,
456) -> Result<Option<BTreeMap<String, String>>, D::Error>
457where
458    D: Deserializer<'de>,
459{
460    use serde::de;
461
462    struct LocalMemoVisitor;
463
464    impl<'de> de::Visitor<'de> for LocalMemoVisitor {
465        type Value = Option<BTreeMap<String, String>>;
466
467        fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
468            f.write_str("null, a string, or a map of string→string")
469        }
470
471        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
472            Ok(None)
473        }
474
475        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
476            Ok(None)
477        }
478
479        fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
480            let mut m = BTreeMap::new();
481            m.insert("note".to_string(), v.to_string());
482            Ok(Some(m))
483        }
484
485        fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
486            let mut m = BTreeMap::new();
487            m.insert("note".to_string(), v);
488            Ok(Some(m))
489        }
490
491        fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
492            let mut m = BTreeMap::new();
493            while let Some((k, v)) = map.next_entry::<String, String>()? {
494                m.insert(k, v);
495            }
496            Ok(Some(m))
497        }
498
499        fn visit_some<D2: Deserializer<'de>>(self, d: D2) -> Result<Self::Value, D2::Error> {
500            d.deserialize_any(Self)
501        }
502    }
503
504    d.deserialize_option(LocalMemoVisitor)
505}