From 5f4fa61310e756ab0fa0490688a774a6b5f37475 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 7 Dec 2022 14:56:45 +0100 Subject: [PATCH 01/54] refactor:depend_admin_user --- lnbits/core/views/api.py | 15 ++++----------- lnbits/decorators.py | 17 +++++++++++++++++ lnbits/extensions/satspay/views_api.py | 11 ++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 995cf9e7..c448a6ab 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -34,11 +34,12 @@ from lnbits.core.models import Payment, Wallet from lnbits.decorators import ( WalletTypeInfo, get_key_type, + require_admin_user, require_admin_key, require_invoice_key, ) from lnbits.helpers import url_for, urlsafe_short_hash -from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE, WALLET +from lnbits.settings import LNBITS_SITE_TITLE, WALLET from lnbits.utils.exchange_rates import ( currencies, fiat_amount_as_satoshis, @@ -84,12 +85,8 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)): @core_app.put("/api/v1/wallet/balance/{amount}") async def api_update_balance( - amount: int, wallet: WalletTypeInfo = Depends(get_key_type) + amount: int, wallet: WalletTypeInfo = Depends(require_admin_user) ): - if wallet.wallet.user not in LNBITS_ADMIN_USERS: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" - ) payHash = urlsafe_short_hash() await create_payment( @@ -687,11 +684,7 @@ async def img(request: Request, data): @core_app.get("/api/v1/audit") -async def api_auditor(wallet: WalletTypeInfo = Depends(get_key_type)): - if wallet.wallet.user not in LNBITS_ADMIN_USERS: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" - ) +async def api_auditor(wallet: WalletTypeInfo = Depends(require_admin_user)): total_balance = await get_total_balance() error_message, node_balance = await WALLET.status() diff --git a/lnbits/decorators.py b/lnbits/decorators.py index d4aa63ae..b8a3d37c 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -172,6 +172,23 @@ async def get_key_type( ) +async def require_admin_user( + r: Request, + api_key_header: str = Security(api_key_header), # type: ignore + api_key_query: str = Security(api_key_query), # type: ignore +): + + token = api_key_header or api_key_query + wallet = await get_key_type(r, token) + + if wallet.wallet.user not in LNBITS_ADMIN_USERS: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" + ) + else: + return wallet + + async def require_admin_key( r: Request, api_key_header: str = Security(api_key_header), # type: ignore diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 09884040..67397132 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -1,20 +1,18 @@ import json from http import HTTPStatus -import httpx from fastapi.params import Depends from loguru import logger from starlette.exceptions import HTTPException -from lnbits.core.crud import get_wallet from lnbits.decorators import ( WalletTypeInfo, get_key_type, + require_admin_user, require_admin_key, require_invoice_key, ) from lnbits.extensions.satspay import satspay_ext -from lnbits.settings import LNBITS_ADMIN_EXTENSIONS, LNBITS_ADMIN_USERS from .crud import ( check_address_balance, @@ -143,14 +141,9 @@ async def api_charge_balance(charge_id): @satspay_ext.post("/api/v1/themes/{css_id}") async def api_themes_save( data: SatsPayThemes, - wallet: WalletTypeInfo = Depends(require_invoice_key), + wallet: WalletTypeInfo = Depends(require_admin_user), css_id: str = None, ): - if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, - detail="Only server admins can create themes.", - ) if css_id: theme = await save_theme(css_id=css_id, data=data) else: From ab4a9370e749ed697ff6068497afee5b9d6aafb4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 7 Dec 2022 14:57:10 +0100 Subject: [PATCH 02/54] style: make format --- lnbits/core/views/api.py | 2 +- lnbits/extensions/satspay/views_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index c448a6ab..5f2b44bd 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -34,8 +34,8 @@ from lnbits.core.models import Payment, Wallet from lnbits.decorators import ( WalletTypeInfo, get_key_type, - require_admin_user, require_admin_key, + require_admin_user, require_invoice_key, ) from lnbits.helpers import url_for, urlsafe_short_hash diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 67397132..c21f31ec 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -8,8 +8,8 @@ from starlette.exceptions import HTTPException from lnbits.decorators import ( WalletTypeInfo, get_key_type, - require_admin_user, require_admin_key, + require_admin_user, require_invoice_key, ) from lnbits.extensions.satspay import satspay_ext From 97d1effe1c0a28ecd6f30f019d5650584a3dcfcf Mon Sep 17 00:00:00 2001 From: Dread <34528298+islandbitcoin@users.noreply.github.com> Date: Mon, 19 Dec 2022 18:55:05 -0500 Subject: [PATCH 03/54] Remove duplicate LNBITS_ALLOWED_USERS --- .env.example | 3 --- 1 file changed, 3 deletions(-) diff --git a/.env.example b/.env.example index 13765574..bb4e64a1 100644 --- a/.env.example +++ b/.env.example @@ -14,9 +14,6 @@ LNBITS_ADMIN_EXTENSIONS="ngrok, admin" # Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available LNBITS_ADMIN_UI=false -# Restricts access, User IDs seperated by comma -LNBITS_ALLOWED_USERS="" - LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" # Ad space description From 4bd0372819a600b22bad8424befc50dc9f14dedd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 20 Dec 2022 10:30:32 +0100 Subject: [PATCH 04/54] deploy: build on minor changes --- .github/workflows/on-tag.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index f6fa53e9..a9f58985 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -7,6 +7,7 @@ on: push: tags: - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+.[0-9]+" - "[0-9]+.[0-9]+.[0-9]+-*" jobs: From 375f5d9f25c6aeacd5f2d4ee7d70150da8660e2d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 20 Dec 2022 11:07:26 +0100 Subject: [PATCH 05/54] docker: update to python 3.10 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a03e1eb2..cc3a14bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim +FROM python:3.10-slim RUN apt-get clean RUN apt-get update From 681883a8f9e74c571bb2d45265986ab23c46ca06 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 19 Dec 2022 19:41:10 +0200 Subject: [PATCH 06/54] fix: catch all exceptions for webhook --- lnbits/extensions/lnurlp/tasks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index 23f312cb..19a39a32 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -2,6 +2,7 @@ import asyncio import json import httpx +from loguru import logger from lnbits.core import db as core_db from lnbits.core.models import Payment @@ -50,7 +51,8 @@ async def on_invoice_paid(payment: Payment) -> None: r = await client.post(pay_link.webhook_url, **kwargs) await mark_webhook_sent(payment, r.status_code) - except (httpx.ConnectError, httpx.RequestError): + except Exception as ex: + logger.error(ex) await mark_webhook_sent(payment, -1) From c5fdd35078d19293c24a895715a72c6d35659426 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 19 Dec 2022 19:42:11 +0200 Subject: [PATCH 07/54] feat: store mor info about the `webhook` call status --- lnbits/extensions/lnurlp/tasks.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index 19a39a32..5de47f2e 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -49,15 +49,22 @@ async def on_invoice_paid(payment: Payment) -> None: if pay_link.webhook_headers: kwargs["headers"] = json.loads(pay_link.webhook_headers) - r = await client.post(pay_link.webhook_url, **kwargs) - await mark_webhook_sent(payment, r.status_code) + r: httpx.Response = await client.post(pay_link.webhook_url, **kwargs) + await mark_webhook_sent( + payment, r.status_code, r.is_success, r.reason_phrase, r.text + ) except Exception as ex: logger.error(ex) - await mark_webhook_sent(payment, -1) + await mark_webhook_sent(payment, -1, False, "Unexpected Error", str(ex)) -async def mark_webhook_sent(payment: Payment, status: int) -> None: - payment.extra["wh_status"] = status +async def mark_webhook_sent( + payment: Payment, status: int, is_success: bool, reason_phrase="", text="" +) -> None: + payment.extra["wh_status"] = status # keep for backwards compability + payment.extra["wh_success"] = is_success + payment.extra["wh_message"] = reason_phrase + payment.extra["wh_response"] = text await core_db.execute( """ From c90f7878d79dbea33a695eb7241a4fce9e5abc23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Tue, 20 Dec 2022 13:14:49 +0100 Subject: [PATCH 08/54] fix usermanager mypy --- lnbits/extensions/usermanager/crud.py | 33 +++++------- lnbits/extensions/usermanager/models.py | 6 +-- lnbits/extensions/usermanager/views.py | 5 +- lnbits/extensions/usermanager/views_api.py | 60 +++++++++++----------- pyproject.toml | 1 - 5 files changed, 50 insertions(+), 55 deletions(-) diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py index 649888a8..d7b64dbf 100644 --- a/lnbits/extensions/usermanager/crud.py +++ b/lnbits/extensions/usermanager/crud.py @@ -10,12 +10,10 @@ from lnbits.core.crud import ( from lnbits.core.models import Payment from . import db -from .models import CreateUserData, Users, Wallets - -### Users +from .models import CreateUserData, User, Wallet -async def create_usermanager_user(data: CreateUserData) -> Users: +async def create_usermanager_user(data: CreateUserData) -> User: account = await create_account() user = await get_user(account.id) assert user, "Newly created user couldn't be retrieved" @@ -50,17 +48,17 @@ async def create_usermanager_user(data: CreateUserData) -> Users: return user_created -async def get_usermanager_user(user_id: str) -> Optional[Users]: +async def get_usermanager_user(user_id: str) -> Optional[User]: row = await db.fetchone("SELECT * FROM usermanager.users WHERE id = ?", (user_id,)) - return Users(**row) if row else None + return User(**row) if row else None -async def get_usermanager_users(user_id: str) -> List[Users]: +async def get_usermanager_users(user_id: str) -> list[User]: rows = await db.fetchall( "SELECT * FROM usermanager.users WHERE admin = ?", (user_id,) ) - return [Users(**row) for row in rows] + return [User(**row) for row in rows] async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> None: @@ -73,12 +71,9 @@ async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> Non await db.execute("""DELETE FROM usermanager.wallets WHERE "user" = ?""", (user_id,)) -### Wallets - - async def create_usermanager_wallet( user_id: str, wallet_name: str, admin_id: str -) -> Wallets: +) -> Wallet: wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name) await db.execute( """ @@ -92,28 +87,28 @@ async def create_usermanager_wallet( return wallet_created -async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]: +async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallet]: row = await db.fetchone( "SELECT * FROM usermanager.wallets WHERE id = ?", (wallet_id,) ) - return Wallets(**row) if row else None + return Wallet(**row) if row else None -async def get_usermanager_wallets(admin_id: str) -> Optional[Wallets]: +async def get_usermanager_wallets(admin_id: str) -> list[Wallet]: rows = await db.fetchall( "SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,) ) - return [Wallets(**row) for row in rows] + return [Wallet(**row) for row in rows] -async def get_usermanager_users_wallets(user_id: str) -> Optional[Wallets]: +async def get_usermanager_users_wallets(user_id: str) -> list[Wallet]: rows = await db.fetchall( """SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,) ) - return [Wallets(**row) for row in rows] + return [Wallet(**row) for row in rows] -async def get_usermanager_wallet_transactions(wallet_id: str) -> Optional[Payment]: +async def get_usermanager_wallet_transactions(wallet_id: str) -> list[Payment]: return await get_payments( wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True ) diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py index 15f50e28..05122cc8 100644 --- a/lnbits/extensions/usermanager/models.py +++ b/lnbits/extensions/usermanager/models.py @@ -19,7 +19,7 @@ class CreateUserWallet(BaseModel): admin_id: str = Query(...) -class Users(BaseModel): +class User(BaseModel): id: str name: str admin: str @@ -27,7 +27,7 @@ class Users(BaseModel): password: Optional[str] = None -class Wallets(BaseModel): +class Wallet(BaseModel): id: str admin: str name: str @@ -36,5 +36,5 @@ class Wallets(BaseModel): inkey: str @classmethod - def from_row(cls, row: Row) -> "Wallets": + def from_row(cls, row: Row) -> "Wallet": return cls(**dict(row)) diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py index 420669b0..1891a447 100644 --- a/lnbits/extensions/usermanager/views.py +++ b/lnbits/extensions/usermanager/views.py @@ -9,7 +9,10 @@ from . import usermanager_ext, usermanager_renderer @usermanager_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): +async def index( + request: Request, + user: User = Depends(check_user_exists) #type: ignore +): 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 b1bf8ef8..06020cf7 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -21,40 +21,44 @@ from .crud import ( get_usermanager_wallet_transactions, get_usermanager_wallets, ) -from .models import CreateUserData, CreateUserWallet - -# Users +from .models import CreateUserData, CreateUserWallet, Wallet @usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK) -async def api_usermanager_users(wallet: WalletTypeInfo = Depends(require_admin_key)): +async def api_usermanager_users( + wallet: WalletTypeInfo = Depends(require_admin_key) #type: ignore +): user_id = wallet.wallet.user return [user.dict() for user in await get_usermanager_users(user_id)] @usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK) -async def api_usermanager_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_usermanager_user( + user_id, + wallet: WalletTypeInfo = Depends(get_key_type) #type: ignore +): user = await get_usermanager_user(user_id) + if not user: + return None return user.dict() -@usermanager_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED) -async def api_usermanager_users_create( - data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) -): +@usermanager_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED, dependencies=[Depends(get_key_type)]) +async def api_usermanager_users_create(data: CreateUserData): user = await create_usermanager_user(data) full = user.dict() - full["wallets"] = [ - wallet.dict() for wallet in await get_usermanager_users_wallets(user.id) - ] + wallets: list[Wallet] = await get_usermanager_users_wallets(user.id) + if wallets: + full["wallets"] = [ + wallet.dict() for wallet in wallets + ] return full -@usermanager_ext.delete("/api/v1/users/{user_id}") +@usermanager_ext.delete("/api/v1/users/{user_id}", dependencies=[Depends(require_admin_key)]) async def api_usermanager_users_delete( user_id, delete_core: bool = Query(True), - wallet: WalletTypeInfo = Depends(require_admin_key), ): user = await get_usermanager_user(user_id) if not user: @@ -84,10 +88,8 @@ async def api_usermanager_activate_extension( # Wallets -@usermanager_ext.post("/api/v1/wallets") -async def api_usermanager_wallets_create( - data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) -): +@usermanager_ext.post("/api/v1/wallets", dependencies=[Depends(get_key_type)]) +async def api_usermanager_wallets_create(data: CreateUserWallet): user = await create_usermanager_wallet( user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id ) @@ -95,31 +97,27 @@ async def api_usermanager_wallets_create( @usermanager_ext.get("/api/v1/wallets") -async def api_usermanager_wallets(wallet: WalletTypeInfo = Depends(require_admin_key)): +async def api_usermanager_wallets( + wallet: WalletTypeInfo = Depends(require_admin_key) #type: ignore +): admin_id = wallet.wallet.user return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)] -@usermanager_ext.get("/api/v1/transactions/{wallet_id}") -async def api_usermanager_wallet_transactions( - wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) -): +@usermanager_ext.get("/api/v1/transactions/{wallet_id}", dependencies=[Depends(get_key_type)]) +async def api_usermanager_wallet_transactions(wallet_id): return await get_usermanager_wallet_transactions(wallet_id) -@usermanager_ext.get("/api/v1/wallets/{user_id}") -async def api_usermanager_users_wallets( - user_id, wallet: WalletTypeInfo = Depends(require_admin_key) -): +@usermanager_ext.get("/api/v1/wallets/{user_id}", dependencies=[Depends(require_admin_key)]) +async def api_usermanager_users_wallets(user_id): return [ s_wallet.dict() for s_wallet in await get_usermanager_users_wallets(user_id) ] -@usermanager_ext.delete("/api/v1/wallets/{wallet_id}") -async def api_usermanager_wallets_delete( - wallet_id, wallet: WalletTypeInfo = Depends(require_admin_key) -): +@usermanager_ext.delete("/api/v1/wallets/{wallet_id}", dependencies=[Depends(require_admin_key)]) +async def api_usermanager_wallets_delete(wallet_id): get_wallet = await get_usermanager_wallet(wallet_id) if not get_wallet: raise HTTPException( diff --git a/pyproject.toml b/pyproject.toml index a08e5f71..e0a42fc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,6 @@ exclude = """(?x)( | ^lnbits/extensions/streamalerts. | ^lnbits/extensions/tipjar. | ^lnbits/extensions/tpos. - | ^lnbits/extensions/usermanager. | ^lnbits/extensions/watchonly. | ^lnbits/extensions/withdraw. | ^lnbits/wallets/lnd_grpc_files. From 22cafe9ed0629f591c2ec606cbc4bf1358a19e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Tue, 20 Dec 2022 13:17:07 +0100 Subject: [PATCH 09/54] remove uneeded if --- lnbits/extensions/usermanager/views_api.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index 06020cf7..84545290 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -47,11 +47,9 @@ async def api_usermanager_user( async def api_usermanager_users_create(data: CreateUserData): user = await create_usermanager_user(data) full = user.dict() - wallets: list[Wallet] = await get_usermanager_users_wallets(user.id) - if wallets: - full["wallets"] = [ - wallet.dict() for wallet in wallets - ] + full["wallets"] = [ + wallet.dict() for wallet in await get_usermanager_users_wallets(user.id) + ] return full From 29869cfcda9575e41a6b91d1beb205ba6a2e44b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Tue, 20 Dec 2022 13:18:37 +0100 Subject: [PATCH 10/54] formatting --- lnbits/extensions/usermanager/views.py | 3 +-- lnbits/extensions/usermanager/views_api.py | 29 +++++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py index 1891a447..814659f7 100644 --- a/lnbits/extensions/usermanager/views.py +++ b/lnbits/extensions/usermanager/views.py @@ -10,8 +10,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 + request: Request, user: User = Depends(check_user_exists) # type: ignore ): 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 84545290..56b5de9a 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -26,7 +26,7 @@ from .models import CreateUserData, CreateUserWallet, Wallet @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), # type: ignore ): user_id = wallet.wallet.user return [user.dict() for user in await get_usermanager_users(user_id)] @@ -34,8 +34,7 @@ async def api_usermanager_users( @usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK) async def api_usermanager_user( - user_id, - wallet: WalletTypeInfo = Depends(get_key_type) #type: ignore + user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore ): user = await get_usermanager_user(user_id) if not user: @@ -43,7 +42,11 @@ async def api_usermanager_user( return user.dict() -@usermanager_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED, dependencies=[Depends(get_key_type)]) +@usermanager_ext.post( + "/api/v1/users", + status_code=HTTPStatus.CREATED, + dependencies=[Depends(get_key_type)], +) async def api_usermanager_users_create(data: CreateUserData): user = await create_usermanager_user(data) full = user.dict() @@ -53,7 +56,9 @@ async def api_usermanager_users_create(data: CreateUserData): return full -@usermanager_ext.delete("/api/v1/users/{user_id}", dependencies=[Depends(require_admin_key)]) +@usermanager_ext.delete( + "/api/v1/users/{user_id}", dependencies=[Depends(require_admin_key)] +) async def api_usermanager_users_delete( user_id, delete_core: bool = Query(True), @@ -96,25 +101,31 @@ 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), # type: ignore ): admin_id = wallet.wallet.user return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)] -@usermanager_ext.get("/api/v1/transactions/{wallet_id}", dependencies=[Depends(get_key_type)]) +@usermanager_ext.get( + "/api/v1/transactions/{wallet_id}", dependencies=[Depends(get_key_type)] +) async def api_usermanager_wallet_transactions(wallet_id): return await get_usermanager_wallet_transactions(wallet_id) -@usermanager_ext.get("/api/v1/wallets/{user_id}", dependencies=[Depends(require_admin_key)]) +@usermanager_ext.get( + "/api/v1/wallets/{user_id}", dependencies=[Depends(require_admin_key)] +) async def api_usermanager_users_wallets(user_id): return [ s_wallet.dict() for s_wallet in await get_usermanager_users_wallets(user_id) ] -@usermanager_ext.delete("/api/v1/wallets/{wallet_id}", dependencies=[Depends(require_admin_key)]) +@usermanager_ext.delete( + "/api/v1/wallets/{wallet_id}", dependencies=[Depends(require_admin_key)] +) async def api_usermanager_wallets_delete(wallet_id): get_wallet = await get_usermanager_wallet(wallet_id) if not get_wallet: From 36ae851a8d4721e13fd8a97ab12ef75e6506e396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Tue, 20 Dec 2022 13:44:14 +0100 Subject: [PATCH 11/54] nitpicking vlad :) --- lnbits/extensions/usermanager/crud.py | 8 ++++---- lnbits/extensions/usermanager/views_api.py | 12 ++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py index d7b64dbf..959914f2 100644 --- a/lnbits/extensions/usermanager/crud.py +++ b/lnbits/extensions/usermanager/crud.py @@ -53,7 +53,7 @@ async def get_usermanager_user(user_id: str) -> Optional[User]: return User(**row) if row else None -async def get_usermanager_users(user_id: str) -> list[User]: +async def get_usermanager_users(user_id: str) -> List[User]: rows = await db.fetchall( "SELECT * FROM usermanager.users WHERE admin = ?", (user_id,) ) @@ -94,21 +94,21 @@ async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallet]: return Wallet(**row) if row else None -async def get_usermanager_wallets(admin_id: str) -> list[Wallet]: +async def get_usermanager_wallets(admin_id: str) -> List[Wallet]: rows = await db.fetchall( "SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,) ) return [Wallet(**row) for row in rows] -async def get_usermanager_users_wallets(user_id: str) -> list[Wallet]: +async def get_usermanager_users_wallets(user_id: str) -> List[Wallet]: rows = await db.fetchall( """SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,) ) return [Wallet(**row) for row in rows] -async def get_usermanager_wallet_transactions(wallet_id: str) -> list[Payment]: +async def get_usermanager_wallet_transactions(wallet_id: str) -> List[Payment]: return await get_payments( wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True ) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index 56b5de9a..bd111d49 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -21,7 +21,7 @@ from .crud import ( get_usermanager_wallet_transactions, get_usermanager_wallets, ) -from .models import CreateUserData, CreateUserWallet, Wallet +from .models import CreateUserData, CreateUserWallet @usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK) @@ -32,14 +32,10 @@ async def api_usermanager_users( return [user.dict() for user in await get_usermanager_users(user_id)] -@usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK) -async def api_usermanager_user( - user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore -): +@usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK, dependencies=[Depends(get_key_type)]) +async def api_usermanager_user(user_id): user = await get_usermanager_user(user_id) - if not user: - return None - return user.dict() + return user.dict() if user else None @usermanager_ext.post( From c2df13bf24dfafbd69db677ccb7329f1d4fccd15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Tue, 20 Dec 2022 14:23:02 +0100 Subject: [PATCH 12/54] formatting --- lnbits/extensions/usermanager/views_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index bd111d49..493a71bc 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -32,7 +32,11 @@ async def api_usermanager_users( return [user.dict() for user in await get_usermanager_users(user_id)] -@usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK, dependencies=[Depends(get_key_type)]) +@usermanager_ext.get( + "/api/v1/users/{user_id}", + status_code=HTTPStatus.OK, + dependencies=[Depends(get_key_type)], +) async def api_usermanager_user(user_id): user = await get_usermanager_user(user_id) return user.dict() if user else None From b68b8a0292516604c5a14f082cf0d1fce05e0cca Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 21 Dec 2022 14:56:27 +0200 Subject: [PATCH 13/54] fix: do not loose error, log it --- lnbits/core/views/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index ebce4b85..2bd19978 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -214,7 +214,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): lnurl_response = resp["reason"] else: lnurl_response = True - except (httpx.ConnectError, httpx.RequestError): + except (httpx.ConnectError, httpx.RequestError) as ex: + logger.error(ex) lnurl_response = False return { From dd4a9f10cf4c965f8efcf7285503bdbf57d04727 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 21 Dec 2022 14:57:00 +0200 Subject: [PATCH 14/54] feat: add function to update the `extra` JSON values --- lnbits/core/crud.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 1c8c71ad..771ff397 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -451,6 +451,36 @@ async def update_payment_details( return +async def update_payment_extra( + payment_hash: str, + extra: dict, + conn: Optional[Connection] = None, +) -> None: + """ + Only update the `extra` field for the payment. + Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them. + """ + + row = await (conn or db).fetchone( + "SELECT hash, extra from apipayments WHERE hash = ?", + (payment_hash), + ) + if not row: + return + existing_extra = json.loads(row["extra"] if row["extra"] else "{}") + new_extra = { + **existing_extra, + **extra, + } + await (conn or db).execute( + """ + UPDATE apipayments SET extra = ? + WHERE hash = ? + """, + (json.dumps(new_extra), payment_hash), + ) + + async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None: await (conn or db).execute( "DELETE FROM apipayments WHERE checking_id = ?", (checking_id,) From 8fae90bb9dcd5976231fc7942f030f00adc8a75e Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 21 Dec 2022 15:03:22 +0200 Subject: [PATCH 15/54] feat: add `webhook_headers` and `webhook_body` to withdraws --- lnbits/extensions/withdraw/crud.py | 6 ++- lnbits/extensions/withdraw/migrations.py | 10 +++++ lnbits/extensions/withdraw/models.py | 4 ++ lnbits/extensions/withdraw/static/js/index.js | 37 +++++++++++++------ .../withdraw/templates/withdraw/index.html | 24 ++++++++++++ 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 9868b057..83404c62 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -27,9 +27,11 @@ async def create_withdraw_link( open_time, usescsv, webhook_url, + webhook_headers, + webhook_body, custom_url ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( link_id, @@ -45,6 +47,8 @@ async def create_withdraw_link( int(datetime.now().timestamp()) + data.wait_time, usescsv, data.webhook_url, + data.webhook_headers, + data.webhook_body, data.custom_url, ), ) diff --git a/lnbits/extensions/withdraw/migrations.py b/lnbits/extensions/withdraw/migrations.py index 0c6ed4fc..95805ae7 100644 --- a/lnbits/extensions/withdraw/migrations.py +++ b/lnbits/extensions/withdraw/migrations.py @@ -122,3 +122,13 @@ async def m005_add_custom_print_design(db): Adds custom print design """ await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;") + + +async def m006_webhook_headers_and_body(db): + """ + Add headers and body to webhooks + """ + await db.execute( + "ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_headers TEXT;" + ) + await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_body TEXT;") diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 2672537f..51c6a1cf 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -16,6 +16,8 @@ class CreateWithdrawData(BaseModel): wait_time: int = Query(..., ge=1) is_unique: bool webhook_url: str = Query(None) + webhook_headers: str = Query(None) + webhook_body: str = Query(None) custom_url: str = Query(None) @@ -35,6 +37,8 @@ class WithdrawLink(BaseModel): usescsv: str = Query(None) number: int = Query(0) webhook_url: str = Query(None) + webhook_headers: str = Query(None) + webhook_body: str = Query(None) custom_url: str = Query(None) @property diff --git a/lnbits/extensions/withdraw/static/js/index.js b/lnbits/extensions/withdraw/static/js/index.js index a3eaa593..ced78439 100644 --- a/lnbits/extensions/withdraw/static/js/index.js +++ b/lnbits/extensions/withdraw/static/js/index.js @@ -63,7 +63,8 @@ new Vue({ secondMultiplierOptions: ['seconds', 'minutes', 'hours'], data: { is_unique: false, - use_custom: false + use_custom: false, + has_webhook: false } }, simpleformDialog: { @@ -188,23 +189,35 @@ new Vue({ }, updateWithdrawLink: function (wallet, data) { var self = this + const body = _.pick( + data, + 'title', + 'min_withdrawable', + 'max_withdrawable', + 'uses', + 'wait_time', + 'is_unique', + 'webhook_url', + 'webhook_headers', + 'webhook_body', + 'custom_url' + ) + + if (data.has_webhook) { + body = { + ...body, + webhook_url: data.webhook_url, + webhook_headers: data.webhook_headers, + webhook_body: data.webhook_body + } + } LNbits.api .request( 'PUT', '/withdraw/api/v1/links/' + data.id, wallet.adminkey, - _.pick( - data, - 'title', - 'min_withdrawable', - 'max_withdrawable', - 'uses', - 'wait_time', - 'is_unique', - 'webhook_url', - 'custom_url' - ) + body ) .then(function (response) { self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) { diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html index 27684f6b..3ae244e6 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ b/lnbits/extensions/withdraw/templates/withdraw/index.html @@ -209,7 +209,13 @@ + + + From 0ab99bef414d47bce4413a4f3fb012b11a055a87 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 21 Dec 2022 15:04:49 +0200 Subject: [PATCH 16/54] refactor: use `update_payment_extra` --- lnbits/extensions/lnurlp/tasks.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index 5de47f2e..72805603 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -5,6 +5,7 @@ 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 from lnbits.tasks import register_invoice_listener @@ -66,10 +67,4 @@ async def mark_webhook_sent( payment.extra["wh_message"] = reason_phrase payment.extra["wh_response"] = text - await core_db.execute( - """ - UPDATE apipayments SET extra = ? - WHERE hash = ? - """, - (json.dumps(payment.extra), payment.payment_hash), - ) + await update_payment_extra(payment.payment_hash, payment.extra) From 60cd40d5e524d77945196d266056f7d24a0e8880 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 21 Dec 2022 15:05:28 +0200 Subject: [PATCH 17/54] feat: `update_payment_extra` with webhook response --- lnbits/extensions/withdraw/lnurl.py | 38 ++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 5737e54f..17ef6464 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -11,6 +11,7 @@ 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 @@ -44,7 +45,11 @@ async def api_lnurl_response(request: Request, unique_hash): "minWithdrawable": link.min_withdrawable * 1000, "maxWithdrawable": link.max_withdrawable * 1000, "defaultDescription": link.title, + "webhook_url": link.webhook_url, + "webhook_headers": link.webhook_headers, + "webhook_body": link.webhook_body, } + return json.dumps(withdrawResponse) @@ -56,7 +61,7 @@ async def api_lnurl_response(request: Request, unique_hash): name="withdraw.api_lnurl_callback", summary="lnurl withdraw callback", description=""" - This enpoints allows you to put unique_hash, k1 + This endpoints allows you to put unique_hash, k1 and a payment_request to get your payment_request paid. """, response_description="JSON with status", @@ -143,18 +148,39 @@ async def api_lnurl_callback( if link.webhook_url: async with httpx.AsyncClient() as client: try: - r = await client.post( - link.webhook_url, - json={ + kwargs = { + "json": { "payment_hash": payment_hash, "payment_request": payment_request, "lnurlw": link.id, }, - timeout=40, + "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, + dict( + { + "wh_success": r.is_success, + "wh_message": r.reason_phrase, + "wh_response": r.text, + } + ), ) 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:", exc) + logger.error( + "Caught exception when dispatching webhook url: " + str(exc) + ) + await update_payment_extra( + payment_hash, + dict({"wh_success": False, "wh_message": str(exc)}), + ) return {"status": "OK"} From ec01c74da183a45b4218f89503a35ad4e1826556 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 21 Dec 2022 15:10:12 +0200 Subject: [PATCH 18/54] fix: `mypy` --- lnbits/core/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 771ff397..8f514bea 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -463,7 +463,7 @@ async def update_payment_extra( row = await (conn or db).fetchone( "SELECT hash, extra from apipayments WHERE hash = ?", - (payment_hash), + (payment_hash,), ) if not row: return From f3c884111d6aed64dfdb8c0a83e6ca92f920ae1d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 21 Dec 2022 15:47:22 +0200 Subject: [PATCH 19/54] refactor: use `dict.update()` --- lnbits/core/crud.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 8f514bea..7d9f2a5b 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -467,17 +467,15 @@ async def update_payment_extra( ) if not row: return - existing_extra = json.loads(row["extra"] if row["extra"] else "{}") - new_extra = { - **existing_extra, - **extra, - } + db_extra = json.loads(row["extra"] if row["extra"] else "{}") + db_extra.update(extra) + await (conn or db).execute( """ UPDATE apipayments SET extra = ? WHERE hash = ? """, - (json.dumps(new_extra), payment_hash), + (json.dumps(db_extra), payment_hash), ) From 7b0f9f61fc2092f6dcf16a354d42e058e7e65c0c Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 21 Dec 2022 15:47:35 +0200 Subject: [PATCH 20/54] fix: remove double dict --- lnbits/extensions/withdraw/lnurl.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 17ef6464..7260df1e 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -164,13 +164,11 @@ async def api_lnurl_callback( r: httpx.Response = await client.post(link.webhook_url, **kwargs) await update_payment_extra( payment_hash, - dict( - { - "wh_success": r.is_success, - "wh_message": r.reason_phrase, - "wh_response": r.text, - } - ), + { + "wh_success": r.is_success, + "wh_message": r.reason_phrase, + "wh_response": r.text, + }, ) except Exception as exc: # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid @@ -179,7 +177,7 @@ async def api_lnurl_callback( ) await update_payment_extra( payment_hash, - dict({"wh_success": False, "wh_message": str(exc)}), + {"wh_success": False, "wh_message": str(exc)}, ) return {"status": "OK"} From 8d5f5e4afbf7230a1cc6f0793009b79f9222c11c Mon Sep 17 00:00:00 2001 From: Axel <84231978+AxelHamburch@users.noreply.github.com> Date: Wed, 21 Dec 2022 21:33:10 +0100 Subject: [PATCH 21/54] Add "How to activat the Admin UI" --- docs/guide/admin_ui.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/guide/admin_ui.md b/docs/guide/admin_ui.md index 1248d3f3..a870a2ba 100644 --- a/docs/guide/admin_ui.md +++ b/docs/guide/admin_ui.md @@ -40,3 +40,33 @@ Allowed Users ============= enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend. + + +How to activat +============= +``` +$ sudo systemctl stop lnbits.service +$ cd ~/lnbits-legend +$ sudo nano .env +``` +-> set: `LNBITS_ADMIN_UI=true` + +Now start LNbits once in the terminal window +``` +$ poetry run lnbits +``` +It will now show you the Super User Account: + +`SUCCESS | ✔️ Access super user account at: https://127.0.0.1:5000/wallet?usr=5711d7..` + +The `/wallet?usr=..` is your super user account. You just have to append it to your normal LNbits web domain. + +After that you will find the __`Admin` / `Manage Server`__ between `Wallets` and `Extensions` + +Here you can design the interface, it has TOPUP to fill wallets and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee. + +Do not forget +``` +sudo systemctl start lnbits.service +``` +A little hint, if you set `RESET TO DEFAULTS`, then a new Super User Account will also be created. The old one is then no longer valid. From b56d08a70e0c4a5866d3299e054fe95723058ef1 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 22 Dec 2022 11:12:19 +0200 Subject: [PATCH 22/54] fix: handle `extra` updates for internal payments --- lnbits/core/crud.py | 22 ++++++++++++++-------- lnbits/extensions/withdraw/lnurl.py | 5 +++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 7d9f2a5b..bed28111 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -452,18 +452,27 @@ async def update_payment_details( async def update_payment_extra( - payment_hash: str, + hash: str, extra: dict, + outgoing: bool = False, + incoming: bool = False, conn: Optional[Connection] = None, ) -> None: """ Only update the `extra` field for the payment. Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them. """ + amount_clause = "" + + if outgoing != incoming: + if outgoing: + amount_clause = "AND amount < 0" + else: + amount_clause = "AND amount > 0" row = await (conn or db).fetchone( - "SELECT hash, extra from apipayments WHERE hash = ?", - (payment_hash,), + f"SELECT hash, extra from apipayments WHERE hash = ? {amount_clause}", + (hash,), ) if not row: return @@ -471,11 +480,8 @@ async def update_payment_extra( db_extra.update(extra) await (conn or db).execute( - """ - UPDATE apipayments SET extra = ? - WHERE hash = ? - """, - (json.dumps(db_extra), payment_hash), + f"UPDATE apipayments SET extra = ? WHERE hash = ? {amount_clause} ", + (json.dumps(db_extra), hash), ) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 7260df1e..48ccaf4a 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -163,12 +163,13 @@ async def api_lnurl_callback( r: httpx.Response = await client.post(link.webhook_url, **kwargs) await update_payment_extra( - payment_hash, - { + 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 From a434731729db18632e134fa9ccceb709f3aed3fd Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 22 Dec 2022 11:28:12 +0200 Subject: [PATCH 23/54] fix: update the rest of `update_payment_extra` calls --- lnbits/core/crud.py | 6 +++--- lnbits/extensions/lnurlp/tasks.py | 4 +++- lnbits/extensions/withdraw/lnurl.py | 7 ++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index bed28111..b38a3d14 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -452,7 +452,7 @@ async def update_payment_details( async def update_payment_extra( - hash: str, + payment_hash: str, extra: dict, outgoing: bool = False, incoming: bool = False, @@ -472,7 +472,7 @@ async def update_payment_extra( row = await (conn or db).fetchone( f"SELECT hash, extra from apipayments WHERE hash = ? {amount_clause}", - (hash,), + (payment_hash,), ) if not row: return @@ -481,7 +481,7 @@ async def update_payment_extra( await (conn or db).execute( f"UPDATE apipayments SET extra = ? WHERE hash = ? {amount_clause} ", - (json.dumps(db_extra), hash), + (json.dumps(db_extra), payment_hash), ) diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index 72805603..e243e1a4 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -67,4 +67,6 @@ async def mark_webhook_sent( payment.extra["wh_message"] = reason_phrase payment.extra["wh_response"] = text - await update_payment_extra(payment.payment_hash, payment.extra) + await update_payment_extra( + payment_hash=payment.payment_hash, extra=payment.extra, incoming=True + ) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 48ccaf4a..86640443 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -163,7 +163,7 @@ async def api_lnurl_callback( r: httpx.Response = await client.post(link.webhook_url, **kwargs) await update_payment_extra( - hash=payment_hash, + payment_hash=payment_hash, extra={ "wh_success": r.is_success, "wh_message": r.reason_phrase, @@ -177,8 +177,9 @@ async def api_lnurl_callback( "Caught exception when dispatching webhook url: " + str(exc) ) await update_payment_extra( - payment_hash, - {"wh_success": False, "wh_message": str(exc)}, + payment_hash=payment_hash, + extra={"wh_success": False, "wh_message": str(exc)}, + outgoing=True, ) return {"status": "OK"} From 49a68df5e4fd3a7c4169949ddf38845c808464d7 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Thu, 22 Dec 2022 11:07:49 +0100 Subject: [PATCH 24/54] Apply suggestions from code review --- docs/guide/admin_ui.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/admin_ui.md b/docs/guide/admin_ui.md index a870a2ba..9637a989 100644 --- a/docs/guide/admin_ui.md +++ b/docs/guide/admin_ui.md @@ -42,7 +42,7 @@ enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend. -How to activat +How to activate ============= ``` $ sudo systemctl stop lnbits.service From 98e0ea5ae271fd11497202ca4137bc44151b481c Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 22 Dec 2022 12:23:46 +0200 Subject: [PATCH 25/54] refactor: only use one (direction) flag for identifying payment --- lnbits/core/crud.py | 8 +------- lnbits/extensions/lnurlp/tasks.py | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index b38a3d14..a80fadf2 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -455,20 +455,14 @@ async def update_payment_extra( payment_hash: str, extra: dict, outgoing: bool = False, - incoming: bool = False, conn: Optional[Connection] = None, ) -> None: """ Only update the `extra` field for the payment. Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them. """ - amount_clause = "" - if outgoing != incoming: - if outgoing: - amount_clause = "AND amount < 0" - else: - amount_clause = "AND amount > 0" + amount_clause = "AND amount < 0" if outgoing else "AND amount > 0" row = await (conn or db).fetchone( f"SELECT hash, extra from apipayments WHERE hash = ? {amount_clause}", diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index e243e1a4..b8da5e43 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -52,21 +52,29 @@ async def on_invoice_paid(payment: Payment) -> None: r: httpx.Response = await client.post(pay_link.webhook_url, **kwargs) await mark_webhook_sent( - payment, r.status_code, r.is_success, r.reason_phrase, r.text + payment.payment_hash, + r.status_code, + r.is_success, + r.reason_phrase, + r.text, ) except Exception as ex: logger.error(ex) - await mark_webhook_sent(payment, -1, False, "Unexpected Error", str(ex)) + await mark_webhook_sent( + payment.payment_hash, -1, False, "Unexpected Error", str(ex) + ) async def mark_webhook_sent( - payment: Payment, status: int, is_success: bool, reason_phrase="", text="" + payment_hash: str, status: int, is_success: bool, reason_phrase="", text="" ) -> None: - payment.extra["wh_status"] = status # keep for backwards compability - payment.extra["wh_success"] = is_success - payment.extra["wh_message"] = reason_phrase - payment.extra["wh_response"] = text await update_payment_extra( - payment_hash=payment.payment_hash, extra=payment.extra, incoming=True + payment_hash, + { + "wh_status": status, # keep for backwards compability + "wh_success": is_success, + "wh_message": reason_phrase, + "wh_response": text, + }, ) From 7c96a12a3ca2b9e1093b770dda84e067a9938251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 22 Dec 2022 17:48:46 +0100 Subject: [PATCH 26/54] fix issue with allowed_users and admin/superuser --- lnbits/decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/decorators.py b/lnbits/decorators.py index e5bc1399..dea0a54b 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -236,8 +236,8 @@ async def check_user_exists(usr: UUID4) -> User: if ( len(settings.lnbits_allowed_users) > 0 and g().user.id not in settings.lnbits_allowed_users - and g().user.id != settings.super_user - and g().user.id not in settings.lnbits_admin_users + or g().user.id != settings.super_user + or g().user.id not in settings.lnbits_admin_users ): raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." From 1f75d5fd412b6727c22aa52f42ac82075b040ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 22 Dec 2022 17:58:49 +0100 Subject: [PATCH 27/54] fix it up --- lnbits/decorators.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lnbits/decorators.py b/lnbits/decorators.py index dea0a54b..74559db6 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -233,15 +233,15 @@ async def check_user_exists(usr: UUID4) -> User: status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." ) - if ( - len(settings.lnbits_allowed_users) > 0 - and g().user.id not in settings.lnbits_allowed_users - or g().user.id != settings.super_user - or g().user.id not in settings.lnbits_admin_users - ): - raise HTTPException( - status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." - ) + if len(settings.lnbits_allowed_users) > 0: + if ( + g().user.id not in settings.lnbits_allowed_users + or g().user.id != settings.super_user + or g().user.id not in settings.lnbits_admin_users + ): + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." + ) return g().user From 090cf414e7754d1eb290310b7e0f3826513bf044 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 22 Dec 2022 19:58:01 +0100 Subject: [PATCH 28/54] cashu: add more logging --- lnbits/extensions/cashu/views_api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 2d10f3f0..2e613b07 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -299,14 +299,20 @@ async def melt_coins( cashu.wallet, invoice_obj.payment_hash ) if status.paid == True: - logger.debug("Cashu: Payment successful, invalidating proofs") + logger.debug( + f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}" + ) await ledger._invalidate_proofs(proofs) + else: + logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}") except Exception as e: + logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {e}") raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=f"Cashu: {str(e)}", ) finally: + logger.debug(f"Cashu: Unset pending for {invoice_obj.payment_hash}: {e}") # delete proofs from pending list await ledger._unset_proofs_pending(proofs) From 0d8ca58a675d8bdf546a064eb07f5cedaabda1f8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 22 Dec 2022 20:10:01 +0100 Subject: [PATCH 29/54] fix logger --- lnbits/extensions/cashu/views_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 2e613b07..1442634c 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -306,13 +306,13 @@ async def melt_coins( else: logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}") except Exception as e: - logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {e}") + logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}") raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=f"Cashu: {str(e)}", ) finally: - logger.debug(f"Cashu: Unset pending for {invoice_obj.payment_hash}: {e}") + logger.debug(f"Cashu: Unset pending for {invoice_obj.payment_hash}") # delete proofs from pending list await ledger._unset_proofs_pending(proofs) From 1be460aec3d2330be404ffa5ace43e2b6d943107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Fri, 23 Dec 2022 11:53:45 +0100 Subject: [PATCH 30/54] fix user not exist bug in frontend --- lnbits/core/views/generic.py | 5 ++++- lnbits/decorators.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 9df133b2..ed7b156d 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -38,7 +38,7 @@ async def favicon(): @core_html_routes.get("/", response_class=HTMLResponse) -async def home(request: Request, lightning: str = None): +async def home(request: Request, lightning: str = ""): return template_renderer().TemplateResponse( "core/index.html", {"request": request, "lnurl": lightning} ) @@ -124,12 +124,15 @@ async def wallet( if ( len(settings.lnbits_allowed_users) > 0 and user_id not in settings.lnbits_allowed_users + and user_id not in settings.lnbits_admin_users + and user_id != settings.super_user ): return template_renderer().TemplateResponse( "error.html", {"request": request, "err": "User not authorized."} ) if user_id == settings.super_user or user_id in settings.lnbits_admin_users: user.admin = True + if not wallet_id: if user.wallets and not wallet_name: # type: ignore wallet = user.wallets[0] # type: ignore diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 74559db6..9aeace40 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -233,15 +233,15 @@ async def check_user_exists(usr: UUID4) -> User: status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." ) - if len(settings.lnbits_allowed_users) > 0: - if ( - g().user.id not in settings.lnbits_allowed_users - or g().user.id != settings.super_user - or g().user.id not in settings.lnbits_admin_users - ): - raise HTTPException( - status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." - ) + if ( + len(settings.lnbits_allowed_users) > 0 + and g().user.id not in settings.lnbits_allowed_users + and g().user.id not in settings.lnbits_admin_users + and g().user.id != settings.super_user + ): + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." + ) return g().user From 886070755c10542a7543423fd6a432e6fa34e55a Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 23 Dec 2022 15:35:06 +0000 Subject: [PATCH 31/54] hacky lnurlpos bump --- lnbits/extensions/lnurldevice/crud.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index e02d23b8..b1fe24aa 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -11,7 +11,8 @@ from .models import createLnurldevice, lnurldevicepayment, lnurldevices async def create_lnurldevice( data: createLnurldevice, ) -> lnurldevices: - lnurldevice_id = urlsafe_short_hash() + if data.device == "pos": + lnurldevice_id = get_lnurldeviceposcount() lnurldevice_key = urlsafe_short_hash() await db.execute( """ @@ -79,13 +80,18 @@ 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 = pos" + ) + return len(row) + 1 + async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices: row = await db.fetchone( "SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,) ) return lnurldevices(**row) if row else None - async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]: wallet_ids = [wallet_ids] q = ",".join(["?"] * len(wallet_ids[0])) From 03846adbfb25ed5fef2fd6bcacc6f5f90fb5c4a7 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 23 Dec 2022 15:40:05 +0000 Subject: [PATCH 32/54] unsupported type --- lnbits/extensions/lnurldevice/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index b1fe24aa..f5165abe 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -12,7 +12,7 @@ async def create_lnurldevice( data: createLnurldevice, ) -> lnurldevices: if data.device == "pos": - lnurldevice_id = get_lnurldeviceposcount() + lnurldevice_id = str(get_lnurldeviceposcount()) lnurldevice_key = urlsafe_short_hash() await db.execute( """ From 0ce987a20eb2e58384044c0041587659d9a99feb Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 23 Dec 2022 15:40:52 +0000 Subject: [PATCH 33/54] await --- lnbits/extensions/lnurldevice/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index f5165abe..ebcadd9a 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -12,7 +12,7 @@ async def create_lnurldevice( data: createLnurldevice, ) -> lnurldevices: if data.device == "pos": - lnurldevice_id = str(get_lnurldeviceposcount()) + lnurldevice_id = str(await get_lnurldeviceposcount()) lnurldevice_key = urlsafe_short_hash() await db.execute( """ From dce50105c9ad287e76d1888c374b14f89ac56fb7 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 23 Dec 2022 15:41:59 +0000 Subject: [PATCH 34/54] no such column --- lnbits/extensions/lnurldevice/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index ebcadd9a..d761edf4 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -82,7 +82,7 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev async def get_lnurldeviceposcount() -> int: row = await db.fetchall( - "SELECT * FROM lnurldevice.lnurldevices WHERE device = pos" + "SELECT * FROM lnurldevice.lnurldevices WHERE device" ) return len(row) + 1 From 8a447ecc4fe9e93d5adcf093008a08ac21538d42 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 23 Dec 2022 22:15:32 +0200 Subject: [PATCH 35/54] fix: onchain wallet deleted for charge --- lnbits/extensions/satspay/crud.py | 2 ++ lnbits/extensions/satspay/views_api.py | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py index bc364323..7cbcec5f 100644 --- a/lnbits/extensions/satspay/crud.py +++ b/lnbits/extensions/satspay/crud.py @@ -24,6 +24,8 @@ async def create_charge(user: str, data: CreateCharge) -> Charges: {"mempool_endpoint": config.mempool_endpoint, "network": config.network} ) onchain = await get_fresh_address(data.onchainwallet) + if not onchain: + raise Exception(f"Wallet '{data.onchainwallet}' can no longer be accessed.") onchainaddress = onchain.address else: onchainaddress = None diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 798e0df9..8c9ed471 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -37,13 +37,18 @@ from .models import CreateCharge, SatsPayThemes async def api_charge_create( data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key) ): - charge = await create_charge(user=wallet.wallet.user, data=data) - return { - **charge.dict(), - **{"time_elapsed": charge.time_elapsed}, - **{"time_left": charge.time_left}, - **{"paid": charge.paid}, - } + try: + charge = await create_charge(user=wallet.wallet.user, data=data) + return { + **charge.dict(), + **{"time_elapsed": charge.time_elapsed}, + **{"time_left": charge.time_left}, + **{"paid": charge.paid}, + } + except Exception as ex: + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex) + ) @satspay_ext.put("/api/v1/charge/{charge_id}") From 50addd73dd6dbb3cacbc09603743567e0d5d0d14 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 23 Dec 2022 22:21:14 +0200 Subject: [PATCH 36/54] chore: log error in debug mode --- lnbits/extensions/satspay/views_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 8c9ed471..4a62c3d5 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -46,6 +46,7 @@ async def api_charge_create( **{"paid": charge.paid}, } except Exception as ex: + logger.debug(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex) ) From cd6f4c909f9693a11ac7f34f8f83a7c8544468b9 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 23 Dec 2022 22:38:19 +0000 Subject: [PATCH 37/54] swaps pos id for number --- lnbits/extensions/lnurldevice/crud.py | 6 ++++-- lnbits/extensions/lnurldevice/models.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index d761edf4..708827b0 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -11,8 +11,10 @@ from .models import createLnurldevice, lnurldevicepayment, lnurldevices async def create_lnurldevice( data: createLnurldevice, ) -> lnurldevices: - if data.device == "pos": + if data.device == "pos" or data.device == "atm": lnurldevice_id = str(await get_lnurldeviceposcount()) + else: + lnurldevice_id = urlsafe_short_hash() lnurldevice_key = urlsafe_short_hash() await db.execute( """ @@ -82,7 +84,7 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev async def get_lnurldeviceposcount() -> int: row = await db.fetchall( - "SELECT * FROM lnurldevice.lnurldevices WHERE device" + "SELECT * FROM lnurldevice.lnurldevices WHERE device = ? OR device = ?", ("pos", "atm",) ) return len(row) + 1 diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index c27470b7..66b215f2 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -17,8 +17,8 @@ class createLnurldevice(BaseModel): wallet: str currency: str device: str - profit: float - amount: int + profit: float = 0 + amount: Optional[int] = 0 pin: int = 0 profit1: float = 0 amount1: int = 0 From dd90c3a2eb35f40ea3aa5fc660d336962bf55193 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 23 Dec 2022 22:47:14 +0000 Subject: [PATCH 38/54] format --- lnbits/extensions/lnurldevice/crud.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index 708827b0..18451848 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -84,16 +84,22 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev async def get_lnurldeviceposcount() -> int: row = await db.fetchall( - "SELECT * FROM lnurldevice.lnurldevices WHERE device = ? OR device = ?", ("pos", "atm",) + "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,) ) return lnurldevices(**row) if row else None + async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]: wallet_ids = [wallet_ids] q = ",".join(["?"] * len(wallet_ids[0])) From 1fd351b959257f211fbf76847a215a0fb195e715 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 02:32:40 +0100 Subject: [PATCH 39/54] refactor wallet --- .../cashu/templates/cashu/wallet.html | 634 +++++++++++------- lnbits/extensions/cashu/views_api.py | 111 +-- 2 files changed, 440 insertions(+), 305 deletions(-) diff --git a/lnbits/extensions/cashu/templates/cashu/wallet.html b/lnbits/extensions/cashu/templates/cashu/wallet.html index 88dffe7c..d2def94d 100644 --- a/lnbits/extensions/cashu/templates/cashu/wallet.html +++ b/lnbits/extensions/cashu/templates/cashu/wallet.html @@ -159,7 +159,7 @@ page_container %} size="lg" color="secondary" class="q-mr-md cursor-pointer" - @click="recheckInvoice(props.row.hash)" + @click="checkInvoice(props.row.hash)" > Check @@ -1528,57 +1528,17 @@ page_container %} return proofs.reduce((s, t) => (s += t.amount), 0) }, + deleteProofs: function (proofs) { + // delete proofs from this.proofs + const usedSecrets = proofs.map(p => p.secret) + this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret)) + this.storeProofs() + return this.proofs + }, + //////////// API /////////// - clearAllWorkers: function () { - if (this.invoiceCheckListener) { - clearInterval(this.invoiceCheckListener) - } - if (this.tokensCheckSpendableListener) { - clearInterval(this.tokensCheckSpendableListener) - } - }, - invoiceCheckWorker: async function () { - let nInterval = 0 - this.clearAllWorkers() - this.invoiceCheckListener = setInterval(async () => { - try { - nInterval += 1 - // exit loop after 2m - if (nInterval > 40) { - console.log('### stopping invoice check worker') - this.clearAllWorkers() - } - console.log('### invoiceCheckWorker setInterval', nInterval) - console.log(this.invoiceData) - - // this will throw an error if the invoice is pending - await this.recheckInvoice(this.invoiceData.hash, false) - - // only without error (invoice paid) will we reach here - console.log('### stopping invoice check worker') - this.clearAllWorkers() - this.invoiceData.bolt11 = '' - this.showInvoiceDetails = false - if (window.navigator.vibrate) navigator.vibrate(200) - this.$q.notify({ - timeout: 5000, - type: 'positive', - message: 'Payment received', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) - } catch (error) { - console.log('not paid yet') - } - }, 3000) - }, + // MINT requestMintButton: async function () { await this.requestMint() @@ -1586,8 +1546,12 @@ page_container %} await this.invoiceCheckWorker() }, + // /mint + requestMint: async function () { - // gets an invoice from the mint to get new tokens + /* + gets an invoice from the mint to get new tokens + */ try { const {data} = await LNbits.api.request( 'GET', @@ -1611,7 +1575,14 @@ page_container %} throw error } }, + + // /mint + mintApi: async function (amounts, payment_hash, verbose = true) { + /* + asks the mint to check whether the invoice with payment_hash has been paid + and requests signing of the attached outputs (blindedMessages) + */ console.log('### promises', payment_hash) try { let secrets = await this.generateSecrets(amounts) @@ -1647,7 +1618,19 @@ page_container %} } this.proofs = this.proofs.concat(proofs) this.storeProofs() + + // update UI await this.setInvoicePaid(payment_hash) + tokensBase64 = btoa(JSON.stringify(proofs)) + + this.historyTokens.push({ + status: 'paid', + amount: amount, + date: currentDateStr(), + token: tokensBase64 + }) + this.storehistoryTokens() + return proofs } catch (error) { console.error(error) @@ -1657,62 +1640,20 @@ page_container %} throw error } }, - splitToSend: async function (proofs, amount, invlalidate = false) { - // splits proofs so the user can keep firstProofs, send scndProofs - try { - const spendableProofs = proofs.filter(p => !p.reserved) - if (this.sumProofs(spendableProofs) < amount) { - this.$q.notify({ - timeout: 5000, - type: 'warning', - message: 'Balance too low', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) - throw Error('balance too low.') - } - let {fristProofs, scndProofs} = await this.split( - spendableProofs, - amount - ) - // set scndProofs in this.proofs as reserved - const usedSecrets = proofs.map(p => p.secret) - for (let i = 0; i < this.proofs.length; i++) { - if (usedSecrets.includes(this.proofs[i].secret)) { - this.proofs[i].reserved = true - } - } - if (invlalidate) { - // delete tokens from db - this.proofs = fristProofs - // add new fristProofs, scndProofs to this.proofs - this.storeProofs() - } - - return {fristProofs, scndProofs} - } catch (error) { - console.error(error) - LNbits.utils.notifyApiError(error) - throw error - } - }, + // SPLIT split: async function (proofs, amount) { + /* + supplies proofs and requests a split from the mint of these + proofs at a specific amount + */ try { if (proofs.length == 0) { throw new Error('no proofs provided.') } let {fristProofs, scndProofs} = await this.splitApi(proofs, amount) - // delete proofs from this.proofs - const usedSecrets = proofs.map(p => p.secret) - this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret)) + this.deleteProofs(proofs) // add new fristProofs, scndProofs to this.proofs this.proofs = this.proofs.concat(fristProofs).concat(scndProofs) this.storeProofs() @@ -1723,6 +1664,9 @@ page_container %} throw error } }, + + // /split + splitApi: async function (proofs, amount) { try { const total = this.sumProofs(proofs) @@ -1782,7 +1726,62 @@ page_container %} } }, + splitToSend: async function (proofs, amount, invlalidate = false) { + /* + splits proofs so the user can keep firstProofs, send scndProofs. + then sets scndProofs as reserved. + + if invalidate, scndProofs (the one to send) are invalidated + */ + try { + const spendableProofs = proofs.filter(p => !p.reserved) + if (this.sumProofs(spendableProofs) < amount) { + this.$q.notify({ + timeout: 5000, + type: 'warning', + message: 'Balance too low', + position: 'top', + actions: [ + { + icon: 'close', + color: 'white', + handler: () => {} + } + ] + }) + throw Error('balance too low.') + } + + // call /split + + let {fristProofs, scndProofs} = await this.split( + spendableProofs, + amount + ) + // set scndProofs in this.proofs as reserved + const usedSecrets = proofs.map(p => p.secret) + for (let i = 0; i < this.proofs.length; i++) { + if (usedSecrets.includes(this.proofs[i].secret)) { + this.proofs[i].reserved = true + } + } + if (invlalidate) { + // delete scndProofs from db + this.deleteProofs(scndProofs) + } + + return {fristProofs, scndProofs} + } catch (error) { + console.error(error) + LNbits.utils.notifyApiError(error) + throw error + } + }, + redeem: async function () { + /* + uses split to receive new tokens. + */ this.showReceiveTokens = false console.log('### receive tokens', this.receiveData.tokensBase64) try { @@ -1793,6 +1792,9 @@ page_container %} const proofs = JSON.parse(tokenJson) const amount = proofs.reduce((s, t) => (s += t.amount), 0) let {fristProofs, scndProofs} = await this.split(proofs, amount) + + // update UI + // HACK: we need to do this so the balance updates this.proofs = this.proofs.concat([]) @@ -1827,13 +1829,18 @@ page_container %} }, sendTokens: async function () { + /* + calls splitToSend, displays token and kicks off the spendableWorker + */ try { - // keep firstProofs, send scndProofs + // keep firstProofs, send scndProofs and delete them (invalidate=true) let {fristProofs, scndProofs} = await this.splitToSend( this.proofs, this.sendData.amount, true ) + + // update UI this.sendData.tokens = scndProofs console.log('### this.sendData.tokens', this.sendData.tokens) this.sendData.tokensBase64 = btoa( @@ -1846,33 +1853,19 @@ page_container %} date: currentDateStr(), token: this.sendData.tokensBase64 }) + + // store "pending" outgoing tokens in history table this.storehistoryTokens() + this.checkTokenSpendableWorker() } catch (error) { console.error(error) throw error } }, - checkFees: async function (payment_request) { - const payload = { - pr: payment_request - } - console.log('#### payload', JSON.stringify(payload)) - try { - const {data} = await LNbits.api.request( - 'POST', - `/cashu/api/v1/${this.mintId}/checkfees`, - '', - payload - ) - console.log('#### checkFees', payment_request, data.fee) - return data.fee - } catch (error) { - console.error(error) - LNbits.utils.notifyApiError(error) - throw error - } - }, + + // /melt + melt: async function () { // todo: get fees from server and add to inputs this.payInvoiceData.blocking = true @@ -1924,8 +1917,20 @@ page_container %} ] }) // delete spent tokens from db - this.proofs = fristProofs - this.storeProofs() + this.deleteProofs(scndProofs) + + // update UI + + tokensBase64 = btoa(JSON.stringify(scndProofs)) + + this.historyTokens.push({ + status: 'paid', + amount: -amount, + date: currentDateStr(), + token: tokensBase64 + }) + this.storehistoryTokens() + console.log({ amount: -amount, bolt11: this.payInvoiceData.data.request, @@ -1953,13 +1958,93 @@ page_container %} throw error } }, + + // /check + + checkProofsSpendable: async function (proofs) { + /* + checks with the mint whether an array of proofs is still + spendable or already invalidated + */ + const payload = { + proofs: proofs.flat() + } + console.log('#### payload', JSON.stringify(payload)) + try { + const {data} = await LNbits.api.request( + 'POST', + `/cashu/api/v1/${this.mintId}/check`, + '', + payload + ) + + // delete proofs from database if it is spent + let spentProofs = proofs.filter((p, pidx) => !data[pidx]) + if (spentProofs.length) { + this.deleteProofs(spentProofs) + + // update UI + tokensBase64 = btoa(JSON.stringify(spentProofs)) + + this.historyTokens.push({ + status: 'paid', + amount: -this.sumProofs(spentProofs), + date: currentDateStr(), + token: tokensBase64 + }) + this.storehistoryTokens() + } + + return data + } catch (error) { + console.error(error) + LNbits.utils.notifyApiError(error) + throw error + } + }, + + // /checkfees + checkFees: async function (payment_request) { + const payload = { + pr: payment_request + } + console.log('#### payload', JSON.stringify(payload)) + try { + const {data} = await LNbits.api.request( + 'POST', + `/cashu/api/v1/${this.mintId}/checkfees`, + '', + payload + ) + console.log('#### checkFees', payment_request, data.fee) + return data.fee + } catch (error) { + console.error(error) + LNbits.utils.notifyApiError(error) + throw error + } + }, + + // /keys + + fetchMintKeys: async function () { + const {data} = await LNbits.api.request( + 'GET', + `/cashu/api/v1/${this.mintId}/keys` + ) + this.keys = data + localStorage.setItem( + this.mintKey(this.mintId, 'keys'), + JSON.stringify(data) + ) + }, setInvoicePaid: async function (payment_hash) { const invoice = this.invoicesCashu.find(i => i.hash === payment_hash) invoice.status = 'paid' this.storeinvoicesCashu() }, - recheckInvoice: async function (payment_hash, verbose = true) { - console.log('### recheckInvoice.hash', payment_hash) + checkInvoice: async function (payment_hash, verbose = true) { + console.log('### checkInvoice.hash', payment_hash) const invoice = this.invoicesCashu.find(i => i.hash === payment_hash) try { proofs = await this.mint(invoice.amount, invoice.hash, verbose) @@ -1969,15 +2054,15 @@ page_container %} throw error } }, - recheckPendingInvoices: async function () { + checkPendingInvoices: async function () { for (const invoice of this.invoicesCashu) { - if (invoice.status === 'pending' && invoice.sat > 0) { - this.recheckInvoice(invoice.hash, false) + if (invoice.status === 'pending' && invoice.amount > 0) { + this.checkInvoice(invoice.hash, false) } } }, - recheckPendingTokens: async function () { + checkPendingTokens: async function () { for (const token of this.historyTokens) { if (token.status === 'pending' && token.amount < 0) { this.checkTokenSpendable(token.token, false) @@ -1990,6 +2075,113 @@ page_container %} this.storehistoryTokens() }, + checkTokenSpendable: async function (token, verbose = true) { + /* + checks whether a base64-encoded token (from the history table) has been spent already. + if it is spent, the appropraite entry in the history table is set to paid. + */ + const tokenJson = atob(token) + const proofs = JSON.parse(tokenJson) + let data = await this.checkProofsSpendable(proofs) + + // iterate through response of form {0: true, 1: false, ...} + let paid = false + for (const [key, spendable] of Object.entries(data)) { + if (!spendable) { + this.setTokenPaid(token) + paid = true + } + } + if (paid) { + console.log('### token paid') + if (window.navigator.vibrate) navigator.vibrate(200) + this.$q.notify({ + timeout: 5000, + type: 'positive', + message: 'Token paid', + position: 'top', + actions: [ + { + icon: 'close', + color: 'white', + handler: () => {} + } + ] + }) + } else { + console.log('### token not paid yet') + if (verbose) { + this.$q.notify({ + timeout: 5000, + color: 'grey', + message: 'Token still pending', + position: 'top', + actions: [ + { + icon: 'close', + color: 'white', + handler: () => {} + } + ] + }) + } + this.sendData.tokens = token + } + return paid + }, + + ////////////// WORKERS ////////////// + + clearAllWorkers: function () { + if (this.invoiceCheckListener) { + clearInterval(this.invoiceCheckListener) + } + if (this.tokensCheckSpendableListener) { + clearInterval(this.tokensCheckSpendableListener) + } + }, + invoiceCheckWorker: async function () { + let nInterval = 0 + this.clearAllWorkers() + this.invoiceCheckListener = setInterval(async () => { + try { + nInterval += 1 + + // exit loop after 2m + if (nInterval > 40) { + console.log('### stopping invoice check worker') + this.clearAllWorkers() + } + console.log('### invoiceCheckWorker setInterval', nInterval) + console.log(this.invoiceData) + + // this will throw an error if the invoice is pending + await this.checkInvoice(this.invoiceData.hash, false) + + // only without error (invoice paid) will we reach here + console.log('### stopping invoice check worker') + this.clearAllWorkers() + this.invoiceData.bolt11 = '' + this.showInvoiceDetails = false + if (window.navigator.vibrate) navigator.vibrate(200) + this.$q.notify({ + timeout: 5000, + type: 'positive', + message: 'Payment received', + position: 'top', + actions: [ + { + icon: 'close', + color: 'white', + handler: () => {} + } + ] + }) + } catch (error) { + console.log('not paid yet') + } + }, 3000) + }, checkTokenSpendableWorker: async function () { let nInterval = 0 this.clearAllWorkers() @@ -2021,83 +2213,6 @@ page_container %} }, 3000) }, - checkTokenSpendable: async function (token, verbose = true) { - const tokenJson = atob(token) - const proofs = JSON.parse(tokenJson) - const payload = { - proofs: proofs.flat() - } - console.log('#### payload', JSON.stringify(payload)) - try { - const {data} = await LNbits.api.request( - 'POST', - `/cashu/api/v1/${this.mintId}/check`, - '', - payload - ) - // iterate through response of form {0: true, 1: false, ...} - let paid = false - for (const [key, spendable] of Object.entries(data)) { - if (!spendable) { - this.setTokenPaid(token) - paid = true - } - } - if (paid) { - console.log('### token paid') - if (window.navigator.vibrate) navigator.vibrate(200) - this.$q.notify({ - timeout: 5000, - type: 'positive', - message: 'Token paid', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) - } else { - console.log('### token not paid yet') - if (verbose) { - this.$q.notify({ - timeout: 5000, - color: 'grey', - message: 'Token still pending', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) - } - this.sendData.tokens = token - } - return paid - } catch (error) { - console.error(error) - LNbits.utils.notifyApiError(error) - throw error - } - }, - - fetchMintKeys: async function () { - const {data} = await LNbits.api.request( - 'GET', - `/cashu/api/v1/${this.mintId}/keys` - ) - this.keys = data - localStorage.setItem( - this.mintKey(this.mintId, 'keys'), - JSON.stringify(data) - ) - }, - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2116,62 +2231,62 @@ page_container %} } }, - checkInvoice: function () { - console.log('#### checkInvoice') - try { - const invoice = decode(this.payInvoiceData.data.request) + // checkInvoice: function () { + // console.log('#### checkInvoice') + // try { + // const invoice = decode(this.payInvoiceData.data.request) - const cleanInvoice = { - msat: invoice.human_readable_part.amount, - sat: invoice.human_readable_part.amount / 1000, - fsat: LNbits.utils.formatSat( - invoice.human_readable_part.amount / 1000 - ) - } + // const cleanInvoice = { + // msat: invoice.human_readable_part.amount, + // sat: invoice.human_readable_part.amount / 1000, + // fsat: LNbits.utils.formatSat( + // invoice.human_readable_part.amount / 1000 + // ) + // } - _.each(invoice.data.tags, tag => { - if (_.isObject(tag) && _.has(tag, 'description')) { - if (tag.description === 'payment_hash') { - cleanInvoice.hash = tag.value - } else if (tag.description === 'description') { - cleanInvoice.description = tag.value - } else if (tag.description === 'expiry') { - var expireDate = new Date( - (invoice.data.time_stamp + tag.value) * 1000 - ) - cleanInvoice.expireDate = Quasar.utils.date.formatDate( - expireDate, - 'YYYY-MM-DDTHH:mm:ss.SSSZ' - ) - cleanInvoice.expired = false // TODO - } - } + // _.each(invoice.data.tags, tag => { + // if (_.isObject(tag) && _.has(tag, 'description')) { + // if (tag.description === 'payment_hash') { + // cleanInvoice.hash = tag.value + // } else if (tag.description === 'description') { + // cleanInvoice.description = tag.value + // } else if (tag.description === 'expiry') { + // var expireDate = new Date( + // (invoice.data.time_stamp + tag.value) * 1000 + // ) + // cleanInvoice.expireDate = Quasar.utils.date.formatDate( + // expireDate, + // 'YYYY-MM-DDTHH:mm:ss.SSSZ' + // ) + // cleanInvoice.expired = false // TODO + // } + // } - this.payInvoiceData.invoice = cleanInvoice - }) + // this.payInvoiceData.invoice = cleanInvoice + // }) - console.log( - '#### this.payInvoiceData.invoice', - this.payInvoiceData.invoice - ) - } catch (error) { - this.$q.notify({ - timeout: 5000, - type: 'warning', - message: 'Could not decode invoice', - caption: error + '', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) - throw error - } - }, + // console.log( + // '#### this.payInvoiceData.invoice', + // this.payInvoiceData.invoice + // ) + // } catch (error) { + // this.$q.notify({ + // timeout: 5000, + // type: 'warning', + // message: 'Could not decode invoice', + // caption: error + '', + // position: 'top', + // actions: [ + // { + // icon: 'close', + // color: 'white', + // handler: () => {} + // } + // ] + // }) + // throw error + // } + // }, ////////////// STORAGE ///////////// @@ -2335,8 +2450,9 @@ page_container %} console.log('#### this.mintId', this.mintId) console.log('#### this.mintName', this.mintName) - this.recheckPendingInvoices() - this.recheckPendingTokens() + this.checkProofsSpendable(this.proofs) + this.checkPendingInvoices() + this.checkPendingTokens() } }) diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 1442634c..9819cecb 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -182,7 +182,7 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR @cashu_ext.post("/api/v1/{cashu_id}/mint") -async def mint_coins( +async def mint( data: MintRequest, cashu_id: str = Query(None), payment_hash: str = Query(None), @@ -206,42 +206,57 @@ async def mint_coins( status_code=HTTPStatus.NOT_FOUND, detail="Mint does not know this invoice.", ) - if invoice.issued == True: + if invoice.issued: raise HTTPException( status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Tokens already issued for this invoice.", ) - total_requested = sum([bm.amount for bm in data.blinded_messages]) - if total_requested > invoice.amount: - raise HTTPException( - status_code=HTTPStatus.PAYMENT_REQUIRED, - detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}", - ) - - status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash) - - if LIGHTNING and status.paid != True: - raise HTTPException( - status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." - ) - try: - keyset = ledger.keysets.keysets[cashu.keyset_id] - - promises = await ledger._generate_promises( - B_s=data.blinded_messages, keyset=keyset - ) - assert len(promises), HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="No promises returned." - ) + # set this invoice as issued await ledger.crud.update_lightning_invoice( db=ledger.db, hash=payment_hash, issued=True ) + status: PaymentStatus = await check_transaction_status( + cashu.wallet, payment_hash + ) + + try: + total_requested = sum([bm.amount for bm in data.blinded_messages]) + if total_requested > invoice.amount: + raise HTTPException( + status_code=HTTPStatus.PAYMENT_REQUIRED, + detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}", + ) + + if not status.paid: + raise HTTPException( + status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." + ) + + keyset = ledger.keysets.keysets[cashu.keyset_id] + + promises = await ledger._generate_promises( + B_s=data.blinded_messages, keyset=keyset + ) + return promises + except (Exception, HTTPException) as e: + logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}") + # unset issued flag because something went wrong + await ledger.crud.update_lightning_invoice( + db=ledger.db, hash=payment_hash, issued=False + ) + raise HTTPException( + status_code=getattr(e, "status_code") + or HTTPStatus.INTERNAL_SERVER_ERROR, + detail=str(e) or getattr(e, "detail"), + ) + else: + # only used for testing when LIGHTNING=false + promises = await ledger._generate_promises( + B_s=data.blinded_messages, keyset=keyset + ) return promises - except Exception as e: - logger.error(e) - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) @cashu_ext.post("/api/v1/{cashu_id}/melt") @@ -285,26 +300,30 @@ async def melt_coins( f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)." ) logger.debug(f"Cashu: Initiating payment of {total_provided} sats") - await pay_invoice( - wallet_id=cashu.wallet, - payment_request=invoice, - description=f"Pay cashu invoice", - extra={"tag": "cashu", "cashu_name": cashu.name}, - ) - - logger.debug( - f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}" - ) - status: PaymentStatus = await check_transaction_status( - cashu.wallet, invoice_obj.payment_hash - ) - if status.paid == True: - logger.debug( - f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}" + try: + await pay_invoice( + wallet_id=cashu.wallet, + payment_request=invoice, + description=f"Pay cashu invoice", + extra={"tag": "cashu", "cashu_name": cashu.name}, ) - await ledger._invalidate_proofs(proofs) - else: - logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}") + except Exception as e: + logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}") + raise e + finally: + logger.debug( + f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}" + ) + status: PaymentStatus = await check_transaction_status( + cashu.wallet, invoice_obj.payment_hash + ) + if status.paid == True: + logger.debug( + f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}" + ) + await ledger._invalidate_proofs(proofs) + else: + logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}") except Exception as e: logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}") raise HTTPException( From b5147181af47dbb4926d594fa8a06624ea27da1a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 04:03:04 +0100 Subject: [PATCH 40/54] feat: add /keys/{keyset} endpoint --- lnbits/extensions/cashu/views_api.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 9819cecb..7e037a3c 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -130,6 +130,28 @@ async def keys(cashu_id: str = Query(None)) -> dict[int, str]: return ledger.get_keyset(keyset_id=cashu.keyset_id) +@cashu_ext.get("/api/v1/{cashu_id}/keys/{idBase64Urlsafe}") +async def keyset_keys( + cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None) +) -> dict[int, str]: + """ + Get the public keys of the mint of a specificy keyset id. + The id is encoded in base64_urlsafe and needs to be converted back to + normal base64 before it can be processed. + """ + + cashu: Union[Cashu, None] = await get_cashu(cashu_id) + + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + + id = idBase64Urlsafe.replace("-", "+").replace("_", "/") + keyset = ledger.get_keyset(keyset_id=id) + return keyset + + @cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK) async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]: """Get the public keys of the mint""" From 5ecbbb1abebe928add787ec52ef91d666681ca11 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 13:58:39 +0100 Subject: [PATCH 41/54] add warning --- lnbits/extensions/cashu/views_api.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 7e037a3c..def9074f 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -46,9 +46,16 @@ from .models import Cashu # --------- extension imports +# WARNING: Do not set this to False in production! This will create +# tokens for free otherwise. This is for testing purposes only! LIGHTNING = True +if not LIGHTNING: + logger.warning( + "Cashu: LIGHTNING is set False! That means that I will create ecash for free!" + ) + ######################################## ############### LNBITS MINTS ########### ######################################## @@ -219,6 +226,8 @@ async def mint( status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." ) + keyset = ledger.keysets.keysets[cashu.keyset_id] + if LIGHTNING: invoice: Invoice = await ledger.crud.get_lightning_invoice( db=ledger.db, hash=payment_hash @@ -256,8 +265,6 @@ async def mint( status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." ) - keyset = ledger.keysets.keysets[cashu.keyset_id] - promises = await ledger._generate_promises( B_s=data.blinded_messages, keyset=keyset ) From 8e2cb187da14a07a295877f897435fcf83d5e8d4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 24 Dec 2022 16:32:38 +0100 Subject: [PATCH 42/54] fix: correctly update ui on token sending --- .../cashu/templates/cashu/wallet.html | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lnbits/extensions/cashu/templates/cashu/wallet.html b/lnbits/extensions/cashu/templates/cashu/wallet.html index d2def94d..00e4644c 100644 --- a/lnbits/extensions/cashu/templates/cashu/wallet.html +++ b/lnbits/extensions/cashu/templates/cashu/wallet.html @@ -1731,7 +1731,7 @@ page_container %} splits proofs so the user can keep firstProofs, send scndProofs. then sets scndProofs as reserved. - if invalidate, scndProofs (the one to send) are invalidated + if invalidate, scndProofs (the one to send) are invalidated */ try { const spendableProofs = proofs.filter(p => !p.reserved) @@ -1961,7 +1961,7 @@ page_container %} // /check - checkProofsSpendable: async function (proofs) { + checkProofsSpendable: async function (proofs, update_history = false) { /* checks with the mint whether an array of proofs is still spendable or already invalidated @@ -1984,15 +1984,17 @@ page_container %} this.deleteProofs(spentProofs) // update UI - tokensBase64 = btoa(JSON.stringify(spentProofs)) + if (update_history) { + tokensBase64 = btoa(JSON.stringify(spentProofs)) - this.historyTokens.push({ - status: 'paid', - amount: -this.sumProofs(spentProofs), - date: currentDateStr(), - token: tokensBase64 - }) - this.storehistoryTokens() + this.historyTokens.push({ + status: 'paid', + amount: -this.sumProofs(spentProofs), + date: currentDateStr(), + token: tokensBase64 + }) + this.storehistoryTokens() + } } return data @@ -2450,7 +2452,7 @@ page_container %} console.log('#### this.mintId', this.mintId) console.log('#### this.mintName', this.mintName) - this.checkProofsSpendable(this.proofs) + this.checkProofsSpendable(this.proofs, true) this.checkPendingInvoices() this.checkPendingTokens() } From b6f0d7b7694426b643e45bb234c63ccc7b2f8a70 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 25 Dec 2022 18:49:51 +0100 Subject: [PATCH 43/54] feat: api health check endpoint --- lnbits/core/views/api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 2bd19978..c497c992 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -71,6 +71,11 @@ from ..services import ( from ..tasks import api_invoice_listeners +@core_app.get("/api/v1/health", status_code=HTTPStatus.OK) +async def health(): + return + + @core_app.get("/api/v1/wallet") async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)): if wallet.wallet_type == 0: From 6825a1f2e60972913b30fbc8fe672738c1baf3ec Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 11:52:40 +0100 Subject: [PATCH 44/54] Update lnbits/extensions/satspay/views_api.py --- lnbits/extensions/satspay/views_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 4a62c3d5..68ce2469 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -46,7 +46,7 @@ async def api_charge_create( **{"paid": charge.paid}, } except Exception as ex: - logger.debug(ex) + logger.debug(f"Satspay error: {str}") raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex) ) From a243e4e32019daaa4011acd4d5edd797f6dcd5a0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 12:12:27 +0100 Subject: [PATCH 45/54] fix: update to latest changes --- lnbits/core/views/api.py | 6 +++--- lnbits/decorators.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 1d3d76f1..5f7cbd38 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -665,9 +665,9 @@ async def img(request: Request, data): ) -@core_app.get("/api/v1/audit") -async def api_auditor(wallet: WalletTypeInfo = Depends(require_admin_user)): - +@core_app.get("/api/v1/audit", dependencies=[Depends(check_admin)]) +async def api_auditor(): + WALLET = get_wallet_class() total_balance = await get_total_balance() error_message, node_balance = await WALLET.status() diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 7f8e84e6..3ef9e850 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -181,7 +181,7 @@ async def require_admin_user( token = api_key_header or api_key_query wallet = await get_key_type(r, token) - if wallet.wallet.user not in LNBITS_ADMIN_USERS: + if wallet.wallet.user not in settings.lnbits_admin_users: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" ) From 55069b0216f2c325a3eb711d341d6b9b93f7baf6 Mon Sep 17 00:00:00 2001 From: arbadacarba <63317640+arbadacarbaYK@users.noreply.github.com> Date: Tue, 27 Dec 2022 11:50:26 +0100 Subject: [PATCH 46/54] Update README.md --- lnbits/extensions/boltcards/README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md index 34a1c05c..494b6613 100644 --- a/lnbits/extensions/boltcards/README.md +++ b/lnbits/extensions/boltcards/README.md @@ -42,18 +42,23 @@ Updated for v0.1.3 - Or you can Click the "KEYS / AUTH LINK" button to copy the auth URL to the clipboard. Then paste it into the Android app (Create Bolt Card -> PASTE AUTH URL). - Click WRITE CARD NOW and approach the NFC card to set it up. DO NOT REMOVE THE CARD PREMATURELY! -## Erasing the card - Boltcard NFC Card Creator -Updated for v0.1.3 +## Rewriting / Erasing the card - Boltcard NFC Card Creator -Since v0.1.2 of Boltcard NFC Card Creator it is possible not only reset the keys but also disable the SUN function and do the complete erase so the card can be use again as a static tag (or set as a new Bolt Card, ofc). +It is possible not only to reset the keys but also to disable the SUN function and completely erasing the card so it can be used again as a static tag or set up as a new Bolt Card. + +IMPORTANT: +* It is immanent that you have access to your old keys so do not erase this card in LNbits before you copied those strings! +* If you tried to write to them and failed you will need the same amount of positive writing sessions to unlock the card. + +- in the BoltCard-Extension click the QR code button next to your old card and copy Key0 +- in the BoltApp click Advanced - Reset keys and paste the Key0 into the first field named Key0 +- repeat with Key1/Key2/Key3/Key0 +- when done pasting all 4 keys scan your card with the BoltApp +- Thats it 🥳 +- Now if there is all success the card can be safely deleted from LNbits (but keep the keys backuped anyway; batter safe than brick). + +You can watch a video of this process here https://www.youtube.com/watch?time_continue=230&v=Pe0YXHawHvQ&feature=emb_logo -- Click the QR code button next to a card to view its details and select WIPE -- OR click the red cross icon on the right side to reach the same -- In the android app (Advanced -> Reset Keys) - - Click SCAN QR CODE to scan the QR - - Or click WIPE DATA in LNbits to copy and paste in to the app (PASTE KEY JSON) -- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY! -- Now if there is all success the card can be safely delete from LNbits (but keep the keys backuped anyway; batter safe than brick). ## Setting the card - computer (hard way) From a0429660f4818897ca3e49d91949cd4e5502b435 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 27 Dec 2022 14:18:00 +0100 Subject: [PATCH 47/54] fix: remove require_admin_user --- lnbits/decorators.py | 17 ----------------- lnbits/extensions/satspay/views_api.py | 1 - 2 files changed, 18 deletions(-) diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 3ef9e850..9aeace40 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -172,23 +172,6 @@ async def get_key_type( ) -async def require_admin_user( - r: Request, - api_key_header: str = Security(api_key_header), # type: ignore - api_key_query: str = Security(api_key_query), # type: ignore -): - - token = api_key_header or api_key_query - wallet = await get_key_type(r, token) - - if wallet.wallet.user not in settings.lnbits_admin_users: - raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" - ) - else: - return wallet - - async def require_admin_key( r: Request, api_key_header: str = Security(api_key_header), # type: ignore diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 04005a88..46dd7330 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -11,7 +11,6 @@ from lnbits.decorators import ( check_admin, get_key_type, require_admin_key, - require_admin_user, require_invoice_key, ) from lnbits.extensions.satspay import satspay_ext From 6e7bfd03eeaa2fd1b67af58dabbeaccc86a87b39 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 27 Dec 2022 14:19:51 +0100 Subject: [PATCH 48/54] clean up --- lnbits/core/views/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 5f7cbd38..85bc394f 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -36,7 +36,6 @@ from lnbits.decorators import ( check_admin, get_key_type, require_admin_key, - require_admin_user, require_invoice_key, ) from lnbits.helpers import url_for, urlsafe_short_hash From ead5ae0d82bb79d8aca24ff3fd7d2d4da3be81d6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 27 Dec 2022 14:50:42 +0100 Subject: [PATCH 49/54] refactor: remove unused imports --- lnbits/core/models.py | 4 ++-- lnbits/core/services.py | 2 -- lnbits/core/tasks.py | 1 - lnbits/core/views/api.py | 5 +---- lnbits/core/views/public_api.py | 1 - 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 65c72b41..138a39f7 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -4,13 +4,13 @@ import hmac import json import time from sqlite3 import Row -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, Optional from ecdsa import SECP256k1, SigningKey # type: ignore from fastapi import Query from lnurl import encode as lnurl_encode # type: ignore from loguru import logger -from pydantic import BaseModel, Extra, validator +from pydantic import BaseModel from lnbits.db import Connection from lnbits.helpers import url_for diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 336d2665..3f02a84e 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -15,9 +15,7 @@ from lnbits import bolt11 from lnbits.db import Connection from lnbits.decorators import ( WalletTypeInfo, - get_key_type, require_admin_key, - require_invoice_key, ) from lnbits.helpers import url_for, urlsafe_short_hash from lnbits.requestvars import g diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index b57e2625..e11f764b 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -4,7 +4,6 @@ from typing import Dict import httpx from loguru import logger -from lnbits.helpers import get_current_extension_name from lnbits.tasks import SseListenersDict, register_invoice_listener from . import db diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 85bc394f..ed2c26e4 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -38,7 +38,7 @@ from lnbits.decorators import ( require_admin_key, require_invoice_key, ) -from lnbits.helpers import url_for, urlsafe_short_hash +from lnbits.helpers import url_for from lnbits.settings import get_wallet_class, settings from lnbits.utils.exchange_rates import ( currencies, @@ -48,14 +48,11 @@ from lnbits.utils.exchange_rates import ( from .. import core_app, db from ..crud import ( - create_payment, get_payments, get_standalone_payment, get_total_balance, - get_wallet, get_wallet_for_key, save_balance_check, - update_payment_status, update_wallet, ) from ..services import ( diff --git a/lnbits/core/views/public_api.py b/lnbits/core/views/public_api.py index 9b0ebc98..56afc176 100644 --- a/lnbits/core/views/public_api.py +++ b/lnbits/core/views/public_api.py @@ -6,7 +6,6 @@ from urllib.parse import urlparse from fastapi import HTTPException from loguru import logger from starlette.requests import Request -from starlette.responses import HTMLResponse from lnbits import bolt11 From cd11f92ed98a60ba8fce64190bf6230adf9df58e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 27 Dec 2022 15:00:41 +0100 Subject: [PATCH 50/54] make format --- lnbits/core/services.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 3f02a84e..e83b5684 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -13,10 +13,7 @@ from loguru import logger from lnbits import bolt11 from lnbits.db import Connection -from lnbits.decorators import ( - WalletTypeInfo, - require_admin_key, -) +from lnbits.decorators import WalletTypeInfo, require_admin_key from lnbits.helpers import url_for, urlsafe_short_hash from lnbits.requestvars import g from lnbits.settings import ( From 86b0cec215ebb083bb044e871b843dbdf389b012 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Wed, 28 Dec 2022 12:48:31 +0100 Subject: [PATCH 51/54] remove result link this shouldn't have been commited since it it listed in gitignore) --- result | 1 - 1 file changed, 1 deletion(-) delete mode 120000 result diff --git a/result b/result deleted file mode 120000 index b0acd55c..00000000 --- a/result +++ /dev/null @@ -1 +0,0 @@ -/nix/store/ds9c48q7hnkdmpzy3aq14kc1x9wrrszd-python3.9-lnbits-0.1.0 \ No newline at end of file From f70b417b4114990546c9ed42392b7e14240ccd7c Mon Sep 17 00:00:00 2001 From: Joel Klabo Date: Wed, 28 Dec 2022 16:06:01 -0800 Subject: [PATCH 52/54] Fix Typo in Migration Logging --- lnbits/core/migrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 81413246..41ba5644 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -224,7 +224,7 @@ async def m007_set_invoice_expiries(db): ) ).fetchall() if len(rows): - logger.info(f"Mirgraion: Checking expiry of {len(rows)} invoices") + logger.info(f"Migration: Checking expiry of {len(rows)} invoices") for i, ( payment_request, checking_id, @@ -238,7 +238,7 @@ async def m007_set_invoice_expiries(db): invoice.date + invoice.expiry ) logger.info( - f"Mirgraion: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}" + f"Migration: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}" ) await db.execute( """ From 3cc27c9d807d54e417a77463b3c79e9205b7231e Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Thu, 29 Dec 2022 13:56:15 +0100 Subject: [PATCH 53/54] Update lnbits/extensions/boltcards/README.md --- lnbits/extensions/boltcards/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md index 494b6613..ceddf144 100644 --- a/lnbits/extensions/boltcards/README.md +++ b/lnbits/extensions/boltcards/README.md @@ -55,7 +55,7 @@ IMPORTANT: - repeat with Key1/Key2/Key3/Key0 - when done pasting all 4 keys scan your card with the BoltApp - Thats it 🥳 -- Now if there is all success the card can be safely deleted from LNbits (but keep the keys backuped anyway; batter safe than brick). +- If everything was successful the card can be safely deleted from LNbits (but keep the keys backed up anyway; batter safe than brick). You can watch a video of this process here https://www.youtube.com/watch?time_continue=230&v=Pe0YXHawHvQ&feature=emb_logo From 8d72354d3f415e6000a590f737be1ec21da504fb Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Thu, 29 Dec 2022 13:56:20 +0100 Subject: [PATCH 54/54] Update lnbits/extensions/boltcards/README.md --- lnbits/extensions/boltcards/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/boltcards/README.md b/lnbits/extensions/boltcards/README.md index ceddf144..10740cf8 100644 --- a/lnbits/extensions/boltcards/README.md +++ b/lnbits/extensions/boltcards/README.md @@ -44,7 +44,7 @@ Updated for v0.1.3 ## Rewriting / Erasing the card - Boltcard NFC Card Creator -It is possible not only to reset the keys but also to disable the SUN function and completely erasing the card so it can be used again as a static tag or set up as a new Bolt Card. +It is possible not only to reset the keys but also to disable the SUN function and completely erase the card so it can be used again as a static tag or set up as a new Bolt Card. IMPORTANT: * It is immanent that you have access to your old keys so do not erase this card in LNbits before you copied those strings!