nvpn 4.1.13

CLI and daemon for Nostr VPN private mesh networks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
#[derive(Debug, Args)]
struct PaidExitArgs {
    #[command(subcommand)]
    command: PaidExitCommand,
}

const DEFAULT_FIPS_PEER_RATING_SCOPE: &str = "fips.peer";
const RATING_FACT_KIND: u64 = 7368;
const RATING_FACT_TYPE: &str = "rating";
const RATING_FACT_SCHEMA: &str = "1";
#[cfg(test)]
const PAID_EXIT_RATING_EVENT_LOOKUP_LIMIT: usize = 500;
const PAID_EXIT_OFFER_EVENT_CACHE_LIMIT: usize = 512;

#[derive(Debug, Subcommand)]
enum PaidExitCommand {
    /// Show paid-exit seller config, wallet, offers, channels, and sessions.
    Status(PaidExitStatusArgs),
    /// Enable this machine as a paid-exit seller, refresh its offer, and optionally publish it.
    Run(PaidExitRunArgs),
    /// Build/sign the local paid-exit offer, and optionally publish it.
    Offer(PaidExitOfferArgs),
    /// Import a signed paid-exit offer event from JSON.
    #[command(name = "import-offer")]
    ImportOffer(PaidExitImportOfferArgs),
    /// Discover and verify paid-exit offers received over Nostr pubsub.
    Discover(PaidExitDiscoverArgs),
    /// Open a local buyer session for a discovered paid-exit offer.
    Buy(PaidExitBuyArgs),
    /// Select an existing buyer session as the active public exit route.
    Use(PaidExitUseArgs),
    /// Measure the realized public exit IP and quality for a paid-exit session.
    Probe(PaidExitProbeArgs),
    /// Record realized exit IP and quality measurements for a paid-exit session.
    #[command(name = "record-probe")]
    RecordProbe(PaidExitRecordProbeArgs),
    /// Export or publish machine-generated paid-exit rating facts.
    Ratings(PaidExitRatingsArgs),
    /// Create a buyer Cashu streaming payment envelope for a paid-exit session.
    #[command(name = "create-payment")]
    CreatePayment(PaidExitCreatePaymentArgs),
    /// Create a fallback fixed Cashu-token lease payment envelope.
    #[command(name = "create-token-lease")]
    CreateTokenLease(PaidExitCreateTokenLeaseArgs),
    /// Sign due buyer Cashu streaming balance updates from the local wallet.
    #[command(name = "stream-payments")]
    StreamPayments(PaidExitStreamPaymentsArgs),
    /// Settle and close a buyer Cashu streaming channel.
    Settle(PaidExitSettleArgs),
    /// Apply an incoming buyer Cashu streaming payment envelope as a seller.
    #[command(name = "apply-payment")]
    ApplyPayment(PaidExitApplyPaymentArgs),
    /// Queue a buyer payment envelope for direct FIPS delivery to the seller.
    #[command(name = "send-payment")]
    SendPayment(PaidExitSendPaymentArgs),
    /// Close a seller Cashu streaming channel and stop routing it.
    Collect(PaidExitCollectArgs),
    /// Close all expired seller Cashu streaming channels with pending credit.
    #[command(name = "collect-due")]
    CollectDue(PaidExitCollectDueArgs),
    /// Manage local paid-route wallet mint metadata.
    Wallet(PaidExitWalletArgs),
}

