Skip to main content

fedimint_gateway_common/
lib.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::str::FromStr;
4use std::time::{Duration, SystemTime};
5
6use bitcoin::address::NetworkUnchecked;
7use bitcoin::hashes::sha256;
8use bitcoin::secp256k1::PublicKey;
9use bitcoin::{Address, Network, OutPoint};
10use clap::Subcommand;
11use envs::{
12    FM_LDK_ALIAS_ENV, FM_LND_MACAROON_ENV, FM_LND_PAYMENT_TIMEOUT_SECS_ENV, FM_LND_RPC_ADDR_ENV,
13    FM_LND_TIME_PREF_ENV, FM_LND_TLS_CERT_ENV, FM_PORT_LDK,
14};
15use fedimint_core::config::{FederationId, JsonClientConfig};
16use fedimint_core::encoding::{Decodable, Encodable};
17use fedimint_core::invite_code::InviteCode;
18use fedimint_core::util::{SafeUrl, get_average, get_median};
19use fedimint_core::{Amount, BitcoinAmountOrAll, secp256k1};
20use fedimint_eventlog::{EventKind, EventLogId, PersistedLogEntry, StructuredPaymentEvents};
21use fedimint_lnv2_common::gateway_api::PaymentFee;
22use fedimint_wallet_client::PegOutFees;
23use lightning::ln::msgs::SocketAddress;
24use lightning_invoice::Bolt11Invoice;
25use serde::de::Error as _;
26use serde::{Deserialize, Deserializer, Serialize, Serializer};
27
28pub mod envs;
29
30pub const V1_API_ENDPOINT: &str = "v1";
31
32pub const ADDRESS_ENDPOINT: &str = "/address";
33pub const ADDRESS_RECHECK_ENDPOINT: &str = "/address_recheck";
34pub const BACKUP_ENDPOINT: &str = "/backup";
35pub const CONFIGURATION_ENDPOINT: &str = "/config";
36pub const CONNECT_FED_ENDPOINT: &str = "/connect_fed";
37pub const CREATE_BOLT11_INVOICE_FOR_OPERATOR_ENDPOINT: &str = "/create_bolt11_invoice_for_operator";
38pub const CREATE_BOLT12_OFFER_FOR_OPERATOR_ENDPOINT: &str = "/create_bolt12_offer_for_operator";
39pub const GATEWAY_INFO_ENDPOINT: &str = "/info";
40pub const INVITE_CODES_ENDPOINT: &str = "/invite_codes";
41pub const GET_BALANCES_ENDPOINT: &str = "/balances";
42pub const GET_INVOICE_ENDPOINT: &str = "/get_invoice";
43pub const GET_LN_ONCHAIN_ADDRESS_ENDPOINT: &str = "/get_ln_onchain_address";
44pub const LEAVE_FED_ENDPOINT: &str = "/leave_fed";
45pub const LIST_CHANNELS_ENDPOINT: &str = "/list_channels";
46pub const LIST_TRANSACTIONS_ENDPOINT: &str = "/list_transactions";
47pub const MNEMONIC_ENDPOINT: &str = "/mnemonic";
48pub const CONNECT_PEER_ENDPOINT: &str = "/connect_peer";
49pub const OPEN_CHANNEL_ENDPOINT: &str = "/open_channel";
50pub const OPEN_CHANNEL_WITH_PUSH_ENDPOINT: &str = "/open_channel_with_push";
51pub const CLOSE_CHANNELS_WITH_PEER_ENDPOINT: &str = "/close_channels_with_peer";
52pub const PAY_INVOICE_FOR_OPERATOR_ENDPOINT: &str = "/pay_invoice_for_operator";
53pub const PAY_OFFER_FOR_OPERATOR_ENDPOINT: &str = "/pay_offer_for_operator";
54pub const PAYMENT_LOG_ENDPOINT: &str = "/payment_log";
55pub const PAYMENT_SUMMARY_ENDPOINT: &str = "/payment_summary";
56pub const PEGIN_FROM_ONCHAIN_ENDPOINT: &str = "/pegin_from_onchain";
57pub const RECEIVE_ECASH_ENDPOINT: &str = "/receive_ecash";
58pub const SET_CHANNEL_FEES_ENDPOINT: &str = "/set_channel_fees";
59pub const SET_FEES_ENDPOINT: &str = "/set_fees";
60pub const STOP_ENDPOINT: &str = "/stop";
61pub const SEND_ONCHAIN_ENDPOINT: &str = "/send_onchain";
62pub const SPEND_ECASH_ENDPOINT: &str = "/spend_ecash";
63pub const WITHDRAW_ENDPOINT: &str = "/withdraw";
64pub const WITHDRAW_TO_ONCHAIN_ENDPOINT: &str = "/withdraw_to_onchain";
65
66pub const DEFAULT_LIGHTNING_PORT: u16 = 9735;
67
68#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct ConnectFedPayload {
70    pub invite_code: String,
71    pub use_tor: Option<bool>,
72    pub recover: Option<bool>,
73}
74
75#[derive(Debug, Serialize, Deserialize, Clone)]
76pub struct LeaveFedPayload {
77    pub federation_id: FederationId,
78}
79
80#[derive(Debug, Serialize, Deserialize)]
81pub struct InfoPayload;
82
83#[derive(Debug, Serialize, Deserialize)]
84pub struct BackupPayload {
85    pub federation_id: FederationId,
86}
87
88#[derive(Debug, Serialize, Deserialize, Clone)]
89pub struct ConfigPayload {
90    pub federation_id: Option<FederationId>,
91}
92
93#[derive(Debug, Serialize, Deserialize, Clone)]
94pub struct DepositAddressPayload {
95    pub federation_id: FederationId,
96}
97
98#[derive(Debug, Serialize, Deserialize, Clone)]
99pub struct PeginFromOnchainPayload {
100    pub federation_id: FederationId,
101    pub amount: BitcoinAmountOrAll,
102    pub fee_rate_sats_per_vbyte: u64,
103}
104
105#[derive(Debug, Serialize, Deserialize, Clone)]
106pub struct DepositAddressRecheckPayload {
107    pub address: Address<NetworkUnchecked>,
108    pub federation_id: FederationId,
109}
110
111#[derive(Debug, Serialize, Deserialize, Clone)]
112pub struct WithdrawPayload {
113    pub federation_id: FederationId,
114    pub amount: BitcoinAmountOrAll,
115    pub address: Address<NetworkUnchecked>,
116    /// When provided (from UI preview flow), uses these quoted fees.
117    /// When None, fetches current fees from the wallet.
118    #[serde(default)]
119    pub quoted_fees: Option<PegOutFees>,
120}
121
122#[derive(Debug, Serialize, Deserialize, Clone)]
123pub struct WithdrawToOnchainPayload {
124    pub federation_id: FederationId,
125    pub amount: BitcoinAmountOrAll,
126}
127
128#[derive(Debug, Serialize, Deserialize, Clone)]
129pub struct WithdrawResponse {
130    pub txid: bitcoin::Txid,
131    pub fees: PegOutFees,
132}
133
134#[derive(Debug, Serialize, Deserialize, Clone)]
135pub struct WithdrawPreviewPayload {
136    pub federation_id: FederationId,
137    pub amount: BitcoinAmountOrAll,
138    pub address: Address<NetworkUnchecked>,
139}
140
141#[derive(Debug, Serialize, Deserialize, Clone)]
142pub struct WithdrawPreviewResponse {
143    pub withdraw_amount: Amount,
144    pub address: String,
145    pub peg_out_fees: PegOutFees,
146    pub total_cost: Amount,
147    /// Estimated mint fees when withdrawing all. None for partial withdrawals.
148    #[serde(default)]
149    pub mint_fees: Option<Amount>,
150}
151
152/// Deprecated, unused, doesn't do anything
153///
154/// Only here for backward-compat reasons.
155#[allow(deprecated)]
156#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
157pub enum ConnectorType {
158    Tcp,
159    Tor,
160}
161
162#[derive(Debug, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
163pub struct FederationConfig {
164    pub invite_code: InviteCode,
165    // Unique integer identifier per-federation that is assigned when the gateways joins a
166    // federation.
167    #[serde(alias = "mint_channel_id")]
168    pub federation_index: u64,
169    pub lightning_fee: PaymentFee,
170    pub transaction_fee: PaymentFee,
171    #[allow(deprecated)] // only here for decoding backward-compat
172    pub _connector: ConnectorType,
173}
174
175/// Information about one of the feds we are connected to
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177pub struct FederationInfo {
178    pub federation_id: FederationId,
179    pub federation_name: Option<String>,
180    pub balance_msat: Amount,
181    pub config: FederationConfig,
182    pub last_backup_time: Option<SystemTime>,
183}
184
185#[derive(Debug, Serialize, Deserialize, PartialEq)]
186pub struct GatewayInfo {
187    pub version_hash: String,
188    pub federations: Vec<FederationInfo>,
189    /// Mapping from short channel id to the federation id that it belongs to.
190    // TODO: Remove this alias once it no longer breaks backwards compatibility.
191    #[serde(alias = "channels")]
192    pub federation_fake_scids: Option<BTreeMap<u64, FederationId>>,
193    pub gateway_state: String,
194    pub lightning_info: LightningInfo,
195    pub lightning_mode: LightningMode,
196    pub registrations: BTreeMap<RegisteredProtocol, (SafeUrl, secp256k1::PublicKey)>,
197}
198
199#[derive(Debug, Serialize, Deserialize, PartialEq)]
200pub struct GatewayFedConfig {
201    pub federations: BTreeMap<FederationId, JsonClientConfig>,
202}
203
204#[derive(Debug, Serialize, Deserialize, Clone)]
205pub struct SetFeesPayload {
206    pub federation_id: Option<FederationId>,
207    pub lightning_base: Option<Amount>,
208    pub lightning_parts_per_million: Option<u64>,
209    pub transaction_base: Option<Amount>,
210    pub transaction_parts_per_million: Option<u64>,
211}
212
213#[derive(Debug, Serialize, Deserialize, Clone)]
214pub struct CreateInvoiceForOperatorPayload {
215    pub amount_msats: u64,
216    pub expiry_secs: Option<u32>,
217    pub description: Option<String>,
218}
219
220#[derive(Debug, Serialize, Deserialize, Clone)]
221pub struct PayInvoiceForOperatorPayload {
222    pub invoice: Bolt11Invoice,
223}
224
225#[derive(Debug, Serialize, Deserialize, Clone)]
226pub struct SpendEcashPayload {
227    /// Federation id of the e-cash to spend
228    pub federation_id: FederationId,
229    /// The amount of e-cash to spend
230    pub amount: Amount,
231}
232
233#[derive(Debug, Serialize, Deserialize, Clone)]
234pub struct SpendEcashResponse {
235    /// OOBNotes.to_string() for v1, base32::encode_prefixed() for v2
236    pub notes: String,
237}
238
239#[derive(Debug, Serialize, Deserialize, Clone)]
240pub struct ReceiveEcashPayload {
241    /// Can be OOBNotes (v1) or ECash (v2)
242    pub notes: String,
243    #[serde(default)]
244    pub wait: bool,
245}
246
247#[derive(Debug, Serialize, Deserialize, Clone)]
248pub struct ReceiveEcashResponse {
249    pub amount: Amount,
250}
251
252#[derive(serde::Serialize, serde::Deserialize, Clone)]
253pub struct GatewayBalances {
254    pub onchain_balance_sats: u64,
255    pub lightning_balance_msats: u64,
256    pub ecash_balances: Vec<FederationBalanceInfo>,
257    pub inbound_lightning_liquidity_msats: u64,
258}
259
260#[derive(serde::Serialize, serde::Deserialize, Clone)]
261pub struct FederationBalanceInfo {
262    pub federation_id: FederationId,
263    pub ecash_balance_msats: Amount,
264}
265
266#[derive(Debug, Serialize, Deserialize, Clone)]
267pub struct MnemonicResponse {
268    pub mnemonic: Vec<String>,
269
270    // Legacy federations are federations that the gateway joined prior to v0.5.0
271    // and do not derive their secrets from the gateway's mnemonic. They also use
272    // a separate database from the gateway's db.
273    pub legacy_federations: Vec<FederationId>,
274}
275
276#[derive(Debug, Serialize, Deserialize, Clone)]
277pub struct PaymentLogPayload {
278    // The position in the log to stop querying. No events will be returned from after
279    // this `EventLogId`. If it is `None`, the last `EventLogId` is used.
280    pub end_position: Option<EventLogId>,
281
282    // The number of events to return
283    pub pagination_size: usize,
284
285    pub federation_id: FederationId,
286
287    /// Filter to only return events of these kinds. If empty, defaults to
288    /// `ALL_GATEWAY_EVENTS` (gateway payment-related events only, not all
289    /// events in the log).
290    ///
291    /// Note: returned event IDs may be non-contiguous because other internal
292    /// events (e.g. `tx-created`, `NoteCreated`) share the same ID space but
293    /// are filtered out.
294    pub event_kinds: Vec<EventKind>,
295}
296
297#[derive(Debug, Serialize, Deserialize, Clone)]
298pub struct PaymentLogResponse(pub Vec<PersistedLogEntry>);
299
300#[derive(Debug, Serialize, Deserialize, Clone)]
301pub struct PaymentSummaryResponse {
302    pub outgoing: PaymentStats,
303    pub incoming: PaymentStats,
304}
305
306#[derive(Debug, Serialize, Deserialize, Clone)]
307pub struct PaymentStats {
308    pub average_latency: Option<Duration>,
309    pub median_latency: Option<Duration>,
310    pub total_fees: Amount,
311    pub total_success: usize,
312    pub total_failure: usize,
313}
314
315impl PaymentStats {
316    /// Computes the payment statistics for the given structured payment events.
317    pub fn compute(events: &StructuredPaymentEvents) -> Self {
318        PaymentStats {
319            average_latency: get_average(&events.latencies_usecs).map(Duration::from_micros),
320            median_latency: get_median(&events.latencies_usecs).map(Duration::from_micros),
321            total_fees: Amount::from_msats(events.fees.iter().map(|a| a.msats).sum()),
322            total_success: events.latencies_usecs.len(),
323            total_failure: events.latencies_failure.len(),
324        }
325    }
326}
327
328#[derive(Debug, Serialize, Deserialize, Clone)]
329pub struct PaymentSummaryPayload {
330    pub start_millis: u64,
331    pub end_millis: u64,
332}
333
334#[derive(Serialize, Deserialize, Debug, Clone)]
335pub struct ChannelInfo {
336    pub remote_pubkey: secp256k1::PublicKey,
337    pub channel_size_sats: u64,
338    pub outbound_liquidity_sats: u64,
339    pub inbound_liquidity_sats: u64,
340    pub is_active: bool,
341    pub funding_outpoint: Option<OutPoint>,
342    pub remote_node_alias: Option<String>,
343    #[serde(default)]
344    pub remote_address: Option<String>,
345    /// The local-side base routing fee (msat) currently advertised for this
346    /// channel. `None` if the backend could not report a fee policy.
347    #[serde(default)]
348    pub base_fee_msat: Option<u64>,
349    /// The local-side proportional routing fee (parts per million) currently
350    /// advertised for this channel. `None` if the backend could not report a
351    /// fee policy.
352    #[serde(default)]
353    pub parts_per_million: Option<u64>,
354}
355
356#[derive(Debug, Serialize, Deserialize, Clone)]
357pub struct OpenChannelRequest {
358    pub pubkey: secp256k1::PublicKey,
359    pub host: String,
360    pub channel_size_sats: u64,
361    pub push_amount_sats: u64,
362    /// Feerate (sat/vB) for the channel-opening on-chain transaction. If
363    /// `None`, the Lightning backend picks a feerate. Not honored by all
364    /// backends (e.g. LDK manages its own fee estimation).
365    #[serde(default, deserialize_with = "empty_string_as_none")]
366    pub fee_rate_sats_per_vbyte: Option<u64>,
367    /// Base routing fee (msat) advertised for the opened channel. If `None`,
368    /// the backend's default policy is used.
369    #[serde(default, deserialize_with = "empty_string_as_none")]
370    pub base_fee_msat: Option<u64>,
371    /// Proportional routing fee (parts per million) advertised for the opened
372    /// channel. If `None`, the backend's default policy is used.
373    #[serde(default, deserialize_with = "empty_string_as_none")]
374    pub parts_per_million: Option<u64>,
375}
376
377#[derive(Debug, Serialize, Deserialize, Clone)]
378pub struct ConnectPeerRequest {
379    pub node_address: NodeAddress,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct NodeAddress {
384    pub pubkey: secp256k1::PublicKey,
385    pub address: SocketAddress,
386}
387
388impl NodeAddress {
389    pub fn host_with_port(&self) -> String {
390        self.address.to_string()
391    }
392}
393
394impl FromStr for NodeAddress {
395    type Err = String;
396
397    fn from_str(input: &str) -> Result<Self, Self::Err> {
398        let (pubkey, address) = input
399            .trim()
400            .split_once('@')
401            .ok_or_else(|| "Expected node address in pubkey@host[:port] format".to_string())?;
402        let pubkey = pubkey
403            .parse()
404            .map_err(|err| format!("Invalid peer public key: {err}"))?;
405        let address = address.trim();
406        if address.is_empty() {
407            return Err("Peer host must not be empty".to_string());
408        }
409        let address = if has_explicit_port(address) {
410            address.to_string()
411        } else {
412            format!("{address}:{DEFAULT_LIGHTNING_PORT}")
413        };
414        let address = SocketAddress::from_str(&address)
415            .map_err(|err| format!("Invalid peer address: {err}"))?;
416
417        Ok(NodeAddress { pubkey, address })
418    }
419}
420
421impl fmt::Display for NodeAddress {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        let address = self.address.to_string();
424        if address.ends_with(&format!(":{DEFAULT_LIGHTNING_PORT}")) {
425            write!(
426                f,
427                "{}@{}",
428                self.pubkey,
429                &address[..address.len() - format!(":{DEFAULT_LIGHTNING_PORT}").len()]
430            )
431        } else {
432            write!(f, "{}@{}", self.pubkey, address)
433        }
434    }
435}
436
437fn has_explicit_port(address: &str) -> bool {
438    if address.starts_with('[') {
439        return address.contains("]:");
440    }
441
442    address.contains(':')
443}
444
445impl Serialize for NodeAddress {
446    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
447    where
448        S: Serializer,
449    {
450        serializer.serialize_str(&self.to_string())
451    }
452}
453
454impl<'de> Deserialize<'de> for NodeAddress {
455    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
456    where
457        D: Deserializer<'de>,
458    {
459        String::deserialize(deserializer)?
460            .parse()
461            .map_err(D::Error::custom)
462    }
463}
464
465#[derive(Debug, Serialize, Deserialize, Clone)]
466pub struct SetChannelFeesRequest {
467    /// Funding outpoint identifying the channel whose advertised routing fees
468    /// should be updated.
469    pub funding_outpoint: OutPoint,
470    /// New base routing fee in millisatoshis.
471    pub base_fee_msat: u64,
472    /// New proportional routing fee in parts per million.
473    pub parts_per_million: u64,
474}
475
476/// Helper for serde: deserializes missing values and empty form strings as
477/// `None`. Lets the same struct be used for JSON payloads and HTMX form
478/// submissions where numeric inputs may be left blank.
479fn empty_string_as_none<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
480where
481    D: serde::Deserializer<'de>,
482    T: std::str::FromStr + Deserialize<'de>,
483    T::Err: std::fmt::Display,
484{
485    #[derive(Deserialize)]
486    #[serde(untagged)]
487    enum StringOrT<T> {
488        String(String),
489        T(T),
490    }
491
492    match Option::<StringOrT<T>>::deserialize(deserializer)? {
493        None => Ok(None),
494        Some(StringOrT::T(value)) => Ok(Some(value)),
495        Some(StringOrT::String(s)) if s.trim().is_empty() => Ok(None),
496        Some(StringOrT::String(s)) => s
497            .trim()
498            .parse::<T>()
499            .map(Some)
500            .map_err(serde::de::Error::custom),
501    }
502}
503
504#[derive(Debug, Serialize, Deserialize, Clone)]
505pub struct SendOnchainRequest {
506    pub address: Address<NetworkUnchecked>,
507    pub amount: BitcoinAmountOrAll,
508    pub fee_rate_sats_per_vbyte: u64,
509}
510
511impl fmt::Display for SendOnchainRequest {
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        write!(
514            f,
515            "SendOnchainRequest {{ address: {}, amount: {}, fee_rate_sats_per_vbyte: {} }}",
516            self.address.assume_checked_ref(),
517            self.amount,
518            self.fee_rate_sats_per_vbyte
519        )
520    }
521}
522
523#[derive(Debug, Serialize, Deserialize, Clone)]
524pub struct CloseChannelsWithPeerRequest {
525    pub pubkey: secp256k1::PublicKey,
526    #[serde(default)]
527    pub force: bool,
528    pub sats_per_vbyte: Option<u64>,
529}
530
531impl fmt::Display for CloseChannelsWithPeerRequest {
532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533        write!(
534            f,
535            "CloseChannelsWithPeerRequest {{ pubkey: {}, force: {}, sats_per_vbyte: {} }}",
536            self.pubkey,
537            self.force,
538            match self.sats_per_vbyte {
539                Some(sats) => sats.to_string(),
540                None => "None".to_string(),
541            }
542        )
543    }
544}
545
546#[derive(Debug, Serialize, Deserialize, Clone)]
547pub struct CloseChannelsWithPeerResponse {
548    pub num_channels_closed: u32,
549}
550
551#[derive(Debug, Serialize, Deserialize, Clone)]
552pub struct GetInvoiceRequest {
553    pub payment_hash: sha256::Hash,
554}
555
556#[derive(Debug, Serialize, Deserialize, Clone)]
557pub struct GetInvoiceResponse {
558    pub preimage: Option<String>,
559    pub payment_hash: Option<sha256::Hash>,
560    pub amount: Amount,
561    pub created_at: SystemTime,
562    pub status: PaymentStatus,
563}
564
565#[derive(Debug, Serialize, Deserialize, Clone)]
566pub struct ListTransactionsPayload {
567    pub start_secs: u64,
568    pub end_secs: u64,
569}
570
571#[derive(Debug, Serialize, Deserialize, Clone)]
572pub struct ListTransactionsResponse {
573    pub transactions: Vec<PaymentDetails>,
574}
575
576#[derive(Debug, Serialize, Deserialize, Clone)]
577pub struct PaymentDetails {
578    pub payment_hash: Option<sha256::Hash>,
579    pub preimage: Option<String>,
580    pub payment_kind: PaymentKind,
581    pub amount: Amount,
582    pub direction: PaymentDirection,
583    pub status: PaymentStatus,
584    pub timestamp_secs: u64,
585}
586
587#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
588pub enum PaymentKind {
589    Bolt11,
590    Bolt12Offer,
591    Bolt12Refund,
592    Onchain,
593}
594
595#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
596pub enum PaymentDirection {
597    Outbound,
598    Inbound,
599}
600
601#[derive(Debug, Serialize, Deserialize, Clone)]
602pub struct CreateOfferPayload {
603    pub amount: Option<Amount>,
604    pub description: Option<String>,
605    pub expiry_secs: Option<u32>,
606    pub quantity: Option<u64>,
607}
608
609#[derive(Debug, Serialize, Deserialize, Clone)]
610pub struct CreateOfferResponse {
611    pub offer: String,
612}
613
614#[derive(Debug, Serialize, Deserialize, Clone)]
615pub struct PayOfferPayload {
616    pub offer: String,
617    pub amount: Option<Amount>,
618    pub quantity: Option<u64>,
619    pub payer_note: Option<String>,
620}
621
622#[derive(Debug, Serialize, Deserialize, Clone)]
623pub struct PayOfferResponse {
624    pub preimage: String,
625}
626
627#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
628pub enum PaymentStatus {
629    Pending,
630    Succeeded,
631    Failed,
632}
633
634/// Default value for LND's `SendPaymentRequest::time_pref`. LND interprets this
635/// as a value in [`LND_TIME_PREF_MIN`, `LND_TIME_PREF_MAX`], where -1 optimizes
636/// purely for fees and 1 optimizes purely for reliability. We default to 0.5 to
637/// favor reliability while still considering fees.
638pub const LND_DEFAULT_TIME_PREF: f64 = 0.5;
639pub const LND_TIME_PREF_MIN: f64 = -1.0;
640pub const LND_TIME_PREF_MAX: f64 = 1.0;
641
642fn parse_lnd_time_pref(s: &str) -> Result<f64, String> {
643    let parsed: f64 = s.parse().map_err(|e| format!("invalid f64: {e}"))?;
644    if !parsed.is_finite() || !(LND_TIME_PREF_MIN..=LND_TIME_PREF_MAX).contains(&parsed) {
645        return Err(format!(
646            "must be a finite value in [{LND_TIME_PREF_MIN}, {LND_TIME_PREF_MAX}]"
647        ));
648    }
649    Ok(parsed)
650}
651
652/// Default value for LND's `SendPaymentRequest::timeout_seconds`, i.e. how long
653/// LND keeps trying to route an outgoing payment before giving up. This does
654/// not cancel an HTLC that is already in flight; it bounds pathfinding/retry.
655pub const LND_DEFAULT_PAYMENT_TIMEOUT_SECS: i32 = 180;
656pub const LND_PAYMENT_TIMEOUT_SECS_MIN: i32 = 1;
657pub const LND_PAYMENT_TIMEOUT_SECS_MAX: i32 = 600;
658
659fn parse_lnd_payment_timeout_secs(s: &str) -> Result<i32, String> {
660    let parsed: i32 = s.parse().map_err(|e| format!("invalid i32: {e}"))?;
661    if !(LND_PAYMENT_TIMEOUT_SECS_MIN..=LND_PAYMENT_TIMEOUT_SECS_MAX).contains(&parsed) {
662        return Err(format!(
663            "must be an integer in [{LND_PAYMENT_TIMEOUT_SECS_MIN}, {LND_PAYMENT_TIMEOUT_SECS_MAX}]"
664        ));
665    }
666    Ok(parsed)
667}
668
669#[derive(Debug, Clone, Subcommand, Serialize, Deserialize, PartialEq)]
670pub enum LightningMode {
671    #[clap(name = "lnd")]
672    Lnd {
673        /// LND RPC address
674        #[arg(long = "lnd-rpc-host", env = FM_LND_RPC_ADDR_ENV)]
675        lnd_rpc_addr: String,
676
677        /// LND TLS cert file path
678        #[arg(long = "lnd-tls-cert", env = FM_LND_TLS_CERT_ENV)]
679        lnd_tls_cert: String,
680
681        /// LND macaroon file path
682        #[arg(long = "lnd-macaroon", env = FM_LND_MACAROON_ENV)]
683        lnd_macaroon: String,
684
685        /// `time_pref` passed to LND `SendPaymentRequest`. -1.0 optimizes
686        /// purely for fees, 1.0 optimizes purely for reliability.
687        #[arg(
688            long = "lnd-time-pref",
689            env = FM_LND_TIME_PREF_ENV,
690            default_value_t = LND_DEFAULT_TIME_PREF,
691            value_parser = parse_lnd_time_pref,
692        )]
693        lnd_time_pref: f64,
694
695        /// How long (in seconds) LND keeps trying to route an outgoing payment
696        /// before giving up. Passed as `timeout_seconds` to LND
697        /// `SendPaymentRequest`. Must be in [1, 600].
698        #[arg(
699            long = "lnd-payment-timeout",
700            env = FM_LND_PAYMENT_TIMEOUT_SECS_ENV,
701            default_value_t = LND_DEFAULT_PAYMENT_TIMEOUT_SECS,
702            value_parser = parse_lnd_payment_timeout_secs,
703        )]
704        lnd_payment_timeout_secs: i32,
705    },
706    #[clap(name = "ldk")]
707    Ldk {
708        /// LDK lightning server port
709        #[arg(long = "ldk-lightning-port", env = FM_PORT_LDK)]
710        lightning_port: u16,
711
712        /// LDK's Alias
713        #[arg(long = "ldk-alias", env = FM_LDK_ALIAS_ENV)]
714        alias: String,
715    },
716}
717
718#[derive(Clone)]
719pub enum ChainSource {
720    Bitcoind {
721        username: String,
722        password: String,
723        server_url: SafeUrl,
724    },
725    Esplora {
726        server_url: SafeUrl,
727    },
728}
729
730impl fmt::Display for ChainSource {
731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732        match self {
733            ChainSource::Bitcoind {
734                username: _,
735                password: _,
736                server_url,
737            } => {
738                write!(f, "Bitcoind source with URL: {server_url}")
739            }
740            ChainSource::Esplora { server_url } => {
741                write!(f, "Esplora source with URL: {server_url}")
742            }
743        }
744    }
745}
746
747#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
748#[serde(rename_all = "snake_case")]
749pub enum LightningInfo {
750    Connected {
751        public_key: PublicKey,
752        alias: String,
753        network: Network,
754        block_height: u64,
755        synced_to_chain: bool,
756    },
757    NotConnected,
758}
759
760#[derive(
761    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Encodable, Decodable, Serialize, Deserialize,
762)]
763#[serde(rename_all = "snake_case")]
764pub enum RegisteredProtocol {
765    Http,
766    Iroh,
767}
768
769#[derive(Debug, Serialize, Deserialize, Clone)]
770pub struct SetMnemonicPayload {
771    pub words: Option<String>,
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    const NODE_PUBKEY: &str = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
779
780    #[test]
781    fn node_address_defaults_lightning_port() {
782        let node_address: NodeAddress = format!("{NODE_PUBKEY}@example.com")
783            .parse()
784            .expect("valid node address");
785
786        assert_eq!(node_address.host_with_port(), "example.com:9735");
787        assert_eq!(
788            node_address.to_string(),
789            format!("{NODE_PUBKEY}@example.com")
790        );
791    }
792
793    #[test]
794    fn node_address_keeps_explicit_non_default_port() {
795        let node_address: NodeAddress = format!("{NODE_PUBKEY}@example.com:9736")
796            .parse()
797            .expect("valid node address");
798
799        assert_eq!(node_address.host_with_port(), "example.com:9736");
800        assert_eq!(
801            node_address.to_string(),
802            format!("{NODE_PUBKEY}@example.com:9736")
803        );
804    }
805
806    #[test]
807    fn node_address_handles_bracketed_ipv6_address() {
808        let node_address: NodeAddress = format!("{NODE_PUBKEY}@[::1]")
809            .parse()
810            .expect("valid node address");
811
812        assert_eq!(
813            node_address.host_with_port(),
814            "[0000:0000:0000:0000:0000:0000:0000:0001]:9735"
815        );
816        assert_eq!(
817            node_address.to_string(),
818            format!("{NODE_PUBKEY}@[0000:0000:0000:0000:0000:0000:0000:0001]")
819        );
820
821        let node_address: NodeAddress = format!("{NODE_PUBKEY}@[::1]:9736")
822            .parse()
823            .expect("valid node address");
824
825        assert_eq!(
826            node_address.host_with_port(),
827            "[0000:0000:0000:0000:0000:0000:0000:0001]:9736"
828        );
829        assert_eq!(
830            node_address.to_string(),
831            format!("{NODE_PUBKEY}@[0000:0000:0000:0000:0000:0000:0000:0001]:9736")
832        );
833    }
834
835    #[test]
836    fn node_address_serde_uses_display_format() {
837        let request = ConnectPeerRequest {
838            node_address: format!("{NODE_PUBKEY}@127.0.0.1:9735")
839                .parse()
840                .expect("valid node address"),
841        };
842
843        let json = serde_json::to_string(&request).expect("can serialize request");
844        assert_eq!(
845            json,
846            format!(r#"{{"node_address":"{NODE_PUBKEY}@127.0.0.1"}}"#)
847        );
848
849        let request: ConnectPeerRequest =
850            serde_json::from_str(&json).expect("can deserialize request");
851        assert_eq!(request.node_address.host_with_port(), "127.0.0.1:9735");
852    }
853}