diff --git a/.env.example b/.env.example index 4edaea97..b7fb9f06 100644 --- a/.env.example +++ b/.env.example @@ -6,14 +6,17 @@ PORT=5000 DEBUG=false +# Allow users and admins by user IDs (comma separated list) LNBITS_ALLOWED_USERS="" LNBITS_ADMIN_USERS="" # Extensions only admin can access LNBITS_ADMIN_EXTENSIONS="ngrok" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" -# csv ad image filepaths or urls, extensions can choose to honor -LNBITS_AD_SPACE="" +# Ad space description +# LNBITS_AD_SPACE_TITLE="Supported by" +# csv ad space, format ";;, ;;", extensions can choose to honor +# LNBITS_AD_SPACE="" # Hides wallet api, extensions can choose to honor LNBITS_HIDE_API=false diff --git a/Dockerfile b/Dockerfile index 6259fe7b..f107f68c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN curl -sSL https://install.python-poetry.org | python3 - ENV PATH="/root/.local/bin:$PATH" WORKDIR /app +RUN mkdir -p lnbits/data COPY . . diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index bb1ca0c1..881d1001 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -229,6 +229,24 @@ async def get_wallet_payment( return Payment.from_row(row) if row else None +async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5): + rows = await db.fetchall( + f""" + SELECT * FROM apipayments + WHERE pending = 'false' + AND extra LIKE ? + AND extra LIKE ? + ORDER BY time DESC LIMIT {limit} + """, + ( + f"%{ext_name}%", + f"%{ext_id}%", + ), + ) + + return rows + + async def get_payments( *, wallet_id: Optional[str] = None, diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index ebecb5e3..d92f384a 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -51,7 +51,7 @@ async def m001_initial(db): f""" CREATE TABLE IF NOT EXISTS apipayments ( payhash TEXT NOT NULL, - amount INTEGER NOT NULL, + amount {db.big_int} NOT NULL, fee INTEGER NOT NULL DEFAULT 0, wallet TEXT NOT NULL, pending BOOLEAN NOT NULL, diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index 68a7b7ed..5f26cb03 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -183,6 +183,23 @@
 
