use crate::AnyaError;
use crate::AnyaResult;
use secp256k1::SecretKey as Secp256k1SecretKey;
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::bitcoin::config::BitcoinConfig;
#[derive(Clone)]
pub struct LightningPublicKey {
pub bytes: [u8; 33],
}
impl fmt::Debug for LightningPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "LightningPublicKey({})", hex::encode(self.bytes))
}
}
impl std::str::FromStr for LightningPublicKey {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != 66 {
return Err("Invalid public key length".to_string());
}
let hex_str = s.strip_prefix("0x").unwrap_or(s);
let mut bytes = [0u8; 33];
hex::decode_to_slice(hex_str, &mut bytes)
.map_err(|e| format!("Invalid hex format: {e}"))?;
Ok(LightningPublicKey { bytes })
}
}
impl LightningPublicKey {
pub fn from_secret_key(
secp: &secp256k1::Secp256k1<secp256k1::All>,
secret_key: &Secp256k1SecretKey,
) -> Self {
let public_key = secp256k1::PublicKey::from_secret_key(secp, secret_key);
let mut bytes = [0u8; 33];
bytes.copy_from_slice(&public_key.serialize());
LightningPublicKey { bytes }
}
}
impl fmt::Display for LightningPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex::encode(self.bytes))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LightningTxid([u8; 32]);
impl LightningTxid {
pub fn from_slice(slice: &[u8]) -> Result<Self, String> {
if slice.len() != 32 {
return Err("Invalid txid length".to_string());
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(slice);
Ok(LightningTxid(bytes))
}
}
impl fmt::Display for LightningTxid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
#[derive(Debug, Clone)]
pub struct NodeInfo {
pub pubkey: String,
pub addresses: Vec<String>,
pub alias: Option<String>,
pub color: Option<String>,
pub features: Vec<String>,
}
pub struct LightningNode {
config: BitcoinConfig,
state: Mutex<LightningState>,
#[allow(dead_code)]
secp: LightningSecp256k1<All>,
pub node_id: LightningPublicKey,
}
struct LightningState {
channels: HashMap<String, Channel>,
peers: HashMap<String, PeerInfo>,
invoices: HashMap<String, Invoice>,
payments: HashMap<String, Payment>,
last_updated: u64,
}
#[derive(Debug, Clone)]
pub struct Channel {
pub channel_id: String,
pub funding_txid: LightningTxid,
pub funding_output_idx: u32,
pub capacity: u64,
pub local_balance: u64,
pub remote_balance: u64,
pub remote_pubkey: LightningPublicKey,
pub is_active: bool,
pub is_public: bool,
pub short_channel_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PeerInfo {
pub pubkey: LightningPublicKey,
pub addresses: Vec<String>,
pub alias: Option<String>,
pub color: Option<String>,
pub is_connected: bool,
pub connected_since: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct Invoice {
pub bolt11: String,
pub payment_hash: String,
pub description: String,
pub amount_msat: Option<u64>,
pub expiry: u32,
pub timestamp: u64,
pub is_paid: bool,
pub paid_at: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct Payment {
pub payment_id: String,
pub payment_hash: String,
pub preimage: Option<String>,
pub amount_msat: u64,
pub fee_msat: u64,
pub status: PaymentStatus,
pub created_at: u64,
pub resolved_at: Option<u64>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaymentStatus {
Pending,
Succeeded,
Failed,
}
pub struct BitcoinLightningBridge {
lightning_node: Arc<LightningNode>,
channel_transactions: Mutex<HashMap<String, ChannelTransaction>>,
funding_addresses: Mutex<HashMap<String, FundingAddress>>,
last_scanned_height: Mutex<u32>,
}
#[derive(Debug, Clone)]
pub struct ChannelTransaction {
pub channel_id: String,
pub funding_txid: LightningTxid,
pub funding_output_idx: u32,
pub funding_amount: u64,
pub status: ChannelTransactionStatus,
pub confirmation_height: Option<u32>,
pub closing_txid: Option<LightningTxid>,
pub created_at: u64,
pub updated_at: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelTransactionStatus {
Pending,
Confirmed,
Closed,
}
#[derive(Debug, Clone)]
pub struct FundingAddress {
pub address: String,
pub required_amount: u64,
pub channel_params: ChannelParameters,
pub created_at: u64,
}
#[derive(Debug, Clone)]
pub struct ChannelParameters {
pub peer_pubkey: LightningPublicKey,
pub push_msat: Option<u64>,
pub is_private: bool,
}
impl LightningNode {
pub fn new(config: &BitcoinConfig) -> AnyaResult<Self> {
let secp = LightningSecp256k1::new();
let node_secret = Secp256k1SecretKey::from_slice(&[0x42; 32])
.map_err(|e| AnyaError::Bitcoin(format!("Failed to create Lightning node key: {e}")))?;
let node_id = LightningPublicKey::from_secret_key(&secp, &node_secret);
let state = LightningState {
channels: HashMap::new(),
peers: HashMap::new(),
invoices: HashMap::new(),
payments: HashMap::new(),
last_updated: current_time(),
};
Ok(Self {
config: config.clone(),
state: Mutex::new(state),
secp,
node_id,
})
}
pub fn get_node_info(&self) -> AnyaResult<NodeInfo> {
Ok(NodeInfo {
pubkey: self.node_id.to_string(),
addresses: vec![format!("127.0.0.1:9735")], alias: Some("Anya Lightning Node".to_string()),
color: Some("#3399FF".to_string()),
features: vec![
"option_static_remotekey".to_string(),
"option_anchor_outputs".to_string(),
"option_route_blinding".to_string(),
],
})
}
pub fn connect_peer(&self, node_pubkey: &str, host: &str, port: u16) -> AnyaResult<()> {
let pubkey = LightningPublicKey::from_str(node_pubkey)
.map_err(|e| AnyaError::Bitcoin(format!("Invalid node pubkey: {e}")))?;
let mut state = self
.state
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
if state.peers.contains_key(node_pubkey) {
return Err(AnyaError::Bitcoin(format!(
"Already connected to {node_pubkey}"
)));
}
let peer_info = PeerInfo {
pubkey,
addresses: vec![format!("{}:{}", host, port)],
alias: None,
color: None,
is_connected: true,
connected_since: Some(current_time()),
};
state.peers.insert(node_pubkey.to_string(), peer_info);
state.last_updated = current_time();
Ok(())
}
pub fn list_peers(&self) -> AnyaResult<Vec<PeerInfo>> {
let state = self
.state
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
Ok(state.peers.values().cloned().collect())
}
pub fn open_channel(
&self,
node_pubkey: &str,
capacity: u64,
push_msat: Option<u64>,
is_private: bool,
) -> AnyaResult<Channel> {
let pubkey = LightningPublicKey::from_str(node_pubkey)
.map_err(|e| AnyaError::Bitcoin(format!("Invalid node pubkey: {e}")))?;
let mut state = self
.state
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
if !state.peers.contains_key(node_pubkey) {
return Err(AnyaError::Bitcoin(format!(
"Not connected to peer {node_pubkey}"
)));
}
let channel_id = format!("channel_{:x}", rand::random::<u64>());
let funding_txid = LightningTxid::from_slice(&[0x42; 32])
.map_err(|e| AnyaError::Bitcoin(format!("Failed to create txid: {e}")))?;
let push_amount = push_msat.unwrap_or(0) / 1000; let local_balance = capacity - push_amount;
let remote_balance = push_amount;
let channel = Channel {
channel_id: channel_id.clone(),
funding_txid,
funding_output_idx: 0,
capacity,
local_balance,
remote_balance,
remote_pubkey: pubkey,
is_active: true,
is_public: !is_private,
short_channel_id: None,
};
state.channels.insert(channel_id, channel.clone());
state.last_updated = current_time();
Ok(channel)
}
pub fn list_channels(&self) -> AnyaResult<Vec<Channel>> {
let state = self
.state
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
Ok(state.channels.values().cloned().collect())
}
pub fn create_invoice(
&self,
amount_msat: Option<u64>,
description: &str,
expiry: Option<u32>,
) -> AnyaResult<Invoice> {
let mut state = self
.state
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
let now = current_time();
let payment_hash = format!("hash_{:x}", rand::random::<u64>());
let network_prefix = match self.config.network.as_str() {
"bitcoin" => "lnbc",
"testnet" => "lntb",
"regtest" => "lnbcrt",
"signet" => "lnsb",
_ => "lnbc", };
let amount_part = match amount_msat {
Some(amt) => format!("{}", amt / 1000), None => "any".to_string(),
};
let bolt11 = format!(
"{}{}{}{}",
network_prefix,
amount_part,
description.chars().take(10).collect::<String>(),
now % 1000000
);
let invoice = Invoice {
bolt11,
payment_hash: payment_hash.clone(),
description: description.to_string(),
amount_msat,
expiry: expiry.unwrap_or(3600), timestamp: now,
is_paid: false,
paid_at: None,
};
state.invoices.insert(payment_hash, invoice.clone());
state.last_updated = now;
Ok(invoice)
}
pub fn pay_invoice(&self, bolt11: &str, amount_msat: Option<u64>) -> AnyaResult<Payment> {
let mut state = self
.state
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
let now = current_time();
let payment_hash = format!("hash_{:x}", rand::random::<u64>());
let payment_id = format!("pay_{:x}", rand::random::<u64>());
let invoice_amount = amount_msat.unwrap_or(10_000);
let preimage = format!("preimage_{:x}", rand::random::<u64>());
let payment = Payment {
payment_id: payment_id.clone(),
payment_hash: payment_hash.clone(),
preimage: Some(preimage),
amount_msat: invoice_amount,
fee_msat: invoice_amount / 100, status: PaymentStatus::Succeeded, created_at: now,
resolved_at: Some(now),
description: Some(format!("Payment for invoice {bolt11}")),
};
state.payments.insert(payment_id, payment.clone());
state.last_updated = now;
Ok(payment)
}
pub fn decode_invoice(&self, bolt11: &str) -> AnyaResult<Invoice> {
let payment_hash = format!("hash_{:x}", rand::random::<u64>());
Ok(Invoice {
bolt11: bolt11.to_string(),
payment_hash,
description: "Decoded invoice".to_string(),
amount_msat: Some(50_000), expiry: 3600,
timestamp: current_time(),
is_paid: false,
paid_at: None,
})
}
pub fn get_payment(&self, payment_hash: &str) -> AnyaResult<Option<Payment>> {
let state = self
.state
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
let payment = state
.payments
.values()
.find(|p| p.payment_hash == payment_hash)
.cloned();
Ok(payment)
}
pub fn list_payments(&self) -> AnyaResult<Vec<Payment>> {
let state = self
.state
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
Ok(state.payments.values().cloned().collect())
}
}
impl BitcoinLightningBridge {
pub fn new(lightning_node: Arc<LightningNode>) -> AnyaResult<Self> {
Ok(Self {
lightning_node,
channel_transactions: Mutex::new(HashMap::new()),
funding_addresses: Mutex::new(HashMap::new()),
last_scanned_height: Mutex::new(0),
})
}
pub fn init(&self, current_height: u32) -> AnyaResult<()> {
let mut last_height = self
.last_scanned_height
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
*last_height = current_height;
Ok(())
}
pub fn create_funding_address(
&self,
peer_pubkey: &str,
amount_sat: u64,
push_msat: Option<u64>,
is_private: bool,
) -> AnyaResult<String> {
let peers = self.lightning_node.list_peers()?;
let pubkey = LightningPublicKey::from_str(peer_pubkey)
.map_err(|e| AnyaError::Bitcoin(format!("Invalid node pubkey: {e}")))?;
let is_connected = peers.iter().any(|p| p.pubkey.to_string() == peer_pubkey);
if !is_connected {
return Err(AnyaError::Bitcoin(format!(
"Not connected to peer {peer_pubkey}"
)));
}
let address = format!("bc1q{:x}", rand::random::<u64>());
let channel_params = ChannelParameters {
peer_pubkey: pubkey,
push_msat,
is_private,
};
let funding_address = FundingAddress {
address: address.clone(),
required_amount: amount_sat,
channel_params,
created_at: current_time(),
};
let mut funding_addresses = self
.funding_addresses
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
funding_addresses.insert(address.clone(), funding_address);
Ok(address)
}
pub fn register_channel_transaction(
&self,
channel_id: &str,
funding_txid: &str,
funding_output_idx: u32,
funding_amount: u64,
) -> AnyaResult<()> {
let txid = LightningTxid::from_slice(&hex::decode(funding_txid).unwrap_or_default())
.map_err(|e| AnyaError::Bitcoin(format!("Invalid txid: {e}")))?;
let channel_transaction = ChannelTransaction {
channel_id: channel_id.to_string(),
funding_txid: txid,
funding_output_idx,
funding_amount,
status: ChannelTransactionStatus::Pending,
confirmation_height: None,
closing_txid: None,
created_at: current_time(),
updated_at: current_time(),
};
let mut transactions = self
.channel_transactions
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
transactions.insert(channel_id.to_string(), channel_transaction);
Ok(())
}
pub fn update_channel_transaction(
&self,
channel_id: &str,
status: ChannelTransactionStatus,
confirmation_height: Option<u32>,
) -> AnyaResult<()> {
let mut transactions = self
.channel_transactions
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
if let Some(transaction) = transactions.get_mut(channel_id) {
transaction.status = status;
transaction.confirmation_height = confirmation_height;
transaction.updated_at = current_time();
}
Ok(())
}
pub fn get_channel_transaction(
&self,
channel_id: &str,
) -> AnyaResult<Option<ChannelTransaction>> {
let transactions = self
.channel_transactions
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
Ok(transactions.get(channel_id).cloned())
}
pub fn list_channel_transactions(&self) -> AnyaResult<Vec<ChannelTransaction>> {
let transactions = self
.channel_transactions
.lock()
.map_err(|e| format!("Mutex lock error: {e}"))?;
Ok(transactions.values().cloned().collect())
}
}
fn current_time() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
type LightningSecp256k1<T> = secp256k1::Secp256k1<T>;
type All = secp256k1::All;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lightning_public_key_from_str() {
let valid_key = "02".to_string() + &"a".repeat(64);
let pubkey = LightningPublicKey::from_str(&valid_key);
assert!(pubkey.is_ok());
}
#[test]
fn test_lightning_public_key_invalid_length() {
let invalid_key = "02".to_string() + &"a".repeat(32); let pubkey = LightningPublicKey::from_str(&invalid_key);
assert!(pubkey.is_err());
}
#[test]
fn test_lightning_txid_from_slice() {
let valid_txid = [0x42u8; 32];
let txid = LightningTxid::from_slice(&valid_txid);
assert!(txid.is_ok());
}
#[test]
fn test_lightning_txid_invalid_length() {
let invalid_txid = [0x42u8; 16]; let txid = LightningTxid::from_slice(&invalid_txid);
assert!(txid.is_err());
}
}