#[derive(Debug, Args)]
struct PaidExitStatusArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitRunArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Stable Nostr d-tag for this seller's paid-exit offer.
    #[arg(long)]
    offer_id: Option<String>,
    /// Publish the refreshed offer over Nostr pubsub.
    #[arg(long)]
    publish: bool,
    /// Do not ask a running daemon to reload after saving seller config.
    #[arg(long)]
    no_reload_daemon: bool,
    #[arg(long)]
    upstream: Option<String>,
    #[arg(long)]
    price_msat_per_gb: Option<u64>,
    /// Replace accepted Cashu mints with a comma-separated list. Empty clears them.
    #[arg(long)]
    accepted_mints: Option<String>,
    /// Add an accepted Cashu mint. Can be supplied more than once.
    #[arg(long = "accepted-mint")]
    accepted_mint: Vec<String>,
    #[arg(long)]
    country_code: Option<String>,
    #[arg(long)]
    asn: Option<u32>,
    #[arg(long)]
    max_channel_capacity_sat: Option<u64>,
    #[arg(long)]
    channel_expiry_secs: Option<u64>,
    /// Free traffic before payment. Accepts byte quantities such as "1 MB".
    #[arg(long, value_name = "UNITS")]
    free_probe_units: Option<String>,
    /// Extra unpaid traffic allowed after payment runs behind.
    #[arg(long, value_name = "UNITS")]
    grace_units: Option<String>,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitOfferArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Stable Nostr d-tag for this seller's paid-exit offer.
    #[arg(long)]
    offer_id: Option<String>,
    /// Publish the signed offer over Nostr pubsub.
    #[arg(long)]
    publish: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitImportOfferArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Signed Nostr paid-route offer event JSON. Omit with --event-stdin or --event-file.
    #[arg(long, conflicts_with_all = ["event_stdin", "event_file"])]
    event: Option<String>,
    /// Read signed offer event JSON from stdin.
    #[arg(long, conflicts_with = "event_file")]
    event_stdin: bool,
    /// File containing signed offer event JSON.
    #[arg(long, conflicts_with = "event_stdin")]
    event_file: Option<PathBuf>,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitDiscoverArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Import only signed offers accepted by this provider npub/link.
    #[arg(long)]
    provider: Option<String>,
    /// Wait up to this many seconds for the daemon's live discovery cache.
    #[arg(long, default_value_t = 0)]
    duration_secs: u64,
    #[arg(long, default_value_t = 50)]
    limit: usize,
    /// Ignore offer events older than this many seconds.
    #[arg(long, default_value_t = 86_400)]
    since_secs: u64,
    /// FIPS peer ratings JSON exported by `fipsctl ratings export`.
    #[arg(long = "fips-peer-ratings", value_name = "PATH")]
    fips_peer_ratings: Option<PathBuf>,
    /// Trusted Nostr pubkey/npub allowed to publish rating facts. Repeat or comma-separate.
    #[arg(long = "trusted-rating-author", value_name = "NPUB_OR_HEX")]
    trusted_rating_authors: Vec<String>,
    /// Rating scope to read from the ratings file.
    #[arg(long = "rating-scope", default_value = DEFAULT_FIPS_PEER_RATING_SCOPE)]
    rating_scope: String,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitBuyArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Store key, offer id, or seller npub of the paid-exit offer to buy.
    offer: Option<String>,
    /// Buy the highest-rated stored paid-exit offer; unknown sellers rank as neutral.
    #[arg(long = "best-rated")]
    best_rated: bool,
    /// Cashu mint URL to use. Defaults to a compatible wallet/default mint.
    #[arg(long)]
    mint: Option<String>,
    #[arg(long)]
    channel_capacity_sat: Option<u64>,
    #[arg(long, default_value_t = 0)]
    initial_paid_msat: u64,
    /// Keep the selected VPN exit unchanged after opening the paid session.
    #[arg(long)]
    no_select_exit_node: bool,
    /// Do not ask a running daemon to reload after selecting the paid exit.
    #[arg(long)]
    no_reload_daemon: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitUseArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Buyer paid-route session id.
    session: String,
    /// Do not ask a running daemon to reload after selecting the paid exit.
    #[arg(long)]
    no_reload_daemon: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitProbeArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Buyer or seller paid-route session id.
    session: String,
    /// Public-IP endpoint. Plain IP or JSON with ip/query/origin is accepted.
    #[arg(long)]
    ip_url: Option<String>,
    /// STUN server for realized public-IP probing. Defaults to configured NAT STUN servers.
    #[arg(long = "stun-server")]
    stun_servers: Vec<String>,
    /// Skip STUN realized public-IP probing and use HTTPS only.
    #[arg(long)]
    no_stun: bool,
    /// GeoIP endpoint template. Use {ip}; otherwise the IP is appended.
    #[arg(long)]
    geoip_url_template: Option<String>,
    /// Skip GeoIP country/ASN lookup.
    #[arg(long)]
    no_geoip: bool,
    /// Download endpoint for rough bandwidth measurement. Use {bytes} or a bytes query is appended.
    #[arg(long)]
    download_url: Option<String>,
    /// Upload endpoint for rough bandwidth measurement.
    #[arg(long)]
    upload_url: Option<String>,
    /// Bytes to transfer for each rough bandwidth direction.
    #[arg(long, default_value_t = DEFAULT_PAID_ROUTE_BANDWIDTH_BYTES)]
    bandwidth_bytes: u64,
    /// Skip rough bandwidth measurement.
    #[arg(long)]
    no_bandwidth: bool,
    /// Number of public-IP samples used for latency/jitter/loss.
    #[arg(long, default_value_t = 3)]
    samples: u8,
    #[arg(long, default_value_t = 5)]
    timeout_secs: u64,
    /// Do not ask a running daemon to reload after saving the probe result.
    #[arg(long)]
    no_reload_daemon: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitRecordProbeArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Buyer or seller paid-route session id.
    session: String,
    /// Realized public exit IP observed through this route.
    #[arg(long)]
    realized_exit_ip: Option<String>,
    /// Country code observed from realized exit IP geolocation.
    #[arg(long)]
    observed_country_code: Option<String>,
    /// ASN observed from realized exit IP geolocation.
    #[arg(long)]
    observed_asn: Option<u32>,
    #[arg(long)]
    latency_ms: Option<u32>,
    #[arg(long)]
    jitter_ms: Option<u32>,
    /// Packet loss in parts per million.
    #[arg(long)]
    packet_loss_ppm: Option<u32>,
    #[arg(long)]
    down_bps: Option<u64>,
    #[arg(long)]
    up_bps: Option<u64>,
    #[arg(long)]
    uptime_secs: Option<u64>,
    #[arg(long)]
    last_seen_unix: Option<u64>,
    /// Do not ask a running daemon to reload after saving the probe result.
    #[arg(long)]
    no_reload_daemon: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitRatingsArgs {
    #[command(subcommand)]
    command: PaidExitRatingsCommand,
}