+ + {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %} +
+ {{ AD_TITLE }} + + + + + +
+ {% endfor %} {% endif %} diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index 4bf6067c..22fbd05d 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -388,9 +388,14 @@ {% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %} - +
{{ AD_TITLE }}
+ + + + + + {% endfor %} {% endif %} diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 983d5a26..11075370 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -155,30 +155,29 @@ class CreateInvoiceData(BaseModel): async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): - if data.description_hash: + if data.description_hash or data.unhashed_description: try: - description_hash = binascii.unhexlify(data.description_hash) + description_hash = ( + binascii.unhexlify(data.description_hash) + if data.description_hash + else b"" + ) + unhashed_description = ( + binascii.unhexlify(data.unhashed_description) + if data.unhashed_description + else b"" + ) except binascii.Error: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail="'description_hash' must be a valid hex string", + detail="'description_hash' and 'unhashed_description' must be a valid hex strings", ) - unhashed_description = b"" - memo = "" - elif data.unhashed_description: - try: - unhashed_description = binascii.unhexlify(data.unhashed_description) - except binascii.Error: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="'unhashed_description' must be a valid hex string", - ) - description_hash = b"" memo = "" else: description_hash = b"" unhashed_description = b"" memo = data.memo or LNBITS_SITE_TITLE + if data.unit == "sat": amount = int(data.amount) else: diff --git a/lnbits/extensions/bleskomat/views_api.py b/lnbits/extensions/bleskomat/views_api.py index e29e3fe7..b6e417bb 100644 --- a/lnbits/extensions/bleskomat/views_api.py +++ b/lnbits/extensions/bleskomat/views_api.py @@ -95,4 +95,4 @@ async def api_bleskomat_delete( ) await delete_bleskomat(bleskomat_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 55cc1e5e..6398c20e 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -380,7 +380,11 @@ Lock key: {{ qrCodeDialog.data.k0 }}
Meta key: {{ qrCodeDialog.data.k1 }}
File key: {{ qrCodeDialog.data.k2 }}
+
+ Always backup all keys that you're trying to write on the card. Without + them you may not be able to change them in the future!

+
diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py index 08651f5d..668e7f77 100644 --- a/lnbits/extensions/events/views_api.py +++ b/lnbits/extensions/events/views_api.py @@ -2,6 +2,7 @@ from http import HTTPStatus from fastapi.param_functions import Query from fastapi.params import Depends +from loguru import logger from starlette.exceptions import HTTPException from starlette.requests import Request @@ -10,7 +11,6 @@ from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.extensions.events.models import CreateEvent, CreateTicket -from loguru import logger from . import events_ext from .crud import ( @@ -79,7 +79,7 @@ async def api_form_delete(event_id, wallet: WalletTypeInfo = Depends(get_key_typ await delete_event(event_id) await delete_event_tickets(event_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT #########Tickets########## diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index d160daee..9d6548b6 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash @@ -6,11 +6,9 @@ from . import db from .models import CreateJukeboxPayment, CreateJukeLinkData, Jukebox, JukeboxPayment -async def create_jukebox( - data: CreateJukeLinkData, inkey: Optional[str] = "" -) -> Jukebox: +async def create_jukebox(data: CreateJukeLinkData) -> Jukebox: juke_id = urlsafe_short_hash() - result = await db.execute( + await db.execute( """ INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -36,13 +34,13 @@ async def create_jukebox( async def update_jukebox( - data: CreateJukeLinkData, juke_id: Optional[str] = "" + data: Union[CreateJukeLinkData, Jukebox], juke_id: str = "" ) -> Optional[Jukebox]: q = ", ".join([f"{field[0]} = ?" for field in data]) items = [f"{field[1]}" for field in data] items.append(juke_id) q = q.replace("user", '"user"', 1) # hack to make user be "user"! - await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items)) + await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items,)) row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) return Jukebox(**row) if row else None @@ -72,7 +70,7 @@ async def delete_jukebox(juke_id: str): """ DELETE FROM jukebox.jukebox WHERE id = ? """, - (juke_id), + (juke_id,), ) @@ -80,7 +78,7 @@ async def delete_jukebox(juke_id: str): async def create_jukebox_payment(data: CreateJukeboxPayment) -> JukeboxPayment: - result = await db.execute( + await db.execute( """ INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid) VALUES (?, ?, ?, ?) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index 90984b03..70cf6523 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -1,6 +1,3 @@ -from sqlite3 import Row -from typing import NamedTuple, Optional - from fastapi.param_functions import Query from pydantic import BaseModel from pydantic.main import BaseModel @@ -20,19 +17,19 @@ class CreateJukeLinkData(BaseModel): class Jukebox(BaseModel): - id: Optional[str] - user: Optional[str] - title: Optional[str] - wallet: Optional[str] - inkey: Optional[str] - sp_user: Optional[str] - sp_secret: Optional[str] - sp_access_token: Optional[str] - sp_refresh_token: Optional[str] - sp_device: Optional[str] - sp_playlists: Optional[str] - price: Optional[int] - profit: Optional[int] + id: str + user: str + title: str + wallet: str + inkey: str + sp_user: str + sp_secret: str + sp_access_token: str + sp_refresh_token: str + sp_device: str + sp_playlists: str + price: int + profit: int class JukeboxPayment(BaseModel): diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py index 5614d926..8a68fd27 100644 --- a/lnbits/extensions/jukebox/tasks.py +++ b/lnbits/extensions/jukebox/tasks.py @@ -17,7 +17,8 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if payment.extra.get("tag") != "jukebox": - # not a jukebox invoice - return - await update_jukebox_payment(payment.payment_hash, paid=True) + if payment.extra: + if payment.extra.get("tag") != "jukebox": + # not a jukebox invoice + return + await update_jukebox_payment(payment.payment_hash, paid=True) diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index 56774394..28359a9a 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -17,7 +17,9 @@ templates = Jinja2Templates(directory="templates") @jukebox_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 jukebox_renderer().TemplateResponse( "jukebox/index.html", {"request": request, "user": user.dict()} ) @@ -31,6 +33,7 @@ async def connect_to_jukebox(request: Request, juke_id): status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist." ) devices = await api_get_jukebox_device_check(juke_id) + deviceConnected = False for device in devices["devices"]: if device["id"] == jukebox.sp_device.split("-")[1]: deviceConnected = True @@ -48,5 +51,5 @@ async def connect_to_jukebox(request: Request, juke_id): else: return jukebox_renderer().TemplateResponse( "jukebox/error.html", - {"request": request, "jukebox": jukebox.jukebox(req=request)}, + {"request": request, "jukebox": jukebox.dict()}, ) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 1f3723a7..5cf1a83b 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -3,7 +3,6 @@ import json from http import HTTPStatus import httpx -from fastapi import Request from fastapi.param_functions import Query from fastapi.params import Depends from starlette.exceptions import HTTPException @@ -29,9 +28,7 @@ from .models import CreateJukeboxPayment, CreateJukeLinkData @jukebox_ext.get("/api/v1/jukebox") async def api_get_jukeboxs( - req: Request, - wallet: WalletTypeInfo = Depends(require_admin_key), - all_wallets: bool = Query(False), + wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore ): wallet_user = wallet.wallet.user @@ -53,54 +50,52 @@ async def api_check_credentials_callbac( access_token: str = Query(None), refresh_token: str = Query(None), ): - sp_code = "" - sp_access_token = "" - sp_refresh_token = "" - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN) if code: jukebox.sp_access_token = code - jukebox = await update_jukebox(jukebox, juke_id=juke_id) + await update_jukebox(jukebox, juke_id=juke_id) if access_token: jukebox.sp_access_token = access_token jukebox.sp_refresh_token = refresh_token - jukebox = await update_jukebox(jukebox, juke_id=juke_id) + await update_jukebox(jukebox, juke_id=juke_id) return "

Success!

You can close this window

" -@jukebox_ext.get("/api/v1/jukebox/{juke_id}") -async def api_check_credentials_check( - juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key) -): +@jukebox_ext.get("/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)]) +async def api_check_credentials_check(juke_id: str = Query(None)): jukebox = await get_jukebox(juke_id) return jukebox -@jukebox_ext.post("/api/v1/jukebox", status_code=HTTPStatus.CREATED) +@jukebox_ext.post( + "/api/v1/jukebox", + status_code=HTTPStatus.CREATED, + dependencies=[Depends(require_admin_key)], +) @jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) async def api_create_update_jukebox( - data: CreateJukeLinkData, - juke_id: str = Query(None), - wallet: WalletTypeInfo = Depends(require_admin_key), + data: CreateJukeLinkData, juke_id: str = Query(None) ): if juke_id: jukebox = await update_jukebox(data, juke_id=juke_id) else: - jukebox = await create_jukebox(data, inkey=wallet.wallet.inkey) + jukebox = await create_jukebox(data) return jukebox -@jukebox_ext.delete("/api/v1/jukebox/{juke_id}") +@jukebox_ext.delete( + "/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)] +) async def api_delete_item( - juke_id=None, wallet: WalletTypeInfo = Depends(require_admin_key) + juke_id: str = Query(None), ): await delete_jukebox(juke_id) - try: - return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)] - except: - raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox") + # try: + # return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)] + # except: + # raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox") ################JUKEBOX ENDPOINTS################## @@ -114,9 +109,8 @@ async def api_get_jukebox_song( sp_playlist: str = Query(None), retry: bool = Query(False), ): - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes") tracks = [] async with httpx.AsyncClient() as client: @@ -152,14 +146,13 @@ async def api_get_jukebox_song( } ) except: - something = None + pass return [track for track in tracks] -async def api_get_token(juke_id=None): - try: - jukebox = await get_jukebox(juke_id) - except: +async def api_get_token(juke_id): + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes") async with httpx.AsyncClient() as client: @@ -187,7 +180,7 @@ async def api_get_token(juke_id=None): jukebox.sp_access_token = r.json()["access_token"] await update_jukebox(jukebox, juke_id=juke_id) except: - something = None + pass return True @@ -198,9 +191,8 @@ async def api_get_token(juke_id=None): async def api_get_jukebox_device_check( juke_id: str = Query(None), retry: bool = Query(False) ): - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes") async with httpx.AsyncClient() as client: rDevice = await client.get( @@ -221,7 +213,7 @@ async def api_get_jukebox_device_check( status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth" ) else: - return api_get_jukebox_device_check(juke_id, retry=True) + return await api_get_jukebox_device_check(juke_id, retry=True) else: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="No device connected" @@ -233,10 +225,8 @@ async def api_get_jukebox_device_check( @jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}") async def api_get_jukebox_invoice(juke_id, song_id): - try: - jukebox = await get_jukebox(juke_id) - - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox") try: @@ -266,8 +256,7 @@ async def api_get_jukebox_invoice(juke_id, song_id): invoice=invoice[1], payment_hash=payment_hash, juke_id=juke_id, song_id=song_id ) jukebox_payment = await create_jukebox_payment(data) - - return data + return jukebox_payment @jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}") @@ -296,13 +285,12 @@ async def api_get_jukebox_invoice_paid( pay_hash: str = Query(None), retry: bool = Query(False), ): - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox") await api_get_jukebox_invoice_check(pay_hash, juke_id) jukebox_payment = await get_jukebox_payment(pay_hash) - if jukebox_payment.paid: + if jukebox_payment and jukebox_payment.paid: async with httpx.AsyncClient() as client: r = await client.get( "https://api.spotify.com/v1/me/player/currently-playing?market=ES", @@ -407,9 +395,8 @@ async def api_get_jukebox_invoice_paid( async def api_get_jukebox_currently( retry: bool = Query(False), juke_id: str = Query(None) ): - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox") async with httpx.AsyncClient() as client: try: diff --git a/lnbits/extensions/livestream/views_api.py b/lnbits/extensions/livestream/views_api.py index cc173a66..0c169a71 100644 --- a/lnbits/extensions/livestream/views_api.py +++ b/lnbits/extensions/livestream/views_api.py @@ -60,14 +60,14 @@ async def api_update_track(track_id, g: WalletTypeInfo = Depends(get_key_type)): ls = await get_or_create_livestream_by_wallet(g.wallet.id) await update_current_track(ls.id, id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT @livestream_ext.put("/api/v1/livestream/fee/{fee_pct}") async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)): ls = await get_or_create_livestream_by_wallet(g.wallet.id) await update_livestream_fee(ls.id, int(fee_pct)) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT @livestream_ext.post("/api/v1/livestream/tracks") @@ -93,8 +93,8 @@ async def api_add_track( return -@livestream_ext.route("/api/v1/livestream/tracks/{track_id}") +@livestream_ext.delete("/api/v1/livestream/tracks/{track_id}") async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)): ls = await get_or_create_livestream_by_wallet(g.wallet.id) await delete_track_from_livestream(ls.id, track_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/lnaddress/views_api.py b/lnbits/extensions/lnaddress/views_api.py index 8f403a38..46ef6b99 100644 --- a/lnbits/extensions/lnaddress/views_api.py +++ b/lnbits/extensions/lnaddress/views_api.py @@ -93,7 +93,7 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type) raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain") await delete_domain(domain_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT # ADDRESSES @@ -253,4 +253,4 @@ async def api_address_delete(address_id, g: WalletTypeInfo = Depends(get_key_typ ) await delete_address(address_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py index 7c9eb52c..cf6145b3 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -78,7 +78,7 @@ async def api_form_delete(form_id, wallet: WalletTypeInfo = Depends(get_key_type await delete_form(form_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT #########tickets########## @@ -160,4 +160,4 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_ raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.") await delete_ticket(ticket_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index b0b223ff..25dcf8c9 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -487,6 +487,17 @@ @click="copyText(lnurlValue, 'LNURL copied to clipboard!')" >Copy LNURL
+ {% raw %}{{ wsMessage }}{% endraw %} + {% raw %}{{ wsMessage }}{% endraw %}
PayLink: served_meta, served_pr, webhook_url, + webhook_headers, + webhook_body, success_text, success_url, comment_chars, currency, fiat_base_multiplier ) - VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?) {returning} """, ( @@ -36,6 +38,8 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: data.min, data.max, data.webhook_url, + data.webhook_headers, + data.webhook_body, data.success_text, data.success_url, data.comment_chars, diff --git a/lnbits/extensions/lnurlp/migrations.py b/lnbits/extensions/lnurlp/migrations.py index 81dd62f8..5258471d 100644 --- a/lnbits/extensions/lnurlp/migrations.py +++ b/lnbits/extensions/lnurlp/migrations.py @@ -60,3 +60,11 @@ async def m004_fiat_base_multiplier(db): await db.execute( "ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" ) + + +async def m005_webhook_headers_and_body(db): + """ + Add headers and body to webhooks + """ + await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;") + await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;") diff --git a/lnbits/extensions/lnurlp/models.py b/lnbits/extensions/lnurlp/models.py index 4bd438a4..2cb4d0ab 100644 --- a/lnbits/extensions/lnurlp/models.py +++ b/lnbits/extensions/lnurlp/models.py @@ -18,6 +18,8 @@ class CreatePayLinkData(BaseModel): currency: str = Query(None) comment_chars: int = Query(0, ge=0, lt=800) webhook_url: str = Query(None) + webhook_headers: str = Query(None) + webhook_body: str = Query(None) success_text: str = Query(None) success_url: str = Query(None) fiat_base_multiplier: int = Query(100, ge=1) @@ -31,6 +33,8 @@ class PayLink(BaseModel): served_meta: int served_pr: int webhook_url: Optional[str] + webhook_headers: Optional[str] + webhook_body: Optional[str] success_text: Optional[str] success_url: Optional[str] currency: Optional[str] diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index 86f1579a..23f312cb 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -33,17 +33,22 @@ async def on_invoice_paid(payment: Payment) -> None: if pay_link and pay_link.webhook_url: async with httpx.AsyncClient() as client: try: - r = await client.post( - pay_link.webhook_url, - json={ + kwargs = { + "json": { "payment_hash": payment.payment_hash, "payment_request": payment.bolt11, "amount": payment.amount, "comment": payment.extra.get("comment"), "lnurlp": pay_link.id, }, - timeout=40, - ) + "timeout": 40, + } + if pay_link.webhook_body: + kwargs["json"]["body"] = json.loads(pay_link.webhook_body) + if pay_link.webhook_headers: + kwargs["headers"] = json.loads(pay_link.webhook_headers) + + r = await client.post(pay_link.webhook_url, **kwargs) await mark_webhook_sent(payment, r.status_code) except (httpx.ConnectError, httpx.RequestError): await mark_webhook_sent(payment, -1) diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/index.html b/lnbits/extensions/lnurlp/templates/lnurlp/index.html index de90f5af..eb594cec 100644 --- a/lnbits/extensions/lnurlp/templates/lnurlp/index.html +++ b/lnbits/extensions/lnurlp/templates/lnurlp/index.html @@ -213,6 +213,24 @@ label="Webhook URL (optional)" hint="A URL to be called whenever this link receives a payment." > + + List[satsdiceL return [satsdiceLink(**row) for row in rows] -async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]: +async def update_satsdice_pay(link_id: str, **kwargs) -> satsdiceLink: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?", @@ -85,10 +84,10 @@ async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]: row = await db.fetchone( "SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,) ) - return satsdiceLink(**row) if row else None + return satsdiceLink(**row) -async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]: +async def increment_satsdice_pay(link_id: str, **kwargs) -> Optional[satsdiceLink]: q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()]) await db.execute( f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?", @@ -100,7 +99,7 @@ async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLin return satsdiceLink(**row) if row else None -async def delete_satsdice_pay(link_id: int) -> None: +async def delete_satsdice_pay(link_id: str) -> None: await db.execute("DELETE FROM satsdice.satsdice_pay WHERE id = ?", (link_id,)) @@ -119,9 +118,15 @@ async def create_satsdice_payment(data: CreateSatsDicePayment) -> satsdicePaymen ) VALUES (?, ?, ?, ?, ?) """, - (data["payment_hash"], data["satsdice_pay"], data["value"], False, False), + ( + data.payment_hash, + data.satsdice_pay, + data.value, + False, + False, + ), ) - payment = await get_satsdice_payment(data["payment_hash"]) + payment = await get_satsdice_payment(data.payment_hash) assert payment, "Newly created withdraw couldn't be retrieved" return payment @@ -134,9 +139,7 @@ async def get_satsdice_payment(payment_hash: str) -> Optional[satsdicePayment]: return satsdicePayment(**row) if row else None -async def update_satsdice_payment( - payment_hash: int, **kwargs -) -> Optional[satsdicePayment]: +async def update_satsdice_payment(payment_hash: str, **kwargs) -> satsdicePayment: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( @@ -147,7 +150,7 @@ async def update_satsdice_payment( "SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?", (payment_hash,), ) - return satsdicePayment(**row) if row else None + return satsdicePayment(**row) ##################SATSDICE WITHDRAW LINKS @@ -168,16 +171,16 @@ async def create_satsdice_withdraw(data: CreateSatsDiceWithdraw) -> satsdiceWith VALUES (?, ?, ?, ?, ?, ?, ?) """, ( - data["payment_hash"], - data["satsdice_pay"], - data["value"], + data.payment_hash, + data.satsdice_pay, + data.value, urlsafe_short_hash(), urlsafe_short_hash(), int(datetime.now().timestamp()), - data["used"], + data.used, ), ) - withdraw = await get_satsdice_withdraw(data["payment_hash"], 0) + withdraw = await get_satsdice_withdraw(data.payment_hash, 0) assert withdraw, "Newly created withdraw couldn't be retrieved" return withdraw @@ -247,7 +250,7 @@ async def delete_satsdice_withdraw(withdraw_id: str) -> None: ) -async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck: +async def create_withdraw_hash_check(the_hash: str, lnurl_id: str): await db.execute( """ INSERT INTO satsdice.hash_checkw ( @@ -262,19 +265,15 @@ async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck: return hashCheck -async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: +async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str): rowid = await db.fetchone( "SELECT * FROM satsdice.hash_checkw WHERE id = ?", (the_hash,) ) rowlnurl = await db.fetchone( "SELECT * FROM satsdice.hash_checkw WHERE lnurl_id = ?", (lnurl_id,) ) - if not rowlnurl: + if not rowlnurl or not rowid: await create_withdraw_hash_check(the_hash, lnurl_id) return {"lnurl": True, "hash": False} else: - if not rowid: - await create_withdraw_hash_check(the_hash, lnurl_id) - return {"lnurl": True, "hash": False} - else: - return {"lnurl": True, "hash": True} + return {"lnurl": True, "hash": True} diff --git a/lnbits/extensions/satsdice/lnurl.py b/lnbits/extensions/satsdice/lnurl.py index caafc3a4..a9b3cf08 100644 --- a/lnbits/extensions/satsdice/lnurl.py +++ b/lnbits/extensions/satsdice/lnurl.py @@ -1,4 +1,3 @@ -import hashlib import json import math from http import HTTPStatus @@ -83,15 +82,18 @@ async def api_lnurlp_callback( success_action = link.success_action(payment_hash=payment_hash, req=req) - data: CreateSatsDicePayment = { - "satsdice_pay": link.id, - "value": amount_received / 1000, - "payment_hash": payment_hash, - } + data = CreateSatsDicePayment( + satsdice_pay=link.id, + value=amount_received / 1000, + payment_hash=payment_hash, + ) await create_satsdice_payment(data) - payResponse = {"pr": payment_request, "successAction": success_action, "routes": []} - + payResponse: dict = { + "pr": payment_request, + "successAction": success_action, + "routes": [], + } return json.dumps(payResponse) @@ -133,9 +135,7 @@ async def api_lnurlw_response(req: Request, unique_hash: str = Query(None)): name="satsdice.api_lnurlw_callback", ) async def api_lnurlw_callback( - req: Request, unique_hash: str = Query(None), - k1: str = Query(None), pr: str = Query(None), ): @@ -146,12 +146,13 @@ async def api_lnurlw_callback( return {"status": "ERROR", "reason": "spent"} paylink = await get_satsdice_pay(link.satsdice_pay) - await update_satsdice_withdraw(link.id, used=1) - await pay_invoice( - wallet_id=paylink.wallet, - payment_request=pr, - max_sat=link.value, - extra={"tag": "withdraw"}, - ) + if paylink: + await update_satsdice_withdraw(link.id, used=1) + await pay_invoice( + wallet_id=paylink.wallet, + payment_request=pr, + max_sat=link.value, + extra={"tag": "withdraw"}, + ) - return {"status": "OK"} + return {"status": "OK"} diff --git a/lnbits/extensions/satsdice/models.py b/lnbits/extensions/satsdice/models.py index fd9af74f..2537f8d7 100644 --- a/lnbits/extensions/satsdice/models.py +++ b/lnbits/extensions/satsdice/models.py @@ -4,7 +4,7 @@ from typing import Dict, Optional from fastapi import Request from fastapi.param_functions import Query -from lnurl import Lnurl, LnurlWithdrawResponse +from lnurl import Lnurl from lnurl import encode as lnurl_encode # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore from pydantic import BaseModel @@ -80,8 +80,7 @@ class satsdiceWithdraw(BaseModel): def is_spent(self) -> bool: return self.used >= 1 - @property - def lnurl_response(self, req: Request) -> LnurlWithdrawResponse: + def lnurl_response(self, req: Request): url = req.url_for("satsdice.api_lnurlw_callback", unique_hash=self.unique_hash) withdrawResponse = { "tag": "withdrawRequest", @@ -99,7 +98,7 @@ class HashCheck(BaseModel): lnurl_id: str @classmethod - def from_row(cls, row: Row) -> "Hash": + def from_row(cls, row: Row): return cls(**dict(row)) diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py index 72e24867..d2b5e601 100644 --- a/lnbits/extensions/satsdice/views.py +++ b/lnbits/extensions/satsdice/views.py @@ -1,6 +1,8 @@ import random from http import HTTPStatus +from io import BytesIO +import pyqrcode from fastapi import Request from fastapi.param_functions import Query from fastapi.params import Depends @@ -20,13 +22,15 @@ from .crud import ( get_satsdice_withdraw, update_satsdice_payment, ) -from .models import CreateSatsDiceWithdraw, satsdiceLink +from .models import CreateSatsDiceWithdraw templates = Jinja2Templates(directory="templates") @satsdice_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 satsdice_renderer().TemplateResponse( "satsdice/index.html", {"request": request, "user": user.dict()} ) @@ -67,7 +71,7 @@ async def displaywin( ) withdrawLink = await get_satsdice_withdraw(payment_hash) payment = await get_satsdice_payment(payment_hash) - if payment.lost: + if not payment or payment.lost: return satsdice_renderer().TemplateResponse( "satsdice/error.html", {"request": request, "link": satsdicelink.id, "paid": False, "lost": True}, @@ -96,13 +100,18 @@ async def displaywin( ) await update_satsdice_payment(payment_hash, paid=1) paylink = await get_satsdice_payment(payment_hash) + if not paylink: + return satsdice_renderer().TemplateResponse( + "satsdice/error.html", + {"request": request, "link": satsdicelink.id, "paid": False, "lost": True}, + ) - data: CreateSatsDiceWithdraw = { - "satsdice_pay": satsdicelink.id, - "value": paylink.value * satsdicelink.multiplier, - "payment_hash": payment_hash, - "used": 0, - } + data = CreateSatsDiceWithdraw( + satsdice_pay=satsdicelink.id, + value=paylink.value * satsdicelink.multiplier, + payment_hash=payment_hash, + used=0, + ) withdrawLink = await create_satsdice_withdraw(data) return satsdice_renderer().TemplateResponse( @@ -121,9 +130,12 @@ async def displaywin( @satsdice_ext.get("/img/{link_id}", response_class=HTMLResponse) async def img(link_id): - link = await get_satsdice_pay(link_id) or abort( - HTTPStatus.NOT_FOUND, "satsdice link does not exist." - ) + link = await get_satsdice_pay(link_id) + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="satsdice link does not exist." + ) + qr = pyqrcode.create(link.lnurl) stream = BytesIO() qr.svg(stream, scale=3) diff --git a/lnbits/extensions/satsdice/views_api.py b/lnbits/extensions/satsdice/views_api.py index bccaa5ff..d33b76b8 100644 --- a/lnbits/extensions/satsdice/views_api.py +++ b/lnbits/extensions/satsdice/views_api.py @@ -15,9 +15,10 @@ from .crud import ( delete_satsdice_pay, get_satsdice_pay, get_satsdice_pays, + get_withdraw_hash_checkw, update_satsdice_pay, ) -from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink +from .models import CreateSatsDiceLink ################LNURL pay @@ -25,13 +26,15 @@ from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink @satsdice_ext.get("/api/v1/links") async def api_links( request: Request, - wallet: WalletTypeInfo = Depends(get_key_type), + wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore all_wallets: bool = Query(False), ): wallet_ids = [wallet.wallet.id] if all_wallets: - wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + user = await get_user(wallet.wallet.user) + if user: + wallet_ids = user.wallet_ids try: links = await get_satsdice_pays(wallet_ids) @@ -46,7 +49,7 @@ async def api_links( @satsdice_ext.get("/api/v1/links/{link_id}") async def api_link_retrieve( - link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) + link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore ): link = await get_satsdice_pay(link_id) @@ -67,7 +70,7 @@ async def api_link_retrieve( @satsdice_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) async def api_link_create_or_update( data: CreateSatsDiceLink, - wallet: WalletTypeInfo = Depends(get_key_type), + wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore link_id: str = Query(None), ): if data.min_bet > data.max_bet: @@ -95,10 +98,10 @@ async def api_link_create_or_update( @satsdice_ext.delete("/api/v1/links/{link_id}") async def api_link_delete( - wallet: WalletTypeInfo = Depends(get_key_type), link_id: str = Query(None) + wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore + link_id: str = Query(None), ): link = await get_satsdice_pay(link_id) - if not link: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." @@ -117,11 +120,12 @@ async def api_link_delete( ##########LNURL withdraw -@satsdice_ext.get("/api/v1/withdraws/{the_hash}/{lnurl_id}") +@satsdice_ext.get( + "/api/v1/withdraws/{the_hash}/{lnurl_id}", dependencies=[Depends(get_key_type)] +) async def api_withdraw_hash_retrieve( - wallet: WalletTypeInfo = Depends(get_key_type), lnurl_id: str = Query(None), the_hash: str = Query(None), ): - hashCheck = await get_withdraw_hash_check(the_hash, lnurl_id) + hashCheck = await get_withdraw_hash_checkw(the_hash, lnurl_id) return hashCheck diff --git a/lnbits/extensions/satspay/README.md b/lnbits/extensions/satspay/README.md index d52547ae..7a12feb3 100644 --- a/lnbits/extensions/satspay/README.md +++ b/lnbits/extensions/satspay/README.md @@ -18,7 +18,7 @@ Easilly create invoices that support Lightning Network and on-chain BTC payment. ![charge form](https://i.imgur.com/F10yRiW.png) 3. The charge will appear on the _Charges_ section\ ![charges](https://i.imgur.com/zqHpVxc.png) -4. Your costumer/payee will get the payment page +4. Your customer/payee will get the payment page - they can choose to pay on LN\ ![offchain payment](https://i.imgur.com/4191SMV.png) - or pay on chain\ diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py index 23d391b7..968c9ab0 100644 --- a/lnbits/extensions/satspay/crud.py +++ b/lnbits/extensions/satspay/crud.py @@ -1,15 +1,15 @@ +import json from typing import List, Optional -import httpx +from loguru import logger from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.helpers import urlsafe_short_hash from ..watchonly.crud import get_config, get_fresh_address - -# from lnbits.db import open_ext_db from . import db +from .helpers import fetch_onchain_balance from .models import Charges, CreateCharge ###############CHARGES########################## @@ -18,6 +18,10 @@ from .models import Charges, CreateCharge async def create_charge(user: str, data: CreateCharge) -> Charges: charge_id = urlsafe_short_hash() if data.onchainwallet: + config = await get_config(user) + data.extra = json.dumps( + {"mempool_endpoint": config.mempool_endpoint, "network": config.network} + ) onchain = await get_fresh_address(data.onchainwallet) onchainaddress = onchain.address else: @@ -48,9 +52,10 @@ async def create_charge(user: str, data: CreateCharge) -> Charges: completelinktext, time, amount, - balance + balance, + extra ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( charge_id, @@ -67,6 +72,7 @@ async def create_charge(user: str, data: CreateCharge) -> Charges: data.time, data.amount, 0, + data.extra, ), ) return await get_charge(charge_id) @@ -98,34 +104,20 @@ async def delete_charge(charge_id: str) -> None: await db.execute("DELETE FROM satspay.charges WHERE id = ?", (charge_id,)) -async def check_address_balance(charge_id: str) -> List[Charges]: +async def check_address_balance(charge_id: str) -> Optional[Charges]: charge = await get_charge(charge_id) + if not charge.paid: if charge.onchainaddress: - config = await get_charge_config(charge_id) try: - async with httpx.AsyncClient() as client: - r = await client.get( - config.mempool_endpoint - + "/api/address/" - + charge.onchainaddress - ) - respAmount = r.json()["chain_stats"]["funded_txo_sum"] - if respAmount > charge.balance: - await update_charge(charge_id=charge_id, balance=respAmount) - except Exception: - pass + respAmount = await fetch_onchain_balance(charge) + if respAmount > charge.balance: + await update_charge(charge_id=charge_id, balance=respAmount) + except Exception as e: + logger.warning(e) if charge.lnbitswallet: invoice_status = await api_payment(charge.payment_hash) if invoice_status["paid"]: return await update_charge(charge_id=charge_id, balance=charge.amount) - row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,)) - return Charges.from_row(row) if row else None - - -async def get_charge_config(charge_id: str): - row = await db.fetchone( - """SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,) - ) - return await get_config(row.user) + return await get_charge(charge_id) diff --git a/lnbits/extensions/satspay/helpers.py b/lnbits/extensions/satspay/helpers.py index 2d15b557..2aa83e1f 100644 --- a/lnbits/extensions/satspay/helpers.py +++ b/lnbits/extensions/satspay/helpers.py @@ -1,8 +1,11 @@ +import httpx +from loguru import logger + from .models import Charges -def compact_charge(charge: Charges): - return { +def public_charge(charge: Charges): + c = { "id": charge.id, "description": charge.description, "onchainaddress": charge.onchainaddress, @@ -13,5 +16,38 @@ def compact_charge(charge: Charges): "balance": charge.balance, "paid": charge.paid, "timestamp": charge.timestamp, - "completelink": charge.completelink, # should be secret? + "time_elapsed": charge.time_elapsed, + "time_left": charge.time_left, + "paid": charge.paid, } + + if charge.paid: + c["completelink"] = charge.completelink + + return c + + +async def call_webhook(charge: Charges): + async with httpx.AsyncClient() as client: + try: + r = await client.post( + charge.webhook, + json=public_charge(charge), + timeout=40, + ) + return {"webhook_success": r.is_success, "webhook_message": r.reason_phrase} + except Exception as e: + logger.warning(f"Failed to call webhook for charge {charge.id}") + logger.warning(e) + return {"webhook_success": False, "webhook_message": str(e)} + + +async def fetch_onchain_balance(charge: Charges): + endpoint = ( + f"{charge.config.mempool_endpoint}/testnet" + if charge.config.network == "Testnet" + else charge.config.mempool_endpoint + ) + async with httpx.AsyncClient() as client: + r = await client.get(endpoint + "/api/address/" + charge.onchainaddress) + return r.json()["chain_stats"]["funded_txo_sum"] diff --git a/lnbits/extensions/satspay/migrations.py b/lnbits/extensions/satspay/migrations.py index 87446c80..2579961f 100644 --- a/lnbits/extensions/satspay/migrations.py +++ b/lnbits/extensions/satspay/migrations.py @@ -26,3 +26,14 @@ async def m001_initial(db): ); """ ) + + +async def m002_add_charge_extra_data(db): + """ + Add 'extra' column for storing various config about the charge (JSON format) + """ + await db.execute( + """ALTER TABLE satspay.charges + ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}'; + """ + ) diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py index daf63f42..1e7c95c9 100644 --- a/lnbits/extensions/satspay/models.py +++ b/lnbits/extensions/satspay/models.py @@ -1,3 +1,4 @@ +import json from datetime import datetime, timedelta from sqlite3 import Row from typing import Optional @@ -15,6 +16,14 @@ class CreateCharge(BaseModel): completelinktext: str = Query(None) time: int = Query(..., ge=1) amount: int = Query(..., ge=1) + extra: str = "{}" + + +class ChargeConfig(BaseModel): + mempool_endpoint: Optional[str] + network: Optional[str] + webhook_success: Optional[bool] = False + webhook_message: Optional[str] class Charges(BaseModel): @@ -28,6 +37,7 @@ class Charges(BaseModel): webhook: Optional[str] completelink: Optional[str] completelinktext: Optional[str] = "Back to Merchant" + extra: str = "{}" time: int amount: int balance: int @@ -54,3 +64,11 @@ class Charges(BaseModel): return True else: return False + + @property + def config(self) -> ChargeConfig: + charge_config = json.loads(self.extra) + return ChargeConfig(**charge_config) + + def must_call_webhook(self): + return self.webhook and self.paid and self.config.webhook_success == False diff --git a/lnbits/extensions/satspay/static/js/utils.js b/lnbits/extensions/satspay/static/js/utils.js index 9b4abbfc..92927955 100644 --- a/lnbits/extensions/satspay/static/js/utils.js +++ b/lnbits/extensions/satspay/static/js/utils.js @@ -14,15 +14,14 @@ const retryWithDelay = async function (fn, retryCount = 0) { } const mapCharge = (obj, oldObj = {}) => { - const charge = _.clone(obj) + const charge = {...oldObj, ...obj} charge.progress = obj.time_left < 0 ? 1 : 1 - obj.time_left / obj.time charge.time = minutesToTime(obj.time) charge.timeLeft = minutesToTime(obj.time_left) - charge.expanded = false charge.displayUrl = ['/satspay/', obj.id].join('') - charge.expanded = oldObj.expanded + charge.expanded = oldObj.expanded || false charge.pendingBalance = oldObj.pendingBalance || 0 return charge } diff --git a/lnbits/extensions/satspay/tasks.py b/lnbits/extensions/satspay/tasks.py index 46c16bbc..ce54b44a 100644 --- a/lnbits/extensions/satspay/tasks.py +++ b/lnbits/extensions/satspay/tasks.py @@ -1,4 +1,5 @@ import asyncio +import json from loguru import logger @@ -7,7 +8,8 @@ from lnbits.extensions.satspay.crud import check_address_balance, get_charge from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -# from .crud import get_ticket, set_ticket_paid +from .crud import update_charge +from .helpers import call_webhook async def wait_for_paid_invoices(): @@ -30,4 +32,9 @@ async def on_invoice_paid(payment: Payment) -> None: return await payment.set_pending(False) - await check_address_balance(charge_id=charge.id) + charge = await check_address_balance(charge_id=charge.id) + + if charge.must_call_webhook(): + resp = await call_webhook(charge) + extra = {**charge.config.dict(), **resp} + await update_charge(charge_id=charge.id, extra=json.dumps(extra)) diff --git a/lnbits/extensions/satspay/templates/satspay/display.html b/lnbits/extensions/satspay/templates/satspay/display.html index 12288c80..a24ed84c 100644 --- a/lnbits/extensions/satspay/templates/satspay/display.html +++ b/lnbits/extensions/satspay/templates/satspay/display.html @@ -109,7 +109,7 @@ @@ -131,7 +131,7 @@ @@ -170,13 +170,17 @@ name="check" style="color: green; font-size: 21.4em" > - +
+
+ +
+
@@ -218,7 +222,7 @@
@@ -303,7 +311,8 @@ data() { return { charge: JSON.parse('{{charge_data | tojson}}'), - mempool_endpoint: '{{mempool_endpoint}}', + mempoolEndpoint: '{{mempool_endpoint}}', + network: '{{network}}', pendingFunds: 0, ws: null, newProgress: 0.4, @@ -316,19 +325,19 @@ cancelListener: () => {} } }, + computed: { + mempoolHostname: function () { + let hostname = new URL(this.mempoolEndpoint).hostname + if (this.network === 'Testnet') { + hostname += '/testnet' + } + return hostname + } + }, methods: { - startPaymentNotifier() { - this.cancelListener() - if (!this.lnbitswallet) return - this.cancelListener = LNbits.events.onInvoicePaid( - this.wallet, - payment => { - this.checkInvoiceBalance() - } - ) - }, checkBalances: async function () { - if (this.charge.hasStaleBalance) return + if (!this.charge.payment_request && this.charge.hasOnchainStaleBalance) + return try { const {data} = await LNbits.api.request( 'GET', @@ -345,7 +354,7 @@ const { bitcoin: {addresses: addressesAPI} } = mempoolJS({ - hostname: new URL(this.mempool_endpoint).hostname + hostname: new URL(this.mempoolEndpoint).hostname }) try { @@ -353,7 +362,8 @@ address: this.charge.onchainaddress }) const newBalance = utxos.reduce((t, u) => t + u.value, 0) - this.charge.hasStaleBalance = this.charge.balance === newBalance + this.charge.hasOnchainStaleBalance = + this.charge.balance === newBalance this.pendingFunds = utxos .filter(u => !u.status.confirmed) @@ -388,10 +398,10 @@ const { bitcoin: {websocket} } = mempoolJS({ - hostname: new URL(this.mempool_endpoint).hostname + hostname: new URL(this.mempoolEndpoint).hostname }) - this.ws = new WebSocket('wss://mempool.space/api/v1/ws') + this.ws = new WebSocket(`wss://${this.mempoolHostname}/api/v1/ws`) this.ws.addEventListener('open', x => { if (this.charge.onchainaddress) { this.trackAddress(this.charge.onchainaddress) @@ -428,13 +438,10 @@ } }, created: async function () { - if (this.charge.lnbitswallet) this.payInvoice() + if (this.charge.payment_request) this.payInvoice() else this.payOnchain() - await this.checkBalances() - // empty for onchain - this.wallet.inkey = '{{ wallet_inkey }}' - this.startPaymentNotifier() + await this.checkBalances() if (!this.charge.paid) { this.loopRefresh() diff --git a/lnbits/extensions/satspay/templates/satspay/index.html b/lnbits/extensions/satspay/templates/satspay/index.html index 396200cf..60c4d519 100644 --- a/lnbits/extensions/satspay/templates/satspay/index.html +++ b/lnbits/extensions/satspay/templates/satspay/index.html @@ -203,9 +203,14 @@ :href="props.row.webhook" target="_blank" style="color: unset; text-decoration: none" - >{{props.row.webhook || props.row.webhook}}{{props.row.webhook}}
+
+ + {{props.row.webhook_message }} + +
ID:
@@ -409,10 +414,11 @@ balance: null, walletLinks: [], chargeLinks: [], - onchainwallet: '', + onchainwallet: null, rescanning: false, mempool: { - endpoint: '' + endpoint: '', + network: 'Mainnet' }, chargesTable: { @@ -505,6 +511,7 @@ methods: { cancelCharge: function (data) { this.formDialogCharge.data.description = '' + this.formDialogCharge.data.onchain = false this.formDialogCharge.data.onchainwallet = '' this.formDialogCharge.data.lnbitswallet = '' this.formDialogCharge.data.time = null @@ -518,7 +525,7 @@ try { const {data} = await LNbits.api.request( 'GET', - '/watchonly/api/v1/wallet', + `/watchonly/api/v1/wallet?network=${this.mempool.network}`, this.g.user.wallets[0].inkey ) this.walletLinks = data.map(w => ({ @@ -538,6 +545,7 @@ this.g.user.wallets[0].inkey ) this.mempool.endpoint = data.mempool_endpoint + this.mempool.network = data.network || 'Mainnet' const url = new URL(this.mempool.endpoint) this.mempool.hostname = url.hostname } catch (error) { @@ -577,7 +585,8 @@ const data = this.formDialogCharge.data data.amount = parseInt(data.amount) data.time = parseInt(data.time) - data.onchainwallet = this.onchainwallet?.id + data.lnbitswallet = data.lnbits ? data.lnbitswallet : null + data.onchainwallet = data.onchain ? this.onchainwallet?.id : null this.createCharge(wallet, data) }, refreshActiveChargesBalance: async function () { @@ -695,8 +704,8 @@ }, created: async function () { await this.getCharges() - await this.getWalletLinks() await this.getWalletConfig() + await this.getWalletLinks() setInterval(() => this.refreshActiveChargesBalance(), 10 * 2000) await this.rescanOnchainAddresses() setInterval(() => this.rescanOnchainAddresses(), 10 * 1000) diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py index b789bf8f..7b769a20 100644 --- a/lnbits/extensions/satspay/views.py +++ b/lnbits/extensions/satspay/views.py @@ -6,12 +6,12 @@ from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import HTMLResponse -from lnbits.core.crud import get_wallet from lnbits.core.models import User from lnbits.decorators import check_user_exists +from lnbits.extensions.satspay.helpers import public_charge from . import satspay_ext, satspay_renderer -from .crud import get_charge, get_charge_config +from .crud import get_charge templates = Jinja2Templates(directory="templates") @@ -30,18 +30,13 @@ async def display(request: Request, charge_id: str): raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist." ) - wallet = await get_wallet(charge.lnbitswallet) - onchainwallet_config = await get_charge_config(charge_id) - inkey = wallet.inkey if wallet else None - mempool_endpoint = ( - onchainwallet_config.mempool_endpoint if onchainwallet_config else None - ) + return satspay_renderer().TemplateResponse( "satspay/display.html", { "request": request, - "charge_data": charge.dict(), - "wallet_inkey": inkey, - "mempool_endpoint": mempool_endpoint, + "charge_data": public_charge(charge), + "mempool_endpoint": charge.config.mempool_endpoint, + "network": charge.config.network, }, ) diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 73c87e7c..bfff55a2 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -1,6 +1,6 @@ +import json from http import HTTPStatus -import httpx from fastapi.params import Depends from starlette.exceptions import HTTPException @@ -20,7 +20,7 @@ from .crud import ( get_charges, update_charge, ) -from .helpers import compact_charge +from .helpers import call_webhook, public_charge from .models import CreateCharge #############################CHARGES########################## @@ -58,6 +58,7 @@ async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): **{"time_elapsed": charge.time_elapsed}, **{"time_left": charge.time_left}, **{"paid": charge.paid}, + **{"webhook_message": charge.config.webhook_message}, } for charge in await get_charges(wallet.wallet.user) ] @@ -94,7 +95,7 @@ async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_ ) await delete_charge(charge_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT #############################BALANCE########################## @@ -119,19 +120,9 @@ async def api_charge_balance(charge_id): status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist." ) - if charge.paid and charge.webhook: - async with httpx.AsyncClient() as client: - try: - r = await client.post( - charge.webhook, - json=compact_charge(charge), - timeout=40, - ) - except AssertionError: - charge.webhook = None - return { - **compact_charge(charge), - **{"time_elapsed": charge.time_elapsed}, - **{"time_left": charge.time_left}, - **{"paid": charge.paid}, - } + if charge.must_call_webhook(): + resp = await call_webhook(charge) + extra = {**charge.config.dict(), **resp} + await update_charge(charge_id=charge.id, extra=json.dumps(extra)) + + return {**public_charge(charge)} diff --git a/lnbits/extensions/scrub/views_api.py b/lnbits/extensions/scrub/views_api.py index 3714a304..cc55c15d 100644 --- a/lnbits/extensions/scrub/views_api.py +++ b/lnbits/extensions/scrub/views_api.py @@ -109,4 +109,4 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi ) await delete_scrub_link(link_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/index.html b/lnbits/extensions/splitpayments/templates/splitpayments/index.html index 5862abc1..1cceb7ba 100644 --- a/lnbits/extensions/splitpayments/templates/splitpayments/index.html +++ b/lnbits/extensions/splitpayments/templates/splitpayments/index.html @@ -31,14 +31,20 @@ style="flex-wrap: nowrap" v-for="(target, t) in targets" > - + option-label="name" + style="width: 1000px" + new-value-mode="add-unique" + use-input + input-debounce="0" + emit-value + > Hit enter to add values + >Hit enter to add values + +
-

{% raw %}{{ famount }}{% endraw %}

+

{% raw %}{{ amountFormatted }}{% endraw %}

{% raw %}{{ fsat }}{% endraw %} sat
@@ -148,6 +148,14 @@
+ + +
-

{% raw %}{{ famount }}{% endraw %}

+

+ {% raw %}{{ amountWithTipFormatted }}{% endraw %} +

{% raw %}{{ fsat }} sat ( + {{ tipAmountSat }} tip)( + {{ tipAmountFormatted }} tip) {% endraw %}
@@ -204,19 +214,48 @@ style="padding: 10px; margin: 3px" unelevated @click="processTipSelection(tip)" - size="xl" + size="lg" :outline="!($q.dark.isActive)" rounded color="primary" - v-for="tip in this.tip_options" + v-for="tip in tip_options.filter(f => f != 'Round')" :key="tip" >{% raw %}{{ tip }}{% endraw %}% -
-
-

No, thanks

+ +
+ + + Ok +
+ No, thanks Close
@@ -256,6 +295,38 @@ style="font-size: min(90vw, 40em)" >
+ + + + + + + + + + + No paid invoices + + + + {%raw%} + + {{payment.amount / 1000}} sats + Hash: {{payment.checking_id.slice(0, 30)}}... + + + {{payment.dateFrom}} + + + {%endraw%} + + + + {% endblock %} {% block styles %} @@ -294,8 +365,13 @@ exchangeRate: null, stack: [], tipAmount: 0.0, + tipRounding: null, hasNFC: false, nfcTagReading: false, + lastPaymentsDialog: { + show: false, + data: [] + }, invoiceDialog: { show: false, data: null, @@ -310,32 +386,81 @@ }, complete: { show: false - } + }, + rounding: false } }, computed: { amount: function () { if (!this.stack.length) return 0.0 - return (Number(this.stack.join('')) / 100).toFixed(2) + return Number(this.stack.join('') / 100) }, - famount: function () { - return LNbits.utils.formatCurrency(this.amount, this.currency) + amountFormatted: function () { + return LNbits.utils.formatCurrency( + this.amount.toFixed(2), + this.currency + ) + }, + amountWithTipFormatted: function () { + return LNbits.utils.formatCurrency( + (this.amount + this.tipAmount).toFixed(2), + this.currency + ) }, sat: function () { if (!this.exchangeRate) return 0 - return Math.ceil( - ((this.amount - this.tipAmount) / this.exchangeRate) * 100000000 - ) + return Math.ceil((this.amount / this.exchangeRate) * 100000000) }, tipAmountSat: function () { if (!this.exchangeRate) return 0 return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000) }, + tipAmountFormatted: function () { + return LNbits.utils.formatSat(this.tipAmountSat) + }, fsat: function () { return LNbits.utils.formatSat(this.sat) + }, + isRoundValid() { + return this.tipRounding > this.amount + }, + roundToSugestion() { + switch (true) { + case this.amount > 50: + toNext = 10 + break + case this.amount > 6: + toNext = 5 + break + case this.amount > 2.5: + toNext = 1 + break + default: + toNext = 0.5 + break + } + + return Math.ceil(this.amount / toNext) * toNext } }, methods: { + setRounding() { + this.rounding = true + this.tipRounding = this.roundToSugestion + this.$nextTick(() => this.$refs.inputRounding.focus()) + }, + calculatePercent() { + let change = ((this.tipRounding - this.amount) / this.amount) * 100 + if (change < 0) { + this.$q.notify({ + type: 'warning', + message: 'Amount with tip must be greater than initial amount.' + }) + this.tipRounding = this.roundToSugestion + return + } + this.processTipSelection(change) + }, closeInvoiceDialog: function () { this.stack = [] this.tipAmount = 0.0 @@ -348,30 +473,18 @@ processTipSelection: function (selectedTipOption) { this.tipDialog.show = false - if (selectedTipOption) { - const tipAmount = parseFloat( - parseFloat((selectedTipOption / 100) * this.amount) - ) - const subtotal = parseFloat(this.amount) - const grandTotal = parseFloat((tipAmount + subtotal).toFixed(2)) - const totalString = grandTotal.toFixed(2).toString() - - this.stack = [] - for (var i = 0; i < totalString.length; i++) { - const char = totalString[i] - - if (char !== '.') { - this.stack.push(char) - } - } - - this.tipAmount = tipAmount + if (!selectedTipOption) { + this.tipAmount = 0.0 + return this.showInvoice() } + this.tipAmount = (selectedTipOption / 100) * this.amount this.showInvoice() }, submitForm: function () { if (this.tip_options && this.tip_options.length) { + this.rounding = false + this.tipRounding = null this.showTipModal() } else { this.showInvoice() @@ -520,6 +633,24 @@ self.exchangeRate = response.data.data['BTC' + self.currency][self.currency] }) + }, + getLastPayments() { + return axios + .get(`/tpos/api/v1/tposs/${this.tposId}/invoices`) + .then(res => { + if (res.data && res.data.length) { + let last = [...res.data] + this.lastPaymentsDialog.data = last.map(obj => { + obj.dateFrom = moment(obj.time * 1000).fromNow() + return obj + }) + } + }) + .catch(e => console.error(e)) + }, + showLastPayments() { + this.getLastPayments() + this.lastPaymentsDialog.show = true } }, created: function () { @@ -529,10 +660,26 @@ '{{ tpos.tip_options | tojson }}' == 'null' ? null : JSON.parse('{{ tpos.tip_options }}') + + if ('{{ tpos.tip_wallet }}') { + this.tip_options.push('Round') + } setInterval(function () { getRates() }, 120000) } }) + {% endblock %} diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index b7f14b98..fe63a247 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -7,7 +7,8 @@ from lnurl import decode as decode_lnurl from loguru import logger from starlette.exceptions import HTTPException -from lnbits.core.crud import get_user +from lnbits.core.crud import get_latest_payments_by_extension, get_user +from lnbits.core.models import Payment from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key @@ -51,7 +52,7 @@ async def api_tpos_delete( raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your TPoS.") await delete_tpos(tpos_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT @tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED) @@ -81,6 +82,30 @@ async def api_tpos_create_invoice( return {"payment_hash": payment_hash, "payment_request": payment_request} +@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices") +async def api_tpos_get_latest_invoices(tpos_id: str = None): + try: + payments = [ + Payment.from_row(row) + for row in await get_latest_payments_by_extension( + ext_name="tpos", ext_id=tpos_id + ) + ] + + except Exception as e: + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + + return [ + { + "checking_id": payment.checking_id, + "amount": payment.amount, + "time": payment.time, + "pending": payment.pending, + } + for payment in payments + ] + + @tpos_ext.post( "/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK ) diff --git a/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html b/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html index c65ad1c4..0df5bebf 100644 --- a/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html +++ b/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html @@ -6,6 +6,7 @@ filled dense v-model.number="feeRate" + step="any" :rules="[val => !!val || 'Field is required']" type="number" label="sats/vbyte" diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 660e5b7d..5737e54f 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -9,7 +9,7 @@ from fastapi import HTTPException from fastapi.param_functions import Query from loguru import logger from starlette.requests import Request -from starlette.responses import HTMLResponse # type: ignore +from starlette.responses import HTMLResponse from lnbits.core.services import pay_invoice @@ -51,10 +51,24 @@ async def api_lnurl_response(request: Request, unique_hash): # CALLBACK -@withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", name="withdraw.api_lnurl_callback") +@withdraw_ext.get( + "/api/v1/lnurl/cb/{unique_hash}", + name="withdraw.api_lnurl_callback", + summary="lnurl withdraw callback", + description=""" + This enpoints allows you to put unique_hash, k1 + and a payment_request to get your payment_request paid. + """, + response_description="JSON with status", + responses={ + 200: {"description": "status: OK"}, + 400: {"description": "k1 is wrong or link open time or withdraw not working."}, + 404: {"description": "withdraw link not found."}, + 405: {"description": "withdraw link is spent."}, + }, +) async def api_lnurl_callback( unique_hash, - request: Request, k1: str = Query(...), pr: str = Query(...), id_unique_hash=None, @@ -63,19 +77,22 @@ async def api_lnurl_callback( now = int(datetime.now().timestamp()) if not link: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found" + status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found." ) if link.is_spent: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent." + status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="withdraw is spent." ) if link.k1 != k1: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Bad request.") + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="k1 is wrong.") if now < link.open_time: - return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."} + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"wait link open_time {link.open_time - now} seconds.", + ) usescsv = "" @@ -95,7 +112,7 @@ async def api_lnurl_callback( usescsv = ",".join(useslist) if not found: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found." + status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found." ) else: usescsv = usescsv[1:] @@ -144,7 +161,9 @@ async def api_lnurl_callback( except Exception as e: await update_withdraw_link(link.id, **changesback) logger.error(traceback.format_exc()) - return {"status": "ERROR", "reason": "Link not working"} + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}" + ) # FOR LNURLs WHICH ARE UNIQUE diff --git a/lnbits/extensions/withdraw/static/js/index.js b/lnbits/extensions/withdraw/static/js/index.js index 943e9024..a3eaa593 100644 --- a/lnbits/extensions/withdraw/static/js/index.js +++ b/lnbits/extensions/withdraw/static/js/index.js @@ -290,8 +290,12 @@ new Vue({ }) } }, - exportCSV: function () { - LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls) + exportCSV() { + LNbits.utils.exportCSV( + this.withdrawLinksTable.columns, + this.withdrawLinks, + 'withdraw-links' + ) } }, created: function () { diff --git a/lnbits/helpers.py b/lnbits/helpers.py index e213240c..83876160 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -163,6 +163,7 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates: ) if settings.LNBITS_AD_SPACE: + t.env.globals["AD_TITLE"] = settings.LNBITS_AD_SPACE_TITLE t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE diff --git a/lnbits/server.py b/lnbits/server.py index e9849851..7aaaa964 100644 --- a/lnbits/server.py +++ b/lnbits/server.py @@ -1,9 +1,7 @@ -import time - import click import uvicorn -from lnbits.settings import HOST, PORT +from lnbits.settings import FORWARDED_ALLOW_IPS, HOST, PORT @click.command( @@ -14,10 +12,20 @@ from lnbits.settings import HOST, PORT ) @click.option("--port", default=PORT, help="Port to listen on") @click.option("--host", default=HOST, help="Host to run LNBits on") +@click.option( + "--forwarded-allow-ips", default=FORWARDED_ALLOW_IPS, help="Allowed proxy servers" +) @click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile") @click.option("--ssl-certfile", default=None, help="Path to SSL certificate") @click.pass_context -def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str): +def main( + ctx, + port: int, + host: str, + forwarded_allow_ips: str, + ssl_keyfile: str, + ssl_certfile: str, +): """Launched with `poetry run lnbits` at root level""" # this beautiful beast parses all command line arguments and passes them to the uvicorn server d = dict() @@ -37,6 +45,7 @@ def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str): "lnbits.__main__:app", port=port, host=host, + forwarded_allow_ips=forwarded_allow_ips, ssl_keyfile=ssl_keyfile, ssl_certfile=ssl_certfile, **d diff --git a/lnbits/settings.py b/lnbits/settings.py index 3f4e31cc..17fce293 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -18,6 +18,8 @@ DEBUG = env.bool("DEBUG", default=False) HOST = env.str("HOST", default="127.0.0.1") PORT = env.int("PORT", default=5000) +FORWARDED_ALLOW_IPS = env.str("FORWARDED_ALLOW_IPS", default="127.0.0.1") + LNBITS_PATH = path.dirname(path.realpath(__file__)) LNBITS_DATA_FOLDER = env.str( "LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data") @@ -38,6 +40,9 @@ LNBITS_DISABLED_EXTENSIONS: List[str] = [ for x in env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str) ] +LNBITS_AD_SPACE_TITLE = env.str( + "LNBITS_AD_SPACE_TITLE", default="Optional Advert Space" +) LNBITS_AD_SPACE = [x.strip(" ") for x in env.list("LNBITS_AD_SPACE", default=[])] LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False) LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits") diff --git a/lnbits/static/images/lnbits-shop-dark.png b/lnbits/static/images/lnbits-shop-dark.png new file mode 100644 index 00000000..3dd677dc Binary files /dev/null and b/lnbits/static/images/lnbits-shop-dark.png differ diff --git a/lnbits/static/images/lnbits-shop-light.png b/lnbits/static/images/lnbits-shop-light.png new file mode 100644 index 00000000..96607cb4 Binary files /dev/null and b/lnbits/static/images/lnbits-shop-light.png differ diff --git a/lnbits/tasks.py b/lnbits/tasks.py index 94e43dcf..de3c69aa 100644 --- a/lnbits/tasks.py +++ b/lnbits/tasks.py @@ -124,7 +124,7 @@ async def check_pending_payments(): while True: async with db.connect() as conn: - logger.debug( + logger.info( f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days" ) start_time: float = time.time() @@ -140,15 +140,15 @@ async def check_pending_payments(): for payment in pending_payments: await payment.check_status(conn=conn) - logger.debug( + logger.info( f"Task: pending check finished for {len(pending_payments)} payments (took {time.time() - start_time:0.3f} s)" ) # we delete expired invoices once upon the first pending check if incoming: - logger.debug("Task: deleting all expired invoices") + logger.info("Task: deleting all expired invoices") start_time: float = time.time() await delete_expired_invoices(conn=conn) - logger.debug( + logger.info( f"Task: expired invoice deletion finished (took {time.time() - start_time:0.3f} s)" ) diff --git a/lnbits/templates/base.html b/lnbits/templates/base.html index 67241bb5..ef270371 100644 --- a/lnbits/templates/base.html +++ b/lnbits/templates/base.html @@ -199,6 +199,18 @@ > + + API DOCS + View LNbits Swagger API docs +