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