From 10a4caff7eb357c2ab5819ce6022467942908f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 25 Aug 2025 12:25:20 +0200 Subject: [PATCH 01/10] feat: add lud17 support (#60) --- crud.py | 9 +++-- models.py | 5 +++ static/js/index.js | 33 ++++++++--------- templates/withdraw/csv.html | 12 ------- templates/withdraw/display.html | 24 +++++++------ templates/withdraw/index.html | 53 +++++++++++---------------- views.py | 47 +++++++++++------------- views_api.py | 64 ++++++++------------------------- 8 files changed, 92 insertions(+), 155 deletions(-) delete mode 100644 templates/withdraw/csv.html diff --git a/crud.py b/crud.py index b63bd88..7d79ccd 100644 --- a/crud.py +++ b/crud.py @@ -4,7 +4,7 @@ import shortuuid from lnbits.db import Database from lnbits.helpers import urlsafe_short_hash -from .models import CreateWithdrawData, HashCheck, WithdrawLink +from .models import CreateWithdrawData, HashCheck, PaginatedWithdraws, WithdrawLink db = Database("ext_withdraw") @@ -66,7 +66,7 @@ async def get_withdraw_link_by_hash(unique_hash: str, num=0) -> WithdrawLink | N async def get_withdraw_links( wallet_ids: list[str], limit: int, offset: int -) -> tuple[list[WithdrawLink], int]: +) -> PaginatedWithdraws: q = ",".join([f"'{w}'" for w in wallet_ids]) query_str = f""" @@ -85,16 +85,15 @@ async def get_withdraw_links( query_params, WithdrawLink, ) - result = await db.execute( f""" SELECT COUNT(*) as total FROM withdraw.withdraw_link WHERE wallet IN ({q}) """ ) - total = result.mappings().first() + result2 = result.mappings().first() - return links, total.total + return PaginatedWithdraws(data=links, total=int(result2.total)) async def remove_unique_withdraw_link(link: WithdrawLink, unique_hash: str) -> None: diff --git a/models.py b/models.py index e709bd4..b476dce 100644 --- a/models.py +++ b/models.py @@ -46,3 +46,8 @@ class WithdrawLink(BaseModel): class HashCheck(BaseModel): hash: bool lnurl: bool + + +class PaginatedWithdraws(BaseModel): + data: list[WithdrawLink] + total: int diff --git a/static/js/index.js b/static/js/index.js index e6c6146..9a8a9c1 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,17 +1,6 @@ -const locationPath = [ - window.location.protocol, - '//', - window.location.host, - window.location.pathname -].join('') - const mapWithdrawLink = function (obj) { obj._data = _.clone(obj) - obj.min_fsat = new Intl.NumberFormat(LOCALE).format(obj.min_withdrawable) - obj.max_fsat = new Intl.NumberFormat(LOCALE).format(obj.max_withdrawable) obj.uses_left = obj.uses - obj.used - obj.print_url = [locationPath, 'print/', obj.id].join('') - obj.withdraw_url = [locationPath, obj.id].join('') obj._data.use_custom = Boolean(obj.custom_url) return obj } @@ -25,6 +14,7 @@ window.app = Vue.createApp({ return { checker: null, withdrawLinks: [], + lnurl: '', withdrawLinksTable: { columns: [ {name: 'title', align: 'left', label: 'Title', field: 'title'}, @@ -34,7 +24,7 @@ window.app = Vue.createApp({ label: 'Created At', field: 'created_at', sortable: true, - format: function (val, row) { + format: function (val) { return new Date(val).toLocaleString() } }, @@ -47,7 +37,7 @@ window.app = Vue.createApp({ { name: 'uses', align: 'right', - label: 'Created', + label: 'Uses', field: 'uses' }, { @@ -56,8 +46,15 @@ window.app = Vue.createApp({ label: 'Uses left', field: 'uses_left' }, - {name: 'min', align: 'right', label: 'Min (sat)', field: 'min_fsat'}, - {name: 'max', align: 'right', label: 'Max (sat)', field: 'max_fsat'} + { + name: 'max_withdrawable', + align: 'right', + label: 'Max (sat)', + field: 'max_withdrawable', + format: v => { + return new Intl.NumberFormat(LOCALE).format(v) + } + } ], pagination: { page: 1, @@ -141,11 +138,9 @@ window.app = Vue.createApp({ }, openQrCodeDialog(linkId) { const link = _.findWhere(this.withdrawLinks, {id: linkId}) - this.qrCodeDialog.data = _.clone(link) - this.qrCodeDialog.data.url = - window.location.protocol + '//' + window.location.host this.qrCodeDialog.show = true + this.activeUrl = `${window.location.origin}/withdraw/api/v1/lnurl/${link.unique_hash}` }, openUpdateDialog(linkId) { let link = _.findWhere(this.withdrawLinks, {id: linkId}) @@ -258,7 +253,7 @@ window.app = Vue.createApp({ '/withdraw/api/v1/links/' + linkId, _.findWhere(this.g.user.wallets, {id: link.wallet}).adminkey ) - .then(response => { + .then(() => { this.withdrawLinks = _.reject(this.withdrawLinks, function (obj) { return obj.id === linkId }) diff --git a/templates/withdraw/csv.html b/templates/withdraw/csv.html deleted file mode 100644 index d393061..0000000 --- a/templates/withdraw/csv.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "print.html" %} {% block page %} {% for page in link %} {% for threes -in page %} {% for one in threes %} {{one}}, {% endfor %} {% endfor %} {% endfor -%} {% endblock %} {% block scripts %} - -{% endblock %} diff --git a/templates/withdraw/display.html b/templates/withdraw/display.html index 34ae1ac..4d06e11 100644 --- a/templates/withdraw/display.html +++ b/templates/withdraw/display.html @@ -4,24 +4,26 @@
- {% if link.is_spent %} - Withdraw is spent. - {% endif %} - - + Withdraw is spent. + +
- Copy LNURL
@@ -52,7 +54,9 @@ mixins: [window.windowMixin], data() { return { - here: location.protocol + '//' + location.host, + spent: {{ 'true' if spent else 'false' }}, + url: `${window.location.origin}/withdraw/api/v1/lnurl/{{ unique_hash }}`, + lnurl: '', nfcTagWriting: false } } diff --git a/templates/withdraw/index.html b/templates/withdraw/index.html index 1014a2c..3b75e11 100644 --- a/templates/withdraw/index.html +++ b/templates/withdraw/index.html @@ -57,43 +57,29 @@ dense size="xs" icon="launch" - :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" - :href="props.row.withdraw_url" + :href="'/withdraw/' + props.row.id" target="_blank" > - shareable link - embeddable image Shareable link csv list CSV download view LNURL view LNURL @@ -139,7 +125,7 @@
- {{SITE_TITLE}} LNURL-withdraw extension + LNbits LNURL withdraw extension
@@ -413,9 +399,11 @@ - +

ID:
Unique: @@ -440,31 +428,32 @@ Copy LNURL - Copy sharable link - Write to NFC + Open sharable link + Print diff --git a/views.py b/views.py index 1a77d40..ba48a5d 100644 --- a/views.py +++ b/views.py @@ -1,7 +1,8 @@ +import io from http import HTTPStatus from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, StreamingResponse from lnbits.core.models import User from lnbits.decorators import check_user_exists from lnbits.helpers import template_renderer @@ -32,21 +33,12 @@ async def display(request: Request, link_id): status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." ) - try: - lnurl = create_lnurl(link, request) - except ValueError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=str(exc), - ) from exc - return withdraw_renderer().TemplateResponse( "withdraw/display.html", { "request": request, - "link": link.json(), - "lnurl": lnurl, - "unique": True, + "spent": link.is_spent, + "unique_hash": link.unique_hash, }, ) @@ -58,8 +50,6 @@ async def print_qr(request: Request, link_id): raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." ) - # response.status_code = HTTPStatus.NOT_FOUND - # return "Withdraw link does not exist." if link.uses == 0: @@ -83,7 +73,7 @@ async def print_qr(request: Request, link_id): status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc), ) from exc - links.append(str(lnurl)) + links.append(str(lnurl.bech32)) count = count + 1 page_link = list(chunks(links, 2)) linked = list(chunks(page_link, 5)) @@ -114,14 +104,12 @@ async def csv(request: Request, link_id): ) if link.uses == 0: - - return withdraw_renderer().TemplateResponse( - "withdraw/csv.html", - {"request": request, "link": link.json(), "unique": False}, + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Withdraw is spent." ) - links = [] - count = 0 + buffer = io.StringIO() + count = 0 for _ in link.usescsv.split(","): linkk = await get_withdraw_link(link_id, count) if not linkk: @@ -135,11 +123,16 @@ async def csv(request: Request, link_id): status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc), ) from exc - links.append(str(lnurl)) - count = count + 1 - page_link = list(chunks(links, 2)) - linked = list(chunks(page_link, 5)) + buffer.write(f"{lnurl.bech32!s}\n") + count += 1 - return withdraw_renderer().TemplateResponse( - "withdraw/csv.html", {"request": request, "link": linked, "unique": True} + # Move buffer cursor to the beginning + buffer.seek(0) + + return StreamingResponse( + buffer, + media_type="text/csv", + headers={ + "Content-Disposition": f"attachment; filename=withdraw-links-{link_id}.csv" + }, ) diff --git a/views_api.py b/views_api.py index 8c4063c..f93d8f1 100644 --- a/views_api.py +++ b/views_api.py @@ -1,9 +1,9 @@ import json from http import HTTPStatus -from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi import APIRouter, Depends, HTTPException, Query from lnbits.core.crud import get_user -from lnbits.core.models import WalletTypeInfo +from lnbits.core.models import SimpleStatus, WalletTypeInfo from lnbits.decorators import require_admin_key, require_invoice_key from .crud import ( @@ -14,51 +14,31 @@ from .crud import ( get_withdraw_links, update_withdraw_link, ) -from .helpers import create_lnurl -from .models import CreateWithdrawData, HashCheck +from .models import CreateWithdrawData, HashCheck, PaginatedWithdraws, WithdrawLink withdraw_ext_api = APIRouter(prefix="/api/v1") @withdraw_ext_api.get("/links", status_code=HTTPStatus.OK) async def api_links( - request: Request, key_info: WalletTypeInfo = Depends(require_invoice_key), all_wallets: bool = Query(False), offset: int = Query(0), limit: int = Query(0), -): +) -> PaginatedWithdraws: wallet_ids = [key_info.wallet.id] if all_wallets: user = await get_user(key_info.wallet.user) wallet_ids = user.wallet_ids if user else [] - links, total = await get_withdraw_links(wallet_ids, limit, offset) - - data = [] - for link in links: - try: - lnurl = create_lnurl(link, request) - except ValueError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=str(exc), - ) from exc - data.append({**link.dict(), **{"lnurl": lnurl}}) - - return { - "data": data, - "total": total, - } + return await get_withdraw_links(wallet_ids, limit, offset) @withdraw_ext_api.get("/links/{link_id}", status_code=HTTPStatus.OK) async def api_link_retrieve( - link_id: str, - request: Request, - key_info: WalletTypeInfo = Depends(require_invoice_key), -): + link_id: str, key_info: WalletTypeInfo = Depends(require_invoice_key) +) -> WithdrawLink: link = await get_withdraw_link(link_id, 0) if not link: @@ -70,24 +50,16 @@ async def api_link_retrieve( raise HTTPException( detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN ) - try: - lnurl = create_lnurl(link, request) - except ValueError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=str(exc), - ) from exc - return {**link.dict(), **{"lnurl": lnurl}} + return link @withdraw_ext_api.post("/links", status_code=HTTPStatus.CREATED) -@withdraw_ext_api.put("/links/{link_id}", status_code=HTTPStatus.OK) +@withdraw_ext_api.put("/links/{link_id}") async def api_link_create_or_update( - request: Request, data: CreateWithdrawData, link_id: str | None = None, key_info: WalletTypeInfo = Depends(require_admin_key), -): +) -> WithdrawLink: if data.uses > 250: raise HTTPException(detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST) @@ -160,21 +132,13 @@ async def api_link_create_or_update( else: link = await create_withdraw_link(wallet_id=key_info.wallet.id, data=data) - try: - lnurl = create_lnurl(link, request) - except ValueError as exc: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=str(exc), - ) from exc - - return {**link.dict(), **{"lnurl": lnurl}} + return link -@withdraw_ext_api.delete("/links/{link_id}", status_code=HTTPStatus.OK) +@withdraw_ext_api.delete("/links/{link_id}") async def api_link_delete( link_id: str, key_info: WalletTypeInfo = Depends(require_admin_key) -): +) -> SimpleStatus: link = await get_withdraw_link(link_id) if not link: @@ -188,7 +152,7 @@ async def api_link_delete( ) await delete_withdraw_link(link_id) - return {"success": True} + return SimpleStatus(success=True, message="Withdraw link deleted.") @withdraw_ext_api.get( From 8efacf2d4cf95605bef5c549c2baeaa2aeb8eceb Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Fri, 12 Sep 2025 14:26:18 +0100 Subject: [PATCH 02/10] fix: print qr code (#62) --- pyproject.toml | 3 +++ templates/withdraw/print_qr.html | 28 ++++++++++++------------- templates/withdraw/print_qr_custom.html | 12 ++++++----- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8f281dd..a091367 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,9 @@ authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/bitcoinswitch_extension" } dependencies = [ "lnbits>1" ] +[tool.poetry] +package-mode = false + [tool.uv] dev-dependencies = [ "black", diff --git a/templates/withdraw/print_qr.html b/templates/withdraw/print_qr.html index 1ccc1e9..3b73b13 100644 --- a/templates/withdraw/print_qr.html +++ b/templates/withdraw/print_qr.html @@ -4,23 +4,21 @@

