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 %}
-
-
-
- {{ col.label}}
-
-
-
-
-
-
-
-
- Click to copy LNURL{{
- col.value.substring(0, 40) }}...
-
- {{ col.value }} Sats
-
- {{ col.value.substring(0, 40) }}
-
-
-
-
-
-
- {% 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.
)"""