From 7d26d8980b70e7a307dccadbe032e78ac2f03064 Mon Sep 17 00:00:00 2001 From: DoktorShift Date: Tue, 13 Jan 2026 17:05:16 +0100 Subject: [PATCH 01/22] Issue14, 25, 26 --- crud.py | 1 + migrations.py | 9 ++++ models.py | 3 ++ static/js/index.js | 70 +++++++++++++++++++++++++------- tasks.py | 7 +++- templates/nwcprovider/index.html | 58 ++++++++++++++++++++++++++ views_api.py | 55 +++++++++++++++++++++++-- 7 files changed, 184 insertions(+), 19 deletions(-) diff --git a/crud.py b/crud.py index 872d0d6..ab4d3a6 100644 --- a/crud.py +++ b/crud.py @@ -53,6 +53,7 @@ async def create_nwc(data: CreateNWCKey) -> NWCKey: permissions=" ".join(data.permissions), created_at=int(time.time()), last_used=int(time.time()), + lud16=data.lud16, ) await db.insert("nwcprovider.keys", nwckey_entry) if data.budgets: diff --git a/migrations.py b/migrations.py index 1a3a9b7..655a7f8 100644 --- a/migrations.py +++ b/migrations.py @@ -119,3 +119,12 @@ async def m006_default_config3(db): """, {"value": "0"}, ) + + +async def m007_add_lud16(db): + """ + Add lud16 column to keys table for lightning address support + """ + await db.execute( + "ALTER TABLE nwcprovider.keys ADD COLUMN lud16 TEXT" + ) diff --git a/models.py b/models.py index a84c46f..fc32685 100644 --- a/models.py +++ b/models.py @@ -15,6 +15,7 @@ class NWCKey(BaseModel): permissions: str created_at: int last_used: int + lud16: str | None = None def get_permissions(self) -> list[str]: try: @@ -67,6 +68,7 @@ class CreateNWCKey(BaseModel): expires_at: int permissions: list[str] budgets: list[NWCNewBudget] | None = None + lud16: str | None = None class DeleteNWC(BaseModel): @@ -102,6 +104,7 @@ class NWCRegistrationRequest(BaseModel): description: str expires_at: int budgets: list[NWCNewBudget] + lud16: str | None = None class NWCGetResponse(BaseModel): diff --git a/static/js/index.js b/static/js/index.js index d593469..274de7e 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -36,7 +36,7 @@ window.app = Vue.createApp({ } ], pagination: { - rowsPerPage: 10 + rowsPerPage: 0 } }, connectDialog: { @@ -58,7 +58,9 @@ window.app = Vue.createApp({ connectionInfoDialog: { show: false, data: {} - } + }, + lud16OptionsAll: [], + lud16Loading: false } }, @@ -119,7 +121,8 @@ window.app = Vue.createApp({ expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7, neverExpires: true, permissions: [], - budgets: [] + budgets: [], + lud16: '' } for (const permission of this.nodePermissions) { this.connectDialog.data.permissions.push({ @@ -128,6 +131,36 @@ window.app = Vue.createApp({ value: permission.value }) } + this.lud16OptionsAll = [] + this.loadLightningAddresses() + }, + async loadLightningAddresses() { + const wallet = this.getWallet() + if (!wallet) { + this.lud16OptionsAll = [] + return + } + this.lud16Loading = true + try { + const response = await LNbits.api.request( + 'GET', + '/nwcprovider/api/v1/lnaddresses', + wallet.adminkey + ) + if (response.data && response.data.length > 0) { + this.lud16OptionsAll = response.data.map(addr => ({ + label: addr.description || addr.username, + value: addr.address + })) + } else { + this.lud16OptionsAll = [] + } + } catch (error) { + console.warn('Could not load lightning addresses:', error) + this.lud16OptionsAll = [] + } finally { + this.lud16Loading = false + } }, deleteBudget(index) { this.connectDialog.data.budgets.splice(index, 1) @@ -295,18 +328,23 @@ window.app = Vue.createApp({ closePairingDialog() { this.pairingDialog.show = false }, - async showPairingDialog(secret) { - let response = await LNbits.api.request( - 'GET', - '/nwcprovider/api/v1/pairing/{SECRET}' - ) - response = response.data - response = response.replace('{SECRET}', secret) - this.pairingDialog.data.pairingUrl = response + getLud16Value() { + const lud16 = this.connectDialog.data.lud16 + return lud16 && lud16.trim() ? lud16.trim() : null + }, + async showPairingDialog(secret, lud16) { + let url = `/nwcprovider/api/v1/pairing/${secret}` + if (lud16) { + url += `?lud16=${encodeURIComponent(lud16)}` + } + let response = await LNbits.api.request('GET', url) + this.pairingDialog.data.pairingUrl = response.data this.pairingDialog.show = true }, async confirmConnectDialog() { const keyPair = await this.generateKeyPair() + // Save lud16 before dialog closes (closeConnectDialog resets it) + const lud16 = this.getLud16Value() // timestamp let expires_at = 0 if (!this.connectDialog.data.neverExpires) { @@ -317,7 +355,8 @@ window.app = Vue.createApp({ permissions: [], description: this.connectDialog.data.description, expires_at: expires_at, - budgets: [] + budgets: [], + lud16: lud16 } for (const permission of this.connectDialog.data.permissions) { if (permission.value) data.permissions.push(permission.key) @@ -366,7 +405,7 @@ window.app = Vue.createApp({ LNbits.utils.notifyApiError('Error creating nwc pairing') return } - this.showPairingDialog(keyPair.privKey) + this.showPairingDialog(keyPair.privKey, lud16) } catch (error) { LNbits.utils.notifyApiError(error) } @@ -375,7 +414,10 @@ window.app = Vue.createApp({ }, created: function () { - this.loadNwcs() + // Auto-select first wallet to show connections by default + if (this.g.user.wallets && this.g.user.wallets.length > 0) { + this.selectedWallet = this.g.user.wallets[0].id + } }, watch: { selectedWallet(newValue, oldValue) { diff --git a/tasks.py b/tasks.py index a19a0d8..73d5647 100644 --- a/tasks.py +++ b/tasks.py @@ -344,8 +344,9 @@ async def _on_lookup_invoice( res: dict = { "type": "outgoing" if payment.is_out else "incoming", "invoice": payment.bolt11, + # Priority: LNURL comment > memo > invoice description "description": ( - invoice_data.description if invoice_data.description else payment.memo + payment.extra.get("comment") or payment.memo or invoice_data.description ), "preimage": preimage if is_settled or payment.is_in else None, "payment_hash": payment.payment_hash, @@ -413,11 +414,13 @@ async def _on_list_transactions( invoice_data = bolt11_decode(p.bolt11) is_settled = not p.pending timestamp = int(p.time.timestamp()) or invoice_data.date + # Priority: LNURL comment > memo > invoice description + description = p.extra.get("comment") or p.memo or invoice_data.description transactions.append( { "type": "outgoing" if p.is_out else "incoming", "invoice": p.bolt11, - "description": invoice_data.description, + "description": description, "description_hash": invoice_data.description_hash, "preimage": p.preimage if is_settled or p.is_in else None, "payment_hash": p.payment_hash, diff --git a/templates/nwcprovider/index.html b/templates/nwcprovider/index.html index 0034330..96f6e66 100644 --- a/templates/nwcprovider/index.html +++ b/templates/nwcprovider/index.html @@ -346,6 +346,64 @@

Add connection

dense label="Description" > + + + +
str: +async def api_get_pairing_url( + req: Request, secret: str, lud16: str | None = None +) -> str: # hardening # assert_sane_string(secret) + if lud16: + assert_sane_string(lud16) # ## # pprivkey: str | None = await get_config_nwc("provider_key") @@ -132,9 +138,10 @@ async def api_get_pairing_url(req: Request, secret: str) -> str: ppubkey = ppk.hex() url = "nostr+walletconnect://" url += ppubkey - url += "?relay=" + relay + url += "?relay=" + quote(relay, safe="") url += "&secret=" + secret - # lud16=? + if lud16: + url += "&lud16=" + quote(lud16, safe="") return url @@ -163,6 +170,7 @@ async def api_register_nwc( expires_at=data.expires_at, permissions=data.permissions, budgets=data.budgets, + lud16=data.lud16, ) ) budgets = await get_budgets_nwc(GetBudgetsNWC(pubkey=pubkey)) @@ -219,3 +227,44 @@ async def api_set_config_nwc(req: Request): for key, value in data.items(): await set_config_nwc(key, value) return await api_get_all_config_nwc() + + +# Get available lightning addresses from lnurlp extension +@nwcprovider_api_router.get("/api/v1/lnaddresses") +async def api_get_lightning_addresses( + req: Request, + wallet: WalletTypeInfo = Depends(require_admin_key), +) -> list[dict]: + """ + Fetch available lightning addresses from lnurlp extension for this wallet. + Returns list of {address, username, description} for dropdown selection. + """ + wallet_id = wallet.wallet.id + + # hardening # + assert_valid_wallet_id(wallet_id) + # ## # + + try: + # Import lnurlp crud - may not be installed + from lnbits.extensions.lnurlp.crud import get_pay_links + + pay_links = await get_pay_links([wallet_id]) + domain = req.url.netloc + + addresses = [] + for link in pay_links: + if link.username: + addresses.append({ + "address": f"{link.username}@{domain}", + "username": link.username, + "description": link.description or "", + }) + + return addresses + except ImportError: + logger.warning("lnurlp extension not available for lightning address lookup") + return [] + except Exception as e: + logger.error(f"Error fetching lightning addresses: {e}") + return [] From d907cc2a91cb06ccd1e78898dd5ea0616e6206ef Mon Sep 17 00:00:00 2001 From: DoktorShift Date: Wed, 14 Jan 2026 15:04:52 +0100 Subject: [PATCH 02/22] Avoid using literals, go with v-text, Get rid of delimiters on .js file --- static/js/admin.js | 1 - static/js/index.js | 1 - templates/nwcprovider/index.html | 36 ++++++++++---------------------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/static/js/admin.js b/static/js/admin.js index e9a405c..56ae919 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,7 +1,6 @@ window.app = Vue.createApp({ el: '#vue', mixins: [windowMixin], - delimiters: ['${', '}'], data: function () { return { config: {}, diff --git a/static/js/index.js b/static/js/index.js index 274de7e..705886a 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,7 +1,6 @@ window.app = Vue.createApp({ el: '#vue', mixins: [windowMixin], - delimiters: ['${', '}'], data: function () { return { selectedWallet: null, diff --git a/templates/nwcprovider/index.html b/templates/nwcprovider/index.html index 96f6e66..8e4ffa4 100644 --- a/templates/nwcprovider/index.html +++ b/templates/nwcprovider/index.html @@ -55,16 +55,14 @@
Connected Apps
> - - ${ col.label } - +