{% for page in link %} - - {% for threes in page %} - - {% for one in threes %} - +
+ {% for row in page %} +
+ {% for one in row %} +
+ +
{% endfor %} -
+ {% endfor %} -
-
- -
-
+
{% endfor %} diff --git a/templates/withdraw/print_qr_custom.html b/templates/withdraw/print_qr_custom.html index 8e34097..c144a9f 100644 --- a/templates/withdraw/print_qr_custom.html +++ b/templates/withdraw/print_qr_custom.html @@ -11,7 +11,8 @@
@@ -61,9 +62,10 @@ .wrapper .lnurlw { display: block; position: absolute; - top: calc(7.3mm + 1rem); - left: calc(7.5mm + 1rem); + top: calc(3mm + 1rem); + left: calc(6mm + 1rem); transform: rotate(45deg); + width: 27mm; } @media print { @@ -83,8 +85,8 @@ .wrapper .lnurlw { display: block; position: absolute; - top: 7.3mm; - left: 7.5mm; + top: 3mm; + left: 6mm; transform: rotate(45deg); } } From d0689b7859a1ffdaafed6493b08d93575a6b61bb Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:00:40 +0100 Subject: [PATCH 03/10] fix: timing logic for time between withdraws (#63) --- crud.py | 2 +- views_lnurl.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crud.py b/crud.py index 7d79ccd..b914ae5 100644 --- a/crud.py +++ b/crud.py @@ -108,7 +108,7 @@ async def remove_unique_withdraw_link(link: WithdrawLink, unique_hash: str) -> N async def increment_withdraw_link(link: WithdrawLink) -> None: link.used = link.used + 1 - link.open_time = int(datetime.now().timestamp()) + link.wait_time + link.open_time = int(datetime.now().timestamp()) await update_withdraw_link(link) diff --git a/views_lnurl.py b/views_lnurl.py index d62b21d..d1bba22 100644 --- a/views_lnurl.py +++ b/views_lnurl.py @@ -99,9 +99,9 @@ async def api_lnurl_callback( now = int(datetime.now().timestamp()) - if now < link.open_time: + if now < link.open_time + link.wait_time: return LnurlErrorResponse( - reason=f"wait link open_time {link.open_time - now} seconds." + reason=f"Wait {link.open_time + link.wait_time - now} seconds." ) if not id_unique_hash and link.is_unique: From 720aa694c14e728b7556f258e8ea2067dc32e575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 6 Oct 2025 18:44:49 +0200 Subject: [PATCH 04/10] fix: revert withdraw to using bech32 `lnurl` field (#65) --- models.py | 10 ++++++++++ views_api.py | 41 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/models.py b/models.py index b476dce..ff8c657 100644 --- a/models.py +++ b/models.py @@ -37,6 +37,16 @@ class WithdrawLink(BaseModel): webhook_body: str = Query(None) custom_url: str = Query(None) created_at: datetime + lnurl: str | None = Field( + default=None, + no_database=True, + deprecated=True, + description=( + "Deprecated: Instead of using this bech32 encoded string, dynamically " + "generate your own static link (lud17/bech32) on the client side. " + "Example: lnurlw://${window.location.hostname}/lnurlw/${id}" + ), + ) @property def is_spent(self) -> bool: diff --git a/views_api.py b/views_api.py index f93d8f1..c80e744 100644 --- a/views_api.py +++ b/views_api.py @@ -1,7 +1,7 @@ import json from http import HTTPStatus -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, Request from lnbits.core.crud import get_user from lnbits.core.models import SimpleStatus, WalletTypeInfo from lnbits.decorators import require_admin_key, require_invoice_key @@ -14,6 +14,7 @@ from .crud import ( get_withdraw_links, update_withdraw_link, ) +from .helpers import create_lnurl from .models import CreateWithdrawData, HashCheck, PaginatedWithdraws, WithdrawLink withdraw_ext_api = APIRouter(prefix="/api/v1") @@ -21,6 +22,7 @@ withdraw_ext_api = APIRouter(prefix="/api/v1") @withdraw_ext_api.get("/links", status_code=HTTPStatus.OK) async def api_links( + request: Request, key_info: WalletTypeInfo = Depends(require_invoice_key), all_wallets: bool = Query(False), offset: int = Query(0), @@ -32,12 +34,26 @@ async def api_links( user = await get_user(key_info.wallet.user) wallet_ids = user.wallet_ids if user else [] - return await get_withdraw_links(wallet_ids, limit, offset) + links = await get_withdraw_links(wallet_ids, limit, offset) + + for linkk in links.data: + try: + lnurl = create_lnurl(linkk, request) + except ValueError as exc: + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=str(exc), + ) from exc + linkk.lnurl = str(lnurl.bech32) + + return links @withdraw_ext_api.get("/links/{link_id}", status_code=HTTPStatus.OK) async def api_link_retrieve( - link_id: str, key_info: WalletTypeInfo = Depends(require_invoice_key) + request: Request, + link_id: str, + key_info: WalletTypeInfo = Depends(require_invoice_key), ) -> WithdrawLink: link = await get_withdraw_link(link_id, 0) @@ -50,12 +66,22 @@ async def api_link_retrieve( raise HTTPException( detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN ) + + try: + lnurl = create_lnurl(link, request) + except ValueError as exc: + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=str(exc), + ) from exc + link.lnurl = str(lnurl.bech32) return link @withdraw_ext_api.post("/links", status_code=HTTPStatus.CREATED) @withdraw_ext_api.put("/links/{link_id}") async def api_link_create_or_update( + request: Request, data: CreateWithdrawData, link_id: str | None = None, key_info: WalletTypeInfo = Depends(require_admin_key), @@ -131,6 +157,15 @@ async def api_link_create_or_update( link = await update_withdraw_link(link) else: link = await create_withdraw_link(wallet_id=key_info.wallet.id, data=data) + try: + lnurl = create_lnurl(link, request) + except ValueError as exc: + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=str(exc), + ) from exc + + link.lnurl = str(lnurl.bech32) return link From eb7f7fda4791b4844ef86ac434e631a64cdd20ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 6 Oct 2025 18:47:56 +0200 Subject: [PATCH 05/10] chore: update to version 1.2.1 (#66) --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 2e0a5c7..292d2d9 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "name": "Withdraw Links", "short_description": "Make LNURL withdraw links", "tile": "/withdraw/static/image/lnurl-withdraw.png", - "version": "1.1.0", + "version": "1.2.1", "min_lnbits_version": "1.3.0", "contributors": [ { From 68ff753cfdf5983110989b5a2fd91c3691b1fa58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 15 Dec 2025 07:41:36 +0100 Subject: [PATCH 06/10] fix: format function for table column (#67) --- static/js/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 9a8a9c1..bcfa832 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -51,9 +51,7 @@ window.app = Vue.createApp({ align: 'right', label: 'Max (sat)', field: 'max_withdrawable', - format: v => { - return new Intl.NumberFormat(LOCALE).format(v) - } + format: LNbits.utils.formatSat } ], pagination: { From 8a20df70fe0c3b4abf176421f332eb0730595899 Mon Sep 17 00:00:00 2001 From: PatMulligan <43773168+PatMulligan@users.noreply.github.com> Date: Sat, 27 Dec 2025 09:45:57 +0100 Subject: [PATCH 07/10] FIX: generate LNURL server-side for unique voucher links (#68) Co-authored-by: Claude Opus 4.5 --- models.py | 5 +++++ static/js/index.js | 2 +- templates/withdraw/display.html | 2 +- views.py | 10 +++++++++- views_api.py | 3 +++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/models.py b/models.py index ff8c657..0b4b910 100644 --- a/models.py +++ b/models.py @@ -47,6 +47,11 @@ class WithdrawLink(BaseModel): "Example: lnurlw://${window.location.hostname}/lnurlw/${id}" ), ) + lnurl_url: str | None = Field( + default=None, + no_database=True, + description="The raw LNURL callback URL (use for QR code generation)", + ) @property def is_spent(self) -> bool: diff --git a/static/js/index.js b/static/js/index.js index bcfa832..a5f4dbf 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -138,7 +138,7 @@ window.app = Vue.createApp({ const link = _.findWhere(this.withdrawLinks, {id: linkId}) this.qrCodeDialog.data = _.clone(link) this.qrCodeDialog.show = true - this.activeUrl = `${window.location.origin}/withdraw/api/v1/lnurl/${link.unique_hash}` + this.activeUrl = link.lnurl_url }, openUpdateDialog(linkId) { let link = _.findWhere(this.withdrawLinks, {id: linkId}) diff --git a/templates/withdraw/display.html b/templates/withdraw/display.html index 4d06e11..f9fbd53 100644 --- a/templates/withdraw/display.html +++ b/templates/withdraw/display.html @@ -55,7 +55,7 @@ data() { return { spent: {{ 'true' if spent else 'false' }}, - url: `${window.location.origin}/withdraw/api/v1/lnurl/{{ unique_hash }}`, + url: '{{ lnurl_url }}', lnurl: '', nfcTagWriting: false } diff --git a/views.py b/views.py index ba48a5d..4fc06e3 100644 --- a/views.py +++ b/views.py @@ -33,12 +33,20 @@ async def display(request: Request, link_id): status_code=HTTPStatus.NOT_FOUND, detail="Withdraw link does not exist." ) + try: + lnurl = create_lnurl(link, request) + except ValueError as exc: + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=str(exc), + ) from exc + return withdraw_renderer().TemplateResponse( "withdraw/display.html", { "request": request, "spent": link.is_spent, - "unique_hash": link.unique_hash, + "lnurl_url": str(lnurl.url), }, ) diff --git a/views_api.py b/views_api.py index c80e744..514372d 100644 --- a/views_api.py +++ b/views_api.py @@ -45,6 +45,7 @@ async def api_links( detail=str(exc), ) from exc linkk.lnurl = str(lnurl.bech32) + linkk.lnurl_url = str(lnurl.url) return links @@ -75,6 +76,7 @@ async def api_link_retrieve( detail=str(exc), ) from exc link.lnurl = str(lnurl.bech32) + link.lnurl_url = str(lnurl.url) return link @@ -166,6 +168,7 @@ async def api_link_create_or_update( ) from exc link.lnurl = str(lnurl.bech32) + link.lnurl_url = str(lnurl.url) return link From ab96594f70c12f2430e3308b887a8f48c7472bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Sat, 27 Dec 2025 09:48:17 +0100 Subject: [PATCH 08/10] chore: update to 1.2.2 --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 292d2d9..de2cba5 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "name": "Withdraw Links", "short_description": "Make LNURL withdraw links", "tile": "/withdraw/static/image/lnurl-withdraw.png", - "version": "1.2.1", + "version": "1.2.2", "min_lnbits_version": "1.3.0", "contributors": [ { From 74852e34941f3d44f47934dcd8cde847ecef7b65 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 17 Mar 2026 21:41:17 +0000 Subject: [PATCH 09/10] feat: add disable option for LNURLw (#70) --- migrations.py | 6 +++++ models.py | 2 ++ static/js/index.js | 12 ++++++---- templates/withdraw/display.html | 9 ++++++- templates/withdraw/index.html | 42 +++++++++++++++++++++++++++++++++ views.py | 2 +- views_lnurl.py | 10 +++++++- 7 files changed, 76 insertions(+), 7 deletions(-) diff --git a/migrations.py b/migrations.py index 754a57f..e27af8a 100644 --- a/migrations.py +++ b/migrations.py @@ -139,3 +139,9 @@ async def m007_add_created_at_timestamp(db): "ALTER TABLE withdraw.withdraw_link " f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}" ) + + +async def m008_add_enabled_column(db): + await db.execute( + "ALTER TABLE withdraw.withdraw_link ADD COLUMN enabled BOOLEAN DEFAULT true;" + ) diff --git a/models.py b/models.py index 0b4b910..e888cdf 100644 --- a/models.py +++ b/models.py @@ -15,6 +15,7 @@ class CreateWithdrawData(BaseModel): webhook_headers: str = Query(None) webhook_body: str = Query(None) custom_url: str = Query(None) + enabled: bool = Query(True) class WithdrawLink(BaseModel): @@ -37,6 +38,7 @@ class WithdrawLink(BaseModel): webhook_body: str = Query(None) custom_url: str = Query(None) created_at: datetime + enabled: bool = Query(True) lnurl: str | None = Field( default=None, no_database=True, diff --git a/static/js/index.js b/static/js/index.js index a5f4dbf..0b42b40 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -68,7 +68,8 @@ window.app = Vue.createApp({ data: { is_unique: false, use_custom: false, - has_webhook: false + has_webhook: false, + enabled: true } }, simpleformDialog: { @@ -78,7 +79,8 @@ window.app = Vue.createApp({ use_custom: false, title: 'Vouchers', min_withdrawable: 0, - wait_time: 1 + wait_time: 1, + enabled: true } }, qrCodeDialog: { @@ -125,13 +127,15 @@ window.app = Vue.createApp({ this.formDialog.data = { is_unique: false, use_custom: false, - has_webhook: false + has_webhook: false, + enabled: true } }, simplecloseFormDialog() { this.simpleformDialog.data = { is_unique: false, - use_custom: false + use_custom: false, + enabled: true } }, openQrCodeDialog(linkId) { diff --git a/templates/withdraw/display.html b/templates/withdraw/display.html index f9fbd53..812c95f 100644 --- a/templates/withdraw/display.html +++ b/templates/withdraw/display.html @@ -7,6 +7,12 @@ Withdraw is spent. + Withdraw is spent. + Withdraw is disabled.