#[derive(Debug, Subcommand)]
enum PaidExitRatingsCommand {
    /// Export a signed rating fact event from a stored paid-exit probe.
    Export(PaidExitRatingsExportArgs),
    /// Publish a signed rating fact event from a stored paid-exit probe.
    Publish(PaidExitRatingsPublishArgs),
}

#[derive(Debug, Args)]
struct PaidExitRatingsExportArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Buyer paid-route session id whose stored probe should rate the seller.
    #[arg(long)]
    session: String,
    /// Rating scope to write into the fact event.
    #[arg(long = "rating-scope", default_value = DEFAULT_FIPS_PEER_RATING_SCOPE)]
    rating_scope: String,
    /// Write `{ "events": [...] }` JSON to this path instead of stdout.
    #[arg(long)]
    output: Option<PathBuf>,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitRatingsPublishArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Buyer paid-route session id whose stored probe should rate the seller.
    #[arg(long)]
    session: String,
    /// Rating scope to write into the fact event.
    #[arg(long = "rating-scope", default_value = DEFAULT_FIPS_PEER_RATING_SCOPE)]
    rating_scope: String,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum PaidExitCreatePaymentKind {
    ChannelOpen,
    BalanceUpdate,
    CooperativeClose,
}

impl From<PaidExitCreatePaymentKind> for BuildPaidRouteBuyerPaymentEnvelopeKind {
    fn from(value: PaidExitCreatePaymentKind) -> Self {
        match value {
            PaidExitCreatePaymentKind::ChannelOpen => Self::ChannelOpen,
            PaidExitCreatePaymentKind::BalanceUpdate => Self::BalanceUpdate,
            PaidExitCreatePaymentKind::CooperativeClose => Self::CooperativeClose,
        }
    }
}

