diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed44344..c428ddd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,7 @@ on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+" + - 'v[0-9]+.[0-9]+.[0-9]+' jobs: release: @@ -22,7 +22,7 @@ jobs: with: token: ${{ secrets.EXT_GITHUB }} repository: lnbits/lnbits-extensions - path: "./lnbits-extensions" + path: './lnbits-extensions' - name: setup git user run: | @@ -31,12 +31,12 @@ jobs: - name: Create pull request in extensions repo env: GH_TOKEN: ${{ secrets.EXT_GITHUB }} - repo_name: "${{ github.event.repository.name }}" - tag: "${{ github.ref_name }}" - branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}" - title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}" - body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}" - archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip" + repo_name: '${{ github.event.repository.name }}' + tag: '${{ github.ref_name }}' + branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}' + title: '[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}' + body: 'https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}' + archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip' run: | cd lnbits-extensions git checkout -b $branch diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed44344..c428ddd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+" + - 'v[0-9]+.[0-9]+.[0-9]+' jobs: release: @@ -22,7 +22,7 @@ jobs: with: token: ${{ secrets.EXT_GITHUB }} repository: lnbits/lnbits-extensions - path: "./lnbits-extensions" + path: './lnbits-extensions' - name: setup git user run: | @@ -31,12 +31,12 @@ jobs: - name: Create pull request in extensions repo env: GH_TOKEN: ${{ secrets.EXT_GITHUB }} - repo_name: "${{ github.event.repository.name }}" - tag: "${{ github.ref_name }}" - branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}" - title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}" - body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}" - archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip" + repo_name: '${{ github.event.repository.name }}' + tag: '${{ github.ref_name }}' + branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}' + title: '[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}' + body: 'https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}' + archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip' run: | cd lnbits-extensions git checkout -b $branch diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e720631..1537439 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: "3.9" + python-version: '3.9' - name: Install dependencies run: | diff --git a/__init__.py b/__init__.py index 86e59d7..3cfa20c 100644 --- a/__init__.py +++ b/__init__.py @@ -45,7 +45,7 @@ def nwcprovider_start(): __all__ = [ "db", "nwcprovider_ext", - "nwcprovider_static_files", "nwcprovider_start", + "nwcprovider_static_files", "nwcprovider_stop", ] diff --git a/migrations.py b/migrations.py index fd12650..da5fdd8 100644 --- a/migrations.py +++ b/migrations.py @@ -87,12 +87,13 @@ async def m004_default_config2(db): Default config """ await db.execute( - """ + """ INSERT INTO nwcprovider.config (key, value) VALUES ('relay_alias', :value) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; """, - {"value": ""}, - ) + {"value": ""}, + ) + async def m005_key_last_used(db): """ diff --git a/nwcp.py b/nwcp.py index 4a5023a..ded5999 100644 --- a/nwcp.py +++ b/nwcp.py @@ -4,7 +4,8 @@ import hashlib import json import random import time -from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union +from collections.abc import Awaitable +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import secp256k1 import websockets.client as websockets diff --git a/static/js/admin.js b/static/js/admin.js index afacb7e..3e88718 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,87 +1,81 @@ window.app = Vue.createApp({ - el: "#vue", + el: '#vue', mixins: [windowMixin], - delimiters: ["${", "}"], + delimiters: ['${', '}'], data: function () { return { - entries: [], + config: {}, columns: [ { - name: "key", + name: 'key', required: true, - label: "Key", - align: "left", - field: (row) => row.key, - sortable: true, + label: 'Key', + align: 'left', + field: row => row.key, + sortable: true }, { - name: "value", + name: 'value', required: true, - label: "Value", - align: "left", - field: (row) => row.value, - sortable: true, - }, - ], - }; + label: 'Value', + align: 'left', + field: row => row.value, + sortable: true + } + ] + } }, methods: { fetchConfig() { - this.entries = []; + this.config = {} LNbits.api .request( - "GET", - "/nwcprovider/api/v1/config", - this.g.user.wallets[0].adminkey, + 'GET', + '/nwcprovider/api/v1/config', + this.g.user.wallets[0].adminkey ) - .then((response) => { - const newEntries = []; - for (const [key, value] of Object.entries(response.data)) { - newEntries.push({ - key: key, - value: value, - }); - } - this.entries.length = 0; - this.entries.push(...newEntries); + .then(response => { + this.config = response.data + console.log('Config fetched:', this.config) }) .catch(function (error) { - console.error("Error fetching config:", error); - }); + console.error('Error fetching config:', error) + }) }, async saveConfig() { - const data = {}; - for (const entry of this.entries) { - data[entry.key] = entry.value; + const data = {} + for (const [key, value] of Object.entries(this.config)) { + data[key] = value } + console.log('Saving config:', data) try { const response = await LNbits.api.request( - "POST", - "/nwcprovider/api/v1/config", + 'POST', + '/nwcprovider/api/v1/config', this.g.user.wallets[0].adminkey, - data, - ); + data + ) Quasar.Notify.create({ - type: "positive", - message: "Config saved!", - }); + type: 'positive', + message: 'Config saved!' + }) Quasar.Notify.create({ - type: "warning", + type: 'warning', message: - "You need to restart the server for the changes to take effect!", - }); + 'You need to restart the server for the changes to take effect!' + }) } catch (error) { Quasar.Notify.create({ - type: "negative", - message: "Error saving config: " + String(error), - }); - console.error("Error saving config:", error); + type: 'negative', + message: 'Error saving config: ' + String(error) + }) + console.error('Error saving config:', error) } - }, + } }, created: function () { - this.fetchConfig(); - }, -}); + this.fetchConfig() + } +}) diff --git a/static/js/index.js b/static/js/index.js index e8a4ec1..d593469 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,7 +1,7 @@ window.app = Vue.createApp({ - el: "#vue", + el: '#vue', mixins: [windowMixin], - delimiters: ["${", "}"], + delimiters: ['${', '}'], data: function () { return { selectedWallet: null, @@ -10,244 +10,244 @@ window.app = Vue.createApp({ nwcsTable: { columns: [ { - name: "description", - align: "left", - label: "Description", - field: "description", + name: 'description', + align: 'left', + label: 'Description', + field: 'description' }, - { name: "status", align: "left", label: "Status", field: "status" }, + {name: 'status', align: 'left', label: 'Status', field: 'status'}, { - name: "last_used", - align: "left", - label: "Last used", - field: "last_used", + name: 'last_used', + align: 'left', + label: 'Last used', + field: 'last_used' }, { - name: "created_at", - align: "left", - label: "Created", - field: "created_at", + name: 'created_at', + align: 'left', + label: 'Created', + field: 'created_at' }, { - name: "expires_at", - align: "left", - label: "Expires", - field: "expires_at", - }, + name: 'expires_at', + align: 'left', + label: 'Expires', + field: 'expires_at' + } ], pagination: { - rowsPerPage: 10, - }, + rowsPerPage: 10 + } }, connectDialog: { show: false, - data: {}, + data: {} }, pairingDialog: { show: false, data: { - pairingUrl: "", - }, + pairingUrl: '' + } }, pairingQrDialog: { show: false, data: { - pairingUrl: "", - }, + pairingUrl: '' + } }, connectionInfoDialog: { show: false, - data: {}, - }, - }; + data: {} + } + } }, methods: { showConnectDialog() { - const wallet = this.getWallet(); + const wallet = this.getWallet() if (!wallet) { Quasar.Notify.create({ - type: "negative", - message: "Please select a wallet first", - }); - return; + type: 'negative', + message: 'Please select a wallet first' + }) + return } else { - this.connectDialog.show = true; + this.connectDialog.show = true } }, openConnectionInfoDialog(data) { - this.connectionInfoDialog.data = data; - this.connectionInfoDialog.show = true; + this.connectionInfoDialog.data = data + this.connectionInfoDialog.show = true }, closeConnectionInfoDialog() { - this.connectionInfoDialog.show = false; + this.connectionInfoDialog.show = false }, openPairingUrl() { - const url = this.pairingDialog.data.pairingUrl; - if (url) window.open(url, "_blank"); + const url = this.pairingDialog.data.pairingUrl + if (url) window.open(url, '_blank') }, go(url) { - window.open(url, "_blank"); + window.open(url, '_blank') }, async copyPairingUrl() { - const url = this.pairingDialog.data.pairingUrl; + const url = this.pairingDialog.data.pairingUrl if (url) { try { - await navigator.clipboard.writeText(url); + await navigator.clipboard.writeText(url) Quasar.Notify.create({ - type: "positive", - message: "URL copied to clipboard", - }); + type: 'positive', + message: 'URL copied to clipboard' + }) } catch (err) { Quasar.Notify.create({ - type: "negative", - message: "Failed to copy URL.", - }); + type: 'negative', + message: 'Failed to copy URL.' + }) } } }, showPairingQR() { - this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl; - this.pairingQrDialog.show = true; + this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl + this.pairingQrDialog.show = true }, closePairingQrDialog() { - this.pairingQrDialog.show = false; + this.pairingQrDialog.show = false }, loadConnectDialogData() { this.connectDialog.data = { - description: "", + description: '', expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7, neverExpires: true, permissions: [], - budgets: [], - }; + budgets: [] + } for (const permission of this.nodePermissions) { this.connectDialog.data.permissions.push({ key: permission.key, name: permission.name, - value: permission.value, - }); + value: permission.value + }) } }, deleteBudget(index) { - this.connectDialog.data.budgets.splice(index, 1); + this.connectDialog.data.budgets.splice(index, 1) }, addBudget() { this.connectDialog.data.budgets.push({ budget_sats: 1000, used_budget_sats: 0, created_at: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000, - expiration: "never", - }); + expiration: 'never' + }) }, closeConnectDialog() { - this.connectDialog.show = false; - this.loadConnectDialogData(); + this.connectDialog.show = false + this.loadConnectDialogData() }, getWallet: function () { - let wallet = undefined; + let wallet = undefined for (let i = 0; i < this.g.user.wallets.length; i++) { if (this.g.user.wallets[i].id == this.selectedWallet) { - wallet = this.g.user.wallets[i]; - break; + wallet = this.g.user.wallets[i] + break } } - return wallet; + return wallet }, async generateKeyPair() { while (!window.NobleSecp256k1) { - await new Promise((resolve) => setTimeout(resolve, 1)); + await new Promise(resolve => setTimeout(resolve, 1)) } - const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey(); - const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes); + const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey() + const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes) const out = { privKeyBytes: privKeyBytes, pubKeyBytes: pubKeyBytes, privKey: window.NobleSecp256k1.etc.bytesToHex(privKeyBytes), - pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1)), - }; - return out; + pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1)) + } + return out }, deleteNWC: async function (pubkey) { Quasar.Dialog.create({ - title: "Confirm Deletion", - message: "Are you sure you want to delete this connection?", + title: 'Confirm Deletion', + message: 'Are you sure you want to delete this connection?', cancel: true, - persistent: true, + persistent: true }) .onOk(async () => { try { - const wallet = this.getWallet(); + const wallet = this.getWallet() const response = await LNbits.api.request( - "DELETE", + 'DELETE', `/nwcprovider/api/v1/nwc/${pubkey}`, - wallet.adminkey, - ); - this.loadNwcs(); + wallet.adminkey + ) + this.loadNwcs() Quasar.Notify.create({ - type: "positive", - message: "Deleted successfully", - }); + type: 'positive', + message: 'Deleted successfully' + }) } catch (error) { - LNbits.utils.notifyApiError(error); + LNbits.utils.notifyApiError(error) } }) .onCancel(() => { // User canceled the operation - }); + }) }, loadNwcs: async function () { - const wallet = this.getWallet(); + const wallet = this.getWallet() if (!wallet) { - this.nwcs = []; - return; + this.nwcs = [] + return } try { const response = await LNbits.api.request( - "GET", - "/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true", - wallet.adminkey, - ); - this.nwcs = response.data; + 'GET', + '/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true', + wallet.adminkey + ) + this.nwcs = response.data } catch (error) { - this.nwcs = []; + this.nwcs = [] } try { const response = await LNbits.api.request( - "GET", - "/nwcprovider/api/v1/permissions", - wallet.adminkey, - ); - const permissions = []; + 'GET', + '/nwcprovider/api/v1/permissions', + wallet.adminkey + ) + const permissions = [] for (const [key, value] of Object.entries(response.data)) { permissions.push({ key: key, name: value.name, - value: value.default, - }); + value: value.default + }) } - this.nodePermissions = permissions; + this.nodePermissions = permissions } catch (error) { - Lnbits.utils.notifyApiError(error); + Lnbits.utils.notifyApiError(error) } - this.loadConnectDialogData(); - const newTableEntries = []; + this.loadConnectDialogData() + const newTableEntries = [] for (const nwc of this.nwcs) { const t = Quasar.date.formatDate( new Date(nwc.data.created_at * 1000), - "YYYY-MM-DD HH:mm", - ); + 'YYYY-MM-DD HH:mm' + ) const e = nwc.data.expires_at > 0 ? Quasar.date.formatDate( new Date(nwc.data.expires_at * 1000), - "YYYY-MM-DD HH:mm", + 'YYYY-MM-DD HH:mm' ) - : "Never"; + : 'Never' const l = Quasar.date.formatDate( new Date(nwc.data.last_used * 1000), - "YYYY-MM-DD HH:mm", - ); + 'YYYY-MM-DD HH:mm' + ) const nwcTableEntry = { description: nwc.data.description, created_at: t, @@ -256,131 +256,130 @@ window.app = Vue.createApp({ pubkey: nwc.data.pubkey, permissions: nwc.data.permissions, budgets: [], - status: "Active", - }; + status: 'Active' + } if ( nwc.data.expires_at > 0 && nwc.data.expires_at < new Date().getTime() / 1000 ) { - nwcTableEntry.status = "Expired"; + nwcTableEntry.status = 'Expired' } for (const budget of nwc.budgets) { const createdAt = Quasar.date.formatDate( new Date(budget.created_at * 1000), - "YYYY-MM-DD HH:mm", - ); - let refreshWindow = budget.refresh_window; + 'YYYY-MM-DD HH:mm' + ) + let refreshWindow = budget.refresh_window if (refreshWindow <= 0) { - refreshWindow = "Never"; + refreshWindow = 'Never' } else if (refreshWindow == 60 * 60 * 24) { - refreshWindow = "Daily"; + refreshWindow = 'Daily' } else if (refreshWindow == 60 * 60 * 24 * 7) { - refreshWindow = "Weekly"; + refreshWindow = 'Weekly' } else if (refreshWindow == 60 * 60 * 24 * 30) { - refreshWindow = "Monthly"; + refreshWindow = 'Monthly' } else if (refreshWindow == 60 * 60 * 24 * 365) { - refreshWindow = "Yearly"; + refreshWindow = 'Yearly' } nwcTableEntry.budgets.push({ budget_sats: budget.budget_msats / 1000, used_budget_sats: budget.used_budget_msats / 1000, created_at: createdAt, - refresh_window: refreshWindow, - }); + refresh_window: refreshWindow + }) } - newTableEntries.push(nwcTableEntry); + newTableEntries.push(nwcTableEntry) } - this.nwcEntries = newTableEntries; + this.nwcEntries = newTableEntries }, closePairingDialog() { - this.pairingDialog.show = false; + 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; - this.pairingDialog.show = true; + 'GET', + '/nwcprovider/api/v1/pairing/{SECRET}' + ) + response = response.data + response = response.replace('{SECRET}', secret) + this.pairingDialog.data.pairingUrl = response + this.pairingDialog.show = true }, async confirmConnectDialog() { - const keyPair = await this.generateKeyPair(); + const keyPair = await this.generateKeyPair() // timestamp - let expires_at = 0; + let expires_at = 0 if (!this.connectDialog.data.neverExpires) { expires_at = - new Date(this.connectDialog.data.expires_at).getTime() / 1000; + new Date(this.connectDialog.data.expires_at).getTime() / 1000 } const data = { permissions: [], description: this.connectDialog.data.description, expires_at: expires_at, - budgets: [], - }; + budgets: [] + } for (const permission of this.connectDialog.data.permissions) { - if (permission.value) data.permissions.push(permission.key); + if (permission.value) data.permissions.push(permission.key) } for (const budget of this.connectDialog.data.budgets) { - const budget_msats = budget.budget_sats * 1000; - let refresh_window = 0; + const budget_msats = budget.budget_sats * 1000 + let refresh_window = 0 switch (budget.expiry) { - case "Daily": - refresh_window = 60 * 60 * 24; - break; - case "Weekly": - refresh_window = 60 * 60 * 24 * 7; - break; - case "Monthly": - refresh_window = 60 * 60 * 24 * 30; - break; - case "Yearly": - refresh_window = 60 * 60 * 24 * 365; - break; - case "Never": - refresh_window = 0; - break; + case 'Daily': + refresh_window = 60 * 60 * 24 + break + case 'Weekly': + refresh_window = 60 * 60 * 24 * 7 + break + case 'Monthly': + refresh_window = 60 * 60 * 24 * 30 + break + case 'Yearly': + refresh_window = 60 * 60 * 24 * 365 + break + case 'Never': + refresh_window = 0 + break } data.budgets.push({ budget_msats: budget_msats, refresh_window: refresh_window, - created_at: - new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000, - }); + created_at: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000 + }) } - const wallet = this.getWallet(); + const wallet = this.getWallet() try { const response = await LNbits.api.request( - "PUT", - "/nwcprovider/api/v1/nwc/" + keyPair.pubKey, + 'PUT', + '/nwcprovider/api/v1/nwc/' + keyPair.pubKey, wallet.adminkey, - data, - ); - this.closeConnectDialog(); + data + ) + this.closeConnectDialog() if ( !response.data || !response.data.data || !response.data.data.pubkey ) { - LNbits.utils.notifyApiError("Error creating nwc pairing"); - return; + LNbits.utils.notifyApiError('Error creating nwc pairing') + return } - this.showPairingDialog(keyPair.privKey); + this.showPairingDialog(keyPair.privKey) } catch (error) { - LNbits.utils.notifyApiError(error); + LNbits.utils.notifyApiError(error) } - this.loadNwcs(); - }, + this.loadNwcs() + } }, created: function () { - this.loadNwcs(); + this.loadNwcs() }, watch: { selectedWallet(newValue, oldValue) { - this.loadNwcs(); - }, - }, -}); + this.loadNwcs() + } + } +}) diff --git a/static/js/noble-secp256k1.min.js b/static/js/noble-secp256k1.min.js index af328c8..c996cd2 100644 --- a/static/js/noble-secp256k1.min.js +++ b/static/js/noble-secp256k1.min.js @@ -4,93 +4,93 @@ const B256 = 2n ** 256n, N = B256 - 0x14551231950b75fc4402da1732fc9bebfn, Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n, - CURVE = { p: P, n: N, a: 0n, b: 7n, Gx, Gy }, + CURVE = {p: P, n: N, a: 0n, b: 7n, Gx, Gy}, fLen = 32, - crv = (t) => mod(mod(t * t) * t + CURVE.b), - err = (t = "") => { - throw new Error(t); + crv = t => mod(mod(t * t) * t + CURVE.b), + err = (t = '') => { + throw new Error(t) }, - big = (t) => "bigint" == typeof t, - str = (t) => "string" == typeof t, - fe = (t) => big(t) && 0n < t && t < P, - ge = (t) => big(t) && 0n < t && t < N, - isu8 = (t) => + big = t => 'bigint' == typeof t, + str = t => 'string' == typeof t, + fe = t => big(t) && 0n < t && t < P, + ge = t => big(t) && 0n < t && t < N, + isu8 = t => t instanceof Uint8Array || - (null != t && "object" == typeof t && "Uint8Array" === t.constructor.name), + (null != t && 'object' == typeof t && 'Uint8Array' === t.constructor.name), au8 = (t, n) => - !isu8(t) || ("number" == typeof n && n > 0 && t.length !== n) - ? err("Uint8Array expected") + !isu8(t) || ('number' == typeof n && n > 0 && t.length !== n) + ? err('Uint8Array expected') : t, - u8n = (t) => new Uint8Array(t), + u8n = t => new Uint8Array(t), toU8 = (t, n) => au8(str(t) ? h2b(t) : u8n(au8(t)), n), mod = (t, n = P) => { - let e = t % n; - return e >= 0n ? e : n + e; + let e = t % n + return e >= 0n ? e : n + e }, - isPoint = (t) => (t instanceof Point ? t : err("Point expected")); + isPoint = t => (t instanceof Point ? t : err('Point expected')) class Point { constructor(t, n, e) { - (this.px = t), (this.py = n), (this.pz = e); + ;((this.px = t), (this.py = n), (this.pz = e)) } static fromAffine(t) { - return 0n === t.x && 0n === t.y ? Point.ZERO : new Point(t.x, t.y, 1n); + return 0n === t.x && 0n === t.y ? Point.ZERO : new Point(t.x, t.y, 1n) } static fromHex(t) { - let n; + let n const e = (t = toU8(t))[0], r = t.subarray(1), o = slcNum(r, 0, 32), - i = t.length; + i = t.length if (33 === i && [2, 3].includes(e)) { - fe(o) || err("Point hex invalid: x not FE"); - let t = sqrt(crv(o)); - !(1 & ~e) !== (1n === (1n & t)) && (t = mod(-t)), - (n = new Point(o, t, 1n)); + fe(o) || err('Point hex invalid: x not FE') + let t = sqrt(crv(o)) + ;(!(1 & ~e) !== (1n === (1n & t)) && (t = mod(-t)), + (n = new Point(o, t, 1n))) } return ( 65 === i && 4 === e && (n = new Point(o, slcNum(r, 32, 64), 1n)), - n ? n.ok() : err("Point is not on curve") - ); + n ? n.ok() : err('Point is not on curve') + ) } static fromPrivateKey(t) { - return G.mul(toPriv(t)); + return G.mul(toPriv(t)) } get x() { - return this.aff().x; + return this.aff().x } get y() { - return this.aff().y; + return this.aff().y } equals(t) { - const { px: n, py: e, pz: r } = this, - { px: o, py: i, pz: s } = isPoint(t), + const {px: n, py: e, pz: r} = this, + {px: o, py: i, pz: s} = isPoint(t), a = mod(n * s), c = mod(o * r), d = mod(e * s), - u = mod(i * r); - return a === c && d === u; + u = mod(i * r) + return a === c && d === u } negate() { - return new Point(this.px, mod(-this.py), this.pz); + return new Point(this.px, mod(-this.py), this.pz) } double() { - return this.add(this); + return this.add(this) } add(t) { - const { px: n, py: e, pz: r } = this, - { px: o, py: i, pz: s } = isPoint(t), - { a, b: c } = CURVE; + const {px: n, py: e, pz: r} = this, + {px: o, py: i, pz: s} = isPoint(t), + {a, b: c} = CURVE let d = 0n, u = 0n, - m = 0n; - const h = mod(3n * c); + m = 0n + const h = mod(3n * c) let l = mod(n * o), y = mod(e * i), f = mod(r * s), p = mod(n + e), - b = mod(o + i); - (p = mod(p * b)), (b = mod(l + y)), (p = mod(p - b)), (b = mod(n + r)); - let g = mod(o + s); + b = mod(o + i) + ;((p = mod(p * b)), (b = mod(l + y)), (p = mod(p - b)), (b = mod(n + r))) + let g = mod(o + s) return ( (b = mod(b * g)), (g = mod(l + f)), @@ -123,300 +123,301 @@ class Point { (m = mod(g * m)), (m = mod(m + l)), new Point(d, u, m) - ); + ) } mul(t, n = !0) { - if (!n && 0n === t) return I; - if ((ge(t) || err("invalid scalar"), this.equals(G))) return wNAF(t).p; + if (!n && 0n === t) return I + if ((ge(t) || err('invalid scalar'), this.equals(G))) return wNAF(t).p let e = I, - r = G; + r = G for (let o = this; t > 0n; o = o.double(), t >>= 1n) - 1n & t ? (e = e.add(o)) : n && (r = r.add(o)); - return e; + 1n & t ? (e = e.add(o)) : n && (r = r.add(o)) + return e } mulAddQUns(t, n, e) { - return this.mul(n, !1).add(t.mul(e, !1)).ok(); + return this.mul(n, !1).add(t.mul(e, !1)).ok() } toAffine() { - const { px: t, py: n, pz: e } = this; - if (this.equals(I)) return { x: 0n, y: 0n }; - if (1n === e) return { x: t, y: n }; - const r = inv(e); + const {px: t, py: n, pz: e} = this + if (this.equals(I)) return {x: 0n, y: 0n} + if (1n === e) return {x: t, y: n} + const r = inv(e) return ( - 1n !== mod(e * r) && err("invalid inverse"), - { x: mod(t * r), y: mod(n * r) } - ); + 1n !== mod(e * r) && err('invalid inverse'), + {x: mod(t * r), y: mod(n * r)} + ) } assertValidity() { - const { x: t, y: n } = this.aff(); + const {x: t, y: n} = this.aff() return ( - (fe(t) && fe(n)) || err("Point invalid: x or y"), - mod(n * n) === crv(t) ? this : err("Point invalid: not on curve") - ); + (fe(t) && fe(n)) || err('Point invalid: x or y'), + mod(n * n) === crv(t) ? this : err('Point invalid: not on curve') + ) } multiply(t) { - return this.mul(t); + return this.mul(t) } aff() { - return this.toAffine(); + return this.toAffine() } ok() { - return this.assertValidity(); + return this.assertValidity() } toHex(t = !0) { - const { x: n, y: e } = this.aff(); + const {x: n, y: e} = this.aff() return ( - (t ? (0n === (1n & e) ? "02" : "03") : "04") + n2h(n) + (t ? "" : n2h(e)) - ); + (t ? (0n === (1n & e) ? '02' : '03') : '04') + n2h(n) + (t ? '' : n2h(e)) + ) } toRawBytes(t = !0) { - return h2b(this.toHex(t)); + return h2b(this.toHex(t)) } } -(Point.BASE = new Point(Gx, Gy, 1n)), (Point.ZERO = new Point(0n, 1n, 0n)); -const { BASE: G, ZERO: I } = Point, - padh = (t, n) => t.toString(16).padStart(n, "0"), - b2h = (t) => +;((Point.BASE = new Point(Gx, Gy, 1n)), (Point.ZERO = new Point(0n, 1n, 0n))) +const {BASE: G, ZERO: I} = Point, + padh = (t, n) => t.toString(16).padStart(n, '0'), + b2h = t => Array.from(t) - .map((t) => padh(t, 2)) - .join(""), - h2b = (t) => { - const n = t.length; - (!str(t) || n % 2) && err("hex invalid 1"); - const e = u8n(n / 2); + .map(t => padh(t, 2)) + .join(''), + h2b = t => { + const n = t.length + ;(!str(t) || n % 2) && err('hex invalid 1') + const e = u8n(n / 2) for (let n = 0; n < e.length; n++) { const r = 2 * n, o = t.slice(r, r + 2), - i = Number.parseInt(o, 16); - (Number.isNaN(i) || i < 0) && err("hex invalid 2"), (e[n] = i); + i = Number.parseInt(o, 16) + ;((Number.isNaN(i) || i < 0) && err('hex invalid 2'), (e[n] = i)) } - return e; + return e }, - b2n = (t) => BigInt("0x" + (b2h(t) || "0")), + b2n = t => BigInt('0x' + (b2h(t) || '0')), slcNum = (t, n, e) => b2n(t.slice(n, e)), - n2b = (t) => - big(t) && t >= 0n && t < B256 ? h2b(padh(t, 64)) : err("bigint expected"), - n2h = (t) => b2h(n2b(t)), + n2b = t => + big(t) && t >= 0n && t < B256 ? h2b(padh(t, 64)) : err('bigint expected'), + n2h = t => b2h(n2b(t)), concatB = (...t) => { - const n = u8n(t.reduce((t, n) => t + au8(n).length, 0)); - let e = 0; + const n = u8n(t.reduce((t, n) => t + au8(n).length, 0)) + let e = 0 return ( - t.forEach((t) => { - n.set(t, e), (e += t.length); + t.forEach(t => { + ;(n.set(t, e), (e += t.length)) }), n - ); + ) }, inv = (t, n = P) => { - (0n === t || n <= 0n) && err("no inverse n=" + t + " mod=" + n); + ;(0n === t || n <= 0n) && err('no inverse n=' + t + ' mod=' + n) let e = mod(t, n), r = n, o = 0n, i = 1n, s = 1n, - a = 0n; + a = 0n for (; 0n !== e; ) { const t = r / e, n = r % e, c = o - s * t, - d = i - a * t; - (r = e), (e = n), (o = s), (i = a), (s = c), (a = d); + d = i - a * t + ;((r = e), (e = n), (o = s), (i = a), (s = c), (a = d)) } - return 1n === r ? mod(o, n) : err("no inverse"); + return 1n === r ? mod(o, n) : err('no inverse') }, - sqrt = (t) => { - let n = 1n; + sqrt = t => { + let n = 1n for (let e = t, r = (P + 1n) / 4n; r > 0n; r >>= 1n) - 1n & r && (n = (n * e) % P), (e = (e * e) % P); - return mod(n * n) === t ? n : err("sqrt invalid"); + (1n & r && (n = (n * e) % P), (e = (e * e) % P)) + return mod(n * n) === t ? n : err('sqrt invalid') }, - toPriv = (t) => ( + toPriv = t => ( big(t) || (t = b2n(toU8(t, 32))), - ge(t) ? t : err("private key out of range") + ge(t) ? t : err('private key out of range') ), - moreThanHalfN = (t) => t > N >> 1n, - getPublicKey = (t, n = !0) => Point.fromPrivateKey(t).toRawBytes(n); + moreThanHalfN = t => t > N >> 1n, + getPublicKey = (t, n = !0) => Point.fromPrivateKey(t).toRawBytes(n) class Signature { constructor(t, n, e) { - (this.r = t), (this.s = n), (this.recovery = e), this.assertValidity(); + ;((this.r = t), (this.s = n), (this.recovery = e), this.assertValidity()) } static fromCompact(t) { return ( - (t = toU8(t, 64)), new Signature(slcNum(t, 0, 32), slcNum(t, 32, 64)) - ); + (t = toU8(t, 64)), + new Signature(slcNum(t, 0, 32), slcNum(t, 32, 64)) + ) } assertValidity() { - return ge(this.r) && ge(this.s) ? this : err(); + return ge(this.r) && ge(this.s) ? this : err() } addRecoveryBit(t) { - return new Signature(this.r, this.s, t); + return new Signature(this.r, this.s, t) } hasHighS() { - return moreThanHalfN(this.s); + return moreThanHalfN(this.s) } normalizeS() { return this.hasHighS() ? new Signature(this.r, mod(this.s, N), this.recovery) - : this; + : this } recoverPublicKey(t) { - const { r: n, s: e, recovery: r } = this; - [0, 1, 2, 3].includes(r) || err("recovery id invalid"); + const {r: n, s: e, recovery: r} = this + ;[0, 1, 2, 3].includes(r) || err('recovery id invalid') const o = bits2int_modN(toU8(t, 32)), - i = 2 === r || 3 === r ? n + N : n; - i >= P && err("q.x invalid"); - const s = 1 & r ? "03" : "02", + i = 2 === r || 3 === r ? n + N : n + i >= P && err('q.x invalid') + const s = 1 & r ? '03' : '02', a = Point.fromHex(s + n2h(i)), c = inv(i, N), d = mod(-o * c, N), - u = mod(e * c, N); - return G.mulAddQUns(a, d, u); + u = mod(e * c, N) + return G.mulAddQUns(a, d, u) } toCompactRawBytes() { - return h2b(this.toCompactHex()); + return h2b(this.toCompactHex()) } toCompactHex() { - return n2h(this.r) + n2h(this.s); + return n2h(this.r) + n2h(this.s) } } -const bits2int = (t) => { +const bits2int = t => { const n = 8 * t.length - 256, - e = b2n(t); - return n > 0 ? e >> BigInt(n) : e; + e = b2n(t) + return n > 0 ? e >> BigInt(n) : e }, - bits2int_modN = (t) => mod(bits2int(t), N), - i2o = (t) => n2b(t), + bits2int_modN = t => mod(bits2int(t), N), + i2o = t => n2b(t), cr = () => - "object" == typeof globalThis && "crypto" in globalThis + 'object' == typeof globalThis && 'crypto' in globalThis ? globalThis.crypto - : void 0; -let _hmacSync; -const optS = { lowS: !0 }, - optV = { lowS: !0 }, + : void 0 +let _hmacSync +const optS = {lowS: !0}, + optV = {lowS: !0}, prepSig = (t, n, e = optS) => { - ["der", "recovered", "canonical"].some((t) => t in e) && - err("sign() legacy options not supported"); - let { lowS: r } = e; - null == r && (r = !0); + ;['der', 'recovered', 'canonical'].some(t => t in e) && + err('sign() legacy options not supported') + let {lowS: r} = e + null == r && (r = !0) const o = bits2int_modN(toU8(t)), i = i2o(o), s = toPriv(n), - a = [i2o(s), i]; - let c = e.extraEntropy; + a = [i2o(s), i] + let c = e.extraEntropy if (c) { - !0 === c && (c = etc.randomBytes(32)); - const t = toU8(c); - 32 !== t.length && err(), a.push(t); + !0 === c && (c = etc.randomBytes(32)) + const t = toU8(c) + ;(32 !== t.length && err(), a.push(t)) } - const d = o; + const d = o return { seed: concatB(...a), - k2sig: (t) => { - const n = bits2int(t); - if (!ge(n)) return; + k2sig: t => { + const n = bits2int(t) + if (!ge(n)) return const e = inv(n, N), o = G.mul(n).aff(), - i = mod(o.x, N); - if (0n === i) return; - const a = mod(e * mod(d + mod(s * i, N), N), N); - if (0n === a) return; + i = mod(o.x, N) + if (0n === i) return + const a = mod(e * mod(d + mod(s * i, N), N), N) + if (0n === a) return let c = a, - u = (o.x === i ? 0 : 2) | Number(1n & o.y); + u = (o.x === i ? 0 : 2) | Number(1n & o.y) return ( r && moreThanHalfN(a) && ((c = mod(-a, N)), (u ^= 1)), new Signature(i, c, u) - ); - }, - }; - }; + ) + } + } + } function hmacDrbg(t) { let n = u8n(32), e = u8n(32), - r = 0; + r = 0 const o = () => { - n.fill(1), e.fill(0), (r = 0); + ;(n.fill(1), e.fill(0), (r = 0)) }, - i = "drbg: tried 1000 values"; + i = 'drbg: tried 1000 values' if (t) { const t = (...t) => etc.hmacSha256Async(e, n, ...t), s = async (r = u8n()) => { - (e = await t(u8n([0]), r)), + ;((e = await t(u8n([0]), r)), (n = await t()), - 0 !== r.length && ((e = await t(u8n([1]), r)), (n = await t())); + 0 !== r.length && ((e = await t(u8n([1]), r)), (n = await t()))) }, - a = async () => (r++ >= 1e3 && err(i), (n = await t()), n); + a = async () => (r++ >= 1e3 && err(i), (n = await t()), n) return async (t, n) => { - let e; - for (o(), await s(t); !(e = n(await a())); ) await s(); - return o(), e; - }; + let e + for (o(), await s(t); !(e = n(await a())); ) await s() + return (o(), e) + } } { const t = (...t) => { - const r = _hmacSync; - return r || err("etc.hmacSha256Sync not set"), r(e, n, ...t); + const r = _hmacSync + return (r || err('etc.hmacSha256Sync not set'), r(e, n, ...t)) }, s = (r = u8n()) => { - (e = t(u8n([0]), r)), + ;((e = t(u8n([0]), r)), (n = t()), - 0 !== r.length && ((e = t(u8n([1]), r)), (n = t())); + 0 !== r.length && ((e = t(u8n([1]), r)), (n = t()))) }, - a = () => (r++ >= 1e3 && err(i), (n = t()), n); + a = () => (r++ >= 1e3 && err(i), (n = t()), n) return (t, n) => { - let e; - for (o(), s(t); !(e = n(a())); ) s(); - return o(), e; - }; + let e + for (o(), s(t); !(e = n(a())); ) s() + return (o(), e) + } } } const signAsync = async (t, n, e = optS) => { - const { seed: r, k2sig: o } = prepSig(t, n, e); - return hmacDrbg(!0)(r, o); + const {seed: r, k2sig: o} = prepSig(t, n, e) + return hmacDrbg(!0)(r, o) }, sign = (t, n, e = optS) => { - const { seed: r, k2sig: o } = prepSig(t, n, e); - return hmacDrbg(!1)(r, o); + const {seed: r, k2sig: o} = prepSig(t, n, e) + return hmacDrbg(!1)(r, o) }, verify = (t, n, e, r = optV) => { let o, i, s, - { lowS: a } = r; - null == a && (a = !0), - "strict" in r && err("verify() legacy options not supported"); - const c = t && "object" == typeof t && "r" in t; - c || 64 === toU8(t).length || err("signature must be 64 bytes"); + {lowS: a} = r + ;(null == a && (a = !0), + 'strict' in r && err('verify() legacy options not supported')) + const c = t && 'object' == typeof t && 'r' in t + c || 64 === toU8(t).length || err('signature must be 64 bytes') try { - (o = c + ;((o = c ? new Signature(t.r, t.s).assertValidity() : Signature.fromCompact(t)), (i = bits2int_modN(toU8(n))), - (s = e instanceof Point ? e.ok() : Point.fromHex(e)); + (s = e instanceof Point ? e.ok() : Point.fromHex(e))) } catch (t) { - return !1; + return !1 } - if (!o) return !1; - const { r: d, s: u } = o; - if (a && moreThanHalfN(u)) return !1; - let m; + if (!o) return !1 + const {r: d, s: u} = o + if (a && moreThanHalfN(u)) return !1 + let m try { const t = inv(u, N), n = mod(i * t, N), - e = mod(d * t, N); - m = G.mulAddQUns(s, n, e).aff(); + e = mod(d * t, N) + m = G.mulAddQUns(s, n, e).aff() } catch (t) { - return !1; + return !1 } - if (!m) return !1; - return mod(m.x, N) === d; + if (!m) return !1 + return mod(m.x, N) === d }, getSharedSecret = (t, n, e = !0) => Point.fromHex(n).mul(toPriv(t)).toRawBytes(e), - hashToPrivateKey = (t) => { - ((t = toU8(t)).length < 40 || t.length > 1024) && - err("expected proper params"); - const n = mod(b2n(t), N - 1n) + 1n; - return n2b(n); + hashToPrivateKey = t => { + ;((t = toU8(t)).length < 40 || t.length > 1024) && + err('expected proper params') + const n = mod(b2n(t), N - 1n) + 1n + return n2b(n) }, etc = { hexToBytes: h2b, @@ -428,84 +429,84 @@ const signAsync = async (t, n, e = optS) => { invert: inv, hmacSha256Async: async (t, ...n) => { const e = cr(), - r = e && e.subtle; - if (!r) return err("etc.hmacSha256Async not set"); + r = e && e.subtle + if (!r) return err('etc.hmacSha256Async not set') const o = await r.importKey( - "raw", + 'raw', t, - { name: "HMAC", hash: { name: "SHA-256" } }, + {name: 'HMAC', hash: {name: 'SHA-256'}}, !1, - ["sign"], - ); - return u8n(await r.sign("HMAC", o, concatB(...n))); + ['sign'] + ) + return u8n(await r.sign('HMAC', o, concatB(...n))) }, hmacSha256Sync: _hmacSync, hashToPrivateKey, randomBytes: (t = 32) => { - const n = cr(); + const n = cr() return ( (n && n.getRandomValues) || - err("crypto.getRandomValues must be defined"), + err('crypto.getRandomValues must be defined'), n.getRandomValues(u8n(t)) - ); - }, + ) + } }, utils = { normPrivateKeyToScalar: toPriv, - isValidPrivateKey: (t) => { + isValidPrivateKey: t => { try { - return !!toPriv(t); + return !!toPriv(t) } catch (t) { - return !1; + return !1 } }, randomPrivateKey: () => hashToPrivateKey(etc.randomBytes(48)), - precompute: (t = 8, n = G) => (n.multiply(3n), n), - }; + precompute: (t = 8, n = G) => (n.multiply(3n), n) + } Object.defineProperties(etc, { hmacSha256Sync: { configurable: !1, get: () => _hmacSync, set(t) { - _hmacSync || (_hmacSync = t); - }, - }, -}); + _hmacSync || (_hmacSync = t) + } + } +}) const W = 8, precompute = () => { - const t = []; + const t = [] let n = G, - e = n; + e = n for (let r = 0; r < 33; r++) { - (e = n), t.push(e); - for (let r = 1; r < 128; r++) (e = e.add(n)), t.push(e); - n = e.double(); + ;((e = n), t.push(e)) + for (let r = 1; r < 128; r++) ((e = e.add(n)), t.push(e)) + n = e.double() } - return t; - }; -let Gpows; -const wNAF = (t) => { + return t + } +let Gpows +const wNAF = t => { const n = Gpows || (Gpows = precompute()), e = (t, n) => { - let e = n.negate(); - return t ? e : n; - }; + let e = n.negate() + return t ? e : n + } let r = I, - o = G; + o = G const i = BigInt(255), - s = BigInt(8); + s = BigInt(8) for (let a = 0; a < 33; a++) { - const c = 128 * a; - let d = Number(t & i); - (t >>= s), d > 128 && ((d -= 256), (t += 1n)); + const c = 128 * a + let d = Number(t & i) + ;((t >>= s), d > 128 && ((d -= 256), (t += 1n))) const u = c, m = c + Math.abs(d) - 1, h = a % 2 != 0, - l = d < 0; - 0 === d ? (o = o.add(e(h, n[u]))) : (r = r.add(e(l, n[m]))); + l = d < 0 + 0 === d ? (o = o.add(e(h, n[u]))) : (r = r.add(e(l, n[m]))) } - return { p: r, f: o }; -}; + return {p: r, f: o} +} export { getPublicKey, sign, @@ -516,5 +517,5 @@ export { etc, utils, Point as ProjectivePoint, - Signature, -}; + Signature +} diff --git a/templates/nwcprovider/admin.html b/templates/nwcprovider/admin.html index 171a6d0..81576b4 100644 --- a/templates/nwcprovider/admin.html +++ b/templates/nwcprovider/admin.html @@ -6,21 +6,56 @@
-
+
- NWC Service Provider - Config + NWC Service Provider Configuration
- + - + - ${entry.key} + + + + - + + + + + + + + + + + diff --git a/templates/nwcprovider/index.html b/templates/nwcprovider/index.html index 143c7af..3c6ecf1 100644 --- a/templates/nwcprovider/index.html +++ b/templates/nwcprovider/index.html @@ -1,8 +1,8 @@ {% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %} diff --git a/tests/integration/test_all.py b/tests/integration/test_all.py index 854aef4..ba6178e 100644 --- a/tests/integration/test_all.py +++ b/tests/integration/test_all.py @@ -997,23 +997,32 @@ async def create_valid_invoice(wallet, amount=1000): raise Exception(f"Failed to create invoice: {error}") return result["invoice"] + @pytest.mark.asyncio async def test_list_transactions(): # Create wallets with required permissions nwc1 = await create_nwc( - "wallet1", "test_list_transactions", ["invoice", "pay", "balance", "history"], [], 0 + "wallet1", + "test_list_transactions", + ["invoice", "pay", "balance", "history"], + [], + 0, ) nwc2 = await create_nwc( - "wallet2", "test_list_transactions", ["invoice", "pay", "balance", "history"], [], 0 + "wallet2", + "test_list_transactions", + ["invoice", "pay", "balance", "history"], + [], + 0, ) - + wallet1 = NWCWallet(nwc1["pairing"]) wallet2 = NWCWallet(nwc2["pairing"]) - + try: await wallet1.start() await wallet2.start() - + # First invoice await wallet1.send_event( "make_invoice", {"amount": 1000, "description": "test invoice 1"} @@ -1021,12 +1030,12 @@ async def test_list_transactions(): result1, _, error = await wallet1.wait_for("make_invoice") assert not error invoice1 = result1["invoice"] - + # Pay first invoice await wallet2.send_event("pay_invoice", {"invoice": invoice1}) _, _, error = await wallet2.wait_for("pay_invoice") assert not error - + # Second invoice await wallet1.send_event( "make_invoice", {"amount": 2000, "description": "test invoice 2"} @@ -1034,12 +1043,12 @@ async def test_list_transactions(): result2, _, error = await wallet1.wait_for("make_invoice") assert not error invoice2 = result2["invoice"] - + # Pay second invoice await wallet2.send_event("pay_invoice", {"invoice": invoice2}) _, _, error = await wallet2.wait_for("pay_invoice") assert not error - + # Test basic transaction listing await wallet1.send_event("list_transactions", {}) result, _, error = await wallet1.wait_for("list_transactions") @@ -1047,14 +1056,14 @@ async def test_list_transactions(): assert "transactions" in result transactions = result["transactions"] assert len(transactions) >= 2 - + # Test limit await wallet1.send_event("list_transactions", {"limit": 1}) result, _, error = await wallet1.wait_for("list_transactions") assert not error limited_txs = result["transactions"] assert len(limited_txs) == 1 - + finally: await wallet1.close() - await wallet2.close() \ No newline at end of file + await wallet2.close()