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 bolt11: str
preimage: str preimage: str
payment_hash: str payment_hash: str
extra: Dict extra: Optional[Dict] = {}
wallet_id: str wallet_id: str
webhook: str webhook: Optional[str]
webhook_status: int webhook_status: Optional[int]
@classmethod @classmethod
def from_row(cls, row: Row): 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 import hashlib
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult import json
from fastapi import Query
from http import HTTPStatus
from binascii import unhexlify 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 import bolt11, lnurl
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.core.models import Wallet
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis from lnbits.decorators import (WalletAdminKeyChecker, WalletInvoiceKeyChecker,
WalletTypeInfo, get_key_type)
from lnbits.helpers import url_for
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
from .. import core_app, db from .. import core_app, db
from ..crud import get_payments, save_balance_check, update_wallet from ..crud import get_payments, save_balance_check, update_wallet
from ..services import ( from ..services import (InvoiceFailure, PaymentFailure, create_invoice,
PaymentFailure, pay_invoice, perform_lnurlauth)
InvoiceFailure,
create_invoice,
pay_invoice,
perform_lnurlauth,
)
from ..tasks import api_invoice_listeners from ..tasks import api_invoice_listeners
@core_app.get( @core_app.get("/api/v1/wallet")
"/api/v1/wallet", async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
# dependencies=[Depends(AuthBearer())]
)
# @api_check_wallet_key("invoice")
async def api_wallet():
return ( 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, HTTPStatus.OK,
) )
@core_app.put("/api/v1/wallet/{new_name}") @core_app.put("/api/v1/wallet/{new_name}")
@api_check_wallet_key("invoice") async def api_update_wallet(new_name: str, wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_update_wallet(new_name: str): await update_wallet(wallet.wallet.id, new_name)
await update_wallet(g().wallet.id, new_name)
return ( return (
{ {
"id": g().wallet.id, "id": wallet.wallet.id,
"name": g().wallet.name, "name": wallet.wallet.name,
"balance": g().wallet.balance_msat, "balance": wallet.wallet.balance_msat,
}, },
HTTPStatus.OK, HTTPStatus.OK,
) )
@core_app.get("/api/v1/payments") @core_app.get("/api/v1/payments")
@api_check_wallet_key("invoice") async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_payments(): return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
return (
await get_payments(wallet_id=g().wallet.id, pending=True, complete=True),
HTTPStatus.OK,
)
class CreateInvoiceData(BaseModel): class CreateInvoiceData(BaseModel):
amount: int = Query(None, ge=1) amount: int = Query(None, ge=1)
@ -75,9 +65,7 @@ class CreateInvoiceData(BaseModel):
extra: Optional[dict] = None extra: Optional[dict] = None
webhook: Optional[str] = None webhook: Optional[str] = None
@api_check_wallet_key("invoice") async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
# async def api_payments_create_invoice(amount: List[str] = Query([type: str = Query(None)])):
async def api_payments_create_invoice(data: CreateInvoiceData):
if "description_hash" in data: if "description_hash" in data:
description_hash = unhexlify(data.description_hash) description_hash = unhexlify(data.description_hash)
memo = "" memo = ""
@ -94,7 +82,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
async with db.connect() as conn: async with db.connect() as conn:
try: try:
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=g().wallet.id, wallet_id=wallet.id,
amount=amount, amount=amount,
memo=memo, memo=memo,
description_hash=description_hash, 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, wallet: Wallet):
async def api_payments_pay_invoice(
bolt11: str = Query(...), wallet: Optional[List[str]] = Query(None)
):
try: try:
payment_hash = await pay_invoice( payment_hash = await pay_invoice(
wallet_id=wallet.id, wallet_id=wallet.id,
@ -179,11 +164,20 @@ async def api_payments_pay_invoice(
) )
@core_app.post("/api/v1/payments") @core_app.post("/api/v1/payments", deprecated=True,
async def api_payments_create(out: bool = True): description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead")
if out is True: async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True,
return await api_payments_pay_invoice() invoiceData: Optional[CreateInvoiceData] = Body(None),
return await api_payments_create_invoice() 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): class CreateLNURLData(BaseModel):
description_hash: str description_hash: str
@ -192,8 +186,7 @@ class CreateLNURLData(BaseModel):
comment: Optional[str] = None comment: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
@core_app.post("/api/v1/payments/lnurl") @core_app.post("/api/v1/payments/lnurl", dependencies=[Depends(WalletAdminKeyChecker())])
@api_check_wallet_key("admin")
async def api_payments_pay_lnurl(data: CreateLNURLData): async def api_payments_pay_lnurl(data: CreateLNURLData):
domain = urlparse(data.callback).netloc domain = urlparse(data.callback).netloc
@ -258,32 +251,9 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
HTTPStatus.CREATED, 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") @core_app.get("/api/v1/payments/sse")
@api_check_wallet_key("invoice", accept_querystring=True) async def api_payments_sse(wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_payments_sse(): this_wallet_id = wallet.wallet.id
this_wallet_id = g().wallet.id
send_payment, receive_payment = trio.open_memory_channel(0) send_payment, receive_payment = trio.open_memory_channel(0)
@ -303,8 +273,9 @@ async def api_payments_sse():
await send_event.send(("keepalive", "")) await send_event.send(("keepalive", ""))
await trio.sleep(25) await trio.sleep(25)
current_app.nursery.start_soon(payment_received) async with trio.open_nursery() as nursery:
current_app.nursery.start_soon(repeat_keepalive) nursery.start_soon(payment_received)
nursery.start_soon(repeat_keepalive)
async def send_events(): async def send_events():
try: try:
@ -332,9 +303,26 @@ async def api_payments_sse():
response.timeout = None response.timeout = None
return response 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}") if not payment:
@api_check_wallet_key("invoice") 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): async def api_lnurlscan(code: str):
try: try:
url = lnurl.decode(code) url = lnurl.decode(code)
@ -443,8 +431,7 @@ async def api_lnurlscan(code: str):
return params return params
@core_app.post("/api/v1/lnurlauth") @core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())])
@api_check_wallet_key("admin")
async def api_perform_lnurlauth(callback: str): async def api_perform_lnurlauth(callback: str):
err = await perform_lnurlauth(callback) err = await perform_lnurlauth(callback)
if err: if err:
@ -452,6 +439,6 @@ async def api_perform_lnurlauth(callback: str):
return "", HTTPStatus.OK return "", HTTPStatus.OK
@core_app.route("/api/v1/currencies", methods=["GET"]) @core_app.get("/api/v1/currencies")
async def api_list_currencies_available(): async def api_list_currencies_available():
return list(currencies.keys()) return list(currencies.keys())

