diff --git a/lnbits/extensions/gerty/__init__.py b/lnbits/extensions/gerty/__init__.py index bd353c78..5b24718a 100644 --- a/lnbits/extensions/gerty/__init__.py +++ b/lnbits/extensions/gerty/__init__.py @@ -5,11 +5,9 @@ from fastapi.staticfiles import StaticFiles from lnbits.db import Database from lnbits.helpers import template_renderer -from lnbits.tasks import catch_everything_and_restart db = Database("ext_gerty") - gerty_static_files = [ { "path": "/gerty/static", diff --git a/lnbits/extensions/gerty/crud.py b/lnbits/extensions/gerty/crud.py index 1164b6ee..5475139c 100644 --- a/lnbits/extensions/gerty/crud.py +++ b/lnbits/extensions/gerty/crud.py @@ -50,11 +50,12 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty: return gerty -async def update_gerty(gerty_id: str, **kwargs) -> Gerty: +async def update_gerty(gerty_id: str, **kwargs) -> Optional[Gerty]: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( f"UPDATE gerty.gertys SET {q} WHERE id = ?", (*kwargs.values(), gerty_id) ) + return await get_gerty(gerty_id) @@ -82,7 +83,7 @@ async def delete_gerty(gerty_id: str) -> None: #############MEMPOOL########### -async def get_mempool_info(endPoint: str, gerty) -> Optional[Mempool]: +async def get_mempool_info(endPoint: str, gerty) -> dict: logger.debug(endPoint) endpoints = MempoolEndpoint() url = "" @@ -116,7 +117,7 @@ async def get_mempool_info(endPoint: str, gerty) -> Optional[Mempool]: mempool_id, json.dumps(response.json()), endPoint, - int(time.time()), + time.time(), gerty.mempool_endpoint, ), ) @@ -128,7 +129,7 @@ async def get_mempool_info(endPoint: str, gerty) -> Optional[Mempool]: "UPDATE gerty.mempool SET data = ?, time = ? WHERE endpoint = ? AND mempool_endpoint = ?", ( json.dumps(response.json()), - int(time.time()), + time.time(), endPoint, gerty.mempool_endpoint, ), diff --git a/lnbits/extensions/gerty/helpers.py b/lnbits/extensions/gerty/helpers.py index 65c69073..3e48c576 100644 --- a/lnbits/extensions/gerty/helpers.py +++ b/lnbits/extensions/gerty/helpers.py @@ -3,15 +3,16 @@ import os import random import textwrap from datetime import datetime, timedelta +from typing import List import httpx from loguru import logger -from lnbits.core.crud import get_user, get_wallet_for_key +from lnbits.core.crud import get_wallet_for_key from lnbits.settings import settings from lnbits.utils.exchange_rates import satoshis_amount_as_fiat -from .crud import get_gerty, get_mempool_info +from .crud import get_mempool_info from .number_prefixer import * @@ -24,8 +25,8 @@ def get_percent_difference(current, previous, precision=3): def get_text_item_dict( text: str, font_size: int, - x_pos: int = None, - y_pos: int = None, + x_pos: int = -1, + y_pos: int = -1, gerty_type: str = "Gerty", ): # Get line size by font size @@ -63,13 +64,41 @@ def get_text_item_dict( # logger.debug('multilineText') # logger.debug(multilineText) - text = {"value": multilineText, "size": font_size} - if x_pos is None and y_pos is None: - text["position"] = "center" + data_text = {"value": multilineText, "size": font_size} + if x_pos == -1 and y_pos == -1: + data_text["position"] = "center" else: - text["x"] = x_pos - text["y"] = y_pos - return text + data_text["x"] = x_pos if x_pos > 0 else 0 + data_text["y"] = y_pos if x_pos > 0 else 0 + return data_text + + +def get_date_suffix(dayNumber): + if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30: + return "th" + else: + return ["st", "nd", "rd"][dayNumber % 10 - 1] + + +def get_time_remaining(seconds, granularity=2): + intervals = ( + # ('weeks', 604800), # 60 * 60 * 24 * 7 + ("days", 86400), # 60 * 60 * 24 + ("hours", 3600), # 60 * 60 + ("minutes", 60), + ("seconds", 1), + ) + + result = [] + + for name, count in intervals: + value = seconds // count + if value: + seconds -= value * count + if value == 1: + name = name.rstrip("s") + result.append("{} {}".format(round(value), name)) + return ", ".join(result[:granularity]) # format a number for nice display output @@ -293,8 +322,7 @@ def get_next_update_time(sleep_time_seconds: int = 0, utc_offset: int = 0): def gerty_should_sleep(utc_offset: int = 0): utc_now = datetime.utcnow() local_time = utc_now + timedelta(hours=utc_offset) - hours = local_time.strftime("%H") - hours = int(hours) + hours = int(local_time.strftime("%H")) if hours >= 22 and hours <= 23: return True else: @@ -352,23 +380,17 @@ async def get_mining_stat(stat_slug: str, gerty): async def api_get_mining_stat(stat_slug: str, gerty): - stat = "" + stat = {} if stat_slug == "mining_current_hash_rate": - async with httpx.AsyncClient() as client: - r = await get_mempool_info("hashrate_1m", gerty) - data = r - stat = {} - stat["current"] = data["currentHashrate"] - stat["1w"] = data["hashrates"][len(data["hashrates"]) - 7]["avgHashrate"] + r = await get_mempool_info("hashrate_1m", gerty) + data = r + stat["current"] = data["currentHashrate"] + stat["1w"] = data["hashrates"][len(data["hashrates"]) - 7]["avgHashrate"] elif stat_slug == "mining_current_difficulty": - async with httpx.AsyncClient() as client: - r = await get_mempool_info("hashrate_1m", gerty) - data = r - stat = {} - stat["current"] = data["currentDifficulty"] - stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2][ - "difficulty" - ] + r = await get_mempool_info("hashrate_1m", gerty) + data = r + stat["current"] = data["currentDifficulty"] + stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2]["difficulty"] return stat @@ -384,7 +406,7 @@ async def get_satoshi(): quote = satoshiQuotes[random.randint(0, len(satoshiQuotes) - 1)] # logger.debug(quote.text) if len(quote["text"]) > maxQuoteLength: - logger.debug("Quote is too long, getting another") + logger.trace("Quote is too long, getting another") return await get_satoshi() else: return quote @@ -399,15 +421,16 @@ def get_screen_slug_by_index(index: int, screens_list): # Get a list of text items for the screen number -async def get_screen_data(screen_num: int, screens_list: dict, gerty): +async def get_screen_data(screen_num: int, screens_list: list, gerty): screen_slug = get_screen_slug_by_index(screen_num, screens_list) # first get the relevant slug from the display_preferences - areas = [] + areas: List = [] title = "" if screen_slug == "dashboard": title = gerty.name areas = await get_dashboard(gerty) + if screen_slug == "lnbits_wallets_balance": wallets = await get_lnbits_wallet_balances(gerty) @@ -505,10 +528,10 @@ async def get_screen_data(screen_num: int, screens_list: dict, gerty): title = "Lightning Network" areas = await get_lightning_stats(gerty) - data = {} - data["title"] = title - data["areas"] = areas - + data = { + "title": title, + "areas": areas, + } return data @@ -570,7 +593,7 @@ async def get_dashboard(gerty): text = [] text.append( get_text_item_dict( - text=await get_time_remaining_next_difficulty_adjustment(gerty), + text=await get_time_remaining_next_difficulty_adjustment(gerty) or "0", font_size=15, gerty_type=gerty.type, ) @@ -602,7 +625,7 @@ async def get_lnbits_wallet_balances(gerty): return wallets -async def get_placeholder_text(): +async def get_placeholder_text(gerty): return [ get_text_item_dict( text="Some placeholder text", @@ -810,14 +833,14 @@ async def get_time_remaining_next_difficulty_adjustment(gerty): r = await get_mempool_info("difficulty_adjustment", gerty) stat = r["remainingTime"] time = get_time_remaining(stat / 1000, 3) - return time + return time async def get_mempool_stat(stat_slug: str, gerty): text = [] if isinstance(gerty.mempool_endpoint, str): if stat_slug == "mempool_tx_count": - r = get_mempool_info("mempool", gerty) + r = await get_mempool_info("mempool", gerty) if stat_slug == "mempool_tx_count": stat = round(r["count"]) text.append( @@ -921,31 +944,3 @@ async def get_mempool_stat(stat_slug: str, gerty): ) ) return text - - -def get_date_suffix(dayNumber): - if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30: - return "th" - else: - return ["st", "nd", "rd"][dayNumber % 10 - 1] - - -def get_time_remaining(seconds, granularity=2): - intervals = ( - # ('weeks', 604800), # 60 * 60 * 24 * 7 - ("days", 86400), # 60 * 60 * 24 - ("hours", 3600), # 60 * 60 - ("minutes", 60), - ("seconds", 1), - ) - - result = [] - - for name, count in intervals: - value = seconds // count - if value: - seconds -= value * count - if value == 1: - name = name.rstrip("s") - result.append("{} {}".format(round(value), name)) - return ", ".join(result[:granularity]) diff --git a/lnbits/extensions/gerty/models.py b/lnbits/extensions/gerty/models.py index 9ff29bda..cb19c2bc 100644 --- a/lnbits/extensions/gerty/models.py +++ b/lnbits/extensions/gerty/models.py @@ -1,5 +1,4 @@ from sqlite3 import Row -from typing import Optional from fastapi import Query from pydantic import BaseModel diff --git a/lnbits/extensions/gerty/templates/gerty/gerty.html b/lnbits/extensions/gerty/templates/gerty/gerty.html index d45484a4..06a29e22 100644 --- a/lnbits/extensions/gerty/templates/gerty/gerty.html +++ b/lnbits/extensions/gerty/templates/gerty/gerty.html @@ -32,7 +32,10 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %} -
+
- +
Mining
@@ -78,7 +81,12 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
- +
Lightning (Last 7 days)
@@ -88,7 +96,6 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}

-
Servers to check
@@ -153,7 +160,13 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %} lnbits_wallets_balance: {}, dashboard_onchain: {}, fun_satoshi_quotes: {}, - fun_exchange_market_rate: {}, + fun_exchange_market_rate: { + unit: '' + }, + dashboard_mining: {}, + lightning_dashboard: {}, + url_checker: {}, + dashboard_mining: {}, gerty: [], gerty_id: `{{gerty}}`, gertyname: '', @@ -182,7 +195,6 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %} LNbits.utils.notifyApiError(error) } } - console.log(this.gerty) for (let i = 0; i < this.gerty.length; i++) { if (this.gerty[i].screen.group == 'lnbits_wallets_balance') { for (let q = 0; q < this.gerty[i].screen.areas.length; q++) { diff --git a/lnbits/extensions/gerty/views.py b/lnbits/extensions/gerty/views.py index 66194a50..33e95d3e 100644 --- a/lnbits/extensions/gerty/views.py +++ b/lnbits/extensions/gerty/views.py @@ -1,10 +1,7 @@ -import json from http import HTTPStatus -from fastapi import Request -from fastapi.params import Depends +from fastapi import Depends, Request from fastapi.templating import Jinja2Templates -from loguru import logger from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse @@ -13,7 +10,6 @@ from lnbits.decorators import check_user_exists from . import gerty_ext, gerty_renderer from .crud import get_gerty -from .views_api import api_gerty_json templates = Jinja2Templates(directory="templates") diff --git a/lnbits/extensions/gerty/views_api.py b/lnbits/extensions/gerty/views_api.py index 7272fb7d..c408504b 100644 --- a/lnbits/extensions/gerty/views_api.py +++ b/lnbits/extensions/gerty/views_api.py @@ -1,24 +1,12 @@ import json -import math -import os -import random -import time -from datetime import datetime from http import HTTPStatus -import httpx -from fastapi import Query -from fastapi.params import Depends -from fastapi.templating import Jinja2Templates -from lnurl import decode as decode_lnurl +from fastapi import Depends, Query from loguru import logger from starlette.exceptions import HTTPException -from lnbits.core.crud import get_user, get_wallet_for_key -from lnbits.core.services import create_invoice -from lnbits.core.views.api import api_payment, api_wallet +from lnbits.core.crud import get_user from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key -from lnbits.utils.exchange_rates import satoshis_amount_as_fiat from . import gerty_ext from .crud import ( @@ -29,8 +17,14 @@ from .crud import ( get_mempool_info, update_gerty, ) -from .helpers import * -from .models import Gerty, MempoolEndpoint +from .helpers import ( + gerty_should_sleep, + get_next_update_time, + get_satoshi, + get_screen_data, + get_screen_slug_by_index, +) +from .models import Gerty @gerty_ext.get("/api/v1/gerty", status_code=HTTPStatus.OK) @@ -39,7 +33,8 @@ async def api_gertys( ): wallet_ids = [wallet.wallet.id] if all_wallets: - 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 [] return [gerty.dict() for gerty in await get_gertys(wallet_ids)] @@ -51,7 +46,6 @@ async def api_link_create_or_update( wallet: WalletTypeInfo = Depends(get_key_type), gerty_id: str = Query(None), ): - logger.debug(data) if gerty_id: gerty = await get_gerty(gerty_id) if not gerty: @@ -67,6 +61,9 @@ async def api_link_create_or_update( data.wallet = wallet.wallet.id gerty = await update_gerty(gerty_id, **data.dict()) + assert gerty, HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist" + ) else: gerty = await create_gerty(wallet_id=wallet.wallet.id, data=data) @@ -93,11 +90,11 @@ async def api_gerty_delete( @gerty_ext.get("/api/v1/gerty/satoshiquote", status_code=HTTPStatus.OK) async def api_gerty_satoshi(): - return await get_satoshi + return await get_satoshi() @gerty_ext.get("/api/v1/gerty/pages/{gerty_id}/{p}") -async def api_gerty_json(gerty_id: str, p: int = None): # page number +async def api_gerty_json(gerty_id: str, p: int = 0): # page number gerty = await get_gerty(gerty_id) if not gerty: @@ -117,7 +114,7 @@ async def api_gerty_json(gerty_id: str, p: int = None): # page number enabled_screen_count += 1 enabled_screens.append(screen_slug) - logger.debug("Screeens " + str(enabled_screens)) + logger.debug("Screens " + str(enabled_screens)) data = await get_screen_data(p, enabled_screens, gerty) next_screen_number = 0 if ((p + 1) >= enabled_screen_count) else p + 1 diff --git a/lnbits/extensions/lndhub/views.py b/lnbits/extensions/lndhub/views.py index 38a33a34..b216f8b1 100644 --- a/lnbits/extensions/lndhub/views.py +++ b/lnbits/extensions/lndhub/views.py @@ -1,5 +1,4 @@ -from fastapi import Request -from fastapi.params import Depends +from fastapi import Depends, Request from lnbits.core.models import User from lnbits.decorators import check_user_exists diff --git a/lnbits/extensions/lndhub/views_api.py b/lnbits/extensions/lndhub/views_api.py index c21c0bfd..1dff5235 100644 --- a/lnbits/extensions/lndhub/views_api.py +++ b/lnbits/extensions/lndhub/views_api.py @@ -1,15 +1,13 @@ -import asyncio import time from base64 import urlsafe_b64encode from http import HTTPStatus -from fastapi.param_functions import Query -from fastapi.params import Depends +from fastapi import Depends, Query from pydantic import BaseModel from starlette.exceptions import HTTPException from lnbits import bolt11 -from lnbits.core.crud import delete_expired_invoices, get_payments +from lnbits.core.crud import get_payments from lnbits.core.services import create_invoice, pay_invoice from lnbits.decorators import WalletTypeInfo from lnbits.settings import get_wallet_class, settings @@ -73,13 +71,13 @@ async def lndhub_addinvoice( } -class Invoice(BaseModel): +class CreateInvoice(BaseModel): invoice: str = Query(...) @lndhub_ext.post("/ext/payinvoice") async def lndhub_payinvoice( - r_invoice: Invoice, wallet: WalletTypeInfo = Depends(require_admin_key) + r_invoice: CreateInvoice, wallet: WalletTypeInfo = Depends(require_admin_key) ): try: await pay_invoice( diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index 18451848..423c6a46 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -1,5 +1,7 @@ from typing import List, Optional, Union +import shortuuid + from lnbits.helpers import urlsafe_short_hash from . import db @@ -12,7 +14,7 @@ async def create_lnurldevice( data: createLnurldevice, ) -> lnurldevices: if data.device == "pos" or data.device == "atm": - lnurldevice_id = str(await get_lnurldeviceposcount()) + lnurldevice_id = shortuuid.uuid()[:5] else: lnurldevice_id = urlsafe_short_hash() lnurldevice_key = urlsafe_short_hash() @@ -82,17 +84,6 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev return lnurldevices(**row) if row else None -async def get_lnurldeviceposcount() -> int: - row = await db.fetchall( - "SELECT * FROM lnurldevice.lnurldevices WHERE device = ? OR device = ?", - ( - "pos", - "atm", - ), - ) - return len(row) + 1 - - async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices: row = await db.fetchone( "SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,) diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index b8da5e43..2b574d42 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -4,7 +4,6 @@ import json import httpx from loguru import logger -from lnbits.core import db as core_db from lnbits.core.crud import update_payment_extra from lnbits.core.models import Payment from lnbits.helpers import get_current_extension_name @@ -22,9 +21,8 @@ async def wait_for_paid_invoices(): await on_invoice_paid(payment) -async def on_invoice_paid(payment: Payment) -> None: - if payment.extra.get("tag") != "lnurlp": - # not an lnurlp invoice +async def on_invoice_paid(payment: Payment): + if not payment.extra or payment.extra.get("tag") != "lnurlp": return if payment.extra.get("wh_status"): @@ -35,22 +33,23 @@ async def on_invoice_paid(payment: Payment) -> None: if pay_link and pay_link.webhook_url: async with httpx.AsyncClient() as client: try: - kwargs = { - "json": { + r: httpx.Response = await client.post( + pay_link.webhook_url, + json={ "payment_hash": payment.payment_hash, "payment_request": payment.bolt11, "amount": payment.amount, "comment": payment.extra.get("comment"), "lnurlp": pay_link.id, + "body": json.loads(pay_link.webhook_body) + if pay_link.webhook_body + else "", }, - "timeout": 40, - } - if pay_link.webhook_body: - kwargs["json"]["body"] = json.loads(pay_link.webhook_body) - if pay_link.webhook_headers: - kwargs["headers"] = json.loads(pay_link.webhook_headers) - - r: httpx.Response = await client.post(pay_link.webhook_url, **kwargs) + headers=json.loads(pay_link.webhook_headers) + if pay_link.webhook_headers + else None, + timeout=40, + ) await mark_webhook_sent( payment.payment_hash, r.status_code, diff --git a/lnbits/extensions/lnurlp/views.py b/lnbits/extensions/lnurlp/views.py index 4e9f487c..9bc78056 100644 --- a/lnbits/extensions/lnurlp/views.py +++ b/lnbits/extensions/lnurlp/views.py @@ -1,7 +1,6 @@ from http import HTTPStatus -from fastapi import Request -from fastapi.params import Depends +from fastapi import Depends, Request from fastapi.templating import Jinja2Templates from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse diff --git a/lnbits/extensions/lnurlp/views_api.py b/lnbits/extensions/lnurlp/views_api.py index d5966bf6..0fa739b0 100644 --- a/lnbits/extensions/lnurlp/views_api.py +++ b/lnbits/extensions/lnurlp/views_api.py @@ -1,9 +1,7 @@ import json from http import HTTPStatus -from fastapi import Request -from fastapi.param_functions import Query -from fastapi.params import Depends +from fastapi import Depends, Query, Request from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore from starlette.exceptions import HTTPException @@ -36,7 +34,8 @@ async def api_links( wallet_ids = [wallet.wallet.id] if all_wallets: - 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 [ @@ -137,6 +136,7 @@ async def api_link_create_or_update( link = await update_pay_link(**data.dict(), link_id=link_id) else: link = await create_pay_link(data, wallet_id=wallet.wallet.id) + assert link return {**link.dict(), "lnurl": link.lnurl(request)} diff --git a/lnbits/extensions/lnurlpayout/README.md b/lnbits/extensions/lnurlpayout/README.md deleted file mode 100644 index ddf209fe..00000000 --- a/lnbits/extensions/lnurlpayout/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# LNURLPayOut - -## Auto-dump a wallets funds to an LNURLpay diff --git a/lnbits/extensions/lnurlpayout/__init__.py b/lnbits/extensions/lnurlpayout/__init__.py deleted file mode 100644 index 9962290c..00000000 --- a/lnbits/extensions/lnurlpayout/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -import asyncio - -from fastapi import APIRouter - -from lnbits.db import Database -from lnbits.helpers import template_renderer -from lnbits.tasks import catch_everything_and_restart - -db = Database("ext_lnurlpayout") - -lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout"]) - - -def lnurlpayout_renderer(): - return template_renderer(["lnbits/extensions/lnurlpayout/templates"]) - - -from .tasks import wait_for_paid_invoices -from .views import * # noqa -from .views_api import * # noqa - - -def lnurlpayout_start(): - loop = asyncio.get_event_loop() - loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/lnurlpayout/config.json.example b/lnbits/extensions/lnurlpayout/config.json.example deleted file mode 100644 index b4160d7b..00000000 --- a/lnbits/extensions/lnurlpayout/config.json.example +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "LNURLPayout", - "short_description": "Autodump wallet funds to LNURLpay", - "icon": "exit_to_app", - "contributors": ["arcbtc","talvasconcelos"] -} diff --git a/lnbits/extensions/lnurlpayout/crud.py b/lnbits/extensions/lnurlpayout/crud.py deleted file mode 100644 index 0f9f98ac..00000000 --- a/lnbits/extensions/lnurlpayout/crud.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import CreateLnurlPayoutData, lnurlpayout - - -async def create_lnurlpayout( - wallet_id: str, admin_key: str, data: CreateLnurlPayoutData -) -> lnurlpayout: - lnurlpayout_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold) - VALUES (?, ?, ?, ?, ?, ?) - """, - ( - lnurlpayout_id, - data.title, - wallet_id, - admin_key, - data.lnurlpay, - data.threshold, - ), - ) - - lnurlpayout = await get_lnurlpayout(lnurlpayout_id) - assert lnurlpayout, "Newly created lnurlpayout couldn't be retrieved" - return lnurlpayout - - -async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]: - row = await db.fetchone( - "SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,) - ) - return lnurlpayout(**row) if row else None - - -async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]: - row = await db.fetchone( - "SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,) - ) - return lnurlpayout(**row) if row else None - - -async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [lnurlpayout(**row) if row else None for row in rows] - - -async def delete_lnurlpayout(lnurlpayout_id: str) -> None: - await db.execute( - "DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,) - ) diff --git a/lnbits/extensions/lnurlpayout/migrations.py b/lnbits/extensions/lnurlpayout/migrations.py deleted file mode 100644 index 7a45e495..00000000 --- a/lnbits/extensions/lnurlpayout/migrations.py +++ /dev/null @@ -1,16 +0,0 @@ -async def m001_initial(db): - """ - Initial lnurlpayouts table. - """ - await db.execute( - f""" - CREATE TABLE lnurlpayout.lnurlpayouts ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - wallet TEXT NOT NULL, - admin_key TEXT NOT NULL, - lnurlpay TEXT NOT NULL, - threshold {db.big_int} NOT NULL - ); - """ - ) diff --git a/lnbits/extensions/lnurlpayout/models.py b/lnbits/extensions/lnurlpayout/models.py deleted file mode 100644 index fc8be575..00000000 --- a/lnbits/extensions/lnurlpayout/models.py +++ /dev/null @@ -1,18 +0,0 @@ -from sqlite3 import Row - -from pydantic import BaseModel - - -class CreateLnurlPayoutData(BaseModel): - title: str - lnurlpay: str - threshold: int - - -class lnurlpayout(BaseModel): - id: str - title: str - wallet: str - admin_key: str - lnurlpay: str - threshold: int diff --git a/lnbits/extensions/lnurlpayout/tasks.py b/lnbits/extensions/lnurlpayout/tasks.py deleted file mode 100644 index 71f299be..00000000 --- a/lnbits/extensions/lnurlpayout/tasks.py +++ /dev/null @@ -1,91 +0,0 @@ -import asyncio -from http import HTTPStatus - -import httpx -from loguru import logger -from starlette.exceptions import HTTPException - -from lnbits.core import db as core_db -from lnbits.core.crud import get_wallet -from lnbits.core.models import Payment -from lnbits.core.services import pay_invoice -from lnbits.core.views.api import api_payments_decode -from lnbits.helpers import get_current_extension_name -from lnbits.tasks import register_invoice_listener - -from .crud import get_lnurlpayout_from_wallet - - -async def wait_for_paid_invoices(): - invoice_queue = asyncio.Queue() - register_invoice_listener(invoice_queue, get_current_extension_name()) - - while True: - payment = await invoice_queue.get() - await on_invoice_paid(payment) - - -async def on_invoice_paid(payment: Payment) -> None: - try: - # Check its got a payout associated with it - lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id) - logger.debug("LNURLpayout", lnurlpayout_link) - if lnurlpayout_link: - - # Check the wallet balance is more than the threshold - - wallet = await get_wallet(lnurlpayout_link.wallet) - threshold = lnurlpayout_link.threshold + (lnurlpayout_link.threshold * 0.02) - - if wallet.balance < threshold: - return - # Get the invoice from the LNURL to pay - async with httpx.AsyncClient() as client: - try: - url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay}) - if str(url["domain"])[0:4] != "http": - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken" - ) - - try: - r = await client.get(str(url["domain"]), timeout=40) - res = r.json() - try: - r = await client.get( - res["callback"] - + "?amount=" - + str( - int((wallet.balance - wallet.balance * 0.02) * 1000) - ), - timeout=40, - ) - res = r.json() - - if hasattr(res, "status") and res["status"] == "ERROR": - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, - detail=res["reason"], - ) - try: - await pay_invoice( - wallet_id=payment.wallet_id, - payment_request=res["pr"], - extra={"tag": "lnurlpayout"}, - ) - return - except: - pass - - except Exception as e: - print("ERROR", str(e)) - return - except (httpx.ConnectError, httpx.RequestError): - return - except Exception: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, - detail="Failed to save LNURLPayout", - ) - except: - return diff --git a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html deleted file mode 100644 index afe24c42..00000000 --- a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - GET - /lnurlpayout/api/v1/lnurlpayouts -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<lnurlpayout_object>, ...] -
Curl example
- curl -X GET {{ request.base_url }}lnurlpayout/api/v1/lnurlpayouts -H - "X-Api-Key: <invoice_key>" - -
-
-
- - - - POST - /lnurlpayout/api/v1/lnurlpayouts -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
- {"name": <string>, "currency": <string*ie USD*>} -
- Returns 201 CREATED (application/json) -
- {"currency": <string>, "id": <string>, "name": - <string>, "wallet": <string>} -
Curl example
- curl -X POST {{ request.base_url }}lnurlpayout/api/v1/lnurlpayouts -d - '{"name": <string>, "currency": <string>}' -H - "Content-type: application/json" -H "X-Api-Key: <admin_key>" - -
-
-
- - - - - DELETE - /lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.base_url - }}lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> -H - "X-Api-Key: <admin_key>" - -
-
-
- - - - GET - /lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<lnurlpayout_object>, ...] -
Curl example
- curl -X GET {{ request.base_url - }}lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> -H - "X-Api-Key: <invoice_key>" - -
-
-
-
diff --git a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html deleted file mode 100644 index 98230949..00000000 --- a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html +++ /dev/null @@ -1,271 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New LNURLPayout - - - - - -
-
-
LNURLPayout
-
-
- Export to CSV -
-
- - {% raw %} - - - - {% endraw %} - -
-
-
- -
- - -
- {{SITE_TITLE}} LNURLPayout extension -
-
- - - - {% include "lnurlpayout/_api_docs.html" %} - - - -
-
- - - - - - - - -
- Create LNURLPayout - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/lnurlpayout/views.py b/lnbits/extensions/lnurlpayout/views.py deleted file mode 100644 index 454a3332..00000000 --- a/lnbits/extensions/lnurlpayout/views.py +++ /dev/null @@ -1,22 +0,0 @@ -from http import HTTPStatus - -from fastapi import Request -from fastapi.params import Depends -from fastapi.templating import Jinja2Templates -from starlette.exceptions import HTTPException -from starlette.responses import HTMLResponse - -from lnbits.core.models import User -from lnbits.decorators import check_user_exists - -from . import lnurlpayout_ext, lnurlpayout_renderer -from .crud import get_lnurlpayout - -templates = Jinja2Templates(directory="templates") - - -@lnurlpayout_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): - return lnurlpayout_renderer().TemplateResponse( - "lnurlpayout/index.html", {"request": request, "user": user.dict()} - ) diff --git a/lnbits/extensions/lnurlpayout/views_api.py b/lnbits/extensions/lnurlpayout/views_api.py deleted file mode 100644 index 324eb5dd..00000000 --- a/lnbits/extensions/lnurlpayout/views_api.py +++ /dev/null @@ -1,118 +0,0 @@ -from http import HTTPStatus - -from fastapi import Query -from fastapi.params import Depends -from starlette.exceptions import HTTPException - -from lnbits.core.crud import get_payments, get_user -from lnbits.core.models import Payment -from lnbits.core.services import create_invoice -from lnbits.core.views.api import api_payment, api_payments_decode -from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key - -from . import lnurlpayout_ext -from .crud import ( - create_lnurlpayout, - delete_lnurlpayout, - get_lnurlpayout, - get_lnurlpayout_from_wallet, - get_lnurlpayouts, -) -from .models import CreateLnurlPayoutData, lnurlpayout -from .tasks import on_invoice_paid - - -@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK) -async def api_lnurlpayouts( - all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) -): - wallet_ids = [wallet.wallet.id] - if all_wallets: - wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids - - return [lnurlpayout.dict() for lnurlpayout in await get_lnurlpayouts(wallet_ids)] - - -@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED) -async def api_lnurlpayout_create( - data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type) -): - if await get_lnurlpayout_from_wallet(wallet.wallet.id): - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, - detail="Wallet already has lnurlpayout set", - ) - return - url = await api_payments_decode({"data": data.lnurlpay}) - if "domain" not in url: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded" - ) - return - if str(url["domain"])[0:4] != "http": - raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL") - return - lnurlpayout = await create_lnurlpayout( - wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data - ) - if not lnurlpayout: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout" - ) - return - return lnurlpayout.dict() - - -@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}") -async def api_lnurlpayout_delete( - lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) -): - lnurlpayout = await get_lnurlpayout(lnurlpayout_id) - - if not lnurlpayout: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="lnurlpayout does not exist." - ) - - if lnurlpayout.wallet != wallet.wallet.id: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout." - ) - - await delete_lnurlpayout(lnurlpayout_id) - return "", HTTPStatus.NO_CONTENT - - -@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK) -async def api_lnurlpayout_check( - lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type) -): - lnurlpayout = await get_lnurlpayout(lnurlpayout_id) - ## THIS - mock_payment = Payment( - checking_id="mock", - pending=False, - amount=1, - fee=1, - time=0000, - bolt11="mock", - preimage="mock", - payment_hash="mock", - wallet_id=lnurlpayout.wallet, - ) - ## INSTEAD OF THIS - # payments = await get_payments( - # wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True - # ) - - result = await on_invoice_paid(mock_payment) - return - - -# get payouts func -# lnurlpayouts = await get_lnurlpayouts(wallet_ids) -# for lnurlpayout in lnurlpayouts: -# payments = await get_payments( -# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True -# ) -# await on_invoice_paid(payments[0]) diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py index 6eb1d5d1..f1417810 100644 --- a/lnbits/extensions/tpos/tasks.py +++ b/lnbits/extensions/tpos/tasks.py @@ -20,10 +20,11 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: + if not payment.extra: + return if payment.extra.get("tag") != "tpos": return - tpos = await get_tpos(payment.extra.get("tposId")) tipAmount = payment.extra.get("tipAmount") strippedPayment = { @@ -34,14 +35,23 @@ async def on_invoice_paid(payment: Payment) -> None: "bolt11": payment.bolt11, } - await websocketUpdater(payment.extra.get("tposId"), str(strippedPayment)) + tpos_id = payment.extra.get("tposId") + assert tpos_id - if tipAmount is None: + tpos = await get_tpos(tpos_id) + assert tpos + + await websocketUpdater(tpos_id, str(strippedPayment)) + + if not tipAmount: # no tip amount return + wallet_id = tpos.tip_wallet + assert wallet_id + payment_hash, payment_request = await create_invoice( - wallet_id=tpos.tip_wallet, + wallet_id=wallet_id, amount=int(tipAmount), # sats internal=True, memo=f"tpos tip", diff --git a/lnbits/extensions/tpos/views.py b/lnbits/extensions/tpos/views.py index dac129a9..fee5914f 100644 --- a/lnbits/extensions/tpos/views.py +++ b/lnbits/extensions/tpos/views.py @@ -1,7 +1,6 @@ from http import HTTPStatus -from fastapi import Request -from fastapi.params import Depends +from fastapi import Depends, Request from fastapi.templating import Jinja2Templates from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index 3a51238a..05537f84 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -1,8 +1,7 @@ from http import HTTPStatus import httpx -from fastapi import Query -from fastapi.params import Depends +from fastapi import Depends, Query from lnurl import decode as decode_lnurl from loguru import logger from starlette.exceptions import HTTPException @@ -25,7 +24,8 @@ async def api_tposs( ): wallet_ids = [wallet.wallet.id] if all_wallets: - 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 [] return [tpos.dict() for tpos in await get_tposs(wallet_ids)] @@ -58,8 +58,9 @@ async def api_tpos_delete( @tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED) async def api_tpos_create_invoice( - amount: int = Query(..., ge=1), tipAmount: int = None, tpos_id: str = None -): + tpos_id: str, amount: int = Query(..., ge=1), tipAmount: int = 0 +) -> dict: + tpos = await get_tpos(tpos_id) if not tpos: @@ -67,7 +68,7 @@ async def api_tpos_create_invoice( status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist." ) - if tipAmount: + if tipAmount > 0: amount += tipAmount try: @@ -84,7 +85,7 @@ async def api_tpos_create_invoice( @tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices") -async def api_tpos_get_latest_invoices(tpos_id: str = None): +async def api_tpos_get_latest_invoices(tpos_id: str): try: payments = [ Payment.from_row(row) @@ -111,7 +112,7 @@ async def api_tpos_get_latest_invoices(tpos_id: str = None): "/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK ) async def api_tpos_pay_invoice( - lnurl_data: PayLnurlWData, payment_request: str = None, tpos_id: str = None + lnurl_data: PayLnurlWData, payment_request: str, tpos_id: str ): tpos = await get_tpos(tpos_id) diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 83404c62..68603f0a 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -1,6 +1,8 @@ from datetime import datetime from typing import List, Optional, Union +import shortuuid + from lnbits.helpers import urlsafe_short_hash from . import db @@ -8,9 +10,10 @@ from .models import CreateWithdrawData, HashCheck, WithdrawLink async def create_withdraw_link( - data: CreateWithdrawData, wallet_id: str, usescsv: str + data: CreateWithdrawData, wallet_id: str ) -> WithdrawLink: link_id = urlsafe_short_hash() + available_links = ",".join([str(i) for i in range(data.uses)]) await db.execute( """ INSERT INTO withdraw.withdraw_link ( @@ -45,7 +48,7 @@ async def create_withdraw_link( urlsafe_short_hash(), urlsafe_short_hash(), int(datetime.now().timestamp()) + data.wait_time, - usescsv, + available_links, data.webhook_url, data.webhook_headers, data.webhook_body, @@ -94,6 +97,26 @@ async def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[Withdraw return [WithdrawLink(**row) for row in rows] +async def remove_unique_withdraw_link(link: WithdrawLink, unique_hash: str) -> None: + unique_links = [ + x.strip() + for x in link.usescsv.split(",") + if unique_hash != shortuuid.uuid(name=link.id + link.unique_hash + x.strip()) + ] + await update_withdraw_link( + link.id, + usescsv=",".join(unique_links), + ) + + +async def increment_withdraw_link(link: WithdrawLink) -> None: + await update_withdraw_link( + link.id, + used=link.used + 1, + open_time=link.wait_time + int(datetime.now().timestamp()), + ) + + async def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]: if "is_unique" in kwargs: kwargs["is_unique"] = int(kwargs["is_unique"]) @@ -132,7 +155,7 @@ async def create_hash_check(the_hash: str, lnurl_id: str) -> HashCheck: return hashCheck -async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: +async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck: rowid = await db.fetchone( "SELECT * FROM withdraw.hash_check WHERE id = ?", (the_hash,) ) @@ -141,10 +164,10 @@ async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: ) if not rowlnurl: await create_hash_check(the_hash, lnurl_id) - return {"lnurl": True, "hash": False} + return HashCheck(lnurl=True, hash=False) else: if not rowid: await create_hash_check(the_hash, lnurl_id) - return {"lnurl": True, "hash": False} + return HashCheck(lnurl=True, hash=False) else: - return {"lnurl": True, "hash": True} + return HashCheck(lnurl=True, hash=True) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 86640443..5ef521fa 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -1,28 +1,27 @@ import json -import traceback from datetime import datetime from http import HTTPStatus import httpx -import shortuuid # type: ignore -from fastapi import HTTPException -from fastapi.param_functions import Query +import shortuuid +from fastapi import HTTPException, Query, Request, Response from loguru import logger -from starlette.requests import Request -from starlette.responses import HTMLResponse from lnbits.core.crud import update_payment_extra from lnbits.core.services import pay_invoice from . import withdraw_ext -from .crud import get_withdraw_link_by_hash, update_withdraw_link - -# FOR LNURLs WHICH ARE NOT UNIQUE +from .crud import ( + get_withdraw_link_by_hash, + increment_withdraw_link, + remove_unique_withdraw_link, +) +from .models import WithdrawLink @withdraw_ext.get( "/api/v1/lnurl/{unique_hash}", - response_class=HTMLResponse, + response_class=Response, name="withdraw.api_lnurl_response", ) async def api_lnurl_response(request: Request, unique_hash): @@ -53,9 +52,6 @@ async def api_lnurl_response(request: Request, unique_hash): return json.dumps(withdrawResponse) -# CALLBACK - - @withdraw_ext.get( "/api/v1/lnurl/cb/{unique_hash}", name="withdraw.api_lnurl_callback", @@ -99,105 +95,79 @@ async def api_lnurl_callback( detail=f"wait link open_time {link.open_time - now} seconds.", ) - usescsv = "" - - for x in range(1, link.uses - link.used): - usecv = link.usescsv.split(",") - usescsv += "," + str(usecv[x]) - usecsvback = usescsv - - found = False - if id_unique_hash is not None: - useslist = link.usescsv.split(",") - for ind, x in enumerate(useslist): - tohash = link.id + link.unique_hash + str(x) - if id_unique_hash == shortuuid.uuid(name=tohash): - found = True - useslist.pop(ind) - usescsv = ",".join(useslist) - if not found: + if id_unique_hash: + if check_unique_link(link, id_unique_hash): + await remove_unique_withdraw_link(link, id_unique_hash) + else: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found." ) - else: - usescsv = usescsv[1:] - - changesback = { - "open_time": link.wait_time, - "used": link.used, - "usescsv": usecsvback, - } try: - changes = { - "open_time": link.wait_time + now, - "used": link.used + 1, - "usescsv": usescsv, - } - await update_withdraw_link(link.id, **changes) - - payment_request = pr - payment_hash = await pay_invoice( wallet_id=link.wallet, - payment_request=payment_request, + payment_request=pr, max_sat=link.max_withdrawable, extra={"tag": "withdraw"}, ) - + await increment_withdraw_link(link) if link.webhook_url: - async with httpx.AsyncClient() as client: - try: - kwargs = { - "json": { - "payment_hash": payment_hash, - "payment_request": payment_request, - "lnurlw": link.id, - }, - "timeout": 40, - } - if link.webhook_body: - kwargs["json"]["body"] = json.loads(link.webhook_body) - if link.webhook_headers: - kwargs["headers"] = json.loads(link.webhook_headers) - - r: httpx.Response = await client.post(link.webhook_url, **kwargs) - await update_payment_extra( - payment_hash=payment_hash, - extra={ - "wh_success": r.is_success, - "wh_message": r.reason_phrase, - "wh_response": r.text, - }, - outgoing=True, - ) - except Exception as exc: - # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid - logger.error( - "Caught exception when dispatching webhook url: " + str(exc) - ) - await update_payment_extra( - payment_hash=payment_hash, - extra={"wh_success": False, "wh_message": str(exc)}, - outgoing=True, - ) - + await dispatch_webhook(link, payment_hash, pr) return {"status": "OK"} - except Exception as e: - await update_withdraw_link(link.id, **changesback) - logger.error(traceback.format_exc()) raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}" ) +def check_unique_link(link: WithdrawLink, unique_hash: str) -> bool: + return any( + unique_hash == shortuuid.uuid(name=link.id + link.unique_hash + x.strip()) + for x in link.usescsv.split(",") + ) + + +async def dispatch_webhook( + link: WithdrawLink, payment_hash: str, payment_request: str +) -> None: + async with httpx.AsyncClient() as client: + try: + r: httpx.Response = await client.post( + link.webhook_url, + json={ + "payment_hash": payment_hash, + "payment_request": payment_request, + "lnurlw": link.id, + "body": json.loads(link.webhook_body) if link.webhook_body else "", + }, + headers=json.loads(link.webhook_headers) + if link.webhook_headers + else None, + timeout=40, + ) + await update_payment_extra( + payment_hash=payment_hash, + extra={ + "wh_success": r.is_success, + "wh_message": r.reason_phrase, + "wh_response": r.text, + }, + outgoing=True, + ) + except Exception as exc: + # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid + logger.error("Caught exception when dispatching webhook url: " + str(exc)) + await update_payment_extra( + payment_hash=payment_hash, + extra={"wh_success": False, "wh_message": str(exc)}, + outgoing=True, + ) + + # FOR LNURLs WHICH ARE UNIQUE - - @withdraw_ext.get( "/api/v1/lnurl/{unique_hash}/{id_unique_hash}", - response_class=HTMLResponse, + response_class=Response, name="withdraw.api_lnurl_multi_response", ) async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash): @@ -213,14 +183,7 @@ async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent." ) - useslist = link.usescsv.split(",") - found = False - for x in useslist: - tohash = link.id + link.unique_hash + str(x) - if id_unique_hash == shortuuid.uuid(name=tohash): - found = True - - if not found: + if not check_unique_link(link, id_unique_hash): raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found." ) diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 51c6a1cf..49421a79 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -1,9 +1,8 @@ -from sqlite3 import Row - -import shortuuid # type: ignore -from fastapi.param_functions import Query +import shortuuid +from fastapi import Query from lnurl import Lnurl, LnurlWithdrawResponse -from lnurl import encode as lnurl_encode # type: ignore +from lnurl import encode as lnurl_encode +from lnurl.models import ClearnetUrl, MilliSatoshi from pydantic import BaseModel from starlette.requests import Request @@ -67,18 +66,14 @@ class WithdrawLink(BaseModel): name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash ) return LnurlWithdrawResponse( - callback=url, + callback=ClearnetUrl(url, scheme="https"), k1=self.k1, - min_withdrawable=self.min_withdrawable * 1000, - max_withdrawable=self.max_withdrawable * 1000, - default_description=self.title, + minWithdrawable=MilliSatoshi(self.min_withdrawable * 1000), + maxWithdrawable=MilliSatoshi(self.max_withdrawable * 1000), + defaultDescription=self.title, ) class HashCheck(BaseModel): - id: str - lnurl_id: str - - @classmethod - def from_row(cls, row: Row) -> "Hash": - return cls(**dict(row)) + hash: bool + lnurl: bool diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index 6d211ed4..e8e5719a 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -2,10 +2,8 @@ from http import HTTPStatus from io import BytesIO import pyqrcode -from fastapi import Request -from fastapi.params import Depends +from fastapi import Depends, HTTPException, Request from fastapi.templating import Jinja2Templates -from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse, StreamingResponse from lnbits.core.models import User diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py index e0d3e56f..525796c9 100644 --- a/lnbits/extensions/withdraw/views_api.py +++ b/lnbits/extensions/withdraw/views_api.py @@ -1,10 +1,7 @@ from http import HTTPStatus -from fastapi.param_functions import Query -from fastapi.params import Depends -from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore -from starlette.exceptions import HTTPException -from starlette.requests import Request +from fastapi import Depends, HTTPException, Query, Request +from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl from lnbits.core.crud import get_user from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key @@ -30,7 +27,8 @@ async def api_links( wallet_ids = [wallet.wallet.id] if all_wallets: - 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 [ @@ -47,7 +45,7 @@ async def api_links( @withdraw_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) async def api_link_retrieve( - link_id, request: Request, wallet: WalletTypeInfo = Depends(get_key_type) + link_id: str, request: Request, wallet: WalletTypeInfo = Depends(get_key_type) ): link = await get_withdraw_link(link_id, 0) @@ -68,7 +66,7 @@ async def api_link_retrieve( async def api_link_create_or_update( req: Request, data: CreateWithdrawData, - link_id: str = None, + link_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key), ): if data.uses > 250: @@ -85,14 +83,6 @@ async def api_link_create_or_update( status_code=HTTPStatus.BAD_REQUEST, ) - usescsv = "" - for i in range(data.uses): - if data.is_unique: - usescsv += "," + str(i + 1) - else: - usescsv += "," + str(1) - usescsv = usescsv[1:] - if link_id: link = await get_withdraw_link(link_id, 0) if not link: @@ -103,13 +93,10 @@ async def api_link_create_or_update( raise HTTPException( detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN ) - link = await update_withdraw_link( - link_id, **data.dict(), usescsv=usescsv, used=0 - ) + link = await update_withdraw_link(link_id, **data.dict()) else: - link = await create_withdraw_link( - wallet_id=wallet.wallet.id, data=data, usescsv=usescsv - ) + link = await create_withdraw_link(wallet_id=wallet.wallet.id, data=data) + assert link return {**link.dict(), **{"lnurl": link.lnurl(req)}} @@ -131,9 +118,11 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi return {"success": True} -@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK) -async def api_hash_retrieve( - the_hash, lnurl_id, wallet: WalletTypeInfo = Depends(get_key_type) -): +@withdraw_ext.get( + "/api/v1/links/{the_hash}/{lnurl_id}", + status_code=HTTPStatus.OK, + dependencies=[Depends(get_key_type)], +) +async def api_hash_retrieve(the_hash, lnurl_id): hashCheck = await get_hash_check(the_hash, lnurl_id) return hashCheck diff --git a/pyproject.toml b/pyproject.toml index f1b4e05f..606420ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,18 +92,13 @@ exclude = """(?x)( ^lnbits/extensions/bleskomat. | ^lnbits/extensions/boltz. | ^lnbits/extensions/boltcards. - | ^lnbits/extensions/gerty. | ^lnbits/extensions/invoices. | ^lnbits/extensions/livestream. | ^lnbits/extensions/lnaddress. - | ^lnbits/extensions/lndhub. | ^lnbits/extensions/lnurldevice. - | ^lnbits/extensions/lnurlp. | ^lnbits/extensions/satspay. | ^lnbits/extensions/streamalerts. - | ^lnbits/extensions/tpos. | ^lnbits/extensions/watchonly. - | ^lnbits/extensions/withdraw. | ^lnbits/wallets/lnd_grpc_files. )"""