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