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
509#[derive(Clone)]
510pub enum NodeConfig {
511    Greenlight { config: GreenlightNodeConfig },
512}
513
514#[derive(Clone, Serialize)]
515pub enum NodeCredentials {
516    Greenlight {
517        credentials: GreenlightDeviceCredentials,
518    },
519}
520
521#[derive(Clone)]
522pub struct GreenlightNodeConfig {
523    pub partner_credentials: Option<GreenlightCredentials>,
524    pub invite_code: Option<String>,
525}
526
527/// Indicates the different kinds of supported environments for [crate::BreezServices].
528#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, EnumString)]
529pub enum EnvironmentType {
530    #[strum(serialize = "production")]
531    Production,
532    #[strum(serialize = "staging")]
533    Staging,
534}
535
536/// Client-specific credentials to connect to and manage a Greenlight node in the cloud
537#[derive(Clone, Serialize, Deserialize)]
538pub struct GreenlightCredentials {
539    pub developer_key: Vec<u8>,
540    pub developer_cert: Vec<u8>,
541}
542
543/// Device credentials used to authenticate to Greenlight with the current device.
544#[derive(Clone, Serialize, Deserialize)]
545pub struct GreenlightDeviceCredentials {
546    pub device: Vec<u8>,
547}
548
549/// Represents a configure node request.
550#[derive(Default)]
551pub struct ConfigureNodeRequest {
552    pub close_to_address: Option<String>,
553}
554
555/// Represents a connect request.
556pub struct ConnectRequest {
557    pub config: Config,
558    pub seed: Vec<u8>,
559    /// If true, only restores an existing node and otherwise result in an error
560    pub restore_only: Option<bool>,
561}
562
563/// Different types of supported filters which can be applied when retrieving the transaction list
564#[derive(PartialEq)]
565pub enum PaymentTypeFilter {
566    Sent,
567    Received,
568    ClosedChannel,
569}
570
571/// A metadata filter which can be applied when retrieving the transaction list
572pub struct MetadataFilter {
573    /// Specifies which field to apply the filter on, using the JSON path format
574    pub json_path: String,
575    /// Specifies which JSON value to filter for.
576    /// As such, strings must be wrapped with quotes ("") in order to be properly filtered
577    pub json_value: String,
578}
579
580/// Different types of supported feerates
581pub enum FeeratePreset {
582    Regular,
583    Economy,
584    Priority,
585}
586
587impl TryFrom<i32> for FeeratePreset {
588    type Error = anyhow::Error;
589
590    fn try_from(value: i32) -> std::result::Result<Self, Self::Error> {
591        match value {
592            0 => Ok(FeeratePreset::Regular),
593            1 => Ok(FeeratePreset::Economy),
594            2 => Ok(FeeratePreset::Priority),
595            _ => Err(anyhow!("Unexpected feerate enum value")),
596        }
597    }
598}
599
600#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
601pub struct BackupStatus {
602    pub backed_up: bool,
603    /// Epoch time, in seconds
604    pub last_backup_time: Option<u64>,
605}
606
607/// The node state of a Greenlight LN node running in the cloud.
608///
609/// Note: The implementation attempts to provide the most up-to-date values,
610/// which may result in some short-lived inconsistencies
611/// (e.g., `channels_balance_msat` may be updated before `inbound_liquidity_msats`).
612#[derive(Serialize, Default, Deserialize, Clone, PartialEq, Eq, Debug)]
613pub struct NodeState {
614    pub id: String,
615    pub block_height: u32,
616    pub channels_balance_msat: u64,
617    pub onchain_balance_msat: u64,
618    #[serde(default)]
619    pub pending_onchain_balance_msat: u64,
620
621    #[serde(default)]
622    pub utxos: Vec<UnspentTransactionOutput>,
623    pub max_payable_msat: u64,
624    pub max_receivable_msat: u64,
625    pub max_single_payment_amount_msat: u64,
626    pub max_chan_reserve_msats: u64,
627    pub connected_peers: Vec<String>,
628
629    /// Maximum receivable in a single payment without requiring a new channel open.
630    pub max_receivable_single_payment_amount_msat: u64,
631
632    /// Total receivable on all available channels
633    pub total_inbound_liquidity_msats: u64,
634}
635
636/// Internal response to a [crate::node_api::NodeAPI::pull_changed] call
637pub struct SyncResponse {
638    pub sync_state: Value,
639    pub node_state: NodeState,
640    pub payments: Vec<crate::models::Payment>,
641    pub channels: Vec<crate::models::Channel>,
642}
643
644/// The status of a payment
645#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
646pub enum PaymentStatus {
647    #[default]
648    Pending = 0,
649    Complete = 1,
650    Failed = 2,
651}
652
653/// Represents a payment, including its [PaymentType] and [PaymentDetails]
654#[derive(Default, PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
655pub struct Payment {
656    pub id: String,
657    pub payment_type: PaymentType,
658    /// Epoch time, in seconds
659    pub payment_time: i64,
660    pub amount_msat: u64,
661    pub fee_msat: u64,
662    pub status: PaymentStatus,
663    pub error: Option<String>,
664    pub description: Option<String>,
665    pub details: PaymentDetails,
666    pub metadata: Option<String>,
667}
668
669/// Represents a payments external information.
670#[derive(Default)]
671pub struct PaymentExternalInfo {
672    pub lnurl_pay_success_action: Option<SuccessActionProcessed>,
673    pub lnurl_pay_domain: Option<String>,
674    pub lnurl_pay_comment: Option<String>,
675    pub lnurl_metadata: Option<String>,
676    pub ln_address: Option<String>,
677    pub lnurl_withdraw_endpoint: Option<String>,
678    pub attempted_amount_msat: Option<u64>,
679    pub attempted_error: Option<String>,
680}
681
682/// Represents a list payments request.
683#[derive(Default)]
684pub struct ListPaymentsRequest {
685    pub filters: Option<Vec<PaymentTypeFilter>>,
686    pub metadata_filters: Option<Vec<MetadataFilter>>,
687    /// Epoch time, in seconds
688    pub from_timestamp: Option<i64>,
689    /// Epoch time, in seconds
690    pub to_timestamp: Option<i64>,
691    pub include_failures: Option<bool>,
692    pub offset: Option<u32>,
693    pub limit: Option<u32>,
694}
695
696/// Represents a payment response.
697#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
698pub struct PaymentResponse {
699    /// Epoch time, in seconds
700    pub payment_time: i64,
701    pub amount_msat: u64,
702    pub fee_msat: u64,
703    pub payment_hash: String,
704    pub payment_preimage: String,
705}
706
707/// Wrapper for the different types of payments
708#[allow(clippy::large_enum_variant)]
709#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
710#[serde(untagged)]
711pub enum PaymentDetails {
712    Ln {
713        #[serde(flatten)]
714        data: LnPaymentDetails,
715    },
716    ClosedChannel {
717        #[serde(flatten)]
718        data: ClosedChannelPaymentDetails,
719    },
720}
721
722impl Default for PaymentDetails {
723    fn default() -> Self {
724        Self::Ln {
725            data: LnPaymentDetails::default(),
726        }
727    }
728}
729
730impl PaymentDetails {
731    pub fn add_pending_expiration_block(&mut self, htlc: Htlc) {
732        if let PaymentDetails::Ln { data } = self {
733            data.pending_expiration_block = Some(htlc.expiry)
734        }
735    }
736}
737
738/// Details of a LN payment, as included in a [Payment]
739#[derive(Default, PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
740pub struct LnPaymentDetails {
741    pub payment_hash: String,
742    pub label: String,
743    pub destination_pubkey: String,
744    pub payment_preimage: String,
745    pub keysend: bool,
746    pub bolt11: String,
747
748    /// Only set for [PaymentType::Received], payments which require to open a channel.
749    /// Represents the actual invoice paid by the sender
750    pub open_channel_bolt11: Option<String>,
751
752    /// Only set for [PaymentType::Sent] payments that are part of a LNURL-pay workflow where
753    /// the endpoint returns a success action
754    pub lnurl_success_action: Option<SuccessActionProcessed>,
755
756    /// Only set for [PaymentType::Sent] payments if it is not a payment to a Lightning Address
757    pub lnurl_pay_domain: Option<String>,
758
759    /// Only set for [PaymentType::Sent] payments if the user sent the comment using LNURL-pay
760    pub lnurl_pay_comment: Option<String>,
761
762    /// Only set for [PaymentType::Sent] payments that are sent to a Lightning Address
763    pub ln_address: Option<String>,
764
765    /// Only set for [PaymentType::Sent] payments where the receiver endpoint returned LNURL metadata
766    pub lnurl_metadata: Option<String>,
767
768    /// Only set for [PaymentType::Received] payments that were received as part of LNURL-withdraw
769    pub lnurl_withdraw_endpoint: Option<String>,
770
771    /// Only set for [PaymentType::Received] payments that were received in the context of a swap
772    pub swap_info: Option<SwapInfo>,
773
774    /// Only set for [PaymentType::Sent] payments that were sent in the context of a reverse swap
775    pub reverse_swap_info: Option<ReverseSwapInfo>,
776
777    /// Only set for [PaymentStatus::Pending] payments that are inflight.
778    pub pending_expiration_block: Option<u32>,
779}
780
781/// Represents the funds that were on the user side of the channel at the time it was closed.
782#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
783pub struct ClosedChannelPaymentDetails {
784    pub state: ChannelState,
785    pub funding_txid: String,
786    pub short_channel_id: Option<String>,
787    /// Can be empty for older closed channels.
788    pub closing_txid: Option<String>,
789}
790
791#[derive(Clone, Debug, Default, Serialize, Deserialize)]
792pub struct ReverseSwapFeesRequest {
793    /// Amount to be sent
794    pub send_amount_sat: Option<u64>,
795    /// Feerate (sat / vByte) for the claim transaction
796    pub claim_tx_feerate: Option<u32>,
797}
798
799#[derive(Clone, Debug, Serialize, Deserialize)]
800pub struct MaxChannelAmount {
801    /// The channel id.
802    pub channel_id: String,
803    /// The max amount can be sent from this channel.
804    pub amount_msat: u64,
805    /// The payment path to be used for the maximum amount.
806    pub path: PaymentPath,
807}
808
809/// Represents a receive payment request.
810#[derive(Clone, Debug, Default, Serialize, Deserialize)]
811pub struct ReceivePaymentRequest {
812    /// The amount in satoshis for this payment request
813    pub amount_msat: u64,
814    /// The description for this payment request.
815    pub description: String,
816    /// Optional preimage for this payment request.
817    /// If specified, it will be used instead of generating a new one.
818    pub preimage: Option<Vec<u8>>,
819    /// If set and valid, these fess options are used when a new channels is needed.
820    /// Otherwise the default fee options will be used.
821    pub opening_fee_params: Option<OpeningFeeParams>,
822    /// If set to true, then the bolt11 invoice returned includes the description hash.
823    pub use_description_hash: Option<bool>,
824    /// if specified, set the time the invoice is valid for, in seconds.
825    pub expiry: Option<u32>,
826    /// if specified, sets the min_final_cltv_expiry for the invoice
827    pub cltv: Option<u32>,
828}
829
830/// Represents a receive payment response.
831///
832/// Breez SDK may have to open a new channel to receive this payment. In that case, the channel will
833/// be opened automatically when the invoice is paid, and the fees will be described in the
834/// `opening_fee_params` and `opening_fee_msat` fields.
835#[derive(Clone, Debug, Serialize, Deserialize)]
836pub struct ReceivePaymentResponse {
837    /// The generated invoice, including any necessary routing hints
838    pub ln_invoice: LNInvoice,
839    /// If set, these are the [OpeningFeeParams] used to calculate the channel opening fees.
840    pub opening_fee_params: Option<OpeningFeeParams>,
841    /// If set, this is the channel opening fee that will be deduced from the invoice amount.
842    pub opening_fee_msat: Option<u64>,
843}
844
845/// Represents a send payment request.
846#[derive(Clone, Debug, Serialize, Deserialize)]
847pub struct SendPaymentRequest {
848    /// The bolt11 invoice
849    pub bolt11: String,
850    /// Trampoline payments outsource pathfinding to the LSP. Trampoline payments can improve
851    /// payment performance, but are generally more expensive in terms of fees and they
852    /// compromise on privacy.
853    pub use_trampoline: bool,
854    /// The amount to pay in millisatoshis. Should only be set when `bolt11` is a zero-amount invoice.
855    pub amount_msat: Option<u64>,
856    /// The external label or identifier of the [Payment]
857    pub label: Option<String>,
858}
859
860/// Represents a TLV entry for a keysend payment.
861#[derive(Clone, Debug, Serialize, Deserialize)]
862pub struct TlvEntry {
863    /// The type field for the TLV
864    pub field_number: u64,
865    /// The value bytes for the TLV
866    pub value: Vec<u8>,
867}
868
869/// Represents a send spontaneous payment request.
870#[derive(Clone, Debug, Serialize, Deserialize)]
871pub struct SendSpontaneousPaymentRequest {
872    /// The node id to send this payment is
873    pub node_id: String,
874    /// The amount in millisatoshis for this payment
875    pub amount_msat: u64,
876    // Optional extra TLVs
877    pub extra_tlvs: Option<Vec<TlvEntry>>,
878    /// The external label or identifier of the [Payment]
879    pub label: Option<String>,
880}
881
882/// Represents a send payment response.
883#[derive(Clone, Debug, Serialize, Deserialize)]
884pub struct SendPaymentResponse {
885    pub payment: Payment,
886}
887
888#[derive(Clone, Debug, Serialize, Deserialize)]
889pub struct ReportPaymentFailureDetails {
890    /// The payment hash of the payment failure
891    pub payment_hash: String,
892    /// The comment or error text
893    pub comment: Option<String>,
894}
895
896/// Represents a report issue request.
897#[derive(Clone, Debug, Serialize, Deserialize)]
898pub enum ReportIssueRequest {
899    PaymentFailure { data: ReportPaymentFailureDetails },
900}
901
902/// Indicates the different service health check statuses.
903#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
904pub enum HealthCheckStatus {
905    Operational,
906    Maintenance,
907    ServiceDisruption,
908}
909
910/// Represents a service health check response.
911#[derive(Clone, Debug, Serialize, Deserialize)]
912pub struct ServiceHealthCheckResponse {
913    pub status: HealthCheckStatus,
914}
915
916/// Trait covering support-related functionality
917#[tonic::async_trait]
918pub trait SupportAPI: Send + Sync {
919    async fn service_health_check(&self) -> SdkResult<ServiceHealthCheckResponse>;
920
921    async fn report_payment_failure(
922        &self,
923        node_state: NodeState,
924        payment: Payment,
925        lsp_id: Option<String>,
926        comment: Option<String>,
927    ) -> SdkResult<()>;
928}
929
930#[derive(Clone)]
931pub struct StaticBackupRequest {
932    pub working_dir: String,
933}
934
935#[derive(Clone, Debug, Serialize, Deserialize)]
936pub struct StaticBackupResponse {
937    pub backup: Option<Vec<String>>,
938}
939
940#[derive(Default)]
941pub struct OpenChannelFeeRequest {
942    pub amount_msat: Option<u64>,
943    pub expiry: Option<u32>,
944}
945
946#[derive(Clone, Debug, Serialize, Deserialize)]
947pub struct OpenChannelFeeResponse {
948    /// Opening fee for receiving the amount set in the [OpenChannelFeeRequest], in case it was set.
949    /// It may be zero if no new channel needs to be opened.
950    pub fee_msat: Option<u64>,
951    /// The fee params for receiving more than the current inbound liquidity.
952    pub fee_params: OpeningFeeParams,
953}
954
955#[derive(Clone, Debug, Default, Serialize, Deserialize)]
956pub struct ReceiveOnchainRequest {
957    pub opening_fee_params: Option<OpeningFeeParams>,
958}
959
960#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
961pub struct ListSwapsRequest {
962    pub status: Option<Vec<SwapStatus>>,
963    /// Epoch time, in seconds. If set, acts as filter for minimum swap creation time, inclusive.
964    pub from_timestamp: Option<i64>,
965    /// Epoch time, in seconds. If set, acts as filter for maximum swap creation time, exclusive.
966    pub to_timestamp: Option<i64>,
967    pub offset: Option<u32>,
968    pub limit: Option<u32>,
969}
970
971#[derive(Clone, Debug, Serialize, Deserialize)]
972pub struct BuyBitcoinRequest {
973    pub provider: BuyBitcoinProvider,
974    pub opening_fee_params: Option<OpeningFeeParams>,
975    /// The optional URL to redirect to after completing the buy.
976    ///
977    /// For Moonpay, see <https://dev.moonpay.com/docs/on-ramp-configure-user-journey-params>
978    pub redirect_url: Option<String>,
979}
980
981#[derive(Clone, Debug, Serialize, Deserialize)]
982pub struct BuyBitcoinResponse {
983    pub url: String,
984    pub opening_fee_params: Option<OpeningFeeParams>,
985}
986
987#[derive(Clone, Debug, Serialize, Deserialize)]
988pub struct RedeemOnchainFundsRequest {
989    pub to_address: String,
990    pub sat_per_vbyte: u32,
991}
992
993#[derive(Clone, Debug, Serialize, Deserialize)]
994pub struct RedeemOnchainFundsResponse {
995    pub txid: Vec<u8>,
996}
997
998pub enum SwapAmountType {
999    Send,
1000    Receive,
1001}
1002
1003/// See [ReverseSwapFeesRequest]
1004pub struct PrepareOnchainPaymentRequest {
1005    /// Depending on `amount_type`, this may be the desired send amount or the desired receive amount.
1006    pub amount_sat: u64,
1007    pub amount_type: SwapAmountType,
1008
1009    /// Feerate (sat / vByte) for the claim transaction
1010    pub claim_tx_feerate: u32,
1011}
1012
1013#[derive(Serialize)]
1014pub struct OnchainPaymentLimitsResponse {
1015    /// Minimum amount the reverse swap service accepts as a send amount
1016    pub min_sat: u64,
1017    /// Maximum amount the reverse swap service accepts as a send amount
1018    pub max_sat: u64,
1019    /// Maximum amount this node can send with the current channels and the current local balance
1020    pub max_payable_sat: u64,
1021}
1022
1023/// Contains fields describing the reverse swap parameters (see [ReverseSwapPairInfo]), as well as
1024/// the resulting send and receive amounts.
1025#[derive(Clone, Debug, Serialize)]
1026pub struct PrepareOnchainPaymentResponse {
1027    pub fees_hash: String,
1028    pub fees_percentage: f64,
1029    pub fees_lockup: u64,
1030    pub fees_claim: u64,
1031
1032    pub sender_amount_sat: u64,
1033    pub recipient_amount_sat: u64,
1034    pub total_fees: u64,
1035}
1036
1037#[derive(Clone, Debug)]
1038pub struct PayOnchainRequest {
1039    pub recipient_address: String,
1040    pub prepare_res: PrepareOnchainPaymentResponse,
1041}
1042
1043#[derive(Serialize)]
1044pub struct PayOnchainResponse {
1045    pub reverse_swap_info: ReverseSwapInfo,
1046}
1047
1048pub struct PrepareRefundRequest {
1049    pub swap_address: String,
1050    pub to_address: String,
1051    pub sat_per_vbyte: u32,
1052    pub unilateral: Option<bool>,
1053}
1054
1055pub struct RefundRequest {
1056    pub swap_address: String,
1057    pub to_address: String,
1058    pub sat_per_vbyte: u32,
1059    pub unilateral: Option<bool>,
1060}
1061
1062pub struct PrepareRefundResponse {
1063    pub refund_tx_weight: u32,
1064    pub refund_tx_fee_sat: u64,
1065}
1066
1067pub struct RefundResponse {
1068    pub refund_tx_id: String,
1069}
1070
1071/// Dynamic fee parameters offered by the LSP for opening a new channel.
1072///
1073/// After they are received, the client shouldn't change them when calling LSP methods,
1074/// otherwise the LSP may reject the call.
1075#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
1076pub struct OpeningFeeParams {
1077    /// The minimum value in millisatoshi we will require for incoming HTLCs on the channel
1078    pub min_msat: u64,
1079    /// The fee in ppm charged over liquidity when buying a channel
1080    pub proportional: u32,
1081    /// The date and time this opening fee params promise expires, in RFC 3339 / ISO 8601 format
1082    pub valid_until: String,
1083    /// The channel can be closed if not used within this duration in blocks
1084    pub max_idle_time: u32,
1085    pub max_client_to_self_delay: u32,
1086    pub promise: String,
1087}
1088
1089impl OpeningFeeParams {
1090    pub(crate) fn valid_until_date(&self) -> Result<DateTime<Utc>> {
1091        Ok(DateTime::parse_from_rfc3339(&self.valid_until).map(|d| d.with_timezone(&Utc))?)
1092    }
1093
1094    pub(crate) fn valid_for(&self, expiry: u32) -> Result<bool> {
1095        Ok(self.valid_until_date()? > Utc::now().add(Duration::seconds(expiry as i64)))
1096    }
1097
1098    pub(crate) fn get_channel_fees_msat_for(&self, amount_msats: u64) -> u64 {
1099        let lsp_fee_msat = amount_msats * self.proportional as u64 / 1_000_000;
1100        let lsp_fee_msat_rounded_to_sat = lsp_fee_msat / 1000 * 1000;
1101
1102        max(lsp_fee_msat_rounded_to_sat, self.min_msat)
1103    }
1104}
1105
1106impl From<OpeningFeeParams> for grpc::OpeningFeeParams {
1107    fn from(ofp: OpeningFeeParams) -> Self {
1108        Self {
1109            min_msat: ofp.min_msat,
1110            proportional: ofp.proportional,
1111            valid_until: ofp.valid_until,
1112            max_idle_time: ofp.max_idle_time,
1113            max_client_to_self_delay: ofp.max_client_to_self_delay,
1114            promise: ofp.promise,
1115        }
1116    }
1117}
1118
1119impl From<grpc::OpeningFeeParams> for OpeningFeeParams {
1120    fn from(gofp: grpc::OpeningFeeParams) -> Self {
1121        Self {
1122            min_msat: gofp.min_msat,
1123            proportional: gofp.proportional,
1124            valid_until: gofp.valid_until,
1125            max_idle_time: gofp.max_idle_time,
1126            max_client_to_self_delay: gofp.max_client_to_self_delay,
1127            promise: gofp.promise,
1128        }
1129    }
1130}
1131
1132impl FromSql for OpeningFeeParams {
1133    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
1134        serde_json::from_str(value.as_str()?).map_err(|_| FromSqlError::InvalidType)
1135    }
1136}
1137
1138impl ToSql for OpeningFeeParams {
1139    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1140        Ok(ToSqlOutput::from(
1141            serde_json::to_string(&self).map_err(|_| FromSqlError::InvalidType)?,
1142        ))
1143    }
1144}
1145
1146pub enum DynamicFeeType {
1147    Cheapest,
1148    Longest,
1149}
1150
1151/// See [OpeningFeeParamsMenu::try_from]
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1153pub struct OpeningFeeParamsMenu {
1154    pub values: Vec<OpeningFeeParams>,
1155}
1156
1157impl OpeningFeeParamsMenu {
1158    /// Initializes and validates itself.
1159    ///
1160    /// This struct should not be persisted as such, because validation happens dynamically based on
1161    /// the current time. At a later point in time, any previously-validated [OpeningFeeParamsMenu]
1162    /// could be invalid. Therefore, the [OpeningFeeParamsMenu] should always be initialized on-the-fly.
1163    pub fn try_from(values: Vec<sdk_common::grpc::OpeningFeeParams>) -> Result<Self> {
1164        let temp = Self {
1165            values: values
1166                .into_iter()
1167                .map(|ofp| ofp.into())
1168                .collect::<Vec<OpeningFeeParams>>(),
1169        };
1170        temp.validate().map(|_| temp)
1171    }
1172
1173    fn validate(&self) -> Result<()> {
1174        // values must be in ascending order
1175        let is_ordered = self.values.windows(2).all(|ofp| {
1176            let larger_min_msat_fee = ofp[0].min_msat < ofp[1].min_msat;
1177            let equal_min_msat_fee = ofp[0].min_msat == ofp[1].min_msat;
1178
1179            let larger_proportional = ofp[0].proportional < ofp[1].proportional;
1180            let equal_proportional = ofp[0].proportional == ofp[1].proportional;
1181
1182            (larger_min_msat_fee && equal_proportional)
1183                || (equal_min_msat_fee && larger_proportional)
1184                || (larger_min_msat_fee && larger_proportional)
1185        });
1186        ensure!(is_ordered, "Validation failed: fee params are not ordered");
1187
1188        // Menu must not contain any item with `valid_until` in the past
1189        let is_expired = self.values.iter().any(|ofp| match ofp.valid_until_date() {
1190            Ok(valid_until) => Utc::now() > valid_until,
1191            Err(_) => {
1192                warn!("Failed to parse valid_until for OpeningFeeParams: {ofp:?}");
1193                false
1194            }
1195        });
1196        ensure!(!is_expired, "Validation failed: expired fee params found");
1197
1198        Ok(())
1199    }
1200
1201    pub fn get_cheapest_opening_fee_params(&self) -> Result<OpeningFeeParams> {
1202        self.values.first().cloned().ok_or_else(|| {
1203            anyhow!("The LSP doesn't support opening new channels: Dynamic fees menu contains no values")
1204        })
1205    }
1206
1207    pub fn get_48h_opening_fee_params(&self) -> Result<OpeningFeeParams> {
1208        // Find the fee params that are valid for at least 48h
1209        let now = Utc::now();
1210        let duration_48h = chrono::Duration::hours(48);
1211        let valid_min_48h: Vec<OpeningFeeParams> = self
1212            .values
1213            .iter()
1214            .filter(|ofp| match ofp.valid_until_date() {
1215                Ok(valid_until) => valid_until - now > duration_48h,
1216                Err(_) => {
1217                    warn!("Failed to parse valid_until for OpeningFeeParams: {ofp:?}");
1218                    false
1219                }
1220            })
1221            .cloned()
1222            .collect();
1223
1224        // Of those, return the first, which is the cheapest
1225        // (sorting order of fee params list was checked when the menu was initialized)
1226        valid_min_48h.first().cloned().ok_or_else(|| {
1227            anyhow!("The LSP doesn't support opening fees that are valid for at least 48 hours")
1228        })
1229    }
1230}
1231
1232/// Lightning channel
1233#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
1234pub struct Channel {
1235    pub funding_txid: String,
1236    pub short_channel_id: Option<String>,
1237    pub state: ChannelState,
1238    pub spendable_msat: u64,
1239    pub local_balance_msat: u64,
1240    pub receivable_msat: u64,
1241    pub closed_at: Option<u64>,
1242    /// The output number of the funding tx which opened the channel
1243    pub funding_outnum: Option<u32>,
1244    pub alias_local: Option<String>,
1245    pub alias_remote: Option<String>,
1246    /// Only set for closed channels.
1247    ///
1248    /// This may be empty for older closed channels, if it was not possible to retrieve the closing txid.
1249    pub closing_txid: Option<String>,
1250
1251    pub htlcs: Vec<Htlc>,
1252}
1253
1254#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
1255pub struct Htlc {
1256    pub expiry: u32,
1257    pub payment_hash: Vec<u8>,
1258}
1259
1260impl Htlc {
1261    pub fn from(expiry: u32, payment_hash: Vec<u8>) -> Self {
1262        Htlc {
1263            expiry,
1264            payment_hash,
1265        }
1266    }
1267}
1268
1269/// State of a Lightning channel
1270#[derive(Clone, PartialEq, Eq, Debug, EnumString, Display, Deserialize, Serialize)]
1271pub enum ChannelState {
1272    PendingOpen,
1273    Opened,
1274    PendingClose,
1275    Closed,
1276}
1277
1278/// The status of a swap
1279#[derive(Copy, Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize)]
1280pub enum SwapStatus {
1281    /// The swap address has been created and either there aren't any confirmed transactions associated with it
1282    /// or there are confirmed transactions that are bellow the lock timeout which means the funds are still
1283    /// eligible to be redeemed normally.
1284    #[default]
1285    Initial = 0,
1286
1287    WaitingConfirmation = 1,
1288
1289    Redeemable = 2,
1290
1291    Redeemed = 3,
1292
1293    /// The swap address has confirmed transactions associated with it and the lock timeout has passed since
1294    /// the earliest confirmed transaction. This means the only way to spend the funds from this address is by
1295    /// broadcasting a refund transaction.
1296    Refundable = 4,
1297
1298    Completed = 5,
1299}
1300
1301impl SwapStatus {
1302    pub(crate) fn unused() -> Vec<SwapStatus> {
1303        vec![SwapStatus::Initial]
1304    }
1305
1306    pub(crate) fn in_progress() -> Vec<SwapStatus> {
1307        vec![SwapStatus::Redeemable, SwapStatus::WaitingConfirmation]
1308    }
1309
1310    pub(crate) fn redeemable() -> Vec<SwapStatus> {
1311        vec![SwapStatus::Redeemable]
1312    }
1313
1314    pub(crate) fn refundable() -> Vec<SwapStatus> {
1315        vec![SwapStatus::Refundable]
1316    }
1317
1318    pub(crate) fn monitored() -> Vec<SwapStatus> {
1319        vec![
1320            SwapStatus::Initial,
1321            SwapStatus::WaitingConfirmation,
1322            SwapStatus::Redeemable,
1323            SwapStatus::Redeemed,
1324            SwapStatus::Refundable,
1325        ]
1326    }
1327
1328    pub(crate) fn unexpired() -> Vec<SwapStatus> {
1329        vec![
1330            SwapStatus::Initial,
1331            SwapStatus::WaitingConfirmation,
1332            SwapStatus::Redeemable,
1333            SwapStatus::Redeemed,
1334        ]
1335    }
1336}
1337
1338impl TryFrom<i32> for SwapStatus {
1339    type Error = anyhow::Error;
1340
1341    fn try_from(value: i32) -> Result<Self, Self::Error> {
1342        match value {
1343            0 => Ok(SwapStatus::Initial),
1344            1 => Ok(SwapStatus::WaitingConfirmation),
1345            2 => Ok(SwapStatus::Redeemable),
1346            3 => Ok(SwapStatus::Redeemed),
1347            4 => Ok(SwapStatus::Refundable),
1348            5 => Ok(SwapStatus::Completed),
1349            _ => Err(anyhow!("illegal value")),
1350        }
1351    }
1352}
1353
1354/// Represents the details of an on-going swap.
1355///
1356/// Once this SwapInfo is created it will be monitored on-chain and its state is
1357/// saved to the persistent storage.
1358///
1359/// The SwapInfo has a status which changes accordingly, documented in [SwapStatus].
1360///
1361
1362#[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize)]
1363pub struct SwapInfo {
1364    /// Bitcoin address for this swap. Sats sent to this address will be swapped.
1365    pub bitcoin_address: String,
1366    /// Relative time lock start, received from [SwapperAPI::create_swap].
1367    pub created_at: i64,
1368    /// Relative time lock for the timeout for the script to be redeemed before swap fails.
1369    pub lock_height: i64,
1370    /// sha256 hash of preimage to used in the claim sript.
1371    pub payment_hash: Vec<u8>,
1372    /// Secret to claim the swap.
1373    pub preimage: Vec<u8>,
1374    /// Secret claim key for the bitcoin address.
1375    pub private_key: Vec<u8>,
1376    /// Public key in binary format of the private claim private key.
1377    pub public_key: Vec<u8>,
1378    /// The public key in binary format from the swapping service. Received from [SwapperAPI::create_swap].
1379    pub swapper_public_key: Vec<u8>,
1380    /// The locking script for the generated bitcoin address. Received from [SwapperAPI::create_swap].
1381    pub script: Vec<u8>,
1382
1383    /// bolt11 invoice to claim the sent funds.
1384    pub bolt11: Option<String>,
1385    /// Amount of millisatoshis claimed from sent funds and paid for via bolt11 invoice.
1386    pub paid_msat: u64,
1387    /// Total count of transactions sent to the swap address.
1388    pub total_incoming_txs: u64,
1389    /// Confirmed onchain sats to be claim with an bolt11 invoice or refunded if swap fails.
1390    pub confirmed_sats: u64,
1391    /// Unconfirmed sats waiting to be confirmed onchain.
1392    pub unconfirmed_sats: u64,
1393    /// Shows the current status of the swap, either `Initial` or `Expired`.
1394    pub status: SwapStatus,
1395    /// Transaction IDs for failed swap attempts.
1396    pub refund_tx_ids: Vec<String>,
1397    /// Refund transaction IDs for ongoing swap awaiting confirmation.
1398    pub unconfirmed_tx_ids: Vec<String>,
1399    /// Transaction IDs that have been confirmed on-chain.
1400    pub confirmed_tx_ids: Vec<String>,
1401    /// The minimum amount of sats one can send in order for the swap to succeed. Received from [SwapperAPI::create_swap].
1402    pub min_allowed_deposit: i64,
1403    /// 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.
1404    pub max_allowed_deposit: i64,
1405    /// The absolute maximum value payable by the swapper. Received from [SwapperAPI::create_swap].
1406    pub max_swapper_payable: i64,
1407    /// Error reason for when swap fails.
1408    pub last_redeem_error: Option<String>,
1409    /// The dynamic fees which is set if a channel opening is needed.
1410    ///
1411    /// This is an optional field for backward compatibility with swaps created before dynamic fees.
1412    ///
1413    /// Swaps created after dynamic fees were introduced always have this field set.
1414    pub channel_opening_fees: Option<OpeningFeeParams>,
1415    /// The block height when the swap was confirmed.
1416    pub confirmed_at: Option<u32>,
1417}
1418
1419/// UTXO known to the LN node
1420#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
1421pub struct UnspentTransactionOutput {
1422    pub txid: Vec<u8>,
1423    pub outnum: u32,
1424    pub amount_millisatoshi: u64,
1425    pub address: String,
1426    #[serde(default)]
1427    pub reserved: bool,
1428}
1429
1430/// Different providers will demand different behaviours when the user is trying to buy bitcoin.
1431#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
1432#[serde(tag = "buy_bitcoin_provider")]
1433pub enum BuyBitcoinProvider {
1434    Moonpay,
1435}
1436
1437/// We need to prepare a redeem_onchain_funds transaction to know what fee will be charged in satoshis.
1438/// This model holds the request data which consists of the address to redeem on-chain funds to and the fee rate in.
1439/// satoshis per vbyte which will be converted to absolute satoshis.
1440#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
1441pub struct PrepareRedeemOnchainFundsRequest {
1442    pub to_address: String,
1443    pub sat_per_vbyte: u32,
1444}
1445
1446/// We need to prepare a redeem_onchain_funds transaction to know what a fee it will be charged in satoshis
1447/// this model holds the response data, which consists of the weight and the absolute fee in sats
1448#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
1449pub struct PrepareRedeemOnchainFundsResponse {
1450    pub tx_weight: u64,
1451    pub tx_fee_sat: u64,
1452}
1453
1454impl FromStr for BuyBitcoinProvider {
1455    type Err = anyhow::Error;
1456
1457    fn from_str(s: &str) -> Result<Self, Self::Err> {
1458        match s {
1459            "moonpay" => Ok(BuyBitcoinProvider::Moonpay),
1460            _ => Err(anyhow!("unknown buy bitcoin provider")),
1461        }
1462    }
1463}
1464
1465#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
1466pub struct PaymentPath {
1467    pub edges: Vec<PaymentPathEdge>,
1468}
1469
1470impl PaymentPath {
1471    pub fn final_hop_amount(&self, first_hop_amount_msat: u64) -> u64 {
1472        let mut max_to_send = first_hop_amount_msat;
1473        for h in self.edges.iter().skip(1) {
1474            max_to_send = h.amount_to_forward(max_to_send);
1475        }
1476        max_to_send
1477    }
1478
1479    pub fn first_hop_amount(&self, final_hop_amount_msat: u64) -> u64 {
1480        let mut first_hop_amount = final_hop_amount_msat;
1481        for h in self.edges.iter().skip(1).rev() {
1482            first_hop_amount = h.amount_from_forward(first_hop_amount);
1483        }
1484        first_hop_amount
1485    }
1486}
1487
1488#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
1489pub struct PaymentPathEdge {
1490    pub node_id: Vec<u8>,
1491    pub short_channel_id: String,
1492    pub channel_delay: u64,
1493    pub base_fee_msat: u64,
1494    pub fee_per_millionth: u64,
1495}
1496
1497impl PaymentPathEdge {
1498    pub(crate) fn amount_to_forward(&self, in_amount_msat: u64) -> u64 {
1499        let amount_to_forward = Self::divide_ceil(
1500            1_000_000 * (in_amount_msat - self.base_fee_msat),
1501            1_000_000 + self.fee_per_millionth,
1502        );
1503
1504        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);
1505        amount_to_forward
1506    }
1507
1508    pub(crate) fn amount_from_forward(&self, forward_amount_msat: u64) -> u64 {
1509        let in_amount_msat = self.base_fee_msat
1510            + forward_amount_msat * (1_000_000 + self.fee_per_millionth) / 1_000_000;
1511
1512        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);
1513        in_amount_msat
1514    }
1515
1516    fn divide_ceil(dividend: u64, factor: u64) -> u64 {
1517        dividend.div_ceil(factor)
1518    }
1519}
1520
1521pub(crate) mod sanitize {
1522    use crate::{FullReverseSwapInfo, SwapInfo};
1523
1524    pub(crate) trait Sanitize {
1525        /// Sanitizes a raw struct to a clone of itself where sensitive fields are blanked
1526        fn sanitize(self) -> Self;
1527    }
1528
1529    pub(crate) fn sanitize_vec<T>(vals: Vec<T>) -> Vec<T>
1530    where
1531        T: Sanitize,
1532    {
1533        vals.into_iter()
1534            .map(|val| val.sanitize())
1535            .collect::<Vec<T>>()
1536    }
1537
1538    impl Sanitize for FullReverseSwapInfo {
1539        fn sanitize(self) -> FullReverseSwapInfo {
1540            FullReverseSwapInfo {
1541                preimage: vec![],
1542                private_key: vec![],
1543                ..self.clone()
1544            }
1545        }
1546    }
1547
1548    impl Sanitize for SwapInfo {
1549        fn sanitize(self) -> SwapInfo {
1550            SwapInfo {
1551                preimage: vec![],
1552                private_key: vec![],
1553                ..self.clone()
1554            }
1555        }
1556    }
1557}
1558
1559#[cfg(test)]
1560mod tests {
1561    use anyhow::Result;
1562    use prost::Message;
1563    use rand::random;
1564    use sdk_common::grpc;
1565
1566    use crate::models::sanitize::Sanitize;
1567    use crate::test_utils::{get_test_ofp, get_test_ofp_48h, rand_string, rand_vec_u8};
1568    use crate::{
1569        FullReverseSwapInfo, OpeningFeeParams, PaymentPath, PaymentPathEdge, ReverseSwapInfoCached,
1570        ReverseSwapStatus, SwapInfo,
1571    };
1572
1573    #[test]
1574    fn test_route_fees() -> Result<()> {
1575        let route = PaymentPath {
1576            edges: vec![
1577                PaymentPathEdge {
1578                    node_id: vec![1],
1579                    short_channel_id: "807189x2048x0".into(),
1580                    channel_delay: 34,
1581                    base_fee_msat: 1000,
1582                    fee_per_millionth: 10,
1583                },
1584                PaymentPathEdge {
1585                    node_id: vec![2],
1586                    short_channel_id: "811871x2726x1".into(),
1587                    channel_delay: 34,
1588                    base_fee_msat: 0,
1589                    fee_per_millionth: 0,
1590                },
1591                PaymentPathEdge {
1592                    node_id: vec![3],
1593                    short_channel_id: "16000000x0x18087".into(),
1594                    channel_delay: 40,
1595                    base_fee_msat: 1000,
1596                    fee_per_millionth: 1,
1597                },
1598            ],
1599        };
1600        assert_eq!(route.final_hop_amount(1141000), 1139999);
1601        assert_eq!(route.first_hop_amount(1139999), 1141000);
1602
1603        let route = PaymentPath {
1604            edges: vec![
1605                PaymentPathEdge {
1606                    node_id: vec![1],
1607                    short_channel_id: "807189x2048x0".into(),
1608                    channel_delay: 34,
1609                    base_fee_msat: 1000,
1610                    fee_per_millionth: 10,
1611                },
1612                PaymentPathEdge {
1613                    node_id: vec![2],
1614                    short_channel_id: "811871x2726x1".into(),
1615                    channel_delay: 34,
1616                    base_fee_msat: 0,
1617                    fee_per_millionth: 0,
1618                },
1619                PaymentPathEdge {
1620                    node_id: vec![3],
1621                    short_channel_id: "16000000x0x18087".into(),
1622                    channel_delay: 40,
1623                    base_fee_msat: 0,
1624                    fee_per_millionth: 2000,
1625                },
1626            ],
1627        };
1628        assert_eq!(route.final_hop_amount(1141314), 1139036);
1629        assert_eq!(route.first_hop_amount(1139036), 1141314);
1630
1631        Ok(())
1632    }
1633
1634    use super::OpeningFeeParamsMenu;
1635
1636    #[test]
1637    fn test_ofp_menu_validation() -> Result<()> {
1638        // Menu with one entry is valid
1639        OpeningFeeParamsMenu::try_from(vec![get_test_ofp(10, 12, true)])?;
1640
1641        // Menu with identical entries (same min_msat, same proportional) is invalid
1642        assert!(OpeningFeeParamsMenu::try_from(vec![
1643            get_test_ofp(10, 12, true),
1644            get_test_ofp(10, 12, true),
1645        ])
1646        .is_err());
1647
1648        // Menu where 2nd item has larger min_fee_msat, same proportional is valid
1649        OpeningFeeParamsMenu::try_from(vec![
1650            get_test_ofp(10, 12, true),
1651            get_test_ofp(12, 12, true),
1652        ])?;
1653
1654        // Menu where 2nd item has same min_fee_msat, larger proportional is valid
1655        OpeningFeeParamsMenu::try_from(vec![
1656            get_test_ofp(10, 12, true),
1657            get_test_ofp(10, 14, true),
1658        ])?;
1659
1660        // Menu where 2nd item has larger min_fee_msat, larger proportional is valid
1661        OpeningFeeParamsMenu::try_from(vec![
1662            get_test_ofp(10, 12, true),
1663            get_test_ofp(12, 14, true),
1664        ])?;
1665
1666        // All other combinations of min_fee_msat / proportional are invalid
1667        // same min_msat, same proportional
1668        assert!(OpeningFeeParamsMenu::try_from(vec![
1669            get_test_ofp(10, 12, true),
1670            get_test_ofp(10, 12, true),
1671        ])
1672        .is_err());
1673        // same min_msat, reverse-sorted proportional
1674        assert!(OpeningFeeParamsMenu::try_from(vec![
1675            get_test_ofp(10, 12, true),
1676            get_test_ofp(10, 10, true),
1677        ])
1678        .is_err());
1679        // reverse-sorted min_msat, same proportional
1680        assert!(OpeningFeeParamsMenu::try_from(vec![
1681            get_test_ofp(12, 10, true),
1682            get_test_ofp(10, 10, true),
1683        ])
1684        .is_err());
1685
1686        // Sorted, but with one expired entry, is invalid
1687        assert!(OpeningFeeParamsMenu::try_from(vec![
1688            get_test_ofp(10, 10, true),
1689            get_test_ofp(12, 12, false),
1690        ])
1691        .is_err());
1692
1693        // Sorted, but with all expired entries, is invalid (because it results in an empty list)
1694        assert!(OpeningFeeParamsMenu::try_from(vec![
1695            get_test_ofp(10, 10, false),
1696            get_test_ofp(12, 12, false),
1697        ])
1698        .is_err());
1699
1700        Ok(())
1701    }
1702
1703    #[test]
1704    fn test_payment_information_ser_de() -> Result<()> {
1705        let dummy_payment_info = grpc::PaymentInformation {
1706            payment_hash: rand_vec_u8(10),
1707            payment_secret: rand_vec_u8(10),
1708            destination: rand_vec_u8(10),
1709            incoming_amount_msat: random(),
1710            outgoing_amount_msat: random(),
1711            tag: "".to_string(),
1712            opening_fee_params: None,
1713        };
1714
1715        let mut buf = Vec::with_capacity(dummy_payment_info.encoded_len());
1716        dummy_payment_info.encode(&mut buf)?;
1717
1718        let decoded_payment_info = grpc::PaymentInformation::decode(&*buf)?;
1719
1720        assert_eq!(dummy_payment_info, decoded_payment_info);
1721
1722        Ok(())
1723    }
1724
1725    #[test]
1726    fn test_dynamic_fee_valid_until_format() -> Result<()> {
1727        let mut ofp: OpeningFeeParams = get_test_ofp(1, 1, true).into();
1728        ofp.valid_until = "2023-08-03T00:30:35.117Z".to_string();
1729        ofp.valid_until_date().map(|_| ())
1730    }
1731
1732    /// Tests whether sanitization works for key structures used in the diagnostic data output
1733    #[test]
1734    fn test_sanitization() -> Result<()> {
1735        let rev_swap_info_sanitized = FullReverseSwapInfo {
1736            id: "rev_swap_id".to_string(),
1737            created_at_block_height: 0,
1738            preimage: rand_vec_u8(32),
1739            private_key: vec![],
1740            claim_pubkey: "claim_pubkey".to_string(),
1741            timeout_block_height: 600_000,
1742            invoice: "645".to_string(),
1743            redeem_script: "redeem_script".to_string(),
1744            onchain_amount_sat: 250,
1745            sat_per_vbyte: Some(50),
1746            receive_amount_sat: None,
1747            cache: ReverseSwapInfoCached {
1748                status: ReverseSwapStatus::CompletedConfirmed,
1749                lockup_txid: Some("lockup_txid".to_string()),
1750                claim_txid: Some("claim_txid".to_string()),
1751            },
1752        }
1753        .sanitize();
1754        assert_eq!(rev_swap_info_sanitized.preimage, Vec::<u8>::new());
1755        assert_eq!(rev_swap_info_sanitized.private_key, Vec::<u8>::new());
1756
1757        let swap_info_sanitized = SwapInfo {
1758            bitcoin_address: rand_string(10),
1759            created_at: 10,
1760            lock_height: random(),
1761            payment_hash: rand_vec_u8(32),
1762            preimage: rand_vec_u8(32),
1763            private_key: rand_vec_u8(32),
1764            public_key: rand_vec_u8(10),
1765            swapper_public_key: rand_vec_u8(10),
1766            script: rand_vec_u8(10),
1767            bolt11: None,
1768            paid_msat: 0,
1769            unconfirmed_sats: 0,
1770            confirmed_sats: 0,
1771            total_incoming_txs: 0,
1772            status: crate::models::SwapStatus::Initial,
1773            refund_tx_ids: Vec::new(),
1774            unconfirmed_tx_ids: Vec::new(),
1775            confirmed_tx_ids: Vec::new(),
1776            min_allowed_deposit: 0,
1777            max_allowed_deposit: 100,
1778            max_swapper_payable: 200,
1779            last_redeem_error: None,
1780            channel_opening_fees: Some(get_test_ofp_48h(random(), random()).into()),
1781            confirmed_at: None,
1782        }
1783        .sanitize();
1784        assert_eq!(swap_info_sanitized.preimage, Vec::<u8>::new());
1785        assert_eq!(swap_info_sanitized.private_key, Vec::<u8>::new());
1786
1787        Ok(())
1788    }
1789}