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 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub btc_esplora_url: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub btc_network: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub btc_address_type: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub btc_backend: Option<BtcBackend>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub btc_core_url: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub btc_core_auth_secret: Option<String>,
72 #[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#[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 pub unit: String,
247 #[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#[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 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 #[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
452pub(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}