#[derive(Debug, Args)]
struct PaidExitCreatePaymentArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Buyer session id.
    session: String,
    /// Payment envelope kind to create.
    #[arg(long, value_enum, default_value_t = PaidExitCreatePaymentKind::BalanceUpdate)]
    kind: PaidExitCreatePaymentKind,
    /// JSON CashuSpilmanPayment snapshot. Omit with --payment-stdin.
    #[arg(long)]
    payment: Option<String>,
    /// Read JSON CashuSpilmanPayment from stdin.
    #[arg(long, conflicts_with = "payment")]
    payment_stdin: bool,
    /// Fund/open the session's Cashu Spilman channel from the local wallet.
    #[arg(long, conflicts_with_all = ["payment", "payment_stdin", "sign_from_wallet"])]
    open_from_wallet: bool,
    /// Sign the payment from the local wallet's persisted Spilman client channel.
    #[arg(long, conflicts_with_all = ["payment", "payment_stdin", "open_from_wallet"])]
    sign_from_wallet: bool,
    /// Override the wallet mint used for --open-from-wallet.
    #[arg(long)]
    mint: Option<String>,
    /// Specific mint keyset id to use for --open-from-wallet.
    #[arg(long)]
    keyset_id: Option<String>,
    /// KeysetInfo JSON for --open-from-wallet. If omitted, fetch from mint.
    #[arg(long, conflicts_with = "keyset_info_file")]
    keyset_info: Option<String>,
    /// File containing KeysetInfo JSON for --open-from-wallet.
    #[arg(long, conflicts_with = "keyset_info")]
    keyset_info_file: Option<PathBuf>,
    /// Maximum value per Cashu output when opening a Spilman channel.
    #[arg(long, default_value_t = 64)]
    max_amount_per_output: u64,
    /// Delivered route units to report. Defaults to current session usage.
    #[arg(long)]
    delivered_units: Option<u64>,
    /// Paid amount in millisats. Defaults from the payment balance and Cashu unit.
    #[arg(long)]
    paid_msat: Option<u64>,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitCreateTokenLeaseArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Buyer session id.
    session: String,
    /// Cashu token to pay the fixed lease. Omit with --token-stdin.
    #[arg(long, required_unless_present = "token_stdin")]
    token: Option<String>,
    /// Read Cashu token from stdin.
    #[arg(long, conflicts_with = "token")]
    token_stdin: bool,
    /// Cashu mint URL for the token. Defaults to the session mint.
    #[arg(long)]
    mint: Option<String>,
    /// Cashu token unit.
    #[arg(long, default_value = "sat")]
    unit: String,
    /// Token amount in the selected Cashu unit.
    #[arg(long, alias = "amount-sat")]
    amount: u64,
    /// Optional route credit in millisats. Defaults from the token amount and unit.
    #[arg(long)]
    paid_msat: Option<u64>,
    /// Override token lease expiry. Defaults to the selected session expiry.
    #[arg(long)]
    expires_at_unix: Option<u64>,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitStreamPaymentsArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Build payment updates without queueing or changing local payment state.
    #[arg(long)]
    dry_run: bool,
    /// Only sign updates at least this many millisats above the last signed balance.
    #[arg(long, default_value_t = 1)]
    min_increment_msat: u64,
    /// Maximum number of due buyer sessions to update. Zero means no limit.
    #[arg(long, default_value_t = 0)]
    limit: usize,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitSettleArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Buyer session id.
    session: String,
    /// Build the close envelope without queueing or changing local payment state.
    #[arg(long)]
    dry_run: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitApplyPaymentArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// JSON StreamingRoutePaymentEnvelope. Omit with --envelope-stdin.
    #[arg(long, required_unless_present = "envelope_stdin")]
    envelope: Option<String>,
    /// Read JSON StreamingRoutePaymentEnvelope from stdin.
    #[arg(long, conflicts_with = "envelope")]
    envelope_stdin: bool,
    /// Do not ask a running daemon to reload after applying a payment.
    #[arg(long)]
    no_reload_daemon: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitSendPaymentArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// JSON StreamingRoutePaymentEnvelope. Omit with --envelope-stdin.
    #[arg(long, required_unless_present = "envelope_stdin")]
    envelope: Option<String>,
    /// Read JSON StreamingRoutePaymentEnvelope from stdin.
    #[arg(long, conflicts_with = "envelope")]
    envelope_stdin: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitCollectArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Seller channel id to close.
    channel: String,
    /// Do not ask a running daemon to reload after closing the channel.
    #[arg(long)]
    no_reload_daemon: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitCollectDueArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    /// Maximum number of due seller channels to close. Zero means no limit.
    #[arg(long, default_value_t = 0)]
    limit: usize,
    /// Do not ask a running daemon to reload after closing due channels.
    #[arg(long)]
    no_reload_daemon: bool,
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Args)]
struct PaidExitWalletArgs {
    #[arg(long)]
    config: Option<PathBuf>,
    #[arg(long)]
    json: bool,
    #[command(subcommand)]
    command: PaidExitWalletCommand,
}

