breez_sdk_core/
models.rs

1use std::cmp::max;
2use std::ops::Add;
3use std::str::FromStr;
4
5use anyhow::{anyhow, ensure, Result};
6use chrono::{DateTime, Duration, Utc};
7use ripemd::Digest;
8use ripemd::Ripemd160;
9use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
10use rusqlite::ToSql;
11use sdk_common::grpc;
12use sdk_common::prelude::Network::*;
13use sdk_common::prelude::*;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use strum_macros::{Display, EnumString};
17
18use crate::bitcoin::blockdata::opcodes;
19use crate::bitcoin::blockdata::script::Builder;
20use crate::bitcoin::hashes::hex::{FromHex, ToHex};
21use crate::bitcoin::hashes::{sha256, Hash};
22use crate::bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
23use crate::bitcoin::{Address, Script};
24use crate::error::SdkResult;
25use crate::lsp::LspInformation;
26use crate::swap_out::boltzswap::{BoltzApiCreateReverseSwapResponse, BoltzApiReverseSwapStatus};
27use crate::swap_out::error::{ReverseSwapError, ReverseSwapResult};
28
29pub const SWAP_PAYMENT_FEE_EXPIRY_SECONDS: u32 = 60 * 60 * 24 * 2; // 2 days
30pub const INVOICE_PAYMENT_FEE_EXPIRY_SECONDS: u32 = 60 * 60; // 60 minutes
31
32/// Different types of supported payments
33#[derive(
34    Default, Clone, PartialEq, Eq, Debug, EnumString, Display, Deserialize, Serialize, Hash,
35)]
36pub enum PaymentType {
37    #[default]
38    Sent,
39    Received,
40    ClosedChannel,
41}
42
43#[derive(Debug)]
44pub struct CustomMessage {
45    pub peer_id: Vec<u8>,
46    pub message_type: u16,
47    pub payload: Vec<u8>,
48}
49
50/// Trait covering LSP-related functionality
51#[tonic::async_trait]
52pub trait LspAPI: Send + Sync {
53    /// List LSPs available to the given pubkey
54    async fn list_lsps(&self, node_pubkey: String) -> SdkResult<Vec<LspInformation>>;
55    /// List all LSPs, active and historical, used by the given pubkey
56    async fn list_used_lsps(&self, node_pubkey: String) -> SdkResult<Vec<LspInformation>>;
57    /// Register for webhook callbacks at the given `webhook_url` whenever a new payment is received
58    async fn register_payment_notifications(
59        &self,
60        lsp_id: String,
61        lsp_pubkey: Vec<u8>,
62        webhook_url: String,
63        webhook_url_signature: String,
64    ) -> SdkResult<grpc::RegisterPaymentNotificationResponse>;
65
66    /// Unregister for webhook callbacks for the given `webhook_url`
67    async fn unregister_payment_notifications(
68        &self,
69        lsp_id: String,
70        lsp_pubkey: Vec<u8>,
71        webhook_url: String,
72        webhook_url_signature: String,
73    ) -> SdkResult<grpc::RemovePaymentNotificationResponse>;
74
75    /// Register a payment to open a new channel with the LSP
76    async fn register_payment(
77        &self,
78        lsp_id: String,
79        lsp_pubkey: Vec<u8>,
80        payment_info: grpc::PaymentInformation,
81    ) -> SdkResult<grpc::RegisterPaymentReply>;
82}
83
84/// Summary of an ongoing swap
85pub struct Swap {
86    pub bitcoin_address: String,
87    pub swapper_pubkey: Vec<u8>,
88    pub lock_height: i64,
89    pub error_message: String,
90    pub required_reserve: i64,
91    /// Absolute minimum amount, in sats, allowed by the swapper for a successful swap
92    pub swapper_min_payable: i64,
93    /// Absolute maximum amount, in sats, allowed by the swapper for a successful swap
94    pub swapper_max_payable: i64,
95}
96
97/// Trait covering functionality involving swaps
98#[tonic::async_trait]
99pub trait SwapperAPI: Send + Sync {
100    async fn complete_swap(&self, bolt11: String) -> Result<()>;
101}
102
103/// Details about the reverse swap fees and parameters, at this point in time
104#[derive(Clone, PartialEq, Debug, Serialize)]
105pub struct ReverseSwapPairInfo {
106    /// Minimum amount of sats a reverse swap is allowed to have given the current feerate conditions
107    pub min: u64,
108    /// Maximum amount of sats a reverse swap is allowed to have given the current feerate conditions
109    pub max: u64,
110    /// Hash of the pair info JSON
111    pub fees_hash: String,
112    /// Percentage fee for the reverse swap service
113    pub fees_percentage: f64,
114    /// Miner fees in sats for locking up funds
115    pub fees_lockup: u64,
116    /// Miner fees in sats for claiming funds. Estimate or exact value, depending on the request args.
117    pub fees_claim: u64,
118    /// Total fees for the reverse swap, in sats, based on the given send amount.
119    ///
120    /// The field is set only when the [ReverseSwapFeesRequest] `send_amount_sat` is known.
121    ///
122    /// If the [ReverseSwapFeesRequest] has the `claim_tx_feerate` empty, this is an estimate. If
123    /// the `claim_tx_feerate` is set, this is the exact value of the total reverse swap fees.
124    pub total_fees: Option<u64>,
125}
126
127/// Details of past or ongoing reverse swaps, as stored in the Breez local DB
128#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
129pub struct FullReverseSwapInfo {
130    /// The reverse swap ID, as reported by the Boltz API in case of a successful creation
131    pub id: String,
132
133    /// The blockheight at the moment the reverse swap was created
134    pub created_at_block_height: u32,
135
136    /// Locally generated preimage, revealed in the last step of the reverse swap
137    pub preimage: Vec<u8>,
138
139    /// Locally generated private key, used to sign the claim tx
140    pub private_key: Vec<u8>,
141
142    /// On-chain destination address, to which the reverse swap will finally send funds to
143    pub claim_pubkey: String,
144
145    pub timeout_block_height: u32,
146
147    /// The HODL invoice
148    pub invoice: String,
149    pub redeem_script: String,
150
151    /// Amount of sats that will be locked.
152    ///
153    /// The final amount sent will be this value minus the claim tx fees.
154    pub onchain_amount_sat: u64,
155
156    /// User-specified feerate for the claim tx.
157    ///
158    /// Used for backward-compatibility with older rev swaps. Superseded by `receive_amount_sat`.
159    pub sat_per_vbyte: Option<u32>,
160
161    /// Amount that will be received onchain in the destination address, at the end of the reverse swap.
162    pub receive_amount_sat: Option<u64>,
163
164    pub cache: ReverseSwapInfoCached,
165}
166
167#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
168pub struct ReverseSwapInfoCached {
169    pub status: ReverseSwapStatus,
170    pub lockup_txid: Option<String>,
171    pub claim_txid: Option<String>,
172}
173
174impl FullReverseSwapInfo {
175    /// Builds the expected redeem script
176    pub(crate) fn build_expected_reverse_swap_script(
177        preimage_hash: Vec<u8>,
178        compressed_pub_key: Vec<u8>,
179        sig: Vec<u8>,
180        lock_height: u32,
181    ) -> ReverseSwapResult<Script> {
182        let mut ripemd160_hasher = Ripemd160::new();
183        ripemd160_hasher.update(preimage_hash);
184        let ripemd160_hash = ripemd160_hasher.finalize();
185
186        // Remove empty non-significant bytes
187        let timeout_height_le_hex = lock_height.to_le_bytes().to_hex();
188        let timeout_height_le_hex_trimmed = timeout_height_le_hex.trim_end_matches("00");
189        let timeout_height_le_bytes = hex::decode(timeout_height_le_hex_trimmed)?;
190
191        Ok(Builder::new()
192            .push_opcode(opcodes::all::OP_SIZE)
193            .push_slice(&[0x20])
194            .push_opcode(opcodes::all::OP_EQUAL)
195            .push_opcode(opcodes::all::OP_IF)
196            .push_opcode(opcodes::all::OP_HASH160)
197            .push_slice(&ripemd160_hash[..])
198            .push_opcode(opcodes::all::OP_EQUALVERIFY)
199            .push_slice(&compressed_pub_key[..])
200            .push_opcode(opcodes::all::OP_ELSE)
201            .push_opcode(opcodes::all::OP_DROP)
202            .push_slice(&timeout_height_le_bytes)
203            .push_opcode(opcodes::all::OP_CLTV)
204            .push_opcode(opcodes::all::OP_DROP)
205            .push_slice(&sig[..])
206            .push_opcode(opcodes::all::OP_ENDIF)
207            .push_opcode(opcodes::all::OP_CHECKSIG)
208            .into_script())
209    }
210
211    /// Validates the redeem script and the lockup address
212    ///
213    /// ### Arguments
214    ///
215    /// * `received_lockup_address` - The lockup address, as received from Boltz in the create rev swap API response
216    /// * `network` - The network type which is one of (Bitcoin, Testnet, Signet, Regtest)
217    pub(crate) fn validate_redeem_script(
218        &self,
219        received_lockup_address: String,
220        network: Network,
221    ) -> ReverseSwapResult<()> {
222        let redeem_script_received = Script::from_hex(&self.redeem_script)?;
223        let asm = redeem_script_received.asm();
224        debug!("received asm = {asm:?}");
225
226        let sk = SecretKey::from_slice(&self.private_key)?;
227        let pk = PublicKey::from_secret_key(&Secp256k1::new(), &sk);
228
229        // The 18th asm element is the refund address, provided by the Boltz service
230        let asm_elements: Vec<&str> = asm.split(' ').collect();
231        let refund_address = asm_elements.get(18).unwrap_or(&"").to_string();
232        let refund_address_bytes = hex::decode(refund_address)?;
233
234        let redeem_script_expected = Self::build_expected_reverse_swap_script(
235            self.get_preimage_hash().to_vec(), // Preimage hash
236            pk.serialize().to_vec(),           // Compressed pubkey
237            refund_address_bytes,
238            self.timeout_block_height,
239        )?;
240        debug!("expected asm = {:?}", redeem_script_expected.asm());
241
242        match redeem_script_received.eq(&redeem_script_expected) {
243            true => {
244                let lockup_addr_expected = &received_lockup_address;
245                let lockup_addr_from_script =
246                    &Address::p2wsh(&redeem_script_received, network.into()).to_string();
247
248                match lockup_addr_from_script == lockup_addr_expected {
249                    true => Ok(()),
250                    false => Err(ReverseSwapError::UnexpectedLockupAddress),
251                }
252            }
253            false => Err(ReverseSwapError::UnexpectedRedeemScript),
254        }
255    }
256
257    pub(crate) fn validate_invoice_amount(
258        &self,
259        expected_amount_msat: u64,
260    ) -> ReverseSwapResult<()> {
261        let inv: crate::lightning_invoice::Bolt11Invoice = self.invoice.parse()?;
262
263        // Validate if received invoice has the same amount as requested by the user
264        let amount_from_invoice_msat = inv.amount_milli_satoshis().unwrap_or_default();
265        match amount_from_invoice_msat == expected_amount_msat {
266            false => Err(ReverseSwapError::unexpected_invoice_amount(
267                "Does not match the request",
268            )),
269            true => Ok(()),
270        }
271    }
272
273    /// Validates the received HODL invoice:
274    ///
275    /// - checks if amount matches the amount requested by the user
276    /// - checks if the payment hash is the same preimage hash (derived from local secret bytes)
277    ///   included in the create request
278    pub(crate) fn validate_invoice(&self, expected_amount_msat: u64) -> ReverseSwapResult<()> {
279        self.validate_invoice_amount(expected_amount_msat)?;
280
281        // Validate if received invoice has the same payment hash as the preimage hash in the request
282        let inv: crate::lightning_invoice::Bolt11Invoice = self.invoice.parse()?;
283        let preimage_hash_from_invoice = inv.payment_hash();
284        let preimage_hash_from_req = &self.get_preimage_hash();
285        match preimage_hash_from_invoice == preimage_hash_from_req {
286            false => Err(ReverseSwapError::unexpected_payment_hash(
287                "Does not match the request",
288            )),
289            true => Ok(()),
290        }
291    }
292
293    /// Derives the lockup address from the redeem script
294    pub(crate) fn get_lockup_address(&self, network: Network) -> ReverseSwapResult<Address> {
295        let redeem_script = Script::from_hex(&self.redeem_script)?;
296        Ok(Address::p2wsh(&redeem_script, network.into()))
297    }
298
299    /// Get the preimage hash sent in the create request
300    pub(crate) fn get_preimage_hash(&self) -> sha256::Hash {
301        sha256::Hash::hash(&self.preimage)
302    }
303
304    /// Get the user-facing info struct using cached values
305    pub(crate) fn get_reverse_swap_info_using_cached_values(&self) -> ReverseSwapInfo {
306        ReverseSwapInfo {
307            id: self.id.clone(),
308            claim_pubkey: self.claim_pubkey.clone(),
309            lockup_txid: self.cache.clone().lockup_txid,
310            claim_txid: self.cache.claim_txid.clone(),
311            onchain_amount_sat: self.onchain_amount_sat,
312            status: self.cache.status,
313        }
314    }
315}
316
317/// Simplified version of [FullReverseSwapInfo], containing only the user-relevant fields
318#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
319pub struct ReverseSwapInfo {
320    pub id: String,
321    pub claim_pubkey: String,
322    /// The lockup tx id, available from the moment the lockup tx is seen in the mempool by the SDK
323    pub lockup_txid: Option<String>,
324    /// The claim tx id, available from the moment the claim tx is broadcast by the SDK
325    pub claim_txid: Option<String>,
326    pub onchain_amount_sat: u64,
327    pub status: ReverseSwapStatus,
328}
329
330/// The possible statuses of a reverse swap, from the Breez SDK perspective.
331///
332/// See [BoltzApiReverseSwapStatus] for the reverse swap status from the Breez endpoint point of view.
333#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
334pub enum ReverseSwapStatus {
335    /// HODL invoice payment is not completed yet
336    ///
337    /// This is also the temporary status of a reverse swap when restoring a node, until `sync` finishes.
338    Initial = 0,
339
340    /// HODL invoice payment was successfully triggered and confirmed by Boltz, but the reverse swap
341    /// is not yet complete
342    InProgress = 1,
343
344    /// An explicit error occurs (validation error, failure reported by Boltz, expiration, etc) and
345    /// the initial invoice funds are returned to the sender (invoice is cancelled or payment failed)
346    Cancelled = 2,
347
348    /// Successfully completed (claim tx has been seen in the mempool)
349    CompletedSeen = 3,
350
351    /// Successfully completed (claim tx has at least one confirmation)
352    CompletedConfirmed = 4,
353}
354
355impl ReverseSwapStatus {
356    pub(crate) fn is_monitored_state(&self) -> bool {
357        matches!(
358            self,
359            ReverseSwapStatus::Initial
360                | ReverseSwapStatus::InProgress
361                | ReverseSwapStatus::CompletedSeen
362        )
363    }
364
365    pub(crate) fn is_blocking_state(&self) -> bool {
366        matches!(
367            self,
368            ReverseSwapStatus::Initial | ReverseSwapStatus::InProgress
369        )
370    }
371}
372
373impl TryFrom<i32> for ReverseSwapStatus {
374    type Error = anyhow::Error;
375
376    fn try_from(value: i32) -> Result<Self, Self::Error> {
377        match value {
378            0 => Ok(ReverseSwapStatus::Initial),
379            1 => Ok(ReverseSwapStatus::InProgress),
380            2 => Ok(ReverseSwapStatus::Cancelled),
381            3 => Ok(ReverseSwapStatus::CompletedSeen),
382            4 => Ok(ReverseSwapStatus::CompletedConfirmed),
383            _ => Err(anyhow!("illegal value")),
384        }
385    }
386}
387
388/// Trait covering Breez Server reverse swap functionality
389#[tonic::async_trait]
390pub(crate) trait ReverseSwapperRoutingAPI: Send + Sync {
391    async fn fetch_reverse_routing_node(&self) -> ReverseSwapResult<Vec<u8>>;
392}
393
394#[tonic::async_trait]
395impl ReverseSwapperRoutingAPI for BreezServer {
396    async fn fetch_reverse_routing_node(&self) -> ReverseSwapResult<Vec<u8>> {
397        let mut client = self.get_swapper_client().await;
398
399        Ok(with_connection_retry!(
400            client.get_reverse_routing_node(grpc::GetReverseRoutingNodeRequest::default())
401        )
402        .await
403        .map(|reply| reply.into_inner().node_id)?)
404    }
405}
406
407/// Trait covering reverse swap functionality on the external service
408#[tonic::async_trait]
409pub(crate) trait ReverseSwapServiceAPI: Send + Sync {
410    /// Lookup the most recent reverse swap pair info using the Boltz API. The fees are only valid
411    /// for a set amount of time.
412    async fn fetch_reverse_swap_fees(&self) -> ReverseSwapResult<ReverseSwapPairInfo>;
413
414    /// Creates a reverse submarine swap on the remote service (Boltz).
415    ///
416    /// # Arguments
417    ///
418    /// * `send_amount_sat` - Amount that is to be swapped
419    /// * `preimage_hash_hex` - Hex of preimage hash
420    /// * `claim_pubkey` - Pubkey of a keypair that can allow the SDK to claim the locked funds
421    /// * `pair_hash` - The hash of the exchange rate, looked-up before this call
422    /// * `routing_node` - Pubkey of a LN node used as routing hint
423    async fn create_reverse_swap_on_remote(
424        &self,
425        send_amount_sat: u64,
426        preimage_hash_hex: String,
427        claim_pubkey: String,
428        pair_hash: String,
429        routing_node: String,
430    ) -> ReverseSwapResult<BoltzApiCreateReverseSwapResponse>;
431
432    /// Performs a live lookup of the reverse swap's status on the Boltz API
433    async fn get_boltz_status(&self, id: String) -> ReverseSwapResult<BoltzApiReverseSwapStatus>;
434
435    /// Fetch the private route hints for the reverse swap node.
436    async fn get_route_hints(&self, routing_node_id: String) -> ReverseSwapResult<Vec<RouteHint>>;
437}
438
439/// Internal SDK log entry
440#[derive(Clone, Debug)]
441pub struct LogEntry {
442    pub line: String,
443    pub level: String,
444}
445
446/// Configuration for the Breez Services
447///
448/// Use [Config::production] or [Config::staging] for default configs of the different supported
449/// environments.
450#[derive(Clone)]
451pub struct Config {
452    pub breezserver: String,
453    pub chainnotifier_url: String,
454    /// If set, this is the mempool.space URL that will be used.
455    ///
456    /// If not set, a list of mempool.space URLs will be used to provide fault-tolerance. If calls
457    /// to the first URL fail, then the call will be repeated to the next URL, and so on.
458    ///
459    /// Note that, if specified, the URL has to be in the format: `https://mempool.space/api`
460    pub mempoolspace_url: Option<String>,
461    /// Directory in which all SDK files (DB, log) are stored. Defaults to ".", otherwise if it's customized,
462    /// the folder should exist before starting the SDK.
463    pub working_dir: String,
464    pub network: Network,
465    pub payment_timeout_sec: u32,
466    pub default_lsp_id: Option<String>,
467    pub api_key: Option<String>,
468    /// Maps to the CLN `maxfeepercent` config when paying invoices (`lightning-pay`)
469    pub maxfee_percent: f64,
470    /// Maps to the CLN `exemptfee` config when paying invoices (`lightning-pay`)
471    pub exemptfee_msat: u64,
472    pub node_config: NodeConfig,
473}
474
475impl Config {
476    pub fn production(api_key: String, node_config: NodeConfig) -> Self {
477        Config {
478            breezserver: PRODUCTION_BREEZSERVER_URL.to_string(),
479            chainnotifier_url: "https://chainnotifier.breez.technology".to_string(),
480            mempoolspace_url: None,
481            working_dir: ".".to_string(),
482            network: Bitcoin,
483            payment_timeout_sec: 60,
484            default_lsp_id: None,
485            api_key: Some(api_key),
486            maxfee_percent: 1.0,
487            exemptfee_msat: 20000,
488            node_config,
489        }
490    }
491
492    pub fn staging(api_key: String, node_config: NodeConfig) -> Self {
493        Config {
494            breezserver: STAGING_BREEZSERVER_URL.to_string(),
495            chainnotifier_url: "https://chainnotifier.breez.technology".to_string(),
496            mempoolspace_url: None,
497            working_dir: ".".to_string(),
498            network: Bitcoin,
499            payment_timeout_sec: 60,
500            default_lsp_id: None,
501            api_key: Some(api_key),
502            maxfee_percent: 0.5,
503            exemptfee_msat: 20000,
504            node_config,
505        }
506    }
507
508    pub fn regtest(api_key: String, node_config: NodeConfig) -> Self {
509        Config {
510            breezserver: REGTEST_BREEZSERVER_URL.to_string(),
511            chainnotifier_url: "https://chainnotifier.breez.technology".to_string(),
512            mempoolspace_url: Some(REGTEST_MEMPOOL_URL.to_string()),
513            working_dir: ".".to_string(),
514            network: Regtest,
515            payment_timeout_sec: 60,
516            default_lsp_id: None,
517            api_key: Some(api_key),
518            maxfee_percent: 0.5,
519            exemptfee_msat: 20000,
520            node_config,
521        }
522    }
523}
524
525#[derive(Clone)]
526pub enum NodeConfig {
527    Greenlight { config: GreenlightNodeConfig },
528}
529
530#[derive(Clone, Serialize)]
531pub enum NodeCredentials {
532    Greenlight {
533        credentials: GreenlightDeviceCredentials,
534    },
535}
536
537#[derive(Clone)]
538pub struct GreenlightNodeConfig {
539    pub partner_credentials: Option<GreenlightCredentials>,
540    pub invite_code: Option<String>,
541}
542
543/// Indicates the different kinds of supported environments for [crate::BreezServices].
544#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, EnumString)]
545pub enum EnvironmentType {
546    #[strum(serialize = "production")]
547    Production,
548    #[strum(serialize = "staging")]
549    Staging,
550    #[strum(serialize = "regtest")]
551    Regtest,
552}
553
554/// Client-specific credentials to connect to and manage a Greenlight node in the cloud
555#[derive(Clone, Serialize, Deserialize)]
556pub struct GreenlightCredentials {
557    pub developer_key: Vec<u8>,
558    pub developer_cert: Vec<u8>,
559}
560
561/// Device credentials used to authenticate to Greenlight with the current device.
562#[derive(Clone, Serialize, Deserialize)]
563pub struct GreenlightDeviceCredentials {
564    pub device: Vec<u8>,
565}
566
567/// Represents a configure node request.
568#[derive(Default)]
569pub struct ConfigureNodeRequest {
570    pub close_to_address: Option<String>,
571}
572
573/// Represents a connect request.
574pub struct ConnectRequest {
575    pub config: Config,
576    pub seed: Vec<u8>,
577    /// If true, only restores an existing node and otherwise result in an error
578    pub restore_only: Option<bool>,
579}
580
581/// Different types of supported filters which can be applied when retrieving the transaction list
582#[derive(PartialEq)]
583pub enum PaymentTypeFilter {
584    Sent,
585    Received,
586    ClosedChannel,
587}
588
589/// A metadata filter which can be applied when retrieving the transaction list
590pub struct MetadataFilter {
591    /// Specifies which field to apply the filter on, using the JSON path format
592    pub json_path: String,
593    /// Specifies which JSON value to filter for.
594    /// As such, strings must be wrapped with quotes ("") in order to be properly filtered
595    pub json_value: String,
596}
597
598/// Different types of supported feerates
599pub enum FeeratePreset {
600    Regular,
601    Economy,
602    Priority,
603}
604
605impl TryFrom<i32> for FeeratePreset {
606    type Error = anyhow::Error;
607
608    fn try_from(value: i32) -> std::result::Result<Self, Self::Error> {
609        match value {
610            0 => Ok(FeeratePreset::Regular),
611            1 => Ok(FeeratePreset::Economy),
612            2 => Ok(FeeratePreset::Priority),
613            _ => Err(anyhow!("Unexpected feerate enum value")),
614        }
615    }
616}
617
618#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
619pub struct BackupStatus {
620    pub backed_up: bool,
621    /// Epoch time, in seconds
622    pub last_backup_time: Option<u64>,
623}
624
625/// The node state of a Greenlight LN node running in the cloud.
626///
627/// Note: The implementation attempts to provide the most up-to-date values,
628/// which may result in some short-lived inconsistencies
629/// (e.g., `channels_balance_msat` may be updated before `inbound_liquidity_msats`).
630#[derive(Serialize, Default, Deserialize, Clone, PartialEq, Eq, Debug)]
631pub struct NodeState {
632    pub id: String,
633    pub block_height: u32,
634    pub channels_balance_msat: u64,
635    pub onchain_balance_msat: u64,
636    #[serde(default)]
637    pub pending_onchain_balance_msat: u64,
638
639    #[serde(default)]
640    pub utxos: Vec<UnspentTransactionOutput>,
641    pub max_payable_msat: u64,
642    pub max_receivable_msat: u64,
643    pub max_single_payment_amount_msat: u64,
644    pub max_chan_reserve_msats: u64,
645    pub connected_peers: Vec<String>,
646
647    /// Maximum receivable in a single payment without requiring a new channel open.
648    pub max_receivable_single_payment_amount_msat: u64,
649
650    /// Total receivable on all available channels
651    pub total_inbound_liquidity_msats: u64,
652}
653
654/// Internal response to a [crate::node_api::NodeAPI::pull_changed] call
655pub struct SyncResponse {
656    pub sync_state: Value,
657    pub node_state: NodeState,
658    pub payments: Vec<crate::models::Payment>,
659    pub channels: Vec<crate::models::Channel>,
660}
661
662/// The status of a payment
663#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
664pub enum PaymentStatus {
665    #[default]
666    Pending = 0,
667    Complete = 1,
668    Failed = 2,
669}
670
671/// Represents a payment, including its [PaymentType] and [PaymentDetails]
672#[derive(Default, PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
673pub struct Payment {
674    pub id: String,
675    pub payment_type: PaymentType,
676    /// Epoch time, in seconds
677    pub payment_time: i64,
678    pub amount_msat: u64,
679    pub fee_msat: u64,
680    pub status: PaymentStatus,
681    pub error: Option<String>,
682    pub description: Option<String>,
683    pub details: PaymentDetails,
684    pub metadata: Option<String>,
685}
686
687/// Represents a payments external information.
688#[derive(Default)]
689pub struct PaymentExternalInfo {
690    pub lnurl_pay_success_action: Option<SuccessActionProcessed>,
691    pub lnurl_pay_domain: Option<String>,
692    pub lnurl_pay_comment: Option<String>,
693    pub lnurl_metadata: Option<String>,
694    pub ln_address: Option<String>,
695    pub lnurl_withdraw_endpoint: Option<String>,
696    pub attempted_amount_msat: Option<u64>,
697    pub attempted_error: Option<String>,
698}
699
700/// Represents a list payments request.
701#[derive(Default)]
702pub struct ListPaymentsRequest {
703    pub filters: Option<Vec<PaymentTypeFilter>>,
704    pub metadata_filters: Option<Vec<MetadataFilter>>,
705    /// Epoch time, in seconds
706    pub from_timestamp: Option<i64>,
707    /// Epoch time, in seconds
708    pub to_timestamp: Option<i64>,
709    pub include_failures: Option<bool>,
710    pub offset: Option<u32>,
711    pub limit: Option<u32>,
712}
713
714/// Represents a payment response.
715#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
716pub struct PaymentResponse {
717    /// Epoch time, in seconds
718    pub payment_time: i64,
719    pub amount_msat: u64,
720    pub fee_msat: u64,
721    pub payment_hash: String,
722    pub payment_preimage: String,
723}
724
725/// Wrapper for the different types of payments
726#[allow(clippy::large_enum_variant)]
727#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
728#[serde(untagged)]
729pub enum PaymentDetails {
730    Ln {
731        #[serde(flatten)]
732        data: LnPaymentDetails,
733    },
734    ClosedChannel {
735        #[serde(flatten)]
736        data: ClosedChannelPaymentDetails,
737    },
738}
739
740impl Default for PaymentDetails {
741    fn default() -> Self {
742        Self::Ln {
743            data: LnPaymentDetails::default(),
744        }
745    }
746}
747
748impl PaymentDetails {
749    pub fn add_pending_expiration_block(&mut self, htlc: Htlc) {
750        if let PaymentDetails::Ln { data } = self {
751            data.pending_expiration_block = Some(htlc.expiry)
752        }
753    }
754}
755
756/// Details of a LN payment, as included in a [Payment]
757#[derive(Default, PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
758pub struct LnPaymentDetails {
759    pub payment_hash: String,
760    pub label: String,
761    pub destination_pubkey: String,
762    pub payment_preimage: String,
763    pub keysend: bool,
764    pub bolt11: String,
765
766    /// Only set for [PaymentType::Received], payments which require to open a channel.
767    /// Represents the actual invoice paid by the sender
768    pub open_channel_bolt11: Option<String>,
769
770    /// Only set for [PaymentType::Sent] payments that are part of a LNURL-pay workflow where
771    /// the endpoint returns a success action
772    pub lnurl_success_action: Option<SuccessActionProcessed>,
773
774    /// Only set for [PaymentType::Sent] payments if it is not a payment to a Lightning Address
775    pub lnurl_pay_domain: Option<String>,
776
777    /// Only set for [PaymentType::Sent] payments if the user sent the comment using LNURL-pay
778    pub lnurl_pay_comment: Option<String>,
779
780    /// Only set for [PaymentType::Sent] payments that are sent to a Lightning Address
781    pub ln_address: Option<String>,
782
783    /// Only set for [PaymentType::Sent] payments where the receiver endpoint returned LNURL metadata
784    pub lnurl_metadata: Option<String>,
785
786    /// Only set for [PaymentType::Received] payments that were received as part of LNURL-withdraw
787    pub lnurl_withdraw_endpoint: Option<String>,
788
789    /// Only set for [PaymentType::Received] payments that were received in the context of a swap
790    pub swap_info: Option<SwapInfo>,
791
792    /// Only set for [PaymentType::Sent] payments that were sent in the context of a reverse swap
793    pub reverse_swap_info: Option<ReverseSwapInfo>,
794
795    /// Only set for [PaymentStatus::Pending] payments that are inflight.
796    pub pending_expiration_block: Option<u32>,
797}
798
799/// Represents the funds that were on the user side of the channel at the time it was closed.
800#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
801pub struct ClosedChannelPaymentDetails {
802    pub state: ChannelState,
803    pub funding_txid: String,
804    pub short_channel_id: Option<String>,
805    /// Can be empty for older closed channels.
806    pub closing_txid: Option<String>,
807}
808
809#[derive(Clone, Debug, Default, Serialize, Deserialize)]
810pub struct ReverseSwapFeesRequest {
811    /// Amount to be sent
812    pub send_amount_sat: Option<u64>,
813    /// Feerate (sat / vByte) for the claim transaction
814    pub claim_tx_feerate: Option<u32>,
815}
816
817#[derive(Clone, Debug, Serialize, Deserialize)]
818pub struct MaxChannelAmount {
819    /// The channel id.
820    pub channel_id: String,
821    /// The max amount can be sent from this channel.
822    pub amount_msat: u64,
823    /// The payment path to be used for the maximum amount.
824    pub path: PaymentPath,
825}
826
827/// Represents a receive payment request.
828#[derive(Clone, Debug, Default, Serialize, Deserialize)]
829pub struct ReceivePaymentRequest {
830    /// The amount in satoshis for this payment request
831    pub amount_msat: u64,
832    /// The description for this payment request.
833    pub description: String,
834    /// Optional preimage for this payment request.
835    /// If specified, it will be used instead of generating a new one.
836    pub preimage: Option<Vec<u8>>,
837    /// If set and valid, these fess options are used when a new channels is needed.
838    /// Otherwise the default fee options will be used.
839    pub opening_fee_params: Option<OpeningFeeParams>,
840    /// If set to true, then the bolt11 invoice returned includes the description hash.
841    pub use_description_hash: Option<bool>,
842    /// if specified, set the time the invoice is valid for, in seconds.
843    pub expiry: Option<u32>,
844    /// if specified, sets the min_final_cltv_expiry for the invoice
845    pub cltv: Option<u32>,
846}
847
848/// Represents a receive payment response.
849///
850/// Breez SDK may have to open a new channel to receive this payment. In that case, the channel will
851/// be opened automatically when the invoice is paid, and the fees will be described in the
852/// `opening_fee_params` and `opening_fee_msat` fields.
853#[derive(Clone, Debug, Serialize, Deserialize)]
854pub struct ReceivePaymentResponse {
855    /// The generated invoice, including any necessary routing hints
856    pub ln_invoice: LNInvoice,
857    /// If set, these are the [OpeningFeeParams] used to calculate the channel opening fees.
858    pub opening_fee_params: Option<OpeningFeeParams>,
859    /// If set, this is the channel opening fee that will be deduced from the invoice amount.
860    pub opening_fee_msat: Option<u64>,
861}
862
863/// Represents a send payment request.
864#[derive(Clone, Debug, Serialize, Deserialize)]
865pub struct SendPaymentRequest {
866    /// The bolt11 invoice
867    pub bolt11: String,
868    /// Trampoline payments outsource pathfinding to the LSP. Trampoline payments can improve
869    /// payment performance, but are generally more expensive in terms of fees and they
870    /// compromise on privacy.
871    pub use_trampoline: bool,
872    /// The amount to pay in millisatoshis. Should only be set when `bolt11` is a zero-amount invoice.
873    pub amount_msat: Option<u64>,
874    /// The external label or identifier of the [Payment]
875    pub label: Option<String>,
876}
877
878/// Represents a TLV entry for a keysend payment.
879#[derive(Clone, Debug, Serialize, Deserialize)]
880pub struct TlvEntry {
881    /// The type field for the TLV
882    pub field_number: u64,
883    /// The value bytes for the TLV
884    pub value: Vec<u8>,
885}
886
887/// Represents a send spontaneous payment request.
888#[derive(Clone, Debug, Serialize, Deserialize)]
889pub struct SendSpontaneousPaymentRequest {
890    /// The node id to send this payment is
891    pub node_id: String,
892    /// The amount in millisatoshis for this payment
893    pub amount_msat: u64,
894    // Optional extra TLVs
895    pub extra_tlvs: Option<Vec<TlvEntry>>,
896    /// The external label or identifier of the [Payment]
897    pub label: Option<String>,
898}
899
900/// Represents a send payment response.
901#[derive(Clone, Debug, Serialize, Deserialize)]
902pub struct SendPaymentResponse {
903    pub payment: Payment,
904}
905
906#[derive(Clone, Debug, Serialize, Deserialize)]
907pub struct ReportPaymentFailureDetails {
908    /// The payment hash of the payment failure
909    pub payment_hash: String,
910    /// The comment or error text
911    pub comment: Option<String>,
912}
913
914/// Represents a report issue request.
915#[derive(Clone, Debug, Serialize, Deserialize)]
916pub enum ReportIssueRequest {
917    PaymentFailure { data: ReportPaymentFailureDetails },
918}
919
920/// Indicates the different service health check statuses.
921#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
922pub enum HealthCheckStatus {
923    Operational,
924    Maintenance,
925    ServiceDisruption,
926}
927
928/// Represents a service health check response.
929#[derive(Clone, Debug, Serialize, Deserialize)]
930pub struct ServiceHealthCheckResponse {
931    pub status: HealthCheckStatus,
932}
933
934/// Trait covering support-related functionality
935#[tonic::async_trait]
936pub trait SupportAPI: Send + Sync {
937    async fn service_health_check(&self) -> SdkResult<ServiceHealthCheckResponse>;
938
939    async fn report_payment_failure(
940        &self,
941        node_state: NodeState,
942        payment: Payment,
943        lsp_id: Option<String>,
944        comment: Option<String>,
945    ) -> SdkResult<()>;
946}
947
948#[derive(Clone)]
949pub struct StaticBackupRequest {
950    pub working_dir: String,
951}
952
953#[derive(Clone, Debug, Serialize, Deserialize)]
954pub struct StaticBackupResponse {
955    pub backup: Option<Vec<String>>,
956}
957
958#[derive(Default)]
959pub struct OpenChannelFeeRequest {
960    pub amount_msat: Option<u64>,
961    pub expiry: Option<u32>,
962}
963
964#[derive(Clone, Debug, Serialize, Deserialize)]
965pub struct OpenChannelFeeResponse {
966    /// Opening fee for receiving the amount set in the [OpenChannelFeeRequest], in case it was set.
967    /// It may be zero if no new channel needs to be opened.
968    pub fee_msat: Option<u64>,
969    /// The fee params for receiving more than the current inbound liquidity.
970    pub fee_params: OpeningFeeParams,
971}
972
973#[derive(Clone, Debug, Default, Serialize, Deserialize)]
974pub struct ReceiveOnchainRequest {
975    pub opening_fee_params: Option<OpeningFeeParams>,
976}
977
978#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
979pub struct ListSwapsRequest {
980    pub status: Option<Vec<SwapStatus>>,
981    /// Epoch time, in seconds. If set, acts as filter for minimum swap creation time, inclusive.
982    pub from_timestamp: Option<i64>,
983    /// Epoch time, in seconds. If set, acts as filter for maximum swap creation time, exclusive.
984    pub to_timestamp: Option<i64>,
985    pub offset: Option<u32>,
986    pub limit: Option<u32>,
987}
988
989#[derive(Clone, Debug, Serialize, Deserialize)]
990pub struct BuyBitcoinRequest {
991    pub provider: BuyBitcoinProvider,
992    pub opening_fee_params: Option<OpeningFeeParams>,
993    /// The optional URL to redirect to after completing the buy.
994    ///
995    /// For Moonpay, see <https://dev.moonpay.com/docs/on-ramp-configure-user-journey-params>
996    pub redirect_url: Option<String>,
997}
998
999#[derive(Clone, Debug, Serialize, Deserialize)]
1000pub struct BuyBitcoinResponse {
1001    pub url: String,
1002    pub opening_fee_params: Option<OpeningFeeParams>,
1003}
1004
1005#[derive(Clone, Debug, Serialize, Deserialize)]
1006pub struct RedeemOnchainFundsRequest {
1007    pub to_address: String,
1008    pub sat_per_vbyte: u32,
1009}
1010
1011#[derive(Clone, Debug, Serialize, Deserialize)]
1012pub struct RedeemOnchainFundsResponse {
1013    pub txid: Vec<u8>,
1014}
1015
1016pub enum SwapAmountType {
1017    Send,
1018    Receive,
1019}
1020
1021/// See [ReverseSwapFeesRequest]
1022pub struct PrepareOnchainPaymentRequest {
1023    /// Depending on `amount_type`, this may be the desired send amount or the desired receive amount.
1024    pub amount_sat: u64,
1025    pub amount_type: SwapAmountType,
1026
1027    /// Feerate (sat / vByte) for the claim transaction
1028    pub claim_tx_feerate: u32,
1029}
1030
1031#[derive(Serialize)]
1032pub struct OnchainPaymentLimitsResponse {
1033    /// Minimum amount the reverse swap service accepts as a send amount
1034    pub min_sat: u64,
1035    /// Maximum amount the reverse swap service accepts as a send amount
1036    pub max_sat: u64,
1037    /// Maximum amount this node can send with the current channels and the current local balance
1038    pub max_payable_sat: u64,
1039}
1040
1041/// Contains fields describing the reverse swap parameters (see [ReverseSwapPairInfo]), as well as
1042/// the resulting send and receive amounts.
1043#[derive(Clone, Debug, Serialize)]
1044pub struct PrepareOnchainPaymentResponse {
1045    pub fees_hash: String,
1046    pub fees_percentage: f64,
1047    pub fees_lockup: u64,
1048    pub fees_claim: u64,
1049
1050    pub sender_amount_sat: u64,
1051    pub recipient_amount_sat: u64,
1052    pub total_fees: u64,
1053}
1054
1055#[derive(Clone, Debug)]
1056pub struct PayOnchainRequest {
1057    pub recipient_address: String,
1058    pub prepare_res: PrepareOnchainPaymentResponse,
1059}
1060
1061#[derive(Serialize)]
1062pub struct PayOnchainResponse {
1063    pub reverse_swap_info: ReverseSwapInfo,
1064}
1065
1066pub struct PrepareRefundRequest {
1067    pub swap_address: String,
1068    pub to_address: String,
1069    pub sat_per_vbyte: u32,
1070    pub unilateral: Option<bool>,
1071}
1072
1073pub struct RefundRequest {
1074    pub swap_address: String,
1075    pub to_address: String,
1076    pub sat_per_vbyte: u32,
1077    pub unilateral: Option<bool>,
1078}
1079
1080pub struct PrepareRefundResponse {
1081    pub refund_tx_weight: u32,
1082    pub refund_tx_fee_sat: u64,
1083}
1084
1085pub struct RefundResponse {
1086    pub refund_tx_id: String,
1087}
1088
1089/// Dynamic fee parameters offered by the LSP for opening a new channel.
1090///
1091/// After they are received, the client shouldn't change them when calling LSP methods,
1092/// otherwise the LSP may reject the call.
1093#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
1094pub struct OpeningFeeParams {
1095    /// The minimum value in millisatoshi we will require for incoming HTLCs on the channel
1096    pub min_msat: u64,
1097    /// The fee in ppm charged over liquidity when buying a channel
1098    pub proportional: u32,
1099    /// The date and time this opening fee params promise expires, in RFC 3339 / ISO 8601 format
1100    pub valid_until: String,
1101    /// The channel can be closed if not used within this duration in blocks
1102    pub max_idle_time: u32,
1103    pub max_client_to_self_delay: u32,
1104    pub promise: String,
1105}
1106
1107impl OpeningFeeParams {
1108    pub(crate) fn valid_until_date(&self) -> Result<DateTime<Utc>> {
1109        Ok(DateTime::parse_from_rfc3339(&self.valid_until).map(|d| d.with_timezone(&Utc))?)
1110    }
1111
1112    pub(crate) fn valid_for(&self, expiry: u32) -> Result<bool> {
1113        Ok(self.valid_until_date()? > Utc::now().add(Duration::seconds(expiry as i64)))
1114    }
1115
1116    pub(crate) fn get_channel_fees_msat_for(&self, amount_msats: u64) -> u64 {
1117        let lsp_fee_msat = amount_msats * self.proportional as u64 / 1_000_000;
1118        let lsp_fee_msat_rounded_to_sat = lsp_fee_msat / 1000 * 1000;
1119
1120        max(lsp_fee_msat_rounded_to_sat, self.min_msat)
1121    }
1122}
1123
1124impl From<OpeningFeeParams> for grpc::OpeningFeeParams {
1125    fn from(ofp: OpeningFeeParams) -> Self {
1126        Self {
1127            min_msat: ofp.min_msat,
1128            proportional: ofp.proportional,
1129            valid_until: ofp.valid_until,
1130            max_idle_time: ofp.max_idle_time,
1131            max_client_to_self_delay: ofp.max_client_to_self_delay,
1132            promise: ofp.promise,
1133        }
1134    }
1135}
1136
1137impl From<grpc::OpeningFeeParams> for OpeningFeeParams {
1138    fn from(gofp: grpc::OpeningFeeParams) -> Self {
1139        Self {
1140            min_msat: gofp.min_msat,
1141            proportional: gofp.proportional,
1142            valid_until: gofp.valid_until,
1143            max_idle_time: gofp.max_idle_time,
1144            max_client_to_self_delay: gofp.max_client_to_self_delay,
1145            promise: gofp.promise,
1146        }
1147    }
1148}
1149
1150impl FromSql for OpeningFeeParams {
1151    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1152        serde_json::from_str(value.as_str()?).map_err(|_| FromSqlError::InvalidType)
1153    }
1154}
1155
1156impl ToSql for OpeningFeeParams {
1157    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1158        Ok(ToSqlOutput::from(
1159            serde_json::to_string(&self).map_err(|_| FromSqlError::InvalidType)?,
1160        ))
1161    }
1162}
1163
1164pub enum DynamicFeeType {
1165    Cheapest,
1166    Longest,
1167}
1168
1169/// See [OpeningFeeParamsMenu::try_from]
1170#[derive(Debug, Clone, Serialize, Deserialize)]
1171pub struct OpeningFeeParamsMenu {
1172    pub values: Vec<OpeningFeeParams>,
1173}
1174
1175impl OpeningFeeParamsMenu {
1176    /// Initializes and validates itself.
1177    ///
1178    /// This struct should not be persisted as such, because validation happens dynamically based on
1179    /// the current time. At a later point in time, any previously-validated [OpeningFeeParamsMenu]
1180    /// could be invalid. Therefore, the [OpeningFeeParamsMenu] should always be initialized on-the-fly.
1181    pub fn try_from(values: Vec<sdk_common::grpc::OpeningFeeParams>) -> Result<Self> {
1182        let temp = Self {
1183            values: values
1184                .into_iter()
1185                .map(|ofp| ofp.into())
1186                .collect::<Vec<OpeningFeeParams>>(),
1187        };
1188        temp.validate().map(|_| temp)
1189    }
1190
1191    fn validate(&self) -> Result<()> {
1192        // values must be in ascending order
1193        let is_ordered = self.values.windows(2).all(|ofp| {
1194            let larger_min_msat_fee = ofp[0].min_msat < ofp[1].min_msat;
1195            let equal_min_msat_fee = ofp[0].min_msat == ofp[1].min_msat;
1196
1197            let larger_proportional = ofp[0].proportional < ofp[1].proportional;
1198            let equal_proportional = ofp[0].proportional == ofp[1].proportional;
1199
1200            (larger_min_msat_fee && equal_proportional)
1201                || (equal_min_msat_fee && larger_proportional)
1202                || (larger_min_msat_fee && larger_proportional)
1203        });
1204        ensure!(is_ordered, "Validation failed: fee params are not ordered");
1205
1206        // Menu must not contain any item with `valid_until` in the past
1207        let is_expired = self.values.iter().any(|ofp| match ofp.valid_until_date() {
1208            Ok(valid_until) => Utc::now() > valid_until,
1209            Err(_) => {
1210                warn!("Failed to parse valid_until for OpeningFeeParams: {ofp:?}");
1211                false
1212            }
1213        });
1214        ensure!(!is_expired, "Validation failed: expired fee params found");
1215
1216        Ok(())
1217    }
1218
1219    pub fn get_cheapest_opening_fee_params(&self) -> Result<OpeningFeeParams> {
1220        self.values.first().cloned().ok_or_else(|| {
1221            anyhow!("The LSP doesn't support opening new channels: Dynamic fees menu contains no values")
1222        })
1223    }
1224
1225    pub fn get_48h_opening_fee_params(&self) -> Result<OpeningFeeParams> {
1226        // Find the fee params that are valid for at least 48h
1227        let now = Utc::now();
1228        let duration_48h = chrono::Duration::hours(48);
1229        let valid_min_48h: Vec<OpeningFeeParams> = self
1230            .values
1231            .iter()
1232            .filter(|ofp| match ofp.valid_until_date() {
1233                Ok(valid_until) => valid_until - now > duration_48h,
1234                Err(_) => {
1235                    warn!("Failed to parse valid_until for OpeningFeeParams: {ofp:?}");
1236                    false
1237                }
1238            })
1239            .cloned()
1240            .collect();
1241
1242        // Of those, return the first, which is the cheapest
1243        // (sorting order of fee params list was checked when the menu was initialized)
1244        valid_min_48h.first().cloned().ok_or_else(|| {
1245            anyhow!("The LSP doesn't support opening fees that are valid for at least 48 hours")
1246        })
1247    }
1248}
1249
1250/// Lightning channel
1251#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
1252pub struct Channel {
1253    pub funding_txid: String,
1254    pub short_channel_id: Option<String>,
1255    pub state: ChannelState,
1256    pub spendable_msat: u64,
1257    pub local_balance_msat: u64,
1258    pub receivable_msat: u64,
1259    pub closed_at: Option<u64>,
1260    /// The output number of the funding tx which opened the channel
1261    pub funding_outnum: Option<u32>,
1262    pub alias_local: Option<String>,
1263    pub alias_remote: Option<String>,
1264    /// Only set for closed channels.
1265    ///
1266    /// This may be empty for older closed channels, if it was not possible to retrieve the closing txid.
1267    pub closing_txid: Option<String>,
1268
1269    pub htlcs: Vec<Htlc>,
1270}
1271
1272#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
1273pub struct Htlc {
1274    pub expiry: u32,
1275    pub payment_hash: Vec<u8>,
1276}
1277
1278impl Htlc {
1279    pub fn from(expiry: u32, payment_hash: Vec<u8>) -> Self {
1280        Htlc {
1281            expiry,
1282            payment_hash,
1283        }
1284    }
1285}
1286
1287/// State of a Lightning channel
1288#[derive(Clone, PartialEq, Eq, Debug, EnumString, Display, Deserialize, Serialize)]
1289pub enum ChannelState {
1290    PendingOpen,
1291    Opened,
1292    PendingClose,
1293    Closed,
1294}
1295
1296/// The status of a swap
1297#[derive(Copy, Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize)]
1298pub enum SwapStatus {
1299    /// The swap address has been created and either there aren't any confirmed transactions associated with it
1300    /// or there are confirmed transactions that are bellow the lock timeout which means the funds are still
1301    /// eligible to be redeemed normally.
1302    #[default]
1303    Initial = 0,
1304
1305    WaitingConfirmation = 1,
1306
1307    Redeemable = 2,
1308
1309    Redeemed = 3,
1310
1311    /// The swap address has confirmed transactions associated with it and the lock timeout has passed since
1312    /// the earliest confirmed transaction. This means the only way to spend the funds from this address is by
1313    /// broadcasting a refund transaction.
1314    Refundable = 4,
1315
1316    Completed = 5,
1317}
1318
1319impl SwapStatus {
1320    pub(crate) fn unused() -> Vec<SwapStatus> {
1321        vec![SwapStatus::Initial]
1322    }
1323
1324    pub(crate) fn in_progress() -> Vec<SwapStatus> {
1325        vec![SwapStatus::Redeemable, SwapStatus::WaitingConfirmation]
1326    }
1327
1328    pub(crate) fn redeemable() -> Vec<SwapStatus> {
1329        vec![SwapStatus::Redeemable]
1330    }
1331
1332    pub(crate) fn refundable() -> Vec<SwapStatus> {
1333        vec![SwapStatus::Refundable]
1334    }
1335
1336    pub(crate) fn monitored() -> Vec<SwapStatus> {
1337        vec![
1338            SwapStatus::Initial,
1339            SwapStatus::WaitingConfirmation,
1340            SwapStatus::Redeemable,
1341            SwapStatus::Redeemed,
1342            SwapStatus::Refundable,
1343        ]
1344    }
1345
1346    pub(crate) fn unexpired() -> Vec<SwapStatus> {
1347        vec![
1348            SwapStatus::Initial,
1349            SwapStatus::WaitingConfirmation,
1350            SwapStatus::Redeemable,
1351            SwapStatus::Redeemed,
1352        ]
1353    }
1354}
1355
1356impl TryFrom<i32> for SwapStatus {
1357    type Error = anyhow::Error;
1358
1359    fn try_from(value: i32) -> Result<Self, Self::Error> {
1360        match value {
1361            0 => Ok(SwapStatus::Initial),
1362            1 => Ok(SwapStatus::WaitingConfirmation),
1363            2 => Ok(SwapStatus::Redeemable),
1364            3 => Ok(SwapStatus::Redeemed),
1365            4 => Ok(SwapStatus::Refundable),
1366            5 => Ok(SwapStatus::Completed),
1367            _ => Err(anyhow!("illegal value")),
1368        }
1369    }
1370}
1371
1372/// Represents the details of an on-going swap.
1373///
1374/// Once this SwapInfo is created it will be monitored on-chain and its state is
1375/// saved to the persistent storage.
1376///
1377/// The SwapInfo has a status which changes accordingly, documented in [SwapStatus].
1378///
1379
1380#[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize)]
1381pub struct SwapInfo {
1382    /// Bitcoin address for this swap. Sats sent to this address will be swapped.
1383    pub bitcoin_address: String,
1384    /// Relative time lock start, received from [SwapperAPI::create_swap].
1385    pub created_at: i64,
1386    /// Relative time lock for the timeout for the script to be redeemed before swap fails.
1387    pub lock_height: i64,
1388    /// sha256 hash of preimage to used in the claim sript.
1389    pub payment_hash: Vec<u8>,
1390    /// Secret to claim the swap.
1391    pub preimage: Vec<u8>,
1392    /// Secret claim key for the bitcoin address.
1393    pub private_key: Vec<u8>,
1394    /// Public key in binary format of the private claim private key.
1395    pub public_key: Vec<u8>,
1396    /// The public key in binary format from the swapping service. Received from [SwapperAPI::create_swap].
1397    pub swapper_public_key: Vec<u8>,
1398    /// The locking script for the generated bitcoin address. Received from [SwapperAPI::create_swap].
1399    pub script: Vec<u8>,
1400
1401    /// bolt11 invoice to claim the sent funds.
1402    pub bolt11: Option<String>,
1403    /// Amount of millisatoshis claimed from sent funds and paid for via bolt11 invoice.
1404    pub paid_msat: u64,
1405    /// Total count of transactions sent to the swap address.
1406    pub total_incoming_txs: u64,
1407    /// Confirmed onchain sats to be claim with an bolt11 invoice or refunded if swap fails.
1408    pub confirmed_sats: u64,
1409    /// Unconfirmed sats waiting to be confirmed onchain.
1410    pub unconfirmed_sats: u64,
1411    /// Shows the current status of the swap, either `Initial` or `Expired`.
1412    pub status: SwapStatus,
1413    /// Transaction IDs for failed swap attempts.
1414    pub refund_tx_ids: Vec<String>,
1415    /// Refund transaction IDs for ongoing swap awaiting confirmation.
1416    pub unconfirmed_tx_ids: Vec<String>,
1417    /// Transaction IDs that have been confirmed on-chain.
1418    pub confirmed_tx_ids: Vec<String>,
1419    /// The minimum amount of sats one can send in order for the swap to succeed. Received from [SwapperAPI::create_swap].
1420    pub min_allowed_deposit: i64,
1421    /// The maximum amount of sats one can send in order for the swap to succeed. This is determined based on `max_swapper_payable` and the node's local balance.
1422    pub max_allowed_deposit: i64,
1423    /// The absolute maximum value payable by the swapper. Received from [SwapperAPI::create_swap].
1424    pub max_swapper_payable: i64,
1425    /// Error reason for when swap fails.
1426    pub last_redeem_error: Option<String>,
1427    /// The dynamic fees which is set if a channel opening is needed.
1428    ///
1429    /// This is an optional field for backward compatibility with swaps created before dynamic fees.
1430    ///
1431    /// Swaps created after dynamic fees were introduced always have this field set.
1432    pub channel_opening_fees: Option<OpeningFeeParams>,
1433    /// The block height when the swap was confirmed.
1434    pub confirmed_at: Option<u32>,
1435}
1436
1437/// UTXO known to the LN node
1438#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
1439pub struct UnspentTransactionOutput {
1440    pub txid: Vec<u8>,
1441    pub outnum: u32,
1442    pub amount_millisatoshi: u64,
1443    pub address: String,
1444    #[serde(default)]
1445    pub reserved: bool,
1446}
1447
1448/// Different providers will demand different behaviours when the user is trying to buy bitcoin.
1449#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
1450#[serde(tag = "buy_bitcoin_provider")]
1451pub enum BuyBitcoinProvider {
1452    Moonpay,
1453}
1454
1455/// We need to prepare a redeem_onchain_funds transaction to know what fee will be charged in satoshis.
1456/// This model holds the request data which consists of the address to redeem on-chain funds to and the fee rate in.
1457/// satoshis per vbyte which will be converted to absolute satoshis.
1458#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
1459pub struct PrepareRedeemOnchainFundsRequest {
1460    pub to_address: String,
1461    pub sat_per_vbyte: u32,
1462}
1463
1464/// We need to prepare a redeem_onchain_funds transaction to know what a fee it will be charged in satoshis
1465/// this model holds the response data, which consists of the weight and the absolute fee in sats
1466#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
1467pub struct PrepareRedeemOnchainFundsResponse {
1468    pub tx_weight: u64,
1469    pub tx_fee_sat: u64,
1470}
1471
1472impl FromStr for BuyBitcoinProvider {
1473    type Err = anyhow::Error;
1474
1475    fn from_str(s: &str) -> Result<Self, Self::Err> {
1476        match s {
1477            "moonpay" => Ok(BuyBitcoinProvider::Moonpay),
1478            _ => Err(anyhow!("unknown buy bitcoin provider")),
1479        }
1480    }
1481}
1482
1483#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
1484pub struct PaymentPath {
1485    pub edges: Vec<PaymentPathEdge>,
1486}
1487
1488impl PaymentPath {
1489    pub fn final_hop_amount(&self, first_hop_amount_msat: u64) -> u64 {
1490        let mut max_to_send = first_hop_amount_msat;
1491        for h in self.edges.iter().skip(1) {
1492            max_to_send = h.amount_to_forward(max_to_send);
1493        }
1494        max_to_send
1495    }
1496
1497    pub fn first_hop_amount(&self, final_hop_amount_msat: u64) -> u64 {
1498        let mut first_hop_amount = final_hop_amount_msat;
1499        for h in self.edges.iter().skip(1).rev() {
1500            first_hop_amount = h.amount_from_forward(first_hop_amount);
1501        }
1502        first_hop_amount
1503    }
1504}
1505
1506#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
1507pub struct PaymentPathEdge {
1508    pub node_id: Vec<u8>,
1509    pub short_channel_id: String,
1510    pub channel_delay: u64,
1511    pub base_fee_msat: u64,
1512    pub fee_per_millionth: u64,
1513}
1514
1515impl PaymentPathEdge {
1516    pub(crate) fn amount_to_forward(&self, in_amount_msat: u64) -> u64 {
1517        let amount_to_forward = Self::divide_ceil(
1518            1_000_000 * (in_amount_msat - self.base_fee_msat),
1519            1_000_000 + self.fee_per_millionth,
1520        );
1521
1522        info!("amount_to_forward: in_amount_msat = {in_amount_msat},base_fee_msat={}, fee_per_millionth={}  amount_to_forward: {}", self.base_fee_msat, self.fee_per_millionth, amount_to_forward);
1523        amount_to_forward
1524    }
1525
1526    pub(crate) fn amount_from_forward(&self, forward_amount_msat: u64) -> u64 {
1527        let in_amount_msat = self.base_fee_msat
1528            + forward_amount_msat * (1_000_000 + self.fee_per_millionth) / 1_000_000;
1529
1530        print!("amount_from_forward: in_amount_msat = {in_amount_msat},base_fee_msat={}, fee_per_millionth={}  amount_to_forward: {}", self.base_fee_msat, self.fee_per_millionth, forward_amount_msat);
1531        in_amount_msat
1532    }
1533
1534    fn divide_ceil(dividend: u64, factor: u64) -> u64 {
1535        dividend.div_ceil(factor)
1536    }
1537}
1538
1539pub(crate) mod sanitize {
1540    use crate::{FullReverseSwapInfo, SwapInfo};
1541
1542    pub(crate) trait Sanitize {
1543        /// Sanitizes a raw struct to a clone of itself where sensitive fields are blanked
1544        fn sanitize(self) -> Self;
1545    }
1546
1547    pub(crate) fn sanitize_vec<T>(vals: Vec<T>) -> Vec<T>
1548    where
1549        T: Sanitize,
1550    {
1551        vals.into_iter()
1552            .map(|val| val.sanitize())
1553            .collect::<Vec<T>>()
1554    }
1555
1556    impl Sanitize for FullReverseSwapInfo {
1557        fn sanitize(self) -> FullReverseSwapInfo {
1558            FullReverseSwapInfo {
1559                preimage: vec![],
1560                private_key: vec![],
1561                ..self.clone()
1562            }
1563        }
1564    }
1565
1566    impl Sanitize for SwapInfo {
1567        fn sanitize(self) -> SwapInfo {
1568            SwapInfo {
1569                preimage: vec![],
1570                private_key: vec![],
1571                ..self.clone()
1572            }
1573        }
1574    }
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579    use anyhow::Result;
1580    use prost::Message;
1581    use rand::random;
1582    use sdk_common::grpc;
1583
1584    use crate::models::sanitize::Sanitize;
1585    use crate::test_utils::{get_test_ofp, get_test_ofp_48h, rand_string, rand_vec_u8};
1586    use crate::{
1587        FullReverseSwapInfo, OpeningFeeParams, PaymentPath, PaymentPathEdge, ReverseSwapInfoCached,
1588        ReverseSwapStatus, SwapInfo,
1589    };
1590
1591    #[test]
1592    fn test_route_fees() -> Result<()> {
1593        let route = PaymentPath {
1594            edges: vec![
1595                PaymentPathEdge {
1596                    node_id: vec![1],
1597                    short_channel_id: "807189x2048x0".into(),
1598                    channel_delay: 34,
1599                    base_fee_msat: 1000,
1600                    fee_per_millionth: 10,
1601                },
1602                PaymentPathEdge {
1603                    node_id: vec![2],
1604                    short_channel_id: "811871x2726x1".into(),
1605                    channel_delay: 34,
1606                    base_fee_msat: 0,
1607                    fee_per_millionth: 0,
1608                },
1609                PaymentPathEdge {
1610                    node_id: vec![3],
1611                    short_channel_id: "16000000x0x18087".into(),
1612                    channel_delay: 40,
1613                    base_fee_msat: 1000,
1614                    fee_per_millionth: 1,
1615                },
1616            ],
1617        };
1618        assert_eq!(route.final_hop_amount(1141000), 1139999);
1619        assert_eq!(route.first_hop_amount(1139999), 1141000);
1620
1621        let route = PaymentPath {
1622            edges: vec![
1623                PaymentPathEdge {
1624                    node_id: vec![1],
1625                    short_channel_id: "807189x2048x0".into(),
1626                    channel_delay: 34,
1627                    base_fee_msat: 1000,
1628                    fee_per_millionth: 10,
1629                },
1630                PaymentPathEdge {
1631                    node_id: vec![2],
1632                    short_channel_id: "811871x2726x1".into(),
1633                    channel_delay: 34,
1634                    base_fee_msat: 0,
1635                    fee_per_millionth: 0,
1636                },
1637                PaymentPathEdge {
1638                    node_id: vec![3],
1639                    short_channel_id: "16000000x0x18087".into(),
1640                    channel_delay: 40,
1641                    base_fee_msat: 0,
1642                    fee_per_millionth: 2000,
1643                },
1644            ],
1645        };
1646        assert_eq!(route.final_hop_amount(1141314), 1139036);
1647        assert_eq!(route.first_hop_amount(1139036), 1141314);
1648
1649        Ok(())
1650    }
1651
1652    use super::OpeningFeeParamsMenu;
1653
1654    #[test]
1655    fn test_ofp_menu_validation() -> Result<()> {
1656        // Menu with one entry is valid
1657        OpeningFeeParamsMenu::try_from(vec![get_test_ofp(10, 12, true)])?;
1658
1659        // Menu with identical entries (same min_msat, same proportional) is invalid
1660        assert!(OpeningFeeParamsMenu::try_from(vec![
1661            get_test_ofp(10, 12, true),
1662            get_test_ofp(10, 12, true),
1663        ])
1664        .is_err());
1665
1666        // Menu where 2nd item has larger min_fee_msat, same proportional is valid
1667        OpeningFeeParamsMenu::try_from(vec![
1668            get_test_ofp(10, 12, true),
1669            get_test_ofp(12, 12, true),
1670        ])?;
1671
1672        // Menu where 2nd item has same min_fee_msat, larger proportional is valid
1673        OpeningFeeParamsMenu::try_from(vec![
1674            get_test_ofp(10, 12, true),
1675            get_test_ofp(10, 14, true),
1676        ])?;
1677
1678        // Menu where 2nd item has larger min_fee_msat, larger proportional is valid
1679        OpeningFeeParamsMenu::try_from(vec![
1680            get_test_ofp(10, 12, true),
1681            get_test_ofp(12, 14, true),
1682        ])?;
1683
1684        // All other combinations of min_fee_msat / proportional are invalid
1685        // same min_msat, same proportional
1686        assert!(OpeningFeeParamsMenu::try_from(vec![
1687            get_test_ofp(10, 12, true),
1688            get_test_ofp(10, 12, true),
1689        ])
1690        .is_err());
1691        // same min_msat, reverse-sorted proportional
1692        assert!(OpeningFeeParamsMenu::try_from(vec![
1693            get_test_ofp(10, 12, true),
1694            get_test_ofp(10, 10, true),
1695        ])
1696        .is_err());
1697        // reverse-sorted min_msat, same proportional
1698        assert!(OpeningFeeParamsMenu::try_from(vec![
1699            get_test_ofp(12, 10, true),
1700            get_test_ofp(10, 10, true),
1701        ])
1702        .is_err());
1703
1704        // Sorted, but with one expired entry, is invalid
1705        assert!(OpeningFeeParamsMenu::try_from(vec![
1706            get_test_ofp(10, 10, true),
1707            get_test_ofp(12, 12, false),
1708        ])
1709        .is_err());
1710
1711        // Sorted, but with all expired entries, is invalid (because it results in an empty list)
1712        assert!(OpeningFeeParamsMenu::try_from(vec![
1713            get_test_ofp(10, 10, false),
1714            get_test_ofp(12, 12, false),
1715        ])
1716        .is_err());
1717
1718        Ok(())
1719    }
1720
1721    #[test]
1722    fn test_payment_information_ser_de() -> Result<()> {
1723        let dummy_payment_info = grpc::PaymentInformation {
1724            payment_hash: rand_vec_u8(10),
1725            payment_secret: rand_vec_u8(10),
1726            destination: rand_vec_u8(10),
1727            incoming_amount_msat: random(),
1728            outgoing_amount_msat: random(),
1729            tag: "".to_string(),
1730            opening_fee_params: None,
1731        };
1732
1733        let mut buf = Vec::with_capacity(dummy_payment_info.encoded_len());
1734        dummy_payment_info.encode(&mut buf)?;
1735
1736        let decoded_payment_info = grpc::PaymentInformation::decode(&*buf)?;
1737
1738        assert_eq!(dummy_payment_info, decoded_payment_info);
1739
1740        Ok(())
1741    }
1742
1743    #[test]
1744    fn test_dynamic_fee_valid_until_format() -> Result<()> {
1745        let mut ofp: OpeningFeeParams = get_test_ofp(1, 1, true).into();
1746        ofp.valid_until = "2023-08-03T00:30:35.117Z".to_string();
1747        ofp.valid_until_date().map(|_| ())
1748    }
1749
1750    /// Tests whether sanitization works for key structures used in the diagnostic data output
1751    #[test]
1752    fn test_sanitization() -> Result<()> {
1753        let rev_swap_info_sanitized = FullReverseSwapInfo {
1754            id: "rev_swap_id".to_string(),
1755            created_at_block_height: 0,
1756            preimage: rand_vec_u8(32),
1757            private_key: vec![],
1758            claim_pubkey: "claim_pubkey".to_string(),
1759            timeout_block_height: 600_000,
1760            invoice: "645".to_string(),
1761            redeem_script: "redeem_script".to_string(),
1762            onchain_amount_sat: 250,
1763            sat_per_vbyte: Some(50),
1764            receive_amount_sat: None,
1765            cache: ReverseSwapInfoCached {
1766                status: ReverseSwapStatus::CompletedConfirmed,
1767                lockup_txid: Some("lockup_txid".to_string()),
1768                claim_txid: Some("claim_txid".to_string()),
1769            },
1770        }
1771        .sanitize();
1772        assert_eq!(rev_swap_info_sanitized.preimage, Vec::<u8>::new());
1773        assert_eq!(rev_swap_info_sanitized.private_key, Vec::<u8>::new());
1774
1775        let swap_info_sanitized = SwapInfo {
1776            bitcoin_address: rand_string(10),
1777            created_at: 10,
1778            lock_height: random(),
1779            payment_hash: rand_vec_u8(32),
1780            preimage: rand_vec_u8(32),
1781            private_key: rand_vec_u8(32),
1782            public_key: rand_vec_u8(10),
1783            swapper_public_key: rand_vec_u8(10),
1784            script: rand_vec_u8(10),
1785            bolt11: None,
1786            paid_msat: 0,
1787            unconfirmed_sats: 0,
1788            confirmed_sats: 0,
1789            total_incoming_txs: 0,
1790            status: crate::models::SwapStatus::Initial,
1791            refund_tx_ids: Vec::new(),
1792            unconfirmed_tx_ids: Vec::new(),
1793            confirmed_tx_ids: Vec::new(),
1794            min_allowed_deposit: 0,
1795            max_allowed_deposit: 100,
1796            max_swapper_payable: 200,
1797            last_redeem_error: None,
1798            channel_opening_fees: Some(get_test_ofp_48h(random(), random()).into()),
1799            confirmed_at: None,
1800        }
1801        .sanitize();
1802        assert_eq!(swap_info_sanitized.preimage, Vec::<u8>::new());
1803        assert_eq!(swap_info_sanitized.private_key, Vec::<u8>::new());
1804
1805        Ok(())
1806    }
1807}