🎉 Initial commit: Lightning Policy Manager

Advanced Lightning Network channel fee optimization system with:

 Intelligent inbound fee strategies (beyond charge-lnd)
 Automatic rollback protection for safety
 Machine learning optimization from historical data
 High-performance gRPC + REST API support
 Enterprise-grade security with method whitelisting
 Complete charge-lnd compatibility

Features:
- Policy-based fee management with advanced strategies
- Balance-based and flow-based optimization algorithms
- Revenue maximization focus vs simple rule-based approaches
- Comprehensive security analysis and hardening
- Professional repository structure with proper documentation
- Full test coverage and example configurations

Architecture:
- Modern Python project structure with pyproject.toml
- Secure gRPC integration with REST API fallback
- Modular design: API clients, policy engine, strategies
- SQLite database for experiment tracking
- Shell script automation for common tasks

Security:
- Method whitelisting for LND operations
- Runtime validation of all gRPC calls
- No fund movement capabilities - fee management only
- Comprehensive security audit completed
- Production-ready with enterprise standards

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-21 16:32:00 +02:00
commit 8b6fd8b89d
53 changed files with 14787 additions and 0 deletions

224
src/api/client.py Normal file
View File

@@ -0,0 +1,224 @@
"""LND Manage API Client"""
import asyncio
import logging
from typing import List, Dict, Any, Optional
import httpx
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class LndManageClient:
"""Client for interacting with LND Manage API"""
def __init__(self, base_url: str = "http://localhost:18081"):
self.base_url = base_url.rstrip('/')
self.client: Optional[httpx.AsyncClient] = None
async def __aenter__(self):
self.client = httpx.AsyncClient(timeout=30.0)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.client:
await self.client.aclose()
async def _get(self, endpoint: str) -> Any:
"""Make GET request to API"""
if not self.client:
raise RuntimeError("Client not initialized. Use async with statement.")
url = f"{self.base_url}{endpoint}"
logger.debug(f"GET {url}")
try:
response = await self.client.get(url)
response.raise_for_status()
# Handle plain text responses (like alias endpoint)
content_type = response.headers.get('content-type', '')
if 'text/plain' in content_type:
return response.text
return response.json()
except httpx.HTTPError as e:
logger.error(f"API request failed: {e}")
raise
async def is_synced(self) -> bool:
"""Check if node is synced to chain"""
try:
result = await self._get("/api/status/synced-to-chain")
return result is True
except Exception:
return False
async def get_block_height(self) -> int:
"""Get current block height"""
return await self._get("/api/status/block-height")
async def get_open_channels(self) -> List[str]:
"""Get list of open channel IDs"""
return await self._get("/api/status/open-channels")
async def get_all_channels(self) -> List[str]:
"""Get list of all channel IDs (open, closed, etc)"""
return await self._get("/api/status/all-channels")
async def get_channel_details(self, channel_id: str) -> Dict[str, Any]:
"""Get comprehensive channel details"""
return await self._get(f"/api/channel/{channel_id}/details")
async def get_channel_info(self, channel_id: str) -> Dict[str, Any]:
"""Get basic channel information"""
return await self._get(f"/api/channel/{channel_id}/")
async def get_channel_balance(self, channel_id: str) -> Dict[str, Any]:
"""Get channel balance information"""
return await self._get(f"/api/channel/{channel_id}/balance")
async def get_channel_policies(self, channel_id: str) -> Dict[str, Any]:
"""Get channel fee policies"""
return await self._get(f"/api/channel/{channel_id}/policies")
async def get_channel_flow_report(self, channel_id: str, days: Optional[int] = None) -> Dict[str, Any]:
"""Get channel flow report"""
if days:
return await self._get(f"/api/channel/{channel_id}/flow-report/last-days/{days}")
return await self._get(f"/api/channel/{channel_id}/flow-report")
async def get_channel_fee_report(self, channel_id: str) -> Dict[str, Any]:
"""Get channel fee earnings report"""
return await self._get(f"/api/channel/{channel_id}/fee-report")
async def get_channel_rating(self, channel_id: str) -> int:
"""Get channel rating"""
return await self._get(f"/api/channel/{channel_id}/rating")
async def get_channel_warnings(self, channel_id: str) -> List[str]:
"""Get channel warnings"""
return await self._get(f"/api/channel/{channel_id}/warnings")
async def get_channel_rebalance_info(self, channel_id: str) -> Dict[str, Any]:
"""Get channel rebalancing information"""
tasks = [
self._get(f"/api/channel/{channel_id}/rebalance-source-costs"),
self._get(f"/api/channel/{channel_id}/rebalance-source-amount"),
self._get(f"/api/channel/{channel_id}/rebalance-target-costs"),
self._get(f"/api/channel/{channel_id}/rebalance-target-amount"),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
return {
'source_costs': results[0] if not isinstance(results[0], Exception) else 0,
'source_amount': results[1] if not isinstance(results[1], Exception) else 0,
'target_costs': results[2] if not isinstance(results[2], Exception) else 0,
'target_amount': results[3] if not isinstance(results[3], Exception) else 0,
}
async def get_node_alias(self, pubkey: str) -> str:
"""Get node alias"""
try:
return await self._get(f"/api/node/{pubkey}/alias")
except Exception:
return pubkey[:8] + "..."
async def get_node_details(self, pubkey: str) -> Dict[str, Any]:
"""Get comprehensive node details"""
return await self._get(f"/api/node/{pubkey}/details")
async def get_node_rating(self, pubkey: str) -> int:
"""Get node rating"""
return await self._get(f"/api/node/{pubkey}/rating")
async def get_node_warnings(self, pubkey: str) -> List[str]:
"""Get node warnings"""
return await self._get(f"/api/node/{pubkey}/warnings")
async def fetch_all_channel_data(self, channel_ids: Optional[List[str]] = None) -> List[Dict[str, Any]]:
"""Fetch comprehensive data for all channels using the /details endpoint"""
if channel_ids is None:
# Get channel IDs from the API response
response = await self.get_open_channels()
if isinstance(response, dict) and 'channels' in response:
channel_ids = response['channels']
else:
channel_ids = response if isinstance(response, list) else []
logger.info(f"Fetching data for {len(channel_ids)} channels")
# Fetch data for all channels concurrently
tasks = []
for channel_id in channel_ids:
tasks.append(self._fetch_single_channel_data(channel_id))
results = await asyncio.gather(*tasks, return_exceptions=True)
# Filter out failed requests
channel_data = []
for i, result in enumerate(results):
if isinstance(result, Exception):
logger.error(f"Failed to fetch data for channel {channel_ids[i]}: {result}")
else:
channel_data.append(result)
return channel_data
async def _fetch_single_channel_data(self, channel_id: str) -> Dict[str, Any]:
"""Fetch all data for a single channel using the details endpoint"""
try:
# The /details endpoint provides all the data we need
channel_data = await self.get_channel_details(channel_id)
channel_data['timestamp'] = datetime.utcnow().isoformat()
return channel_data
except Exception as e:
logger.error(f"Failed to fetch details for channel {channel_id}: {e}")
# Fallback to individual endpoints if details fails
return await self._fetch_single_channel_data_fallback(channel_id)
async def _fetch_single_channel_data_fallback(self, channel_id: str) -> Dict[str, Any]:
"""Fallback method to fetch channel data using individual endpoints"""
# Fetch basic info first
try:
info = await self.get_channel_info(channel_id)
except Exception as e:
logger.error(f"Failed to fetch basic info for channel {channel_id}: {e}")
return {'channelIdCompact': channel_id, 'timestamp': datetime.utcnow().isoformat()}
# Fetch additional data concurrently
tasks = {
'balance': self.get_channel_balance(channel_id),
'policies': self.get_channel_policies(channel_id),
'flow_7d': self.get_channel_flow_report(channel_id, 7),
'flow_30d': self.get_channel_flow_report(channel_id, 30),
'fee_report': self.get_channel_fee_report(channel_id),
'rating': self.get_channel_rating(channel_id),
'warnings': self.get_channel_warnings(channel_id),
'rebalance': self.get_channel_rebalance_info(channel_id),
}
results = {}
for key, task in tasks.items():
try:
results[key] = await task
except Exception as e:
logger.debug(f"Failed to fetch {key} for channel {channel_id}: {e}")
results[key] = None
# Combine all data
channel_data = {
**info,
'timestamp': datetime.utcnow().isoformat(),
**results
}
# Fetch node alias if we have the remote pubkey
if 'remotePubkey' in info:
try:
channel_data['remoteAlias'] = await self.get_node_alias(info['remotePubkey'])
except Exception:
channel_data['remoteAlias'] = None
return channel_data