Improve configuration form for readability and UX

This commit is contained in:
blackcoffeexbt 2025-09-10 12:00:45 +01:00
commit abdd7a781b
12 changed files with 552 additions and 512 deletions

View file

@ -1,7 +1,7 @@
on: on:
push: push:
tags: tags:
- "v[0-9]+.[0-9]+.[0-9]+" - 'v[0-9]+.[0-9]+.[0-9]+'
jobs: jobs:
release: release:
@ -22,7 +22,7 @@ jobs:
with: with:
token: ${{ secrets.EXT_GITHUB }} token: ${{ secrets.EXT_GITHUB }}
repository: lnbits/lnbits-extensions repository: lnbits/lnbits-extensions
path: "./lnbits-extensions" path: './lnbits-extensions'
- name: setup git user - name: setup git user
run: | run: |
@ -31,12 +31,12 @@ jobs:
- name: Create pull request in extensions repo - name: Create pull request in extensions repo
env: env:
GH_TOKEN: ${{ secrets.EXT_GITHUB }} GH_TOKEN: ${{ secrets.EXT_GITHUB }}
repo_name: "${{ github.event.repository.name }}" repo_name: '${{ github.event.repository.name }}'
tag: "${{ github.ref_name }}" tag: '${{ github.ref_name }}'
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}" branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ 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 }}" 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" archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
run: | run: |
cd lnbits-extensions cd lnbits-extensions
git checkout -b $branch git checkout -b $branch

View file

@ -1,7 +1,7 @@
on: on:
push: push:
tags: tags:
- "v[0-9]+.[0-9]+.[0-9]+" - 'v[0-9]+.[0-9]+.[0-9]+'
jobs: jobs:
release: release:
@ -22,7 +22,7 @@ jobs:
with: with:
token: ${{ secrets.EXT_GITHUB }} token: ${{ secrets.EXT_GITHUB }}
repository: lnbits/lnbits-extensions repository: lnbits/lnbits-extensions
path: "./lnbits-extensions" path: './lnbits-extensions'
- name: setup git user - name: setup git user
run: | run: |
@ -31,12 +31,12 @@ jobs:
- name: Create pull request in extensions repo - name: Create pull request in extensions repo
env: env:
GH_TOKEN: ${{ secrets.EXT_GITHUB }} GH_TOKEN: ${{ secrets.EXT_GITHUB }}
repo_name: "${{ github.event.repository.name }}" repo_name: '${{ github.event.repository.name }}'
tag: "${{ github.ref_name }}" tag: '${{ github.ref_name }}'
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}" branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ 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 }}" 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" archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
run: | run: |
cd lnbits-extensions cd lnbits-extensions
git checkout -b $branch git checkout -b $branch

View file

@ -15,7 +15,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: "3.9" python-version: '3.9'
- name: Install dependencies - name: Install dependencies
run: | run: |

View file

@ -45,7 +45,7 @@ def nwcprovider_start():
__all__ = [ __all__ = [
"db", "db",
"nwcprovider_ext", "nwcprovider_ext",
"nwcprovider_static_files",
"nwcprovider_start", "nwcprovider_start",
"nwcprovider_static_files",
"nwcprovider_stop", "nwcprovider_stop",
] ]

View file

@ -94,6 +94,7 @@ async def m004_default_config2(db):
{"value": ""}, {"value": ""},
) )
async def m005_key_last_used(db): async def m005_key_last_used(db):
""" """
Add last_used to keys Add last_used to keys

View file

@ -4,7 +4,8 @@ import hashlib
import json import json
import random import random
import time 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 secp256k1
import websockets.client as websockets import websockets.client as websockets

View file

