use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::error::{BitcoinError, Result};
#[allow(unused_imports)]
use crate::lightning::{ChannelPoint, LightningProvider, NodeInfo, PaymentStatus};
#[async_trait]
pub trait LightningV2Provider: LightningProvider {
async fn open_dual_funded_channel(
&self,
request: DualFundedChannelRequest,
) -> Result<ChannelPoint>;
async fn splice_channel(&self, request: SpliceRequest) -> Result<SpliceResult>;
async fn accept_zero_conf_channel(&self, channel_point: &ChannelPoint) -> Result<()>;
async fn pay_multipath(&self, request: MultiPathPaymentRequest) -> Result<MultiPathPayment>;
async fn create_offer(&self, request: OfferRequest) -> Result<Offer>;
async fn pay_offer(&self, offer_string: &str, amount_msat: Option<u64>)
-> Result<OfferPayment>;
async fn get_route_suggestions(
&self,
destination: &str,
amount_msat: u64,
) -> Result<Vec<RouteSuggestion>>;
async fn optimize_liquidity(&self) -> Result<LiquidityOptimization>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DualFundedChannelRequest {
pub node_pubkey: String,
pub our_funding_sats: u64,
pub their_funding_sats: u64,
pub fee_rate_sat_per_vbyte: u64,
pub private: bool,
pub locktime: Option<u32>,
}
impl DualFundedChannelRequest {
pub fn new(
node_pubkey: impl Into<String>,
our_funding_sats: u64,
their_funding_sats: u64,
) -> Self {
Self {
node_pubkey: node_pubkey.into(),
our_funding_sats,
their_funding_sats,
fee_rate_sat_per_vbyte: 1,
private: false,
locktime: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpliceRequest {
pub channel_point: ChannelPoint,
pub amount_sats: i64,
pub fee_rate_sat_per_vbyte: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpliceResult {
pub new_channel_point: ChannelPoint,
pub splice_txid: String,
pub new_capacity_sats: u64,
pub splice_type: SpliceType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpliceType {
SpliceIn,
SpliceOut,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiPathPaymentRequest {
pub destination: String,
pub amount_msat: u64,
pub payment_hash: String,
pub payment_secret: Option<String>,
pub max_fee_msat: u64,
pub max_paths: u32,
pub timeout_secs: u32,
}
impl MultiPathPaymentRequest {
pub fn new(
destination: impl Into<String>,
amount_msat: u64,
payment_hash: impl Into<String>,
) -> Self {
Self {
destination: destination.into(),
amount_msat,
payment_hash: payment_hash.into(),
payment_secret: None,
max_fee_msat: amount_msat / 100, max_paths: 4,
timeout_secs: 60,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiPathPayment {
pub payment_hash: String,
pub payment_preimage: String,
pub amount_msat: u64,
pub fee_msat: u64,
pub paths: Vec<PaymentPath>,
pub status: PaymentStatus,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentPath {
pub amount_msat: u64,
pub fee_msat: u64,
pub num_hops: u32,
pub succeeded: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfferRequest {
pub description: String,
pub amount_msat: Option<u64>,
pub issuer: Option<String>,
pub features: Vec<u64>,
pub absolute_expiry: Option<u64>,
pub paths: Vec<BlindedPath>,
pub quantity_max: Option<u64>,
}
impl OfferRequest {
pub fn new(description: impl Into<String>) -> Self {
Self {
description: description.into(),
amount_msat: None,
issuer: None,
features: vec![],
absolute_expiry: None,
paths: vec![],
quantity_max: None,
}
}
pub fn with_amount(mut self, amount_msat: u64) -> Self {
self.amount_msat = Some(amount_msat);
self
}
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn with_expiry(mut self, expiry_secs: u64) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
self.absolute_expiry = Some(now + expiry_secs);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Offer {
pub offer_id: String,
pub offer_string: String,
pub description: String,
pub amount_msat: Option<u64>,
pub issuer: Option<String>,
pub absolute_expiry: Option<u64>,
pub active: bool,
}
impl Offer {
pub fn is_expired(&self) -> bool {
if let Some(expiry) = self.absolute_expiry {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
now > expiry
} else {
false
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfferPayment {
pub payment_hash: String,
pub payment_preimage: Option<String>,
pub amount_msat: u64,
pub fee_msat: u64,
pub status: PaymentStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlindedPath {
pub introduction_node: String,
pub blinding_point: String,
pub blinded_hops: Vec<BlindedHop>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlindedHop {
pub blinded_node_id: String,
pub encrypted_data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteSuggestion {
pub hops: Vec<RouteHop>,
pub total_fee_msat: u64,
pub success_probability: f64,
pub timelock_delta: u32,
pub score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteHop {
pub pubkey: String,
pub short_channel_id: String,
pub amount_msat: u64,
pub fee_msat: u64,
pub cltv_expiry_delta: u32,
pub capacity_sats: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidityOptimization {
pub rebalance_operations: Vec<RebalanceOp>,
pub needs_inbound: Vec<String>,
pub needs_outbound: Vec<String>,
pub liquidity_score: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RebalanceOp {
pub from_channel: String,
pub to_channel: String,
pub amount_sats: u64,
pub estimated_fee_msat: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RoutingStrategy {
LowFee,
HighReliability,
FastSettle,
Balanced,
MaxPrivacy,
}
pub struct LiquidityManager {
provider: Box<dyn LightningV2Provider>,
config: LiquidityConfig,
}
impl LiquidityManager {
pub fn new(provider: Box<dyn LightningV2Provider>, config: LiquidityConfig) -> Self {
Self { provider, config }
}
pub async fn auto_optimize(&self) -> Result<LiquidityOptimization> {
self.provider.optimize_liquidity().await
}
pub async fn needs_more_channels(&self) -> Result<bool> {
let info = self.provider.get_info().await?;
Ok(info.num_active_channels < self.config.min_channels)
}
pub async fn execute_rebalancing(&self, ops: Vec<RebalanceOp>) -> Result<Vec<RebalanceResult>> {
let mut results = Vec::new();
for op in ops {
let result = self.execute_single_rebalance(&op).await;
results.push(RebalanceResult {
operation: op.clone(),
success: result.is_ok(),
error: result.err().map(|e| e.to_string()),
});
}
Ok(results)
}
#[allow(dead_code)]
async fn execute_single_rebalance(&self, op: &RebalanceOp) -> Result<()> {
let _ = op;
Err(BitcoinError::Wallet(
"Rebalancing not yet implemented".to_string(),
))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidityConfig {
pub min_channels: u32,
pub target_inbound_sats: u64,
pub target_outbound_sats: u64,
pub max_rebalance_fee_sats: u64,
pub rebalance_interval: Duration,
}
impl Default for LiquidityConfig {
fn default() -> Self {
Self {
min_channels: 3,
target_inbound_sats: 500_000,
target_outbound_sats: 500_000,
max_rebalance_fee_sats: 1000,
rebalance_interval: Duration::from_secs(3600), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RebalanceResult {
pub operation: RebalanceOp,
pub success: bool,
pub error: Option<String>,
}
pub struct MultiPathRouter {
provider: Box<dyn LightningV2Provider>,
strategy: RoutingStrategy,
}
impl MultiPathRouter {
pub fn new(provider: Box<dyn LightningV2Provider>, strategy: RoutingStrategy) -> Self {
Self { provider, strategy }
}
pub async fn find_routes(
&self,
destination: &str,
amount_msat: u64,
max_paths: u32,
) -> Result<Vec<RouteSuggestion>> {
let suggestions = self
.provider
.get_route_suggestions(destination, amount_msat)
.await?;
let mut filtered = self.filter_by_strategy(suggestions);
filtered.truncate(max_paths as usize);
Ok(filtered)
}
fn filter_by_strategy(&self, mut routes: Vec<RouteSuggestion>) -> Vec<RouteSuggestion> {
routes.sort_by(|a, b| {
let score_a = self.calculate_route_score(a);
let score_b = self.calculate_route_score(b);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
routes
}
fn calculate_route_score(&self, route: &RouteSuggestion) -> f64 {
match self.strategy {
RoutingStrategy::LowFee => 1.0 / (route.total_fee_msat as f64 + 1.0),
RoutingStrategy::HighReliability => route.success_probability,
RoutingStrategy::FastSettle => 1.0 / (route.timelock_delta as f64 + 1.0),
RoutingStrategy::Balanced => {
route.success_probability * 0.5
+ (1.0 / (route.total_fee_msat as f64 + 1.0)) * 0.3
+ (1.0 / (route.timelock_delta as f64 + 1.0)) * 0.2
}
RoutingStrategy::MaxPrivacy => route.hops.len() as f64,
}
}
pub async fn pay(&self, request: MultiPathPaymentRequest) -> Result<MultiPathPayment> {
self.provider.pay_multipath(request).await
}
}
pub struct ZeroConfManager {
provider: Box<dyn LightningV2Provider>,
trusted_peers: Vec<String>,
}
impl ZeroConfManager {
pub fn new(provider: Box<dyn LightningV2Provider>) -> Self {
Self {
provider,
trusted_peers: Vec::new(),
}
}
pub fn add_trusted_peer(&mut self, pubkey: impl Into<String>) {
self.trusted_peers.push(pubkey.into());
}
pub fn is_trusted(&self, pubkey: &str) -> bool {
self.trusted_peers.contains(&pubkey.to_string())
}
pub async fn accept_channel(
&self,
channel_point: &ChannelPoint,
peer_pubkey: &str,
) -> Result<()> {
if !self.is_trusted(peer_pubkey) {
return Err(BitcoinError::Wallet(
"Peer not trusted for zero-conf channels".to_string(),
));
}
self.provider.accept_zero_conf_channel(channel_point).await
}
}
pub struct OfferManager {
provider: Box<dyn LightningV2Provider>,
active_offers: HashMap<String, Offer>,
}
impl OfferManager {
pub fn new(provider: Box<dyn LightningV2Provider>) -> Self {
Self {
provider,
active_offers: HashMap::new(),
}
}
pub async fn create_offer(&mut self, request: OfferRequest) -> Result<Offer> {
let offer = self.provider.create_offer(request).await?;
self.active_offers
.insert(offer.offer_id.clone(), offer.clone());
Ok(offer)
}
pub fn get_offer(&self, offer_id: &str) -> Option<&Offer> {
self.active_offers.get(offer_id)
}
pub fn deactivate_offer(&mut self, offer_id: &str) -> Result<()> {
if let Some(offer) = self.active_offers.get_mut(offer_id) {
offer.active = false;
Ok(())
} else {
Err(BitcoinError::Wallet("Offer not found".to_string()))
}
}
pub async fn pay_offer(
&self,
offer_string: &str,
amount_msat: Option<u64>,
) -> Result<OfferPayment> {
self.provider.pay_offer(offer_string, amount_msat).await
}
pub fn cleanup_expired(&mut self) -> usize {
let initial_count = self.active_offers.len();
self.active_offers.retain(|_, offer| !offer.is_expired());
initial_count - self.active_offers.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dual_funded_channel_request() {
let request = DualFundedChannelRequest::new("03abc...".to_string(), 1_000_000, 500_000);
assert_eq!(request.our_funding_sats, 1_000_000);
assert_eq!(request.their_funding_sats, 500_000);
}
#[test]
fn test_multipath_payment_request() {
let request =
MultiPathPaymentRequest::new("03def...".to_string(), 100_000_000, "payment_hash_123");
assert_eq!(request.amount_msat, 100_000_000);
assert_eq!(request.max_paths, 4);
}
#[test]
fn test_offer_builder() {
let offer_request = OfferRequest::new("Test product")
.with_amount(50_000_000)
.with_issuer("Test Store")
.with_expiry(86400);
assert_eq!(offer_request.amount_msat, Some(50_000_000));
assert_eq!(offer_request.issuer, Some("Test Store".to_string()));
assert!(offer_request.absolute_expiry.is_some());
}
#[test]
fn test_routing_strategy_scoring() {
let route = RouteSuggestion {
hops: vec![],
total_fee_msat: 1000,
success_probability: 0.95,
timelock_delta: 40,
score: 0.0,
};
let router = MultiPathRouter {
provider: Box::new(MockProvider),
strategy: RoutingStrategy::HighReliability,
};
let score = router.calculate_route_score(&route);
assert!((score - 0.95).abs() < 0.01);
}
#[test]
fn test_liquidity_config_default() {
let config = LiquidityConfig::default();
assert_eq!(config.min_channels, 3);
assert_eq!(config.target_inbound_sats, 500_000);
}
#[test]
fn test_splice_type() {
assert_eq!(SpliceType::SpliceIn, SpliceType::SpliceIn);
assert_ne!(SpliceType::SpliceIn, SpliceType::SpliceOut);
}
#[test]
fn test_offer_expiry() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let expired_offer = Offer {
offer_id: "offer1".to_string(),
offer_string: "lno1...".to_string(),
description: "Test".to_string(),
amount_msat: Some(1000),
issuer: None,
absolute_expiry: Some(now - 3600), active: true,
};
assert!(expired_offer.is_expired());
let active_offer = Offer {
offer_id: "offer2".to_string(),
offer_string: "lno1...".to_string(),
description: "Test".to_string(),
amount_msat: Some(1000),
issuer: None,
absolute_expiry: Some(now + 3600), active: true,
};
assert!(!active_offer.is_expired());
}
struct MockProvider;
#[async_trait]
impl LightningProvider for MockProvider {
async fn get_info(&self) -> Result<NodeInfo> {
unimplemented!()
}
async fn create_invoice(
&self,
_request: crate::lightning::InvoiceRequest,
) -> Result<crate::lightning::Invoice> {
unimplemented!()
}
async fn get_invoice(&self, _payment_hash: &str) -> Result<crate::lightning::Invoice> {
unimplemented!()
}
async fn pay_invoice(
&self,
_bolt11: &str,
_max_fee_msat: Option<u64>,
) -> Result<crate::lightning::Payment> {
unimplemented!()
}
async fn get_balance(&self) -> Result<crate::lightning::ChannelBalance> {
unimplemented!()
}
async fn list_channels(&self) -> Result<Vec<crate::lightning::Channel>> {
unimplemented!()
}
async fn open_channel(
&self,
_request: crate::lightning::OpenChannelRequest,
) -> Result<ChannelPoint> {
unimplemented!()
}
async fn close_channel(
&self,
_channel_point: &ChannelPoint,
_force: bool,
) -> Result<String> {
unimplemented!()
}
async fn subscribe_invoices(&self) -> Result<crate::lightning::InvoiceSubscription> {
unimplemented!()
}
}
#[async_trait]
impl LightningV2Provider for MockProvider {
async fn open_dual_funded_channel(
&self,
_request: DualFundedChannelRequest,
) -> Result<ChannelPoint> {
unimplemented!()
}
async fn splice_channel(&self, _request: SpliceRequest) -> Result<SpliceResult> {
unimplemented!()
}
async fn accept_zero_conf_channel(&self, _channel_point: &ChannelPoint) -> Result<()> {
unimplemented!()
}
async fn pay_multipath(
&self,
_request: MultiPathPaymentRequest,
) -> Result<MultiPathPayment> {
unimplemented!()
}
async fn create_offer(&self, _request: OfferRequest) -> Result<Offer> {
unimplemented!()
}
async fn pay_offer(
&self,
_offer_string: &str,
_amount_msat: Option<u64>,
) -> Result<OfferPayment> {
unimplemented!()
}
async fn get_route_suggestions(
&self,
_destination: &str,
_amount_msat: u64,
) -> Result<Vec<RouteSuggestion>> {
unimplemented!()
}
async fn optimize_liquidity(&self) -> Result<LiquidityOptimization> {
unimplemented!()
}
}
}