mirror of
https://github.com/lnbits/lnbits.git
synced 2025-06-29 18:10:46 +02:00
fix: several more API calls restored
This commit is contained in:
@ -84,10 +84,10 @@ class Payment(BaseModel):
|
||||
bolt11: str
|
||||
preimage: str
|
||||
payment_hash: str
|
||||
extra: Dict
|
||||
extra: Optional[Dict] = {}
|
||||
wallet_id: str
|
||||
webhook: str
|
||||
webhook_status: int
|
||||
webhook: Optional[str]
|
||||
webhook_status: Optional[int]
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
|
@ -1,69 +1,59 @@
|
||||
from lnbits.helpers import url_for
|
||||
from fastapi.param_functions import Depends
|
||||
from lnbits.auth_bearer import AuthBearer
|
||||
from pydantic import BaseModel
|
||||
import trio
|
||||
import json
|
||||
import httpx
|
||||
import hashlib
|
||||
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
||||
|
||||
from fastapi import Query
|
||||
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
from binascii import unhexlify
|
||||
from typing import Dict, List, Optional, Union
|
||||
from http import HTTPStatus
|
||||
from typing import Dict, Optional, Union
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
import httpx
|
||||
import trio
|
||||
from fastapi import Query, security
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.param_functions import Depends
|
||||
from fastapi.params import Body
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits import bolt11, lnurl
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
|
||||
from lnbits.core.models import Wallet
|
||||
from lnbits.decorators import (WalletAdminKeyChecker, WalletInvoiceKeyChecker,
|
||||
WalletTypeInfo, get_key_type)
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
|
||||
|
||||
from .. import core_app, db
|
||||
from ..crud import get_payments, save_balance_check, update_wallet
|
||||
from ..services import (
|
||||
PaymentFailure,
|
||||
InvoiceFailure,
|
||||
create_invoice,
|
||||
pay_invoice,
|
||||
perform_lnurlauth,
|
||||
)
|
||||
from ..services import (InvoiceFailure, PaymentFailure, create_invoice,
|
||||
pay_invoice, perform_lnurlauth)
|
||||
from ..tasks import api_invoice_listeners
|
||||
|
||||
|
||||
@core_app.get(
|
||||
"/api/v1/wallet",
|
||||
# dependencies=[Depends(AuthBearer())]
|
||||
)
|
||||
# @api_check_wallet_key("invoice")
|
||||
async def api_wallet():
|
||||
@core_app.get("/api/v1/wallet")
|
||||
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
return (
|
||||
{"id": g().wallet.id, "name": g().wallet.name, "balance": g().wallet.balance_msat},
|
||||
{"id": wallet.wallet.id, "name": wallet.wallet.name, "balance": wallet.wallet.balance_msat},
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@core_app.put("/api/v1/wallet/{new_name}")
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_update_wallet(new_name: str):
|
||||
await update_wallet(g().wallet.id, new_name)
|
||||
async def api_update_wallet(new_name: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
await update_wallet(wallet.wallet.id, new_name)
|
||||
return (
|
||||
{
|
||||
"id": g().wallet.id,
|
||||
"name": g().wallet.name,
|
||||
"balance": g().wallet.balance_msat,
|
||||
"id": wallet.wallet.id,
|
||||
"name": wallet.wallet.name,
|
||||
"balance": wallet.wallet.balance_msat,
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/payments")
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_payments():
|
||||
return (
|
||||
await get_payments(wallet_id=g().wallet.id, pending=True, complete=True),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
|
||||
|
||||
|
||||
|
||||
class CreateInvoiceData(BaseModel):
|
||||
amount: int = Query(None, ge=1)
|
||||
@ -75,9 +65,7 @@ class CreateInvoiceData(BaseModel):
|
||||
extra: Optional[dict] = None
|
||||
webhook: Optional[str] = None
|
||||
|
||||
@api_check_wallet_key("invoice")
|
||||
# async def api_payments_create_invoice(amount: List[str] = Query([type: str = Query(None)])):
|
||||
async def api_payments_create_invoice(data: CreateInvoiceData):
|
||||
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
if "description_hash" in data:
|
||||
description_hash = unhexlify(data.description_hash)
|
||||
memo = ""
|
||||
@ -94,7 +82,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
|
||||
async with db.connect() as conn:
|
||||
try:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=g().wallet.id,
|
||||
wallet_id=wallet.id,
|
||||
amount=amount,
|
||||
memo=memo,
|
||||
description_hash=description_hash,
|
||||
@ -151,10 +139,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
|
||||
)
|
||||
|
||||
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_payments_pay_invoice(
|
||||
bolt11: str = Query(...), wallet: Optional[List[str]] = Query(None)
|
||||
):
|
||||
async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||
try:
|
||||
payment_hash = await pay_invoice(
|
||||
wallet_id=wallet.id,
|
||||
@ -179,11 +164,20 @@ async def api_payments_pay_invoice(
|
||||
)
|
||||
|
||||
|
||||
@core_app.post("/api/v1/payments")
|
||||
async def api_payments_create(out: bool = True):
|
||||
if out is True:
|
||||
return await api_payments_pay_invoice()
|
||||
return await api_payments_create_invoice()
|
||||
@core_app.post("/api/v1/payments", deprecated=True,
|
||||
description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead")
|
||||
async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True,
|
||||
invoiceData: Optional[CreateInvoiceData] = Body(None),
|
||||
bolt11: Optional[str] = Query(None)):
|
||||
|
||||
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
||||
|
||||
if out is True and wallet.wallet_type == 0:
|
||||
if not bolt11:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="BOLT11 string is invalid or not given")
|
||||
return await api_payments_pay_invoice(bolt11, wallet.wallet) # admin key
|
||||
return await api_payments_create_invoice(invoiceData, wallet.wallet) # invoice key
|
||||
|
||||
class CreateLNURLData(BaseModel):
|
||||
description_hash: str
|
||||
@ -192,8 +186,7 @@ class CreateLNURLData(BaseModel):
|
||||
comment: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
@core_app.post("/api/v1/payments/lnurl")
|
||||
@api_check_wallet_key("admin")
|
||||
@core_app.post("/api/v1/payments/lnurl", dependencies=[Depends(WalletAdminKeyChecker())])
|
||||
async def api_payments_pay_lnurl(data: CreateLNURLData):
|
||||
domain = urlparse(data.callback).netloc
|
||||
|
||||
@ -258,32 +251,9 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
|
||||
HTTPStatus.CREATED,
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/payments/{payment_hash}")
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_payment(payment_hash):
|
||||
payment = await g().wallet.get_payment(payment_hash)
|
||||
|
||||
if not payment:
|
||||
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
|
||||
elif not payment.pending:
|
||||
return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK
|
||||
|
||||
try:
|
||||
await payment.check_pending()
|
||||
except Exception:
|
||||
return {"paid": False}, HTTPStatus.OK
|
||||
|
||||
return (
|
||||
{"paid": not payment.pending, "preimage": payment.preimage},
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/payments/sse")
|
||||
@api_check_wallet_key("invoice", accept_querystring=True)
|
||||
async def api_payments_sse():
|
||||
this_wallet_id = g().wallet.id
|
||||
async def api_payments_sse(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
this_wallet_id = wallet.wallet.id
|
||||
|
||||
send_payment, receive_payment = trio.open_memory_channel(0)
|
||||
|
||||
@ -303,9 +273,10 @@ async def api_payments_sse():
|
||||
await send_event.send(("keepalive", ""))
|
||||
await trio.sleep(25)
|
||||
|
||||
current_app.nursery.start_soon(payment_received)
|
||||
current_app.nursery.start_soon(repeat_keepalive)
|
||||
|
||||
async with trio.open_nursery() as nursery:
|
||||
nursery.start_soon(payment_received)
|
||||
nursery.start_soon(repeat_keepalive)
|
||||
|
||||
async def send_events():
|
||||
try:
|
||||
async for typ, data in event_to_send:
|
||||
@ -332,9 +303,26 @@ async def api_payments_sse():
|
||||
response.timeout = None
|
||||
return response
|
||||
|
||||
@core_app.get("/api/v1/payments/{payment_hash}")
|
||||
async def api_payment(payment_hash, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
payment = await wallet.wallet.get_payment(payment_hash)
|
||||
|
||||
@core_app.get("/api/v1/lnurlscan/{code}")
|
||||
@api_check_wallet_key("invoice")
|
||||
if not payment:
|
||||
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
|
||||
elif not payment.pending:
|
||||
return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK
|
||||
|
||||
try:
|
||||
await payment.check_pending()
|
||||
except Exception:
|
||||
return {"paid": False}, HTTPStatus.OK
|
||||
|
||||
return (
|
||||
{"paid": not payment.pending, "preimage": payment.preimage},
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
@core_app.get("/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())])
|
||||
async def api_lnurlscan(code: str):
|
||||
try:
|
||||
url = lnurl.decode(code)
|
||||
@ -443,8 +431,7 @@ async def api_lnurlscan(code: str):
|
||||
return params
|
||||
|
||||
|
||||
@core_app.post("/api/v1/lnurlauth")
|
||||
@api_check_wallet_key("admin")
|
||||
@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())])
|
||||
async def api_perform_lnurlauth(callback: str):
|
||||
err = await perform_lnurlauth(callback)
|
||||
if err:
|
||||
@ -452,6 +439,6 @@ async def api_perform_lnurlauth(callback: str):
|
||||
return "", HTTPStatus.OK
|
||||
|
||||
|
||||
@core_app.route("/api/v1/currencies", methods=["GET"])
|
||||
@core_app.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available():
|
||||
return list(currencies.keys())
|
||||
|
@ -2,12 +2,14 @@ from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Request, status
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.param_functions import Body
|
||||
from fastapi.params import Depends, Query
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from pydantic.types import UUID4
|
||||
from starlette.responses import HTMLResponse
|
||||
import trio
|
||||
|
||||
from lnbits.core import db
|
||||
from lnbits.helpers import template_renderer, url_for
|
||||
@ -40,9 +42,7 @@ async def extensions(request: Request, enable: str, disable: str):
|
||||
extension_to_disable = disable
|
||||
|
||||
if extension_to_enable and extension_to_disable:
|
||||
abort(
|
||||
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
|
||||
)
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
|
||||
|
||||
if extension_to_enable:
|
||||
await update_user_extension(
|
||||
@ -142,7 +142,8 @@ async def lnurl_full_withdraw_callback(request: Request):
|
||||
except:
|
||||
pass
|
||||
|
||||
current_app.nursery.start_soon(pay)
|
||||
async with trio.open_nursery() as n:
|
||||
n.start_soon(pay)
|
||||
|
||||
balance_notify = request.args.get("balanceNotify")
|
||||
if balance_notify:
|
||||
@ -159,7 +160,7 @@ async def deletewallet(request: Request):
|
||||
user_wallet_ids = g().user.wallet_ids
|
||||
|
||||
if wallet_id not in user_wallet_ids:
|
||||
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||
else:
|
||||
await delete_wallet(user_id=g().user.id, wallet_id=wallet_id)
|
||||
user_wallet_ids.remove(wallet_id)
|
||||
@ -186,16 +187,17 @@ async def lnurlwallet(request: Request):
|
||||
user = await get_user(account.id, conn=conn)
|
||||
wallet = await create_wallet(user_id=user.id, conn=conn)
|
||||
|
||||
current_app.nursery.start_soon(
|
||||
redeem_lnurl_withdraw,
|
||||
wallet.id,
|
||||
request.args.get("lightning"),
|
||||
"LNbits initial funding: voucher redeem.",
|
||||
{"tag": "lnurlwallet"},
|
||||
5, # wait 5 seconds before sending the invoice to the service
|
||||
async with trio.open_nursery() as n:
|
||||
n.start_soon(
|
||||
redeem_lnurl_withdraw,
|
||||
wallet.id,
|
||||
request.args.get("lightning"),
|
||||
"LNbits initial funding: voucher redeem.",
|
||||
{"tag": "lnurlwallet"},
|
||||
5, # wait 5 seconds before sending the invoice to the service
|
||||
)
|
||||
|
||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
||||
return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
|
||||
|
||||
|
||||
@core_html_routes.get("/manifest/{usr}.webmanifest")
|
||||
|
@ -1,35 +1,114 @@
|
||||
from cerberus import Validator # type: ignore
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi.security import api_key
|
||||
from lnbits.core.models import Wallet
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from cerberus import Validator # type: ignore
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.params import Security
|
||||
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
from lnbits.settings import LNBITS_ALLOWED_USERS
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import LNBITS_ALLOWED_USERS
|
||||
|
||||
def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
|
||||
def wrap(view):
|
||||
@wraps(view)
|
||||
async def wrapped_view(**kwargs):
|
||||
try:
|
||||
key_value = request.headers.get("X-Api-Key") or request.args["api-key"]
|
||||
g().wallet = await get_wallet_for_key(key_value, key_type)
|
||||
except KeyError:
|
||||
return (
|
||||
jsonify({"message": "`X-Api-Key` header missing."}),
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not g().wallet:
|
||||
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
|
||||
class KeyChecker(SecurityBase):
|
||||
def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
self._key_type = "invoice"
|
||||
self._api_key = api_key
|
||||
if api_key:
|
||||
self.model: APIKey= APIKey(
|
||||
**{"in": APIKeyIn.query}, name="X-API-KEY", description="Wallet API Key - QUERY"
|
||||
)
|
||||
else:
|
||||
self.model: APIKey= APIKey(
|
||||
**{"in": APIKeyIn.header}, name="X-API-KEY", description="Wallet API Key - HEADER"
|
||||
)
|
||||
self.wallet = None
|
||||
|
||||
return await view(**kwargs)
|
||||
async def __call__(self, request: Request) -> Wallet:
|
||||
try:
|
||||
key_value = self._api_key if self._api_key else request.headers.get("X-API-KEY") or request.query_params["api-key"]
|
||||
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
|
||||
# Also, we should not return the wallet here - thats silly.
|
||||
# Possibly store it in a Redis DB
|
||||
self.wallet = await get_wallet_for_key(key_value, self._key_type)
|
||||
if not self.wallet:
|
||||
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid key or expired key.")
|
||||
|
||||
return wrapped_view
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="`X-API-KEY` header missing.")
|
||||
|
||||
return wrap
|
||||
class WalletInvoiceKeyChecker(KeyChecker):
|
||||
"""
|
||||
WalletInvoiceKeyChecker will ensure that the provided invoice
|
||||
wallet key is correct and populate g().wallet with the wallet
|
||||
for the key in `X-API-key`.
|
||||
|
||||
The checker will raise an HTTPException when the key is wrong in some ways.
|
||||
"""
|
||||
def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
|
||||
super().__init__(scheme_name, auto_error, api_key)
|
||||
self._key_type = "invoice"
|
||||
|
||||
class WalletAdminKeyChecker(KeyChecker):
|
||||
"""
|
||||
WalletAdminKeyChecker will ensure that the provided admin
|
||||
wallet key is correct and populate g().wallet with the wallet
|
||||
for the key in `X-API-key`.
|
||||
|
||||
The checker will raise an HTTPException when the key is wrong in some ways.
|
||||
"""
|
||||
def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
|
||||
super().__init__(scheme_name, auto_error, api_key)
|
||||
self._key_type = "admin"
|
||||
|
||||
class WalletTypeInfo():
|
||||
wallet_type: int
|
||||
wallet: Wallet
|
||||
|
||||
def __init__(self, wallet_type: int, wallet: Wallet) -> None:
|
||||
self.wallet_type = wallet_type
|
||||
self.wallet = wallet
|
||||
|
||||
|
||||
api_key_header = APIKeyHeader(name="X-API-KEY", auto_error=False, description="Admin or Invoice key for wallet API's")
|
||||
api_key_query = APIKeyQuery(name="api-key", auto_error=False, description="Admin or Invoice key for wallet API's")
|
||||
async def get_key_type(r: Request,
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_query: str = Security(api_key_query)) -> WalletTypeInfo:
|
||||
# 0: admin
|
||||
# 1: invoice
|
||||
# 2: invalid
|
||||
try:
|
||||
checker = WalletAdminKeyChecker(api_key=api_key_query)
|
||||
await checker.__call__(r)
|
||||
return WalletTypeInfo(0, checker.wallet)
|
||||
except HTTPException as e:
|
||||
if e.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
pass
|
||||
except:
|
||||
raise
|
||||
|
||||
try:
|
||||
checker = WalletInvoiceKeyChecker()
|
||||
await checker.__call__(r)
|
||||
return WalletTypeInfo(1, checker.wallet)
|
||||
except HTTPException as e:
|
||||
if e.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
return WalletTypeInfo(2, None)
|
||||
except:
|
||||
raise
|
||||
|
||||
def api_validate_post_request(*, schema: dict):
|
||||
def wrap(view):
|
||||
|
@ -161,24 +161,22 @@ window.LNbits = {
|
||||
return newWallet
|
||||
},
|
||||
payment: function (data) {
|
||||
var obj = _.object(
|
||||
[
|
||||
'checking_id',
|
||||
'pending',
|
||||
'amount',
|
||||
'fee',
|
||||
'memo',
|
||||
'time',
|
||||
'bolt11',
|
||||
'preimage',
|
||||
'payment_hash',
|
||||
'extra',
|
||||
'wallet_id',
|
||||
'webhook',
|
||||
'webhook_status'
|
||||
],
|
||||
data
|
||||
)
|
||||
obj = {
|
||||
checking_id:data.id,
|
||||
pending: data.pending,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
memo: data.memo,
|
||||
time: data.time,
|
||||
bolt11: data.bolt11,
|
||||
preimage: data.preimage,
|
||||
payment_hash: data.payment_hash,
|
||||
extra: data.extra,
|
||||
wallet_id: data.wallet_id,
|
||||
webhook: data.webhook,
|
||||
webhook_status: data.webhook_status,
|
||||
}
|
||||
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
|
Reference in New Issue
Block a user