diff --git a/lnbits/core/models.py b/lnbits/core/models.py
index e292362a..31383667 100644
--- a/lnbits/core/models.py
+++ b/lnbits/core/models.py
@@ -88,7 +88,7 @@ class Payment(BaseModel):
preimage: str
payment_hash: str
expiry: Optional[float]
- extra: Optional[Dict] = {}
+ extra: Dict = {}
wallet_id: str
webhook: Optional[str]
webhook_status: Optional[int]
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html
index 0f68a0fe..46cfc690 100644
--- a/lnbits/core/templates/core/wallet.html
+++ b/lnbits/core/templates/core/wallet.html
@@ -468,10 +468,10 @@
dense
v-model.number="receive.data.amount"
:label="'Amount (' + receive.unit + ') *'"
- :mask="receive.unit != 'sats' ? '#.##' : '#'"
+ :mask="receive.unit != 'sat' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
- :step="receive.unit != 'sats' ? '0.01' : '1'"
+ :step="receive.unit != 'sat' ? '0.01' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
diff --git a/lnbits/extensions/boltcards/crud.py b/lnbits/extensions/boltcards/crud.py
index 4fae31f9..cc5d5193 100644
--- a/lnbits/extensions/boltcards/crud.py
+++ b/lnbits/extensions/boltcards/crud.py
@@ -1,5 +1,5 @@
import secrets
-from datetime import date, datetime
+from datetime import datetime
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
@@ -124,7 +124,6 @@ async def get_card_by_otp(otp: str) -> Optional[Card]:
async def delete_card(card_id: str) -> None:
# Delete cards
- card = await get_card(card_id)
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
# Delete hits
hits = await get_hits([card_id])
@@ -146,7 +145,7 @@ async def update_card_counter(counter: int, id: str):
async def enable_disable_card(enable: bool, id: str) -> Optional[Card]:
- row = await db.execute(
+ await db.execute(
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
(enable, id),
)
@@ -161,7 +160,7 @@ async def update_card_otp(otp: str, id: str):
async def get_hit(hit_id: str) -> Optional[Hit]:
- row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id))
+ row = await db.fetchone(f"SELECT * FROM boltcards.hits WHERE id = ?", (hit_id,))
if not row:
return None
@@ -182,7 +181,7 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
return [Hit(**row) for row in rows]
-async def get_hits_today(card_id: str) -> Optional[Hit]:
+async def get_hits_today(card_id: str) -> List[Hit]:
rows = await db.fetchall(
f"SELECT * FROM boltcards.hits WHERE card_id = ?",
(card_id,),
@@ -259,7 +258,7 @@ async def create_refund(hit_id, refund_amount) -> Refund:
async def get_refund(refund_id: str) -> Optional[Refund]:
row = await db.fetchone(
- f"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id)
+ f"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id,)
)
if not row:
return None
@@ -267,7 +266,7 @@ async def get_refund(refund_id: str) -> Optional[Refund]:
return Refund.parse_obj(refund)
-async def get_refunds(hits_ids: Union[str, List[str]]) -> List[Refund]:
+async def get_refunds(hits_ids: List[Hit]) -> List[Refund]:
if len(hits_ids) == 0:
return []
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 3a99073a..d0430372 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -3,13 +3,9 @@ import secrets
from http import HTTPStatus
from urllib.parse import urlparse
-from fastapi import Request
-from fastapi.param_functions import Query
-from fastapi.params import Depends, Query
-from lnurl import encode as lnurl_encode # type: ignore
-from lnurl.types import LnurlPayMetadata # type: ignore
-from starlette.exceptions import HTTPException
-from starlette.requests import Request
+from fastapi import HTTPException, Query, Request
+from lnurl import encode as lnurl_encode
+from lnurl.types import LnurlPayMetadata
from starlette.responses import HTMLResponse
from lnbits import bolt11
@@ -28,14 +24,13 @@ from .crud import (
update_card_counter,
update_card_otp,
)
-from .models import CreateCardData
from .nxp424 import decryptSUN, getSunMAC
###############LNURLWITHDRAW#################
# /boltcards/api/v1/scan?p=00000000000000000000000000000000&c=0000000000000000
@boltcards_ext.get("/api/v1/scan/{external_id}")
-async def api_scan(p, c, request: Request, external_id: str = None):
+async def api_scan(p, c, request: Request, external_id: str = Query(None)):
# some wallets send everything as lower case, no bueno
p = p.upper()
c = c.upper()
@@ -63,6 +58,7 @@ async def api_scan(p, c, request: Request, external_id: str = None):
await update_card_counter(ctr_int, card.id)
# gathering some info for hit record
+ assert request.client
ip = request.client.host
if "x-real-ip" in request.headers:
ip = request.headers["x-real-ip"]
@@ -95,7 +91,6 @@ async def api_scan(p, c, request: Request, external_id: str = None):
name="boltcards.lnurl_callback",
)
async def lnurl_callback(
- request: Request,
pr: str = Query(None),
k1: str = Query(None),
):
@@ -120,7 +115,9 @@ async def lnurl_callback(
return {"status": "ERROR", "reason": "Failed to decode payment request"}
card = await get_card(hit.card_id)
+ assert card
hit = await spend_hit(id=hit.id, amount=int(invoice.amount_msat / 1000))
+ assert hit
try:
await pay_invoice(
wallet_id=card.wallet,
@@ -155,7 +152,7 @@ async def api_auth(a, request: Request):
response = {
"card_name": card.card_name,
- "id": 1,
+ "id": str(1),
"k0": card.k0,
"k1": card.k1,
"k2": card.k2,
@@ -163,7 +160,7 @@ async def api_auth(a, request: Request):
"k4": card.k2,
"lnurlw_base": "lnurlw://" + lnurlw_base,
"protocol_name": "new_bolt_card_response",
- "protocol_version": 1,
+ "protocol_version": str(1),
}
return response
@@ -179,7 +176,9 @@ async def api_auth(a, request: Request):
)
async def lnurlp_response(req: Request, hit_id: str = Query(None)):
hit = await get_hit(hit_id)
+ assert hit
card = await get_card(hit.card_id)
+ assert card
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
if not card.enable:
@@ -199,17 +198,17 @@ async def lnurlp_response(req: Request, hit_id: str = Query(None)):
response_class=HTMLResponse,
name="boltcards.lnurlp_callback",
)
-async def lnurlp_callback(
- req: Request, hit_id: str = Query(None), amount: str = Query(None)
-):
+async def lnurlp_callback(hit_id: str = Query(None), amount: str = Query(None)):
hit = await get_hit(hit_id)
+ assert hit
card = await get_card(hit.card_id)
+ assert card
if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
- payment_hash, payment_request = await create_invoice(
+ _, payment_request = await create_invoice(
wallet_id=card.wallet,
- amount=int(amount) / 1000,
+ amount=int(int(amount) / 1000),
memo=f"Refund {hit_id}",
unhashed_description=LnurlPayMetadata(
json.dumps([["text/plain", "Refund"]])
diff --git a/lnbits/extensions/boltcards/models.py b/lnbits/extensions/boltcards/models.py
index 47ca1df0..5ea4be15 100644
--- a/lnbits/extensions/boltcards/models.py
+++ b/lnbits/extensions/boltcards/models.py
@@ -1,14 +1,11 @@
+import json
from sqlite3 import Row
-from typing import Optional
-from fastapi import Request
-from fastapi.params import Query
+from fastapi import Query, Request
from lnurl import Lnurl
-from lnurl import encode as lnurl_encode # type: ignore
-from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
-from lnurl.types import LnurlPayMetadata # type: ignore
+from lnurl import encode as lnurl_encode
+from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel
-from pydantic.main import BaseModel
ZERO_KEY = "00000000000000000000000000000000"
@@ -32,6 +29,7 @@ class Card(BaseModel):
otp: str
time: int
+ @classmethod
def from_row(cls, row: Row) -> "Card":
return cls(**dict(row))
@@ -40,7 +38,7 @@ class Card(BaseModel):
return lnurl_encode(url)
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
- return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
+ return LnurlPayMetadata(json.dumps([["text/plain", self.card_name]]))
class CreateCardData(BaseModel):
@@ -69,6 +67,7 @@ class Hit(BaseModel):
amount: int
time: int
+ @classmethod
def from_row(cls, row: Row) -> "Hit":
return cls(**dict(row))
@@ -79,5 +78,6 @@ class Refund(BaseModel):
refund_amount: int
time: int
+ @classmethod
def from_row(cls, row: Row) -> "Refund":
return cls(**dict(row))
diff --git a/lnbits/extensions/boltcards/tasks.py b/lnbits/extensions/boltcards/tasks.py
index c1e99b76..ab2547af 100644
--- a/lnbits/extensions/boltcards/tasks.py
+++ b/lnbits/extensions/boltcards/tasks.py
@@ -1,8 +1,6 @@
import asyncio
import json
-import httpx
-
from lnbits.core import db as core_db
from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
@@ -21,22 +19,25 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
+
if not payment.extra.get("refund"):
return
if payment.extra.get("wh_status"):
# this webhook has already been sent
return
- hit = await get_hit(payment.extra.get("refund"))
+
+ hit = await get_hit(str(payment.extra.get("refund")))
if hit:
- refund = await create_refund(
- hit_id=hit.id, refund_amount=(payment.amount / 1000)
- )
+ await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
await mark_webhook_sent(payment, 1)
async def mark_webhook_sent(payment: Payment, status: int) -> None:
+ if not payment.extra:
+ return
+
payment.extra["wh_status"] = status
await core_db.execute(
diff --git a/lnbits/extensions/boltcards/views.py b/lnbits/extensions/boltcards/views.py
index 8fcbb7de..273cfcbf 100644
--- a/lnbits/extensions/boltcards/views.py
+++ b/lnbits/extensions/boltcards/views.py
@@ -1,5 +1,4 @@
-from fastapi import FastAPI, Request
-from fastapi.params import Depends
+from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
diff --git a/lnbits/extensions/boltcards/views_api.py b/lnbits/extensions/boltcards/views_api.py
index c18c33d0..feca12e0 100644
--- a/lnbits/extensions/boltcards/views_api.py
+++ b/lnbits/extensions/boltcards/views_api.py
@@ -1,10 +1,6 @@
-import secrets
from http import HTTPStatus
-from fastapi.params import Depends, Query
-from loguru import logger
-from starlette.exceptions import HTTPException
-from starlette.requests import Request
+from fastapi import Depends, HTTPException, Query
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
@@ -15,13 +11,11 @@ from .crud import (
delete_card,
enable_disable_card,
get_card,
- get_card_by_otp,
get_card_by_uid,
get_cards,
get_hits,
get_refunds,
update_card,
- update_card_otp,
)
from .models import CreateCardData
@@ -33,7 +27,8 @@ async def api_cards(
wallet_ids = [g.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
return [card.dict() for card in await get_cards(wallet_ids)]
@@ -41,9 +36,8 @@ async def api_cards(
@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
async def api_card_create_or_update(
- # req: Request,
data: CreateCardData,
- card_id: str = None,
+ card_id: str = Query(None),
wallet: WalletTypeInfo = Depends(require_admin_key),
):
try:
@@ -95,6 +89,7 @@ async def api_card_create_or_update(
status_code=HTTPStatus.BAD_REQUEST,
)
card = await create_card(wallet_id=wallet.wallet.id, data=data)
+ assert card
return card.dict()
@@ -110,6 +105,7 @@ async def enable_card(
if card.wallet != wallet.wallet.id:
raise HTTPException(detail="Not your card.", status_code=HTTPStatus.FORBIDDEN)
card = await enable_disable_card(enable=enable, id=card_id)
+ assert card
return card.dict()
@@ -136,7 +132,8 @@ async def api_hits(
wallet_ids = [g.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
cards = await get_cards(wallet_ids)
cards_ids = []
@@ -153,15 +150,13 @@ async def api_refunds(
wallet_ids = [g.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
cards = await get_cards(wallet_ids)
cards_ids = []
for card in cards:
cards_ids.append(card.id)
hits = await get_hits(cards_ids)
- hits_ids = []
- for hit in hits:
- hits_ids.append(hit.id)
- return [refund.dict() for refund in await get_refunds(hits_ids)]
+ return [refund.dict() for refund in await get_refunds(hits)]
diff --git a/lnbits/extensions/cashu/tasks.py b/lnbits/extensions/cashu/tasks.py
index 9de17a1c..bf49171c 100644
--- a/lnbits/extensions/cashu/tasks.py
+++ b/lnbits/extensions/cashu/tasks.py
@@ -28,6 +28,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra and not payment.extra.get("tag") == "cashu":
+ if payment.extra.get("tag") != "cashu":
return
+
return
diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py
index 384070cd..4975b5a3 100644
--- a/lnbits/extensions/copilot/tasks.py
+++ b/lnbits/extensions/copilot/tasks.py
@@ -24,12 +24,12 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- webhook = None
- data = None
- if not payment.extra or payment.extra.get("tag") != "copilot":
+ if payment.extra.get("tag") != "copilot":
# not an copilot invoice
return
+ webhook = None
+ data = None
copilot = await get_copilot(payment.extra.get("copilotid", -1))
if not copilot:
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 2fc0a7c1..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 = ""
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/invoices/crud.py b/lnbits/extensions/invoices/crud.py
index 4fd055e9..9a05f9c5 100644
--- a/lnbits/extensions/invoices/crud.py
+++ b/lnbits/extensions/invoices/crud.py
@@ -6,7 +6,6 @@ from . import db
from .models import (
CreateInvoiceData,
CreateInvoiceItemData,
- CreatePaymentData,
Invoice,
InvoiceItem,
Payment,
@@ -30,7 +29,7 @@ async def get_invoice_items(invoice_id: str) -> List[InvoiceItem]:
return [InvoiceItem.from_row(row) for row in rows]
-async def get_invoice_item(item_id: str) -> InvoiceItem:
+async def get_invoice_item(item_id: str) -> Optional[InvoiceItem]:
row = await db.fetchone(
"SELECT * FROM invoices.invoice_items WHERE id = ?", (item_id,)
)
@@ -61,7 +60,7 @@ async def get_invoice_payments(invoice_id: str) -> List[Payment]:
return [Payment.from_row(row) for row in rows]
-async def get_invoice_payment(payment_id: str) -> Payment:
+async def get_invoice_payment(payment_id: str) -> Optional[Payment]:
row = await db.fetchone(
"SELECT * FROM invoices.payments WHERE id = ?", (payment_id,)
)
@@ -120,7 +119,9 @@ async def create_invoice_items(
return invoice_items
-async def update_invoice_internal(wallet_id: str, data: UpdateInvoiceData) -> Invoice:
+async def update_invoice_internal(
+ wallet_id: str, data: Union[UpdateInvoiceData, Invoice]
+) -> Invoice:
await db.execute(
"""
UPDATE invoices.invoices
@@ -155,21 +156,21 @@ async def update_invoice_items(
updated_items.append(item.id)
await db.execute(
"""
- UPDATE invoices.invoice_items
+ UPDATE invoices.invoice_items
SET description = ?, amount = ?
WHERE id = ?
""",
(item.description, int(item.amount * 100), item.id),
)
- placeholders = ",".join("?" for i in range(len(updated_items)))
+ placeholders = ",".join("?" for _ in range(len(updated_items)))
if not placeholders:
placeholders = "?"
- updated_items = ("skip",)
+ updated_items = ["skip"]
await db.execute(
f"""
- DELETE FROM invoices.invoice_items
+ DELETE FROM invoices.invoice_items
WHERE invoice_id = ?
AND id NOT IN ({placeholders})
""",
@@ -180,8 +181,11 @@ async def update_invoice_items(
)
for item in data:
- if not item.id:
- await create_invoice_items(invoice_id=invoice_id, data=[item])
+ if not item:
+ await create_invoice_items(
+ invoice_id=invoice_id,
+ data=[CreateInvoiceItemData(description=item.description)],
+ )
invoice_items = await get_invoice_items(invoice_id)
return invoice_items
diff --git a/lnbits/extensions/invoices/models.py b/lnbits/extensions/invoices/models.py
index adf03e46..6f0e63cb 100644
--- a/lnbits/extensions/invoices/models.py
+++ b/lnbits/extensions/invoices/models.py
@@ -2,7 +2,7 @@ from enum import Enum
from sqlite3 import Row
from typing import List, Optional
-from fastapi.param_functions import Query
+from fastapi import Query
from pydantic import BaseModel
diff --git a/lnbits/extensions/invoices/tasks.py b/lnbits/extensions/invoices/tasks.py
index 61bcb7b4..c8a829db 100644
--- a/lnbits/extensions/invoices/tasks.py
+++ b/lnbits/extensions/invoices/tasks.py
@@ -1,9 +1,7 @@
import asyncio
-import json
from lnbits.core.models import Payment
-from lnbits.helpers import urlsafe_short_hash
-from lnbits.tasks import internal_invoice_queue, register_invoice_listener
+from lnbits.tasks import register_invoice_listener
from .crud import (
create_invoice_payment,
@@ -14,6 +12,7 @@ from .crud import (
get_payments_total,
update_invoice_internal,
)
+from .models import InvoiceStatusEnum
async def wait_for_paid_invoices():
@@ -27,16 +26,18 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") != "invoices":
- # not relevant
return
invoice_id = payment.extra.get("invoice_id")
+ assert invoice_id
- payment = await create_invoice_payment(
- invoice_id=invoice_id, amount=payment.extra.get("famount")
- )
+ amount = payment.extra.get("famount")
+ assert amount
+
+ await create_invoice_payment(invoice_id=invoice_id, amount=amount)
invoice = await get_invoice(invoice_id)
+ assert invoice
invoice_items = await get_invoice_items(invoice_id)
invoice_total = await get_invoice_total(invoice_items)
@@ -45,7 +46,7 @@ async def on_invoice_paid(payment: Payment) -> None:
payments_total = await get_payments_total(invoice_payments)
if payments_total >= invoice_total:
- invoice.status = "paid"
+ invoice.status = InvoiceStatusEnum.paid
await update_invoice_internal(invoice.wallet, invoice)
return
diff --git a/lnbits/extensions/invoices/templates/invoices/pay.html b/lnbits/extensions/invoices/templates/invoices/pay.html
index 636dbc90..82f1765e 100644
--- a/lnbits/extensions/invoices/templates/invoices/pay.html
+++ b/lnbits/extensions/invoices/templates/invoices/pay.html
@@ -257,7 +257,7 @@ block page %}
>
diff --git a/lnbits/extensions/invoices/views.py b/lnbits/extensions/invoices/views.py
index b492a67c..cc35b351 100644
--- a/lnbits/extensions/invoices/views.py
+++ b/lnbits/extensions/invoices/views.py
@@ -1,10 +1,8 @@
from datetime import datetime
from http import HTTPStatus
-from fastapi import FastAPI, 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
from lnbits.core.models import User
diff --git a/lnbits/extensions/invoices/views_api.py b/lnbits/extensions/invoices/views_api.py
index 23a262e3..1a7762a8 100644
--- a/lnbits/extensions/invoices/views_api.py
+++ b/lnbits/extensions/invoices/views_api.py
@@ -1,14 +1,12 @@
from http import HTTPStatus
-from fastapi import Query
-from fastapi.params import Depends
+from fastapi import Depends, HTTPException, Query
from loguru import logger
-from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
-from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
+from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from . import invoices_ext
@@ -33,7 +31,8 @@ async def api_invoices(
):
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 [invoice.dict() for invoice in await get_invoices(wallet_ids)]
@@ -83,9 +82,7 @@ async def api_invoice_update(
@invoices_ext.post(
"/api/v1/invoice/{invoice_id}/payments", status_code=HTTPStatus.CREATED
)
-async def api_invoices_create_payment(
- famount: int = Query(..., ge=1), invoice_id: str = None
-):
+async def api_invoices_create_payment(invoice_id: str, famount: int = Query(..., ge=1)):
invoice = await get_invoice(invoice_id)
invoice_items = await get_invoice_items(invoice_id)
invoice_total = await get_invoice_total(invoice_items)
diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py
index 8a68fd27..37489edb 100644
--- a/lnbits/extensions/jukebox/tasks.py
+++ b/lnbits/extensions/jukebox/tasks.py
@@ -17,8 +17,8 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra:
- if payment.extra.get("tag") != "jukebox":
- # not a jukebox invoice
- return
- await update_jukebox_payment(payment.payment_hash, paid=True)
+ if payment.extra.get("tag") != "jukebox":
+ # not a jukebox invoice
+ return
+
+ await update_jukebox_payment(payment.payment_hash, paid=True)
diff --git a/lnbits/extensions/lnaddress/cloudflare.py b/lnbits/extensions/lnaddress/cloudflare.py
index 981a37b0..679cb515 100644
--- a/lnbits/extensions/lnaddress/cloudflare.py
+++ b/lnbits/extensions/lnaddress/cloudflare.py
@@ -16,7 +16,7 @@ async def cloudflare_create_record(domain: Domains, ip: str):
"Content-Type": "application/json",
}
- cf_response = ""
+ cf_response = {}
async with httpx.AsyncClient() as client:
try:
r = await client.post(
@@ -31,9 +31,9 @@ async def cloudflare_create_record(domain: Domains, ip: str):
},
timeout=40,
)
- cf_response = json.loads(r.text)
+ cf_response = r.json()
except AssertionError:
- cf_response = "Error occured"
+ cf_response = {"error": "Error occured"}
return cf_response
@@ -53,3 +53,4 @@ async def cloudflare_deleterecord(domain: Domains, domain_id: str):
cf_response = r.text
except AssertionError:
cf_response = "Error occured"
+ return cf_response
diff --git a/lnbits/extensions/lnaddress/crud.py b/lnbits/extensions/lnaddress/crud.py
index 25338215..0e590ec8 100644
--- a/lnbits/extensions/lnaddress/crud.py
+++ b/lnbits/extensions/lnaddress/crud.py
@@ -128,6 +128,7 @@ async def get_addresses(wallet_ids: Union[str, List[str]]) -> List[Addresses]:
async def set_address_paid(payment_hash: str) -> Addresses:
address = await get_address(payment_hash)
+ assert address
if address.paid == False:
await db.execute(
@@ -146,6 +147,7 @@ async def set_address_paid(payment_hash: str) -> Addresses:
async def set_address_renewed(address_id: str, duration: int):
address = await get_address(address_id)
+ assert address
extend_duration = int(address.duration) + duration
await db.execute(
diff --git a/lnbits/extensions/lnaddress/lnurl.py b/lnbits/extensions/lnaddress/lnurl.py
index 6f799439..c4c3cea5 100644
--- a/lnbits/extensions/lnaddress/lnurl.py
+++ b/lnbits/extensions/lnaddress/lnurl.py
@@ -1,17 +1,9 @@
-import hashlib
-import json
from datetime import datetime, timedelta
import httpx
-from fastapi.params import Query
-from lnurl import ( # type: ignore
- LnurlErrorResponse,
- LnurlPayActionResponse,
- LnurlPayResponse,
-)
+from fastapi import Query, Request
+from lnurl import LnurlErrorResponse
from loguru import logger
-from starlette.requests import Request
-from starlette.responses import HTMLResponse
from . import lnaddress_ext
from .crud import get_address, get_address_by_username, get_domain
@@ -52,6 +44,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
amount_received = amount
domain = await get_domain(address.domain)
+ assert domain
base_url = (
address.wallet_endpoint[:-1]
@@ -79,7 +72,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
)
r = call.json()
- except AssertionError as e:
+ except Exception:
return LnurlErrorResponse(reason="ERROR")
# resp = LnurlPayActionResponse(pr=r["payment_request"], routes=[])
diff --git a/lnbits/extensions/lnaddress/models.py b/lnbits/extensions/lnaddress/models.py
index 248f856c..77eb3cd3 100644
--- a/lnbits/extensions/lnaddress/models.py
+++ b/lnbits/extensions/lnaddress/models.py
@@ -1,9 +1,9 @@
import json
from typing import Optional
-from fastapi.params import Query
+from fastapi import Query
from lnurl.types import LnurlPayMetadata
-from pydantic.main import BaseModel
+from pydantic import BaseModel
class CreateDomain(BaseModel):
diff --git a/lnbits/extensions/lnaddress/tasks.py b/lnbits/extensions/lnaddress/tasks.py
index 0c377eec..3699c463 100644
--- a/lnbits/extensions/lnaddress/tasks.py
+++ b/lnbits/extensions/lnaddress/tasks.py
@@ -1,6 +1,7 @@
import asyncio
import httpx
+from loguru import logger
from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
@@ -21,7 +22,9 @@ async def wait_for_paid_invoices():
async def call_webhook_on_paid(payment_hash):
### Use webhook to notify about cloudflare registration
address = await get_address(payment_hash)
+ assert address
domain = await get_domain(address.domain)
+ assert domain
if not domain.webhook:
return
@@ -39,24 +42,23 @@ async def call_webhook_on_paid(payment_hash):
},
timeout=40,
)
- except AssertionError:
- webhook = None
+ r.raise_for_status()
+ except Exception as e:
+ logger.error(f"lnaddress: error calling webhook on paid: {str(e)}")
async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra.get("tag") == "lnaddress":
+ if payment.extra.get("tag") == "lnaddress":
await payment.set_pending(False)
await set_address_paid(payment_hash=payment.payment_hash)
await call_webhook_on_paid(payment_hash=payment.payment_hash)
elif payment.extra.get("tag") == "renew lnaddress":
-
await payment.set_pending(False)
await set_address_renewed(
address_id=payment.extra["id"], duration=payment.extra["duration"]
)
await call_webhook_on_paid(payment_hash=payment.payment_hash)
-
else:
return
diff --git a/lnbits/extensions/lnaddress/views.py b/lnbits/extensions/lnaddress/views.py
index 8c838f0c..d1a7be83 100644
--- a/lnbits/extensions/lnaddress/views.py
+++ b/lnbits/extensions/lnaddress/views.py
@@ -1,10 +1,8 @@
from http import HTTPStatus
from urllib.parse import urlparse
-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
from lnbits.core.crud import get_wallet
@@ -35,6 +33,7 @@ async def display(domain_id, request: Request):
await purge_addresses(domain_id)
wallet = await get_wallet(domain.wallet)
+ assert wallet
url = urlparse(str(request.url))
return lnaddress_renderer().TemplateResponse(
diff --git a/lnbits/extensions/lnaddress/views_api.py b/lnbits/extensions/lnaddress/views_api.py
index 46ef6b99..d9e50e9d 100644
--- a/lnbits/extensions/lnaddress/views_api.py
+++ b/lnbits/extensions/lnaddress/views_api.py
@@ -1,9 +1,7 @@
from http import HTTPStatus
from urllib.parse import urlparse
-from fastapi import Request
-from fastapi.params import Depends, Query
-from starlette.exceptions import HTTPException
+from fastapi import Depends, HTTPException, Query, Request
from lnbits.core.crud import get_user
from lnbits.core.services import check_transaction_status, create_invoice
@@ -11,7 +9,7 @@ from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.lnaddress.models import CreateAddress, CreateDomain
from . import lnaddress_ext
-from .cloudflare import cloudflare_create_record, cloudflare_deleterecord
+from .cloudflare import cloudflare_create_record
from .crud import (
check_address_available,
create_address,
@@ -35,7 +33,8 @@ async def api_domains(
wallet_ids = [g.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
return [domain.dict() for domain in await get_domains(wallet_ids)]
@@ -69,7 +68,7 @@ async def api_domain_create(
cf_response = await cloudflare_create_record(domain=domain, ip=root_url)
- if not cf_response or cf_response["success"] != True:
+ if not cf_response or not cf_response["success"]:
await delete_domain(domain.id)
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
@@ -106,7 +105,8 @@ async def api_addresses(
wallet_ids = [g.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
return [address.dict() for address in await get_addresses(wallet_ids)]
@@ -227,7 +227,9 @@ async def api_lnaddress_make_address(
@lnaddress_ext.get("/api/v1/addresses/{payment_hash}")
async def api_address_send_address(payment_hash):
address = await get_address(payment_hash)
+ assert address
domain = await get_domain(address.domain)
+ assert domain
try:
status = await check_transaction_status(domain.wallet, payment_hash)
is_paid = not status.pending
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/lnticket/tasks.py b/lnbits/extensions/lnticket/tasks.py
index e84a7512..746ebea9 100644
--- a/lnbits/extensions/lnticket/tasks.py
+++ b/lnbits/extensions/lnticket/tasks.py
@@ -19,7 +19,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if not payment.extra or payment.extra.get("tag") != "lnticket":
+ if payment.extra.get("tag") != "lnticket":
# not a lnticket invoice
return
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/crud.py b/lnbits/extensions/lnurlp/crud.py
index d02ae80e..d5963b95 100644
--- a/lnbits/extensions/lnurlp/crud.py
+++ b/lnbits/extensions/lnurlp/crud.py
@@ -1,19 +1,18 @@
from typing import List, Optional, Union
-from lnbits.db import SQLITE
+from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import CreatePayLinkData, PayLink
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
+ link_id = urlsafe_short_hash()[:6]
- returning = "" if db.type == SQLITE else "RETURNING ID"
- method = db.execute if db.type == SQLITE else db.fetchone
-
- result = await (method)(
+ result = await db.execute(
f"""
INSERT INTO lnurlp.pay_links (
+ id,
wallet,
description,
min,
@@ -29,10 +28,10 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
currency,
fiat_base_multiplier
)
- VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
- {returning}
+ VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
+ link_id,
wallet_id,
data.description,
data.min,
@@ -47,17 +46,13 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
data.fiat_base_multiplier,
),
)
- if db.type == SQLITE:
- link_id = result._result_proxy.lastrowid
- else:
- link_id = result[0]
link = await get_pay_link(link_id)
assert link, "Newly created link couldn't be retrieved"
return link
-async def get_pay_link(link_id: int) -> Optional[PayLink]:
+async def get_pay_link(link_id: str) -> Optional[PayLink]:
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None
diff --git a/lnbits/extensions/lnurlp/migrations.py b/lnbits/extensions/lnurlp/migrations.py
index c4edd3aa..44df5ba9 100644
--- a/lnbits/extensions/lnurlp/migrations.py
+++ b/lnbits/extensions/lnurlp/migrations.py
@@ -68,3 +68,76 @@ async def m005_webhook_headers_and_body(db):
"""
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
+
+
+async def m006_redux(db):
+ """
+ Add UUID ID's to links and migrates existing data
+ """
+ await db.execute("ALTER TABLE lnurlp.pay_links RENAME TO pay_links_old")
+ await db.execute(
+ f"""
+ CREATE TABLE lnurlp.pay_links (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ description TEXT NOT NULL,
+ min INTEGER NOT NULL,
+ max INTEGER,
+ currency TEXT,
+ fiat_base_multiplier INTEGER DEFAULT 1,
+ served_meta INTEGER NOT NULL,
+ served_pr INTEGER NOT NULL,
+ webhook_url TEXT,
+ success_text TEXT,
+ success_url TEXT,
+ comment_chars INTEGER DEFAULT 0,
+ webhook_headers TEXT,
+ webhook_body TEXT
+ );
+ """
+ )
+
+ for row in [
+ list(row) for row in await db.fetchall("SELECT * FROM lnurlp.pay_links_old")
+ ]:
+ await db.execute(
+ """
+ INSERT INTO lnurlp.pay_links (
+ id,
+ wallet,
+ description,
+ min,
+ served_meta,
+ served_pr,
+ webhook_url,
+ success_text,
+ success_url,
+ currency,
+ comment_chars,
+ max,
+ fiat_base_multiplier,
+ webhook_headers,
+ webhook_body
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ row[0],
+ row[1],
+ row[2],
+ row[3],
+ row[4],
+ row[5],
+ row[6],
+ row[7],
+ row[8],
+ row[9],
+ row[10],
+ row[11],
+ row[12],
+ row[13],
+ row[14],
+ ),
+ )
+
+ await db.execute("DROP TABLE lnurlp.pay_links_old")
diff --git a/lnbits/extensions/lnurlp/models.py b/lnbits/extensions/lnurlp/models.py
index 2cb4d0ab..42ea2926 100644
--- a/lnbits/extensions/lnurlp/models.py
+++ b/lnbits/extensions/lnurlp/models.py
@@ -26,7 +26,7 @@ class CreatePayLinkData(BaseModel):
class PayLink(BaseModel):
- id: int
+ id: str
wallet: str
description: str
min: float
diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py
index b8da5e43..ea01e04f 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:
+async def on_invoice_paid(payment: Payment):
if payment.extra.get("tag") != "lnurlp":
- # not an lnurlp invoice
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/market/README.md b/lnbits/extensions/market/README.md
new file mode 100644
index 00000000..22d38e0d
--- /dev/null
+++ b/lnbits/extensions/market/README.md
@@ -0,0 +1,9 @@
+Market
+A movable market stand
+Make a list of products to sell, point the list to an relay (or many), stack sats.
+Market is a movable market stand, for anon transactions. You then give permission for an relay to list those products. Delivery addresses are sent through the Lightning Network.
+
+
+API endpoints
+
+curl -X GET http://YOUR-TOR-ADDRESS
diff --git a/lnbits/extensions/market/__init__.py b/lnbits/extensions/market/__init__.py
new file mode 100644
index 00000000..3795ec73
--- /dev/null
+++ b/lnbits/extensions/market/__init__.py
@@ -0,0 +1,43 @@
+import asyncio
+
+from fastapi import APIRouter
+from starlette.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_market")
+
+market_ext: APIRouter = APIRouter(prefix="/market", tags=["market"])
+
+market_static_files = [
+ {
+ "path": "/market/static",
+ "app": StaticFiles(directory="lnbits/extensions/market/static"),
+ "name": "market_static",
+ }
+]
+
+# if 'nostradmin' not in LNBITS_ADMIN_EXTENSIONS:
+# @market_ext.get("/", response_class=HTMLResponse)
+# async def index(request: Request):
+# return template_renderer().TemplateResponse(
+# "error.html", {"request": request, "err": "Ask system admin to enable NostrAdmin!"}
+# )
+# else:
+
+
+def market_renderer():
+ return template_renderer(["lnbits/extensions/market/templates"])
+ # return template_renderer(["lnbits/extensions/market/templates"])
+
+
+from .tasks import wait_for_paid_invoices
+from .views import * # noqa
+from .views_api import * # noqa
+
+
+def market_start():
+ loop = asyncio.get_event_loop()
+ loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/market/config.json b/lnbits/extensions/market/config.json
new file mode 100644
index 00000000..8a294867
--- /dev/null
+++ b/lnbits/extensions/market/config.json
@@ -0,0 +1,6 @@
+{
+ "name": "Marketplace",
+ "short_description": "Webshop/market on LNbits",
+ "tile": "/market/static/images/bitcoin-shop.png",
+ "contributors": ["benarc", "talvasconcelos"]
+}
diff --git a/lnbits/extensions/market/crud.py b/lnbits/extensions/market/crud.py
new file mode 100644
index 00000000..1d9c28be
--- /dev/null
+++ b/lnbits/extensions/market/crud.py
@@ -0,0 +1,492 @@
+from base64 import urlsafe_b64encode
+from typing import List, Optional, Union
+from uuid import uuid4
+
+# from lnbits.db import open_ext_db
+from lnbits.db import SQLITE
+from lnbits.helpers import urlsafe_short_hash
+from lnbits.settings import WALLET
+
+from . import db
+from .models import (
+ ChatMessage,
+ CreateChatMessage,
+ CreateMarket,
+ CreateMarketStalls,
+ Market,
+ MarketSettings,
+ OrderDetail,
+ Orders,
+ Products,
+ Stalls,
+ Zones,
+ createOrder,
+ createOrderDetails,
+ createProduct,
+ createStalls,
+ createZones,
+)
+
+###Products
+
+
+async def create_market_product(data: createProduct) -> Products:
+ product_id = urlsafe_short_hash()
+ await db.execute(
+ f"""
+ INSERT INTO market.products (id, stall, product, categories, description, image, price, quantity)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ product_id,
+ data.stall,
+ data.product,
+ data.categories,
+ data.description,
+ data.image,
+ data.price,
+ data.quantity,
+ ),
+ )
+ product = await get_market_product(product_id)
+ assert product, "Newly created product couldn't be retrieved"
+ return product
+
+
+async def update_market_product(product_id: str, **kwargs) -> Optional[Products]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+
+ await db.execute(
+ f"UPDATE market.products SET {q} WHERE id = ?",
+ (*kwargs.values(), product_id),
+ )
+ row = await db.fetchone("SELECT * FROM market.products WHERE id = ?", (product_id,))
+
+ return Products(**row) if row else None
+
+
+async def get_market_product(product_id: str) -> Optional[Products]:
+ row = await db.fetchone("SELECT * FROM market.products WHERE id = ?", (product_id,))
+ return Products(**row) if row else None
+
+
+async def get_market_products(stall_ids: Union[str, List[str]]) -> List[Products]:
+ if isinstance(stall_ids, str):
+ stall_ids = [stall_ids]
+
+ # with open_ext_db("market") as db:
+ q = ",".join(["?"] * len(stall_ids))
+ rows = await db.fetchall(
+ f"""
+ SELECT * FROM market.products WHERE stall IN ({q})
+ """,
+ (*stall_ids,),
+ )
+ return [Products(**row) for row in rows]
+
+
+async def delete_market_product(product_id: str) -> None:
+ await db.execute("DELETE FROM market.products WHERE id = ?", (product_id,))
+
+
+###zones
+
+
+async def create_market_zone(user, data: createZones) -> Zones:
+ zone_id = urlsafe_short_hash()
+ await db.execute(
+ f"""
+ INSERT INTO market.zones (
+ id,
+ "user",
+ cost,
+ countries
+
+ )
+ VALUES (?, ?, ?, ?)
+ """,
+ (zone_id, user, data.cost, data.countries.lower()),
+ )
+
+ zone = await get_market_zone(zone_id)
+ assert zone, "Newly created zone couldn't be retrieved"
+ return zone
+
+
+async def update_market_zone(zone_id: str, **kwargs) -> Optional[Zones]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+ await db.execute(
+ f"UPDATE market.zones SET {q} WHERE id = ?",
+ (*kwargs.values(), zone_id),
+ )
+ row = await db.fetchone("SELECT * FROM market.zones WHERE id = ?", (zone_id,))
+ return Zones(**row) if row else None
+
+
+async def get_market_zone(zone_id: str) -> Optional[Zones]:
+ row = await db.fetchone("SELECT * FROM market.zones WHERE id = ?", (zone_id,))
+ return Zones(**row) if row else None
+
+
+async def get_market_zones(user: str) -> List[Zones]:
+ rows = await db.fetchall('SELECT * FROM market.zones WHERE "user" = ?', (user,))
+ return [Zones(**row) for row in rows]
+
+
+async def delete_market_zone(zone_id: str) -> None:
+ await db.execute("DELETE FROM market.zones WHERE id = ?", (zone_id,))
+
+
+###Stalls
+
+
+async def create_market_stall(data: createStalls) -> Stalls:
+ stall_id = urlsafe_short_hash()
+ await db.execute(
+ f"""
+ INSERT INTO market.stalls (
+ id,
+ wallet,
+ name,
+ currency,
+ publickey,
+ relays,
+ shippingzones
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ stall_id,
+ data.wallet,
+ data.name,
+ data.currency,
+ data.publickey,
+ data.relays,
+ data.shippingzones,
+ ),
+ )
+
+ stall = await get_market_stall(stall_id)
+ assert stall, "Newly created stall couldn't be retrieved"
+ return stall
+
+
+async def update_market_stall(stall_id: str, **kwargs) -> Optional[Stalls]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+ await db.execute(
+ f"UPDATE market.stalls SET {q} WHERE id = ?",
+ (*kwargs.values(), stall_id),
+ )
+ row = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
+ return Stalls(**row) if row else None
+
+
+async def get_market_stall(stall_id: str) -> Optional[Stalls]:
+ row = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
+ return Stalls(**row) if row else None
+
+
+async def get_market_stalls(wallet_ids: Union[str, List[str]]) -> List[Stalls]:
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM market.stalls WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+ return [Stalls(**row) for row in rows]
+
+
+async def get_market_stalls_by_ids(stall_ids: Union[str, List[str]]) -> List[Stalls]:
+ q = ",".join(["?"] * len(stall_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM market.stalls WHERE id IN ({q})", (*stall_ids,)
+ )
+ return [Stalls(**row) for row in rows]
+
+
+async def delete_market_stall(stall_id: str) -> None:
+ await db.execute("DELETE FROM market.stalls WHERE id = ?", (stall_id,))
+
+
+###Orders
+
+
+async def create_market_order(data: createOrder, invoiceid: str):
+ returning = "" if db.type == SQLITE else "RETURNING ID"
+ method = db.execute if db.type == SQLITE else db.fetchone
+
+ result = await (method)(
+ f"""
+ INSERT INTO market.orders (wallet, shippingzone, address, email, total, invoiceid, paid, shipped)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ {returning}
+ """,
+ (
+ data.wallet,
+ data.shippingzone,
+ data.address,
+ data.email,
+ data.total,
+ invoiceid,
+ False,
+ False,
+ ),
+ )
+ if db.type == SQLITE:
+ return result._result_proxy.lastrowid
+ else:
+ return result[0]
+
+
+async def create_market_order_details(order_id: str, data: List[createOrderDetails]):
+ for item in data:
+ item_id = urlsafe_short_hash()
+ await db.execute(
+ """
+ INSERT INTO market.order_details (id, order_id, product_id, quantity)
+ VALUES (?, ?, ?, ?)
+ """,
+ (
+ item_id,
+ order_id,
+ item.product_id,
+ item.quantity,
+ ),
+ )
+ order_details = await get_market_order_details(order_id)
+ return order_details
+
+
+async def get_market_order_details(order_id: str) -> List[OrderDetail]:
+ rows = await db.fetchall(
+ f"SELECT * FROM market.order_details WHERE order_id = ?", (order_id,)
+ )
+
+ return [OrderDetail(**row) for row in rows]
+
+
+async def get_market_order(order_id: str) -> Optional[Orders]:
+ row = await db.fetchone("SELECT * FROM market.orders WHERE id = ?", (order_id,))
+ return Orders(**row) if row else None
+
+
+async def get_market_order_invoiceid(invoice_id: str) -> Optional[Orders]:
+ row = await db.fetchone(
+ "SELECT * FROM market.orders WHERE invoiceid = ?", (invoice_id,)
+ )
+ return Orders(**row) if row else None
+
+
+async def set_market_order_paid(payment_hash: str):
+ await db.execute(
+ """
+ UPDATE market.orders
+ SET paid = true
+ WHERE invoiceid = ?
+ """,
+ (payment_hash,),
+ )
+
+
+async def set_market_order_pubkey(payment_hash: str, pubkey: str):
+ await db.execute(
+ """
+ UPDATE market.orders
+ SET pubkey = ?
+ WHERE invoiceid = ?
+ """,
+ (
+ pubkey,
+ payment_hash,
+ ),
+ )
+
+
+async def update_market_product_stock(products):
+
+ q = "\n".join(
+ [f"""WHEN id='{p.product_id}' THEN quantity - {p.quantity}""" for p in products]
+ )
+ v = ",".join(["?"] * len(products))
+
+ await db.execute(
+ f"""
+ UPDATE market.products
+ SET quantity=(CASE
+ {q}
+ END)
+ WHERE id IN ({v});
+ """,
+ (*[p.product_id for p in products],),
+ )
+
+
+async def get_market_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
+ if isinstance(wallet_ids, str):
+ wallet_ids = [wallet_ids]
+
+ q = ",".join(["?"] * len(wallet_ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM market.orders WHERE wallet IN ({q})", (*wallet_ids,)
+ )
+ #
+ return [Orders(**row) for row in rows]
+
+
+async def delete_market_order(order_id: str) -> None:
+ await db.execute("DELETE FROM market.orders WHERE id = ?", (order_id,))
+
+
+### Market/Marketplace
+
+
+async def get_market_markets(user: str) -> List[Market]:
+ rows = await db.fetchall("SELECT * FROM market.markets WHERE usr = ?", (user,))
+ return [Market(**row) for row in rows]
+
+
+async def get_market_market(market_id: str) -> Optional[Market]:
+ row = await db.fetchone("SELECT * FROM market.markets WHERE id = ?", (market_id,))
+ return Market(**row) if row else None
+
+
+async def get_market_market_stalls(market_id: str):
+ rows = await db.fetchall(
+ "SELECT * FROM market.market_stalls WHERE marketid = ?", (market_id,)
+ )
+
+ ids = [row["stallid"] for row in rows]
+
+ return await get_market_stalls_by_ids(ids)
+
+
+async def create_market_market(data: CreateMarket):
+ market_id = urlsafe_short_hash()
+
+ await db.execute(
+ """
+ INSERT INTO market.markets (id, usr, name)
+ VALUES (?, ?, ?)
+ """,
+ (
+ market_id,
+ data.usr,
+ data.name,
+ ),
+ )
+ market = await get_market_market(market_id)
+ assert market, "Newly created market couldn't be retrieved"
+ return market
+
+
+async def create_market_market_stalls(market_id: str, data: List[str]):
+ for stallid in data:
+ id = urlsafe_short_hash()
+
+ await db.execute(
+ """
+ INSERT INTO market.market_stalls (id, marketid, stallid)
+ VALUES (?, ?, ?)
+ """,
+ (
+ id,
+ market_id,
+ stallid,
+ ),
+ )
+ market_stalls = await get_market_market_stalls(market_id)
+ return market_stalls
+
+
+async def update_market_market(market_id: str, name: str):
+ await db.execute(
+ "UPDATE market.markets SET name = ? WHERE id = ?",
+ (name, market_id),
+ )
+ await db.execute(
+ "DELETE FROM market.market_stalls WHERE marketid = ?",
+ (market_id,),
+ )
+
+ market = await get_market_market(market_id)
+ return market
+
+
+### CHAT / MESSAGES
+
+
+async def create_chat_message(data: CreateChatMessage):
+ await db.execute(
+ """
+ INSERT INTO market.messages (msg, pubkey, id_conversation)
+ VALUES (?, ?, ?)
+ """,
+ (
+ data.msg,
+ data.pubkey,
+ data.room_name,
+ ),
+ )
+
+
+async def get_market_latest_chat_messages(room_name: str):
+ rows = await db.fetchall(
+ "SELECT * FROM market.messages WHERE id_conversation = ? ORDER BY timestamp DESC LIMIT 20",
+ (room_name,),
+ )
+
+ return [ChatMessage(**row) for row in rows]
+
+
+async def get_market_chat_messages(room_name: str):
+ rows = await db.fetchall(
+ "SELECT * FROM market.messages WHERE id_conversation = ? ORDER BY timestamp DESC",
+ (room_name,),
+ )
+
+ return [ChatMessage(**row) for row in rows]
+
+
+async def get_market_chat_by_merchant(ids: List[str]) -> List[ChatMessage]:
+
+ q = ",".join(["?"] * len(ids))
+ rows = await db.fetchall(
+ f"SELECT * FROM market.messages WHERE id_conversation IN ({q})",
+ (*ids,),
+ )
+ return [ChatMessage(**row) for row in rows]
+
+
+async def get_market_settings(user) -> Optional[MarketSettings]:
+ row = await db.fetchone(
+ """SELECT * FROM market.settings WHERE "user" = ?""", (user,)
+ )
+
+ return MarketSettings(**row) if row else None
+
+
+async def create_market_settings(user: str, data):
+ await db.execute(
+ """
+ INSERT INTO market.settings ("user", currency, fiat_base_multiplier)
+ VALUES (?, ?, ?)
+ """,
+ (
+ user,
+ data.currency,
+ data.fiat_base_multiplier,
+ ),
+ )
+
+
+async def set_market_settings(user: str, data):
+ await db.execute(
+ """
+ UPDATE market.settings
+ SET currency = ?, fiat_base_multiplier = ?
+ WHERE "user" = ?;
+ """,
+ (
+ data.currency,
+ data.fiat_base_multiplier,
+ user,
+ ),
+ )
diff --git a/lnbits/extensions/market/migrations.py b/lnbits/extensions/market/migrations.py
new file mode 100644
index 00000000..72b584f9
--- /dev/null
+++ b/lnbits/extensions/market/migrations.py
@@ -0,0 +1,156 @@
+async def m001_initial(db):
+ """
+ Initial Market settings table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE market.settings (
+ "user" TEXT PRIMARY KEY,
+ currency TEXT DEFAULT 'sat',
+ fiat_base_multiplier INTEGER DEFAULT 1
+ );
+ """
+ )
+
+ """
+ Initial stalls table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE market.stalls (
+ id TEXT PRIMARY KEY,
+ wallet TEXT NOT NULL,
+ name TEXT NOT NULL,
+ currency TEXT,
+ publickey TEXT,
+ relays TEXT,
+ shippingzones TEXT NOT NULL,
+ rating INTEGER DEFAULT 0
+ );
+ """
+ )
+
+ """
+ Initial products table.
+ """
+ await db.execute(
+ f"""
+ CREATE TABLE market.products (
+ id TEXT PRIMARY KEY,
+ stall TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE,
+ product TEXT NOT NULL,
+ categories TEXT,
+ description TEXT,
+ image TEXT,
+ price INTEGER NOT NULL,
+ quantity INTEGER NOT NULL,
+ rating INTEGER DEFAULT 0
+ );
+ """
+ )
+
+ """
+ Initial zones table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE market.zones (
+ id TEXT PRIMARY KEY,
+ "user" TEXT NOT NULL,
+ cost TEXT NOT NULL,
+ countries TEXT NOT NULL
+ );
+ """
+ )
+
+ """
+ Initial orders table.
+ """
+ await db.execute(
+ f"""
+ CREATE TABLE market.orders (
+ id {db.serial_primary_key},
+ wallet TEXT NOT NULL,
+ username TEXT,
+ pubkey TEXT,
+ shippingzone TEXT NOT NULL,
+ address TEXT NOT NULL,
+ email TEXT NOT NULL,
+ total INTEGER NOT NULL,
+ invoiceid TEXT NOT NULL,
+ paid BOOLEAN NOT NULL,
+ shipped BOOLEAN NOT NULL,
+ time TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
+
+ """
+ Initial order details table.
+ """
+ await db.execute(
+ f"""
+ CREATE TABLE market.order_details (
+ id TEXT PRIMARY KEY,
+ order_id INTEGER NOT NULL REFERENCES {db.references_schema}orders (id) ON DELETE CASCADE,
+ product_id TEXT NOT NULL REFERENCES {db.references_schema}products (id) ON DELETE CASCADE,
+ quantity INTEGER NOT NULL
+ );
+ """
+ )
+
+ """
+ Initial market table.
+ """
+ await db.execute(
+ """
+ CREATE TABLE market.markets (
+ id TEXT PRIMARY KEY,
+ usr TEXT NOT NULL,
+ name TEXT
+ );
+ """
+ )
+
+ """
+ Initial market stalls table.
+ """
+ await db.execute(
+ f"""
+ CREATE TABLE market.market_stalls (
+ id TEXT PRIMARY KEY,
+ marketid TEXT NOT NULL REFERENCES {db.references_schema}markets (id) ON DELETE CASCADE,
+ stallid TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE
+ );
+ """
+ )
+
+ """
+ Initial chat messages table.
+ """
+ await db.execute(
+ f"""
+ CREATE TABLE market.messages (
+ id {db.serial_primary_key},
+ msg TEXT NOT NULL,
+ pubkey TEXT NOT NULL,
+ id_conversation TEXT NOT NULL,
+ timestamp TIMESTAMP NOT NULL DEFAULT """
+ + db.timestamp_now
+ + """
+ );
+ """
+ )
+
+ if db.type != "SQLITE":
+ """
+ Create indexes for message fetching
+ """
+ await db.execute(
+ "CREATE INDEX idx_messages_timestamp ON market.messages (timestamp DESC)"
+ )
+ await db.execute(
+ "CREATE INDEX idx_messages_conversations ON market.messages (id_conversation)"
+ )
diff --git a/lnbits/extensions/market/models.py b/lnbits/extensions/market/models.py
new file mode 100644
index 00000000..ea7f6f20
--- /dev/null
+++ b/lnbits/extensions/market/models.py
@@ -0,0 +1,135 @@
+from typing import List, Optional
+
+from fastapi.param_functions import Query
+from pydantic import BaseModel
+
+
+class MarketSettings(BaseModel):
+ user: str
+ currency: str
+ fiat_base_multiplier: int
+
+
+class SetSettings(BaseModel):
+ currency: str
+ fiat_base_multiplier: int = Query(100, ge=1)
+
+
+class Stalls(BaseModel):
+ id: str
+ wallet: str
+ name: str
+ currency: str
+ publickey: Optional[str]
+ relays: Optional[str]
+ shippingzones: str
+
+
+class createStalls(BaseModel):
+ wallet: str = Query(...)
+ name: str = Query(...)
+ currency: str = Query("sat")
+ publickey: str = Query(None)
+ relays: str = Query(None)
+ shippingzones: str = Query(...)
+
+
+class createProduct(BaseModel):
+ stall: str = Query(...)
+ product: str = Query(...)
+ categories: str = Query(None)
+ description: str = Query(None)
+ image: str = Query(None)
+ price: float = Query(0, ge=0)
+ quantity: int = Query(0, ge=0)
+
+
+class Products(BaseModel):
+ id: str
+ stall: str
+ product: str
+ categories: Optional[str]
+ description: Optional[str]
+ image: Optional[str]
+ price: float
+ quantity: int
+
+
+class createZones(BaseModel):
+ cost: float = Query(0, ge=0)
+ countries: str = Query(...)
+
+
+class Zones(BaseModel):
+ id: str
+ user: str
+ cost: float
+ countries: str
+
+
+class OrderDetail(BaseModel):
+ id: str
+ order_id: str
+ product_id: str
+ quantity: int
+
+
+class createOrderDetails(BaseModel):
+ product_id: str = Query(...)
+ quantity: int = Query(..., ge=1)
+
+
+class createOrder(BaseModel):
+ wallet: str = Query(...)
+ username: str = Query(None)
+ pubkey: str = Query(None)
+ shippingzone: str = Query(...)
+ address: str = Query(...)
+ email: str = Query(...)
+ total: int = Query(...)
+ products: List[createOrderDetails]
+
+
+class Orders(BaseModel):
+ id: str
+ wallet: str
+ username: Optional[str]
+ pubkey: Optional[str]
+ shippingzone: str
+ address: str
+ email: str
+ total: int
+ invoiceid: str
+ paid: bool
+ shipped: bool
+ time: int
+
+
+class CreateMarket(BaseModel):
+ usr: str = Query(...)
+ name: str = Query(None)
+ stalls: List[str] = Query(...)
+
+
+class Market(BaseModel):
+ id: str
+ usr: str
+ name: Optional[str]
+
+
+class CreateMarketStalls(BaseModel):
+ stallid: str
+
+
+class ChatMessage(BaseModel):
+ id: str
+ msg: str
+ pubkey: str
+ id_conversation: str
+ timestamp: int
+
+
+class CreateChatMessage(BaseModel):
+ msg: str = Query(..., min_length=1)
+ pubkey: str = Query(...)
+ room_name: str = Query(...)
diff --git a/lnbits/extensions/market/notifier.py b/lnbits/extensions/market/notifier.py
new file mode 100644
index 00000000..e2bf7c91
--- /dev/null
+++ b/lnbits/extensions/market/notifier.py
@@ -0,0 +1,91 @@
+## adapted from https://github.com/Sentymental/chat-fastapi-websocket
+"""
+Create a class Notifier that will handle messages
+and delivery to the specific person
+"""
+
+import json
+from collections import defaultdict
+
+from fastapi import WebSocket
+from loguru import logger
+
+from lnbits.extensions.market.crud import create_chat_message
+from lnbits.extensions.market.models import CreateChatMessage
+
+
+class Notifier:
+ """
+ Manages chatrooms, sessions and members.
+
+ Methods:
+ - get_notification_generator(self): async generator with notification messages
+ - get_members(self, room_name: str): get members in room
+ - push(message: str, room_name: str): push message
+ - connect(websocket: WebSocket, room_name: str): connect to room
+ - remove(websocket: WebSocket, room_name: str): remove
+ - _notify(message: str, room_name: str): notifier
+ """
+
+ def __init__(self):
+ # Create sessions as a dict:
+ self.sessions: dict = defaultdict(dict)
+
+ # Create notification generator:
+ self.generator = self.get_notification_generator()
+
+ async def get_notification_generator(self):
+ """Notification Generator"""
+
+ while True:
+ message = yield
+ msg = message["message"]
+ room_name = message["room_name"]
+ await self._notify(msg, room_name)
+
+ def get_members(self, room_name: str):
+ """Get all members in a room"""
+
+ try:
+ logger.info(f"Looking for members in room: {room_name}")
+ return self.sessions[room_name]
+
+ except Exception:
+ logger.exception(f"There is no member in room: {room_name}")
+ return None
+
+ async def push(self, message: str, room_name: str = None):
+ """Push a message"""
+
+ message_body = {"message": message, "room_name": room_name}
+ await self.generator.asend(message_body)
+
+ async def connect(self, websocket: WebSocket, room_name: str):
+ """Connect to room"""
+
+ await websocket.accept()
+ if self.sessions[room_name] == {} or len(self.sessions[room_name]) == 0:
+ self.sessions[room_name] = []
+
+ self.sessions[room_name].append(websocket)
+ print(f"Connections ...: {self.sessions[room_name]}")
+
+ def remove(self, websocket: WebSocket, room_name: str):
+ """Remove websocket from room"""
+
+ self.sessions[room_name].remove(websocket)
+ print(f"Connection removed...\nOpen connections...: {self.sessions[room_name]}")
+
+ async def _notify(self, message: str, room_name: str):
+ """Notifier"""
+ d = json.loads(message)
+ d["room_name"] = room_name
+ db_msg = CreateChatMessage.parse_obj(d)
+ await create_chat_message(data=db_msg)
+
+ remaining_sessions = []
+ while len(self.sessions[room_name]) > 0:
+ websocket = self.sessions[room_name].pop()
+ await websocket.send_text(message)
+ remaining_sessions.append(websocket)
+ self.sessions[room_name] = remaining_sessions
diff --git a/lnbits/extensions/market/static/images/bitcoin-shop.png b/lnbits/extensions/market/static/images/bitcoin-shop.png
new file mode 100644
index 00000000..debffbb2
Binary files /dev/null and b/lnbits/extensions/market/static/images/bitcoin-shop.png differ
diff --git a/lnbits/extensions/market/static/images/placeholder.png b/lnbits/extensions/market/static/images/placeholder.png
new file mode 100644
index 00000000..c7d3a947
Binary files /dev/null and b/lnbits/extensions/market/static/images/placeholder.png differ
diff --git a/lnbits/extensions/market/tasks.py b/lnbits/extensions/market/tasks.py
new file mode 100644
index 00000000..b102e0f1
--- /dev/null
+++ b/lnbits/extensions/market/tasks.py
@@ -0,0 +1,39 @@
+import asyncio
+
+from loguru import logger
+
+from lnbits.core.models import Payment
+from lnbits.tasks import register_invoice_listener
+
+from .crud import (
+ get_market_order_details,
+ get_market_order_invoiceid,
+ set_market_order_paid,
+ update_market_product_stock,
+)
+
+
+async def wait_for_paid_invoices():
+ invoice_queue = asyncio.Queue()
+ register_invoice_listener(invoice_queue)
+
+ while True:
+ payment = await invoice_queue.get()
+ await on_invoice_paid(payment)
+
+
+async def on_invoice_paid(payment: Payment) -> None:
+ if payment.extra.get("tag") != "market":
+ return
+
+ order = await get_market_order_invoiceid(payment.payment_hash)
+ if not order:
+ logger.error("this should never happen", payment)
+ return
+
+ # set order as paid
+ await set_market_order_paid(payment.payment_hash)
+
+ # deduct items sold from stock
+ details = await get_market_order_details(order.id)
+ await update_market_product_stock(details)
diff --git a/lnbits/extensions/market/templates/market/_api_docs.html b/lnbits/extensions/market/templates/market/_api_docs.html
new file mode 100644
index 00000000..f0d97dbf
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/_api_docs.html
@@ -0,0 +1,128 @@
+
+
+
+
+ LNbits Market (Nostr support coming soon)
+
+
+
+ - Create Shipping Zones you're willing to ship to
+ - Create a Stall to list yiur products on
+ - Create products to put on the Stall
+ - Take orders
+ - Includes chat support!
+
+ The first LNbits market idea 'Diagon Alley' helped create Nostr, and soon
+ this market extension will have the option to work on Nostr 'Diagon Alley'
+ mode, by the merchant, market, and buyer all having keys, and data being
+ routed through Nostr relays.
+
+
+ Created by,
+ Tal Vasconcelos,
+ Ben Arc
+
+
+
+
+
+
+
+
+
+ GET
+ /market/api/v1/stall/products/<relay_id>
+ Body (application/json)
+
+ Returns 201 CREATED (application/json)
+
+ Product JSON list
+ Curl example
+ curl -X GET {{ request.url_root
+ }}api/v1/stall/products/<relay_id>
+
+
+
+
+
+
+ POST
+ /market/api/v1/stall/order/<relay_id>
+ Body (application/json)
+ {"id": <string>, "address": <string>, "shippingzone":
+ <integer>, "email": <string>, "quantity":
+ <integer>}
+
+ Returns 201 CREATED (application/json)
+
+ {"checking_id": <string>,"payment_request":
+ <string>}
+ Curl example
+ curl -X POST {{ request.url_root
+ }}api/v1/stall/order/<relay_id> -d '{"id": <product_id&>,
+ "email": <customer_email>, "address": <customer_address>,
+ "quantity": 2, "shippingzone": 1}' -H "Content-type: application/json"
+
+
+
+
+
+
+
+ GET
+ /market/api/v1/stall/checkshipped/<checking_id>
+ Headers
+
+ Returns 200 OK (application/json)
+
+ {"shipped": <boolean>}
+ Curl example
+ curl -X GET {{ request.url_root
+ }}api/v1/stall/checkshipped/<checking_id> -H "Content-type:
+ application/json"
+
+
+
+
diff --git a/lnbits/extensions/market/templates/market/_chat_box.html b/lnbits/extensions/market/templates/market/_chat_box.html
new file mode 100644
index 00000000..05b0c58f
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/_chat_box.html
@@ -0,0 +1,58 @@
+
+
+ Messages
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lnbits/extensions/market/templates/market/_dialogs.html b/lnbits/extensions/market/templates/market/_dialogs.html
new file mode 100644
index 00000000..d2a8dd0a
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/_dialogs.html
@@ -0,0 +1,393 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update Product
+
+ Create Product
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+ Update Shipping Zone
+ Create Shipping Zone
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+ Update Marketplace
+ Launch Marketplace
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Generate keys
+
+
+ Restore keys
+
+
+
+
+
+
+
+
+ Update Stall
+ Create Stall
+ Cancel
+
+
+
+
+
+
+
+ How to use Market
+
+
+ Create Shipping Zones you're willing to ship to. You can define
+ different values for different zones.
+
+
+
+
+
+ Create a Stall and provide private and public keys to use for
+ communication. If you don't have one, LNbits will create a key pair for
+ you. It will be saved and can be used on other stalls.
+
+
+
+
+
+
+ Create your products, add a small description and an image. Choose to
+ what stall, if you have more than one, it belongs to
+
+
+
+
+
+
+
+
+
+
diff --git a/lnbits/extensions/market/templates/market/_tables.html b/lnbits/extensions/market/templates/market/_tables.html
new file mode 100644
index 00000000..c6fd665b
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/_tables.html
@@ -0,0 +1,440 @@
+
+
+
+
+
+
Orders
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+ Product shipped?
+
+
+
+
+
+
+
+
+
+
+
+ Order Details
+
+
+
+ Products
+ {{ products.length && (_.findWhere(products, {id:
+ col.product_id})).product }}
+ Quantity: {{ col.quantity }}
+
+
+
+
+
+ Shipping to
+ {{ props.row.address }}
+
+
+
+
+
+ User info
+ {{ props.row.username }}
+ {{ props.row.email }}
+ {{ props.row.pubkey }}
+
+
+
+
+ Total
+ {{ props.row.total }}
+
+
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+
+ Products
+
+
+ Add a product
+
+
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+ Link to pass to stall relay
+
+
+ {{ col.value }}
+
+
+
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+
Market Stalls
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+ Stall simple UI marketping cart
+
+
+ {{ col.value }}
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+
Marketplaces
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+ Link to pass to stall relay
+
+
+ {{ col.name == 'stalls' ? stallName(col.value) : col.value }}
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+
Shipping Zones
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
diff --git a/lnbits/extensions/market/templates/market/index.html b/lnbits/extensions/market/templates/market/index.html
new file mode 100644
index 00000000..ffcb612b
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/index.html
@@ -0,0 +1,1419 @@
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
+%} {% block page %}
+
+ {% include "market/_dialogs.html" %}
+
+
+
+ + Shipping Zone Create a shipping zone
+ + Stall
+
+ Create a market stall to list products on
+
+ + Stall
+
+ Create a market stall to list products on
+
+ + Product List a product
+ + Product List a product
+ setCurrency(value)"
+ >
+ Create Market
+
+ Makes a simple frontend market for your stalls (not
+ NOSTR)
+
+
+
+ Market
+ Make a market of multiple stalls.
+
+
+
+
+ Coming soon...
+ Export all Data
+
+ Export all data (markets, products, orders, etc...)
+
+
+
+ {% include "market/_tables.html" %}
+
+
+
+
+
+
Keys
+
+
+ Export to CSV
+
+
+
+
+
+
+
+ {% raw %}
+
+
+ {{ keys[type] }}
+
+
+ {{ type == 'pubkey' ? 'Public Key' : 'Private Key' }}
Click to copy
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+
+
+ LNbits Market Extension (Nostr support coming soon)
+
+
+
+
+ {% include "market/_api_docs.html" %}
+
+
+
+ {% include "market/_chat_box.html" %}
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+
+
+
+
+
+{% endblock %}
diff --git a/lnbits/extensions/market/templates/market/market.html b/lnbits/extensions/market/templates/market/market.html
new file mode 100644
index 00000000..e59bb245
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/market.html
@@ -0,0 +1,175 @@
+{% extends "public.html" %} {% block page %}
+
+
+
+
+ Market: {{ market.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% raw %}
+
+
+
+
+
+ {{ item.product }}
+
+
+
+
+
+
+
+
+
+ {{ item.stallName }}
+
+
+ {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }}
+
+
+ {{ getAmountFormated(item.price, item.currency) }}
+ ({{ getValueInSats(item.price, item.currency) }} sats)
+
+
{{item.quantity}} left
+
+
+ {{cat}}
+
+
+
{{ item.description }}
+
+
+
+
+
+
+ Stall: {{ item.stallName }}
+
+ Visit Stall
+
+
+ {% endraw %}
+
+
+
+{% endblock %} {% block scripts %}
+
+{% endblock %}
diff --git a/lnbits/extensions/market/templates/market/order.html b/lnbits/extensions/market/templates/market/order.html
new file mode 100644
index 00000000..5be606f9
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/order.html
@@ -0,0 +1,564 @@
+{% extends "public.html" %} {% block page %}
+
+
+
+
+
+ {% raw %}
+ {{ stall.name }}
+
+ Public Key: {{ sliceKey(stall.publickey) }}
+ Click to copy
+
+ {% endraw %}
+
+
+
+
+ { changeOrder() }"
+ emit-value
+ >
+
+
+
+
+ {% raw %}
+
+
+ {{p.quantity}} x
+
+
+
+
+
+
+
+ {{ p.name }}
+
+
+
+ {{ getAmountFormated(p.price) }}
+ {{p.price}} sats
+
+
+ {% endraw %}
+
+
+
+
+
+
+ Bellow are the keys needed to contact the merchant. They are
+ stored in the browser!
+
+
+
+
+ {% raw %}
+
+
+ {{ user.keys[type] }}
+
+
+ {{ type == 'publickey' ? 'Public Key' : 'Private Key' }}
+
+ {% endraw %}
+
+
+
+
+
+ Backup keys
+ Download your keys
+
+ Restore keys
+ Restore keys
+
+ Delete data
+ Delete all data from browser
+
+
+
+
+
+ Export, or send, this page to another device
+
+
+
+ Click to copy
+
+
+
+ Copy URL
+ Export, or send, this page to another device
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bookmark this page
+
+ Don't forget to bookmark this page to be able to check on your order!
+
+
+ You can backup your keys, and export the page to another device also.
+
+
+ Close
+
+
+
+
+{% endblock %} {% block scripts %}
+
+
+
+
+{% endblock %}
diff --git a/lnbits/extensions/market/templates/market/product.html b/lnbits/extensions/market/templates/market/product.html
new file mode 100644
index 00000000..66f56691
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/product.html
@@ -0,0 +1,14 @@
+{% extends "public.html" %} {% block page %}
+Product page
+{% endblock %} {% block scripts %}
+
+{% endblock %}
diff --git a/lnbits/extensions/market/templates/market/stall.html b/lnbits/extensions/market/templates/market/stall.html
new file mode 100644
index 00000000..f9189b30
--- /dev/null
+++ b/lnbits/extensions/market/templates/market/stall.html
@@ -0,0 +1,531 @@
+{% extends "public.html" %} {% block page %}
+
+
+
+
+ Stall: {{ stall.name }}
+
+
+
+
+
+
+
+
+
+ {% raw %}
+
+ {{ cart.size }}
+
+ {% endraw %}
+
+
+ {% raw %}
+
+
+ {{p.quantity}} x
+
+
+
+
+
+
+
+
+ {{ p.name }}
+
+
+
+
+ {{unit != 'sat' ? getAmountFormated(p.price) : p.price +
+ 'sats'}}
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% raw %}
+
+
+
+ Add to cart
+
+
+
+ {{ item.product }}
+
+
+
+
+
+
+
+
+
+ {{ item.price }} satsBTC {{ (item.price / 1e8).toFixed(8) }}
+
+
+ {{ getAmountFormated(item.price) }}
+ ({{ getValueInSats(item.price) }} sats)
+
+ {{item.quantity}} left
+
+
+ {{cat}}
+
+
+
{{ item.description }}
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+
+ Click to restore saved public key
+
+
+
+
+
+ Select the shipping zone:
+
+
+
+
+ {% raw %} Total: {{ unit != 'sat' ? getAmountFormated(finalCost) :
+ finalCost + 'sats' }}
+ ({{ getValueInSats(finalCost) }} sats)
+ {% endraw %}
+
+
+ Checkout
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+ Copy invoice
+ Close
+
+
+
+
+{% endblock %} {% block scripts %}
+
+{% endblock %}
diff --git a/lnbits/extensions/market/views.py b/lnbits/extensions/market/views.py
new file mode 100644
index 00000000..23bc5706
--- /dev/null
+++ b/lnbits/extensions/market/views.py
@@ -0,0 +1,177 @@
+import json
+from http import HTTPStatus
+from typing import List
+
+from fastapi import (
+ BackgroundTasks,
+ Depends,
+ Query,
+ Request,
+ WebSocket,
+ WebSocketDisconnect,
+)
+from fastapi.templating import Jinja2Templates
+from loguru import logger
+from starlette.exceptions import HTTPException
+from starlette.responses import HTMLResponse
+
+from lnbits.core.models import User
+from lnbits.decorators import check_user_exists # type: ignore
+from lnbits.extensions.market import market_ext, market_renderer
+from lnbits.extensions.market.models import CreateChatMessage, SetSettings
+from lnbits.extensions.market.notifier import Notifier
+
+from .crud import (
+ create_chat_message,
+ create_market_settings,
+ get_market_market,
+ get_market_market_stalls,
+ get_market_order_details,
+ get_market_order_invoiceid,
+ get_market_products,
+ get_market_settings,
+ get_market_stall,
+ get_market_zone,
+ get_market_zones,
+ update_market_product_stock,
+)
+
+templates = Jinja2Templates(directory="templates")
+
+
+@market_ext.get("/", response_class=HTMLResponse)
+async def index(request: Request, user: User = Depends(check_user_exists)):
+ settings = await get_market_settings(user=user.id)
+
+ if not settings:
+ await create_market_settings(
+ user=user.id, data=SetSettings(currency="sat", fiat_base_multiplier=1)
+ )
+ settings = await get_market_settings(user.id)
+ assert settings
+ return market_renderer().TemplateResponse(
+ "market/index.html",
+ {"request": request, "user": user.dict(), "currency": settings.currency},
+ )
+
+
+@market_ext.get("/stalls/{stall_id}", response_class=HTMLResponse)
+async def stall(request: Request, stall_id):
+ stall = await get_market_stall(stall_id)
+ products = await get_market_products(stall_id)
+
+ if not stall:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Stall does not exist."
+ )
+
+ zones = []
+ for id in stall.shippingzones.split(","):
+ zone = await get_market_zone(id)
+ assert zone
+ z = zone.dict()
+ zones.append({"label": z["countries"], "cost": z["cost"], "value": z["id"]})
+
+ _stall = stall.dict()
+
+ _stall["zones"] = zones
+
+ return market_renderer().TemplateResponse(
+ "market/stall.html",
+ {
+ "request": request,
+ "stall": _stall,
+ "products": [product.dict() for product in products],
+ },
+ )
+
+
+@market_ext.get("/market/{market_id}", response_class=HTMLResponse)
+async def market(request: Request, market_id):
+ market = await get_market_market(market_id)
+
+ if not market:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Marketplace doesn't exist."
+ )
+
+ stalls = await get_market_market_stalls(market_id)
+ stalls_ids = [stall.id for stall in stalls]
+ products = [product.dict() for product in await get_market_products(stalls_ids)]
+
+ return market_renderer().TemplateResponse(
+ "market/market.html",
+ {
+ "request": request,
+ "market": market,
+ "stalls": [stall.dict() for stall in stalls],
+ "products": products,
+ },
+ )
+
+
+@market_ext.get("/order", response_class=HTMLResponse)
+async def order_chat(
+ request: Request,
+ merch: str = Query(...),
+ invoice_id: str = Query(...),
+ keys: str = Query(None),
+):
+ stall = await get_market_stall(merch)
+ assert stall
+ order = await get_market_order_invoiceid(invoice_id)
+ assert order
+ _order = await get_market_order_details(order.id)
+ products = await get_market_products(stall.id)
+ assert products
+
+ return market_renderer().TemplateResponse(
+ "market/order.html",
+ {
+ "request": request,
+ "stall": {
+ "id": stall.id,
+ "name": stall.name,
+ "publickey": stall.publickey,
+ "wallet": stall.wallet,
+ "currency": stall.currency,
+ },
+ "publickey": keys.split(",")[0] if keys else None,
+ "privatekey": keys.split(",")[1] if keys else None,
+ "order_id": order.invoiceid,
+ "order": [details.dict() for details in _order],
+ "products": [product.dict() for product in products],
+ },
+ )
+
+
+##################WEBSOCKET ROUTES########################
+
+# Initialize Notifier:
+notifier = Notifier()
+
+
+@market_ext.websocket("/ws/{room_name}")
+async def websocket_endpoint(
+ websocket: WebSocket, room_name: str, background_tasks: BackgroundTasks
+):
+ await notifier.connect(websocket, room_name)
+ try:
+ while True:
+ data = await websocket.receive_text()
+ d = json.loads(data)
+ d["room_name"] = room_name
+
+ room_members = (
+ notifier.get_members(room_name)
+ if notifier.get_members(room_name) is not None
+ else []
+ )
+
+ if websocket not in room_members:
+ print("Sender not in room member: Reconnecting...")
+ await notifier.connect(websocket, room_name)
+ await notifier._notify(data, room_name)
+
+ except WebSocketDisconnect:
+ notifier.remove(websocket, room_name)
diff --git a/lnbits/extensions/market/views_api.py b/lnbits/extensions/market/views_api.py
new file mode 100644
index 00000000..045bc0fc
--- /dev/null
+++ b/lnbits/extensions/market/views_api.py
@@ -0,0 +1,518 @@
+from base64 import urlsafe_b64encode
+from http import HTTPStatus
+from typing import List, Union
+from uuid import uuid4
+
+from fastapi import Body, Depends, Query, Request
+from loguru import logger
+from starlette.exceptions import HTTPException
+
+from lnbits.core.crud import get_user
+from lnbits.core.services import create_invoice
+from lnbits.core.views.api import api_payment
+from lnbits.decorators import (
+ WalletTypeInfo,
+ get_key_type,
+ require_admin_key,
+ require_invoice_key,
+)
+from lnbits.helpers import urlsafe_short_hash
+from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis
+
+from . import db, market_ext
+from .crud import (
+ create_market_market,
+ create_market_market_stalls,
+ create_market_order,
+ create_market_order_details,
+ create_market_product,
+ create_market_settings,
+ create_market_stall,
+ create_market_zone,
+ delete_market_order,
+ delete_market_product,
+ delete_market_stall,
+ delete_market_zone,
+ get_market_chat_by_merchant,
+ get_market_chat_messages,
+ get_market_latest_chat_messages,
+ get_market_market,
+ get_market_market_stalls,
+ get_market_markets,
+ get_market_order,
+ get_market_order_details,
+ get_market_order_invoiceid,
+ get_market_orders,
+ get_market_product,
+ get_market_products,
+ get_market_settings,
+ get_market_stall,
+ get_market_stalls,
+ get_market_stalls_by_ids,
+ get_market_zone,
+ get_market_zones,
+ set_market_order_pubkey,
+ set_market_settings,
+ update_market_market,
+ update_market_product,
+ update_market_stall,
+ update_market_zone,
+)
+from .models import (
+ CreateMarket,
+ CreateMarketStalls,
+ Orders,
+ Products,
+ SetSettings,
+ Stalls,
+ Zones,
+ createOrder,
+ createProduct,
+ createStalls,
+ createZones,
+)
+
+# from lnbits.db import open_ext_db
+
+
+### Products
+@market_ext.get("/api/v1/products")
+async def api_market_products(
+ wallet: WalletTypeInfo = Depends(require_invoice_key),
+ all_stalls: bool = Query(False),
+):
+ wallet_ids = [wallet.wallet.id]
+
+ if all_stalls:
+ user = await get_user(wallet.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
+
+ stalls = [stall.id for stall in await get_market_stalls(wallet_ids)]
+
+ if not stalls:
+ return
+
+ return [product.dict() for product in await get_market_products(stalls)]
+
+
+@market_ext.post("/api/v1/products")
+@market_ext.put("/api/v1/products/{product_id}")
+async def api_market_product_create(
+ data: createProduct,
+ product_id=None,
+ wallet: WalletTypeInfo = Depends(require_invoice_key),
+):
+ # For fiat currencies,
+ # we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
+ settings = await get_market_settings(user=wallet.wallet.user)
+ assert settings
+
+ stall = await get_market_stall(stall_id=data.stall)
+ assert stall
+
+ if stall.currency != "sat":
+ data.price *= settings.fiat_base_multiplier
+
+ if product_id:
+ product = await get_market_product(product_id)
+ if not product:
+ return {"message": "Product does not exist."}
+
+ # stall = await get_market_stall(stall_id=product.stall)
+ if stall.wallet != wallet.wallet.id:
+ return {"message": "Not your product."}
+
+ product = await update_market_product(product_id, **data.dict())
+ else:
+ product = await create_market_product(data=data)
+ assert product
+ return product.dict()
+
+
+@market_ext.delete("/api/v1/products/{product_id}")
+async def api_market_products_delete(
+ product_id, wallet: WalletTypeInfo = Depends(require_admin_key)
+):
+ product = await get_market_product(product_id)
+
+ if not product:
+ return {"message": "Product does not exist."}
+
+ stall = await get_market_stall(product.stall)
+ assert stall
+
+ if stall.wallet != wallet.wallet.id:
+ return {"message": "Not your Market."}
+
+ await delete_market_product(product_id)
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+# # # Shippingzones
+
+
+@market_ext.get("/api/v1/zones")
+async def api_market_zones(wallet: WalletTypeInfo = Depends(get_key_type)):
+
+ return await get_market_zones(wallet.wallet.user)
+
+
+@market_ext.post("/api/v1/zones")
+async def api_market_zone_create(
+ data: createZones, wallet: WalletTypeInfo = Depends(get_key_type)
+):
+ zone = await create_market_zone(user=wallet.wallet.user, data=data)
+ return zone.dict()
+
+
+@market_ext.post("/api/v1/zones/{zone_id}")
+async def api_market_zone_update(
+ data: createZones,
+ zone_id: str,
+ wallet: WalletTypeInfo = Depends(require_admin_key),
+):
+ zone = await get_market_zone(zone_id)
+ if not zone:
+ return {"message": "Zone does not exist."}
+ if zone.user != wallet.wallet.user:
+ return {"message": "Not your record."}
+ zone = await update_market_zone(zone_id, **data.dict())
+ return zone
+
+
+@market_ext.delete("/api/v1/zones/{zone_id}")
+async def api_market_zone_delete(
+ zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)
+):
+ zone = await get_market_zone(zone_id)
+
+ if not zone:
+ return {"message": "zone does not exist."}
+
+ if zone.user != wallet.wallet.user:
+ return {"message": "Not your zone."}
+
+ await delete_market_zone(zone_id)
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+# # # Stalls
+
+
+@market_ext.get("/api/v1/stalls")
+async def api_market_stalls(
+ wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
+):
+ wallet_ids = [wallet.wallet.id]
+
+ if all_wallets:
+ user = await get_user(wallet.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
+
+ return [stall.dict() for stall in await get_market_stalls(wallet_ids)]
+
+
+@market_ext.post("/api/v1/stalls")
+@market_ext.put("/api/v1/stalls/{stall_id}")
+async def api_market_stall_create(
+ data: createStalls,
+ stall_id: str = None,
+ wallet: WalletTypeInfo = Depends(require_invoice_key),
+):
+
+ if stall_id:
+ stall = await get_market_stall(stall_id)
+ if not stall:
+ return {"message": "Withdraw stall does not exist."}
+
+ if stall.wallet != wallet.wallet.id:
+ return {"message": "Not your withdraw stall."}
+
+ stall = await update_market_stall(stall_id, **data.dict())
+ else:
+ stall = await create_market_stall(data=data)
+ assert stall
+ return stall.dict()
+
+
+@market_ext.delete("/api/v1/stalls/{stall_id}")
+async def api_market_stall_delete(
+ stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
+):
+ stall = await get_market_stall(stall_id)
+
+ if not stall:
+ return {"message": "Stall does not exist."}
+
+ if stall.wallet != wallet.wallet.id:
+ return {"message": "Not your Stall."}
+
+ await delete_market_stall(stall_id)
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+###Orders
+
+
+@market_ext.get("/api/v1/orders")
+async def api_market_orders(
+ wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
+):
+ wallet_ids = [wallet.wallet.id]
+ if all_wallets:
+ user = await get_user(wallet.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
+
+ orders = await get_market_orders(wallet_ids)
+ if not orders:
+ return
+ orders_with_details = []
+ for order in orders:
+ _order = order.dict()
+ _order["details"] = await get_market_order_details(_order["id"])
+ orders_with_details.append(_order)
+ try:
+ return orders_with_details # [order for order in orders]
+ # return [order.dict() for order in await get_market_orders(wallet_ids)]
+ except:
+ return {"message": "We could not retrieve the orders."}
+
+
+@market_ext.get("/api/v1/orders/{order_id}")
+async def api_market_order_by_id(order_id: str):
+ order = await get_market_order(order_id)
+ assert order
+ _order = order.dict()
+ _order["details"] = await get_market_order_details(order_id)
+
+ return _order
+
+
+@market_ext.post("/api/v1/orders")
+async def api_market_order_create(data: createOrder):
+ ref = urlsafe_short_hash()
+
+ payment_hash, payment_request = await create_invoice(
+ wallet_id=data.wallet,
+ amount=data.total,
+ memo=f"New order on Market",
+ extra={
+ "tag": "market",
+ "reference": ref,
+ },
+ )
+ order_id = await create_market_order(invoiceid=payment_hash, data=data)
+ logger.debug(f"ORDER ID {order_id}")
+ logger.debug(f"PRODUCTS {data.products}")
+ await create_market_order_details(order_id=order_id, data=data.products)
+ return {
+ "payment_hash": payment_hash,
+ "payment_request": payment_request,
+ "order_reference": ref,
+ }
+
+
+@market_ext.get("/api/v1/orders/payments/{payment_hash}")
+async def api_market_check_payment(payment_hash: str):
+ order = await get_market_order_invoiceid(payment_hash)
+ if not order:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Order does not exist."
+ )
+ try:
+ status = await api_payment(payment_hash)
+
+ except Exception as exc:
+ logger.error(exc)
+ return {"paid": False}
+ return status
+
+
+@market_ext.delete("/api/v1/orders/{order_id}")
+async def api_market_order_delete(
+ order_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
+):
+ order = await get_market_order(order_id)
+
+ if not order:
+ return {"message": "Order does not exist."}
+
+ if order.wallet != wallet.wallet.id:
+ return {"message": "Not your Order."}
+
+ await delete_market_order(order_id)
+
+ raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+# @market_ext.get("/api/v1/orders/paid/{order_id}")
+# async def api_market_order_paid(
+# order_id, wallet: WalletTypeInfo = Depends(require_admin_key)
+# ):
+# await db.execute(
+# "UPDATE market.orders SET paid = ? WHERE id = ?",
+# (
+# True,
+# order_id,
+# ),
+# )
+# return "", HTTPStatus.OK
+
+
+@market_ext.get("/api/v1/order/pubkey/{payment_hash}/{pubkey}")
+async def api_market_order_pubkey(payment_hash: str, pubkey: str):
+ await set_market_order_pubkey(payment_hash, pubkey)
+ return "", HTTPStatus.OK
+
+
+@market_ext.get("/api/v1/orders/shipped/{order_id}")
+async def api_market_order_shipped(
+ order_id, shipped: bool = Query(...), wallet: WalletTypeInfo = Depends(get_key_type)
+):
+ await db.execute(
+ "UPDATE market.orders SET shipped = ? WHERE id = ?",
+ (
+ shipped,
+ order_id,
+ ),
+ )
+ order = await db.fetchone("SELECT * FROM market.orders WHERE id = ?", (order_id,))
+
+ return order
+
+
+###List products based on stall id
+
+
+# @market_ext.get("/api/v1/stall/products/{stall_id}")
+# async def api_market_stall_products(
+# stall_id, wallet: WalletTypeInfo = Depends(get_key_type)
+# ):
+
+# rows = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
+# if not rows:
+# return {"message": "Stall does not exist."}
+
+# products = db.fetchone("SELECT * FROM market.products WHERE wallet = ?", (rows[1],))
+# if not products:
+# return {"message": "No products"}
+
+# return [products.dict() for products in await get_market_products(rows[1])]
+
+
+###Check a product has been shipped
+
+
+# @market_ext.get("/api/v1/stall/checkshipped/{checking_id}")
+# async def api_market_stall_checkshipped(
+# checking_id, wallet: WalletTypeInfo = Depends(get_key_type)
+# ):
+# rows = await db.fetchone(
+# "SELECT * FROM market.orders WHERE invoiceid = ?", (checking_id,)
+# )
+# return {"shipped": rows["shipped"]}
+
+
+##
+# MARKETS
+##
+
+
+@market_ext.get("/api/v1/markets")
+async def api_market_markets(wallet: WalletTypeInfo = Depends(get_key_type)):
+ # await get_market_market_stalls(market_id="FzpWnMyHQMcRppiGVua4eY")
+ try:
+ return [
+ market.dict() for market in await get_market_markets(wallet.wallet.user)
+ ]
+ except:
+ return {"message": "We could not retrieve the markets."}
+
+
+@market_ext.get("/api/v1/markets/{market_id}/stalls")
+async def api_market_market_stalls(market_id: str):
+ stall_ids = await get_market_market_stalls(market_id)
+ return stall_ids
+
+
+@market_ext.post("/api/v1/markets")
+@market_ext.put("/api/v1/markets/{market_id}")
+async def api_market_market_create(
+ data: CreateMarket,
+ market_id: str = None,
+ wallet: WalletTypeInfo = Depends(require_invoice_key),
+):
+ if market_id:
+ market = await get_market_market(market_id)
+ if not market:
+ return {"message": "Market does not exist."}
+
+ if market.usr != wallet.wallet.user:
+ return {"message": "Not your market."}
+
+ market = await update_market_market(market_id, data.name)
+ else:
+ market = await create_market_market(data=data)
+
+ assert market
+ await create_market_market_stalls(market_id=market.id, data=data.stalls)
+
+ return market.dict()
+
+
+## MESSAGES/CHAT
+
+
+@market_ext.get("/api/v1/chat/messages/merchant")
+async def api_get_merchant_messages(
+ orders: str = Query(...), wallet: WalletTypeInfo = Depends(require_admin_key)
+):
+ return [msg.dict() for msg in await get_market_chat_by_merchant(orders.split(","))]
+
+
+@market_ext.get("/api/v1/chat/messages/{room_name}")
+async def api_get_latest_chat_msg(room_name: str, all_messages: bool = Query(False)):
+ if all_messages:
+ messages = await get_market_chat_messages(room_name)
+ else:
+ messages = await get_market_latest_chat_messages(room_name)
+
+ return messages
+
+
+@market_ext.get("/api/v1/currencies")
+async def api_list_currencies_available():
+ return list(currencies.keys())
+
+
+@market_ext.get("/api/v1/settings")
+async def api_get_settings(wallet: WalletTypeInfo = Depends(require_admin_key)):
+ user = wallet.wallet.user
+
+ settings = await get_market_settings(user)
+
+ return settings
+
+
+@market_ext.post("/api/v1/settings")
+@market_ext.put("/api/v1/settings/{usr}")
+async def api_set_settings(
+ data: SetSettings,
+ usr: str = None,
+ wallet: WalletTypeInfo = Depends(require_admin_key),
+):
+ if usr:
+ if usr != wallet.wallet.user:
+ return {"message": "Not your Market."}
+
+ settings = await get_market_settings(user=usr)
+ assert settings
+
+ if settings.user != wallet.wallet.user:
+ return {"message": "Not your Market."}
+
+ return await set_market_settings(usr, data)
+
+ user = wallet.wallet.user
+
+ return await create_market_settings(user, data)
diff --git a/lnbits/extensions/nostrnip5/tasks.py b/lnbits/extensions/nostrnip5/tasks.py
index 30e8cec6..f0d0c965 100644
--- a/lnbits/extensions/nostrnip5/tasks.py
+++ b/lnbits/extensions/nostrnip5/tasks.py
@@ -18,8 +18,6 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if not payment.extra:
- return
if payment.extra.get("tag") != "nostrnip5":
return
diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py
index f6ff0c36..ca4e6bac 100644
--- a/lnbits/extensions/offlineshop/lnurl.py
+++ b/lnbits/extensions/offlineshop/lnurl.py
@@ -1,15 +1,9 @@
-import hashlib
-
-from fastapi.params import Query
-from lnurl import ( # type: ignore
- LnurlErrorResponse,
- LnurlPayActionResponse,
- LnurlPayResponse,
-)
+from fastapi import Query
+from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse
+from lnurl.models import ClearnetUrl, LightningInvoice, MilliSatoshi
from starlette.requests import Request
from lnbits.core.services import create_invoice
-from lnbits.extensions.offlineshop.models import Item
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from . import offlineshop_ext
@@ -17,8 +11,8 @@ from .crud import get_item, get_shop
@offlineshop_ext.get("/lnurl/{item_id}", name="offlineshop.lnurl_response")
-async def lnurl_response(req: Request, item_id: int = Query(...)):
- item = await get_item(item_id) # type: Item
+async def lnurl_response(req: Request, item_id: int = Query(...)) -> dict:
+ item = await get_item(item_id)
if not item:
return {"status": "ERROR", "reason": "Item not found."}
@@ -32,9 +26,11 @@ async def lnurl_response(req: Request, item_id: int = Query(...)):
) * 1000
resp = LnurlPayResponse(
- callback=req.url_for("offlineshop.lnurl_callback", item_id=item.id),
- min_sendable=price_msat,
- max_sendable=price_msat,
+ callback=ClearnetUrl(
+ req.url_for("offlineshop.lnurl_callback", item_id=item.id), scheme="https"
+ ),
+ minSendable=MilliSatoshi(price_msat),
+ maxSendable=MilliSatoshi(price_msat),
metadata=await item.lnurlpay_metadata(),
)
@@ -43,7 +39,7 @@ async def lnurl_response(req: Request, item_id: int = Query(...)):
@offlineshop_ext.get("/lnurl/cb/{item_id}", name="offlineshop.lnurl_callback")
async def lnurl_callback(request: Request, item_id: int):
- item = await get_item(item_id) # type: Item
+ item = await get_item(item_id)
if not item:
return {"status": "ERROR", "reason": "Couldn't find item."}
@@ -67,6 +63,7 @@ async def lnurl_callback(request: Request, item_id: int):
).dict()
shop = await get_shop(item.shop)
+ assert shop
try:
payment_hash, payment_request = await create_invoice(
@@ -77,14 +74,15 @@ async def lnurl_callback(request: Request, item_id: int):
extra={"tag": "offlineshop", "item": item.id},
)
except Exception as exc:
- return LnurlErrorResponse(reason=exc.message).dict()
+ return LnurlErrorResponse(reason=str(exc)).dict()
- resp = LnurlPayActionResponse(
- pr=payment_request,
- success_action=item.success_action(shop, payment_hash, request)
- if shop.method
- else None,
- routes=[],
- )
+ if shop.method:
+ success_action = item.success_action(shop, payment_hash, request)
+ assert success_action
+ resp = LnurlPayActionResponse(
+ pr=LightningInvoice(payment_request),
+ successAction=success_action,
+ routes=[],
+ )
- return resp.dict()
+ return resp.dict()
diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py
index ca5c73a5..d2e3b3d2 100644
--- a/lnbits/extensions/offlineshop/models.py
+++ b/lnbits/extensions/offlineshop/models.py
@@ -5,9 +5,9 @@ from collections import OrderedDict
from sqlite3 import Row
from typing import Dict, List, Optional
-from lnurl import encode as lnurl_encode # type: ignore
-from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
-from lnurl.types import LnurlPayMetadata # type: ignore
+from lnurl import encode as lnurl_encode
+from lnurl.models import ClearnetUrl, Max144Str, UrlAction
+from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel
from starlette.requests import Request
@@ -119,11 +119,16 @@ class Item(BaseModel):
def success_action(
self, shop: Shop, payment_hash: str, req: Request
- ) -> Optional[LnurlPaySuccessAction]:
+ ) -> Optional[UrlAction]:
if not shop.wordlist:
return None
return UrlAction(
- url=req.url_for("offlineshop.confirmation_code", p=payment_hash),
- description="Open to get the confirmation code for your purchase.",
+ url=ClearnetUrl(
+ req.url_for("offlineshop.confirmation_code", p=payment_hash),
+ scheme="https",
+ ),
+ description=Max144Str(
+ "Open to get the confirmation code for your purchase."
+ ),
)
diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py
index 34bb7a03..3c2aaf5a 100644
--- a/lnbits/extensions/offlineshop/views.py
+++ b/lnbits/extensions/offlineshop/views.py
@@ -3,8 +3,7 @@ from datetime import datetime
from http import HTTPStatus
from typing import List
-from fastapi import HTTPException, Request
-from fastapi.params import Depends, Query
+from fastapi import Depends, HTTPException, Query, Request
from starlette.responses import HTMLResponse
from lnbits.core.crud import get_standalone_payment
@@ -25,10 +24,10 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
@offlineshop_ext.get("/print", response_class=HTMLResponse)
-async def print_qr_codes(request: Request, items: List[int] = None):
+async def print_qr_codes(request: Request):
items = []
for item_id in request.query_params.get("items").split(","):
- item = await get_item(item_id) # type: Item
+ item = await get_item(item_id)
if item:
items.append(
{
@@ -53,7 +52,8 @@ async def confirmation_code(p: str = Query(...)):
payment_hash = p
await api_payment(payment_hash)
- payment: Payment = await get_standalone_payment(payment_hash)
+
+ payment = await get_standalone_payment(payment_hash)
if not payment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
@@ -72,8 +72,13 @@ async def confirmation_code(p: str = Query(...)):
detail="Too much time has passed." + style,
)
- item = await get_item(payment.extra.get("item"))
+ assert payment.extra
+ item_id = payment.extra.get("item")
+ assert item_id
+ item = await get_item(item_id)
+ assert item
shop = await get_shop(item.shop)
+ assert shop
return (
f"""
diff --git a/lnbits/extensions/offlineshop/views_api.py b/lnbits/extensions/offlineshop/views_api.py
index 71583b9e..5e9f7e0d 100644
--- a/lnbits/extensions/offlineshop/views_api.py
+++ b/lnbits/extensions/offlineshop/views_api.py
@@ -1,13 +1,9 @@
from http import HTTPStatus
from typing import Optional
-from fastapi import Query
-from fastapi.params import Depends
+from fastapi import Depends, HTTPException, Query, Request, Response
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
-from pydantic.main import BaseModel
-from starlette.exceptions import HTTPException
-from starlette.requests import Request
-from starlette.responses import HTMLResponse # type: ignore
+from pydantic import BaseModel
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.utils.exchange_rates import currencies
@@ -34,6 +30,7 @@ async def api_shop_from_wallet(
r: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
+ assert shop
items = await get_items(shop.id)
try:
return {
@@ -62,6 +59,7 @@ async def api_add_or_update_item(
data: CreateItemsData, item_id=None, wallet: WalletTypeInfo = Depends(get_key_type)
):
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
+ assert shop
if data.unit != "sat":
data.price = data.price * 100
if item_id == None:
@@ -71,11 +69,11 @@ async def api_add_or_update_item(
data.name,
data.description,
data.image,
- data.price,
+ int(data.price),
data.unit,
data.fiat_base_multiplier,
)
- return HTMLResponse(status_code=HTTPStatus.CREATED)
+ return Response(status_code=HTTPStatus.CREATED)
else:
await update_item(
shop.id,
@@ -83,7 +81,7 @@ async def api_add_or_update_item(
data.name,
data.description,
data.image,
- data.price,
+ int(data.price),
data.unit,
data.fiat_base_multiplier,
)
@@ -92,6 +90,7 @@ async def api_add_or_update_item(
@offlineshop_ext.delete("/api/v1/offlineshop/items/{item_id}")
async def api_delete_item(item_id, wallet: WalletTypeInfo = Depends(get_key_type)):
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
+ assert shop
await delete_item_from_shop(shop.id, item_id)
return "", HTTPStatus.NO_CONTENT
@@ -107,7 +106,7 @@ async def api_set_method(
):
method = data.method
- wordlist = data.wordlist.split("\n") if data.wordlist else None
+ wordlist = data.wordlist.split("\n") if data.wordlist else []
wordlist = [word.strip() for word in wordlist if word.strip()]
shop = await get_or_create_shop_by_wallet(wallet.wallet.id)
diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py
index 78433838..4fb14695 100644
--- a/lnbits/extensions/satspay/crud.py
+++ b/lnbits/extensions/satspay/crud.py
@@ -12,14 +12,13 @@ from . import db
from .helpers import fetch_onchain_balance
from .models import Charges, CreateCharge, SatsPayThemes
-###############CHARGES##########################
-
-async def create_charge(user: str, data: CreateCharge) -> Charges:
+async def create_charge(user: str, data: CreateCharge) -> Optional[Charges]:
data = CreateCharge(**data.dict())
charge_id = urlsafe_short_hash()
if data.onchainwallet:
config = await get_config(user)
+ assert config
data.extra = json.dumps(
{"mempool_endpoint": config.mempool_endpoint, "network": config.network}
)
@@ -92,7 +91,7 @@ async def update_charge(charge_id: str, **kwargs) -> Optional[Charges]:
return Charges.from_row(row) if row else None
-async def get_charge(charge_id: str) -> Charges:
+async def get_charge(charge_id: str) -> Optional[Charges]:
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
return Charges.from_row(row) if row else None
@@ -111,6 +110,7 @@ async def delete_charge(charge_id: str) -> None:
async def check_address_balance(charge_id: str) -> Optional[Charges]:
charge = await get_charge(charge_id)
+ assert charge
if not charge.paid:
if charge.onchainaddress:
@@ -131,7 +131,7 @@ async def check_address_balance(charge_id: str) -> Optional[Charges]:
################## SETTINGS ###################
-async def save_theme(data: SatsPayThemes, css_id: str = None):
+async def save_theme(data: SatsPayThemes, css_id: Optional[str]):
# insert or update
if css_id:
await db.execute(
@@ -162,7 +162,7 @@ async def save_theme(data: SatsPayThemes, css_id: str = None):
return await get_theme(css_id)
-async def get_theme(css_id: str) -> SatsPayThemes:
+async def get_theme(css_id: str) -> Optional[SatsPayThemes]:
row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
return SatsPayThemes.from_row(row) if row else None
diff --git a/lnbits/extensions/satspay/helpers.py b/lnbits/extensions/satspay/helpers.py
index b21a3ae2..8596d368 100644
--- a/lnbits/extensions/satspay/helpers.py
+++ b/lnbits/extensions/satspay/helpers.py
@@ -32,6 +32,7 @@ def public_charge(charge: Charges):
async def call_webhook(charge: Charges):
async with httpx.AsyncClient() as client:
try:
+ assert charge.webhook
r = await client.post(
charge.webhook,
json=public_charge(charge),
@@ -54,6 +55,8 @@ async def fetch_onchain_balance(charge: Charges):
if charge.config.network == "Testnet"
else charge.config.mempool_endpoint
)
+ assert endpoint
+ assert charge.onchainaddress
async with httpx.AsyncClient() as client:
r = await client.get(endpoint + "/api/address/" + charge.onchainaddress)
return r.json()["chain_stats"]["funded_txo_sum"]
diff --git a/lnbits/extensions/satspay/tasks.py b/lnbits/extensions/satspay/tasks.py
index ce54b44a..992e5eb6 100644
--- a/lnbits/extensions/satspay/tasks.py
+++ b/lnbits/extensions/satspay/tasks.py
@@ -22,10 +22,12 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
+
if payment.extra.get("tag") != "charge":
# not a charge invoice
return
+ assert payment.memo
charge = await get_charge(payment.memo)
if not charge:
logger.error("this should never happen", payment)
@@ -33,6 +35,7 @@ async def on_invoice_paid(payment: Payment) -> None:
await payment.set_pending(False)
charge = await check_address_balance(charge_id=charge.id)
+ assert charge
if charge.must_call_webhook():
resp = await call_webhook(charge)
diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py
index 90f8a6b9..15a4403d 100644
--- a/lnbits/extensions/satspay/views.py
+++ b/lnbits/extensions/satspay/views.py
@@ -1,10 +1,7 @@
from http import HTTPStatus
-from fastapi import Response
-from fastapi.param_functions import Depends
+from fastapi import Depends, HTTPException, Request, Response
from fastapi.templating import Jinja2Templates
-from starlette.exceptions import HTTPException
-from starlette.requests import Request
from starlette.responses import HTMLResponse
from lnbits.core.models import User
diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py
index 08c731cb..98c338ed 100644
--- a/lnbits/extensions/satspay/views_api.py
+++ b/lnbits/extensions/satspay/views_api.py
@@ -1,9 +1,8 @@
import json
from http import HTTPStatus
-from fastapi import Depends, Query
+from fastapi import Depends, HTTPException, Query
from loguru import logger
-from starlette.exceptions import HTTPException
from lnbits.decorators import (
WalletTypeInfo,
@@ -29,8 +28,6 @@ from .crud import (
from .helpers import call_webhook, public_charge
from .models import CreateCharge, SatsPayThemes
-#############################CHARGES##########################
-
@satspay_ext.post("/api/v1/charge")
async def api_charge_create(
@@ -38,6 +35,7 @@ async def api_charge_create(
):
try:
charge = await create_charge(user=wallet.wallet.user, data=data)
+ assert charge
return {
**charge.dict(),
**{"time_elapsed": charge.time_elapsed},
@@ -51,13 +49,15 @@ async def api_charge_create(
)
-@satspay_ext.put("/api/v1/charge/{charge_id}")
+@satspay_ext.put(
+ "/api/v1/charge/{charge_id}", dependencies=[Depends(require_admin_key)]
+)
async def api_charge_update(
data: CreateCharge,
- wallet: WalletTypeInfo = Depends(require_admin_key),
- charge_id=None,
+ charge_id: str,
):
charge = await update_charge(charge_id=charge_id, data=data)
+ assert charge
return charge.dict()
@@ -78,10 +78,8 @@ async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
return ""
-@satspay_ext.get("/api/v1/charge/{charge_id}")
-async def api_charge_retrieve(
- charge_id, wallet: WalletTypeInfo = Depends(get_key_type)
-):
+@satspay_ext.get("/api/v1/charge/{charge_id}", dependencies=[Depends(get_key_type)])
+async def api_charge_retrieve(charge_id: str):
charge = await get_charge(charge_id)
if not charge:
@@ -97,8 +95,8 @@ async def api_charge_retrieve(
}
-@satspay_ext.delete("/api/v1/charge/{charge_id}")
-async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_type)):
+@satspay_ext.delete("/api/v1/charge/{charge_id}", dependencies=[Depends(get_key_type)])
+async def api_charge_delete(charge_id: str):
charge = await get_charge(charge_id)
if not charge:
@@ -155,7 +153,7 @@ async def api_themes_save(
theme = await save_theme(css_id=css_id, data=data)
else:
data.user = wallet.wallet.user
- theme = await save_theme(data=data)
+ theme = await save_theme(data=data, css_id="no_id")
return theme
@@ -169,8 +167,8 @@ async def api_themes_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
return ""
-@satspay_ext.delete("/api/v1/themes/{theme_id}")
-async def api_theme_delete(theme_id, wallet: WalletTypeInfo = Depends(get_key_type)):
+@satspay_ext.delete("/api/v1/themes/{theme_id}", dependencies=[Depends(get_key_type)])
+async def api_theme_delete(theme_id):
theme = await get_theme(theme_id)
if not theme:
diff --git a/lnbits/extensions/scrub/tasks.py b/lnbits/extensions/scrub/tasks.py
index 096cbef9..26249bb1 100644
--- a/lnbits/extensions/scrub/tasks.py
+++ b/lnbits/extensions/scrub/tasks.py
@@ -27,7 +27,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment):
# (avoid loops)
- if payment.extra and payment.extra.get("tag") == "scrubed":
+ if payment.extra.get("tag") == "scrubed":
# already scrubbed
return
diff --git a/lnbits/extensions/streamalerts/crud.py b/lnbits/extensions/streamalerts/crud.py
index 1e7239a9..37583117 100644
--- a/lnbits/extensions/streamalerts/crud.py
+++ b/lnbits/extensions/streamalerts/crud.py
@@ -25,15 +25,20 @@ async def get_charge_details(service_id):
These might be different depending for services implemented in the future.
"""
- details = {"time": 1440}
service = await get_service(service_id)
+ assert service
+
wallet_id = service.wallet
wallet = await get_wallet(wallet_id)
+ assert wallet
+
user = wallet.user
- details["user"] = user
- details["lnbitswallet"] = wallet_id
- details["onchainwallet"] = service.onchain
- return details
+ return {
+ "time": 1440,
+ "user": user,
+ "lnbitswallet": wallet_id,
+ "onchainwallet": service.onchain,
+ }
async def create_donation(
@@ -71,7 +76,7 @@ async def create_donation(
return donation
-async def post_donation(donation_id: str) -> tuple:
+async def post_donation(donation_id: str) -> dict:
"""Post donations to their respective third party APIs
If the donation has already been posted, it will not be posted again.
@@ -97,7 +102,6 @@ async def post_donation(donation_id: str) -> tuple:
}
async with httpx.AsyncClient() as client:
response = await client.post(url, data=data)
- status = [s for s in list(HTTPStatus) if s == response.status_code][0]
elif service.servicename == "StreamElements":
return {"message": "StreamElements not yet supported!"}
else:
diff --git a/lnbits/extensions/streamalerts/models.py b/lnbits/extensions/streamalerts/models.py
index 4a365cba..ae0ffab5 100644
--- a/lnbits/extensions/streamalerts/models.py
+++ b/lnbits/extensions/streamalerts/models.py
@@ -1,8 +1,8 @@
from sqlite3 import Row
from typing import Optional
-from fastapi.params import Query
-from pydantic.main import BaseModel
+from fastapi import Query
+from pydantic import BaseModel
class CreateService(BaseModel):
diff --git a/lnbits/extensions/streamalerts/views.py b/lnbits/extensions/streamalerts/views.py
index 595b841e..ac63e9c5 100644
--- a/lnbits/extensions/streamalerts/views.py
+++ b/lnbits/extensions/streamalerts/views.py
@@ -1,6 +1,6 @@
from http import HTTPStatus
-from fastapi.param_functions import Depends
+from fastapi import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.requests import Request
diff --git a/lnbits/extensions/streamalerts/views_api.py b/lnbits/extensions/streamalerts/views_api.py
index 058f5126..0134fe82 100644
--- a/lnbits/extensions/streamalerts/views_api.py
+++ b/lnbits/extensions/streamalerts/views_api.py
@@ -1,6 +1,6 @@
from http import HTTPStatus
-from fastapi.params import Depends, Query
+from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import RedirectResponse
@@ -84,6 +84,8 @@ async def api_authenticate_service(
"""
service = await get_service(service_id)
+ assert service
+
if service.state != state:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="State doesn't match!"
@@ -113,6 +115,7 @@ async def api_create_donation(data: CreateDonation, request: Request):
webhook_base = request.url.scheme + "://" + request.headers["Host"]
service_id = data.service
service = await get_service(service_id)
+ assert service
charge_details = await get_charge_details(service.id)
name = data.name if data.name else "Anonymous"
@@ -157,7 +160,8 @@ async def api_post_donation(request: Request, data: ValidateDonation):
@streamalerts_ext.get("/api/v1/services")
async def api_get_services(g: WalletTypeInfo = Depends(get_key_type)):
"""Return list of all services assigned to wallet with given invoice key"""
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
services = []
for wallet_id in wallet_ids:
new_services = await get_services(wallet_id)
@@ -170,7 +174,8 @@ async def api_get_donations(g: WalletTypeInfo = Depends(get_key_type)):
"""Return list of all donations assigned to wallet with given invoice
key
"""
- wallet_ids = (await get_user(g.wallet.user)).wallet_ids
+ user = await get_user(g.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
donations = []
for wallet_id in wallet_ids:
new_donations = await get_donations(wallet_id)
diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py
index c5a7f47b..f9e0c8ee 100644
--- a/lnbits/extensions/subdomains/tasks.py
+++ b/lnbits/extensions/subdomains/tasks.py
@@ -20,7 +20,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if not payment.extra or payment.extra.get("tag") != "lnsubdomain":
+ if payment.extra.get("tag") != "lnsubdomain":
# not an lnurlp invoice
return
diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py
index ad018808..fe9fb7e6 100644
--- a/lnbits/extensions/tpos/tasks.py
+++ b/lnbits/extensions/tpos/tasks.py
@@ -26,7 +26,6 @@ async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") != "tpos":
return
- tpos = await get_tpos(payment.extra.get("tposId"))
tipAmount = payment.extra.get("tipAmount")
strippedPayment = {
@@ -37,14 +36,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 76dcf9c4..ada54b3f 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:
@@ -89,7 +90,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)
@@ -116,7 +117,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..83dd0593 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()
+ link_id = urlsafe_short_hash()[:6]
+ 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 186e2123..03dbbc8d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -91,20 +91,9 @@ files = "lnbits"
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/offlineshop.
- | ^lnbits/extensions/satspay.
- | ^lnbits/extensions/streamalerts.
- | ^lnbits/extensions/tpos.
| ^lnbits/extensions/watchonly.
- | ^lnbits/extensions/withdraw.
| ^lnbits/wallets/lnd_grpc_files.
)"""