use std::cmp::max;
use std::ops::Add;
use std::str::FromStr;
use anyhow::{anyhow, ensure, Result};
use chrono::{DateTime, Duration, Utc};
use ripemd::Digest;
use ripemd::Ripemd160;
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::ToSql;
use sdk_common::grpc;
use sdk_common::prelude::Network::*;
use sdk_common::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use strum_macros::{Display, EnumString};
use crate::bitcoin::blockdata::opcodes;
use crate::bitcoin::blockdata::script::Builder;
use crate::bitcoin::hashes::hex::{FromHex, ToHex};
use crate::bitcoin::hashes::{sha256, Hash};
use crate::bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use crate::bitcoin::{Address, Script};
use crate::error::SdkResult;
use crate::lsp::LspInformation;
use crate::persist::swap::SwapChainInfo;
use crate::swap_in::error::{SwapError, SwapResult};
use crate::swap_out::boltzswap::{BoltzApiCreateReverseSwapResponse, BoltzApiReverseSwapStatus};
use crate::swap_out::error::{ReverseSwapError, ReverseSwapResult};
pub const SWAP_PAYMENT_FEE_EXPIRY_SECONDS: u32 = 60 * 60 * 24 * 2; pub const INVOICE_PAYMENT_FEE_EXPIRY_SECONDS: u32 = 60 * 60; #[derive(Clone, PartialEq, Eq, Debug, EnumString, Display, Deserialize, Serialize, Hash)]
pub enum PaymentType {
Sent,
Received,
ClosedChannel,
}
#[derive(Debug)]
pub struct CustomMessage {
pub peer_id: Vec<u8>,
pub message_type: u16,
pub payload: Vec<u8>,
}
#[tonic::async_trait]
pub trait LspAPI: Send + Sync {
async fn list_lsps(&self, node_pubkey: String) -> SdkResult<Vec<LspInformation>>;
async fn list_used_lsps(&self, node_pubkey: String) -> SdkResult<Vec<LspInformation>>;
async fn register_payment_notifications(
&self,
lsp_id: String,
lsp_pubkey: Vec<u8>,
webhook_url: String,
webhook_url_signature: String,
) -> SdkResult<grpc::RegisterPaymentNotificationResponse>;
async fn unregister_payment_notifications(
&self,
lsp_id: String,
lsp_pubkey: Vec<u8>,
webhook_url: String,
webhook_url_signature: String,
) -> SdkResult<grpc::RemovePaymentNotificationResponse>;
async fn register_payment(
&self,
lsp_id: String,
lsp_pubkey: Vec<u8>,
payment_info: grpc::PaymentInformation,
) -> SdkResult<grpc::RegisterPaymentReply>;
}
pub struct Swap {
pub bitcoin_address: String,
pub swapper_pubkey: Vec<u8>,
pub lock_height: i64,
pub error_message: String,
pub required_reserve: i64,
pub swapper_min_payable: i64,
pub swapper_max_payable: i64,
}
#[tonic::async_trait]
pub trait SwapperAPI: Send + Sync {
async fn create_swap(
&self,
hash: Vec<u8>,
payer_pubkey: Vec<u8>,
node_pubkey: String,
) -> SwapResult<Swap>;
async fn complete_swap(&self, bolt11: String) -> Result<()>;
}
#[derive(Clone, PartialEq, Debug, Serialize)]
pub struct ReverseSwapPairInfo {
pub min: u64,
pub max: u64,
pub fees_hash: String,
pub fees_percentage: f64,
pub fees_lockup: u64,
pub fees_claim: u64,
pub total_fees: Option<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct FullReverseSwapInfo {
pub id: String,
pub created_at_block_height: u32,
pub preimage: Vec<u8>,
pub private_key: Vec<u8>,
pub claim_pubkey: String,
pub timeout_block_height: u32,
pub invoice: String,
pub redeem_script: String,
pub onchain_amount_sat: u64,
pub sat_per_vbyte: Option<u32>,
pub receive_amount_sat: Option<u64>,
pub cache: ReverseSwapInfoCached,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ReverseSwapInfoCached {
pub status: ReverseSwapStatus,
pub lockup_txid: Option<String>,
pub claim_txid: Option<String>,
}
impl FullReverseSwapInfo {
pub(crate) fn build_expected_reverse_swap_script(
preimage_hash: Vec<u8>,
compressed_pub_key: Vec<u8>,
sig: Vec<u8>,
lock_height: u32,
) -> ReverseSwapResult<Script> {
let mut ripemd160_hasher = Ripemd160::new();
ripemd160_hasher.update(preimage_hash);
let ripemd160_hash = ripemd160_hasher.finalize();
let timeout_height_le_hex = lock_height.to_le_bytes().to_hex();
let timeout_height_le_hex_trimmed = timeout_height_le_hex.trim_end_matches("00");
let timeout_height_le_bytes = hex::decode(timeout_height_le_hex_trimmed)?;
Ok(Builder::new()
.push_opcode(opcodes::all::OP_SIZE)
.push_slice(&[0x20])
.push_opcode(opcodes::all::OP_EQUAL)
.push_opcode(opcodes::all::OP_IF)
.push_opcode(opcodes::all::OP_HASH160)
.push_slice(&ripemd160_hash[..])
.push_opcode(opcodes::all::OP_EQUALVERIFY)
.push_slice(&compressed_pub_key[..])
.push_opcode(opcodes::all::OP_ELSE)
.push_opcode(opcodes::all::OP_DROP)
.push_slice(&timeout_height_le_bytes)
.push_opcode(opcodes::all::OP_CLTV)
.push_opcode(opcodes::all::OP_DROP)
.push_slice(&sig[..])
.push_opcode(opcodes::all::OP_ENDIF)
.push_opcode(opcodes::all::OP_CHECKSIG)
.into_script())
}
pub(crate) fn validate_redeem_script(
&self,
received_lockup_address: String,
network: Network,
) -> ReverseSwapResult<()> {
let redeem_script_received = Script::from_hex(&self.redeem_script)?;
let asm = redeem_script_received.asm();
debug!("received asm = {asm:?}");
let sk = SecretKey::from_slice(&self.private_key)?;
let pk = PublicKey::from_secret_key(&Secp256k1::new(), &sk);
let asm_elements: Vec<&str> = asm.split(' ').collect();
let refund_address = asm_elements.get(18).unwrap_or(&"").to_string();
let refund_address_bytes = hex::decode(refund_address)?;
let redeem_script_expected = Self::build_expected_reverse_swap_script(
self.get_preimage_hash().to_vec(), pk.serialize().to_vec(), refund_address_bytes,
self.timeout_block_height,
)?;
debug!("expected asm = {:?}", redeem_script_expected.asm());
match redeem_script_received.eq(&redeem_script_expected) {
true => {
let lockup_addr_expected = &received_lockup_address;
let lockup_addr_from_script =
&Address::p2wsh(&redeem_script_received, network.into()).to_string();
match lockup_addr_from_script == lockup_addr_expected {
true => Ok(()),
false => Err(ReverseSwapError::UnexpectedLockupAddress),
}
}
false => Err(ReverseSwapError::UnexpectedRedeemScript),
}
}
pub(crate) fn validate_invoice_amount(
&self,
expected_amount_msat: u64,
) -> ReverseSwapResult<()> {
let inv: crate::lightning_invoice::Bolt11Invoice = self.invoice.parse()?;
let amount_from_invoice_msat = inv.amount_milli_satoshis().unwrap_or_default();
match amount_from_invoice_msat == expected_amount_msat {
false => Err(ReverseSwapError::unexpected_invoice_amount(
"Does not match the request",
)),
true => Ok(()),
}
}
pub(crate) fn validate_invoice(&self, expected_amount_msat: u64) -> ReverseSwapResult<()> {
self.validate_invoice_amount(expected_amount_msat)?;
let inv: crate::lightning_invoice::Bolt11Invoice = self.invoice.parse()?;
let preimage_hash_from_invoice = inv.payment_hash();
let preimage_hash_from_req = &self.get_preimage_hash();
match preimage_hash_from_invoice == preimage_hash_from_req {
false => Err(ReverseSwapError::unexpected_payment_hash(
"Does not match the request",
)),
true => Ok(()),
}
}
pub(crate) fn get_lockup_address(&self, network: Network) -> ReverseSwapResult<Address> {
let redeem_script = Script::from_hex(&self.redeem_script)?;
Ok(Address::p2wsh(&redeem_script, network.into()))
}
pub(crate) fn get_preimage_hash(&self) -> sha256::Hash {
sha256::Hash::hash(&self.preimage)
}
pub(crate) fn get_reverse_swap_info_using_cached_values(&self) -> ReverseSwapInfo {
ReverseSwapInfo {
id: self.id.clone(),
claim_pubkey: self.claim_pubkey.clone(),
lockup_txid: self.cache.clone().lockup_txid,
claim_txid: self.cache.claim_txid.clone(),
onchain_amount_sat: self.onchain_amount_sat,
status: self.cache.status,
}
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
pub struct ReverseSwapInfo {
pub id: String,
pub claim_pubkey: String,
pub lockup_txid: Option<String>,
pub claim_txid: Option<String>,
pub onchain_amount_sat: u64,
pub status: ReverseSwapStatus,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum ReverseSwapStatus {
Initial = 0,
InProgress = 1,
Cancelled = 2,
CompletedSeen = 3,
CompletedConfirmed = 4,
}
impl ReverseSwapStatus {
pub(crate) fn is_monitored_state(&self) -> bool {
matches!(
self,
ReverseSwapStatus::Initial
| ReverseSwapStatus::InProgress
| ReverseSwapStatus::CompletedSeen
)
}
pub(crate) fn is_blocking_state(&self) -> bool {
matches!(
self,
ReverseSwapStatus::Initial | ReverseSwapStatus::InProgress
)
}
}
impl TryFrom<i32> for ReverseSwapStatus {
type Error = anyhow::Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(ReverseSwapStatus::Initial),
1 => Ok(ReverseSwapStatus::InProgress),
2 => Ok(ReverseSwapStatus::Cancelled),
3 => Ok(ReverseSwapStatus::CompletedSeen),
4 => Ok(ReverseSwapStatus::CompletedConfirmed),
_ => Err(anyhow!("illegal value")),
}
}
}
#[tonic::async_trait]
pub(crate) trait ReverseSwapperRoutingAPI: Send + Sync {
async fn fetch_reverse_routing_node(&self) -> ReverseSwapResult<Vec<u8>>;
}
#[tonic::async_trait]
impl ReverseSwapperRoutingAPI for BreezServer {
async fn fetch_reverse_routing_node(&self) -> ReverseSwapResult<Vec<u8>> {
Ok(self
.get_swapper_client()
.await
.get_reverse_routing_node(grpc::GetReverseRoutingNodeRequest::default())
.await
.map(|reply| reply.into_inner().node_id)?)
}
}
#[tonic::async_trait]
pub(crate) trait ReverseSwapServiceAPI: Send + Sync {
async fn fetch_reverse_swap_fees(&self) -> ReverseSwapResult<ReverseSwapPairInfo>;
async fn create_reverse_swap_on_remote(
&self,
send_amount_sat: u64,
preimage_hash_hex: String,
claim_pubkey: String,
pair_hash: String,
routing_node: String,
) -> ReverseSwapResult<BoltzApiCreateReverseSwapResponse>;
async fn get_boltz_status(&self, id: String) -> ReverseSwapResult<BoltzApiReverseSwapStatus>;
async fn get_route_hints(&self, routing_node_id: String) -> ReverseSwapResult<Vec<RouteHint>>;
}
#[derive(Clone, Debug)]
pub struct LogEntry {
pub line: String,
pub level: String,
}
#[derive(Clone)]
pub struct Config {
pub breezserver: String,
pub chainnotifier_url: String,
pub mempoolspace_url: Option<String>,
pub working_dir: String,
pub network: Network,
pub payment_timeout_sec: u32,
pub default_lsp_id: Option<String>,
pub api_key: Option<String>,
pub maxfee_percent: f64,
pub exemptfee_msat: u64,
pub node_config: NodeConfig,
}
impl Config {
pub fn production(api_key: String, node_config: NodeConfig) -> Self {
Config {
breezserver: PRODUCTION_BREEZSERVER_URL.to_string(),
chainnotifier_url: "https://chainnotifier.breez.technology".to_string(),
mempoolspace_url: None,
working_dir: ".".to_string(),
network: Bitcoin,
payment_timeout_sec: 60,
default_lsp_id: None,
api_key: Some(api_key),
maxfee_percent: 1.0,
exemptfee_msat: 20000,
node_config,
}
}
pub fn staging(api_key: String, node_config: NodeConfig) -> Self {
Config {
breezserver: STAGING_BREEZSERVER_URL.to_string(),
chainnotifier_url: "https://chainnotifier.breez.technology".to_string(),
mempoolspace_url: None,
working_dir: ".".to_string(),
network: Bitcoin,
payment_timeout_sec: 60,
default_lsp_id: None,
api_key: Some(api_key),
maxfee_percent: 0.5,
exemptfee_msat: 20000,
node_config,
}
}
}
#[derive(Clone)]
pub enum NodeConfig {
Greenlight { config: GreenlightNodeConfig },
}
#[derive(Clone, Serialize)]
pub enum NodeCredentials {
Greenlight {
credentials: GreenlightDeviceCredentials,
},
}
#[derive(Clone)]
pub struct GreenlightNodeConfig {
pub partner_credentials: Option<GreenlightCredentials>,
pub invite_code: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, EnumString)]
pub enum EnvironmentType {
#[strum(serialize = "production")]
Production,
#[strum(serialize = "staging")]
Staging,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct GreenlightCredentials {
pub developer_key: Vec<u8>,
pub developer_cert: Vec<u8>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct GreenlightDeviceCredentials {
pub device: Vec<u8>,
}
#[derive(Default)]
pub struct ConfigureNodeRequest {
pub close_to_address: Option<String>,
}
pub struct ConnectRequest {
pub config: Config,
pub seed: Vec<u8>,
pub restore_only: Option<bool>,
}
#[derive(PartialEq)]
pub enum PaymentTypeFilter {
Sent,
Received,
ClosedChannel,
}
pub struct MetadataFilter {
pub json_path: String,
pub json_value: String,
}
pub enum FeeratePreset {
Regular,
Economy,
Priority,
}
impl TryFrom<i32> for FeeratePreset {
type Error = anyhow::Error;
fn try_from(value: i32) -> std::result::Result<Self, Self::Error> {
match value {
0 => Ok(FeeratePreset::Regular),
1 => Ok(FeeratePreset::Economy),
2 => Ok(FeeratePreset::Priority),
_ => Err(anyhow!("Unexpected feerate enum value")),
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct BackupStatus {
pub backed_up: bool,
pub last_backup_time: Option<u64>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct NodeState {
pub id: String,
pub block_height: u32,
pub channels_balance_msat: u64,
pub onchain_balance_msat: u64,
#[serde(default)]
pub pending_onchain_balance_msat: u64,
#[serde(default)]
pub utxos: Vec<UnspentTransactionOutput>,
pub max_payable_msat: u64,
pub max_receivable_msat: u64,
pub max_single_payment_amount_msat: u64,
pub max_chan_reserve_msats: u64,
pub connected_peers: Vec<String>,
pub max_receivable_single_payment_amount_msat: u64,
pub total_inbound_liquidity_msats: u64,
}
pub struct SyncResponse {
pub sync_state: Value,
pub node_state: NodeState,
pub payments: Vec<crate::models::Payment>,
pub channels: Vec<crate::models::Channel>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum PaymentStatus {
Pending = 0,
Complete = 1,
Failed = 2,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct Payment {
pub id: String,
pub payment_type: PaymentType,
pub payment_time: i64,
pub amount_msat: u64,
pub fee_msat: u64,
pub status: PaymentStatus,
pub error: Option<String>,
pub description: Option<String>,
pub details: PaymentDetails,
pub metadata: Option<String>,
}
#[derive(Default)]
pub struct PaymentExternalInfo {
pub lnurl_pay_success_action: Option<SuccessActionProcessed>,
pub lnurl_pay_domain: Option<String>,
pub lnurl_pay_comment: Option<String>,
pub lnurl_metadata: Option<String>,
pub ln_address: Option<String>,
pub lnurl_withdraw_endpoint: Option<String>,
pub attempted_amount_msat: Option<u64>,
pub attempted_error: Option<String>,
}
#[derive(Default)]
pub struct ListPaymentsRequest {
pub filters: Option<Vec<PaymentTypeFilter>>,
pub metadata_filters: Option<Vec<MetadataFilter>>,
pub from_timestamp: Option<i64>,
pub to_timestamp: Option<i64>,
pub include_failures: Option<bool>,
pub offset: Option<u32>,
pub limit: Option<u32>,
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct PaymentResponse {
pub payment_time: i64,
pub amount_msat: u64,
pub fee_msat: u64,
pub payment_hash: String,
pub payment_preimage: String,
}
#[allow(clippy::large_enum_variant)]
#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PaymentDetails {
Ln {
#[serde(flatten)]
data: LnPaymentDetails,
},
ClosedChannel {
#[serde(flatten)]
data: ClosedChannelPaymentDetails,
},
}
impl PaymentDetails {
pub fn add_pending_expiration_block(&mut self, htlc: Htlc) {
if let PaymentDetails::Ln { data } = self {
data.pending_expiration_block = Some(htlc.expiry)
}
}
}
#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
pub struct LnPaymentDetails {
pub payment_hash: String,
pub label: String,
pub destination_pubkey: String,
pub payment_preimage: String,
pub keysend: bool,
pub bolt11: String,
pub open_channel_bolt11: Option<String>,
pub lnurl_success_action: Option<SuccessActionProcessed>,
pub lnurl_pay_domain: Option<String>,
pub lnurl_pay_comment: Option<String>,
pub ln_address: Option<String>,
pub lnurl_metadata: Option<String>,
pub lnurl_withdraw_endpoint: Option<String>,
pub swap_info: Option<SwapInfo>,
pub reverse_swap_info: Option<ReverseSwapInfo>,
pub pending_expiration_block: Option<u32>,
}
#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
pub struct ClosedChannelPaymentDetails {
pub state: ChannelState,
pub funding_txid: String,
pub short_channel_id: Option<String>,
pub closing_txid: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ReverseSwapFeesRequest {
pub send_amount_sat: Option<u64>,
pub claim_tx_feerate: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MaxChannelAmount {
pub channel_id: String,
pub amount_msat: u64,
pub path: PaymentPath,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ReceivePaymentRequest {
pub amount_msat: u64,
pub description: String,
pub preimage: Option<Vec<u8>>,
pub opening_fee_params: Option<OpeningFeeParams>,
pub use_description_hash: Option<bool>,
pub expiry: Option<u32>,
pub cltv: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ReceivePaymentResponse {
pub ln_invoice: LNInvoice,
pub opening_fee_params: Option<OpeningFeeParams>,
pub opening_fee_msat: Option<u64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SendPaymentRequest {
pub bolt11: String,
pub use_trampoline: bool,
pub amount_msat: Option<u64>,
pub label: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TlvEntry {
pub field_number: u64,
pub value: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SendSpontaneousPaymentRequest {
pub node_id: String,
pub amount_msat: u64,
pub extra_tlvs: Option<Vec<TlvEntry>>,
pub label: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SendPaymentResponse {
pub payment: Payment,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ReportPaymentFailureDetails {
pub payment_hash: String,
pub comment: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ReportIssueRequest {
PaymentFailure { data: ReportPaymentFailureDetails },
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum HealthCheckStatus {
Operational,
Maintenance,
ServiceDisruption,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ServiceHealthCheckResponse {
pub status: HealthCheckStatus,
}
#[tonic::async_trait]
pub trait SupportAPI: Send + Sync {
async fn service_health_check(&self) -> SdkResult<ServiceHealthCheckResponse>;
async fn report_payment_failure(
&self,
node_state: NodeState,
payment: Payment,
lsp_id: Option<String>,
comment: Option<String>,
) -> SdkResult<()>;
}
#[derive(Clone)]
pub struct StaticBackupRequest {
pub working_dir: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StaticBackupResponse {
pub backup: Option<Vec<String>>,
}
#[derive(Default)]
pub struct OpenChannelFeeRequest {
pub amount_msat: Option<u64>,
pub expiry: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OpenChannelFeeResponse {
pub fee_msat: Option<u64>,
pub fee_params: OpeningFeeParams,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ReceiveOnchainRequest {
pub opening_fee_params: Option<OpeningFeeParams>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ListSwapsRequest {
pub status: Option<Vec<SwapStatus>>,
pub from_timestamp: Option<i64>,
pub to_timestamp: Option<i64>,
pub offset: Option<u32>,
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BuyBitcoinRequest {
pub provider: BuyBitcoinProvider,
pub opening_fee_params: Option<OpeningFeeParams>,
pub redirect_url: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BuyBitcoinResponse {
pub url: String,
pub opening_fee_params: Option<OpeningFeeParams>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RedeemOnchainFundsRequest {
pub to_address: String,
pub sat_per_vbyte: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RedeemOnchainFundsResponse {
pub txid: Vec<u8>,
}
pub enum SwapAmountType {
Send,
Receive,
}
pub struct PrepareOnchainPaymentRequest {
pub amount_sat: u64,
pub amount_type: SwapAmountType,
pub claim_tx_feerate: u32,
}
#[derive(Serialize)]
pub struct OnchainPaymentLimitsResponse {
pub min_sat: u64,
pub max_sat: u64,
pub max_payable_sat: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PrepareOnchainPaymentResponse {
pub fees_hash: String,
pub fees_percentage: f64,
pub fees_lockup: u64,
pub fees_claim: u64,
pub sender_amount_sat: u64,
pub recipient_amount_sat: u64,
pub total_fees: u64,
}
#[derive(Clone, Debug)]
pub struct PayOnchainRequest {
pub recipient_address: String,
pub prepare_res: PrepareOnchainPaymentResponse,
}
#[derive(Serialize)]
pub struct PayOnchainResponse {
pub reverse_swap_info: ReverseSwapInfo,
}
pub struct PrepareRefundRequest {
pub swap_address: String,
pub to_address: String,
pub sat_per_vbyte: u32,
}
pub struct RefundRequest {
pub swap_address: String,
pub to_address: String,
pub sat_per_vbyte: u32,
}
pub struct PrepareRefundResponse {
pub refund_tx_weight: u32,
pub refund_tx_fee_sat: u64,
}
pub struct RefundResponse {
pub refund_tx_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct OpeningFeeParams {
pub min_msat: u64,
pub proportional: u32,
pub valid_until: String,
pub max_idle_time: u32,
pub max_client_to_self_delay: u32,
pub promise: String,
}
impl OpeningFeeParams {
pub(crate) fn valid_until_date(&self) -> Result<DateTime<Utc>> {
Ok(DateTime::parse_from_rfc3339(&self.valid_until).map(|d| d.with_timezone(&Utc))?)
}
pub(crate) fn valid_for(&self, expiry: u32) -> Result<bool> {
Ok(self.valid_until_date()? > Utc::now().add(Duration::seconds(expiry as i64)))
}
pub(crate) fn get_channel_fees_msat_for(&self, amount_msats: u64) -> u64 {
let lsp_fee_msat = amount_msats * self.proportional as u64 / 1_000_000;
let lsp_fee_msat_rounded_to_sat = lsp_fee_msat / 1000 * 1000;
max(lsp_fee_msat_rounded_to_sat, self.min_msat)
}
}
impl From<OpeningFeeParams> for grpc::OpeningFeeParams {
fn from(ofp: OpeningFeeParams) -> Self {
Self {
min_msat: ofp.min_msat,
proportional: ofp.proportional,
valid_until: ofp.valid_until,
max_idle_time: ofp.max_idle_time,
max_client_to_self_delay: ofp.max_client_to_self_delay,
promise: ofp.promise,
}
}
}
impl From<grpc::OpeningFeeParams> for OpeningFeeParams {
fn from(gofp: grpc::OpeningFeeParams) -> Self {
Self {
min_msat: gofp.min_msat,
proportional: gofp.proportional,
valid_until: gofp.valid_until,
max_idle_time: gofp.max_idle_time,
max_client_to_self_delay: gofp.max_client_to_self_delay,
promise: gofp.promise,
}
}
}
impl FromSql for OpeningFeeParams {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
serde_json::from_str(value.as_str()?).map_err(|_| FromSqlError::InvalidType)
}
}
impl ToSql for OpeningFeeParams {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(ToSqlOutput::from(
serde_json::to_string(&self).map_err(|_| FromSqlError::InvalidType)?,
))
}
}
pub enum DynamicFeeType {
Cheapest,
Longest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpeningFeeParamsMenu {
pub values: Vec<OpeningFeeParams>,
}
impl OpeningFeeParamsMenu {
pub fn try_from(values: Vec<sdk_common::grpc::OpeningFeeParams>) -> Result<Self> {
let temp = Self {
values: values
.into_iter()
.map(|ofp| ofp.into())
.collect::<Vec<OpeningFeeParams>>(),
};
temp.validate().map(|_| temp)
}
fn validate(&self) -> Result<()> {
let is_ordered = self.values.windows(2).all(|ofp| {
let larger_min_msat_fee = ofp[0].min_msat < ofp[1].min_msat;
let equal_min_msat_fee = ofp[0].min_msat == ofp[1].min_msat;
let larger_proportional = ofp[0].proportional < ofp[1].proportional;
let equal_proportional = ofp[0].proportional == ofp[1].proportional;
(larger_min_msat_fee && equal_proportional)
|| (equal_min_msat_fee && larger_proportional)
|| (larger_min_msat_fee && larger_proportional)
});
ensure!(is_ordered, "Validation failed: fee params are not ordered");
let is_expired = self.values.iter().any(|ofp| match ofp.valid_until_date() {
Ok(valid_until) => Utc::now() > valid_until,
Err(_) => {
warn!("Failed to parse valid_until for OpeningFeeParams: {ofp:?}");
false
}
});
ensure!(!is_expired, "Validation failed: expired fee params found");
Ok(())
}
pub fn get_cheapest_opening_fee_params(&self) -> Result<OpeningFeeParams> {
self.values.first().cloned().ok_or_else(|| {
anyhow!("The LSP doesn't support opening new channels: Dynamic fees menu contains no values")
})
}
pub fn get_48h_opening_fee_params(&self) -> Result<OpeningFeeParams> {
let now = Utc::now();
let duration_48h = chrono::Duration::hours(48);
let valid_min_48h: Vec<OpeningFeeParams> = self
.values
.iter()
.filter(|ofp| match ofp.valid_until_date() {
Ok(valid_until) => valid_until - now > duration_48h,
Err(_) => {
warn!("Failed to parse valid_until for OpeningFeeParams: {ofp:?}");
false
}
})
.cloned()
.collect();
valid_min_48h.first().cloned().ok_or_else(|| {
anyhow!("The LSP doesn't support opening fees that are valid for at least 48 hours")
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
pub struct Channel {
pub funding_txid: String,
pub short_channel_id: Option<String>,
pub state: ChannelState,
pub spendable_msat: u64,
pub local_balance_msat: u64,
pub receivable_msat: u64,
pub closed_at: Option<u64>,
pub funding_outnum: Option<u32>,
pub alias_local: Option<String>,
pub alias_remote: Option<String>,
pub closing_txid: Option<String>,
pub htlcs: Vec<Htlc>,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
pub struct Htlc {
pub expiry: u32,
pub payment_hash: Vec<u8>,
}
impl Htlc {
pub fn from(expiry: u32, payment_hash: Vec<u8>) -> Self {
Htlc {
expiry,
payment_hash,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, EnumString, Display, Deserialize, Serialize)]
pub enum ChannelState {
PendingOpen,
Opened,
PendingClose,
Closed,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum SwapStatus {
Initial = 0,
WaitingConfirmation = 1,
Redeemable = 2,
Redeemed = 3,
Refundable = 4,
Completed = 5,
}
impl SwapStatus {
pub(crate) fn unused() -> Vec<SwapStatus> {
vec![SwapStatus::Initial]
}
pub(crate) fn in_progress() -> Vec<SwapStatus> {
vec![SwapStatus::Redeemable, SwapStatus::WaitingConfirmation]
}
pub(crate) fn redeemable() -> Vec<SwapStatus> {
vec![SwapStatus::Redeemable]
}
pub(crate) fn refundable() -> Vec<SwapStatus> {
vec![SwapStatus::Refundable]
}
pub(crate) fn monitored() -> Vec<SwapStatus> {
vec![
SwapStatus::Initial,
SwapStatus::WaitingConfirmation,
SwapStatus::Redeemable,
SwapStatus::Redeemed,
SwapStatus::Refundable,
]
}
pub(crate) fn unexpired() -> Vec<SwapStatus> {
vec![
SwapStatus::Initial,
SwapStatus::WaitingConfirmation,
SwapStatus::Redeemable,
SwapStatus::Redeemed,
]
}
}
impl TryFrom<i32> for SwapStatus {
type Error = anyhow::Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(SwapStatus::Initial),
1 => Ok(SwapStatus::WaitingConfirmation),
2 => Ok(SwapStatus::Redeemable),
3 => Ok(SwapStatus::Redeemed),
4 => Ok(SwapStatus::Refundable),
5 => Ok(SwapStatus::Completed),
_ => Err(anyhow!("illegal value")),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct SwapInfo {
pub bitcoin_address: String,
pub created_at: i64,
pub lock_height: i64,
pub payment_hash: Vec<u8>,
pub preimage: Vec<u8>,
pub private_key: Vec<u8>,
pub public_key: Vec<u8>,
pub swapper_public_key: Vec<u8>,
pub script: Vec<u8>,
pub bolt11: Option<String>,
pub paid_msat: u64,
pub total_incoming_txs: u64,
pub confirmed_sats: u64,
pub unconfirmed_sats: u64,
pub status: SwapStatus,
pub refund_tx_ids: Vec<String>,
pub unconfirmed_tx_ids: Vec<String>,
pub confirmed_tx_ids: Vec<String>,
pub min_allowed_deposit: i64,
pub max_allowed_deposit: i64,
pub max_swapper_payable: i64,
pub last_redeem_error: Option<String>,
pub channel_opening_fees: Option<OpeningFeeParams>,
pub confirmed_at: Option<u32>,
}
impl SwapInfo {
pub(crate) fn with_chain_info(&self, onchain_info: SwapChainInfo, tip: u32) -> Self {
let new_info = Self {
confirmed_sats: onchain_info.confirmed_sats,
unconfirmed_sats: onchain_info.unconfirmed_sats,
confirmed_tx_ids: onchain_info.confirmed_tx_ids,
unconfirmed_tx_ids: onchain_info.unconfirmed_tx_ids,
confirmed_at: onchain_info.confirmed_at,
..self.clone()
};
Self {
status: new_info.calculate_status(tip),
..new_info
}
}
pub(crate) fn with_paid_amount(&self, paid_msat: u64, tip: u32) -> Self {
let new_info = Self {
paid_msat,
..self.clone()
};
Self {
status: new_info.calculate_status(tip),
..new_info
}
}
fn calculate_status(&self, tip: u32) -> SwapStatus {
let mut passed_timelock = false;
if let Some(confirmed_at) = self.confirmed_at {
passed_timelock = (tip - confirmed_at) as i64 > self.lock_height;
}
if passed_timelock {
return match self.confirmed_sats {
0 => SwapStatus::Completed,
_ => match (self.paid_msat, self.total_incoming_txs) {
(paid, 1) if paid > 0 => SwapStatus::Completed,
_ => SwapStatus::Refundable,
},
};
}
match (
self.confirmed_at,
self.unconfirmed_sats,
self.confirmed_sats,
self.paid_msat,
) {
(Some(_), 0, 0, _) => SwapStatus::Completed,
(_, _, _, paid) if paid > 0 => SwapStatus::Redeemed,
(_, _, confirmed, _) if confirmed > 0 => SwapStatus::Redeemable,
(_, unconfirmed, _, _) if unconfirmed > 0 => SwapStatus::WaitingConfirmation,
_ => SwapStatus::Initial,
}
}
pub(crate) fn validate_swap_limits(&self) -> SwapResult<()> {
ensure_sdk!(
self.max_allowed_deposit >= self.min_allowed_deposit,
SwapError::unsupported_swap_limits("No allowed deposit amounts")
);
Ok(())
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct UnspentTransactionOutput {
pub txid: Vec<u8>,
pub outnum: u32,
pub amount_millisatoshi: u64,
pub address: String,
#[serde(default)]
pub reserved: bool,
}
#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "buy_bitcoin_provider")]
pub enum BuyBitcoinProvider {
Moonpay,
}
#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
pub struct PrepareRedeemOnchainFundsRequest {
pub to_address: String,
pub sat_per_vbyte: u32,
}
#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
pub struct PrepareRedeemOnchainFundsResponse {
pub tx_weight: u64,
pub tx_fee_sat: u64,
}
impl FromStr for BuyBitcoinProvider {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"moonpay" => Ok(BuyBitcoinProvider::Moonpay),
_ => Err(anyhow!("unknown buy bitcoin provider")),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct PaymentPath {
pub edges: Vec<PaymentPathEdge>,
}
impl PaymentPath {
pub fn final_hop_amount(&self, first_hop_amount_msat: u64) -> u64 {
let mut max_to_send = first_hop_amount_msat;
for h in self.edges.iter().skip(1) {
max_to_send = h.amount_to_forward(max_to_send);
}
max_to_send
}
pub fn first_hop_amount(&self, final_hop_amount_msat: u64) -> u64 {
let mut first_hop_amount = final_hop_amount_msat;
for h in self.edges.iter().skip(1).rev() {
first_hop_amount = h.amount_from_forward(first_hop_amount);
}
first_hop_amount
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct PaymentPathEdge {
pub node_id: Vec<u8>,
pub short_channel_id: String,
pub channel_delay: u64,
pub base_fee_msat: u64,
pub fee_per_millionth: u64,
}
impl PaymentPathEdge {
pub(crate) fn amount_to_forward(&self, in_amount_msat: u64) -> u64 {
let amount_to_forward = Self::divide_ceil(
1_000_000 * (in_amount_msat - self.base_fee_msat),
1_000_000 + self.fee_per_millionth,
);
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);
amount_to_forward
}
pub(crate) fn amount_from_forward(&self, forward_amount_msat: u64) -> u64 {
let in_amount_msat = self.base_fee_msat
+ forward_amount_msat * (1_000_000 + self.fee_per_millionth) / 1_000_000;
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);
in_amount_msat
}
fn divide_ceil(dividend: u64, factor: u64) -> u64 {
(dividend + factor - 1) / factor
}
}
pub(crate) mod sanitize {
use crate::{FullReverseSwapInfo, SwapInfo};
pub(crate) trait Sanitize {
fn sanitize(self) -> Self;
}
pub(crate) fn sanitize_vec<T>(vals: Vec<T>) -> Vec<T>
where
T: Sanitize,
{
vals.into_iter()
.map(|val| val.sanitize())
.collect::<Vec<T>>()
}
impl Sanitize for FullReverseSwapInfo {
fn sanitize(self) -> FullReverseSwapInfo {
FullReverseSwapInfo {
preimage: vec![],
private_key: vec![],
..self.clone()
}
}
}
impl Sanitize for SwapInfo {
fn sanitize(self) -> SwapInfo {
SwapInfo {
preimage: vec![],
private_key: vec![],
..self.clone()
}
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use prost::Message;
use rand::random;
use sdk_common::grpc;
use crate::models::sanitize::Sanitize;
use crate::test_utils::{get_test_ofp, get_test_ofp_48h, rand_string, rand_vec_u8};
use crate::{
FullReverseSwapInfo, OpeningFeeParams, PaymentPath, PaymentPathEdge, ReverseSwapInfoCached,
ReverseSwapStatus, SwapInfo,
};
#[test]
fn test_route_fees() -> Result<()> {
let route = PaymentPath {
edges: vec![
PaymentPathEdge {
node_id: vec![1],
short_channel_id: "807189x2048x0".into(),
channel_delay: 34,
base_fee_msat: 1000,
fee_per_millionth: 10,
},
PaymentPathEdge {
node_id: vec![2],
short_channel_id: "811871x2726x1".into(),
channel_delay: 34,
base_fee_msat: 0,
fee_per_millionth: 0,
},
PaymentPathEdge {
node_id: vec![3],
short_channel_id: "16000000x0x18087".into(),
channel_delay: 40,
base_fee_msat: 1000,
fee_per_millionth: 1,
},
],
};
assert_eq!(route.final_hop_amount(1141000), 1139999);
assert_eq!(route.first_hop_amount(1139999), 1141000);
let route = PaymentPath {
edges: vec![
PaymentPathEdge {
node_id: vec![1],
short_channel_id: "807189x2048x0".into(),
channel_delay: 34,
base_fee_msat: 1000,
fee_per_millionth: 10,
},
PaymentPathEdge {
node_id: vec![2],
short_channel_id: "811871x2726x1".into(),
channel_delay: 34,
base_fee_msat: 0,
fee_per_millionth: 0,
},
PaymentPathEdge {
node_id: vec![3],
short_channel_id: "16000000x0x18087".into(),
channel_delay: 40,
base_fee_msat: 0,
fee_per_millionth: 2000,
},
],
};
assert_eq!(route.final_hop_amount(1141314), 1139036);
assert_eq!(route.first_hop_amount(1139036), 1141314);
Ok(())
}
use super::OpeningFeeParamsMenu;
#[test]
fn test_ofp_menu_validation() -> Result<()> {
OpeningFeeParamsMenu::try_from(vec![get_test_ofp(10, 12, true)])?;
assert!(OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(10, 12, true),
get_test_ofp(10, 12, true),
])
.is_err());
OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(10, 12, true),
get_test_ofp(12, 12, true),
])?;
OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(10, 12, true),
get_test_ofp(10, 14, true),
])?;
OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(10, 12, true),
get_test_ofp(12, 14, true),
])?;
assert!(OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(10, 12, true),
get_test_ofp(10, 12, true),
])
.is_err());
assert!(OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(10, 12, true),
get_test_ofp(10, 10, true),
])
.is_err());
assert!(OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(12, 10, true),
get_test_ofp(10, 10, true),
])
.is_err());
assert!(OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(10, 10, true),
get_test_ofp(12, 12, false),
])
.is_err());
assert!(OpeningFeeParamsMenu::try_from(vec![
get_test_ofp(10, 10, false),
get_test_ofp(12, 12, false),
])
.is_err());
Ok(())
}
#[test]
fn test_payment_information_ser_de() -> Result<()> {
let dummy_payment_info = grpc::PaymentInformation {
payment_hash: rand_vec_u8(10),
payment_secret: rand_vec_u8(10),
destination: rand_vec_u8(10),
incoming_amount_msat: random(),
outgoing_amount_msat: random(),
tag: "".to_string(),
opening_fee_params: None,
};
let mut buf = Vec::with_capacity(dummy_payment_info.encoded_len());
dummy_payment_info.encode(&mut buf)?;
let decoded_payment_info = grpc::PaymentInformation::decode(&*buf)?;
assert_eq!(dummy_payment_info, decoded_payment_info);
Ok(())
}
#[test]
fn test_dynamic_fee_valid_until_format() -> Result<()> {
let mut ofp: OpeningFeeParams = get_test_ofp(1, 1, true).into();
ofp.valid_until = "2023-08-03T00:30:35.117Z".to_string();
ofp.valid_until_date().map(|_| ())
}
#[test]
fn test_sanitization() -> Result<()> {
let rev_swap_info_sanitized = FullReverseSwapInfo {
id: "rev_swap_id".to_string(),
created_at_block_height: 0,
preimage: rand_vec_u8(32),
private_key: vec![],
claim_pubkey: "claim_pubkey".to_string(),
timeout_block_height: 600_000,
invoice: "645".to_string(),
redeem_script: "redeem_script".to_string(),
onchain_amount_sat: 250,
sat_per_vbyte: Some(50),
receive_amount_sat: None,
cache: ReverseSwapInfoCached {
status: ReverseSwapStatus::CompletedConfirmed,
lockup_txid: Some("lockup_txid".to_string()),
claim_txid: Some("claim_txid".to_string()),
},
}
.sanitize();
assert_eq!(rev_swap_info_sanitized.preimage, Vec::<u8>::new());
assert_eq!(rev_swap_info_sanitized.private_key, Vec::<u8>::new());
let swap_info_sanitized = SwapInfo {
bitcoin_address: rand_string(10),
created_at: 10,
lock_height: random(),
payment_hash: rand_vec_u8(32),
preimage: rand_vec_u8(32),
private_key: rand_vec_u8(32),
public_key: rand_vec_u8(10),
swapper_public_key: rand_vec_u8(10),
script: rand_vec_u8(10),
bolt11: None,
paid_msat: 0,
unconfirmed_sats: 0,
confirmed_sats: 0,
total_incoming_txs: 0,
status: crate::models::SwapStatus::Initial,
refund_tx_ids: Vec::new(),
unconfirmed_tx_ids: Vec::new(),
confirmed_tx_ids: Vec::new(),
min_allowed_deposit: 0,
max_allowed_deposit: 100,
max_swapper_payable: 200,
last_redeem_error: None,
channel_opening_fees: Some(get_test_ofp_48h(random(), random()).into()),
confirmed_at: None,
}
.sanitize();
assert_eq!(swap_info_sanitized.preimage, Vec::<u8>::new());
assert_eq!(swap_info_sanitized.private_key, Vec::<u8>::new());
Ok(())
}
}