diff --git a/.env.example b/.env.example
index bb4e64a1..6a3710c2 100644
--- a/.env.example
+++ b/.env.example
@@ -11,7 +11,11 @@ LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
+
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
+# Warning: Enabling this will make LNbits ignore this configuration file. Your settings will
+# be stored in your database and you will be able to change them only through the Admin UI.
+# Disable this to make LNbits use this config file again.
LNBITS_ADMIN_UI=false
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
diff --git a/README.md b/README.md
index a22c857c..3bc169dd 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
-See [legend.lnbits.org](https://legend.lnbits.org) for more detailed documentation.
+See [docs.lnbits.org](https://docs.lnbits.org) for more detailed documentation.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
@@ -70,7 +70,7 @@ Wallets can be easily generated and given out to people at events (one click mul
If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
-[docs]: https://legend.lnbits.org/
+[docs]: https://docs.lnbits.org/
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
[github-mypy-badge]: https://github.com/lnbits/lnbits/workflows/mypy/badge.svg
diff --git a/docs/CNAME b/docs/CNAME
index 9981b110..e7e04e60 100644
--- a/docs/CNAME
+++ b/docs/CNAME
@@ -1 +1 @@
-legend.lnbits.org
\ No newline at end of file
+docs.lnbits.org
\ No newline at end of file
diff --git a/docs/_config.yml b/docs/_config.yml
index 6c3d6512..d937f5dc 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -3,7 +3,7 @@ remote_theme: pmarsceill/just-the-docs
color_scheme: dark
logo: "/logos/lnbits-full--inverse.png"
search_enabled: true
-url: https://legend.lnbits.org
+url: https://docs.lnbits.org
aux_links:
"LNbits on GitHub":
- "//github.com/lnbits/lnbits"
diff --git a/docs/devs/api.md b/docs/devs/api.md
index a8217b9c..8e150889 100644
--- a/docs/devs/api.md
+++ b/docs/devs/api.md
@@ -9,4 +9,4 @@ nav_order: 3
API reference
=============
-[Swagger Docs](https://legend.lnbits.org/devs/swagger.html)
+[Swagger Docs](https://docs.lnbits.org/devs/swagger.html)
diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py
index 41b73b7d..0bc40158 100644
--- a/lnbits/bolt11.py
+++ b/lnbits/bolt11.py
@@ -74,7 +74,7 @@ def decode(pr: str) -> Invoice:
data_length = len(tagdata) / 5
if tag == "d":
- invoice.description = _trim_to_bytes(tagdata).decode("utf-8")
+ invoice.description = _trim_to_bytes(tagdata).decode()
elif tag == "h" and data_length == 52:
invoice.description_hash = _trim_to_bytes(tagdata).hex()
elif tag == "p" and data_length == 52:
@@ -260,7 +260,7 @@ class LnAddr(object):
def __str__(self):
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
- bytes.hex(self.pubkey.serialize()).decode("utf-8"),
+ bytes.hex(self.pubkey.serialize()).decode(),
self.amount,
self.currency,
", ".join([k + "=" + str(v) for k, v in self.tags]),
diff --git a/lnbits/core/models.py b/lnbits/core/models.py
index 138a39f7..e292362a 100644
--- a/lnbits/core/models.py
+++ b/lnbits/core/models.py
@@ -46,8 +46,8 @@ class Wallet(BaseModel):
return ""
def lnurlauth_key(self, domain: str) -> SigningKey:
- hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
- linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
+ hashing_key = hashlib.sha256(self.id.encode()).digest()
+ linking_key = hmac.digest(hashing_key, domain.encode(), "sha256")
return SigningKey.from_string(
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
diff --git a/lnbits/core/templates/core/extensions.html b/lnbits/core/templates/core/extensions.html
index 1b527903..88e50269 100644
--- a/lnbits/core/templates/core/extensions.html
+++ b/lnbits/core/templates/core/extensions.html
@@ -23,14 +23,55 @@
>
-
- {% raw %}
- {{ extension.name }}
- {{ extension.shortDescription }} {% endraw %}
+
+
+
+
+
+ {% raw %}
+
+ {{ extension.name }}
+
+
+ {{ extension.shortDescription }}
+
+
+ {{ extension.name }}
+
+
+ {{ extension.shortDescription }}
+
+ {% endraw %}
+
+
+
+
+
+ Ratings coming soon
+
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html
index 9de96956..46cfc690 100644
--- a/lnbits/core/templates/core/wallet.html
+++ b/lnbits/core/templates/core/wallet.html
@@ -23,7 +23,7 @@
{% raw %}{{ formattedBalance }} {% endraw %}
{{LNBITS_DENOMINATION}}
-
-
-
-
-
diff --git a/lnbits/core/views/admin_api.py b/lnbits/core/views/admin_api.py
index 20eaeea3..4a2c5a2b 100644
--- a/lnbits/core/views/admin_api.py
+++ b/lnbits/core/views/admin_api.py
@@ -17,7 +17,7 @@ from ..crud import delete_admin_settings, get_admin_settings, update_admin_setti
@core_app.get("/admin/api/v1/settings/")
async def api_get_settings(
- user: User = Depends(check_admin), # type: ignore
+ user: User = Depends(check_admin),
) -> Optional[AdminSettings]:
admin_settings = await get_admin_settings(user.super_user)
return admin_settings
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py
index 1c67474f..d545df9a 100644
--- a/lnbits/core/views/api.py
+++ b/lnbits/core/views/api.py
@@ -12,6 +12,7 @@ import async_timeout
import httpx
import pyqrcode
from fastapi import (
+ Body,
Depends,
Header,
Query,
@@ -21,7 +22,6 @@ from fastapi import (
WebSocketDisconnect,
)
from fastapi.exceptions import HTTPException
-from fastapi.params import Body
from loguru import logger
from pydantic import BaseModel
from pydantic.fields import Field
@@ -251,7 +251,7 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
)
async def api_payments_create(
wallet: WalletTypeInfo = Depends(require_invoice_key),
- invoiceData: CreateInvoiceData = Body(...), # type: ignore
+ invoiceData: CreateInvoiceData = Body(...),
):
if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11:
@@ -387,7 +387,7 @@ async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
jdata = json.dumps(dict(data.dict(), pending=False))
yield dict(data=jdata, event=typ)
- except asyncio.CancelledError as e:
+ except asyncio.CancelledError:
logger.debug(f"removing listener for wallet {uid}")
api_invoice_listeners.pop(uid)
task.cancel()
@@ -536,7 +536,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
params.update(
description_hash=hashlib.sha256(
- data["metadata"].encode("utf-8")
+ data["metadata"].encode()
).hexdigest()
)
metadata = json.loads(data["metadata"])
diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py
index ed7b156d..ab19feb8 100644
--- a/lnbits/core/views/generic.py
+++ b/lnbits/core/views/generic.py
@@ -2,9 +2,8 @@ import asyncio
from http import HTTPStatus
from typing import Optional
-from fastapi import Request, status
+from fastapi import Depends, Query, Request, status
from fastapi.exceptions import HTTPException
-from fastapi.params import Depends, Query
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.routing import APIRouter
from loguru import logger
@@ -49,9 +48,9 @@ async def home(request: Request, lightning: str = ""):
)
async def extensions(
request: Request,
- user: User = Depends(check_user_exists), # type: ignore
- enable: str = Query(None), # type: ignore
- disable: str = Query(None), # type: ignore
+ user: User = Depends(check_user_exists),
+ enable: str = Query(None),
+ disable: str = Query(None),
):
extension_to_enable = enable
extension_to_disable = disable
@@ -103,10 +102,10 @@ nothing: create everything
""",
)
async def wallet(
- request: Request = Query(None), # type: ignore
- nme: Optional[str] = Query(None), # type: ignore
- usr: Optional[UUID4] = Query(None), # type: ignore
- wal: Optional[UUID4] = Query(None), # type: ignore
+ request: Request = Query(None),
+ nme: Optional[str] = Query(None),
+ usr: Optional[UUID4] = Query(None),
+ wal: Optional[UUID4] = Query(None),
):
user_id = usr.hex if usr else None
wallet_id = wal.hex if wal else None
@@ -132,6 +131,8 @@ async def wallet(
)
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
user.admin = True
+ if user_id == settings.super_user:
+ user.super_user = True
if not wallet_id:
if user.wallets and not wallet_name: # type: ignore
@@ -217,7 +218,7 @@ async def lnurl_full_withdraw_callback(request: Request):
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
-async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)): # type: ignore
+async def deletewallet(wal: str = Query(...), usr: str = Query(...)):
user = await get_user(usr)
user_wallet_ids = [u.id for u in user.wallets] # type: ignore
@@ -313,7 +314,7 @@ async def manifest(usr: str):
@core_html_routes.get("/admin", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_admin)): # type: ignore
+async def index(request: Request, user: User = Depends(check_admin)):
WALLET = get_wallet_class()
_, balance = await WALLET.status()
diff --git a/lnbits/decorators.py b/lnbits/decorators.py
index 9aeace40..42d2b7a3 100644
--- a/lnbits/decorators.py
+++ b/lnbits/decorators.py
@@ -1,11 +1,8 @@
from http import HTTPStatus
-from typing import Union
-from cerberus import Validator # type: ignore
-from fastapi import status
+from fastapi import Security, status
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import APIKey, APIKeyIn
-from fastapi.params import Security
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
from fastapi.security.base import SecurityBase
from pydantic.types import UUID4
@@ -118,8 +115,8 @@ api_key_query = APIKeyQuery(
async def get_key_type(
r: Request,
- api_key_header: str = Security(api_key_header), # type: ignore
- api_key_query: str = Security(api_key_query), # type: ignore
+ api_key_header: str = Security(api_key_header),
+ api_key_query: str = Security(api_key_query),
) -> WalletTypeInfo:
# 0: admin
# 1: invoice
@@ -174,8 +171,8 @@ async def get_key_type(
async def require_admin_key(
r: Request,
- api_key_header: str = Security(api_key_header), # type: ignore
- api_key_query: str = Security(api_key_query), # type: ignore
+ api_key_header: str = Security(api_key_header),
+ api_key_query: str = Security(api_key_query),
):
token = api_key_header or api_key_query
@@ -200,8 +197,8 @@ async def require_admin_key(
async def require_invoice_key(
r: Request,
- api_key_header: str = Security(api_key_header), # type: ignore
- api_key_query: str = Security(api_key_query), # type: ignore
+ api_key_header: str = Security(api_key_header),
+ api_key_query: str = Security(api_key_query),
):
token = api_key_header or api_key_query
diff --git a/lnbits/extensions/bleskomat/config.json b/lnbits/extensions/bleskomat/config.json
index 99244df1..f3cd7d8e 100644
--- a/lnbits/extensions/bleskomat/config.json
+++ b/lnbits/extensions/bleskomat/config.json
@@ -1,6 +1,6 @@
{
"name": "Bleskomat",
"short_description": "Connect a Bleskomat ATM to an lnbits",
- "icon": "money",
+ "tile": "/bleskomat/static/image/bleskomat.png",
"contributors": ["chill117"]
}
diff --git a/lnbits/extensions/bleskomat/static/image/bleskomat.png b/lnbits/extensions/bleskomat/static/image/bleskomat.png
new file mode 100644
index 00000000..cc728083
Binary files /dev/null and b/lnbits/extensions/bleskomat/static/image/bleskomat.png differ
diff --git a/lnbits/extensions/boltcards/config.json b/lnbits/extensions/boltcards/config.json
index e46070d3..0551f181 100644
--- a/lnbits/extensions/boltcards/config.json
+++ b/lnbits/extensions/boltcards/config.json
@@ -1,6 +1,6 @@
{
"name": "Bolt Cards",
"short_description": "Self custody Bolt Cards with one time LNURLw",
- "icon": "payment",
+ "tile": "/boltcards/static/image/boltcard.png",
"contributors": ["iwarpbtc", "arcbtc", "leesalminen"]
}
diff --git a/lnbits/extensions/boltcards/lnurl.py b/lnbits/extensions/boltcards/lnurl.py
index 43d64eee..3a99073a 100644
--- a/lnbits/extensions/boltcards/lnurl.py
+++ b/lnbits/extensions/boltcards/lnurl.py
@@ -213,7 +213,7 @@ async def lnurlp_callback(
memo=f"Refund {hit_id}",
unhashed_description=LnurlPayMetadata(
json.dumps([["text/plain", "Refund"]])
- ).encode("utf-8"),
+ ).encode(),
extra={"refund": hit_id},
)
diff --git a/lnbits/extensions/boltcards/static/image/boltcard.png b/lnbits/extensions/boltcards/static/image/boltcard.png
new file mode 100644
index 00000000..ce79906e
Binary files /dev/null and b/lnbits/extensions/boltcards/static/image/boltcard.png differ
diff --git a/lnbits/extensions/boltz/__init__.py b/lnbits/extensions/boltz/__init__.py
index 7eb2e5a7..9a3d5d89 100644
--- a/lnbits/extensions/boltz/__init__.py
+++ b/lnbits/extensions/boltz/__init__.py
@@ -1,6 +1,7 @@
import asyncio
from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -15,6 +16,14 @@ def boltz_renderer():
return template_renderer(["lnbits/extensions/boltz/templates"])
+boltz_static_files = [
+ {
+ "path": "/boltz/static",
+ "app": StaticFiles(directory="lnbits/extensions/boltz/static"),
+ "name": "boltz_static",
+ }
+]
+
from .tasks import check_for_pending_swaps, wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
diff --git a/lnbits/extensions/boltz/boltz.py b/lnbits/extensions/boltz/boltz.py
index 97ccb3ad..31d927ea 100644
--- a/lnbits/extensions/boltz/boltz.py
+++ b/lnbits/extensions/boltz/boltz.py
@@ -55,7 +55,7 @@ async def create_swap(data: CreateSubmarineSwap) -> SubmarineSwap:
raise
refund_privkey = ec.PrivateKey(os.urandom(32), True, net)
- refund_pubkey_hex = bytes.hex(refund_privkey.sec()).decode("UTF-8")
+ refund_pubkey_hex = bytes.hex(refund_privkey.sec()).decode()
res = req_wrap(
"post",
@@ -120,7 +120,7 @@ async def create_reverse_swap(
return False
claim_privkey = ec.PrivateKey(os.urandom(32), True, net)
- claim_pubkey_hex = bytes.hex(claim_privkey.sec()).decode("UTF-8")
+ claim_pubkey_hex = bytes.hex(claim_privkey.sec()).decode()
preimage = os.urandom(32)
preimage_hash = sha256(preimage).hexdigest()
diff --git a/lnbits/extensions/boltz/config.json b/lnbits/extensions/boltz/config.json
index 0f69d2a5..db678207 100644
--- a/lnbits/extensions/boltz/config.json
+++ b/lnbits/extensions/boltz/config.json
@@ -1,6 +1,6 @@
{
"name": "Boltz",
"short_description": "Perform onchain/offchain swaps",
- "icon": "swap_horiz",
+ "tile": "/boltz/static/image/boltz.png",
"contributors": ["dni"]
}
diff --git a/lnbits/extensions/boltz/static/image/boltz.png b/lnbits/extensions/boltz/static/image/boltz.png
new file mode 100644
index 00000000..2dcefc94
Binary files /dev/null and b/lnbits/extensions/boltz/static/image/boltz.png differ
diff --git a/lnbits/extensions/cashu/config.json b/lnbits/extensions/cashu/config.json
index af202d43..14ff1743 100644
--- a/lnbits/extensions/cashu/config.json
+++ b/lnbits/extensions/cashu/config.json
@@ -1,7 +1,7 @@
{
"name": "Cashu",
"short_description": "Ecash mint and wallet",
- "icon": "account_balance",
+ "tile": "/cashu/static/image/cashu.png",
"contributors": ["calle", "vlad", "arcbtc"],
"hidden": false
}
diff --git a/lnbits/extensions/cashu/static/image/cashu.png b/lnbits/extensions/cashu/static/image/cashu.png
new file mode 100644
index 00000000..e90611fc
Binary files /dev/null and b/lnbits/extensions/cashu/static/image/cashu.png differ
diff --git a/lnbits/extensions/cashu/views.py b/lnbits/extensions/cashu/views.py
index 1aab68bd..85adf040 100644
--- a/lnbits/extensions/cashu/views.py
+++ b/lnbits/extensions/cashu/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
@@ -18,7 +17,7 @@ templates = Jinja2Templates(directory="templates")
@cashu_ext.get("/", response_class=HTMLResponse)
async def index(
request: Request,
- user: User = Depends(check_user_exists), # type: ignore
+ user: User = Depends(check_user_exists),
):
return cashu_renderer().TemplateResponse(
"cashu/index.html", {"request": request, "user": user.dict()}
diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py
index def9074f..b66bbdc1 100644
--- a/lnbits/extensions/cashu/views_api.py
+++ b/lnbits/extensions/cashu/views_api.py
@@ -1,10 +1,7 @@
-import json
import math
from http import HTTPStatus
from typing import Dict, List, Union
-import httpx
-
# -------- cashu imports
from cashu.core.base import (
BlindedSignature,
@@ -17,14 +14,10 @@ from cashu.core.base import (
MeltRequest,
MintRequest,
PostSplitResponse,
- Proof,
SplitRequest,
)
-from fastapi import Query
-from fastapi.params import Depends
-from lnurl import decode as decode_lnurl
+from fastapi import Depends, Query
from loguru import logger
-from secp256k1 import PublicKey
from starlette.exceptions import HTTPException
from lnbits import bolt11
@@ -35,7 +28,6 @@ from lnbits.core.services import (
fee_reserve,
pay_invoice,
)
-from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.helpers import urlsafe_short_hash
from lnbits.wallets.base import PaymentStatus
@@ -63,7 +55,7 @@ if not LIGHTNING:
@cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK)
async def api_cashus(
- all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
"""
Get all mints of this wallet.
@@ -80,7 +72,7 @@ async def api_cashus(
@cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED)
async def api_cashu_create(
data: Cashu,
- wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ wallet: WalletTypeInfo = Depends(get_key_type),
):
"""
Create a new mint for this wallet.
@@ -98,7 +90,7 @@ async def api_cashu_create(
@cashu_ext.delete("/api/v1/mints/{cashu_id}")
async def api_cashu_delete(
- cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) # type: ignore
+ cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
"""
Delete an existing cashu mint.
diff --git a/lnbits/extensions/copilot/config.json b/lnbits/extensions/copilot/config.json
index a4ecb3b5..fc754999 100644
--- a/lnbits/extensions/copilot/config.json
+++ b/lnbits/extensions/copilot/config.json
@@ -1,7 +1,7 @@
{
"name": "Streamer Copilot",
"short_description": "Video tips/animations/webhooks",
- "icon": "face",
+ "tile": "/copilot/static/bitcoin-streaming.png",
"contributors": [
"arcbtc"
]
diff --git a/lnbits/extensions/copilot/lnurl.py b/lnbits/extensions/copilot/lnurl.py
index fa199691..d8ededf3 100644
--- a/lnbits/extensions/copilot/lnurl.py
+++ b/lnbits/extensions/copilot/lnurl.py
@@ -75,7 +75,7 @@ async def lnurl_callback(
memo=cp.lnurl_title,
unhashed_description=(
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
- ).encode("utf-8"),
+ ).encode(),
extra={"tag": "copilot", "copilotid": cp.id, "comment": comment},
)
payResponse = {"pr": payment_request, "routes": []}
diff --git a/lnbits/extensions/copilot/static/bitcoin-streaming.png b/lnbits/extensions/copilot/static/bitcoin-streaming.png
new file mode 100644
index 00000000..1022baf2
Binary files /dev/null and b/lnbits/extensions/copilot/static/bitcoin-streaming.png differ
diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py
index 3b1ebf03..7b66366d 100644
--- a/lnbits/extensions/copilot/views.py
+++ b/lnbits/extensions/copilot/views.py
@@ -1,7 +1,6 @@
from typing import List
-from fastapi import Request, WebSocket, WebSocketDisconnect
-from fastapi.params import Depends
+from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse # type: ignore
@@ -9,15 +8,12 @@ from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import copilot_ext, copilot_renderer
-from .crud import get_copilot
templates = Jinja2Templates(directory="templates")
@copilot_ext.get("/", response_class=HTMLResponse)
-async def index(
- request: Request, user: User = Depends(check_user_exists) # type: ignore
-):
+async def index(request: Request, user: User = Depends(check_user_exists)):
return copilot_renderer().TemplateResponse(
"copilot/index.html", {"request": request, "user": user.dict()}
)
diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py
index 39d0f7fd..f0621202 100644
--- a/lnbits/extensions/copilot/views_api.py
+++ b/lnbits/extensions/copilot/views_api.py
@@ -1,8 +1,6 @@
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 starlette.exceptions import HTTPException
from lnbits.core.services import websocketUpdater
@@ -22,9 +20,7 @@ from .models import CreateCopilotData
@copilot_ext.get("/api/v1/copilot")
-async def api_copilots_retrieve(
- req: Request, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
-):
+async def api_copilots_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_user = wallet.wallet.user
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
try:
@@ -37,7 +33,7 @@ async def api_copilots_retrieve(
async def api_copilot_retrieve(
req: Request,
copilot_id: str = Query(None),
- wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ wallet: WalletTypeInfo = Depends(get_key_type),
):
copilot = await get_copilot(copilot_id)
if not copilot:
@@ -54,7 +50,7 @@ async def api_copilot_retrieve(
async def api_copilot_create_or_update(
data: CreateCopilotData,
copilot_id: str = Query(None),
- wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
+ wallet: WalletTypeInfo = Depends(require_admin_key),
):
data.user = wallet.wallet.user
data.wallet = wallet.wallet.id
@@ -68,7 +64,7 @@ async def api_copilot_create_or_update(
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
async def api_copilot_delete(
copilot_id: str = Query(None),
- wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
+ wallet: WalletTypeInfo = Depends(require_admin_key),
):
copilot = await get_copilot(copilot_id)
diff --git a/lnbits/extensions/discordbot/config.json b/lnbits/extensions/discordbot/config.json
index eb674122..4c28d229 100644
--- a/lnbits/extensions/discordbot/config.json
+++ b/lnbits/extensions/discordbot/config.json
@@ -1,6 +1,6 @@
{
"name": "Discord Bot",
"short_description": "Generate users and wallets",
- "icon": "person_add",
+ "tile": "/discordbot/static/image/discordbot.png",
"contributors": ["bitcoingamer21"]
}
diff --git a/lnbits/extensions/discordbot/static/image/discordbot.png b/lnbits/extensions/discordbot/static/image/discordbot.png
new file mode 100644
index 00000000..55894164
Binary files /dev/null and b/lnbits/extensions/discordbot/static/image/discordbot.png differ
diff --git a/lnbits/extensions/discordbot/views.py b/lnbits/extensions/discordbot/views.py
index ec7d18cc..80988c24 100644
--- a/lnbits/extensions/discordbot/views.py
+++ b/lnbits/extensions/discordbot/views.py
@@ -1,5 +1,4 @@
-from fastapi import Request
-from fastapi.params import Depends
+from fastapi import Depends, Request
from starlette.responses import HTMLResponse
from lnbits.core.models import User
@@ -9,9 +8,7 @@ from . import discordbot_ext, discordbot_renderer
@discordbot_ext.get("/", response_class=HTMLResponse)
-async def index(
- request: Request, user: User = Depends(check_user_exists) # type: ignore
-):
+async def index(request: Request, user: User = Depends(check_user_exists)):
return discordbot_renderer().TemplateResponse(
"discordbot/index.html", {"request": request, "user": user.dict()}
)
diff --git a/lnbits/extensions/discordbot/views_api.py b/lnbits/extensions/discordbot/views_api.py
index b69c274a..4fa57192 100644
--- a/lnbits/extensions/discordbot/views_api.py
+++ b/lnbits/extensions/discordbot/views_api.py
@@ -1,7 +1,6 @@
from http import HTTPStatus
-from fastapi import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core import update_user_extension
@@ -28,16 +27,14 @@ from .models import CreateUserData, CreateUserWallet
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
async def api_discordbot_users(
- wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ wallet: WalletTypeInfo = Depends(get_key_type),
):
user_id = wallet.wallet.user
return [user.dict() for user in await get_discordbot_users(user_id)]
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
-async def api_discordbot_user(
- user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
-):
+async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
user = await get_discordbot_user(user_id)
if user:
return user.dict()
@@ -45,7 +42,7 @@ async def api_discordbot_user(
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
async def api_discordbot_users_create(
- data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
):
user = await create_discordbot_user(data)
full = user.dict()
@@ -57,7 +54,7 @@ async def api_discordbot_users_create(
@discordbot_ext.delete("/api/v1/users/{user_id}")
async def api_discordbot_users_delete(
- user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ user_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
user = await get_discordbot_user(user_id)
if not user:
@@ -89,7 +86,7 @@ async def api_discordbot_activate_extension(
@discordbot_ext.post("/api/v1/wallets")
async def api_discordbot_wallets_create(
- data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
):
user = await create_discordbot_wallet(
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
@@ -99,7 +96,7 @@ async def api_discordbot_wallets_create(
@discordbot_ext.get("/api/v1/wallets")
async def api_discordbot_wallets(
- wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ wallet: WalletTypeInfo = Depends(get_key_type),
):
admin_id = wallet.wallet.user
return await get_discordbot_wallets(admin_id)
@@ -107,21 +104,21 @@ async def api_discordbot_wallets(
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
async def api_discordbot_wallet_transactions(
- wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
return await get_discordbot_wallet_transactions(wallet_id)
@discordbot_ext.get("/api/v1/wallets/{user_id}")
async def api_discordbot_users_wallets(
- user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ user_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
return await get_discordbot_users_wallets(user_id)
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
async def api_discordbot_wallets_delete(
- wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
get_wallet = await get_discordbot_wallet(wallet_id)
if not get_wallet:
diff --git a/lnbits/extensions/events/__init__.py b/lnbits/extensions/events/__init__.py
index f689aaa6..b2cb7540 100644
--- a/lnbits/extensions/events/__init__.py
+++ b/lnbits/extensions/events/__init__.py
@@ -1,6 +1,7 @@
import asyncio
from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -11,6 +12,14 @@ db = Database("ext_events")
events_ext: APIRouter = APIRouter(prefix="/events", tags=["Events"])
+events_static_files = [
+ {
+ "path": "/events/static",
+ "app": StaticFiles(packages=[("lnbits", "extensions/events/static")]),
+ "name": "events_static",
+ }
+]
+
def events_renderer():
return template_renderer(["lnbits/extensions/events/templates"])
diff --git a/lnbits/extensions/events/config.json b/lnbits/extensions/events/config.json
index 6bc144ab..a62bcc47 100644
--- a/lnbits/extensions/events/config.json
+++ b/lnbits/extensions/events/config.json
@@ -1,6 +1,6 @@
{
"name": "Events",
"short_description": "Sell and register event tickets",
- "icon": "local_activity",
+ "tile": "/events/static/image/events.png",
"contributors": ["benarc"]
}
diff --git a/lnbits/extensions/events/static/image/events.png b/lnbits/extensions/events/static/image/events.png
new file mode 100644
index 00000000..65c1bddb
Binary files /dev/null and b/lnbits/extensions/events/static/image/events.png differ
diff --git a/lnbits/extensions/events/tasks.py b/lnbits/extensions/events/tasks.py
index d29215bf..5eae7373 100644
--- a/lnbits/extensions/events/tasks.py
+++ b/lnbits/extensions/events/tasks.py
@@ -1,15 +1,6 @@
import asyncio
-import json
-from http import HTTPStatus
-from urllib.parse import urlparse
-import httpx
-from fastapi import HTTPException
-from loguru import logger
-
-from lnbits import bolt11
from lnbits.core.models import Payment
-from lnbits.core.services import pay_invoice
from lnbits.extensions.events.models import CreateTicket
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
@@ -29,11 +20,17 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
# (avoid loops)
if (
- "events" == payment.extra.get("tag")
+ payment.extra
+ and "events" == payment.extra.get("tag")
and payment.extra.get("name")
and payment.extra.get("email")
):
- CreateTicket.name = str(payment.extra.get("name"))
- CreateTicket.email = str(payment.extra.get("email"))
- await api_ticket_send_ticket(payment.memo, payment.payment_hash, CreateTicket)
+ await api_ticket_send_ticket(
+ payment.memo,
+ payment.payment_hash,
+ CreateTicket(
+ name=str(payment.extra.get("name")),
+ email=str(payment.extra.get("email")),
+ ),
+ )
return
diff --git a/lnbits/extensions/events/views.py b/lnbits/extensions/events/views.py
index 16875e81..4ed56795 100644
--- a/lnbits/extensions/events/views.py
+++ b/lnbits/extensions/events/views.py
@@ -1,8 +1,7 @@
from datetime import date, datetime
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/events/views_api.py b/lnbits/extensions/events/views_api.py
index 668e7f77..4ed3932f 100644
--- a/lnbits/extensions/events/views_api.py
+++ b/lnbits/extensions/events/views_api.py
@@ -1,10 +1,7 @@
from http import HTTPStatus
-from fastapi.param_functions import Query
-from fastapi.params import Depends
-from loguru import logger
+from fastapi import Depends, Query
from starlette.exceptions import HTTPException
-from starlette.requests import Request
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
@@ -38,7 +35,8 @@ async def api_events(
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 [event.dict() for event in await get_events(wallet_ids)]
@@ -92,7 +90,8 @@ async def api_tickets(
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 [ticket.dict() for ticket in await get_tickets(wallet_ids)]
@@ -119,26 +118,32 @@ async def api_ticket_make_ticket(event_id, name, email):
@events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
async def api_ticket_send_ticket(event_id, payment_hash, data: CreateTicket):
event = await get_event(event_id)
- try:
- status = await api_payment(payment_hash)
- if status["paid"]:
- ticket = await create_ticket(
- payment_hash=payment_hash,
- wallet=event.wallet,
- event=event_id,
- name=data.name,
- email=data.email,
+ if not event:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail=f"Event could not be fetched.",
+ )
+
+ status = await api_payment(payment_hash)
+ if status["paid"]:
+
+ exists = await get_ticket(payment_hash)
+ if exists:
+ return {"paid": True, "ticket_id": exists.id}
+
+ ticket = await create_ticket(
+ payment_hash=payment_hash,
+ wallet=event.wallet,
+ event=event_id,
+ name=data.name,
+ email=data.email,
+ )
+ if not ticket:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail=f"Event could not be fetched.",
)
-
- if not ticket:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND,
- detail=f"Event could not be fetched.",
- )
-
- return {"paid": True, "ticket_id": ticket.id}
- except Exception:
- raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid")
+ return {"paid": True, "ticket_id": ticket.id}
return {"paid": False}
diff --git a/lnbits/extensions/example/example.config.json b/lnbits/extensions/example/example.config.json
index b8eec193..14848443 100644
--- a/lnbits/extensions/example/example.config.json
+++ b/lnbits/extensions/example/example.config.json
@@ -1,6 +1,6 @@
{
"name": "Build your own!!",
"short_description": "Join us, make an extension",
- "icon": "info",
+ "tile": "/cashu/static/image/tile.png",
"contributors": ["github_username"]
}
diff --git a/lnbits/extensions/example/views.py b/lnbits/extensions/example/views.py
index 29b257f4..b69b5194 100644
--- a/lnbits/extensions/example/views.py
+++ b/lnbits/extensions/example/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
@@ -14,7 +13,7 @@ templates = Jinja2Templates(directory="templates")
@example_ext.get("/", response_class=HTMLResponse)
async def index(
request: Request,
- user: User = Depends(check_user_exists), # type: ignore
+ user: User = Depends(check_user_exists),
):
return example_renderer().TemplateResponse(
"example/index.html", {"request": request, "user": user.dict()}
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/config.json b/lnbits/extensions/gerty/config.json
index a36437be..85cb4d42 100644
--- a/lnbits/extensions/gerty/config.json
+++ b/lnbits/extensions/gerty/config.json
@@ -1,6 +1,6 @@
{
"name": "Gerty",
"short_description": "Desktop bitcoin Assistant",
- "icon": "sentiment_satisfied",
+ "tile": "/gerty/static/gerty.png",
"contributors": ["arcbtc", "blackcoffeebtc"]
}
diff --git a/lnbits/extensions/gerty/crud.py b/lnbits/extensions/gerty/crud.py
index 1164b6ee..5475139c 100644
--- a/lnbits/extensions/gerty/crud.py
+++ b/lnbits/extensions/gerty/crud.py
@@ -50,11 +50,12 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
return gerty
-async def update_gerty(gerty_id: str, **kwargs) -> Gerty:
+async def update_gerty(gerty_id: str, **kwargs) -> Optional[Gerty]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE gerty.gertys SET {q} WHERE id = ?", (*kwargs.values(), gerty_id)
)
+
return await get_gerty(gerty_id)
@@ -82,7 +83,7 @@ async def delete_gerty(gerty_id: str) -> None:
#############MEMPOOL###########
-async def get_mempool_info(endPoint: str, gerty) -> Optional[Mempool]:
+async def get_mempool_info(endPoint: str, gerty) -> dict:
logger.debug(endPoint)
endpoints = MempoolEndpoint()
url = ""
@@ -116,7 +117,7 @@ async def get_mempool_info(endPoint: str, gerty) -> Optional[Mempool]:
mempool_id,
json.dumps(response.json()),
endPoint,
- int(time.time()),
+ time.time(),
gerty.mempool_endpoint,
),
)
@@ -128,7 +129,7 @@ async def get_mempool_info(endPoint: str, gerty) -> Optional[Mempool]:
"UPDATE gerty.mempool SET data = ?, time = ? WHERE endpoint = ? AND mempool_endpoint = ?",
(
json.dumps(response.json()),
- int(time.time()),
+ time.time(),
endPoint,
gerty.mempool_endpoint,
),
diff --git a/lnbits/extensions/gerty/helpers.py b/lnbits/extensions/gerty/helpers.py
index 65c69073..3e48c576 100644
--- a/lnbits/extensions/gerty/helpers.py
+++ b/lnbits/extensions/gerty/helpers.py
@@ -3,15 +3,16 @@ import os
import random
import textwrap
from datetime import datetime, timedelta
+from typing import List
import httpx
from loguru import logger
-from lnbits.core.crud import get_user, get_wallet_for_key
+from lnbits.core.crud import get_wallet_for_key
from lnbits.settings import settings
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat
-from .crud import get_gerty, get_mempool_info
+from .crud import get_mempool_info
from .number_prefixer import *
@@ -24,8 +25,8 @@ def get_percent_difference(current, previous, precision=3):
def get_text_item_dict(
text: str,
font_size: int,
- x_pos: int = None,
- y_pos: int = None,
+ x_pos: int = -1,
+ y_pos: int = -1,
gerty_type: str = "Gerty",
):
# Get line size by font size
@@ -63,13 +64,41 @@ def get_text_item_dict(
# logger.debug('multilineText')
# logger.debug(multilineText)
- text = {"value": multilineText, "size": font_size}
- if x_pos is None and y_pos is None:
- text["position"] = "center"
+ data_text = {"value": multilineText, "size": font_size}
+ if x_pos == -1 and y_pos == -1:
+ data_text["position"] = "center"
else:
- text["x"] = x_pos
- text["y"] = y_pos
- return text
+ data_text["x"] = x_pos if x_pos > 0 else 0
+ data_text["y"] = y_pos if x_pos > 0 else 0
+ return data_text
+
+
+def get_date_suffix(dayNumber):
+ if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30:
+ return "th"
+ else:
+ return ["st", "nd", "rd"][dayNumber % 10 - 1]
+
+
+def get_time_remaining(seconds, granularity=2):
+ intervals = (
+ # ('weeks', 604800), # 60 * 60 * 24 * 7
+ ("days", 86400), # 60 * 60 * 24
+ ("hours", 3600), # 60 * 60
+ ("minutes", 60),
+ ("seconds", 1),
+ )
+
+ result = []
+
+ for name, count in intervals:
+ value = seconds // count
+ if value:
+ seconds -= value * count
+ if value == 1:
+ name = name.rstrip("s")
+ result.append("{} {}".format(round(value), name))
+ return ", ".join(result[:granularity])
# format a number for nice display output
@@ -293,8 +322,7 @@ def get_next_update_time(sleep_time_seconds: int = 0, utc_offset: int = 0):
def gerty_should_sleep(utc_offset: int = 0):
utc_now = datetime.utcnow()
local_time = utc_now + timedelta(hours=utc_offset)
- hours = local_time.strftime("%H")
- hours = int(hours)
+ hours = int(local_time.strftime("%H"))
if hours >= 22 and hours <= 23:
return True
else:
@@ -352,23 +380,17 @@ async def get_mining_stat(stat_slug: str, gerty):
async def api_get_mining_stat(stat_slug: str, gerty):
- stat = ""
+ stat = {}
if stat_slug == "mining_current_hash_rate":
- async with httpx.AsyncClient() as client:
- r = await get_mempool_info("hashrate_1m", gerty)
- data = r
- stat = {}
- stat["current"] = data["currentHashrate"]
- stat["1w"] = data["hashrates"][len(data["hashrates"]) - 7]["avgHashrate"]
+ r = await get_mempool_info("hashrate_1m", gerty)
+ data = r
+ stat["current"] = data["currentHashrate"]
+ stat["1w"] = data["hashrates"][len(data["hashrates"]) - 7]["avgHashrate"]
elif stat_slug == "mining_current_difficulty":
- async with httpx.AsyncClient() as client:
- r = await get_mempool_info("hashrate_1m", gerty)
- data = r
- stat = {}
- stat["current"] = data["currentDifficulty"]
- stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2][
- "difficulty"
- ]
+ r = await get_mempool_info("hashrate_1m", gerty)
+ data = r
+ stat["current"] = data["currentDifficulty"]
+ stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2]["difficulty"]
return stat
@@ -384,7 +406,7 @@ async def get_satoshi():
quote = satoshiQuotes[random.randint(0, len(satoshiQuotes) - 1)]
# logger.debug(quote.text)
if len(quote["text"]) > maxQuoteLength:
- logger.debug("Quote is too long, getting another")
+ logger.trace("Quote is too long, getting another")
return await get_satoshi()
else:
return quote
@@ -399,15 +421,16 @@ def get_screen_slug_by_index(index: int, screens_list):
# Get a list of text items for the screen number
-async def get_screen_data(screen_num: int, screens_list: dict, gerty):
+async def get_screen_data(screen_num: int, screens_list: list, gerty):
screen_slug = get_screen_slug_by_index(screen_num, screens_list)
# first get the relevant slug from the display_preferences
- areas = []
+ areas: List = []
title = ""
if screen_slug == "dashboard":
title = gerty.name
areas = await get_dashboard(gerty)
+
if screen_slug == "lnbits_wallets_balance":
wallets = await get_lnbits_wallet_balances(gerty)
@@ -505,10 +528,10 @@ async def get_screen_data(screen_num: int, screens_list: dict, gerty):
title = "Lightning Network"
areas = await get_lightning_stats(gerty)
- data = {}
- data["title"] = title
- data["areas"] = areas
-
+ data = {
+ "title": title,
+ "areas": areas,
+ }
return data
@@ -570,7 +593,7 @@ async def get_dashboard(gerty):
text = []
text.append(
get_text_item_dict(
- text=await get_time_remaining_next_difficulty_adjustment(gerty),
+ text=await get_time_remaining_next_difficulty_adjustment(gerty) or "0",
font_size=15,
gerty_type=gerty.type,
)
@@ -602,7 +625,7 @@ async def get_lnbits_wallet_balances(gerty):
return wallets
-async def get_placeholder_text():
+async def get_placeholder_text(gerty):
return [
get_text_item_dict(
text="Some placeholder text",
@@ -810,14 +833,14 @@ async def get_time_remaining_next_difficulty_adjustment(gerty):
r = await get_mempool_info("difficulty_adjustment", gerty)
stat = r["remainingTime"]
time = get_time_remaining(stat / 1000, 3)
- return time
+ return time
async def get_mempool_stat(stat_slug: str, gerty):
text = []
if isinstance(gerty.mempool_endpoint, str):
if stat_slug == "mempool_tx_count":
- r = get_mempool_info("mempool", gerty)
+ r = await get_mempool_info("mempool", gerty)
if stat_slug == "mempool_tx_count":
stat = round(r["count"])
text.append(
@@ -921,31 +944,3 @@ async def get_mempool_stat(stat_slug: str, gerty):
)
)
return text
-
-
-def get_date_suffix(dayNumber):
- if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30:
- return "th"
- else:
- return ["st", "nd", "rd"][dayNumber % 10 - 1]
-
-
-def get_time_remaining(seconds, granularity=2):
- intervals = (
- # ('weeks', 604800), # 60 * 60 * 24 * 7
- ("days", 86400), # 60 * 60 * 24
- ("hours", 3600), # 60 * 60
- ("minutes", 60),
- ("seconds", 1),
- )
-
- result = []
-
- for name, count in intervals:
- value = seconds // count
- if value:
- seconds -= value * count
- if value == 1:
- name = name.rstrip("s")
- result.append("{} {}".format(round(value), name))
- return ", ".join(result[:granularity])
diff --git a/lnbits/extensions/gerty/models.py b/lnbits/extensions/gerty/models.py
index 9ff29bda..cb19c2bc 100644
--- a/lnbits/extensions/gerty/models.py
+++ b/lnbits/extensions/gerty/models.py
@@ -1,5 +1,4 @@
from sqlite3 import Row
-from typing import Optional
from fastapi import Query
from pydantic import BaseModel
diff --git a/lnbits/extensions/gerty/static/gerty.png b/lnbits/extensions/gerty/static/gerty.png
new file mode 100644
index 00000000..8633442a
Binary files /dev/null and b/lnbits/extensions/gerty/static/gerty.png differ
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/hivemind/__init__.py b/lnbits/extensions/hivemind/__init__.py
index 3aebd05f..afa677ce 100644
--- a/lnbits/extensions/hivemind/__init__.py
+++ b/lnbits/extensions/hivemind/__init__.py
@@ -1,4 +1,5 @@
from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -12,4 +13,12 @@ def hivemind_renderer():
return template_renderer(["lnbits/extensions/hivemind/templates"])
+hivemind_static_files = [
+ {
+ "path": "/hivemind/static",
+ "app": StaticFiles(packages=[("lnbits", "extensions/hivemind/static")]),
+ "name": "hivemind_static",
+ }
+]
+
from .views import * # noqa
diff --git a/lnbits/extensions/hivemind/config.json b/lnbits/extensions/hivemind/config.json
index a5469b15..2a146fbe 100644
--- a/lnbits/extensions/hivemind/config.json
+++ b/lnbits/extensions/hivemind/config.json
@@ -1,6 +1,6 @@
{
"name": "Hivemind",
"short_description": "Make cheap talk expensive!",
- "icon": "batch_prediction",
+ "tile": "/hivemind/static/image/hivemind.png",
"contributors": ["fiatjaf"]
}
diff --git a/lnbits/extensions/hivemind/static/image/hivemind.png b/lnbits/extensions/hivemind/static/image/hivemind.png
new file mode 100644
index 00000000..6848e3ca
Binary files /dev/null and b/lnbits/extensions/hivemind/static/image/hivemind.png differ
diff --git a/lnbits/extensions/invoices/config.json b/lnbits/extensions/invoices/config.json
index 0811e0ef..a604fec5 100644
--- a/lnbits/extensions/invoices/config.json
+++ b/lnbits/extensions/invoices/config.json
@@ -1,6 +1,6 @@
{
"name": "Invoices",
"short_description": "Create invoices for your clients.",
- "icon": "request_quote",
+ "tile": "/invoices/static/image/invoices.png",
"contributors": ["leesalminen"]
}
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/static/image/invoices.png b/lnbits/extensions/invoices/static/image/invoices.png
new file mode 100644
index 00000000..823f9dee
Binary files /dev/null and b/lnbits/extensions/invoices/static/image/invoices.png differ
diff --git a/lnbits/extensions/invoices/tasks.py b/lnbits/extensions/invoices/tasks.py
index 61bcb7b4..ae76b9e3 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():
@@ -26,17 +25,22 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
+ if not payment.extra:
+ return
+
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 +49,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/config.json b/lnbits/extensions/jukebox/config.json
index 6b57bec4..283ab82b 100644
--- a/lnbits/extensions/jukebox/config.json
+++ b/lnbits/extensions/jukebox/config.json
@@ -1,6 +1,6 @@
{
"name": "Spotify Jukebox",
"short_description": "Spotify jukebox middleware",
- "icon": "radio",
+ "tile": "/jukebox/static/image/jukebox.png",
"contributors": ["benarc"]
}
diff --git a/lnbits/extensions/jukebox/static/image/jukebox.png b/lnbits/extensions/jukebox/static/image/jukebox.png
new file mode 100644
index 00000000..bf78edf9
Binary files /dev/null and b/lnbits/extensions/jukebox/static/image/jukebox.png differ
diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py
index 28359a9a..2220da08 100644
--- a/lnbits/extensions/jukebox/views.py
+++ b/lnbits/extensions/jukebox/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
@@ -17,9 +16,7 @@ templates = Jinja2Templates(directory="templates")
@jukebox_ext.get("/", response_class=HTMLResponse)
-async def index(
- request: Request, user: User = Depends(check_user_exists) # type: ignore
-):
+async def index(request: Request, user: User = Depends(check_user_exists)):
return jukebox_renderer().TemplateResponse(
"jukebox/index.html", {"request": request, "user": user.dict()}
)
diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py
index 5cf1a83b..1847ca50 100644
--- a/lnbits/extensions/jukebox/views_api.py
+++ b/lnbits/extensions/jukebox/views_api.py
@@ -3,10 +3,9 @@ import json
from http import HTTPStatus
import httpx
-from fastapi.param_functions import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query
from starlette.exceptions import HTTPException
-from starlette.responses import HTMLResponse # type: ignore
+from starlette.responses import HTMLResponse
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
@@ -28,7 +27,7 @@ from .models import CreateJukeboxPayment, CreateJukeLinkData
@jukebox_ext.get("/api/v1/jukebox")
async def api_get_jukeboxs(
- wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
+ wallet: WalletTypeInfo = Depends(require_admin_key),
):
wallet_user = wallet.wallet.user
diff --git a/lnbits/extensions/livestream/config.json b/lnbits/extensions/livestream/config.json
index 12ba6b79..d2674e70 100644
--- a/lnbits/extensions/livestream/config.json
+++ b/lnbits/extensions/livestream/config.json
@@ -1,7 +1,7 @@
{
"name": "DJ Livestream",
"short_description": "Sell tracks and split revenue (lnurl-pay)",
- "icon": "speaker",
+ "tile": "/livestream/static/image/livestream.png",
"contributors": [
"fiatjaf",
"cryptograffiti"
diff --git a/lnbits/extensions/livestream/lnurl.py b/lnbits/extensions/livestream/lnurl.py
index 349bb280..89e431e5 100644
--- a/lnbits/extensions/livestream/lnurl.py
+++ b/lnbits/extensions/livestream/lnurl.py
@@ -90,7 +90,7 @@ async def lnurl_callback(
wallet_id=ls.wallet,
amount=int(amount_received / 1000),
memo=await track.fullname(),
- unhashed_description=(await track.lnurlpay_metadata()).encode("utf-8"),
+ unhashed_description=(await track.lnurlpay_metadata()).encode(),
extra={"tag": "livestream", "track": track.id, "comment": comment},
)
diff --git a/lnbits/extensions/livestream/models.py b/lnbits/extensions/livestream/models.py
index dd057c0c..0034f4a7 100644
--- a/lnbits/extensions/livestream/models.py
+++ b/lnbits/extensions/livestream/models.py
@@ -1,12 +1,12 @@
import json
from typing import Optional
-from fastapi.params import Query
+from fastapi import Query
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 pydantic.main import BaseModel
+from pydantic import BaseModel
from starlette.requests import Request
diff --git a/lnbits/extensions/livestream/static/image/livestream.png b/lnbits/extensions/livestream/static/image/livestream.png
new file mode 100644
index 00000000..7d53d566
Binary files /dev/null and b/lnbits/extensions/livestream/static/image/livestream.png differ
diff --git a/lnbits/extensions/lnaddress/__init__.py b/lnbits/extensions/lnaddress/__init__.py
index 6e8afa37..a507a1dc 100644
--- a/lnbits/extensions/lnaddress/__init__.py
+++ b/lnbits/extensions/lnaddress/__init__.py
@@ -1,6 +1,7 @@
import asyncio
from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -10,6 +11,14 @@ db = Database("ext_lnaddress")
lnaddress_ext: APIRouter = APIRouter(prefix="/lnaddress", tags=["lnaddress"])
+lnaddress_static_files = [
+ {
+ "path": "/lnaddress/static",
+ "app": StaticFiles(directory="lnbits/extensions/lnaddress/static"),
+ "name": "lnaddress_static",
+ }
+]
+
def lnaddress_renderer():
return template_renderer(["lnbits/extensions/lnaddress/templates"])
diff --git a/lnbits/extensions/lnaddress/config.json b/lnbits/extensions/lnaddress/config.json
index f9946f35..5eaa4948 100644
--- a/lnbits/extensions/lnaddress/config.json
+++ b/lnbits/extensions/lnaddress/config.json
@@ -1,6 +1,6 @@
{
"name": "Lightning Address",
"short_description": "Sell LN addresses for your domain",
- "icon": "alternate_email",
+ "tile": "/lnaddress/static/image/lnaddress.png",
"contributors": ["talvasconcelos"]
}
diff --git a/lnbits/extensions/lnaddress/lnurl.py b/lnbits/extensions/lnaddress/lnurl.py
index 49fd30e0..6f799439 100644
--- a/lnbits/extensions/lnaddress/lnurl.py
+++ b/lnbits/extensions/lnaddress/lnurl.py
@@ -72,7 +72,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
"amount": int(amount_received / 1000),
"description_hash": (
await address.lnurlpay_metadata(domain=domain.domain)
- ).encode("utf-8"),
+ ).encode(),
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
},
timeout=40,
diff --git a/lnbits/extensions/lnaddress/static/image/lnaddress.png b/lnbits/extensions/lnaddress/static/image/lnaddress.png
new file mode 100644
index 00000000..c94dedbc
Binary files /dev/null and b/lnbits/extensions/lnaddress/static/image/lnaddress.png differ
diff --git a/lnbits/extensions/lndhub/__init__.py b/lnbits/extensions/lndhub/__init__.py
index 5980ab0d..1d1effcf 100644
--- a/lnbits/extensions/lndhub/__init__.py
+++ b/lnbits/extensions/lndhub/__init__.py
@@ -1,4 +1,5 @@
from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -7,6 +8,14 @@ db = Database("ext_lndhub")
lndhub_ext: APIRouter = APIRouter(prefix="/lndhub", tags=["lndhub"])
+lndhub_static_files = [
+ {
+ "path": "/lndhub/static",
+ "app": StaticFiles(directory="lnbits/extensions/lndhub/static"),
+ "name": "lndhub_static",
+ }
+]
+
def lndhub_renderer():
return template_renderer(["lnbits/extensions/lndhub/templates"])
diff --git a/lnbits/extensions/lndhub/config.json b/lnbits/extensions/lndhub/config.json
index 6285ff80..30a2ce59 100644
--- a/lnbits/extensions/lndhub/config.json
+++ b/lnbits/extensions/lndhub/config.json
@@ -1,6 +1,6 @@
{
"name": "LndHub",
"short_description": "Access lnbits from BlueWallet or Zeus",
- "icon": "navigation",
+ "tile": "/lndhub/static/image/lndhub.png",
"contributors": ["fiatjaf"]
}
diff --git a/lnbits/extensions/lndhub/decorators.py b/lnbits/extensions/lndhub/decorators.py
index 4698e9b9..fcadce27 100644
--- a/lnbits/extensions/lndhub/decorators.py
+++ b/lnbits/extensions/lndhub/decorators.py
@@ -23,7 +23,7 @@ async def check_wallet(
)
t = api_key_header_auth.split(" ")[1]
- _, token = b64decode(t).decode("utf-8").split(":")
+ _, token = b64decode(t).decode().split(":")
return await get_key_type(r, api_key_header=token)
diff --git a/lnbits/extensions/lndhub/static/image/lndhub.png b/lnbits/extensions/lndhub/static/image/lndhub.png
new file mode 100644
index 00000000..f5e95a6e
Binary files /dev/null and b/lnbits/extensions/lndhub/static/image/lndhub.png differ
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 4da33a3e..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
@@ -35,9 +33,9 @@ async def lndhub_auth(data: AuthData):
token = (
data.refresh_token
if data.refresh_token
- else urlsafe_b64encode(
- (data.login + ":" + data.password).encode("utf-8")
- ).decode("ascii")
+ else urlsafe_b64encode((data.login + ":" + data.password).encode()).decode(
+ "ascii"
+ )
)
return {"refresh_token": token, "access_token": token}
@@ -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/__init__.py b/lnbits/extensions/lnticket/__init__.py
index cb793f4d..3c52fd2a 100644
--- a/lnbits/extensions/lnticket/__init__.py
+++ b/lnbits/extensions/lnticket/__init__.py
@@ -2,6 +2,7 @@ import asyncio
import json
from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -11,6 +12,14 @@ db = Database("ext_lnticket")
lnticket_ext: APIRouter = APIRouter(prefix="/lnticket", tags=["LNTicket"])
+lnticket_static_files = [
+ {
+ "path": "/lnticket/static",
+ "app": StaticFiles(directory="lnbits/extensions/lnticket/static"),
+ "name": "lnticket_static",
+ }
+]
+
def lnticket_renderer():
return template_renderer(["lnbits/extensions/lnticket/templates"])
diff --git a/lnbits/extensions/lnticket/config.json b/lnbits/extensions/lnticket/config.json
index 99581b8f..e8e55f2f 100644
--- a/lnbits/extensions/lnticket/config.json
+++ b/lnbits/extensions/lnticket/config.json
@@ -1,6 +1,6 @@
{
"name": "Support Tickets",
"short_description": "LN support ticket system",
- "icon": "contact_support",
+ "tile": "/lnticket/static/image/lntickets.png",
"contributors": ["benarc"]
}
diff --git a/lnbits/extensions/lnticket/static/image/lntickets.png b/lnbits/extensions/lnticket/static/image/lntickets.png
new file mode 100644
index 00000000..875b4154
Binary files /dev/null and b/lnbits/extensions/lnticket/static/image/lntickets.png differ
diff --git a/lnbits/extensions/lnticket/tasks.py b/lnbits/extensions/lnticket/tasks.py
index 746ebea9..e84a7512 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 payment.extra.get("tag") != "lnticket":
+ if not payment.extra or payment.extra.get("tag") != "lnticket":
# not a lnticket invoice
return
diff --git a/lnbits/extensions/lnticket/views.py b/lnbits/extensions/lnticket/views.py
index 9c11c79e..9bb1d9b3 100644
--- a/lnbits/extensions/lnticket/views.py
+++ b/lnbits/extensions/lnticket/views.py
@@ -33,6 +33,7 @@ async def display(request: Request, form_id):
)
wallet = await get_wallet(form.wallet)
+ assert wallet
return lnticket_renderer().TemplateResponse(
"lnticket/display.html",
diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py
index cf6145b3..35d6eaff 100644
--- a/lnbits/extensions/lnticket/views_api.py
+++ b/lnbits/extensions/lnticket/views_api.py
@@ -1,8 +1,7 @@
import re
from http import HTTPStatus
-from fastapi import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
@@ -35,7 +34,8 @@ async def api_forms_get(
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 [form.dict() for form in await get_forms(wallet_ids)]
@@ -91,7 +91,8 @@ async def api_tickets(
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 [form.dict() for form in await get_tickets(wallet_ids)]
diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py
index d2010c44..8ef39145 100644
--- a/lnbits/extensions/lnurldevice/__init__.py
+++ b/lnbits/extensions/lnurldevice/__init__.py
@@ -1,6 +1,7 @@
import asyncio
from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -10,6 +11,14 @@ db = Database("ext_lnurldevice")
lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice"])
+lnurldevice_static_files = [
+ {
+ "path": "/lnurldevice/static",
+ "app": StaticFiles(directory="lnbits/extensions/lnurldevice/static"),
+ "name": "lnurldevice_static",
+ }
+]
+
def lnurldevice_renderer():
return template_renderer(["lnbits/extensions/lnurldevice/templates"])
diff --git a/lnbits/extensions/lnurldevice/config.json b/lnbits/extensions/lnurldevice/config.json
index 66b4891a..0712d729 100644
--- a/lnbits/extensions/lnurldevice/config.json
+++ b/lnbits/extensions/lnurldevice/config.json
@@ -1,6 +1,6 @@
{
"name": "LNURLDevice",
"short_description": "For offline LNURL devices",
- "icon": "point_of_sale",
+ "tile": "/lnurldevice/static/image/lnurldevice.png",
"contributors": ["arcbtc"]
}
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/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py
index dd8dcb08..e0384159 100644
--- a/lnbits/extensions/lnurldevice/lnurl.py
+++ b/lnbits/extensions/lnurldevice/lnurl.py
@@ -246,7 +246,7 @@ async def lnurl_callback(
wallet_id=device.wallet,
amount=int(lnurldevicepayment.sats / 1000),
memo=device.id + " PIN " + str(lnurldevicepayment.pin),
- unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
+ unhashed_description=device.lnurlpay_metadata.encode(),
extra={
"tag": "Switch",
"pin": str(lnurldevicepayment.pin),
@@ -267,7 +267,7 @@ async def lnurl_callback(
wallet_id=device.wallet,
amount=int(lnurldevicepayment.sats / 1000),
memo=device.title,
- unhashed_description=device.lnurlpay_metadata.encode("utf-8"),
+ unhashed_description=device.lnurlpay_metadata.encode(),
extra={"tag": "PoS"},
)
lnurldevicepayment = await update_lnurldevicepayment(
diff --git a/lnbits/extensions/lnurldevice/static/image/lnurldevice.png b/lnbits/extensions/lnurldevice/static/image/lnurldevice.png
new file mode 100644
index 00000000..3a5304f6
Binary files /dev/null and b/lnbits/extensions/lnurldevice/static/image/lnurldevice.png differ
diff --git a/lnbits/extensions/lnurlp/config.json b/lnbits/extensions/lnurlp/config.json
index 294afe73..d3e046de 100644
--- a/lnbits/extensions/lnurlp/config.json
+++ b/lnbits/extensions/lnurlp/config.json
@@ -1,7 +1,7 @@
{
"name": "LNURLp",
"short_description": "Make reusable LNURL pay links",
- "icon": "receipt",
+ "tile": "/lnurlp/static/image/lnurl-pay.png",
"contributors": [
"arcbtc",
"eillarra",
diff --git a/lnbits/extensions/lnurlp/crud.py b/lnbits/extensions/lnurlp/crud.py
index 86300419..d02ae80e 100644
--- a/lnbits/extensions/lnurlp/crud.py
+++ b/lnbits/extensions/lnurlp/crud.py
@@ -1,18 +1,19 @@
from typing import List, Optional, Union
-from lnbits.helpers import urlsafe_short_hash
+from lnbits.db import SQLITE
from . import db
from .models import CreatePayLinkData, PayLink
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
- link_id = urlsafe_short_hash()
- result = await db.execute(
+ returning = "" if db.type == SQLITE else "RETURNING ID"
+ method = db.execute if db.type == SQLITE else db.fetchone
+
+ result = await (method)(
f"""
INSERT INTO lnurlp.pay_links (
- id,
wallet,
description,
min,
@@ -28,11 +29,10 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
currency,
fiat_base_multiplier
)
- VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
{returning}
""",
(
- link_id,
wallet_id,
data.description,
data.min,
@@ -47,6 +47,10 @@ 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"
diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py
index dea8037d..3f7df926 100644
--- a/lnbits/extensions/lnurlp/lnurl.py
+++ b/lnbits/extensions/lnurlp/lnurl.py
@@ -87,7 +87,7 @@ async def api_lnurl_callback(request: Request, link_id):
wallet_id=link.wallet,
amount=int(amount_received / 1000),
memo=link.description,
- unhashed_description=link.lnurlpay_metadata.encode("utf-8"),
+ unhashed_description=link.lnurlpay_metadata.encode(),
extra={
"tag": "lnurlp",
"link": link.id,
diff --git a/lnbits/extensions/lnurlp/migrations.py b/lnbits/extensions/lnurlp/migrations.py
index 44df5ba9..c4edd3aa 100644
--- a/lnbits/extensions/lnurlp/migrations.py
+++ b/lnbits/extensions/lnurlp/migrations.py
@@ -68,76 +68,3 @@ 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 42ea2926..2cb4d0ab 100644
--- a/lnbits/extensions/lnurlp/models.py
+++ b/lnbits/extensions/lnurlp/models.py
@@ -26,7 +26,7 @@ class CreatePayLinkData(BaseModel):
class PayLink(BaseModel):
- id: str
+ id: int
wallet: str
description: str
min: float
diff --git a/lnbits/extensions/lnurlp/static/image/lnurl-pay.png b/lnbits/extensions/lnurlp/static/image/lnurl-pay.png
new file mode 100644
index 00000000..36af81a7
Binary files /dev/null and b/lnbits/extensions/lnurlp/static/image/lnurl-pay.png differ
diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py
index b8da5e43..2b574d42 100644
--- a/lnbits/extensions/lnurlp/tasks.py
+++ b/lnbits/extensions/lnurlp/tasks.py
@@ -4,7 +4,6 @@ import json
import httpx
from loguru import logger
-from lnbits.core import db as core_db
from lnbits.core.crud import update_payment_extra
from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
@@ -22,9 +21,8 @@ async def wait_for_paid_invoices():
await on_invoice_paid(payment)
-async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra.get("tag") != "lnurlp":
- # not an lnurlp invoice
+async def on_invoice_paid(payment: Payment):
+ if not payment.extra or payment.extra.get("tag") != "lnurlp":
return
if payment.extra.get("wh_status"):
@@ -35,22 +33,23 @@ async def on_invoice_paid(payment: Payment) -> None:
if pay_link and pay_link.webhook_url:
async with httpx.AsyncClient() as client:
try:
- kwargs = {
- "json": {
+ r: httpx.Response = await client.post(
+ pay_link.webhook_url,
+ json={
"payment_hash": payment.payment_hash,
"payment_request": payment.bolt11,
"amount": payment.amount,
"comment": payment.extra.get("comment"),
"lnurlp": pay_link.id,
+ "body": json.loads(pay_link.webhook_body)
+ if pay_link.webhook_body
+ else "",
},
- "timeout": 40,
- }
- if pay_link.webhook_body:
- kwargs["json"]["body"] = json.loads(pay_link.webhook_body)
- if pay_link.webhook_headers:
- kwargs["headers"] = json.loads(pay_link.webhook_headers)
-
- r: httpx.Response = await client.post(pay_link.webhook_url, **kwargs)
+ headers=json.loads(pay_link.webhook_headers)
+ if pay_link.webhook_headers
+ else None,
+ timeout=40,
+ )
await mark_webhook_sent(
payment.payment_hash,
r.status_code,
diff --git a/lnbits/extensions/lnurlp/views.py b/lnbits/extensions/lnurlp/views.py
index 4e9f487c..9bc78056 100644
--- a/lnbits/extensions/lnurlp/views.py
+++ b/lnbits/extensions/lnurlp/views.py
@@ -1,7 +1,6 @@
from http import HTTPStatus
-from fastapi import Request
-from fastapi.params import Depends
+from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
diff --git a/lnbits/extensions/lnurlp/views_api.py b/lnbits/extensions/lnurlp/views_api.py
index d5966bf6..0fa739b0 100644
--- a/lnbits/extensions/lnurlp/views_api.py
+++ b/lnbits/extensions/lnurlp/views_api.py
@@ -1,9 +1,7 @@
import json
from http import HTTPStatus
-from fastapi import Request
-from fastapi.param_functions import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query, Request
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from starlette.exceptions import HTTPException
@@ -36,7 +34,8 @@ async def api_links(
wallet_ids = [wallet.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
+ user = await get_user(wallet.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
try:
return [
@@ -137,6 +136,7 @@ async def api_link_create_or_update(
link = await update_pay_link(**data.dict(), link_id=link_id)
else:
link = await create_pay_link(data, wallet_id=wallet.wallet.id)
+ assert link
return {**link.dict(), "lnurl": link.lnurl(request)}
diff --git a/lnbits/extensions/lnurlpayout/README.md b/lnbits/extensions/lnurlpayout/README.md
deleted file mode 100644
index ddf209fe..00000000
--- a/lnbits/extensions/lnurlpayout/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# LNURLPayOut
-
-## Auto-dump a wallets funds to an LNURLpay
diff --git a/lnbits/extensions/lnurlpayout/__init__.py b/lnbits/extensions/lnurlpayout/__init__.py
deleted file mode 100644
index 9962290c..00000000
--- a/lnbits/extensions/lnurlpayout/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import asyncio
-
-from fastapi import APIRouter
-
-from lnbits.db import Database
-from lnbits.helpers import template_renderer
-from lnbits.tasks import catch_everything_and_restart
-
-db = Database("ext_lnurlpayout")
-
-lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout"])
-
-
-def lnurlpayout_renderer():
- return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
-
-
-from .tasks import wait_for_paid_invoices
-from .views import * # noqa
-from .views_api import * # noqa
-
-
-def lnurlpayout_start():
- loop = asyncio.get_event_loop()
- loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
diff --git a/lnbits/extensions/lnurlpayout/config.json.example b/lnbits/extensions/lnurlpayout/config.json.example
deleted file mode 100644
index b4160d7b..00000000
--- a/lnbits/extensions/lnurlpayout/config.json.example
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "LNURLPayout",
- "short_description": "Autodump wallet funds to LNURLpay",
- "icon": "exit_to_app",
- "contributors": ["arcbtc","talvasconcelos"]
-}
diff --git a/lnbits/extensions/lnurlpayout/crud.py b/lnbits/extensions/lnurlpayout/crud.py
deleted file mode 100644
index 0f9f98ac..00000000
--- a/lnbits/extensions/lnurlpayout/crud.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from typing import List, Optional, Union
-
-from lnbits.helpers import urlsafe_short_hash
-
-from . import db
-from .models import CreateLnurlPayoutData, lnurlpayout
-
-
-async def create_lnurlpayout(
- wallet_id: str, admin_key: str, data: CreateLnurlPayoutData
-) -> lnurlpayout:
- lnurlpayout_id = urlsafe_short_hash()
- await db.execute(
- """
- INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (
- lnurlpayout_id,
- data.title,
- wallet_id,
- admin_key,
- data.lnurlpay,
- data.threshold,
- ),
- )
-
- lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
- assert lnurlpayout, "Newly created lnurlpayout couldn't be retrieved"
- return lnurlpayout
-
-
-async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]:
- row = await db.fetchone(
- "SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
- )
- return lnurlpayout(**row) if row else None
-
-
-async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]:
- row = await db.fetchone(
- "SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,)
- )
- return lnurlpayout(**row) if row else None
-
-
-async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
- if isinstance(wallet_ids, str):
- wallet_ids = [wallet_ids]
-
- q = ",".join(["?"] * len(wallet_ids))
- rows = await db.fetchall(
- f"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet IN ({q})", (*wallet_ids,)
- )
-
- return [lnurlpayout(**row) if row else None for row in rows]
-
-
-async def delete_lnurlpayout(lnurlpayout_id: str) -> None:
- await db.execute(
- "DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
- )
diff --git a/lnbits/extensions/lnurlpayout/migrations.py b/lnbits/extensions/lnurlpayout/migrations.py
deleted file mode 100644
index 7a45e495..00000000
--- a/lnbits/extensions/lnurlpayout/migrations.py
+++ /dev/null
@@ -1,16 +0,0 @@
-async def m001_initial(db):
- """
- Initial lnurlpayouts table.
- """
- await db.execute(
- f"""
- CREATE TABLE lnurlpayout.lnurlpayouts (
- id TEXT PRIMARY KEY,
- title TEXT NOT NULL,
- wallet TEXT NOT NULL,
- admin_key TEXT NOT NULL,
- lnurlpay TEXT NOT NULL,
- threshold {db.big_int} NOT NULL
- );
- """
- )
diff --git a/lnbits/extensions/lnurlpayout/models.py b/lnbits/extensions/lnurlpayout/models.py
deleted file mode 100644
index fc8be575..00000000
--- a/lnbits/extensions/lnurlpayout/models.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from sqlite3 import Row
-
-from pydantic import BaseModel
-
-
-class CreateLnurlPayoutData(BaseModel):
- title: str
- lnurlpay: str
- threshold: int
-
-
-class lnurlpayout(BaseModel):
- id: str
- title: str
- wallet: str
- admin_key: str
- lnurlpay: str
- threshold: int
diff --git a/lnbits/extensions/lnurlpayout/tasks.py b/lnbits/extensions/lnurlpayout/tasks.py
deleted file mode 100644
index 71f299be..00000000
--- a/lnbits/extensions/lnurlpayout/tasks.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import asyncio
-from http import HTTPStatus
-
-import httpx
-from loguru import logger
-from starlette.exceptions import HTTPException
-
-from lnbits.core import db as core_db
-from lnbits.core.crud import get_wallet
-from lnbits.core.models import Payment
-from lnbits.core.services import pay_invoice
-from lnbits.core.views.api import api_payments_decode
-from lnbits.helpers import get_current_extension_name
-from lnbits.tasks import register_invoice_listener
-
-from .crud import get_lnurlpayout_from_wallet
-
-
-async def wait_for_paid_invoices():
- invoice_queue = asyncio.Queue()
- register_invoice_listener(invoice_queue, get_current_extension_name())
-
- while True:
- payment = await invoice_queue.get()
- await on_invoice_paid(payment)
-
-
-async def on_invoice_paid(payment: Payment) -> None:
- try:
- # Check its got a payout associated with it
- lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
- logger.debug("LNURLpayout", lnurlpayout_link)
- if lnurlpayout_link:
-
- # Check the wallet balance is more than the threshold
-
- wallet = await get_wallet(lnurlpayout_link.wallet)
- threshold = lnurlpayout_link.threshold + (lnurlpayout_link.threshold * 0.02)
-
- if wallet.balance < threshold:
- return
- # Get the invoice from the LNURL to pay
- async with httpx.AsyncClient() as client:
- try:
- url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
- if str(url["domain"])[0:4] != "http":
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
- )
-
- try:
- r = await client.get(str(url["domain"]), timeout=40)
- res = r.json()
- try:
- r = await client.get(
- res["callback"]
- + "?amount="
- + str(
- int((wallet.balance - wallet.balance * 0.02) * 1000)
- ),
- timeout=40,
- )
- res = r.json()
-
- if hasattr(res, "status") and res["status"] == "ERROR":
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail=res["reason"],
- )
- try:
- await pay_invoice(
- wallet_id=payment.wallet_id,
- payment_request=res["pr"],
- extra={"tag": "lnurlpayout"},
- )
- return
- except:
- pass
-
- except Exception as e:
- print("ERROR", str(e))
- return
- except (httpx.ConnectError, httpx.RequestError):
- return
- except Exception:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="Failed to save LNURLPayout",
- )
- except:
- return
diff --git a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html
deleted file mode 100644
index afe24c42..00000000
--- a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/_api_docs.html
+++ /dev/null
@@ -1,119 +0,0 @@
-
-
-
-
-
- GET
- /lnurlpayout/api/v1/lnurlpayouts
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- [<lnurlpayout_object>, ...]
- Curl example
- curl -X GET {{ request.base_url }}lnurlpayout/api/v1/lnurlpayouts -H
- "X-Api-Key: <invoice_key>"
-
-
-
-
-
-
-
- POST
- /lnurlpayout/api/v1/lnurlpayouts
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
- {"name": <string>, "currency": <string*ie USD*>}
-
- Returns 201 CREATED (application/json)
-
- {"currency": <string>, "id": <string>, "name":
- <string>, "wallet": <string>}
- Curl example
- curl -X POST {{ request.base_url }}lnurlpayout/api/v1/lnurlpayouts -d
- '{"name": <string>, "currency": <string>}' -H
- "Content-type: application/json" -H "X-Api-Key: <admin_key>"
-
-
-
-
-
-
-
-
- DELETE
- /lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id>
- Headers
- {"X-Api-Key": <admin_key>}
- Returns 204 NO CONTENT
-
- Curl example
- curl -X DELETE {{ request.base_url
- }}lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> -H
- "X-Api-Key: <admin_key>"
-
-
-
-
-
-
-
- GET
- /lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id>
- Headers
- {"X-Api-Key": <invoice_key>}
- Body (application/json)
-
- Returns 200 OK (application/json)
-
- [<lnurlpayout_object>, ...]
- Curl example
- curl -X GET {{ request.base_url
- }}lnurlpayout/api/v1/lnurlpayouts/<lnurlpayout_id> -H
- "X-Api-Key: <invoice_key>"
-
-
-
-
-
diff --git a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html b/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html
deleted file mode 100644
index 98230949..00000000
--- a/lnbits/extensions/lnurlpayout/templates/lnurlpayout/index.html
+++ /dev/null
@@ -1,271 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New LNURLPayout
-
-
-
-
-
-
-
-
LNURLPayout
-
-
- Export to CSV
-
-
-
- {% raw %}
-
-
-
- {{ col.label}}
-
-
-
-
-
-
-
-
- Click to copy LNURL{{
- col.value.substring(0, 40) }}...
-
- {{ col.value }} Sats
-
- {{ col.value.substring(0, 40) }}
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} LNURLPayout extension
-
-
-
-
-
- {% include "lnurlpayout/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Create LNURLPayout
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %}
diff --git a/lnbits/extensions/lnurlpayout/views.py b/lnbits/extensions/lnurlpayout/views.py
deleted file mode 100644
index 454a3332..00000000
--- a/lnbits/extensions/lnurlpayout/views.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Request
-from fastapi.params import Depends
-from fastapi.templating import Jinja2Templates
-from starlette.exceptions import HTTPException
-from starlette.responses import HTMLResponse
-
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-
-from . import lnurlpayout_ext, lnurlpayout_renderer
-from .crud import get_lnurlpayout
-
-templates = Jinja2Templates(directory="templates")
-
-
-@lnurlpayout_ext.get("/", response_class=HTMLResponse)
-async def index(request: Request, user: User = Depends(check_user_exists)):
- return lnurlpayout_renderer().TemplateResponse(
- "lnurlpayout/index.html", {"request": request, "user": user.dict()}
- )
diff --git a/lnbits/extensions/lnurlpayout/views_api.py b/lnbits/extensions/lnurlpayout/views_api.py
deleted file mode 100644
index 324eb5dd..00000000
--- a/lnbits/extensions/lnurlpayout/views_api.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from http import HTTPStatus
-
-from fastapi import Query
-from fastapi.params import Depends
-from starlette.exceptions import HTTPException
-
-from lnbits.core.crud import get_payments, get_user
-from lnbits.core.models import Payment
-from lnbits.core.services import create_invoice
-from lnbits.core.views.api import api_payment, api_payments_decode
-from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
-
-from . import lnurlpayout_ext
-from .crud import (
- create_lnurlpayout,
- delete_lnurlpayout,
- get_lnurlpayout,
- get_lnurlpayout_from_wallet,
- get_lnurlpayouts,
-)
-from .models import CreateLnurlPayoutData, lnurlpayout
-from .tasks import on_invoice_paid
-
-
-@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
-async def api_lnurlpayouts(
- all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
-):
- wallet_ids = [wallet.wallet.id]
- if all_wallets:
- wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
-
- return [lnurlpayout.dict() for lnurlpayout in await get_lnurlpayouts(wallet_ids)]
-
-
-@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED)
-async def api_lnurlpayout_create(
- data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- if await get_lnurlpayout_from_wallet(wallet.wallet.id):
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN,
- detail="Wallet already has lnurlpayout set",
- )
- return
- url = await api_payments_decode({"data": data.lnurlpay})
- if "domain" not in url:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded"
- )
- return
- if str(url["domain"])[0:4] != "http":
- raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
- return
- lnurlpayout = await create_lnurlpayout(
- wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data
- )
- if not lnurlpayout:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout"
- )
- return
- return lnurlpayout.dict()
-
-
-@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
-async def api_lnurlpayout_delete(
- lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
-):
- lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
-
- if not lnurlpayout:
- raise HTTPException(
- status_code=HTTPStatus.NOT_FOUND, detail="lnurlpayout does not exist."
- )
-
- if lnurlpayout.wallet != wallet.wallet.id:
- raise HTTPException(
- status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout."
- )
-
- await delete_lnurlpayout(lnurlpayout_id)
- return "", HTTPStatus.NO_CONTENT
-
-
-@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
-async def api_lnurlpayout_check(
- lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
-):
- lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
- ## THIS
- mock_payment = Payment(
- checking_id="mock",
- pending=False,
- amount=1,
- fee=1,
- time=0000,
- bolt11="mock",
- preimage="mock",
- payment_hash="mock",
- wallet_id=lnurlpayout.wallet,
- )
- ## INSTEAD OF THIS
- # payments = await get_payments(
- # wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
- # )
-
- result = await on_invoice_paid(mock_payment)
- return
-
-
-# get payouts func
-# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
-# for lnurlpayout in lnurlpayouts:
-# payments = await get_payments(
-# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
-# )
-# await on_invoice_paid(payments[0])
diff --git a/lnbits/extensions/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..004ebb4d
--- /dev/null
+++ b/lnbits/extensions/market/tasks.py
@@ -0,0 +1,42 @@
+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 not payment.extra:
+ return
+
+ 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/ngrok/views.py b/lnbits/extensions/ngrok/views.py
index 16c3708a..81c8b24e 100644
--- a/lnbits/extensions/ngrok/views.py
+++ b/lnbits/extensions/ngrok/views.py
@@ -1,8 +1,7 @@
# type: ignore
from os import getenv
-from fastapi import Request
-from fastapi.params import Depends
+from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from pyngrok import conf, ngrok
@@ -35,9 +34,7 @@ ngrok_tunnel = ngrok.connect(port)
@ngrok_ext.get("/")
-async def index(
- request: Request, user: User = Depends(check_user_exists) # type: ignore
-):
+async def index(request: Request, user: User = Depends(check_user_exists)):
return ngrok_renderer().TemplateResponse(
"ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()}
)
diff --git a/lnbits/extensions/nostrnip5/config.json b/lnbits/extensions/nostrnip5/config.json
index 658723aa..8621b17c 100644
--- a/lnbits/extensions/nostrnip5/config.json
+++ b/lnbits/extensions/nostrnip5/config.json
@@ -1,6 +1,6 @@
{
"name": "Nostr NIP-5",
"short_description": "Verify addresses for Nostr NIP-5",
- "icon": "request_quote",
+ "tile": "/nostrnip5/static/image/nostrnip5.png",
"contributors": ["leesalminen"]
}
diff --git a/lnbits/extensions/nostrnip5/static/image/nostrnip5.png b/lnbits/extensions/nostrnip5/static/image/nostrnip5.png
new file mode 100644
index 00000000..91dc47f0
Binary files /dev/null and b/lnbits/extensions/nostrnip5/static/image/nostrnip5.png differ
diff --git a/lnbits/extensions/offlineshop/config.json b/lnbits/extensions/offlineshop/config.json
index 0dcb1d6b..94dcd478 100644
--- a/lnbits/extensions/offlineshop/config.json
+++ b/lnbits/extensions/offlineshop/config.json
@@ -1,7 +1,7 @@
{
"name": "OfflineShop",
"short_description": "Receive payments for products offline!",
- "icon": "nature_people",
+ "tile": "/offlineshop/static/image/offlineshop.png",
"contributors": [
"fiatjaf"
]
diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py
index f50df99a..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,24 +63,26 @@ 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(
wallet_id=shop.wallet,
amount=int(amount_received / 1000),
memo=item.name,
- unhashed_description=(await item.lnurlpay_metadata()).encode("utf-8"),
+ unhashed_description=(await item.lnurlpay_metadata()).encode(),
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/static/image/offlineshop.png b/lnbits/extensions/offlineshop/static/image/offlineshop.png
new file mode 100644
index 00000000..24241d4f
Binary files /dev/null and b/lnbits/extensions/offlineshop/static/image/offlineshop.png differ
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/paywall/__init__.py b/lnbits/extensions/paywall/__init__.py
index af1fab63..d7adcded 100644
--- a/lnbits/extensions/paywall/__init__.py
+++ b/lnbits/extensions/paywall/__init__.py
@@ -1,4 +1,5 @@
from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -7,6 +8,14 @@ db = Database("ext_paywall")
paywall_ext: APIRouter = APIRouter(prefix="/paywall", tags=["Paywall"])
+paywall_static_files = [
+ {
+ "path": "/paywall/static",
+ "app": StaticFiles(directory="lnbits/extensions/paywall/static"),
+ "name": "paywall_static",
+ }
+]
+
def paywall_renderer():
return template_renderer(["lnbits/extensions/paywall/templates"])
diff --git a/lnbits/extensions/paywall/config.json b/lnbits/extensions/paywall/config.json
index d08ce7ba..749d1989 100644
--- a/lnbits/extensions/paywall/config.json
+++ b/lnbits/extensions/paywall/config.json
@@ -1,6 +1,6 @@
{
"name": "Paywall",
"short_description": "Create paywalls for content",
- "icon": "policy",
+ "tile": "/paywall/static/image/paywall.png",
"contributors": ["eillarra"]
}
diff --git a/lnbits/extensions/paywall/static/image/paywall.png b/lnbits/extensions/paywall/static/image/paywall.png
new file mode 100644
index 00000000..0331a953
Binary files /dev/null and b/lnbits/extensions/paywall/static/image/paywall.png differ
diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py
index 3c1cd8fc..26480e01 100644
--- a/lnbits/extensions/paywall/views_api.py
+++ b/lnbits/extensions/paywall/views_api.py
@@ -19,7 +19,8 @@ async def api_paywalls(
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 [paywall.dict() for paywall in await get_paywalls(wallet_ids)]
@@ -57,6 +58,7 @@ async def api_paywall_create_invoice(
data: CreatePaywallInvoice, paywall_id: str = Query(None)
):
paywall = await get_paywall(paywall_id)
+ assert paywall
if data.amount < paywall.amount:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
@@ -94,7 +96,9 @@ async def api_paywal_check_invoice(
if is_paid:
wallet = await get_wallet(paywall.wallet)
+ assert wallet
payment = await wallet.get_payment(payment_hash)
+ assert payment
await payment.set_pending(False)
return {"paid": True, "url": paywall.url, "remembers": paywall.remembers}
diff --git a/lnbits/extensions/satsdice/__init__.py b/lnbits/extensions/satsdice/__init__.py
index c20b032e..aaa56af2 100644
--- a/lnbits/extensions/satsdice/__init__.py
+++ b/lnbits/extensions/satsdice/__init__.py
@@ -1,4 +1,5 @@
from fastapi import APIRouter
+from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -7,6 +8,14 @@ db = Database("ext_satsdice")
satsdice_ext: APIRouter = APIRouter(prefix="/satsdice", tags=["satsdice"])
+satsdice_static_files = [
+ {
+ "path": "/satsdice/static",
+ "app": StaticFiles(directory="lnbits/extensions/satsdice/static"),
+ "name": "satsdice_static",
+ }
+]
+
def satsdice_renderer():
return template_renderer(["lnbits/extensions/satsdice/templates"])
diff --git a/lnbits/extensions/satsdice/config.json b/lnbits/extensions/satsdice/config.json
index e4c2eddb..3f4355fe 100644
--- a/lnbits/extensions/satsdice/config.json
+++ b/lnbits/extensions/satsdice/config.json
@@ -1,6 +1,6 @@
{
"name": "Sats Dice",
"short_description": "LNURL Satoshi dice",
- "icon": "casino",
+ "tile": "/satsdice/static/image/satsdice.png",
"contributors": ["arcbtc"]
}
diff --git a/lnbits/extensions/satsdice/lnurl.py b/lnbits/extensions/satsdice/lnurl.py
index a9b3cf08..1e9c6c09 100644
--- a/lnbits/extensions/satsdice/lnurl.py
+++ b/lnbits/extensions/satsdice/lnurl.py
@@ -76,7 +76,7 @@ async def api_lnurlp_callback(
wallet_id=link.wallet,
amount=int(amount_received / 1000),
memo="Satsdice bet",
- unhashed_description=link.lnurlpay_metadata.encode("utf-8"),
+ unhashed_description=link.lnurlpay_metadata.encode(),
extra={"tag": "satsdice", "link": link.id, "comment": "comment"},
)
diff --git a/lnbits/extensions/satsdice/static/image/satsdice.png b/lnbits/extensions/satsdice/static/image/satsdice.png
new file mode 100644
index 00000000..8c7ccaf9
Binary files /dev/null and b/lnbits/extensions/satsdice/static/image/satsdice.png differ
diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py
index d2b5e601..e51c1e10 100644
--- a/lnbits/extensions/satsdice/views.py
+++ b/lnbits/extensions/satsdice/views.py
@@ -3,9 +3,7 @@ from http import HTTPStatus
from io import BytesIO
import pyqrcode
-from fastapi import Request
-from fastapi.param_functions import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
@@ -28,9 +26,7 @@ templates = Jinja2Templates(directory="templates")
@satsdice_ext.get("/", response_class=HTMLResponse)
-async def index(
- request: Request, user: User = Depends(check_user_exists) # type: ignore
-):
+async def index(request: Request, user: User = Depends(check_user_exists)):
return satsdice_renderer().TemplateResponse(
"satsdice/index.html", {"request": request, "user": user.dict()}
)
@@ -108,7 +104,7 @@ async def displaywin(
data = CreateSatsDiceWithdraw(
satsdice_pay=satsdicelink.id,
- value=paylink.value * satsdicelink.multiplier,
+ value=int(paylink.value * satsdicelink.multiplier),
payment_hash=payment_hash,
used=0,
)
diff --git a/lnbits/extensions/satsdice/views_api.py b/lnbits/extensions/satsdice/views_api.py
index d33b76b8..77c2f1d4 100644
--- a/lnbits/extensions/satsdice/views_api.py
+++ b/lnbits/extensions/satsdice/views_api.py
@@ -1,8 +1,6 @@
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
@@ -26,7 +24,7 @@ from .models import CreateSatsDiceLink
@satsdice_ext.get("/api/v1/links")
async def api_links(
request: Request,
- wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ wallet: WalletTypeInfo = Depends(get_key_type),
all_wallets: bool = Query(False),
):
wallet_ids = [wallet.wallet.id]
@@ -49,7 +47,7 @@ async def api_links(
@satsdice_ext.get("/api/v1/links/{link_id}")
async def api_link_retrieve(
- link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
):
link = await get_satsdice_pay(link_id)
@@ -70,7 +68,7 @@ async def api_link_retrieve(
@satsdice_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_create_or_update(
data: CreateSatsDiceLink,
- wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ wallet: WalletTypeInfo = Depends(get_key_type),
link_id: str = Query(None),
):
if data.min_bet > data.max_bet:
@@ -98,7 +96,7 @@ async def api_link_create_or_update(
@satsdice_ext.delete("/api/v1/links/{link_id}")
async def api_link_delete(
- wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ wallet: WalletTypeInfo = Depends(get_key_type),
link_id: str = Query(None),
):
link = await get_satsdice_pay(link_id)
diff --git a/lnbits/extensions/satspay/config.json b/lnbits/extensions/satspay/config.json
index fe9e3df4..6104d360 100644
--- a/lnbits/extensions/satspay/config.json
+++ b/lnbits/extensions/satspay/config.json
@@ -1,6 +1,6 @@
{
"name": "SatsPay Server",
"short_description": "Create onchain and LN charges",
- "icon": "payment",
+ "tile": "/satspay/static/image/satspay.png",
"contributors": ["arcbtc"]
}
diff --git a/lnbits/extensions/satspay/static/image/satspay.png b/lnbits/extensions/satspay/static/image/satspay.png
new file mode 100644
index 00000000..82791407
Binary files /dev/null and b/lnbits/extensions/satspay/static/image/satspay.png differ
diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py
index 90a46929..08c731cb 100644
--- a/lnbits/extensions/satspay/views_api.py
+++ b/lnbits/extensions/satspay/views_api.py
@@ -1,8 +1,7 @@
import json
from http import HTTPStatus
-from fastapi import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query
from loguru import logger
from starlette.exceptions import HTTPException
diff --git a/lnbits/extensions/scrub/config.json b/lnbits/extensions/scrub/config.json
index df9e0038..93eb871a 100644
--- a/lnbits/extensions/scrub/config.json
+++ b/lnbits/extensions/scrub/config.json
@@ -1,6 +1,6 @@
{
"name": "Scrub",
"short_description": "Pass payments to LNURLp/LNaddress",
- "icon": "send",
+ "tile": "/scrub/static/image/scrub.png",
"contributors": ["arcbtc", "talvasconcelos"]
}
diff --git a/lnbits/extensions/scrub/static/image/scrub.png b/lnbits/extensions/scrub/static/image/scrub.png
new file mode 100644
index 00000000..b3d4d24f
Binary files /dev/null and b/lnbits/extensions/scrub/static/image/scrub.png differ
diff --git a/lnbits/extensions/splitpayments/config.json b/lnbits/extensions/splitpayments/config.json
index 898dcbde..1e0c9671 100644
--- a/lnbits/extensions/splitpayments/config.json
+++ b/lnbits/extensions/splitpayments/config.json
@@ -1,6 +1,6 @@
{
"name": "Split Payments",
"short_description": "Split incoming payments across wallets",
- "icon": "call_split",
+ "tile": "/splitpayments/static/image/split-payments.png",
"contributors": ["fiatjaf", "cryptograffiti"]
}
diff --git a/lnbits/extensions/splitpayments/static/image/split-payments.png b/lnbits/extensions/splitpayments/static/image/split-payments.png
new file mode 100644
index 00000000..10b8e7f2
Binary files /dev/null and b/lnbits/extensions/splitpayments/static/image/split-payments.png differ
diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py
index 33768805..d9f53f20 100644
--- a/lnbits/extensions/splitpayments/tasks.py
+++ b/lnbits/extensions/splitpayments/tasks.py
@@ -20,7 +20,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
- if payment.extra.get("tag") == "splitpayments":
+ if not payment.extra or payment.extra.get("tag") == "splitpayments":
# already a splitted payment, ignore
return
diff --git a/lnbits/extensions/splitpayments/views.py b/lnbits/extensions/splitpayments/views.py
index 056c7563..c33fd424 100644
--- a/lnbits/extensions/splitpayments/views.py
+++ b/lnbits/extensions/splitpayments/views.py
@@ -1,5 +1,4 @@
-from fastapi import 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/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py
index 41a11c94..f83f2b4f 100644
--- a/lnbits/extensions/splitpayments/views_api.py
+++ b/lnbits/extensions/splitpayments/views_api.py
@@ -1,7 +1,6 @@
from http import HTTPStatus
-from fastapi import Request
-from fastapi.params import Depends
+from fastapi import Depends, Request
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_wallet, get_wallet_for_key
diff --git a/lnbits/extensions/streamalerts/__init__.py b/lnbits/extensions/streamalerts/__init__.py
index 44076e2e..0e431124 100644
--- a/lnbits/extensions/streamalerts/__init__.py
+++ b/lnbits/extensions/streamalerts/__init__.py
@@ -1,4 +1,5 @@
from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -7,6 +8,14 @@ db = Database("ext_streamalerts")
streamalerts_ext: APIRouter = APIRouter(prefix="/streamalerts", tags=["streamalerts"])
+streamalerts_static_files = [
+ {
+ "path": "/streamalerts/static",
+ "app": StaticFiles(directory="lnbits/extensions/streamalerts/static"),
+ "name": "streamalerts_static",
+ }
+]
+
def streamalerts_renderer():
return template_renderer(["lnbits/extensions/streamalerts/templates"])
diff --git a/lnbits/extensions/streamalerts/config.json b/lnbits/extensions/streamalerts/config.json
index f94886c9..24451b24 100644
--- a/lnbits/extensions/streamalerts/config.json
+++ b/lnbits/extensions/streamalerts/config.json
@@ -1,6 +1,6 @@
{
"name": "Stream Alerts",
"short_description": "Bitcoin donations in stream alerts",
- "icon": "notifications_active",
+ "tile": "/streamalerts/static/image/streamalerts.png",
"contributors": ["Fittiboy"]
}
diff --git a/lnbits/extensions/streamalerts/static/image/streamalerts.png b/lnbits/extensions/streamalerts/static/image/streamalerts.png
new file mode 100644
index 00000000..63724ec3
Binary files /dev/null and b/lnbits/extensions/streamalerts/static/image/streamalerts.png differ
diff --git a/lnbits/extensions/subdomains/__init__.py b/lnbits/extensions/subdomains/__init__.py
index fbaa8a4c..0b0774dc 100644
--- a/lnbits/extensions/subdomains/__init__.py
+++ b/lnbits/extensions/subdomains/__init__.py
@@ -1,6 +1,7 @@
import asyncio
from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -10,6 +11,14 @@ db = Database("ext_subdomains")
subdomains_ext: APIRouter = APIRouter(prefix="/subdomains", tags=["subdomains"])
+subdomains_static_files = [
+ {
+ "path": "/subdomains/static",
+ "app": StaticFiles(directory="lnbits/extensions/subdomains/static"),
+ "name": "subdomains_static",
+ }
+]
+
def subdomains_renderer():
return template_renderer(["lnbits/extensions/subdomains/templates"])
diff --git a/lnbits/extensions/subdomains/config.json b/lnbits/extensions/subdomains/config.json
index 6bf9480c..cec2ec64 100644
--- a/lnbits/extensions/subdomains/config.json
+++ b/lnbits/extensions/subdomains/config.json
@@ -1,6 +1,6 @@
{
"name": "Subdomains",
"short_description": "Sell subdomains of your domain",
- "icon": "domain",
+ "tile": "/subdomains/static/image/subdomains.png",
"contributors": ["grmkris"]
}
diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py
index 39e17615..552c37c7 100644
--- a/lnbits/extensions/subdomains/models.py
+++ b/lnbits/extensions/subdomains/models.py
@@ -1,26 +1,26 @@
-from fastapi.params import Query
-from pydantic.main import BaseModel
+from fastapi import Query
+from pydantic import BaseModel
class CreateDomain(BaseModel):
- wallet: str = Query(...) # type: ignore
- domain: str = Query(...) # type: ignore
- cf_token: str = Query(...) # type: ignore
- cf_zone_id: str = Query(...) # type: ignore
- webhook: str = Query("") # type: ignore
- description: str = Query(..., min_length=0) # type: ignore
- cost: int = Query(..., ge=0) # type: ignore
- allowed_record_types: str = Query(...) # type: ignore
+ wallet: str = Query(...)
+ domain: str = Query(...)
+ cf_token: str = Query(...)
+ cf_zone_id: str = Query(...)
+ webhook: str = Query("")
+ description: str = Query(..., min_length=0)
+ cost: int = Query(..., ge=0)
+ allowed_record_types: str = Query(...)
class CreateSubdomain(BaseModel):
- domain: str = Query(...) # type: ignore
- subdomain: str = Query(...) # type: ignore
- email: str = Query(...) # type: ignore
- ip: str = Query(...) # type: ignore
- sats: int = Query(..., ge=0) # type: ignore
- duration: int = Query(...) # type: ignore
- record_type: str = Query(...) # type: ignore
+ domain: str = Query(...)
+ subdomain: str = Query(...)
+ email: str = Query(...)
+ ip: str = Query(...)
+ sats: int = Query(..., ge=0)
+ duration: int = Query(...)
+ record_type: str = Query(...)
class Domains(BaseModel):
diff --git a/lnbits/extensions/subdomains/static/image/subdomains.png b/lnbits/extensions/subdomains/static/image/subdomains.png
new file mode 100644
index 00000000..c552cb7b
Binary files /dev/null and b/lnbits/extensions/subdomains/static/image/subdomains.png differ
diff --git a/lnbits/extensions/subdomains/views.py b/lnbits/extensions/subdomains/views.py
index 962f850d..95348ec4 100644
--- a/lnbits/extensions/subdomains/views.py
+++ b/lnbits/extensions/subdomains/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/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py
index b30daabd..2b20bd1f 100644
--- a/lnbits/extensions/subdomains/views_api.py
+++ b/lnbits/extensions/subdomains/views_api.py
@@ -1,7 +1,6 @@
from http import HTTPStatus
-from fastapi import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
@@ -29,7 +28,7 @@ from .crud import (
@subdomains_ext.get("/api/v1/domains")
async def api_domains(
- g: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ g: WalletTypeInfo = Depends(get_key_type),
all_wallets: bool = Query(False),
):
wallet_ids = [g.wallet.id]
@@ -47,7 +46,7 @@ async def api_domains(
async def api_domain_create(
data: CreateDomain,
domain_id=None,
- g: WalletTypeInfo = Depends(get_key_type), # type: ignore
+ g: WalletTypeInfo = Depends(get_key_type),
):
if domain_id:
domain = await get_domain(domain_id)
@@ -68,9 +67,7 @@ async def api_domain_create(
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
-async def api_domain_delete(
- domain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
-):
+async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)):
domain = await get_domain(domain_id)
if not domain:
@@ -89,7 +86,7 @@ async def api_domain_delete(
@subdomains_ext.get("/api/v1/subdomains")
async def api_subdomains(
- all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type) # type: ignore
+ all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [g.wallet.id]
@@ -169,6 +166,7 @@ async def api_subdomain_make_subdomain(domain_id, data: CreateSubdomain):
@subdomains_ext.get("/api/v1/subdomains/{payment_hash}")
async def api_subdomain_send_subdomain(payment_hash):
subdomain = await get_subdomain(payment_hash)
+ assert subdomain
try:
status = await check_transaction_status(subdomain.wallet, payment_hash)
is_paid = not status.pending
@@ -182,9 +180,7 @@ async def api_subdomain_send_subdomain(payment_hash):
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
-async def api_subdomain_delete(
- subdomain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
-):
+async def api_subdomain_delete(subdomain_id, g: WalletTypeInfo = Depends(get_key_type)):
subdomain = await get_subdomain(subdomain_id)
if not subdomain:
diff --git a/lnbits/extensions/tipjar/__init__.py b/lnbits/extensions/tipjar/__init__.py
index a4b50c04..a64de43f 100644
--- a/lnbits/extensions/tipjar/__init__.py
+++ b/lnbits/extensions/tipjar/__init__.py
@@ -1,4 +1,5 @@
from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -7,6 +8,14 @@ db = Database("ext_tipjar")
tipjar_ext: APIRouter = APIRouter(prefix="/tipjar", tags=["tipjar"])
+tipjar_static_files = [
+ {
+ "path": "/tipjar/static",
+ "app": StaticFiles(directory="lnbits/extensions/tipjar/static"),
+ "name": "tipjar_static",
+ }
+]
+
def tipjar_renderer():
return template_renderer(["lnbits/extensions/tipjar/templates"])
diff --git a/lnbits/extensions/tipjar/config.json b/lnbits/extensions/tipjar/config.json
index e48eb4ea..90f634ed 100644
--- a/lnbits/extensions/tipjar/config.json
+++ b/lnbits/extensions/tipjar/config.json
@@ -1,6 +1,6 @@
{
"name": "Tip Jar",
"short_description": "Accept Bitcoin donations, with messages attached!",
- "icon": "favorite",
+ "tile": "/tipjar/static/image/tipjar.png",
"contributors": ["Fittiboy"]
}
diff --git a/lnbits/extensions/tipjar/static/image/tipjar.png b/lnbits/extensions/tipjar/static/image/tipjar.png
new file mode 100644
index 00000000..6f0d69b7
Binary files /dev/null and b/lnbits/extensions/tipjar/static/image/tipjar.png differ
diff --git a/lnbits/extensions/tpos/__init__.py b/lnbits/extensions/tpos/__init__.py
index 3ce618aa..c1b5a7dd 100644
--- a/lnbits/extensions/tpos/__init__.py
+++ b/lnbits/extensions/tpos/__init__.py
@@ -1,6 +1,7 @@
import asyncio
from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -10,6 +11,14 @@ db = Database("ext_tpos")
tpos_ext: APIRouter = APIRouter(prefix="/tpos", tags=["TPoS"])
+tpos_static_files = [
+ {
+ "path": "/tpos/static",
+ "app": StaticFiles(directory="lnbits/extensions/tpos/static"),
+ "name": "tpos_static",
+ }
+]
+
def tpos_renderer():
return template_renderer(["lnbits/extensions/tpos/templates"])
diff --git a/lnbits/extensions/tpos/config.json b/lnbits/extensions/tpos/config.json
index 3bd1a71a..0c118e1a 100644
--- a/lnbits/extensions/tpos/config.json
+++ b/lnbits/extensions/tpos/config.json
@@ -1,6 +1,6 @@
{
"name": "TPoS",
"short_description": "A shareable PoS terminal!",
- "icon": "dialpad",
+ "tile": "/tpos/static/image/tpos.png",
"contributors": ["talvasconcelos", "arcbtc", "leesalminen"]
}
diff --git a/lnbits/extensions/tpos/static/image/tpos.png b/lnbits/extensions/tpos/static/image/tpos.png
new file mode 100644
index 00000000..c663032d
Binary files /dev/null and b/lnbits/extensions/tpos/static/image/tpos.png differ
diff --git a/lnbits/extensions/tpos/tasks.py b/lnbits/extensions/tpos/tasks.py
index 6eb1d5d1..f1417810 100644
--- a/lnbits/extensions/tpos/tasks.py
+++ b/lnbits/extensions/tpos/tasks.py
@@ -20,10 +20,11 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
+ if not payment.extra:
+ return
if payment.extra.get("tag") != "tpos":
return
- tpos = await get_tpos(payment.extra.get("tposId"))
tipAmount = payment.extra.get("tipAmount")
strippedPayment = {
@@ -34,14 +35,23 @@ async def on_invoice_paid(payment: Payment) -> None:
"bolt11": payment.bolt11,
}
- await websocketUpdater(payment.extra.get("tposId"), str(strippedPayment))
+ tpos_id = payment.extra.get("tposId")
+ assert tpos_id
- if tipAmount is None:
+ tpos = await get_tpos(tpos_id)
+ assert tpos
+
+ await websocketUpdater(tpos_id, str(strippedPayment))
+
+ if not tipAmount:
# no tip amount
return
+ wallet_id = tpos.tip_wallet
+ assert wallet_id
+
payment_hash, payment_request = await create_invoice(
- wallet_id=tpos.tip_wallet,
+ wallet_id=wallet_id,
amount=int(tipAmount), # sats
internal=True,
memo=f"tpos tip",
diff --git a/lnbits/extensions/tpos/views.py b/lnbits/extensions/tpos/views.py
index dac129a9..fee5914f 100644
--- a/lnbits/extensions/tpos/views.py
+++ b/lnbits/extensions/tpos/views.py
@@ -1,7 +1,6 @@
from http import HTTPStatus
-from fastapi import Request
-from fastapi.params import Depends
+from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py
index 3a51238a..05537f84 100644
--- a/lnbits/extensions/tpos/views_api.py
+++ b/lnbits/extensions/tpos/views_api.py
@@ -1,8 +1,7 @@
from http import HTTPStatus
import httpx
-from fastapi import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query
from lnurl import decode as decode_lnurl
from loguru import logger
from starlette.exceptions import HTTPException
@@ -25,7 +24,8 @@ async def api_tposs(
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
- wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
+ user = await get_user(wallet.wallet.user)
+ wallet_ids = user.wallet_ids if user else []
return [tpos.dict() for tpos in await get_tposs(wallet_ids)]
@@ -58,8 +58,9 @@ async def api_tpos_delete(
@tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED)
async def api_tpos_create_invoice(
- amount: int = Query(..., ge=1), tipAmount: int = None, tpos_id: str = None
-):
+ tpos_id: str, amount: int = Query(..., ge=1), tipAmount: int = 0
+) -> dict:
+
tpos = await get_tpos(tpos_id)
if not tpos:
@@ -67,7 +68,7 @@ async def api_tpos_create_invoice(
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
)
- if tipAmount:
+ if tipAmount > 0:
amount += tipAmount
try:
@@ -84,7 +85,7 @@ async def api_tpos_create_invoice(
@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices")
-async def api_tpos_get_latest_invoices(tpos_id: str = None):
+async def api_tpos_get_latest_invoices(tpos_id: str):
try:
payments = [
Payment.from_row(row)
@@ -111,7 +112,7 @@ async def api_tpos_get_latest_invoices(tpos_id: str = None):
"/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK
)
async def api_tpos_pay_invoice(
- lnurl_data: PayLnurlWData, payment_request: str = None, tpos_id: str = None
+ lnurl_data: PayLnurlWData, payment_request: str, tpos_id: str
):
tpos = await get_tpos(tpos_id)
diff --git a/lnbits/extensions/usermanager/__init__.py b/lnbits/extensions/usermanager/__init__.py
index 8ea02f6f..8d8ff557 100644
--- a/lnbits/extensions/usermanager/__init__.py
+++ b/lnbits/extensions/usermanager/__init__.py
@@ -1,4 +1,5 @@
from fastapi import APIRouter
+from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
@@ -7,6 +8,14 @@ db = Database("ext_usermanager")
usermanager_ext: APIRouter = APIRouter(prefix="/usermanager", tags=["usermanager"])
+usermanager_static_files = [
+ {
+ "path": "/usermanager/static",
+ "app": StaticFiles(directory="lnbits/extensions/usermanager/static"),
+ "name": "usermanager_static",
+ }
+]
+
def usermanager_renderer():
return template_renderer(["lnbits/extensions/usermanager/templates"])
diff --git a/lnbits/extensions/usermanager/config.json b/lnbits/extensions/usermanager/config.json
index 7391ec29..0b38a083 100644
--- a/lnbits/extensions/usermanager/config.json
+++ b/lnbits/extensions/usermanager/config.json
@@ -1,6 +1,6 @@
{
"name": "User Manager",
"short_description": "Generate users and wallets",
- "icon": "person_add",
+ "tile": "/usermanager/static/image/usermanager.png",
"contributors": ["benarc"]
}
diff --git a/lnbits/extensions/usermanager/static/image/usermanager.png b/lnbits/extensions/usermanager/static/image/usermanager.png
new file mode 100644
index 00000000..a38b2b3e
Binary files /dev/null and b/lnbits/extensions/usermanager/static/image/usermanager.png differ
diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py
index 814659f7..115cbfa6 100644
--- a/lnbits/extensions/usermanager/views.py
+++ b/lnbits/extensions/usermanager/views.py
@@ -1,5 +1,4 @@
-from fastapi import Request
-from fastapi.params import Depends
+from fastapi import Depends, Request
from starlette.responses import HTMLResponse
from lnbits.core.models import User
@@ -9,9 +8,7 @@ from . import usermanager_ext, usermanager_renderer
@usermanager_ext.get("/", response_class=HTMLResponse)
-async def index(
- request: Request, user: User = Depends(check_user_exists) # type: ignore
-):
+async def index(request: Request, user: User = Depends(check_user_exists)):
return usermanager_renderer().TemplateResponse(
"usermanager/index.html", {"request": request, "user": user.dict()}
)
diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py
index 493a71bc..27c6f3cb 100644
--- a/lnbits/extensions/usermanager/views_api.py
+++ b/lnbits/extensions/usermanager/views_api.py
@@ -1,7 +1,6 @@
from http import HTTPStatus
-from fastapi import Query
-from fastapi.params import Depends
+from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core import update_user_extension
@@ -26,7 +25,7 @@ from .models import CreateUserData, CreateUserWallet
@usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
async def api_usermanager_users(
- wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
+ wallet: WalletTypeInfo = Depends(require_admin_key),
):
user_id = wallet.wallet.user
return [user.dict() for user in await get_usermanager_users(user_id)]
@@ -101,7 +100,7 @@ async def api_usermanager_wallets_create(data: CreateUserWallet):
@usermanager_ext.get("/api/v1/wallets")
async def api_usermanager_wallets(
- wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
+ wallet: WalletTypeInfo = Depends(require_admin_key),
):
admin_id = wallet.wallet.user
return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)]
diff --git a/lnbits/extensions/watchonly/config.json b/lnbits/extensions/watchonly/config.json
index 6331418c..c9fec893 100644
--- a/lnbits/extensions/watchonly/config.json
+++ b/lnbits/extensions/watchonly/config.json
@@ -1,7 +1,7 @@
{
"name": "Onchain Wallet",
"short_description": "Onchain watch only wallets",
- "icon": "visibility",
+ "tile": "/watchonly/static/bitcoin-wallet.png",
"contributors": [
"arcbtc",
"motorina0"
diff --git a/lnbits/extensions/watchonly/static/bitcoin-wallet.png b/lnbits/extensions/watchonly/static/bitcoin-wallet.png
new file mode 100644
index 00000000..3cd5ac0f
Binary files /dev/null and b/lnbits/extensions/watchonly/static/bitcoin-wallet.png differ
diff --git a/lnbits/extensions/withdraw/config.json b/lnbits/extensions/withdraw/config.json
index de82e7f1..c22d69c8 100644
--- a/lnbits/extensions/withdraw/config.json
+++ b/lnbits/extensions/withdraw/config.json
@@ -1,6 +1,6 @@
{
"name": "LNURLw",
"short_description": "Make LNURL withdraw links",
- "icon": "crop_free",
+ "tile": "/withdraw/static/image/lnurl-withdraw.png",
"contributors": ["arcbtc", "eillarra"]
}
diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py
index 83404c62..68603f0a 100644
--- a/lnbits/extensions/withdraw/crud.py
+++ b/lnbits/extensions/withdraw/crud.py
@@ -1,6 +1,8 @@
from datetime import datetime
from typing import List, Optional, Union
+import shortuuid
+
from lnbits.helpers import urlsafe_short_hash
from . import db
@@ -8,9 +10,10 @@ from .models import CreateWithdrawData, HashCheck, WithdrawLink
async def create_withdraw_link(
- data: CreateWithdrawData, wallet_id: str, usescsv: str
+ data: CreateWithdrawData, wallet_id: str
) -> WithdrawLink:
link_id = urlsafe_short_hash()
+ available_links = ",".join([str(i) for i in range(data.uses)])
await db.execute(
"""
INSERT INTO withdraw.withdraw_link (
@@ -45,7 +48,7 @@ async def create_withdraw_link(
urlsafe_short_hash(),
urlsafe_short_hash(),
int(datetime.now().timestamp()) + data.wait_time,
- usescsv,
+ available_links,
data.webhook_url,
data.webhook_headers,
data.webhook_body,
@@ -94,6 +97,26 @@ async def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[Withdraw
return [WithdrawLink(**row) for row in rows]
+async def remove_unique_withdraw_link(link: WithdrawLink, unique_hash: str) -> None:
+ unique_links = [
+ x.strip()
+ for x in link.usescsv.split(",")
+ if unique_hash != shortuuid.uuid(name=link.id + link.unique_hash + x.strip())
+ ]
+ await update_withdraw_link(
+ link.id,
+ usescsv=",".join(unique_links),
+ )
+
+
+async def increment_withdraw_link(link: WithdrawLink) -> None:
+ await update_withdraw_link(
+ link.id,
+ used=link.used + 1,
+ open_time=link.wait_time + int(datetime.now().timestamp()),
+ )
+
+
async def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]:
if "is_unique" in kwargs:
kwargs["is_unique"] = int(kwargs["is_unique"])
@@ -132,7 +155,7 @@ async def create_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
return hashCheck
-async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]:
+async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
rowid = await db.fetchone(
"SELECT * FROM withdraw.hash_check WHERE id = ?", (the_hash,)
)
@@ -141,10 +164,10 @@ async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]:
)
if not rowlnurl:
await create_hash_check(the_hash, lnurl_id)
- return {"lnurl": True, "hash": False}
+ return HashCheck(lnurl=True, hash=False)
else:
if not rowid:
await create_hash_check(the_hash, lnurl_id)
- return {"lnurl": True, "hash": False}
+ return HashCheck(lnurl=True, hash=False)
else:
- return {"lnurl": True, "hash": True}
+ return HashCheck(lnurl=True, hash=True)
diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py
index 86640443..5ef521fa 100644
--- a/lnbits/extensions/withdraw/lnurl.py
+++ b/lnbits/extensions/withdraw/lnurl.py
@@ -1,28 +1,27 @@
import json
-import traceback
from datetime import datetime
from http import HTTPStatus
import httpx
-import shortuuid # type: ignore
-from fastapi import HTTPException
-from fastapi.param_functions import Query
+import shortuuid
+from fastapi import HTTPException, Query, Request, Response
from loguru import logger
-from starlette.requests import Request
-from starlette.responses import HTMLResponse
from lnbits.core.crud import update_payment_extra
from lnbits.core.services import pay_invoice
from . import withdraw_ext
-from .crud import get_withdraw_link_by_hash, update_withdraw_link
-
-# FOR LNURLs WHICH ARE NOT UNIQUE
+from .crud import (
+ get_withdraw_link_by_hash,
+ increment_withdraw_link,
+ remove_unique_withdraw_link,
+)
+from .models import WithdrawLink
@withdraw_ext.get(
"/api/v1/lnurl/{unique_hash}",
- response_class=HTMLResponse,
+ response_class=Response,
name="withdraw.api_lnurl_response",
)
async def api_lnurl_response(request: Request, unique_hash):
@@ -53,9 +52,6 @@ async def api_lnurl_response(request: Request, unique_hash):
return json.dumps(withdrawResponse)
-# CALLBACK
-
-
@withdraw_ext.get(
"/api/v1/lnurl/cb/{unique_hash}",
name="withdraw.api_lnurl_callback",
@@ -99,105 +95,79 @@ async def api_lnurl_callback(
detail=f"wait link open_time {link.open_time - now} seconds.",
)
- usescsv = ""
-
- for x in range(1, link.uses - link.used):
- usecv = link.usescsv.split(",")
- usescsv += "," + str(usecv[x])
- usecsvback = usescsv
-
- found = False
- if id_unique_hash is not None:
- useslist = link.usescsv.split(",")
- for ind, x in enumerate(useslist):
- tohash = link.id + link.unique_hash + str(x)
- if id_unique_hash == shortuuid.uuid(name=tohash):
- found = True
- useslist.pop(ind)
- usescsv = ",".join(useslist)
- if not found:
+ if id_unique_hash:
+ if check_unique_link(link, id_unique_hash):
+ await remove_unique_withdraw_link(link, id_unique_hash)
+ else:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found."
)
- else:
- usescsv = usescsv[1:]
-
- changesback = {
- "open_time": link.wait_time,
- "used": link.used,
- "usescsv": usecsvback,
- }
try:
- changes = {
- "open_time": link.wait_time + now,
- "used": link.used + 1,
- "usescsv": usescsv,
- }
- await update_withdraw_link(link.id, **changes)
-
- payment_request = pr
-
payment_hash = await pay_invoice(
wallet_id=link.wallet,
- payment_request=payment_request,
+ payment_request=pr,
max_sat=link.max_withdrawable,
extra={"tag": "withdraw"},
)
-
+ await increment_withdraw_link(link)
if link.webhook_url:
- async with httpx.AsyncClient() as client:
- try:
- kwargs = {
- "json": {
- "payment_hash": payment_hash,
- "payment_request": payment_request,
- "lnurlw": link.id,
- },
- "timeout": 40,
- }
- if link.webhook_body:
- kwargs["json"]["body"] = json.loads(link.webhook_body)
- if link.webhook_headers:
- kwargs["headers"] = json.loads(link.webhook_headers)
-
- r: httpx.Response = await client.post(link.webhook_url, **kwargs)
- await update_payment_extra(
- payment_hash=payment_hash,
- extra={
- "wh_success": r.is_success,
- "wh_message": r.reason_phrase,
- "wh_response": r.text,
- },
- outgoing=True,
- )
- except Exception as exc:
- # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid
- logger.error(
- "Caught exception when dispatching webhook url: " + str(exc)
- )
- await update_payment_extra(
- payment_hash=payment_hash,
- extra={"wh_success": False, "wh_message": str(exc)},
- outgoing=True,
- )
-
+ await dispatch_webhook(link, payment_hash, pr)
return {"status": "OK"}
-
except Exception as e:
- await update_withdraw_link(link.id, **changesback)
- logger.error(traceback.format_exc())
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}"
)
+def check_unique_link(link: WithdrawLink, unique_hash: str) -> bool:
+ return any(
+ unique_hash == shortuuid.uuid(name=link.id + link.unique_hash + x.strip())
+ for x in link.usescsv.split(",")
+ )
+
+
+async def dispatch_webhook(
+ link: WithdrawLink, payment_hash: str, payment_request: str
+) -> None:
+ async with httpx.AsyncClient() as client:
+ try:
+ r: httpx.Response = await client.post(
+ link.webhook_url,
+ json={
+ "payment_hash": payment_hash,
+ "payment_request": payment_request,
+ "lnurlw": link.id,
+ "body": json.loads(link.webhook_body) if link.webhook_body else "",
+ },
+ headers=json.loads(link.webhook_headers)
+ if link.webhook_headers
+ else None,
+ timeout=40,
+ )
+ await update_payment_extra(
+ payment_hash=payment_hash,
+ extra={
+ "wh_success": r.is_success,
+ "wh_message": r.reason_phrase,
+ "wh_response": r.text,
+ },
+ outgoing=True,
+ )
+ except Exception as exc:
+ # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid
+ logger.error("Caught exception when dispatching webhook url: " + str(exc))
+ await update_payment_extra(
+ payment_hash=payment_hash,
+ extra={"wh_success": False, "wh_message": str(exc)},
+ outgoing=True,
+ )
+
+
# FOR LNURLs WHICH ARE UNIQUE
-
-
@withdraw_ext.get(
"/api/v1/lnurl/{unique_hash}/{id_unique_hash}",
- response_class=HTMLResponse,
+ response_class=Response,
name="withdraw.api_lnurl_multi_response",
)
async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash):
@@ -213,14 +183,7 @@ async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash
status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent."
)
- useslist = link.usescsv.split(",")
- found = False
- for x in useslist:
- tohash = link.id + link.unique_hash + str(x)
- if id_unique_hash == shortuuid.uuid(name=tohash):
- found = True
-
- if not found:
+ if not check_unique_link(link, id_unique_hash):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found."
)
diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py
index 51c6a1cf..49421a79 100644
--- a/lnbits/extensions/withdraw/models.py
+++ b/lnbits/extensions/withdraw/models.py
@@ -1,9 +1,8 @@
-from sqlite3 import Row
-
-import shortuuid # type: ignore
-from fastapi.param_functions import Query
+import shortuuid
+from fastapi import Query
from lnurl import Lnurl, LnurlWithdrawResponse
-from lnurl import encode as lnurl_encode # type: ignore
+from lnurl import encode as lnurl_encode
+from lnurl.models import ClearnetUrl, MilliSatoshi
from pydantic import BaseModel
from starlette.requests import Request
@@ -67,18 +66,14 @@ class WithdrawLink(BaseModel):
name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash
)
return LnurlWithdrawResponse(
- callback=url,
+ callback=ClearnetUrl(url, scheme="https"),
k1=self.k1,
- min_withdrawable=self.min_withdrawable * 1000,
- max_withdrawable=self.max_withdrawable * 1000,
- default_description=self.title,
+ minWithdrawable=MilliSatoshi(self.min_withdrawable * 1000),
+ maxWithdrawable=MilliSatoshi(self.max_withdrawable * 1000),
+ defaultDescription=self.title,
)
class HashCheck(BaseModel):
- id: str
- lnurl_id: str
-
- @classmethod
- def from_row(cls, row: Row) -> "Hash":
- return cls(**dict(row))
+ hash: bool
+ lnurl: bool
diff --git a/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png b/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png
new file mode 100644
index 00000000..4f036423
Binary files /dev/null and b/lnbits/extensions/withdraw/static/image/lnurl-withdraw.png differ
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/lnbits/helpers.py b/lnbits/helpers.py
index b98e3bc4..d3a4e6ea 100644
--- a/lnbits/helpers.py
+++ b/lnbits/helpers.py
@@ -17,7 +17,7 @@ class Extension(NamedTuple):
is_admin_only: bool
name: Optional[str] = None
short_description: Optional[str] = None
- icon: Optional[str] = None
+ tile: Optional[str] = None
contributors: Optional[List[str]] = None
hidden: bool = False
migration_module: Optional[str] = None
@@ -63,7 +63,7 @@ class ExtensionManager:
is_admin_only,
config.get("name"),
config.get("short_description"),
- config.get("icon"),
+ config.get("tile"),
config.get("contributors"),
config.get("hidden") or False,
config.get("migration_module"),
diff --git a/lnbits/lnurl.py b/lnbits/lnurl.py
index 4c285da1..6039545b 100644
--- a/lnbits/lnurl.py
+++ b/lnbits/lnurl.py
@@ -6,11 +6,11 @@ def decode(lnurl: str) -> str:
assert data
bech32_data = convertbits(data, 5, 8, False)
assert bech32_data
- return bytes(bech32_data).decode("utf-8")
+ return bytes(bech32_data).decode()
def encode(url: str) -> str:
- bech32_data = convertbits(url.encode("utf-8"), 8, 5, True)
+ bech32_data = convertbits(url.encode(), 8, 5, True)
assert bech32_data
lnurl = bech32_encode("lnurl", bech32_data)
return lnurl.upper()
diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js
index a1b42cac..32b075b7 100644
--- a/lnbits/static/js/base.js
+++ b/lnbits/static/js/base.js
@@ -126,7 +126,7 @@ window.LNbits = {
'isAdminOnly',
'name',
'shortDescription',
- 'icon',
+ 'tile',
'contributors',
'hidden'
],
diff --git a/lnbits/static/js/components.js b/lnbits/static/js/components.js
index 8d550137..88be819d 100644
--- a/lnbits/static/js/components.js
+++ b/lnbits/static/js/components.js
@@ -118,16 +118,15 @@ Vue.component('lnbits-extension-list', {
:active="extension.isActive"
tag="a" :href="[extension.url, '?usr=', user.id].join('')">
-
-
+
+
- {{ extension.name }}
+ {{ extension.name }}
diff --git a/lnbits/static/scss/base.scss b/lnbits/static/scss/base.scss
index 672a85b6..f6b0762e 100644
--- a/lnbits/static/scss/base.scss
+++ b/lnbits/static/scss/base.scss
@@ -137,6 +137,10 @@ video {
-moz-osx-font-smoothing: grayscale;
}
+.q-rating__icon {
+ font-size: 1em;
+}
+
// text-wrap
.text-wrap {
word-break: break-word;
diff --git a/lnbits/wallets/cln.py b/lnbits/wallets/cln.py
index 4cb72f97..b04fdb53 100644
--- a/lnbits/wallets/cln.py
+++ b/lnbits/wallets/cln.py
@@ -96,7 +96,7 @@ class CoreLightningWallet(Wallet):
r = self.ln.invoice(
msatoshi=msat,
label=label,
- description=unhashed_description.decode("utf-8")
+ description=unhashed_description.decode()
if unhashed_description
else memo,
exposeprivatechannels=True,
diff --git a/lnbits/wallets/eclair.py b/lnbits/wallets/eclair.py
index 94d21066..57eec582 100644
--- a/lnbits/wallets/eclair.py
+++ b/lnbits/wallets/eclair.py
@@ -44,7 +44,7 @@ class EclairWallet(Wallet):
self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws"
passw = settings.eclair_pass
- encodedAuth = base64.b64encode(f":{passw}".encode("utf-8"))
+ encodedAuth = base64.b64encode(f":{passw}".encode())
auth = str(encodedAuth, "utf-8")
self.auth = {"Authorization": f"Basic {auth}"}
diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py
index 73458e8c..db864fd6 100644
--- a/lnbits/wallets/fake.py
+++ b/lnbits/wallets/fake.py
@@ -23,8 +23,8 @@ class FakeWallet(Wallet):
secret: str = settings.fake_wallet_secret
privkey: str = hashlib.pbkdf2_hmac(
"sha256",
- secret.encode("utf-8"),
- ("FakeWallet").encode("utf-8"),
+ secret.encode(),
+ ("FakeWallet").encode(),
2048,
32,
).hex()
@@ -68,9 +68,7 @@ class FakeWallet(Wallet):
data["description"] = memo
randomHash = (
data["privkey"][:6]
- + hashlib.sha256(str(random.getrandbits(256)).encode("utf-8")).hexdigest()[
- 6:
- ]
+ + hashlib.sha256(str(random.getrandbits(256)).encode()).hexdigest()[6:]
)
data["paymenthash"] = randomHash
payment_request = encode(data)
diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py
index 6a196775..3e644796 100644
--- a/lnbits/wallets/lndgrpc.py
+++ b/lnbits/wallets/lndgrpc.py
@@ -71,7 +71,7 @@ def b64_to_bytes(checking_id: str) -> bytes:
def bytes_to_b64(r_hash: bytes) -> str:
- return base64.b64encode(r_hash).decode("utf-8").replace("/", "_")
+ return base64.b64encode(r_hash).decode().replace("/", "_")
def hex_to_b64(hex_str: str) -> str:
diff --git a/pyproject.toml b/pyproject.toml
index ff186de3..5cfbf4dc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,23 +92,12 @@ exclude = """(?x)(
^lnbits/extensions/bleskomat.
| ^lnbits/extensions/boltz.
| ^lnbits/extensions/boltcards.
- | ^lnbits/extensions/events.
- | ^lnbits/extensions/gerty.
- | ^lnbits/extensions/invoices.
| ^lnbits/extensions/livestream.
| ^lnbits/extensions/lnaddress.
- | ^lnbits/extensions/lndhub.
- | ^lnbits/extensions/lnticket.
| ^lnbits/extensions/lnurldevice.
- | ^lnbits/extensions/lnurlp.
- | ^lnbits/extensions/offlineshop.
- | ^lnbits/extensions/paywall.
| ^lnbits/extensions/satspay.
- | ^lnbits/extensions/splitpayments.
| ^lnbits/extensions/streamalerts.
- | ^lnbits/extensions/tpos.
| ^lnbits/extensions/watchonly.
- | ^lnbits/extensions/withdraw.
| ^lnbits/wallets/lnd_grpc_files.
)"""
diff --git a/tests/core/views/test_api.py b/tests/core/views/test_api.py
index fe41fd4e..c6e828b4 100644
--- a/tests/core/views/test_api.py
+++ b/tests/core/views/test_api.py
@@ -215,7 +215,7 @@ async def test_api_payment_with_key(invoice, inkey_headers_from):
@pytest.mark.asyncio
async def test_create_invoice_with_description_hash(client, inkey_headers_to):
data = await get_random_invoice_data()
- descr_hash = hashlib.sha256("asdasdasd".encode("utf-8")).hexdigest()
+ descr_hash = hashlib.sha256("asdasdasd".encode()).hexdigest()
data["description_hash"] = descr_hash
response = await client.post(
@@ -232,8 +232,8 @@ async def test_create_invoice_with_description_hash(client, inkey_headers_to):
@pytest.mark.asyncio
async def test_create_invoice_with_unhashed_description(client, inkey_headers_to):
data = await get_random_invoice_data()
- descr_hash = hashlib.sha256("asdasdasd".encode("utf-8")).hexdigest()
- data["unhashed_description"] = "asdasdasd".encode("utf-8").hex()
+ descr_hash = hashlib.sha256("asdasdasd".encode()).hexdigest()
+ data["unhashed_description"] = "asdasdasd".encode().hex()
response = await client.post(
"/api/v1/payments", json=data, headers=inkey_headers_to