diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index 182df743a..0ab520da7 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -7,8 +7,6 @@ from lnbits.helpers import urlsafe_short_hash from . import db from .models import createLnurldevice, lnurldevicepayment, lnurldevices -###############lnurldeviceS########################## - async def create_lnurldevice( data: createLnurldevice, @@ -69,10 +67,12 @@ async def create_lnurldevice( data.pin4, ), ) - return await get_lnurldevice(lnurldevice_id) + device = await get_lnurldevice(lnurldevice_id) + assert device + return device -async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldevices]: +async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> lnurldevices: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( f"UPDATE lnurldevice.lnurldevices SET {q} WHERE id = ?", @@ -81,19 +81,18 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev row = await db.fetchone( "SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,) ) - return lnurldevices(**row) if row else None + return lnurldevices(**row) -async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices: +async def get_lnurldevice(lnurldevice_id: str) -> Optional[lnurldevices]: row = await db.fetchone( "SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,) ) return lnurldevices(**row) if row else None -async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]: - wallet_ids = [wallet_ids] - q = ",".join(["?"] * len(wallet_ids[0])) +async def get_lnurldevices(wallet_ids: List[str]) -> List[lnurldevices]: + q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall( f""" SELECT * FROM lnurldevice.lnurldevices WHERE wallet IN ({q}) @@ -102,7 +101,7 @@ async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevic (*wallet_ids,), ) - return [lnurldevices(**row) if row else None for row in rows] + return [lnurldevices(**row) for row in rows] async def delete_lnurldevice(lnurldevice_id: str) -> None: @@ -110,8 +109,6 @@ async def delete_lnurldevice(lnurldevice_id: str) -> None: "DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,) ) - ########################lnuldevice payments########################### - async def create_lnurldevicepayment( deviceid: str, @@ -121,6 +118,7 @@ async def create_lnurldevicepayment( sats: Optional[int] = 0, ) -> lnurldevicepayment: device = await get_lnurldevice(deviceid) + assert device if device.device == "atm": lnurldevicepayment_id = shortuuid.uuid(name=payload) else: @@ -139,7 +137,9 @@ async def create_lnurldevicepayment( """, (lnurldevicepayment_id, deviceid, payload, pin, payhash, sats), ) - return await get_lnurldevicepayment(lnurldevicepayment_id) + dpayment = await get_lnurldevicepayment(lnurldevicepayment_id) + assert dpayment + return dpayment async def update_lnurldevicepayment( @@ -157,7 +157,9 @@ async def update_lnurldevicepayment( return lnurldevicepayment(**row) if row else None -async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment: +async def get_lnurldevicepayment( + lnurldevicepayment_id: str, +) -> Optional[lnurldevicepayment]: row = await db.fetchone( "SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", (lnurldevicepayment_id,), @@ -165,7 +167,9 @@ async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayme return lnurldevicepayment(**row) if row else None -async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment: +async def get_lnurlpayload( + lnurldevicepayment_payload: str, +) -> Optional[lnurldevicepayment]: row = await db.fetchone( "SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?", (lnurldevicepayment_payload,), diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index 34de20fae..eba2a6930 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -1,16 +1,11 @@ import base64 -import hashlib import hmac from http import HTTPStatus from io import BytesIO -from typing import Optional import shortuuid from embit import bech32, compact -from fastapi import Request -from fastapi.param_functions import Query -from loguru import logger -from starlette.exceptions import HTTPException +from fastapi import HTTPException, Query, Request from lnbits import bolt11 from lnbits.core.services import create_invoice @@ -44,7 +39,9 @@ def bech32_decode(bech): encoding = bech32.bech32_verify_checksum(hrp, data) if encoding is None: return - return bytes(bech32.convertbits(data[:-6], 5, 8, False)) + bits = bech32.convertbits(data[:-6], 5, 8, False) + assert bits + return bytes(bits) def xor_decrypt(key, blob): @@ -105,6 +102,8 @@ async def lnurl_v1_params( "reason": f"lnurldevice {device_id} not found on this server", } if device.device == "switch": + # TODO: AMOUNT IN CENT was never reference here + amount_in_cent = 0 price_msat = ( await fiat_amount_as_satoshis(float(profit), device.currency) if device.currency != "sat" @@ -160,23 +159,18 @@ async def lnurl_v1_params( if device.device != "atm": return {"status": "ERROR", "reason": "Not ATM device."} price_msat = int(price_msat * (1 - (device.profit / 100)) / 1000) - lnurldevicepayment = await get_lnurldevicepayment(shortuuid.uuid(name=p)) - if lnurldevicepayment: - logger.debug("lnurldevicepayment") - logger.debug(lnurldevicepayment) - logger.debug("lnurldevicepayment") - if lnurldevicepayment.payload == lnurldevicepayment.payhash: - return {"status": "ERROR", "reason": f"Payment already claimed"} - else: + try: lnurldevicepayment = await create_lnurldevicepayment( deviceid=device.id, payload=p, sats=price_msat * 1000, - pin=pin, + pin=str(pin), payhash="payment_hash", ) + except: + return {"status": "ERROR", "reason": "Could not create ATM payment."} if not lnurldevicepayment: - return {"status": "ERROR", "reason": "Could not create payment."} + return {"status": "ERROR", "reason": "Could not create ATM payment."} return { "tag": "withdrawRequest", "callback": request.url_for( @@ -193,7 +187,7 @@ async def lnurl_v1_params( deviceid=device.id, payload=p, sats=price_msat * 1000, - pin=pin, + pin=str(pin), payhash="payment_hash", ) if not lnurldevicepayment: @@ -221,6 +215,10 @@ async def lnurl_callback( k1: str = Query(None), ): lnurldevicepayment = await get_lnurldevicepayment(paymentid) + if not lnurldevicepayment: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="lnurldevicepayment not found." + ) device = await get_lnurldevice(lnurldevicepayment.deviceid) if not device: raise HTTPException( @@ -241,13 +239,17 @@ async def lnurl_callback( else: if lnurldevicepayment.payload != k1: return {"status": "ERROR", "reason": "Bad K1"} - lnurldevicepayment = await update_lnurldevicepayment( + if lnurldevicepayment.payhash != "payment_hash": + return {"status": "ERROR", "reason": f"Payment already claimed"} + + lnurldevicepayment_updated = await update_lnurldevicepayment( lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload ) + assert lnurldevicepayment_updated await pay_invoice( wallet_id=device.wallet, payment_request=pr, - max_sat=lnurldevicepayment.sats / 1000, + max_sat=int(lnurldevicepayment_updated.sats / 1000), extra={"tag": "withdraw"}, ) return {"status": "OK"} diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index 66b215f27..f9640de1c 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -3,13 +3,9 @@ from sqlite3 import Row from typing import List, Optional from fastapi import Request -from lnurl import Lnurl -from lnurl import encode as lnurl_encode # type: ignore -from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore -from lnurl.types import LnurlPayMetadata # type: ignore -from loguru import logger +from lnurl import encode as lnurl_encode +from lnurl.types import LnurlPayMetadata from pydantic import BaseModel -from pydantic.main import BaseModel class createLnurldevice(BaseModel): @@ -58,6 +54,7 @@ class lnurldevices(BaseModel): pin4: int timestamp: str + @classmethod def from_row(cls, row: Row) -> "lnurldevices": return cls(**dict(row)) diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index 8ad9772ca..9aec173ec 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -1,18 +1,11 @@ import asyncio -import json -from http import HTTPStatus -from urllib.parse import urlparse -import httpx -from fastapi import HTTPException - -from lnbits import bolt11 from lnbits.core.models import Payment -from lnbits.core.services import pay_invoice, websocketUpdater +from lnbits.core.services import websocketUpdater from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment +from .crud import get_lnurldevicepayment, update_lnurldevicepayment async def wait_for_paid_invoices(): @@ -27,14 +20,15 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: # (avoid loops) if "Switch" == payment.extra.get("tag"): - lnurldevicepayment = await get_lnurldevicepayment(payment.extra.get("id")) + lnurldevicepayment = await get_lnurldevicepayment(payment.extra["id"]) if not lnurldevicepayment: return if lnurldevicepayment.payhash == "used": return lnurldevicepayment = await update_lnurldevicepayment( - lnurldevicepayment_id=payment.extra.get("id"), payhash="used" + lnurldevicepayment_id=payment.extra["id"], payhash="used" ) + assert lnurldevicepayment return await websocketUpdater( lnurldevicepayment.deviceid, str(lnurldevicepayment.pin) + "-" + str(lnurldevicepayment.payload), diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index f1be4f0da..a6256a415 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -1,12 +1,7 @@ from http import HTTPStatus -from io import BytesIO -import pyqrcode -from fastapi import Request -from fastapi.param_functions import Query -from fastapi.params import Depends +from fastapi import Depends, HTTPException, Query, Request from fastapi.templating import Jinja2Templates -from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse, StreamingResponse from lnbits.core.crud import update_payment_status @@ -62,4 +57,6 @@ async def img(request: Request, lnurldevice_id): raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist." ) - return lnurldevice.lnurl(request) + # error: "lnurldevices" has no attribute "lnurl" + # return lnurldevice.lnurl(request) + return None diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py index c6766423c..d657c879b 100644 --- a/lnbits/extensions/lnurldevice/views_api.py +++ b/lnbits/extensions/lnurldevice/views_api.py @@ -1,9 +1,6 @@ from http import HTTPStatus -from fastapi import Request -from fastapi.param_functions import Query -from fastapi.params import Depends -from starlette.exceptions import HTTPException +from fastapi import Depends, HTTPException, Query, Request from lnbits.core.crud import get_user from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key @@ -26,9 +23,6 @@ async def api_list_currencies_available(): return list(currencies.keys()) -#######################lnurldevice########################## - - @lnurldevice_ext.post("/api/v1/lnurlpos") @lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}") async def api_lnurldevice_create_or_update( @@ -41,7 +35,7 @@ async def api_lnurldevice_create_or_update( lnurldevice = await create_lnurldevice(data) return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} else: - lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id) + lnurldevice = await update_lnurldevice(lnurldevice_id, **data.dict()) return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} @@ -49,7 +43,8 @@ async def api_lnurldevice_create_or_update( async def api_lnurldevices_retrieve( req: Request, wallet: WalletTypeInfo = Depends(get_key_type) ): - wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + user = await get_user(wallet.wallet.user) + wallet_ids = user.wallet_ids if user else [] try: return [ {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} @@ -65,10 +60,11 @@ async def api_lnurldevices_retrieve( return "" -@lnurldevice_ext.get("/api/v1/lnurlpos/{lnurldevice_id}") +@lnurldevice_ext.get( + "/api/v1/lnurlpos/{lnurldevice_id}", dependencies=[Depends(get_key_type)] +) async def api_lnurldevice_retrieve( req: Request, - wallet: WalletTypeInfo = Depends(get_key_type), lnurldevice_id: str = Query(None), ): lnurldevice = await get_lnurldevice(lnurldevice_id) @@ -76,23 +72,18 @@ async def api_lnurldevice_retrieve( raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist" ) - if not lnurldevice.lnurl_toggle: - return {**lnurldevice.dict()} return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} -@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}") -async def api_lnurldevice_delete( - wallet: WalletTypeInfo = Depends(require_admin_key), - lnurldevice_id: str = Query(None), -): +@lnurldevice_ext.delete( + "/api/v1/lnurlpos/{lnurldevice_id}", dependencies=[Depends(require_admin_key)] +) +async def api_lnurldevice_delete(lnurldevice_id: str = Query(None)): lnurldevice = await get_lnurldevice(lnurldevice_id) - if not lnurldevice: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist." ) await delete_lnurldevice(lnurldevice_id) - return "", HTTPStatus.NO_CONTENT diff --git a/pyproject.toml b/pyproject.toml index b41cff10b..c3026c6f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,6 @@ exclude = """(?x)( ^lnbits/extensions/bleskomat. | ^lnbits/extensions/boltz. | ^lnbits/extensions/livestream. - | ^lnbits/extensions/lnurldevice. | ^lnbits/wallets/lnd_grpc_files. )"""