Skip to main content

fedimint_gateway_common/
lib.rs

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