View File

@ -2,12 +2,14 @@ from http import HTTPStatus
from typing import Optional from typing import Optional
from fastapi import Request, status from fastapi import Request, status
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Body from fastapi.param_functions import Body
from fastapi.params import Depends, Query from fastapi.params import Depends, Query
from fastapi.responses import FileResponse, RedirectResponse from fastapi.responses import FileResponse, RedirectResponse
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from pydantic.types import UUID4 from pydantic.types import UUID4
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
import trio
from lnbits.core import db from lnbits.core import db
from lnbits.helpers import template_renderer, url_for 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 extension_to_disable = disable
if extension_to_enable and extension_to_disable: if extension_to_enable and extension_to_disable:
abort( raise HTTPException(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
)
if extension_to_enable: if extension_to_enable:
await update_user_extension( await update_user_extension(
@ -142,7 +142,8 @@ async def lnurl_full_withdraw_callback(request: Request):
except: except:
pass pass
current_app.nursery.start_soon(pay) async with trio.open_nursery() as n:
n.start_soon(pay)
balance_notify = request.args.get("balanceNotify") balance_notify = request.args.get("balanceNotify")
if balance_notify: if balance_notify:
@ -159,7 +160,7 @@ async def deletewallet(request: Request):
user_wallet_ids = g().user.wallet_ids user_wallet_ids = g().user.wallet_ids
if wallet_id not in 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: else:
await delete_wallet(user_id=g().user.id, wallet_id=wallet_id) await delete_wallet(user_id=g().user.id, wallet_id=wallet_id)
user_wallet_ids.remove(wallet_id) user_wallet_ids.remove(wallet_id)
@ -186,7 +187,8 @@ async def lnurlwallet(request: Request):
user = await get_user(account.id, conn=conn) user = await get_user(account.id, conn=conn)
wallet = await create_wallet(user_id=user.id, conn=conn) wallet = await create_wallet(user_id=user.id, conn=conn)
current_app.nursery.start_soon( async with trio.open_nursery() as n:
n.start_soon(
redeem_lnurl_withdraw, redeem_lnurl_withdraw,
wallet.id, wallet.id,
request.args.get("lightning"), request.args.get("lightning"),
@ -195,7 +197,7 @@ async def lnurlwallet(request: Request):
5, # wait 5 seconds before sending the invoice to the service 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") @core_html_routes.get("/manifest/{usr}.webmanifest")

View File

@ -1,35 +1,114 @@
from cerberus import Validator # type: ignore
from functools import wraps from functools import wraps
from http import HTTPStatus from http import HTTPStatus
from fastapi.security import api_key
from lnbits.core.models import Wallet
from typing import List, Union from typing import List, Union
from uuid import UUID 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.core.crud import get_user, get_wallet_for_key
from lnbits.settings import LNBITS_ALLOWED_USERS
from lnbits.requestvars import g 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): class KeyChecker(SecurityBase):
@wraps(view) def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
async def wrapped_view(**kwargs): self.scheme_name = scheme_name or self.__class__.__name__
try: self.auto_error = auto_error
key_value = request.headers.get("X-Api-Key") or request.args["api-key"] self._key_type = "invoice"
g().wallet = await get_wallet_for_key(key_value, key_type) self._api_key = api_key
except KeyError: if api_key:
return ( self.model: APIKey= APIKey(
jsonify({"message": "`X-Api-Key` header missing."}), **{"in": APIKeyIn.query}, name="X-API-KEY", description="Wallet API Key - QUERY"
HTTPStatus.BAD_REQUEST,
) )
else:
self.model: APIKey= APIKey(
**{"in": APIKeyIn.header}, name="X-API-KEY", description="Wallet API Key - HEADER"
)
self.wallet = None
if not g().wallet: async def __call__(self, request: Request) -> Wallet:
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED 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 await view(**kwargs) except KeyError:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST,
detail="`X-API-KEY` header missing.")
return wrapped_view 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`.
return wrap 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 api_validate_post_request(*, schema: dict):
def wrap(view): def wrap(view):

View File

@ -161,24 +161,22 @@ window.LNbits = {
return newWallet return newWallet
}, },
payment: function (data) { payment: function (data) {
var obj = _.object( obj = {
[ checking_id:data.id,
'checking_id', pending: data.pending,
'pending', amount: data.amount,
'amount', fee: data.fee,
'fee', memo: data.memo,
'memo', time: data.time,
'time', bolt11: data.bolt11,
'bolt11', preimage: data.preimage,
'preimage', payment_hash: data.payment_hash,
'payment_hash', extra: data.extra,
'extra', wallet_id: data.wallet_id,
'wallet_id', webhook: data.webhook,
'webhook', webhook_status: data.webhook_status,
'webhook_status' }
],
data
)
obj.date = Quasar.utils.date.formatDate( obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000), new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm' 'YYYY-MM-DD HH:mm'