fix: several more API calls restored

This commit is contained in:
Stefan Stammberger
2021-08-29 19:38:42 +02:00
parent 5ae124408e
commit fe79709698
5 changed files with 208 additions and 142 deletions

View File

@ -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):

View File

@ -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())

View File

@ -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")

View File

@ -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):

View File

@ -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'