blitz_api/app/lightning/router.py
2025-05-06 09:16:46 +02:00

522 lines
16 KiB
Python

from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query, status
from fastapi.params import Depends
from app.auth.auth_bearer import JWTBearer
from app.lightning.docs import (
get_balance_response_desc,
new_address_desc,
open_channel_desc,
send_coins_desc,
send_payment_desc,
unlock_wallet_desc,
)
from app.lightning.models import (
Channel,
FeeRevenue,
GenericTx,
Invoice,
LnInfo,
NewAddressInput,
OnChainTransaction,
Payment,
PaymentRequest,
SendCoinsInput,
SendCoinsResponse,
UnlockWalletInput,
WalletBalance,
)
from app.lightning.service import (
add_invoice,
channel_close,
channel_list,
channel_open,
decode_pay_request,
get_fee_revenue,
get_ln_info,
get_wallet_balance,
list_all_tx,
list_invoices,
list_on_chain_tx,
list_payments,
new_address,
send_coins,
send_payment,
unlock_wallet,
)
_PREFIX = "lightning"
router = APIRouter(prefix=f"/{_PREFIX}", tags=["Lightning"])
responses = {
423: {
"description": (
"LND only: Wallet is locked. Unlock via /lightning/unlock-wallet."
)
}
}
@router.post(
"/add-invoice",
name=f"{_PREFIX}.add-invoice",
summary="Addinvoice adds a new Invoice to the database.",
description="For additional information see [LND docs](https://api.lightning.community/#addinvoice)",
dependencies=[Depends(JWTBearer())],
response_model=Invoice,
responses=responses,
)
async def addinvoice(
value_msat: int = Query(description="The amount of msat of the invoice", ge=0),
memo: str = Query("", description="The memo of the invoice"),
expiry: int = Query(3600, description="Expiry time in seconds"),
is_keysend: bool = Query(
False, description="LND only: Whether this invoice is a keysend invoice."
),
):
try:
return await add_invoice(memo, value_msat, expiry, is_keysend)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.get(
"/get-balance",
name=f"{_PREFIX}.get-balance",
summary="Get the current on chain and channel balances of the lighting wallet.",
response_description=get_balance_response_desc,
dependencies=[Depends(JWTBearer())],
response_model=WalletBalance,
responses=responses,
)
async def getwalletbalance():
try:
return await get_wallet_balance()
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.get(
"/get-fee-revenue",
name=f"{_PREFIX}.get-fee-revenue",
summary="Returns the daily, weekly and monthly fee revenue earned.",
description=(
"Currently, year and total fees are always null. "
"Backends don't return these values by default. Implementation in BlitzAPI "
"remains a [to-do](https://github.com/fusion44/blitz_api/issues/64)."
),
dependencies=[Depends(JWTBearer())],
response_model=FeeRevenue,
responses=responses,
)
async def get_fee_revenue_path() -> FeeRevenue:
try:
return await get_fee_revenue()
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.get(
"/list-all-tx",
name=f"{_PREFIX}.list-all-tx",
summary="Lists all on-chain transactions, payments and invoices in the wallet",
description=(
"Returns a list with all on-chain transaction, payments and invoices "
"combined into one list. The index of each tx is only valid for each identical "
"set of parameters. "
),
dependencies=[Depends(JWTBearer())],
response_model=List[GenericTx],
responses=responses,
)
async def list_all_tx_path(
successful_only: bool = Query(
False,
description=(
"If set, only successful transaction will be returned in the response."
),
),
index_offset: int = Query(
0,
description=(
"The index of an transaction that will be used as either the "
"start or end of a query to determine which invoices should be returned in "
"the response."
),
),
max_tx: int = Query(
0,
description=(
"The max number of transaction to return in the response to "
"this query. Will return all transactions when set to 0 or null."
),
),
reversed: bool = Query(
False,
description=(
"If set, the transactions returned will result from seeking backwards "
"from the specified index offset. This can be used to paginate backwards."
),
),
):
try:
return await list_all_tx(successful_only, index_offset, max_tx, reversed)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.get(
"/list-invoices",
name=f"{_PREFIX}.list-invoices",
summary="Lists all invoices from the wallet. Modeled after LND implementation.",
response_model=List[Invoice],
response_description="A list of all invoices created.",
dependencies=[Depends(JWTBearer())],
responses=responses,
)
async def list_invoices_path(
pending_only: bool = Query(
False,
description=(
"If set, only invoices that are not settled and not canceled "
"will be returned in the response."
),
),
index_offset: int = Query(
0,
description=(
"The index of an invoice that will be used as either the start or end "
"of a query to determine which invoices should be returned in the response."
),
),
num_max_invoices: int = Query(
0,
description=(
"The max number of invoices to return in the response to this query. "
"This will return all invoices when set to 0 or null."
),
),
reversed: bool = Query(
False,
description=(
"If set, the invoices returned will result from seeking backwards "
"from the specified index offset. This can be used to paginate backwards."
),
),
):
try:
return await list_invoices(
pending_only,
index_offset,
num_max_invoices,
reversed,
)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.get(
"/list-onchain-tx",
name=f"{_PREFIX}.list-onchain-tx",
summary="Lists all onchain transactions from the wallet",
response_model=List[OnChainTransaction],
response_description="A list of all on-chain transactions made.",
dependencies=[Depends(JWTBearer())],
responses=responses,
)
async def list_on_chain_tx_path():
try:
return await list_on_chain_tx()
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.get(
"/list-payments",
name=f"{_PREFIX}.list-payments",
summary=(
"Returns a list of all outgoing payments. Modeled after LND implementation."
),
response_model=List[Payment],
response_description="A list of all payments made.",
dependencies=[Depends(JWTBearer())],
responses=responses,
)
async def list_payments_path(
include_incomplete: bool = Query(
True,
description=(
"If true, then return payments that have not yet fully completed. "
"This means that pending payments, as well as failed payments will show up "
"if this field is set to true. This flag doesn't change the meaning of the "
"indices, which are tied to individual payments."
),
),
index_offset: int = Query(
0,
description=(
"The index of a payment that will be used as either the start or "
"end of a query to determine which payments should be returned in the "
"response. The index_offset is exclusive. In the case of a zero "
"index_offset, the query will start with the oldest payment when "
"paginating forwards, or will end with the most recent payment when "
"paginating backwards."
),
),
max_payments: int = Query(
0,
description=(
"The maximal number of payments returned in the response to this query."
),
),
reversed: bool = Query(
False,
description=(
"If set, the payments returned will result from seeking backwards "
"from the specified index offset. This can be used to paginate backwards. "
"The order of the returned payments is always oldest first (ascending "
"index order)."
),
),
):
try:
return await list_payments(
include_incomplete, index_offset, max_payments, reversed
)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.post(
"/new-address",
name=f"{_PREFIX}.new-address",
summary="Generate a new on-chain address",
description=new_address_desc,
response_description="The newly generated wallet address",
dependencies=[Depends(JWTBearer())],
response_model=str,
responses=responses,
)
async def new_address_path(input: NewAddressInput):
try:
return await new_address(input)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.post(
"/send-coins",
name=f"{_PREFIX}.send-coins",
summary="Attempt to send on-chain funds.",
description=send_coins_desc,
response_description="Either an error or a SendCoinsResponse object on success",
dependencies=[Depends(JWTBearer())],
response_model=SendCoinsResponse,
responses={
412: {"description": "When not enough funds are available."},
423: responses[423],
},
)
async def send_coins_path(input: SendCoinsInput):
try:
return await send_coins(input=input)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.post(
"/open-channel",
name=f"{_PREFIX}.open-channel",
summary="open a new lightning channel",
description=open_channel_desc,
dependencies=[Depends(JWTBearer())],
response_model=str,
responses={
412: {"description": "When not enough funds are available."},
423: responses[423],
504: {"description": "When the peer is not reachable."},
},
)
async def open_channel_path(
local_funding_amount: int, node_URI: str, target_confs: int = 3
):
try:
return await channel_open(local_funding_amount, node_URI, target_confs)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
except ValueError as r:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=r.args[0])
@router.get(
"/list-channels",
name=f"{_PREFIX}.list-channels",
summary="Returns a list of open channels",
response_model=List[Channel],
response_description="A list of all open channels.",
dependencies=[Depends(JWTBearer())],
responses=responses,
)
async def list_channels_path():
try:
return await channel_list()
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
except ValueError as r:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=r.args[0])
@router.post(
"/close-channel",
name=f"{_PREFIX}.close-channel",
summary="close a channel",
description="For additional information see [LND docs](https://api.lightning.community/#closechannel)",
dependencies=[Depends(JWTBearer())],
response_model=str,
responses=responses,
)
async def close_channel_path(channel_id: str, force_close: bool):
try:
return await channel_close(channel_id, force_close)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
except ValueError as r:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=r.args[0])
@router.post(
"/send-payment",
name=f"{_PREFIX}.send-payment",
summary="Attempt to pay a payment request.",
description=send_payment_desc,
response_description="Either an error or a Payment object on success",
dependencies=[Depends(JWTBearer())],
response_model=Payment,
responses={
400: {
"description": (
"Possible error messages:"
"* invalid bech32 string"
"* amount must be specified when paying a zero amount invoice"
"* amount must not be specified when paying a non-zero amount invoice"
)
},
409: {
"description": (
"[LND only] When attempting to pay an already paid invoice. "
"CLN will return the payment object of the previously paid invoice. "
"Info: [GitHub](https://github.com/fusion44/blitz_api/issues/131)"
),
},
423: responses[423],
},
)
async def sendpayment(
pay_req: str,
timeout_seconds: int = 5,
fee_limit_msat: int = 8000,
amount_msat: Optional[int] = None,
):
try:
return await send_payment(pay_req, timeout_seconds, fee_limit_msat, amount_msat)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.get(
"/get-info",
name=f"{_PREFIX}.get-info",
summary="Request information about the currently running lightning node.",
response_description="Either an error or a LnInfo object on success",
dependencies=[Depends(JWTBearer())],
response_model=LnInfo,
responses=responses,
)
async def get_info():
try:
return await get_ln_info()
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.get(
"/decode-pay-req",
name=f"{_PREFIX}.decode-pay-req",
summary=(
"DecodePayReq takes an encoded payment request string and attempts to "
"decode it, returning a full description of the conditions encoded within the "
"payment request."
),
response_model=PaymentRequest,
response_description=(
"A fully decoded payment request or a HTTP status 400 if the "
"payment request cannot be decoded."
),
dependencies=[Depends(JWTBearer())],
responses=responses,
)
async def get_decode_pay_request(
pay_req: str = Query(..., description="The payment request string to be decoded"),
):
try:
return await decode_pay_request(pay_req)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])
@router.post(
"/unlock-wallet",
name=f"{_PREFIX}.unlock-wallet",
summary="Unlocks a locked wallet.",
response_model=bool,
response_description=unlock_wallet_desc,
dependencies=[Depends(JWTBearer())],
responses={
401: {
"description": (
"Either JWT token is not ok OR wallet password is wrong, "
"observe the detail message."
)
},
412: {"description": "Wallet already unlocked"},
},
)
async def unlock_wallet_path(input: UnlockWalletInput) -> bool:
try:
return await unlock_wallet(input.password)
except HTTPException:
raise
except NotImplementedError as r:
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=r.args[0])