Revert "Improve configuration form for readability and UX"

This commit is contained in:
Arc 2025-09-11 05:15:23 +01:00 • committed by GitHub
commit 93ba3ba628
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 512 additions and 552 deletions

View file

@ -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

View file

@ -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

View file

@ -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: |

View file

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

View file

@ -87,13 +87,12 @@ 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):
"""

View file

@ -4,8 +4,7 @@ import hashlib
import json
import random
import time
from collections.abc import Awaitable
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union
import secp256k1
import websockets.client as websockets

View file

@ -1,81 +1,87 @@
window.app = Vue.createApp({
el: '#vue',
el: "#vue",
mixins: [windowMixin],
delimiters: ['${', '}'],
delimiters: ["${", "}"],
data: function () {
return {
config: {},
entries: [],
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.config = {}
this.entries = [];
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 => {
this.config = response.data
console.log('Config fetched:', this.config)
.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);
})
.catch(function (error) {
console.error('Error fetching config:', error)
})
console.error("Error fetching config:", error);
});
},
async saveConfig() {
const data = {}
for (const [key, value] of Object.entries(this.config)) {
data[key] = value
const data = {};
for (const entry of this.entries) {
data[entry.key] = entry.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();
},
});

View file

@ -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,130 +256,131 @@ 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();
},
},
});

View file

@ -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,301 +123,300 @@ 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,
@ -429,84 +428,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,
@ -517,5 +516,5 @@ export {
etc,
utils,
Point as ProjectivePoint,
Signature
}
Signature,
};

View file

@ -6,56 +6,21 @@
<div class="col-12 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center wrap q-mb-md">
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">
NWC Service Provider Configuration
NWC Service Provider - Config
</h5>
</div>
</div>
<q-markup-table flat wrap-cells="true">
<q-markup-table flat>
<tbody>
<q-tr>
<q-tr v-for="entry in entries" :key="entry.key">
<q-td>
<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>
<span> ${entry.key} </span>
</q-td>
</q-tr>
<q-tr>
<q-td>
<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-input v-model="entry.value" />
</q-td>
</q-tr>
</tbody>

View file

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

View file

@ -997,23 +997,14 @@ 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"])