diff --git a/.gitignore b/.gitignore index acd0773..f92a874 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ data .vscode dump .venv +.claude 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..0afa062 100644 --- a/migrations.py +++ b/migrations.py @@ -119,3 +119,10 @@ 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 fa52baa..93f7fb2 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): @@ -109,6 +112,13 @@ class NWCGetResponse(BaseModel): budgets: list[NWCBudget] +class NWCGetAllResponse(BaseModel): + data: NWCKey + budgets: list[NWCBudget] + wallet_id: str + wallet_name: str + + class NWCConfig(BaseModel): key: str value: str diff --git a/nwcp.py b/nwcp.py index efa5279..6a7a385 100644 --- a/nwcp.py +++ b/nwcp.py @@ -103,6 +103,12 @@ def __init__( # List of supported methods self.supported_methods: list[str] = [] + # List of supported notification types + self.supported_notifications: list[str] = [ + "payment_received", + "payment_sent", + ] + # Keep track of the number of subscriptions (used for unique subid) self.subscriptions_count: int = 0 @@ -308,11 +314,14 @@ async def _send_info_event(self): """ Build and publish the NWC service info event (kind 13194). """ + tags = [["p", self.public_key_hex]] + if self.supported_notifications: + tags.append(["notifications", " ".join(self.supported_notifications)]) event = { "kind": 13194, "content": " ".join(self.supported_methods), "created_at": int(time.time()), - "tags": [["p", self.public_key_hex]], + "tags": tags, } self._sign_event(event) await self._send(["EVENT", event]) @@ -415,6 +424,32 @@ def _extract_expiration_from_tags(self, tags: list) -> int: pass return expiration + async def send_notification( + self, client_pubkey: str, notification_type: str, notification: dict + ): + """ + Send a NIP-47 notification event (kind 23196) to a client. + + Args: + client_pubkey: The client's public key hex. + notification_type: The notification type (e.g. "payment_received"). + notification: The notification payload dict. + """ + content = self._json_dumps( + { + "notification_type": notification_type, + "notification": notification, + } + ) + event: dict = { + "kind": 23196, + "created_at": int(time.time()), + "tags": [["p", client_pubkey]], + "content": self.private_key.encrypt_message(content, client_pubkey), + } + self._sign_event(event) + await self._send(["EVENT", event]) + async def _on_event_message(self, msg): if not self.sub: return diff --git a/permission.py b/permission.py index 9caf17b..60b6be6 100644 --- a/permission.py +++ b/permission.py @@ -30,4 +30,9 @@ "default": True, }, "info": {"name": "Read account info", "methods": ["get_info"], "default": True}, + "notifications": { + "name": "Receive payment notifications", + "methods": [], + "default": True, + }, } diff --git a/pyproject.toml b/pyproject.toml index dc659bf..abdcfa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,9 @@ package-mode = false [tool.mypy] plugins = ["pydantic.mypy"] +exclude = [ + "tests/standalone/", +] [[tool.mypy.overrides]] module = [ diff --git a/static/image/Bildschirmfoto 2026-02-03 um 17.25.24.png b/static/image/Bildschirmfoto 2026-02-03 um 17.25.24.png new file mode 100644 index 0000000..fd79541 Binary files /dev/null and b/static/image/Bildschirmfoto 2026-02-03 um 17.25.24.png differ diff --git a/static/image/NWC_Banner_Extension.png b/static/image/NWC_Banner_Extension.png new file mode 100644 index 0000000..824036a Binary files /dev/null and b/static/image/NWC_Banner_Extension.png differ 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 d593469..58eac8c 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,20 +1,32 @@ window.app = Vue.createApp({ el: '#vue', mixins: [windowMixin], - delimiters: ['${', '}'], data: function () { return { - selectedWallet: null, + selectedWallet: 'all', + dialogWallet: null, nodePermissions: [], nwcEntries: [], nwcsTable: { columns: [ + { + name: 'wallet_name', + align: 'left', + label: 'Wallet', + field: 'wallet_name' + }, { name: 'description', align: 'left', label: 'Description', field: 'description' }, + { + name: 'lud16', + align: 'left', + label: 'Lightning Address', + field: 'lud16' + }, {name: 'status', align: 'left', label: 'Status', field: 'status'}, { name: 'last_used', @@ -36,7 +48,7 @@ window.app = Vue.createApp({ } ], pagination: { - rowsPerPage: 10 + rowsPerPage: 0 } }, connectDialog: { @@ -58,22 +70,58 @@ window.app = Vue.createApp({ connectionInfoDialog: { show: false, data: {} - } + }, + lud16OptionsAll: [], + lud16Loading: false } }, + computed: { + walletOptions() { + // Count connections per wallet + const counts = {} + for (const entry of this.nwcEntries) { + if (entry.wallet_id) { + counts[entry.wallet_id] = (counts[entry.wallet_id] || 0) + 1 + } + } + + const options = [{label: 'All Wallets', value: 'all', count: null}] + for (const wallet of this.g.user.wallets) { + options.push({ + label: wallet.name, + value: wallet.id, + count: counts[wallet.id] || 0 + }) + } + return options + }, + visibleColumns() { + if (this.selectedWallet === 'all') { + return this.nwcsTable.columns.map(c => c.name) + } + return this.nwcsTable.columns + .filter(c => c.name !== 'wallet_name') + .map(c => c.name) + } + }, methods: { showConnectDialog() { - const wallet = this.getWallet() - if (!wallet) { - Quasar.Notify.create({ - type: 'negative', - message: 'Please select a wallet first' - }) - return + // When "All Wallets" is selected, default dialog wallet to first wallet + if (this.selectedWallet === 'all') { + if (this.g.user.wallets && this.g.user.wallets.length > 0) { + this.dialogWallet = this.g.user.wallets[0].id + } else { + Quasar.Notify.create({ + type: 'negative', + message: 'No wallets found' + }) + return + } } else { - this.connectDialog.show = true + this.dialogWallet = this.selectedWallet } + this.connectDialog.show = true }, openConnectionInfoDialog(data) { this.connectionInfoDialog.data = data @@ -89,6 +137,10 @@ window.app = Vue.createApp({ go(url) { window.open(url, '_blank') }, + switchToWallet(walletId) { + // Switch to the selected wallet in the dropdown + this.selectedWallet = walletId + }, async copyPairingUrl() { const url = this.pairingDialog.data.pairingUrl if (url) { @@ -119,7 +171,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 +181,44 @@ window.app = Vue.createApp({ value: permission.value }) } + this.lud16OptionsAll = [] + this.loadLightningAddresses() + }, + getDialogWallet() { + for (let i = 0; i < this.g.user.wallets.length; i++) { + if (this.g.user.wallets[i].id === this.dialogWallet) { + return this.g.user.wallets[i] + } + } + return null + }, + async loadLightningAddresses() { + const wallet = this.getDialogWallet() + 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) @@ -168,25 +259,42 @@ window.app = Vue.createApp({ } return out }, - deleteNWC: async function (pubkey) { + deleteNWC: async function (row) { Quasar.Dialog.create({ title: 'Confirm Deletion', - message: 'Are you sure you want to delete this connection?', + message: 'This will disconnect the app. Are you sure?', cancel: true, persistent: true }) .onOk(async () => { try { - const wallet = this.getWallet() + // When in "All Wallets" view, use the wallet_id from the row + let adminkey + if (this.selectedWallet === 'all') { + const wallet = this.g.user.wallets.find( + w => w.id === row.wallet_id + ) + if (!wallet) { + Quasar.Notify.create({ + type: 'negative', + message: 'Could not find wallet for this connection' + }) + return + } + adminkey = wallet.adminkey + } else { + const wallet = this.getWallet() + adminkey = wallet.adminkey + } const response = await LNbits.api.request( 'DELETE', - `/nwcprovider/api/v1/nwc/${pubkey}`, - wallet.adminkey + `/nwcprovider/api/v1/nwc/${row.pubkey}`, + adminkey ) this.loadNwcs() Quasar.Notify.create({ type: 'positive', - message: 'Deleted successfully' + message: 'Connection removed' }) } catch (error) { LNbits.utils.notifyApiError(error) @@ -197,17 +305,32 @@ window.app = Vue.createApp({ }) }, loadNwcs: async function () { - const wallet = this.getWallet() - if (!wallet) { - this.nwcs = [] - return + // Get any wallet's adminkey for API calls (needed for "all" view) + let adminkey + if (this.selectedWallet === 'all') { + if (this.g.user.wallets && this.g.user.wallets.length > 0) { + adminkey = this.g.user.wallets[0].adminkey + } else { + this.nwcs = [] + this.nwcEntries = [] + return + } + } else { + const wallet = this.getWallet() + if (!wallet) { + this.nwcs = [] + this.nwcEntries = [] + return + } + adminkey = wallet.adminkey } + try { - const response = await LNbits.api.request( - 'GET', - '/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true', - wallet.adminkey - ) + const endpoint = + this.selectedWallet === 'all' + ? '/nwcprovider/api/v1/nwc/all?include_expired=true&calculate_spent_budget=true' + : '/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true' + const response = await LNbits.api.request('GET', endpoint, adminkey) this.nwcs = response.data } catch (error) { this.nwcs = [] @@ -216,7 +339,7 @@ window.app = Vue.createApp({ const response = await LNbits.api.request( 'GET', '/nwcprovider/api/v1/permissions', - wallet.adminkey + adminkey ) const permissions = [] for (const [key, value] of Object.entries(response.data)) { @@ -228,7 +351,7 @@ window.app = Vue.createApp({ } this.nodePermissions = permissions } catch (error) { - Lnbits.utils.notifyApiError(error) + LNbits.utils.notifyApiError(error) } this.loadConnectDialogData() const newTableEntries = [] @@ -250,13 +373,16 @@ window.app = Vue.createApp({ ) const nwcTableEntry = { description: nwc.data.description, + lud16: nwc.data.lud16 || '-', created_at: t, expires_at: e, last_used: l, pubkey: nwc.data.pubkey, permissions: nwc.data.permissions, budgets: [], - status: 'Active' + status: 'Active', + wallet_id: nwc.wallet_id || null, + wallet_name: nwc.wallet_name || '' } if ( nwc.data.expires_at > 0 && @@ -290,23 +416,34 @@ window.app = Vue.createApp({ } newTableEntries.push(nwcTableEntry) } + // Sort by wallet name when showing all wallets + if (this.selectedWallet === 'all') { + newTableEntries.sort((a, b) => + a.wallet_name.localeCompare(b.wallet_name) + ) + } this.nwcEntries = newTableEntries }, 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 +454,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) @@ -348,7 +486,15 @@ window.app = Vue.createApp({ created_at: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000 }) } - const wallet = this.getWallet() + // Use dialogWallet for creating connections (handles "All Wallets" case) + const wallet = this.getDialogWallet() + if (!wallet) { + Quasar.Notify.create({ + type: 'negative', + message: 'Select a wallet first' + }) + return + } try { const response = await LNbits.api.request( @@ -366,7 +512,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,11 +521,20 @@ window.app = Vue.createApp({ }, created: function () { + // Default to "All Wallets" to show all connections + this.selectedWallet = 'all' + // Load connections on initial page load this.loadNwcs() }, watch: { selectedWallet(newValue, oldValue) { this.loadNwcs() + }, + dialogWallet(newValue, oldValue) { + // Reload lightning addresses when dialog wallet changes + if (newValue && this.connectDialog.show) { + this.loadLightningAddresses() + } } } }) diff --git a/tasks.py b/tasks.py index 412f2b5..62e2cdd 100644 --- a/tasks.py +++ b/tasks.py @@ -14,12 +14,13 @@ from lnbits.db import Filters from lnbits.exceptions import PaymentError from lnbits.settings import settings +from lnbits.tasks import register_invoice_listener from lnbits.wallets.base import PaymentStatus from loguru import logger -from .crud import get_config_nwc, get_nwc, tracked_spend_nwc +from .crud import get_config_nwc, get_nwc, get_wallet_nwcs, tracked_spend_nwc from .execution_queue import execution_queue -from .models import GetNWC, NWCKey, TrackedSpendNWC +from .models import GetNWC, GetWalletNWC, NWCKey, TrackedSpendNWC from .nwcp import NWCServiceProvider from .paranoia import ( assert_boolean, @@ -59,6 +60,84 @@ async def _check(nwc: NWCKey | None, method: str) -> dict | None: return None +def _build_transaction_data(payment: Payment) -> dict: + """ + Build the NIP-47 transaction data dict from a Payment object. + Used for lookup_invoice, list_transactions, and notification payloads. + """ + invoice_data = bolt11_decode(payment.bolt11) + is_settled = not payment.pending + timestamp = int(payment.time.timestamp()) or int(invoice_data.date) + expiry = int(payment.expiry.timestamp()) if payment.expiry else timestamp + 3600 + preimage = ( + payment.preimage + or "0000000000000000000000000000000000000000000000000000000000000000" + ) + # Derive NIP-47 state from Payment status + if payment.success: + state = "settled" + elif payment.failed: + state = "failed" + elif payment.is_expired: + state = "expired" + else: + state = "pending" + res: dict = { + "type": "outgoing" if payment.is_out else "incoming", + "state": state, + "invoice": payment.bolt11, + # Fallback chain so a human-readable description reaches the NWC client. + "description": ( + (payment.extra or {}).get("comment") + or invoice_data.description + or payment.memo + ), + "preimage": preimage if is_settled or payment.is_in else None, + "payment_hash": payment.payment_hash, + "amount": abs(payment.msat), + "fees_paid": abs(payment.fee), + "created_at": timestamp, + "expires_at": expiry, + "settled_at": timestamp if is_settled else None, + "metadata": {}, + } + if invoice_data.description_hash: + res["description_hash"] = invoice_data.description_hash + return res + + +async def _send_notification_to_wallet( + sp: NWCServiceProvider, + wallet_id: str, + notification_type: str, + notification: dict, + exclude_pubkey: str | None = None, +): + """ + Send a notification to all NWC keys on a wallet that have the + 'notifications' permission. Best-effort: errors are logged, never raised. + """ + try: + nwc_keys = await get_wallet_nwcs(GetWalletNWC(wallet=wallet_id)) + for nwc_key in nwc_keys: + if exclude_pubkey and nwc_key.pubkey == exclude_pubkey: + continue + permissions = nwc_key.get_permissions() + if "notifications" not in permissions: + continue + try: + await sp.send_notification( + nwc_key.pubkey, notification_type, notification + ) + except Exception as e: + logger.debug( + f"Failed to send {notification_type} notification " + f"to {nwc_key.pubkey[:8]}...: {e}" + ) + except Exception as e: + logger.debug(f"Failed to send notifications for wallet {wallet_id}: {e}") + + async def _process_invoice( wallet_id: str, pubkey: str, @@ -112,7 +191,7 @@ async def execute_payment() -> str: payment_status: PaymentStatus | None = None while wait_for_preimage: payment_status = await check_transaction_status(wallet_id, payment_hash) - if payment_status.success: + if payment_status and payment_status.success: break if payment_status.failed: return { @@ -169,13 +248,37 @@ async def _on_pay_invoice( if error: return [(None, error, [])] preimage = res.get("preimage") + payment_hash = res.get("payment_hash") out = { "preimage": preimage, } - # await log_nwc(pubkey, payload) + await _notify_payment_sent(sp, nwc.wallet, pubkey, payment_hash) return [(out, None, [])] +async def _notify_payment_sent( + sp: NWCServiceProvider, + wallet_id: str, + exclude_pubkey: str, + payment_hash: str | None, +) -> None: + """Send payment_sent notification to other keys on this wallet.""" + try: + if payment_hash: + payment = await get_wallet_payment(wallet_id, payment_hash) + if payment: + notification = _build_transaction_data(payment) + await _send_notification_to_wallet( + sp, + wallet_id, + "payment_sent", + notification, + exclude_pubkey=exclude_pubkey, + ) + except Exception as e: + logger.debug(f"Failed to send payment_sent notification: {e}") + + async def _on_multi_pay_invoice( sp: NWCServiceProvider, pubkey: str, payload: dict ) -> list[tuple[dict | None, dict | None, list]]: @@ -221,14 +324,16 @@ async def _on_multi_pay_invoice( if error: results.append((None, error, [])) else: + payment_hash = res.get("payment_hash") r = ( { "preimage": res.get("preimage"), }, None, - [["d", invoice_id if invoice_id else res.get("payment_hash")]], + [["d", invoice_id if invoice_id else payment_hash]], ) results.append(r) + await _notify_payment_sent(sp, nwc.wallet, pubkey, payment_hash) except Exception as e: results.append((None, {"code": "INTERNAL", "message": str(e)}, [])) # await log_nwc(pubkey, payload) @@ -341,31 +446,7 @@ async def _on_lookup_invoice( payment = await get_wallet_payment(nwc.wallet, payment_hash) if not payment: raise Exception("Payment not found") - invoice_data = bolt11_decode(payment.bolt11) - is_settled = not payment.pending - timestamp = int(payment.time.timestamp()) or int(invoice_data.date) - expiry = int(payment.expiry.timestamp()) if payment.expiry else timestamp + 3600 - preimage = ( - payment.preimage - or "0000000000000000000000000000000000000000000000000000000000000000" - ) - res: dict = { - "type": "outgoing" if payment.is_out else "incoming", - "invoice": payment.bolt11, - "description": ( - invoice_data.description if invoice_data.description else payment.memo - ), - "preimage": preimage if is_settled or payment.is_in else None, - "payment_hash": payment.payment_hash, - "amount": abs(payment.msat), - "fees_paid": abs(payment.fee), - "created_at": timestamp, - "expires_at": expiry, - "settled_at": timestamp if is_settled else None, - "metadata": {}, - } - if invoice_data.description_hash: - res["description_hash"] = invoice_data.description_hash + res = _build_transaction_data(payment) # await log_nwc(pubkey, payload) return [(res, None, [])] @@ -416,32 +497,8 @@ async def _on_list_transactions( offset=offset, ) transactions: list[dict] = [] - p: Payment for p in history: - invoice_data = bolt11_decode(p.bolt11) - is_settled = not p.pending - timestamp = int(p.time.timestamp()) or invoice_data.date - transactions.append( - { - "type": "outgoing" if p.is_out else "incoming", - "invoice": p.bolt11, - # Fallback chain so a human-readable description reaches - # the NWC client. Mirror of `_on_lookup_invoice` - "description": ( - (p.extra or {}).get("comment") - or invoice_data.description - or p.memo - ), - "description_hash": invoice_data.description_hash, - "preimage": p.preimage if is_settled or p.is_in else None, - "payment_hash": p.payment_hash, - "amount": abs(p.msat), - "fees_paid": p.fee, - "created_at": timestamp, - "settled_at": timestamp if is_settled else None, - "metadata": {}, - } - ) + transactions.append(_build_transaction_data(p)) # await log_nwc(pubkey, payload) return [({"transactions": transactions}, None, [])] @@ -494,6 +551,10 @@ async def _on_get_info( if spm in allowed_methods: account_methods.append(spm) break + # Include supported notifications if key has notifications permission + notifications = [] + if "notifications" in permissions: + notifications = sp.supported_notifications # await log_nwc(pubkey, payload) return [ ( @@ -504,6 +565,7 @@ async def _on_get_info( "block_height": 0, "block_hash": "", "methods": account_methods, + "notifications": notifications, }, None, [], @@ -511,6 +573,26 @@ async def _on_get_info( ] +async def _on_invoice_paid(sp: NWCServiceProvider, payment: Payment): + """Handle an incoming payment by sending payment_received notifications.""" + try: + notification = _build_transaction_data(payment) + await _send_notification_to_wallet( + sp, payment.wallet_id, "payment_received", notification + ) + except Exception as e: + logger.debug(f"Failed to handle invoice paid notification: {e}") + + +async def _notification_listener(sp: NWCServiceProvider): + """Listen for incoming paid invoices and send payment_received notifications.""" + queue: asyncio.Queue = asyncio.Queue() + register_invoice_listener(queue, "ext_nwcprovider") + while True: + payment = await queue.get() + await _on_invoice_paid(sp, payment) + + async def handle_nwc(): priv_key = await get_config_nwc("provider_key") relay = await get_config_nwc("relay") @@ -528,10 +610,12 @@ async def handle_nwc(): # nwcsp.addRequestListener("multi_pay_keysend", _on_multi_pay_keysend) ### await nwcsp.start() + notification_task = asyncio.create_task(_notification_listener(nwcsp)) try: while True: await asyncio.sleep(3600) except asyncio.CancelledError: + notification_task.cancel() await nwcsp.cleanup() raise diff --git a/templates/nwcprovider/admin.html b/templates/nwcprovider/admin.html index 81576b4..0ff4709 100644 --- a/templates/nwcprovider/admin.html +++ b/templates/nwcprovider/admin.html @@ -22,7 +22,7 @@
label="Nostr Relay URL" filled wrap - :hint="'URL of the Nostr relay for dispatching and receiving NWC events. Use public relays or a custom one. Specify `nostrclient` to use the Nostr Client extension'" + :hint="'URL of the Nostr relay for dispatching and receiving NWC events. Use public relays or a custom one. Enter nostrclient to use the Nostr Client extension'" > @@ -41,7 +41,7 @@
@@ -51,7 +51,7 @@
+ > + + Connected Apps
round /> + +
+ +
+ Connect apps to enable automated payments within your budget limits. +
+ +
+ + - +