@ -1,87 +1,81 @@
window.app = Vue.createApp({ window.app = Vue.createApp({
el: "#vue", el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
delimiters: ["${", "}"], delimiters: ['${', '}'],
data: function () { data: function () {
return { return {
entries: [], config: {},
columns: [ columns: [
{ {
name: "key", name: 'key',
required: true, required: true,
label: "Key", label: 'Key',
align: "left", align: 'left',
field: (row) => row.key, field: row => row.key,
sortable: true, sortable: true
}, },
{ {
name: "value", name: 'value',
required: true, required: true,
label: "Value", label: 'Value',
align: "left", align: 'left',
field: (row) => row.value, field: row => row.value,
sortable: true, sortable: true
}, }
], ]
}; }
}, },
methods: { methods: {
fetchConfig() { fetchConfig() {
this.entries = []; this.config = {}
LNbits.api LNbits.api
.request( .request(
"GET", 'GET',
"/nwcprovider/api/v1/config", '/nwcprovider/api/v1/config',
this.g.user.wallets[0].adminkey, this.g.user.wallets[0].adminkey
) )
.then((response) => { .then(response => {
const newEntries = []; this.config = response.data
for (const [key, value] of Object.entries(response.data)) { console.log('Config fetched:', this.config)
newEntries.push({
key: key,
value: value,
});
}
this.entries.length = 0;
this.entries.push(...newEntries);
}) })
.catch(function (error) { .catch(function (error) {
console.error("Error fetching config:", error); console.error('Error fetching config:', error)
}); })
}, },
async saveConfig() { async saveConfig() {
const data = {}; const data = {}
for (const entry of this.entries) { for (const [key, value] of Object.entries(this.config)) {
data[entry.key] = entry.value; data[key] = value
} }
console.log('Saving config:', data)
try { try {
const response = await LNbits.api.request( const response = await LNbits.api.request(
"POST", 'POST',
"/nwcprovider/api/v1/config", '/nwcprovider/api/v1/config',
this.g.user.wallets[0].adminkey, this.g.user.wallets[0].adminkey,
data, data
); )
Quasar.Notify.create({ Quasar.Notify.create({
type: "positive", type: 'positive',
message: "Config saved!", message: 'Config saved!'
}); })
Quasar.Notify.create({ Quasar.Notify.create({
type: "warning", type: 'warning',
message: 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) { } catch (error) {
Quasar.Notify.create({ Quasar.Notify.create({
type: "negative", type: 'negative',
message: "Error saving config: " + String(error), message: 'Error saving config: ' + String(error)
}); })
console.error("Error saving config:", error); console.error('Error saving config:', error)
}
} }
},
}, },
created: function () { created: function () {
this.fetchConfig(); this.fetchConfig()
}, }
}); })

View file

@ -1,7 +1,7 @@
window.app = Vue.createApp({ window.app = Vue.createApp({
el: "#vue", el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
delimiters: ["${", "}"], delimiters: ['${', '}'],
data: function () { data: function () {
return { return {
selectedWallet: null, selectedWallet: null,
@ -10,244 +10,244 @@ window.app = Vue.createApp({
nwcsTable: { nwcsTable: {
columns: [ columns: [
{ {
name: "description", name: 'description',
align: "left", align: 'left',
label: "Description", label: 'Description',
field: "description", field: 'description'
}, },
{ name: "status", align: "left", label: "Status", field: "status" }, {name: 'status', align: 'left', label: 'Status', field: 'status'},
{ {
name: "last_used", name: 'last_used',
align: "left", align: 'left',
label: "Last used", label: 'Last used',
field: "last_used", field: 'last_used'
}, },
{ {
name: "created_at", name: 'created_at',
align: "left", align: 'left',
label: "Created", label: 'Created',
field: "created_at", field: 'created_at'
}, },
{ {
name: "expires_at", name: 'expires_at',
align: "left", align: 'left',
label: "Expires", label: 'Expires',
field: "expires_at", field: 'expires_at'
}, }
], ],
pagination: { pagination: {
rowsPerPage: 10, rowsPerPage: 10
}, }
}, },
connectDialog: { connectDialog: {
show: false, show: false,
data: {}, data: {}
}, },
pairingDialog: { pairingDialog: {
show: false, show: false,
data: { data: {
pairingUrl: "", pairingUrl: ''
}, }
}, },
pairingQrDialog: { pairingQrDialog: {
show: false, show: false,
data: { data: {
pairingUrl: "", pairingUrl: ''
}, }
}, },
connectionInfoDialog: { connectionInfoDialog: {
show: false, show: false,
data: {}, data: {}
}, }
}; }
}, },
methods: { methods: {
showConnectDialog() { showConnectDialog() {
const wallet = this.getWallet(); const wallet = this.getWallet()
if (!wallet) { if (!wallet) {
Quasar.Notify.create({ Quasar.Notify.create({
type: "negative", type: 'negative',
message: "Please select a wallet first", message: 'Please select a wallet first'
}); })
return; return
} else { } else {
this.connectDialog.show = true; this.connectDialog.show = true
} }
}, },
openConnectionInfoDialog(data) { openConnectionInfoDialog(data) {
this.connectionInfoDialog.data = data; this.connectionInfoDialog.data = data
this.connectionInfoDialog.show = true; this.connectionInfoDialog.show = true
}, },
closeConnectionInfoDialog() { closeConnectionInfoDialog() {
this.connectionInfoDialog.show = false; this.connectionInfoDialog.show = false
}, },
openPairingUrl() { openPairingUrl() {
const url = this.pairingDialog.data.pairingUrl; const url = this.pairingDialog.data.pairingUrl
if (url) window.open(url, "_blank"); if (url) window.open(url, '_blank')
}, },
go(url) { go(url) {
window.open(url, "_blank"); window.open(url, '_blank')
}, },
async copyPairingUrl() { async copyPairingUrl() {
const url = this.pairingDialog.data.pairingUrl; const url = this.pairingDialog.data.pairingUrl
if (url) { if (url) {
try { try {
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url)
Quasar.Notify.create({ Quasar.Notify.create({
type: "positive", type: 'positive',
message: "URL copied to clipboard", message: 'URL copied to clipboard'
}); })
} catch (err) { } catch (err) {
Quasar.Notify.create({ Quasar.Notify.create({
type: "negative", type: 'negative',
message: "Failed to copy URL.", message: 'Failed to copy URL.'
}); })
} }
} }
}, },
showPairingQR() { showPairingQR() {
this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl; this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl
this.pairingQrDialog.show = true; this.pairingQrDialog.show = true
}, },
closePairingQrDialog() { closePairingQrDialog() {
this.pairingQrDialog.show = false; this.pairingQrDialog.show = false
}, },
loadConnectDialogData() { loadConnectDialogData() {
this.connectDialog.data = { this.connectDialog.data = {
description: "", description: '',
expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7, expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7,
neverExpires: true, neverExpires: true,
permissions: [], permissions: [],
budgets: [], budgets: []
}; }
for (const permission of this.nodePermissions) { for (const permission of this.nodePermissions) {
this.connectDialog.data.permissions.push({ this.connectDialog.data.permissions.push({
key: permission.key, key: permission.key,
name: permission.name, name: permission.name,
value: permission.value, value: permission.value
}); })
} }
}, },
deleteBudget(index) { deleteBudget(index) {
this.connectDialog.data.budgets.splice(index, 1); this.connectDialog.data.budgets.splice(index, 1)
}, },
addBudget() { addBudget() {
this.connectDialog.data.budgets.push({ this.connectDialog.data.budgets.push({
budget_sats: 1000, budget_sats: 1000,
used_budget_sats: 0, used_budget_sats: 0,
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,
expiration: "never", expiration: 'never'
}); })
}, },
closeConnectDialog() { closeConnectDialog() {
this.connectDialog.show = false; this.connectDialog.show = false
this.loadConnectDialogData(); this.loadConnectDialogData()
}, },
getWallet: function () { getWallet: function () {
let wallet = undefined; let wallet = undefined
for (let i = 0; i < this.g.user.wallets.length; i++) { for (let i = 0; i < this.g.user.wallets.length; i++) {
if (this.g.user.wallets[i].id == this.selectedWallet) { if (this.g.user.wallets[i].id == this.selectedWallet) {
wallet = this.g.user.wallets[i]; wallet = this.g.user.wallets[i]
break; break
} }
} }
return wallet; return wallet
}, },
async generateKeyPair() { async generateKeyPair() {
while (!window.NobleSecp256k1) { while (!window.NobleSecp256k1) {
await new Promise((resolve) => setTimeout(resolve, 1)); await new Promise(resolve => setTimeout(resolve, 1))
} }
const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey(); const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey()
const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes); const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes)
const out = { const out = {
privKeyBytes: privKeyBytes, privKeyBytes: privKeyBytes,
pubKeyBytes: pubKeyBytes, pubKeyBytes: pubKeyBytes,
privKey: window.NobleSecp256k1.etc.bytesToHex(privKeyBytes), privKey: window.NobleSecp256k1.etc.bytesToHex(privKeyBytes),
pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1)), pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1))
}; }
return out; return out
}, },
deleteNWC: async function (pubkey) { deleteNWC: async function (pubkey) {
Quasar.Dialog.create({ Quasar.Dialog.create({
title: "Confirm Deletion", title: 'Confirm Deletion',
message: "Are you sure you want to delete this connection?", message: 'Are you sure you want to delete this connection?',
cancel: true, cancel: true,
persistent: true, persistent: true
}) })
.onOk(async () => { .onOk(async () => {
try { try {
const wallet = this.getWallet(); const wallet = this.getWallet()
const response = await LNbits.api.request( const response = await LNbits.api.request(
"DELETE", 'DELETE',
`/nwcprovider/api/v1/nwc/${pubkey}`, `/nwcprovider/api/v1/nwc/${pubkey}`,
wallet.adminkey, wallet.adminkey
); )
this.loadNwcs(); this.loadNwcs()
Quasar.Notify.create({ Quasar.Notify.create({
type: "positive", type: 'positive',
message: "Deleted successfully", message: 'Deleted successfully'
}); })
} catch (error) { } catch (error) {
LNbits.utils.notifyApiError(error); LNbits.utils.notifyApiError(error)
} }
}) })
.onCancel(() => { .onCancel(() => {
// User canceled the operation // User canceled the operation
}); })
}, },
loadNwcs: async function () { loadNwcs: async function () {
const wallet = this.getWallet(); const wallet = this.getWallet()
if (!wallet) { if (!wallet) {
this.nwcs = []; this.nwcs = []
return; return
} }
try { try {
const response = await LNbits.api.request( const response = await LNbits.api.request(
"GET", 'GET',
"/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true", '/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true',
wallet.adminkey, wallet.adminkey
); )
this.nwcs = response.data; this.nwcs = response.data
} catch (error) { } catch (error) {
this.nwcs = []; this.nwcs = []
} }
try { try {
const response = await LNbits.api.request( const response = await LNbits.api.request(
"GET", 'GET',
"/nwcprovider/api/v1/permissions", '/nwcprovider/api/v1/permissions',
wallet.adminkey, wallet.adminkey
); )
const permissions = []; const permissions = []
for (const [key, value] of Object.entries(response.data)) { for (const [key, value] of Object.entries(response.data)) {
permissions.push({ permissions.push({
key: key, key: key,
name: value.name, name: value.name,
value: value.default, value: value.default
}); })
} }
this.nodePermissions = permissions; this.nodePermissions = permissions
} catch (error) { } catch (error) {
Lnbits.utils.notifyApiError(error); Lnbits.utils.notifyApiError(error)
} }
this.loadConnectDialogData(); this.loadConnectDialogData()
const newTableEntries = []; const newTableEntries = []
for (const nwc of this.nwcs) { for (const nwc of this.nwcs) {
const t = Quasar.date.formatDate( const t = Quasar.date.formatDate(
new Date(nwc.data.created_at * 1000), new Date(nwc.data.created_at * 1000),
"YYYY-MM-DD HH:mm", 'YYYY-MM-DD HH:mm'
); )
const e = const e =
nwc.data.expires_at > 0 nwc.data.expires_at > 0
? Quasar.date.formatDate( ? Quasar.date.formatDate(
new Date(nwc.data.expires_at * 1000), new Date(nwc.data.expires_at * 1000),
"YYYY-MM-DD HH:mm", 'YYYY-MM-DD HH:mm'
) )
: "Never"; : 'Never'
const l = Quasar.date.formatDate( const l = Quasar.date.formatDate(
new Date(nwc.data.last_used * 1000), new Date(nwc.data.last_used * 1000),
"YYYY-MM-DD HH:mm", 'YYYY-MM-DD HH:mm'
); )
const nwcTableEntry = { const nwcTableEntry = {
description: nwc.data.description, description: nwc.data.description,
created_at: t, created_at: t,
@ -256,131 +256,130 @@ window.app = Vue.createApp({
pubkey: nwc.data.pubkey, pubkey: nwc.data.pubkey,
permissions: nwc.data.permissions, permissions: nwc.data.permissions,
budgets: [], budgets: [],
status: "Active", status: 'Active'
}; }
if ( if (
nwc.data.expires_at > 0 && nwc.data.expires_at > 0 &&
nwc.data.expires_at < new Date().getTime() / 1000 nwc.data.expires_at < new Date().getTime() / 1000
) { ) {
nwcTableEntry.status = "Expired"; nwcTableEntry.status = 'Expired'
} }
for (const budget of nwc.budgets) { for (const budget of nwc.budgets) {
const createdAt = Quasar.date.formatDate( const createdAt = Quasar.date.formatDate(
new Date(budget.created_at * 1000), new Date(budget.created_at * 1000),
"YYYY-MM-DD HH:mm", 'YYYY-MM-DD HH:mm'
); )
let refreshWindow = budget.refresh_window; let refreshWindow = budget.refresh_window
if (refreshWindow <= 0) { if (refreshWindow <= 0) {
refreshWindow = "Never"; refreshWindow = 'Never'
} else if (refreshWindow == 60 * 60 * 24) { } else if (refreshWindow == 60 * 60 * 24) {
refreshWindow = "Daily"; refreshWindow = 'Daily'
} else if (refreshWindow == 60 * 60 * 24 * 7) { } else if (refreshWindow == 60 * 60 * 24 * 7) {
refreshWindow = "Weekly"; refreshWindow = 'Weekly'
} else if (refreshWindow == 60 * 60 * 24 * 30) { } else if (refreshWindow == 60 * 60 * 24 * 30) {
refreshWindow = "Monthly"; refreshWindow = 'Monthly'
} else if (refreshWindow == 60 * 60 * 24 * 365) { } else if (refreshWindow == 60 * 60 * 24 * 365) {
refreshWindow = "Yearly"; refreshWindow = 'Yearly'
} }
nwcTableEntry.budgets.push({ nwcTableEntry.budgets.push({
budget_sats: budget.budget_msats / 1000, budget_sats: budget.budget_msats / 1000,
used_budget_sats: budget.used_budget_msats / 1000, used_budget_sats: budget.used_budget_msats / 1000,
created_at: createdAt, created_at: createdAt,
refresh_window: refreshWindow, refresh_window: refreshWindow
}); })
} }
newTableEntries.push(nwcTableEntry); newTableEntries.push(nwcTableEntry)
} }
this.nwcEntries = newTableEntries; this.nwcEntries = newTableEntries
}, },
closePairingDialog() { closePairingDialog() {
this.pairingDialog.show = false; this.pairingDialog.show = false
}, },
async showPairingDialog(secret) { async showPairingDialog(secret) {
let response = await LNbits.api.request( let response = await LNbits.api.request(
"GET", 'GET',
"/nwcprovider/api/v1/pairing/{SECRET}", '/nwcprovider/api/v1/pairing/{SECRET}'
); )
response = response.data; response = response.data
response = response.replace("{SECRET}", secret); response = response.replace('{SECRET}', secret)
this.pairingDialog.data.pairingUrl = response; this.pairingDialog.data.pairingUrl = response
this.pairingDialog.show = true; this.pairingDialog.show = true
}, },
async confirmConnectDialog() { async confirmConnectDialog() {
const keyPair = await this.generateKeyPair(); const keyPair = await this.generateKeyPair()
// timestamp // timestamp
let expires_at = 0; let expires_at = 0
if (!this.connectDialog.data.neverExpires) { if (!this.connectDialog.data.neverExpires) {
expires_at = expires_at =
new Date(this.connectDialog.data.expires_at).getTime() / 1000; new Date(this.connectDialog.data.expires_at).getTime() / 1000
} }
const data = { const data = {
permissions: [], permissions: [],
description: this.connectDialog.data.description, description: this.connectDialog.data.description,
expires_at: expires_at, expires_at: expires_at,
budgets: [], budgets: []
}; }
for (const permission of this.connectDialog.data.permissions) { 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) { for (const budget of this.connectDialog.data.budgets) {
const budget_msats = budget.budget_sats * 1000; const budget_msats = budget.budget_sats * 1000
let refresh_window = 0; let refresh_window = 0
switch (budget.expiry) { switch (budget.expiry) {
case "Daily": case 'Daily':
refresh_window = 60 * 60 * 24; refresh_window = 60 * 60 * 24
break; break
case "Weekly": case 'Weekly':
refresh_window = 60 * 60 * 24 * 7; refresh_window = 60 * 60 * 24 * 7
break; break
case "Monthly": case 'Monthly':
refresh_window = 60 * 60 * 24 * 30; refresh_window = 60 * 60 * 24 * 30
break; break
case "Yearly": case 'Yearly':
refresh_window = 60 * 60 * 24 * 365; refresh_window = 60 * 60 * 24 * 365
break; break
case "Never": case 'Never':
refresh_window = 0; refresh_window = 0
break; break
} }
data.budgets.push({ data.budgets.push({
budget_msats: budget_msats, budget_msats: budget_msats,
refresh_window: refresh_window, refresh_window: refresh_window,
created_at: created_at: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000
new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000, })
});
} }
const wallet = this.getWallet(); const wallet = this.getWallet()
try { try {
const response = await LNbits.api.request( const response = await LNbits.api.request(
"PUT", 'PUT',
"/nwcprovider/api/v1/nwc/" + keyPair.pubKey, '/nwcprovider/api/v1/nwc/' + keyPair.pubKey,
wallet.adminkey, wallet.adminkey,
data, data
); )
this.closeConnectDialog(); this.closeConnectDialog()
if ( if (
!response.data || !response.data ||
!response.data.data || !response.data.data ||
!response.data.data.pubkey !response.data.data.pubkey
) { ) {
LNbits.utils.notifyApiError("Error creating nwc pairing"); LNbits.utils.notifyApiError('Error creating nwc pairing')
return; return
} }
this.showPairingDialog(keyPair.privKey); this.showPairingDialog(keyPair.privKey)
} catch (error) { } catch (error) {
LNbits.utils.notifyApiError(error); LNbits.utils.notifyApiError(error)
}
this.loadNwcs()
} }
this.loadNwcs();
},
}, },
created: function () { created: function () {
this.loadNwcs(); this.loadNwcs()
}, },
watch: { watch: {
selectedWallet(newValue, oldValue) { selectedWallet(newValue, oldValue) {
this.loadNwcs(); this.loadNwcs()
}, }
}, }
}); })

View file

@ -4,93 +4,93 @@ const B256 = 2n ** 256n,
N = B256 - 0x14551231950b75fc4402da1732fc9bebfn, N = B256 - 0x14551231950b75fc4402da1732fc9bebfn,
Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n, 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, fLen = 32,
crv = (t) => mod(mod(t * t) * t + CURVE.b), crv = t => mod(mod(t * t) * t + CURVE.b),
err = (t = "") => { err = (t = '') => {
throw new Error(t); throw new Error(t)
}, },
big = (t) => "bigint" == typeof t, big = t => 'bigint' == typeof t,
str = (t) => "string" == typeof t, str = t => 'string' == typeof t,
fe = (t) => big(t) && 0n < t && t < P, fe = t => big(t) && 0n < t && t < P,
ge = (t) => big(t) && 0n < t && t < N, ge = t => big(t) && 0n < t && t < N,
isu8 = (t) => isu8 = t =>
t instanceof Uint8Array || t instanceof Uint8Array ||
(null != t && "object" == typeof t && "Uint8Array" === t.constructor.name), (null != t && 'object' == typeof t && 'Uint8Array' === t.constructor.name),
au8 = (t, n) => au8 = (t, n) =>
!isu8(t) || ("number" == typeof n && n > 0 && t.length !== n) !isu8(t) || ('number' == typeof n && n > 0 && t.length !== n)
? err("Uint8Array expected") ? err('Uint8Array expected')
: t, : t,
u8n = (t) => new Uint8Array(t), u8n = t => new Uint8Array(t),
toU8 = (t, n) => au8(str(t) ? h2b(t) : u8n(au8(t)), n), toU8 = (t, n) => au8(str(t) ? h2b(t) : u8n(au8(t)), n),
mod = (t, n = P) => { mod = (t, n = P) => {
let e = t % n; let e = t % n
return e >= 0n ? e : n + e; 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 { class Point {
constructor(t, n, e) { 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) { 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) { static fromHex(t) {
let n; let n
const e = (t = toU8(t))[0], const e = (t = toU8(t))[0],
r = t.subarray(1), r = t.subarray(1),
o = slcNum(r, 0, 32), o = slcNum(r, 0, 32),
i = t.length; i = t.length
if (33 === i && [2, 3].includes(e)) { if (33 === i && [2, 3].includes(e)) {
fe(o) || err("Point hex invalid: x not FE"); fe(o) || err('Point hex invalid: x not FE')
let t = sqrt(crv(o)); let t = sqrt(crv(o))
!(1 & ~e) !== (1n === (1n & t)) && (t = mod(-t)), ;(!(1 & ~e) !== (1n === (1n & t)) && (t = mod(-t)),
(n = new Point(o, t, 1n)); (n = new Point(o, t, 1n)))
} }
return ( return (
65 === i && 4 === e && (n = new Point(o, slcNum(r, 32, 64), 1n)), 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) { static fromPrivateKey(t) {
return G.mul(toPriv(t)); return G.mul(toPriv(t))
} }
get x() { get x() {
return this.aff().x; return this.aff().x
} }
get y() { get y() {
return this.aff().y; return this.aff().y
} }
equals(t) { equals(t) {
const { px: n, py: e, pz: r } = this, const {px: n, py: e, pz: r} = this,
{ px: o, py: i, pz: s } = isPoint(t), {px: o, py: i, pz: s} = isPoint(t),
a = mod(n * s), a = mod(n * s),
c = mod(o * r), c = mod(o * r),
d = mod(e * s), d = mod(e * s),
u = mod(i * r); u = mod(i * r)
return a === c && d === u; return a === c && d === u
} }
negate() { negate() {
return new Point(this.px, mod(-this.py), this.pz); return new Point(this.px, mod(-this.py), this.pz)
} }
double() { double() {
return this.add(this); return this.add(this)
} }
add(t) { add(t) {
const { px: n, py: e, pz: r } = this, const {px: n, py: e, pz: r} = this,
{ px: o, py: i, pz: s } = isPoint(t), {px: o, py: i, pz: s} = isPoint(t),
{ a, b: c } = CURVE; {a, b: c} = CURVE
let d = 0n, let d = 0n,
u = 0n, u = 0n,
m = 0n; m = 0n
const h = mod(3n * c); const h = mod(3n * c)
let l = mod(n * o), let l = mod(n * o),
y = mod(e * i), y = mod(e * i),
f = mod(r * s), f = mod(r * s),
p = mod(n + e), p = mod(n + e),
b = mod(o + i); b = mod(o + i)
(p = mod(p * b)), (b = mod(l + y)), (p = mod(p - b)), (b = mod(n + r)); ;((p = mod(p * b)), (b = mod(l + y)), (p = mod(p - b)), (b = mod(n + r)))
let g = mod(o + s); let g = mod(o + s)
return ( return (
(b = mod(b * g)), (b = mod(b * g)),
(g = mod(l + f)), (g = mod(l + f)),
@ -123,300 +123,301 @@ class Point {
(m = mod(g * m)), (m = mod(g * m)),
(m = mod(m + l)), (m = mod(m + l)),
new Point(d, u, m) new Point(d, u, m)
); )
} }
mul(t, n = !0) { mul(t, n = !0) {
if (!n && 0n === t) return I; if (!n && 0n === t) return I
if ((ge(t) || err("invalid scalar"), this.equals(G))) return wNAF(t).p; if ((ge(t) || err('invalid scalar'), this.equals(G))) return wNAF(t).p
let e = I, let e = I,
r = G; r = G
for (let o = this; t > 0n; o = o.double(), t >>= 1n) for (let o = this; t > 0n; o = o.double(), t >>= 1n)
1n & t ? (e = e.add(o)) : n && (r = r.add(o)); 1n & t ? (e = e.add(o)) : n && (r = r.add(o))
return e; return e
} }
mulAddQUns(t, n, 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() { toAffine() {
const { px: t, py: n, pz: e } = this; const {px: t, py: n, pz: e} = this
if (this.equals(I)) return { x: 0n, y: 0n }; if (this.equals(I)) return {x: 0n, y: 0n}
if (1n === e) return { x: t, y: n }; if (1n === e) return {x: t, y: n}
const r = inv(e); const r = inv(e)
return ( return (
1n !== mod(e * r) && err("invalid inverse"), 1n !== mod(e * r) && err('invalid inverse'),
{ x: mod(t * r), y: mod(n * r) } {x: mod(t * r), y: mod(n * r)}
); )
} }
assertValidity() { assertValidity() {
const { x: t, y: n } = this.aff(); const {x: t, y: n} = this.aff()
return ( return (
(fe(t) && fe(n)) || err("Point invalid: x or y"), (fe(t) && fe(n)) || err('Point invalid: x or y'),
mod(n * n) === crv(t) ? this : err("Point invalid: not on curve") mod(n * n) === crv(t) ? this : err('Point invalid: not on curve')
); )
} }
multiply(t) { multiply(t) {
return this.mul(t); return this.mul(t)
} }
aff() { aff() {
return this.toAffine(); return this.toAffine()
} }
ok() { ok() {
return this.assertValidity(); return this.assertValidity()
} }
toHex(t = !0) { toHex(t = !0) {
const { x: n, y: e } = this.aff(); const {x: n, y: e} = this.aff()
return ( 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) { 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)); ;((Point.BASE = new Point(Gx, Gy, 1n)), (Point.ZERO = new Point(0n, 1n, 0n)))
const { BASE: G, ZERO: I } = Point, const {BASE: G, ZERO: I} = Point,
padh = (t, n) => t.toString(16).padStart(n, "0"), padh = (t, n) => t.toString(16).padStart(n, '0'),
b2h = (t) => b2h = t =>
Array.from(t) Array.from(t)
.map((t) => padh(t, 2)) .map(t => padh(t, 2))
.join(""), .join(''),
h2b = (t) => { h2b = t => {
const n = t.length; const n = t.length
(!str(t) || n % 2) && err("hex invalid 1"); ;(!str(t) || n % 2) && err('hex invalid 1')
const e = u8n(n / 2); const e = u8n(n / 2)
for (let n = 0; n < e.length; n++) { for (let n = 0; n < e.length; n++) {
const r = 2 * n, const r = 2 * n,
o = t.slice(r, r + 2), o = t.slice(r, r + 2),
i = Number.parseInt(o, 16); i = Number.parseInt(o, 16)
(Number.isNaN(i) || i < 0) && err("hex invalid 2"), (e[n] = i); ;((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)), slcNum = (t, n, e) => b2n(t.slice(n, e)),
n2b = (t) => n2b = t =>
big(t) && t >= 0n && t < B256 ? h2b(padh(t, 64)) : err("bigint expected"), big(t) && t >= 0n && t < B256 ? h2b(padh(t, 64)) : err('bigint expected'),
n2h = (t) => b2h(n2b(t)), n2h = t => b2h(n2b(t)),
concatB = (...t) => { concatB = (...t) => {
const n = u8n(t.reduce((t, n) => t + au8(n).length, 0)); const n = u8n(t.reduce((t, n) => t + au8(n).length, 0))
let e = 0; let e = 0
return ( return (
t.forEach((t) => { t.forEach(t => {
n.set(t, e), (e += t.length); ;(n.set(t, e), (e += t.length))
}), }),
n n
); )
}, },
inv = (t, n = P) => { 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), let e = mod(t, n),
r = n, r = n,
o = 0n, o = 0n,
i = 1n, i = 1n,
s = 1n, s = 1n,
a = 0n; a = 0n
for (; 0n !== e; ) { for (; 0n !== e; ) {
const t = r / e, const t = r / e,
n = r % e, n = r % e,
c = o - s * t, c = o - s * t,
d = i - a * t; d = i - a * t
(r = e), (e = n), (o = s), (i = a), (s = c), (a = d); ;((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) => { sqrt = t => {
let n = 1n; let n = 1n
for (let e = t, r = (P + 1n) / 4n; r > 0n; r >>= 1n) for (let e = t, r = (P + 1n) / 4n; r > 0n; r >>= 1n)
1n & r && (n = (n * e) % P), (e = (e * e) % P); (1n & r && (n = (n * e) % P), (e = (e * e) % P))
return mod(n * n) === t ? n : err("sqrt invalid"); return mod(n * n) === t ? n : err('sqrt invalid')
}, },
toPriv = (t) => ( toPriv = t => (
big(t) || (t = b2n(toU8(t, 32))), 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, moreThanHalfN = t => t > N >> 1n,
getPublicKey = (t, n = !0) => Point.fromPrivateKey(t).toRawBytes(n); getPublicKey = (t, n = !0) => Point.fromPrivateKey(t).toRawBytes(n)
class Signature { class Signature {
constructor(t, n, e) { 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) { static fromCompact(t) {
return ( 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() { assertValidity() {
return ge(this.r) && ge(this.s) ? this : err(); return ge(this.r) && ge(this.s) ? this : err()
} }
addRecoveryBit(t) { addRecoveryBit(t) {
return new Signature(this.r, this.s, t); return new Signature(this.r, this.s, t)
} }
hasHighS() { hasHighS() {
return moreThanHalfN(this.s); return moreThanHalfN(this.s)
} }
normalizeS() { normalizeS() {
return this.hasHighS() return this.hasHighS()
? new Signature(this.r, mod(this.s, N), this.recovery) ? new Signature(this.r, mod(this.s, N), this.recovery)
: this; : this
} }
recoverPublicKey(t) { recoverPublicKey(t) {
const { r: n, s: e, recovery: r } = this; const {r: n, s: e, recovery: r} = this
[0, 1, 2, 3].includes(r) || err("recovery id invalid"); ;[0, 1, 2, 3].includes(r) || err('recovery id invalid')
const o = bits2int_modN(toU8(t, 32)), const o = bits2int_modN(toU8(t, 32)),
i = 2 === r || 3 === r ? n + N : n; i = 2 === r || 3 === r ? n + N : n
i >= P && err("q.x invalid"); i >= P && err('q.x invalid')
const s = 1 & r ? "03" : "02", const s = 1 & r ? '03' : '02',
a = Point.fromHex(s + n2h(i)), a = Point.fromHex(s + n2h(i)),
c = inv(i, N), c = inv(i, N),
d = mod(-o * c, N), d = mod(-o * c, N),
u = mod(e * c, N); u = mod(e * c, N)
return G.mulAddQUns(a, d, u); return G.mulAddQUns(a, d, u)
} }
toCompactRawBytes() { toCompactRawBytes() {
return h2b(this.toCompactHex()); return h2b(this.toCompactHex())
} }
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, const n = 8 * t.length - 256,
e = b2n(t); e = b2n(t)
return n > 0 ? e >> BigInt(n) : e; return n > 0 ? e >> BigInt(n) : e
}, },
bits2int_modN = (t) => mod(bits2int(t), N), bits2int_modN = t => mod(bits2int(t), N),
i2o = (t) => n2b(t), i2o = t => n2b(t),
cr = () => cr = () =>
"object" == typeof globalThis && "crypto" in globalThis 'object' == typeof globalThis && 'crypto' in globalThis
? globalThis.crypto ? globalThis.crypto
: void 0; : void 0
let _hmacSync; let _hmacSync
const optS = { lowS: !0 }, const optS = {lowS: !0},
optV = { lowS: !0 }, optV = {lowS: !0},
prepSig = (t, n, e = optS) => { prepSig = (t, n, e = optS) => {
["der", "recovered", "canonical"].some((t) => t in e) && ;['der', 'recovered', 'canonical'].some(t => t in e) &&
err("sign() legacy options not supported"); err('sign() legacy options not supported')
let { lowS: r } = e; let {lowS: r} = e
null == r && (r = !0); null == r && (r = !0)
const o = bits2int_modN(toU8(t)), const o = bits2int_modN(toU8(t)),
i = i2o(o), i = i2o(o),
s = toPriv(n), s = toPriv(n),
a = [i2o(s), i]; a = [i2o(s), i]
let c = e.extraEntropy; let c = e.extraEntropy
if (c) { if (c) {
!0 === c && (c = etc.randomBytes(32)); !0 === c && (c = etc.randomBytes(32))
const t = toU8(c); const t = toU8(c)
32 !== t.length && err(), a.push(t); ;(32 !== t.length && err(), a.push(t))
} }
const d = o; const d = o
return { return {
seed: concatB(...a), seed: concatB(...a),
k2sig: (t) => { k2sig: t => {
const n = bits2int(t); const n = bits2int(t)
if (!ge(n)) return; if (!ge(n)) return
const e = inv(n, N), const e = inv(n, N),
o = G.mul(n).aff(), o = G.mul(n).aff(),
i = mod(o.x, N); i = mod(o.x, N)
if (0n === i) return; if (0n === i) return
const a = mod(e * mod(d + mod(s * i, N), N), N); const a = mod(e * mod(d + mod(s * i, N), N), N)
if (0n === a) return; if (0n === a) return
let c = a, let c = a,
u = (o.x === i ? 0 : 2) | Number(1n & o.y); u = (o.x === i ? 0 : 2) | Number(1n & o.y)
return ( return (
r && moreThanHalfN(a) && ((c = mod(-a, N)), (u ^= 1)), r && moreThanHalfN(a) && ((c = mod(-a, N)), (u ^= 1)),
new Signature(i, c, u) new Signature(i, c, u)
); )
}, }
}; }
}; }
function hmacDrbg(t) { function hmacDrbg(t) {
let n = u8n(32), let n = u8n(32),
e = u8n(32), e = u8n(32),
r = 0; r = 0
const o = () => { 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) { if (t) {
const t = (...t) => etc.hmacSha256Async(e, n, ...t), const t = (...t) => etc.hmacSha256Async(e, n, ...t),
s = async (r = u8n()) => { s = async (r = u8n()) => {
(e = await t(u8n([0]), r)), ;((e = await t(u8n([0]), r)),
(n = await t()), (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) => { return async (t, n) => {
let e; let e
for (o(), await s(t); !(e = n(await a())); ) await s(); for (o(), await s(t); !(e = n(await a())); ) await s()
return o(), e; return (o(), e)
}; }
} }
{ {
const t = (...t) => { const t = (...t) => {
const r = _hmacSync; const r = _hmacSync
return r || err("etc.hmacSha256Sync not set"), r(e, n, ...t); return (r || err('etc.hmacSha256Sync not set'), r(e, n, ...t))
}, },
s = (r = u8n()) => { s = (r = u8n()) => {
(e = t(u8n([0]), r)), ;((e = t(u8n([0]), r)),
(n = t()), (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) => { return (t, n) => {
let e; let e
for (o(), s(t); !(e = n(a())); ) s(); for (o(), s(t); !(e = n(a())); ) s()
return o(), e; return (o(), e)
}; }
} }
} }
const signAsync = async (t, n, e = optS) => { const signAsync = async (t, n, e = optS) => {
const { seed: r, k2sig: o } = prepSig(t, n, e); const {seed: r, k2sig: o} = prepSig(t, n, e)
return hmacDrbg(!0)(r, o); return hmacDrbg(!0)(r, o)
}, },
sign = (t, n, e = optS) => { sign = (t, n, e = optS) => {
const { seed: r, k2sig: o } = prepSig(t, n, e); const {seed: r, k2sig: o} = prepSig(t, n, e)
return hmacDrbg(!1)(r, o); return hmacDrbg(!1)(r, o)
}, },
verify = (t, n, e, r = optV) => { verify = (t, n, e, r = optV) => {
let o, let o,
i, i,
s, s,
{ lowS: a } = r; {lowS: a} = r
null == a && (a = !0), ;(null == a && (a = !0),
"strict" in r && err("verify() legacy options not supported"); 'strict' in r && err('verify() legacy options not supported'))
const c = t && "object" == typeof t && "r" in t; const c = t && 'object' == typeof t && 'r' in t
c || 64 === toU8(t).length || err("signature must be 64 bytes"); c || 64 === toU8(t).length || err('signature must be 64 bytes')
try { try {
(o = c ;((o = c
? new Signature(t.r, t.s).assertValidity() ? new Signature(t.r, t.s).assertValidity()
: Signature.fromCompact(t)), : Signature.fromCompact(t)),
(i = bits2int_modN(toU8(n))), (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) { } catch (t) {
return !1; return !1
} }
if (!o) return !1; if (!o) return !1
const { r: d, s: u } = o; const {r: d, s: u} = o
if (a && moreThanHalfN(u)) return !1; if (a && moreThanHalfN(u)) return !1
let m; let m
try { try {
const t = inv(u, N), const t = inv(u, N),
n = mod(i * t, N), n = mod(i * t, N),
e = mod(d * t, N); e = mod(d * t, N)
m = G.mulAddQUns(s, n, e).aff(); m = G.mulAddQUns(s, n, e).aff()
} catch (t) { } catch (t) {
return !1; return !1
} }
if (!m) return !1; if (!m) return !1
return mod(m.x, N) === d; return mod(m.x, N) === d
}, },
getSharedSecret = (t, n, e = !0) => getSharedSecret = (t, n, e = !0) =>
Point.fromHex(n).mul(toPriv(t)).toRawBytes(e), Point.fromHex(n).mul(toPriv(t)).toRawBytes(e),
hashToPrivateKey = (t) => { hashToPrivateKey = t => {
((t = toU8(t)).length < 40 || t.length > 1024) && ;((t = toU8(t)).length < 40 || t.length > 1024) &&
err("expected proper params"); err('expected proper params')
const n = mod(b2n(t), N - 1n) + 1n; const n = mod(b2n(t), N - 1n) + 1n
return n2b(n); return n2b(n)
}, },
etc = { etc = {
hexToBytes: h2b, hexToBytes: h2b,
@ -428,84 +429,84 @@ const signAsync = async (t, n, e = optS) => {
invert: inv, invert: inv,
hmacSha256Async: async (t, ...n) => { hmacSha256Async: async (t, ...n) => {
const e = cr(), const e = cr(),
r = e && e.subtle; r = e && e.subtle
if (!r) return err("etc.hmacSha256Async not set"); if (!r) return err('etc.hmacSha256Async not set')
const o = await r.importKey( const o = await r.importKey(
"raw", 'raw',
t, t,
{ name: "HMAC", hash: { name: "SHA-256" } }, {name: 'HMAC', hash: {name: 'SHA-256'}},
!1, !1,
["sign"], ['sign']
); )
return u8n(await r.sign("HMAC", o, concatB(...n))); return u8n(await r.sign('HMAC', o, concatB(...n)))
}, },
hmacSha256Sync: _hmacSync, hmacSha256Sync: _hmacSync,
hashToPrivateKey, hashToPrivateKey,
randomBytes: (t = 32) => { randomBytes: (t = 32) => {
const n = cr(); const n = cr()
return ( return (
(n && n.getRandomValues) || (n && n.getRandomValues) ||
err("crypto.getRandomValues must be defined"), err('crypto.getRandomValues must be defined'),
n.getRandomValues(u8n(t)) n.getRandomValues(u8n(t))
); )
}, }
}, },
utils = { utils = {
normPrivateKeyToScalar: toPriv, normPrivateKeyToScalar: toPriv,
isValidPrivateKey: (t) => { isValidPrivateKey: t => {
try { try {
return !!toPriv(t); return !!toPriv(t)
} catch (t) { } catch (t) {
return !1; return !1
} }
}, },
randomPrivateKey: () => hashToPrivateKey(etc.randomBytes(48)), 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, { Object.defineProperties(etc, {
hmacSha256Sync: { hmacSha256Sync: {
configurable: !1, configurable: !1,
get: () => _hmacSync, get: () => _hmacSync,
set(t) { set(t) {
_hmacSync || (_hmacSync = t); _hmacSync || (_hmacSync = t)
}, }
}, }
}); })
const W = 8, const W = 8,
precompute = () => { precompute = () => {
const t = []; const t = []
let n = G, let n = G,
e = n; e = n
for (let r = 0; r < 33; r++) { for (let r = 0; r < 33; r++) {
(e = n), t.push(e); ;((e = n), t.push(e))
for (let r = 1; r < 128; r++) (e = e.add(n)), t.push(e); for (let r = 1; r < 128; r++) ((e = e.add(n)), t.push(e))
n = e.double(); n = e.double()
} }
return t; return t
}; }
let Gpows; let Gpows
const wNAF = (t) => { const wNAF = t => {
const n = Gpows || (Gpows = precompute()), const n = Gpows || (Gpows = precompute()),
e = (t, n) => { e = (t, n) => {
let e = n.negate(); let e = n.negate()
return t ? e : n; return t ? e : n
}; }
let r = I, let r = I,
o = G; o = G
const i = BigInt(255), const i = BigInt(255),
s = BigInt(8); s = BigInt(8)
for (let a = 0; a < 33; a++) { for (let a = 0; a < 33; a++) {
const c = 128 * a; const c = 128 * a
let d = Number(t & i); let d = Number(t & i)
(t >>= s), d > 128 && ((d -= 256), (t += 1n)); ;((t >>= s), d > 128 && ((d -= 256), (t += 1n)))
const u = c, const u = c,
m = c + Math.abs(d) - 1, m = c + Math.abs(d) - 1,
h = a % 2 != 0, h = a % 2 != 0,
l = d < 0; l = d < 0
0 === d ? (o = o.add(e(h, n[u]))) : (r = r.add(e(l, n[m]))); 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 { export {
getPublicKey, getPublicKey,
sign, sign,
@ -516,5 +517,5 @@ export {
etc, etc,
utils, utils,
Point as ProjectivePoint, Point as ProjectivePoint,
Signature, Signature
}; }

View file

@ -6,21 +6,56 @@
<div class="col-12 q-gutter-y-md"> <div class="col-12 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="row items-center no-wrap q-mb-md"> <div class="row items-center wrap q-mb-md">
<div class="col"> <div class="col">
<h5 class="text-subtitle1 q-my-none"> <h5 class="text-subtitle1 q-my-none">
NWC Service Provider - Config NWC Service Provider Configuration
</h5> </h5>
</div> </div>
</div> </div>
<q-markup-table flat> <q-markup-table flat wrap-cells="true">
<tbody> <tbody>
<q-tr v-for="entry in entries" :key="entry.key"> <q-tr>
<q-td> <q-td>
<span> ${entry.key} </span> <q-input
v-model="config.relay"
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'"
>
</q-input>
</q-td> </q-td>
</q-tr>
<q-tr>
<q-td> <q-td>
<q-input v-model="entry.value" /> <q-input
filled
label="Relay Alias"
v-model="config.relay_alias"
:hint="'Relay URL to display in pairing URLs. If your relay has a different public URL than the one set in \'Nostr Relay URL\' set it here.'"
/>
</q-td>
</q-tr>
<q-tr>
<q-td>
<q-input
filled
label="NWC Provider Secret Key "
v-model="config.provider_key"
:hint="'The secret key for the NWC Service Provider. You don\'t need to change this unless your key has been compromised.'"
/>
</q-td>
</q-tr>
<q-tr>
<q-td>
<q-input
filled
label="Time Period To Handle Missed Events"
v-model="config.handle_missed_events"
type="number"
:hint="'Number of seconds to look back for processing events missed while offline. Setting it to 0 disables this functionality.'"
/>
</q-td> </q-td>
</q-tr> </q-tr>
</tbody> </tbody>

View file

@ -1,8 +1,8 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }} %} {% block scripts %} {{ window_vars(user) }}
<script type="module"> <script type="module">
import * as NobleSecp256k1 from "/nwcprovider/static/js/noble-secp256k1.min.js"; import * as NobleSecp256k1 from '/nwcprovider/static/js/noble-secp256k1.min.js'
window.NobleSecp256k1 = NobleSecp256k1; window.NobleSecp256k1 = NobleSecp256k1
</script> </script>
<script src="{{ static_url_for('nwcprovider/static', path='js/index.js') }}"></script> <script src="{{ static_url_for('nwcprovider/static', path='js/index.js') }}"></script>
{% endblock %} {% block page %} {% endblock %} {% block page %}

View file

@ -997,14 +997,23 @@ async def create_valid_invoice(wallet, amount=1000):
raise Exception(f"Failed to create invoice: {error}") raise Exception(f"Failed to create invoice: {error}")
return result["invoice"] return result["invoice"]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_list_transactions(): async def test_list_transactions():
# Create wallets with required permissions # Create wallets with required permissions
nwc1 = await create_nwc( 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( 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"]) wallet1 = NWCWallet(nwc1["pairing"])