#[derive(Debug, Subcommand)]
enum PaidExitWalletCommand {
    /// Show configured wallet mints and local Cashu wallet balances.
    Show(PaidExitWalletShowArgs),
    /// Create a Lightning invoice to top up the Cashu wallet.
    #[command(name = "topup", alias = "top-up")]
    Topup(PaidExitWalletTopupArgs),
    /// Receive/import a Cashu token into the wallet.
    #[command(alias = "import")]
    Receive(PaidExitWalletReceiveArgs),
    /// Inspect a Cashu token before choosing whether to redeem it.
    Inspect(PaidExitWalletReceiveArgs),
    /// Send/export a Cashu token from the wallet.
    #[command(alias = "export")]
    Send(PaidExitWalletSendArgs),
    /// Pay a Lightning invoice from the Cashu wallet.
    Withdraw(PaidExitWalletWithdrawArgs),
    /// Add or update an accepted Cashu mint.
    AddMint(PaidExitWalletAddMintArgs),
    /// Remove a Cashu mint.
    RemoveMint(PaidExitWalletMintUrlArgs),
    /// Set the default Cashu mint.
    SetDefault(PaidExitWalletMintUrlArgs),
}

#[derive(Debug, Args)]
struct PaidExitWalletShowArgs {
    /// Refresh pending mint quotes and incomplete sends before showing balances.
    #[arg(long)]
    refresh: bool,
    /// Include recent wallet activity.
    #[arg(long)]
    activity: bool,
}

#[derive(Debug, Args)]
struct PaidExitWalletTopupArgs {
    /// Amount to mint, in sats.
    amount_sat: u64,
    /// Cashu mint URL. Defaults to the configured wallet default mint.
    #[arg(long)]
    mint: Option<String>,
}

#[derive(Debug, Args)]
struct PaidExitWalletReceiveArgs {
    /// Cashu token. Omit with --token-stdin to read from stdin.
    token: Option<String>,
    #[arg(long)]
    token_stdin: bool,
}

#[derive(Debug, Args)]
struct PaidExitWalletSendArgs {
    /// Amount to send, in sats.
    amount_sat: u64,
    /// Cashu mint URL. Defaults to the configured wallet default mint.
    #[arg(long)]
    mint: Option<String>,
}

#[derive(Debug, Args)]
struct PaidExitWalletWithdrawArgs {
    /// BOLT11 invoice to pay.
    invoice: String,
    /// Cashu mint URL. Defaults to the configured wallet default mint.
    #[arg(long)]
    mint: Option<String>,
}

#[derive(Debug, Args)]
struct PaidExitWalletAddMintArgs {
    /// Cashu mint URL.
    url: String,
    #[arg(long)]
    label: Option<String>,
    #[arg(long)]
    balance_msat: Option<u64>,
    #[arg(long)]
    make_default: bool,
}

#[derive(Debug, Args)]
struct PaidExitWalletMintUrlArgs {
    /// Cashu mint URL.
    url: String,
}