Merge branch 'main' into fix/mypy-offlineshop

This commit is contained in:
ben 2023-01-04 19:39:01 +00:00
commit 71f2923ee1
33 changed files with 271 additions and 1065 deletions

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
from sqlite3 import Row
from typing import Optional
from fastapi import Query
from pydantic import BaseModel

View file

@ -32,7 +32,10 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
</q-card>
</div>
<div class="q-pa-md row items-start q-gutter-md" v-if="lnbits_wallets_balance">
<div
class="q-pa-md row items-start q-gutter-md"
v-if="lnbits_wallets_balance[0]"
>
<q-card
class="q-pa-sm"
v-for="(wallet, t) in lnbits_wallets_balance"
@ -49,7 +52,7 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
<div
class="q-pa-md row items-start q-gutter-md"
v-if="dashboard_onchain || dashboard_mining || lightning_dashboard"
v-if="dashboard_onchain[0] || dashboard_mining[0] || lightning_dashboard[0] || url_checker[0]"
>
<q-card
class="q-pa-sm"
@ -67,7 +70,7 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
</q-card-section>
</q-card>
<q-card class="q-pa-sm" v-if="dashboard_mining" unelevated class="q-pa-sm">
<q-card class="q-pa-sm" v-if="dashboard_mining[0]" unelevated class="q-pa-sm">
<q-card-section>
<div class="text-h6">Mining</div>
</q-card-section>
@ -78,7 +81,12 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
</q-card-section>
</q-card>
<q-card class="q-pa-sm" v-if="lightning_dashboard" unelevated class="q-pa-sm">
<q-card
class="q-pa-sm"
v-if="lightning_dashboard[0]"
unelevated
class="q-pa-sm"
>
<q-card-section>
<div class="text-h6">Lightning (Last 7 days)</div>
</q-card-section>
@ -88,7 +96,6 @@ gertyname }}{% endraw %}{% endblock %}{% block page %} {% raw %}
</p>
</q-card-section>
</q-card>
<q-card class="q-pa-sm" v-if="url_checker" unelevated class="q-pa-sm">
<q-card-section>
<div class="text-h6">Servers to check</div>
@ -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++) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +0,0 @@
# LNURLPayOut
## Auto-dump a wallets funds to an LNURLpay

View file

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

View file

@ -1,6 +0,0 @@
{
"name": "LNURLPayout",
"short_description": "Autodump wallet funds to LNURLpay",
"icon": "exit_to_app",
"contributors": ["arcbtc","talvasconcelos"]
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,119 +0,0 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/lnurlpayout"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List lnurlpayout">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/lnurlpayout/api/v1/lnurlpayouts</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpayout_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}lnurlpayout/api/v1/lnurlpayouts -H
"X-Api-Key: &lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create a lnurlpayout"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/lnurlpayout/api/v1/lnurlpayouts</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"name": &lt;string&gt;, "currency": &lt;string*ie USD*&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name":
&lt;string&gt;, "wallet": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}lnurlpayout/api/v1/lnurlpayouts -d
'{"name": &lt;string&gt;, "currency": &lt;string&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a lnurlpayout"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/lnurlpayout/api/v1/lnurlpayouts/&lt;lnurlpayout_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}lnurlpayout/api/v1/lnurlpayouts/&lt;lnurlpayout_id&gt; -H
"X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Check lnurlpayout"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/lnurlpayout/api/v1/lnurlpayouts/&lt;lnurlpayout_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpayout_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}lnurlpayout/api/v1/lnurlpayouts/&lt;lnurlpayout_id&gt; -H
"X-Api-Key: &lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View file

@ -1,271 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New LNURLPayout</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">LNURLPayout</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="lnurlpayouts"
row-key="id"
:columns="lnurlpayoutsTable.columns"
:pagination.sync="lnurlpayoutsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label}}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<a
class="text-secondary"
v-if="col.label == 'LNURLPay'"
@click="copyText(col.value)"
><q-tooltip>Click to copy LNURL</q-tooltip>{{
col.value.substring(0, 40) }}...</a
>
<div v-else-if="col.label == 'Threshold'">
{{ col.value }} Sats
</div>
<div v-else>{{ col.value.substring(0, 40) }}</div>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deletelnurlpayout(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} LNURLPayout extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "lnurlpayout/_api_docs.html" %}
<q-separator></q-separator>
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="createlnurlpayout" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.title"
label="Title"
placeholder="Title"
type="text"
></q-input>
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.lnurlpay"
label="LNURLPay"
placeholder="LNURLPay"
type="text"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.threshold"
label="Threshold (100k sats max)"
placeholder="Threshold"
type="number"
max="100000"
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.threshold == null"
type="submit"
>Create LNURLPayout</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var maplnurlpayout = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.lnurlpayout = ['/lnurlpayout/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
lnurlpayouts: [],
lnurlpayoutsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'title', align: 'left', label: 'Title', field: 'title'},
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
{
name: 'lnurlpay',
align: 'left',
label: 'LNURLPay',
field: 'lnurlpay'
},
{
name: 'threshold',
align: 'left',
label: 'Threshold',
field: 'threshold'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
}
}
},
methods: {
closeFormDialog: function () {
this.formDialog.data = {}
},
getlnurlpayouts: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnurlpayout/api/v1/lnurlpayouts?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.lnurlpayouts = response.data.map(function (obj) {
return maplnurlpayout(obj)
})
})
},
createlnurlpayout: function () {
var data = {
title: this.formDialog.data.title,
lnurlpay: this.formDialog.data.lnurlpay,
threshold: this.formDialog.data.threshold
}
var self = this
LNbits.api
.request(
'POST',
'/lnurlpayout/api/v1/lnurlpayouts',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data
)
.then(function (response) {
console.log(data)
self.lnurlpayouts.push(maplnurlpayout(response.data))
self.formDialog.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deletelnurlpayout: function (lnurlpayoutId) {
var self = this
var lnurlpayout = _.findWhere(this.lnurlpayouts, {id: lnurlpayoutId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this lnurlpayout?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnurlpayout/api/v1/lnurlpayouts/' + lnurlpayoutId,
_.findWhere(self.g.user.wallets, {id: lnurlpayout.wallet})
.adminkey
)
.then(function (response) {
self.lnurlpayouts = _.reject(self.lnurlpayouts, function (obj) {
return obj.id == lnurlpayoutId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(
this.lnurlpayoutsTable.columns,
this.lnurlpayouts
)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getlnurlpayouts()
}
}
})
</script>
{% endblock %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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