mirror of
https://github.com/aljazceru/payments-rest-api.git
synced 2025-12-20 23:14:22 +01:00
291 lines
9.5 KiB
Python
291 lines
9.5 KiB
Python
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
|
|
from dotenv import load_dotenv
|
|
from enum import Enum
|
|
from nodeless import PaymentHandler
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
app = FastAPI(
|
|
title="Breez Nodeless Payments API",
|
|
description="A FastAPI implementation of Breez SDK for Lightning/Liquid payments",
|
|
version="1.0.0"
|
|
)
|
|
|
|
API_KEY_NAME = "x-api-key"
|
|
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
|
API_KEY = os.getenv("API_SECRET")
|
|
|
|
from fastapi import APIRouter
|
|
|
|
ln_router = APIRouter(prefix="/v1/lnurl", tags=["lnurl"])
|
|
|
|
# --- Models ---
|
|
class PaymentMethodEnum(str, Enum):
|
|
LIGHTNING = "LIGHTNING"
|
|
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_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 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
|
|
amount_sat: int
|
|
fees_sat: int
|
|
payment_type: str
|
|
status: 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]
|
|
|
|
class ReceiveResponse(BaseModel):
|
|
destination: str
|
|
fees_sat: int
|
|
|
|
class SendResponse(BaseModel):
|
|
status: str
|
|
destination: Optional[str] = None
|
|
fees_sat: Optional[int] = None
|
|
payment_hash: Optional[str] = None
|
|
swap_id: Optional[str] = None
|
|
|
|
class SendOnchainResponse(BaseModel):
|
|
status: str
|
|
address: str
|
|
fees_sat: Optional[int] = None
|
|
|
|
# LNURL Models
|
|
class ParseInputBody(BaseModel):
|
|
input: str
|
|
|
|
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
|
|
|
|
# --- 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,
|
|
detail="Invalid API Key",
|
|
headers={"WWW-Authenticate": "ApiKey"},
|
|
)
|
|
return api_key
|
|
|
|
def get_payment_handler():
|
|
try:
|
|
return PaymentHandler()
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to initialize PaymentHandler: {str(e)}")
|
|
|
|
# --- API Endpoints ---
|
|
@app.get("/list_payments", response_model=PaymentListResponse)
|
|
async def list_payments(
|
|
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),
|
|
handler: PaymentHandler = Depends(get_payment_handler)
|
|
):
|
|
try:
|
|
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))
|
|
|
|
@app.post("/receive_payment", response_model=ReceiveResponse)
|
|
async def receive_payment(
|
|
request: ReceivePaymentBody,
|
|
api_key: str = Depends(get_api_key),
|
|
handler: PaymentHandler = Depends(get_payment_handler)
|
|
):
|
|
try:
|
|
result = handler.receive_payment(
|
|
amount=request.amount,
|
|
payment_method=request.method.value,
|
|
description=request.description,
|
|
asset_id=request.asset_id
|
|
)
|
|
return result
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.post("/send_payment", response_model=SendResponse)
|
|
async def send_payment(
|
|
request: SendPaymentBody,
|
|
api_key: str = Depends(get_api_key),
|
|
handler: PaymentHandler = Depends(get_payment_handler)
|
|
):
|
|
try:
|
|
result = handler.send_payment(
|
|
destination=request.destination,
|
|
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))
|
|
|
|
@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"}
|
|
|
|
@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.include_router(ln_router)
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8000) |