Merge pull request #2 from breez/nodeless-lib

creating nodeless lib
This commit is contained in:
2025-05-17 11:53:15 +02:00
committed by GitHub
7 changed files with 3427 additions and 248 deletions

View File

@@ -10,6 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-venv \
python3-pip \
python3-full \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
@@ -17,24 +18,24 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 && \
update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1
# Install Poetry
RUN pip install poetry --break-system-packages
# Create a virtual environment
RUN python3 -m venv /app/venv
# Install Poetry in the virtual environment
RUN /app/venv/bin/pip install poetry
# Copy project files
COPY pyproject.toml .
COPY main.py .
COPY fly/pyproject.toml .
COPY fly/main.py .
COPY nodeless.py .
# Copy environment file template
COPY fly/.env.example .env
# Create a README.md file if it doesn't exist to satisfy Poetry
RUN touch README.md
# Copy environment file template
COPY .env.example .env
# Configure Poetry to not create a virtual environment
RUN poetry config virtualenvs.create false
# Install dependencies without installing the project itself
RUN poetry install --no-interaction --no-ansi --no-root
# Install dependencies using the virtual environment's pip
RUN /app/venv/bin/poetry install --no-interaction --no-ansi --no-root
# Create tmp directory for Breez SDK
RUN mkdir -p ./tmp
@@ -44,6 +45,7 @@ EXPOSE 8000
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV PATH="/app/venv/bin:$PATH"
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Run the application (now using the venv's Python)
CMD ["/app/venv/bin/uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -46,51 +46,36 @@ class BreezClient:
)
return self._handle_response(response)
def receive_payment(self, amount, method="LIGHTNING"):
"""
Generate a Lightning/Bitcoin/Liquid invoice to receive payment.
Args:
amount (int): Amount in satoshis to receive
method (str, optional): Payment method (LIGHTNING or LIQUID)
Returns:
dict: JSON response with invoice details
"""
def receive_payment(self, amount, method="LIGHTNING", description=None, asset_id=None):
payload = {
"amount": amount,
"method": method
}
if description is not None:
payload["description"] = description
if asset_id is not None:
payload["asset_id"] = asset_id
response = requests.post(
f"{self.api_url}/receive_payment",
json=payload,
f"{self.api_url}/receive_payment",
json=payload,
headers=self.headers
)
return self._handle_response(response)
def send_payment(self, destination, amount=None, drain=False):
"""
Send a payment via Lightning or Liquid.
Args:
destination (str): Payment destination (invoice or address)
amount (int, optional): Amount in satoshis to send
drain (bool, optional): Whether to drain the wallet
Returns:
dict: JSON response with payment details
"""
def send_payment(self, destination, amount_sat=None, amount_asset=None, asset_id=None, drain=False):
payload = {
"destination": destination
"destination": destination,
"drain": drain
}
if amount is not None:
payload["amount"] = amount
if drain:
payload["drain"] = True
if amount_sat is not None:
payload["amount_sat"] = amount_sat
if amount_asset is not None:
payload["amount_asset"] = amount_asset
if asset_id is not None:
payload["asset_id"] = asset_id
response = requests.post(
f"{self.api_url}/send_payment",
json=payload,
f"{self.api_url}/send_payment",
json=payload,
headers=self.headers
)
return self._handle_response(response)
@@ -105,6 +90,86 @@ class BreezClient:
response = requests.get(f"{self.api_url}/health")
return self._handle_response(response)
def send_onchain(self, address, amount_sat=None, drain=False, fee_rate_sat_per_vbyte=None):
"""
Send an onchain (Bitcoin or Liquid) payment.
Args:
address (str): Destination address
amount_sat (int, optional): Amount in satoshis
drain (bool, optional): Drain all funds
fee_rate_sat_per_vbyte (int, optional): Custom fee rate
Returns:
dict: JSON response
"""
payload = {
"address": address,
"drain": drain
}
if amount_sat is not None:
payload["amount_sat"] = amount_sat
if fee_rate_sat_per_vbyte is not None:
payload["fee_rate_sat_per_vbyte"] = fee_rate_sat_per_vbyte
response = requests.post(
f"{self.api_url}/send_onchain",
json=payload,
headers=self.headers
)
return self._handle_response(response)
# LNURL-related endpoints (all under /v1/ln/)
def parse_input(self, input_str):
response = requests.post(
f"{self.api_url}/v1/ln/parse_input",
json={"input": input_str},
headers=self.headers
)
return self._handle_response(response)
def prepare_lnurl_pay(self, data, amount_sat, comment=None, validate_success_action_url=True):
payload = {
"data": data,
"amount_sat": amount_sat,
"comment": comment,
"validate_success_action_url": validate_success_action_url
}
response = requests.post(
f"{self.api_url}/v1/ln/prepare_lnurl_pay",
json=payload,
headers=self.headers
)
return self._handle_response(response)
def lnurl_pay(self, prepare_response):
payload = {"prepare_response": prepare_response}
response = requests.post(
f"{self.api_url}/v1/ln/lnurl_pay",
json=payload,
headers=self.headers
)
return self._handle_response(response)
def lnurl_auth(self, data):
payload = {"data": data}
response = requests.post(
f"{self.api_url}/v1/ln/lnurl_auth",
json=payload,
headers=self.headers
)
return self._handle_response(response)
def lnurl_withdraw(self, data, amount_msat, comment=None):
payload = {
"data": data,
"amount_msat": amount_msat,
"comment": comment
}
response = requests.post(
f"{self.api_url}/v1/ln/lnurl_withdraw",
json=payload,
headers=self.headers
)
return self._handle_response(response)
def _handle_response(self, response):
"""Helper method to handle API responses."""
try:
@@ -123,7 +188,7 @@ class BreezClient:
if __name__ == "__main__":
# Configuration
API_URL = "http://localhost:8000" # Change to your deployed API URL
API_KEY = "" # Set your API key here
API_KEY = "kurac" # Set your API key here
# Initialize client
breez = BreezClient(api_url=API_URL, api_key=API_KEY)
@@ -135,14 +200,29 @@ if __name__ == "__main__":
# List payments
print("\n🔄 Listing Payments...")
print(json.dumps(breez.list_payments(), indent=2))
# Generate an invoice to receive payment
#print("\n💰 Generating invoice to receive payment...")
#invoice = breez.receive_payment(amount=1000, method="LIGHTNING")
#print(json.dumps(invoice, indent=2))
#print(f"Invoice: {invoice.get('destination', 'Error generating invoice')}")
# Send payment example (commented out for safety)
#print("\n🚀 Sending Payment...")
#result = breez.send_payment(destination="", amount=1111)
#print(json.dumps(result, indent=2))
# LNURL Example Usage
# lnurl = "lnurl1dp68gurn8ghj7mrww4exctnrdakj7mrww4exctnrdakj7mrww4exctnrdakj7" # Replace with a real LNURL
# print("\n🔎 Parsing LNURL...")
# parsed = breez.parse_input(lnurl)
# print(json.dumps(parsed, indent=2))
# if parsed.get("type") == "LN_URL_PAY":
# print("\n📝 Preparing LNURL-Pay...")
# prepare = breez.prepare_lnurl_pay(parsed["data"], amount_sat=1000)
# print(json.dumps(prepare, indent=2))
# print("\n🚀 Executing LNURL-Pay...")
# result = breez.lnurl_pay(prepare)
# print(json.dumps(result, indent=2))
# elif parsed.get("type") == "LN_URL_AUTH":
# print("\n🔐 Executing LNURL-Auth...")
# result = breez.lnurl_auth(parsed["data"])
# print(json.dumps(result, indent=2))
# elif parsed.get("type") == "LN_URL_WITHDRAW":
# print("\n💸 Executing LNURL-Withdraw...")
# result = breez.lnurl_withdraw(parsed["data"], amount_msat=1000_000)
# print(json.dumps(result, indent=2))
# Onchain payment example (commented out for safety)
# print("\n⛓ Sending Onchain Payment...")
# result = breez.send_onchain(address="bitcoin_address_here", amount_sat=10000)
# print(json.dumps(result, indent=2))

View File

@@ -1,73 +1,177 @@
from fastapi import FastAPI, Depends, HTTPException, Header, Query
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException, Header, Query, APIRouter
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel, Field
from typing import Optional, Dict, List, Any, Union
import os
import json
from dotenv import load_dotenv
from enum import Enum
from breez_sdk_liquid import (
LiquidNetwork,
PayAmount,
ConnectRequest,
PrepareSendRequest,
SendPaymentRequest,
PrepareReceiveRequest,
ReceivePaymentRequest,
EventListener,
SdkEvent,
connect,
default_config,
PaymentMethod,
ListPaymentsRequest,
ReceiveAmount
)
from nodeless import PaymentHandler
from shopify.router import router as shopify_router
import logging
import threading
import asyncio
import time
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Load environment variables
load_dotenv()
# Create FastAPI app
_payment_handler = None
_handler_lock = threading.Lock()
_sync_task = None
_last_sync_time = 0
_consecutive_sync_failures = 0
async def periodic_sync_check():
"""Background task to periodically check SDK sync status and attempt resync if needed."""
global _last_sync_time, _consecutive_sync_failures, _payment_handler
while True:
try:
current_time = time.time()
if not _payment_handler:
logger.warning("Payment handler not initialized, waiting...")
await asyncio.sleep(5)
continue
is_synced = _payment_handler.listener.is_synced()
sync_age = current_time - _last_sync_time if _last_sync_time > 0 else float('inf')
# Log sync status with detailed metrics
logger.info(f"SDK sync status check - Synced: {is_synced}, Last sync age: {sync_age:.1f}s, Consecutive failures: {_consecutive_sync_failures}")
if not is_synced or sync_age > 30: # Force resync if not synced or sync is older than 30 seconds
logger.warning(f"SDK sync needed - Status: {'Not synced' if not is_synced else 'Sync too old'}")
# Attempt resync with progressively longer timeouts based on consecutive failures
timeout = min(5 + (_consecutive_sync_failures * 2), 30) # Increase timeout up to 30 seconds
if _payment_handler.wait_for_sync(timeout_seconds=timeout):
logger.info("SDK resync successful")
_last_sync_time = time.time()
_consecutive_sync_failures = 0
else:
logger.error(f"SDK resync failed after {timeout}s timeout")
_consecutive_sync_failures += 1
# If we have too many consecutive failures, try to reinitialize handler
if _consecutive_sync_failures >= 5:
logger.warning("Too many consecutive sync failures, attempting to reinitialize handler...")
try:
with _handler_lock:
_payment_handler.disconnect()
_payment_handler = PaymentHandler()
_consecutive_sync_failures = 0
logger.info("Payment handler reinitialized successfully")
except Exception as e:
logger.error(f"Failed to reinitialize payment handler: {e}")
else:
_last_sync_time = current_time
_consecutive_sync_failures = 0
# Adjust sleep time based on sync status
sleep_time = 10 if not is_synced or _consecutive_sync_failures > 0 else 30
await asyncio.sleep(sleep_time)
except Exception as e:
logger.error(f"Error in periodic sync check: {e}")
_consecutive_sync_failures += 1
await asyncio.sleep(5) # Short sleep on error before retrying
def get_payment_handler():
global _payment_handler
if _payment_handler is None:
with _handler_lock:
if _payment_handler is None:
try:
_payment_handler = PaymentHandler()
except Exception as e:
logger.error(f"Failed to initialize PaymentHandler: {str(e)}")
raise HTTPException(
status_code=500,
detail="Failed to initialize payment system"
)
return _payment_handler
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Lifespan context manager for FastAPI application.
Handles startup and shutdown events.
"""
# Startup
global _payment_handler, _sync_task
try:
_payment_handler = PaymentHandler()
logger.info("Payment system initialized during startup")
# Start background sync check task
_sync_task = asyncio.create_task(periodic_sync_check())
logger.info("Background sync check task started")
except Exception as e:
logger.error(f"Failed to initialize payment system during startup: {str(e)}")
# Don't raise here, let the handler initialize on first request if needed
yield # Server is running
# Shutdown
if _sync_task:
_sync_task.cancel()
try:
await _sync_task
except asyncio.CancelledError:
pass
logger.info("Background sync check task stopped")
if _payment_handler:
try:
_payment_handler.disconnect()
logger.info("Payment system disconnected during shutdown")
except Exception as e:
logger.error(f"Error during payment system shutdown: {str(e)}")
app = FastAPI(
title="Breez Nodeless Payments API",
description="A FastAPI implementation of Breez SDK for Lightning/Liquid payments",
version="1.0.0"
version="1.0.0",
lifespan=lifespan
)
# API Key authentication
API_KEY_NAME = "x-api-key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
# Configure API key from environment variable
API_KEY = os.getenv("API_SECRET")
BREEZ_API_KEY = os.getenv("BREEZ_API_KEY")
SEED_PHRASE = os.getenv("SEED_PHRASE")
# Models for request/response
# Load environment variables
ln_router = APIRouter(prefix="/v1/lnurl", tags=["lnurl"])
app.include_router(shopify_router)
# --- Models ---
class PaymentMethodEnum(str, Enum):
LIGHTNING = "LIGHTNING"
LIQUID = "LIQUID"
BITCOIN_ADDRESS = "BITCOIN_ADDRESS"
LIQUID_ADDRESS = "LIQUID_ADDRESS"
class ReceivePaymentBody(BaseModel):
amount: int = Field(..., description="Amount in satoshis to receive")
method: PaymentMethodEnum = Field(PaymentMethodEnum.LIGHTNING, description="Payment method")
description: Optional[str] = Field(None, description="Optional description for invoice")
asset_id: Optional[str] = Field(None, description="Asset ID for Liquid asset (optional)")
class SendPaymentBody(BaseModel):
destination: str = Field(..., description="Payment destination (invoice or address)")
amount: Optional[int] = Field(None, description="Amount in satoshis to send")
amount_sat: Optional[int] = Field(None, description="Amount in satoshis to send (for Bitcoin)")
amount_asset: Optional[float] = Field(None, description="Amount to send (for asset payments)")
asset_id: Optional[str] = Field(None, description="Asset ID for Liquid asset (optional)")
drain: bool = Field(False, description="Whether to drain the wallet")
class ListPaymentsParams:
def __init__(
self,
from_timestamp: Optional[int] = Query(None, description="Filter payments from this timestamp"),
to_timestamp: Optional[int] = Query(None, description="Filter payments to this timestamp"),
offset: Optional[int] = Query(None, description="Pagination offset"),
limit: Optional[int] = Query(None, description="Pagination limit")
):
self.from_timestamp = from_timestamp
self.to_timestamp = to_timestamp
self.offset = offset
self.limit = limit
class SendOnchainBody(BaseModel):
address: str = Field(..., description="Destination Bitcoin or Liquid address")
amount_sat: Optional[int] = Field(None, description="Amount in satoshis to send (ignored if drain)")
drain: bool = Field(False, description="Send all funds")
fee_rate_sat_per_vbyte: Optional[int] = Field(None, description="Custom fee rate (optional)")
class PaymentResponse(BaseModel):
timestamp: int
@@ -75,9 +179,11 @@ class PaymentResponse(BaseModel):
fees_sat: int
payment_type: str
status: str
details: str
destination: str
details: Any
destination: Optional[str] = None
tx_id: Optional[str] = None
payment_hash: Optional[str] = None
swap_id: Optional[str] = None
class PaymentListResponse(BaseModel):
payments: List[PaymentResponse]
@@ -87,139 +193,56 @@ class ReceiveResponse(BaseModel):
fees_sat: int
class SendResponse(BaseModel):
payment_status: str
destination: str
fees_sat: int
status: str
destination: Optional[str] = None
fees_sat: Optional[int] = None
payment_hash: Optional[str] = None
swap_id: Optional[str] = None
# Breez SDK Event Listener
class SdkListener(EventListener):
def __init__(self):
self.synced = False
self.paid = []
class SendOnchainResponse(BaseModel):
status: str
address: str
fees_sat: Optional[int] = None
def on_event(self, event):
if isinstance(event, SdkEvent.SYNCED):
self.synced = True
if isinstance(event, SdkEvent.PAYMENT_SUCCEEDED):
if event.details.destination:
self.paid.append(event.details.destination)
def is_paid(self, destination: str):
return destination in self.paid
class PaymentStatusResponse(BaseModel):
status: str
amount_sat: Optional[int] = None
fees_sat: Optional[int] = None
payment_time: Optional[int] = None
payment_hash: Optional[str] = None
error: Optional[str] = None
# Initialize Breez SDK client
class BreezClient:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(BreezClient, cls).__new__(cls)
cls._instance.initialize()
return cls._instance
def initialize(self):
if not BREEZ_API_KEY:
raise Exception("Missing Breez API key in environment variables")
if not SEED_PHRASE:
raise Exception("Missing seed phrase in environment variables")
config = default_config(LiquidNetwork.MAINNET, BREEZ_API_KEY)
config.working_dir = './tmp'
connect_request = ConnectRequest(config=config, mnemonic=SEED_PHRASE)
self.instance = connect(connect_request)
self.listener = SdkListener()
self.instance.add_event_listener(self.listener)
self.is_initialized = True
def wait_for_sync(self, timeout_seconds: int = 30):
"""Wait for the SDK to sync before proceeding."""
import time
start_time = time.time()
while time.time() - start_time < timeout_seconds:
if self.listener.synced:
return True
time.sleep(1)
raise Exception("Sync timeout: SDK did not sync within the allocated time.")
def list_payments(self, params: ListPaymentsParams) -> List[Dict[str, Any]]:
self.wait_for_sync()
req = ListPaymentsRequest(
from_timestamp=params.from_timestamp,
to_timestamp=params.to_timestamp,
offset=params.offset,
limit=params.limit
)
payments = self.instance.list_payments(req)
payment_list = []
for payment in payments:
payment_dict = {
'timestamp': payment.timestamp,
'amount_sat': payment.amount_sat,
'fees_sat': payment.fees_sat,
'payment_type': str(payment.payment_type),
'status': str(payment.status),
'details': str(payment.details),
'destination': payment.destination,
'tx_id': payment.tx_id
}
payment_list.append(payment_dict)
return payment_list
def receive_payment(self, amount: int, payment_method: str = 'LIGHTNING') -> Dict[str, Any]:
try:
self.wait_for_sync()
except Exception as e:
raise Exception(f"Error during SDK sync: {e}")
try:
if isinstance(amount, int):
receive_amount = ReceiveAmount.BITCOIN(amount)
else:
receive_amount = amount
prepare_req = PrepareReceiveRequest(payment_method=getattr(PaymentMethod, payment_method), amount=receive_amount)
except Exception as e:
raise Exception(f"Error preparing receive request: {e}")
try:
prepare_res = self.instance.prepare_receive_payment(prepare_req)
except Exception as e:
raise Exception(f"Error preparing receive payment: {e}")
try:
req = ReceivePaymentRequest(prepare_response=prepare_res)
res = self.instance.receive_payment(req)
except Exception as e:
raise Exception(f"Error receiving payment: {e}")
return {
'destination': res.destination,
'fees_sat': prepare_res.fees_sat
}
def send_payment(self, destination: str, amount: Optional[int] = None, drain: bool = False) -> Dict[str, Any]:
self.wait_for_sync()
pay_amount = PayAmount.DRAIN if drain else PayAmount.BITCOIN(amount) if amount else None
prepare_req = PrepareSendRequest(destination=destination, amount=pay_amount)
prepare_res = self.instance.prepare_send_payment(prepare_req)
req = SendPaymentRequest(prepare_response=prepare_res)
res = self.instance.send_payment(req)
return {
'payment_status': 'success',
'destination': res.payment.destination,
'fees_sat': prepare_res.fees_sat
}
# LNURL Models
class ParseInputBody(BaseModel):
input: str
# Dependency for API key validation
class PrepareLnurlPayBody(BaseModel):
data: Dict[str, Any] # The .data dict from parse_input
amount_sat: int
comment: Optional[str] = None
validate_success_action_url: Optional[bool] = True
class LnurlPayBody(BaseModel):
prepare_response: Dict[str, Any] # The dict from prepare_lnurl_pay
class LnurlAuthBody(BaseModel):
data: Dict[str, Any] # The .data dict from parse_input
class LnurlWithdrawBody(BaseModel):
data: Dict[str, Any] # The .data dict from parse_input
amount_msat: int
comment: Optional[str] = None
# Exchange Rate Models
class ExchangeRateResponse(BaseModel):
currency: Optional[str] = None
rate: Optional[float] = None
rates: Optional[Dict[str, float]] = None
# --- Dependencies ---
async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)):
if not API_KEY:
raise HTTPException(status_code=500, detail="API key not configured on server")
if api_key != API_KEY:
raise HTTPException(
status_code=401,
@@ -228,22 +251,24 @@ async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)):
)
return api_key
# Dependency for Breez client
def get_breez_client():
try:
return BreezClient()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to initialize Breez client: {str(e)}")
# API Routes
# --- API Endpoints ---
@app.get("/list_payments", response_model=PaymentListResponse)
async def list_payments(
params: ListPaymentsParams = Depends(),
from_timestamp: Optional[int] = Query(None),
to_timestamp: Optional[int] = Query(None),
offset: Optional[int] = Query(None),
limit: Optional[int] = Query(None),
api_key: str = Depends(get_api_key),
client: BreezClient = Depends(get_breez_client)
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
payments = client.list_payments(params)
params = {
"from_timestamp": from_timestamp,
"to_timestamp": to_timestamp,
"offset": offset,
"limit": limit
}
payments = handler.list_payments(params)
return {"payments": payments}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -252,12 +277,14 @@ async def list_payments(
async def receive_payment(
request: ReceivePaymentBody,
api_key: str = Depends(get_api_key),
client: BreezClient = Depends(get_breez_client)
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
result = client.receive_payment(
result = handler.receive_payment(
amount=request.amount,
payment_method=request.method
payment_method=request.method.value,
description=request.description,
asset_id=request.asset_id
)
return result
except Exception as e:
@@ -267,22 +294,224 @@ async def receive_payment(
async def send_payment(
request: SendPaymentBody,
api_key: str = Depends(get_api_key),
client: BreezClient = Depends(get_breez_client)
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
result = client.send_payment(
result = handler.send_payment(
destination=request.destination,
amount=request.amount,
amount_sat=request.amount_sat,
amount_asset=request.amount_asset,
asset_id=request.asset_id,
drain=request.drain
)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# Health check endpoint
@app.post("/send_onchain", response_model=SendOnchainResponse)
async def send_onchain(
request: SendOnchainBody,
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
# Prepare onchain payment
prepare = handler.prepare_pay_onchain(
amount_sat=request.amount_sat,
drain=request.drain,
fee_rate_sat_per_vbyte=request.fee_rate_sat_per_vbyte
)
# Execute onchain payment
handler.pay_onchain(
address=request.address,
prepare_response=prepare
)
return {"status": "initiated", "address": request.address, "fees_sat": prepare.get("total_fees_sat")}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/onchain_limits")
async def onchain_limits(
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
return handler.fetch_onchain_limits()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health():
return {"status": "ok"}
global _payment_handler
if _payment_handler and _payment_handler.listener.is_synced():
return {"status": "ok", "sdk_synced": True}
return {"status": "ok", "sdk_synced": False}
@app.get("/check_payment_status/{destination}", response_model=PaymentStatusResponse)
async def check_payment_status(
destination: str,
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
"""
Check the status of a payment by its destination/invoice.
Args:
destination: The payment destination (invoice) to check
Returns:
Payment status information including status, amount, fees, and timestamps
"""
logger.info(f"Received payment status check request for destination: {destination[:30]}...")
try:
logger.debug("Initializing PaymentHandler...")
logger.debug(f"Handler instance: {handler}")
logger.debug("Calling check_payment_status method...")
result = handler.check_payment_status(destination)
logger.info(f"Payment status check successful. Status: {result.get('status', 'unknown')}")
logger.debug(f"Full result: {result}")
return result
except ValueError as e:
logger.error(f"Validation error in check_payment_status: {str(e)}")
logger.exception("Validation error details:")
raise HTTPException(status_code=400, detail=str(e))
except AttributeError as e:
logger.error(f"Attribute error in check_payment_status: {str(e)}")
logger.error(f"Handler methods: {dir(handler)}")
logger.exception("Attribute error details:")
raise HTTPException(status_code=500, detail=f"Server configuration error: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in check_payment_status: {str(e)}")
logger.exception("Full error details:")
raise HTTPException(status_code=500, detail=str(e))
@ln_router.post("/parse_input")
async def parse_input(
request: ParseInputBody,
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
return handler.parse_input(request.input)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@ln_router.post("/prepare")
async def prepare(
request: PrepareLnurlPayBody,
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
from breez_sdk_liquid import LnUrlPayRequestData
data_obj = LnUrlPayRequestData(**request.data)
return handler.prepare_lnurl_pay(
data=data_obj,
amount_sat=request.amount_sat,
comment=request.comment,
validate_success_action_url=request.validate_success_action_url
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@ln_router.post("/pay")
async def pay(
request: LnurlPayBody,
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
from breez_sdk_liquid import PrepareLnUrlPayResponse
prepare_obj = PrepareLnUrlPayResponse(**request.prepare_response)
return handler.lnurl_pay(prepare_obj)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@ln_router.post("/auth")
async def auth(
request: LnurlAuthBody,
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
from breez_sdk_liquid import LnUrlAuthRequestData
data_obj = LnUrlAuthRequestData(**request.data)
return {"success": handler.lnurl_auth(data_obj)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@ln_router.post("/withdraw")
async def withdraw(
request: LnurlWithdrawBody,
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
try:
from breez_sdk_liquid import LnUrlWithdrawRequestData
data_obj = LnUrlWithdrawRequestData(**request.data)
return handler.lnurl_withdraw(
data=data_obj,
amount_msat=request.amount_msat,
comment=request.comment
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/exchange_rates/{currency}", response_model=ExchangeRateResponse)
async def get_exchange_rate(
currency: Optional[str] = None,
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
"""
Get current exchange rates, optionally filtered by currency.
Args:
currency: Optional currency code (e.g., 'EUR', 'USD'). If not provided, returns all rates.
Returns:
Exchange rate information for the specified currency or all available currencies.
"""
logger.info(f"Received exchange rate request for currency: {currency}")
try:
result = handler.get_exchange_rate(currency)
# Format response based on whether a specific currency was requested
if currency:
return ExchangeRateResponse(
currency=result['currency'],
rate=result['rate']
)
else:
return ExchangeRateResponse(rates=result)
except ValueError as e:
logger.error(f"Currency not found: {str(e)}")
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Error fetching exchange rate: {str(e)}")
logger.exception("Full error details:")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/exchange_rates", response_model=ExchangeRateResponse)
async def get_all_exchange_rates(
api_key: str = Depends(get_api_key),
handler: PaymentHandler = Depends(get_payment_handler)
):
"""
Get all available exchange rates.
Returns:
Dictionary of all available exchange rates.
"""
logger.info("Received request for all exchange rates")
try:
result = handler.get_exchange_rate()
return ExchangeRateResponse(rates=result)
except Exception as e:
logger.error(f"Error fetching exchange rates: {str(e)}")
logger.exception("Full error details:")
raise HTTPException(status_code=500, detail=str(e))
app.include_router(ln_router)
if __name__ == "__main__":
import uvicorn

1472
fly/nodeless.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ package-mode = false
python = "^3.10"
fastapi = "^0.111.0"
uvicorn = {extras = ["standard"], version = "^0.30.1"}
breez-sdk-liquid = "*"
breez-sdk-liquid = "0.8.2"
python-dotenv = "^1.0.1"
requests = "^2.31.0"

4
fly/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
fastapi
breez-sdk-liquid
python-dotenv

1392
nodeless.py Normal file

File diff suppressed because it is too large Load Diff