format
This commit is contained in:
parent
ac69ae9846
commit
44d3e28182
11 changed files with 630 additions and 638 deletions
14
README.md
14
README.md
|
|
@ -12,13 +12,12 @@ Configure the extension from the "Settings" page in the top right menu when logg
|
|||
|
||||
### Configuration Options:
|
||||
|
||||
| Key | Description | Default |
|
||||
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||
| relay | URL of the nostr relay for dispatching and receiving NWC events. Use public relays or a custom one. Specify `nostrclient` to connect to the [nostrclient extension](https://github.com/lnbits/nostrclient). | nostrclient |
|
||||
| provider_key | Nostr secret key of the NWC Service Provider. | Random key generated on install |
|
||||
| relay_alias | Relay URL to display in pairing URLs. Set if different from `relay`. | Empty (uses the `relay` value) |
|
||||
| handle_missed_events | Number of seconds to look back for processing events missed while offline. Setting it to 0 disables this functionality. | 0 |
|
||||
|
||||
| Key | Description | Default |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||
| relay | URL of the nostr relay for dispatching and receiving NWC events. Use public relays or a custom one. Specify `nostrclient` to connect to the [nostrclient extension](https://github.com/lnbits/nostrclient). | nostrclient |
|
||||
| provider_key | Nostr secret key of the NWC Service Provider. | Random key generated on install |
|
||||
| relay_alias | Relay URL to display in pairing URLs. Set if different from `relay`. | Empty (uses the `relay` value) |
|
||||
| handle_missed_events | Number of seconds to look back for processing events missed while offline. Setting it to 0 disables this functionality. | 0 |
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
|
|
@ -28,7 +27,6 @@ Configure the extension from the "Settings" page in the top right menu when logg
|
|||
>
|
||||
> For this reason, unless you are trying to tackle this specific issue, it is recommended to leave this setting at `0`.
|
||||
|
||||
|
||||
### Using Nostrclient
|
||||
|
||||
The extension is preconfigured to connect to the nostrclient extension. Install it on the same LNbits instance and configure it to expose public websocket endpoints. Refer to the [nostrclient documentation](https://github.com/lnbits/nostrclient) for more information.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ def nwcprovider_stop():
|
|||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
def nwcprovider_start():
|
||||
task = create_permanent_unique_task("ext_nwcprovider", handle_nwc)
|
||||
scheduled_tasks.append(task)
|
||||
|
|
|
|||
49
crud.py
49
crud.py
|
|
@ -8,14 +8,22 @@ from .models import (
|
|||
CreateNWCKey,
|
||||
DeleteNWC,
|
||||
GetBudgetsNWC,
|
||||
GetNWC,
|
||||
GetWalletNWC,
|
||||
NWCBudget,
|
||||
NWCKey,
|
||||
NWCNewBudget,
|
||||
TrackedSpendNWC,
|
||||
GetNWC,
|
||||
NWCNewBudget
|
||||
)
|
||||
from .paranoia import assert_valid_wallet_id, assert_valid_pubkey, assert_sane_string, assert_valid_timestamp_seconds, assert_valid_expiration_seconds, assert_valid_msats, assert_valid_positive_int
|
||||
from .paranoia import (
|
||||
assert_sane_string,
|
||||
assert_valid_expiration_seconds,
|
||||
assert_valid_msats,
|
||||
assert_valid_positive_int,
|
||||
assert_valid_pubkey,
|
||||
assert_valid_timestamp_seconds,
|
||||
assert_valid_wallet_id,
|
||||
)
|
||||
|
||||
db = Database("ext_nwcprovider")
|
||||
|
||||
|
|
@ -73,7 +81,7 @@ async def delete_nwc(data: DeleteNWC) -> None:
|
|||
|
||||
|
||||
async def get_wallet_nwcs(data: GetWalletNWC) -> List[NWCKey]:
|
||||
expires = int(time.time()) if not data.include_expired else -1
|
||||
expires = int(time.time()) if not data.include_expired else -1
|
||||
|
||||
# hardening #
|
||||
assert_valid_wallet_id(data.wallet)
|
||||
|
|
@ -94,7 +102,7 @@ async def get_wallet_nwcs(data: GetWalletNWC) -> List[NWCKey]:
|
|||
|
||||
|
||||
async def get_nwc(data: GetNWC) -> Optional[NWCKey]:
|
||||
expires = int(time.time()) if not data.include_expired else -1
|
||||
expires = int(time.time()) if not data.include_expired else -1
|
||||
|
||||
# hardening #
|
||||
assert_valid_pubkey(data.pubkey)
|
||||
|
|
@ -174,7 +182,9 @@ async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]:
|
|||
"next_cycle": next_cycle,
|
||||
},
|
||||
)
|
||||
tot_spent_in_range_msats = next(iter(tot_spent_in_range_msats.values())) or 0
|
||||
tot_spent_in_range_msats = (
|
||||
next(iter(tot_spent_in_range_msats.values())) or 0
|
||||
)
|
||||
|
||||
# hardening #
|
||||
assert_valid_msats(tot_spent_in_range_msats)
|
||||
|
|
@ -193,9 +203,7 @@ async def tracked_spend_nwc(data: TrackedSpendNWC, action):
|
|||
# ## #
|
||||
|
||||
created_at = int(time.time())
|
||||
budgets = await get_budgets_nwc(GetBudgetsNWC(
|
||||
pubkey=data.pubkey
|
||||
))
|
||||
budgets = await get_budgets_nwc(GetBudgetsNWC(pubkey=data.pubkey))
|
||||
in_budget = True
|
||||
for budget in budgets:
|
||||
last_cycle, next_cycle = budget.get_timestamp_range()
|
||||
|
|
@ -206,18 +214,25 @@ async def tracked_spend_nwc(data: TrackedSpendNWC, action):
|
|||
# ## #
|
||||
|
||||
tot_spent_in_range_msats = (
|
||||
next(iter((await db.fetchone(
|
||||
"""
|
||||
next(
|
||||
iter(
|
||||
(
|
||||
await db.fetchone(
|
||||
"""
|
||||
SELECT SUM(amount_msats) FROM nwcprovider.spent
|
||||
WHERE pubkey = :pubkey AND created_at >=
|
||||
:last_cycle AND created_at < :next_cycle
|
||||
""",
|
||||
{
|
||||
"pubkey": data.pubkey,
|
||||
"last_cycle": last_cycle,
|
||||
"next_cycle": next_cycle,
|
||||
},
|
||||
)).values())) or 0
|
||||
{
|
||||
"pubkey": data.pubkey,
|
||||
"last_cycle": last_cycle,
|
||||
"next_cycle": next_cycle,
|
||||
},
|
||||
)
|
||||
).values()
|
||||
)
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
# hardening #
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ from typing import Any, Dict, List, Optional
|
|||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .nwcp import NWCServiceProvider
|
||||
|
||||
|
||||
class NWCKey(BaseModel):
|
||||
pubkey: str
|
||||
|
|
@ -29,8 +27,6 @@ class NWCKey(BaseModel):
|
|||
return cls(**row)
|
||||
|
||||
|
||||
|
||||
|
||||
class NWCBudget(BaseModel):
|
||||
id: int
|
||||
pubkey: str
|
||||
|
|
@ -111,4 +107,3 @@ class NWCRegistrationRequest(BaseModel):
|
|||
class NWCGetResponse(BaseModel):
|
||||
data: NWCKey
|
||||
budgets: List[NWCBudget]
|
||||
|
||||
|
|
|
|||
16
nwcp.py
16
nwcp.py
|
|
@ -59,19 +59,24 @@ class MainSubscription:
|
|||
if now - event["created_at"] > expire:
|
||||
del self.events[event_id]
|
||||
deleted_ids.append(event_id)
|
||||
self.responses = [event_id for event_id in self.responses if event_id not in deleted_ids]
|
||||
self.responses = [
|
||||
event_id for event_id in self.responses if event_id not in deleted_ids
|
||||
]
|
||||
|
||||
if len(deleted_ids) > 0:
|
||||
logger.debug("Garbage collected " + str(len(deleted_ids)) + " events")
|
||||
|
||||
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class NWCServiceProvider:
|
||||
def __init__(self, private_key: Optional[str] = None, relay: Optional[str] = None, handle_missed_events: int = 0):
|
||||
def __init__(
|
||||
self,
|
||||
private_key: Optional[str] = None,
|
||||
relay: Optional[str] = None,
|
||||
handle_missed_events: int = 0,
|
||||
):
|
||||
if not relay: # Connect to nostrclient
|
||||
relay = "nostrclient"
|
||||
if relay == "nostrclient":
|
||||
|
|
@ -145,7 +150,6 @@ class NWCServiceProvider:
|
|||
self.sub.gc(self.handle_missed_events)
|
||||
await asyncio.sleep(60)
|
||||
|
||||
|
||||
def get_supported_methods(self):
|
||||
"""
|
||||
Returns the list of supported methods by this service provider.
|
||||
|
|
@ -270,7 +274,7 @@ class NWCServiceProvider:
|
|||
"kinds": [23194],
|
||||
"#p": [self.public_key_hex],
|
||||
# Since the last handle_missed_events seconds (handles reboots)
|
||||
"since": int(time.time()) - self.handle_missed_events
|
||||
"since": int(time.time()) - self.handle_missed_events,
|
||||
}
|
||||
self.sub.requests_sub_id = self._get_new_subid()
|
||||
# Create responses subscription (needed to track previosly responded requests)
|
||||
|
|
|
|||
56
paranoia.py
56
paranoia.py
|
|
@ -3,38 +3,43 @@ from loguru import logger
|
|||
|
||||
ENABLE_HARDENING = True
|
||||
|
||||
def panic(reason:str):
|
||||
|
||||
def panic(reason: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
logger.error(f"hardening: {reason}")
|
||||
raise ValueError(f"hardening: {reason}")
|
||||
|
||||
|
||||
# Throw if string contains any non-printable characters
|
||||
def assert_printable(v:str):
|
||||
def assert_printable(v: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
if not isinstance(v, str):
|
||||
panic("not a string "+str(v))
|
||||
panic("not a string " + str(v))
|
||||
if not v.isprintable():
|
||||
panic("string contains non-printable characters")
|
||||
|
||||
|
||||
# Check if number is valid int and not NaN
|
||||
def assert_valid_int(v:int):
|
||||
def assert_valid_int(v: int):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
if not isinstance(v, int):
|
||||
panic("number is not a valid int")
|
||||
|
||||
|
||||
# Check if number is valid positive int
|
||||
def assert_valid_positive_int(v:int):
|
||||
def assert_valid_positive_int(v: int):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_valid_int(v)
|
||||
if v < 0:
|
||||
panic("number is not positive")
|
||||
|
||||
|
||||
# Check if number is a valid sats amount
|
||||
def assert_valid_sats(v:int):
|
||||
def assert_valid_sats(v: int):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_valid_positive_int(v)
|
||||
|
|
@ -42,8 +47,9 @@ def assert_valid_sats(v:int):
|
|||
if v >= max_sats_value:
|
||||
panic("sats amount looks too high")
|
||||
|
||||
|
||||
# Check if number is a valid msats amount
|
||||
def assert_valid_msats(v:int):
|
||||
def assert_valid_msats(v: int):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_valid_positive_int(v)
|
||||
|
|
@ -53,27 +59,29 @@ def assert_valid_msats(v:int):
|
|||
|
||||
|
||||
# Check if string is a valid sha256 hash
|
||||
def assert_valid_sha256(v:str):
|
||||
def assert_valid_sha256(v: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_printable(v)
|
||||
if len(v) != 64 or not all(c in "0123456789abcdef" for c in v):
|
||||
panic("string is not a valid sha256 hash")
|
||||
|
||||
|
||||
# Check if value is an hash of an unexpected input (eg. empty strings, booleans etc)
|
||||
def assert_no_badhash(v:str):
|
||||
def assert_no_badhash(v: str):
|
||||
bad_hashes = [
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", # empty string
|
||||
"36a9e7f1c95b82ffb99743e0c5c4ce95d83c9a430aac59f84ef3cbfab6145068", # 1 space string
|
||||
"c1c4b7fbd3e146bb14ec6258e5231c1ec703590721ff1e321b179a62b5857c9c", # None
|
||||
"cdca0b9bb2325fc8ed7eba7734a3a1f876d919221399b6587ae7d26305adee9d", # True
|
||||
"f9e08f8b038b1b401497f17da3adc120667ac742bf035657869a6ca1cd180e69", # False
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", # empty string
|
||||
"36a9e7f1c95b82ffb99743e0c5c4ce95d83c9a430aac59f84ef3cbfab6145068", # 1 space string
|
||||
"c1c4b7fbd3e146bb14ec6258e5231c1ec703590721ff1e321b179a62b5857c9c", # None
|
||||
"cdca0b9bb2325fc8ed7eba7734a3a1f876d919221399b6587ae7d26305adee9d", # True
|
||||
"f9e08f8b038b1b401497f17da3adc120667ac742bf035657869a6ca1cd180e69", # False
|
||||
]
|
||||
if v in bad_hashes:
|
||||
panic("bad hash detected")
|
||||
|
||||
|
||||
# Check if valid nostr pubkey
|
||||
def assert_valid_pubkey(v:str):
|
||||
def assert_valid_pubkey(v: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_valid_sha256(v)
|
||||
|
|
@ -81,7 +89,7 @@ def assert_valid_pubkey(v:str):
|
|||
|
||||
|
||||
# Check if valid wallet id
|
||||
def assert_valid_wallet_id(v:str):
|
||||
def assert_valid_wallet_id(v: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_printable(v)
|
||||
|
|
@ -90,7 +98,7 @@ def assert_valid_wallet_id(v:str):
|
|||
|
||||
|
||||
# Check if valid timestamp in seconds
|
||||
def assert_valid_timestamp_seconds(v:int):
|
||||
def assert_valid_timestamp_seconds(v: int):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_valid_positive_int(v)
|
||||
|
|
@ -112,7 +120,7 @@ def assert_valid_expiration_seconds(v: int):
|
|||
|
||||
|
||||
# Check if string is within sane parameters
|
||||
def assert_sane_string(v:str):
|
||||
def assert_sane_string(v: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_printable(v)
|
||||
|
|
@ -121,35 +129,37 @@ def assert_sane_string(v:str):
|
|||
|
||||
|
||||
# Check if string is a non-empty string
|
||||
def assert_non_empty_string(v:str):
|
||||
def assert_non_empty_string(v: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_printable(v)
|
||||
if len(v.strip()) == 0:
|
||||
panic("string is empty")
|
||||
|
||||
|
||||
# Assert valid json
|
||||
def assert_valid_json(v:str):
|
||||
def assert_valid_json(v: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_non_empty_string(v)
|
||||
try:
|
||||
import json
|
||||
|
||||
json.loads(v)
|
||||
except:
|
||||
panic("string is not valid json")
|
||||
|
||||
|
||||
# Check if string is a valid bolt11 invoice
|
||||
def assert_valid_bolt11(invoice:str):
|
||||
def assert_valid_bolt11(invoice: str):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
assert_printable(invoice)
|
||||
|
||||
|
||||
# Check if boolean
|
||||
def assert_boolean(v:bool):
|
||||
def assert_boolean(v: bool):
|
||||
if not ENABLE_HARDENING:
|
||||
return
|
||||
if not isinstance(v, bool):
|
||||
panic("not a boolean")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,86 +1,87 @@
|
|||
window.app = Vue.createApp({
|
||||
el: "#vue",
|
||||
mixins: [windowMixin],
|
||||
delimiters: ["${", "}"],
|
||||
data: function () {
|
||||
return {
|
||||
entries: [],
|
||||
columns: [
|
||||
{
|
||||
name: "key",
|
||||
required: true,
|
||||
label: "Key",
|
||||
align: "left",
|
||||
field: (row) => row.key,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
required: true,
|
||||
label: "Value",
|
||||
align: "left",
|
||||
field: (row) => row.value,
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
el: "#vue",
|
||||
mixins: [windowMixin],
|
||||
delimiters: ["${", "}"],
|
||||
data: function () {
|
||||
return {
|
||||
entries: [],
|
||||
columns: [
|
||||
{
|
||||
name: "key",
|
||||
required: true,
|
||||
label: "Key",
|
||||
align: "left",
|
||||
field: (row) => row.key,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
required: true,
|
||||
label: "Value",
|
||||
align: "left",
|
||||
field: (row) => row.value,
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchConfig() {
|
||||
this.entries = [];
|
||||
LNbits.api
|
||||
.request(
|
||||
"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);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error("Error fetching config:", error);
|
||||
});
|
||||
},
|
||||
async saveConfig() {
|
||||
const data = {};
|
||||
for (const entry of this.entries) {
|
||||
data[entry.key] = entry.value;
|
||||
}
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
"POST",
|
||||
"/nwcprovider/api/v1/config",
|
||||
this.g.user.wallets[0].adminkey,
|
||||
data,
|
||||
);
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Config saved!',
|
||||
});
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: '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);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchConfig() {
|
||||
this.entries = [];
|
||||
LNbits.api
|
||||
.request(
|
||||
"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);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error("Error fetching config:", error);
|
||||
});
|
||||
},
|
||||
async saveConfig() {
|
||||
const data = {};
|
||||
for (const entry of this.entries) {
|
||||
data[entry.key] = entry.value;
|
||||
}
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
"POST",
|
||||
"/nwcprovider/api/v1/config",
|
||||
this.g.user.wallets[0].adminkey,
|
||||
data,
|
||||
);
|
||||
Quasar.Notify.create({
|
||||
type: "positive",
|
||||
message: "Config saved!",
|
||||
});
|
||||
Quasar.Notify.create({
|
||||
type: "warning",
|
||||
message:
|
||||
"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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.fetchConfig();
|
||||
},
|
||||
});
|
||||
created: function () {
|
||||
this.fetchConfig();
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,389 +1,386 @@
|
|||
window.app = Vue.createApp({
|
||||
el: "#vue",
|
||||
mixins: [windowMixin],
|
||||
delimiters: ["${", "}"],
|
||||
data: function () {
|
||||
return {
|
||||
selectedWallet: null,
|
||||
nodePermissions: [],
|
||||
nwcEntries: [],
|
||||
nwcsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: "description",
|
||||
align: "left",
|
||||
label: "Description",
|
||||
field: "description",
|
||||
},
|
||||
{ name: "status", align: "left", label: "Status", field: "status" },
|
||||
{
|
||||
name: "last_used",
|
||||
align: "left",
|
||||
label: "Last used",
|
||||
field: "last_used",
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
align: "left",
|
||||
label: "Created",
|
||||
field: "created_at",
|
||||
},
|
||||
{
|
||||
name: "expires_at",
|
||||
align: "left",
|
||||
label: "Expires",
|
||||
field: "expires_at",
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10,
|
||||
window.app = Vue.createApp({
|
||||
el: "#vue",
|
||||
mixins: [windowMixin],
|
||||
delimiters: ["${", "}"],
|
||||
data: function () {
|
||||
return {
|
||||
selectedWallet: null,
|
||||
nodePermissions: [],
|
||||
nwcEntries: [],
|
||||
nwcsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: "description",
|
||||
align: "left",
|
||||
label: "Description",
|
||||
field: "description",
|
||||
},
|
||||
},
|
||||
connectDialog: {
|
||||
show: false,
|
||||
data: {},
|
||||
},
|
||||
pairingDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
pairingUrl: "",
|
||||
{ name: "status", align: "left", label: "Status", field: "status" },
|
||||
{
|
||||
name: "last_used",
|
||||
align: "left",
|
||||
label: "Last used",
|
||||
field: "last_used",
|
||||
},
|
||||
},
|
||||
pairingQrDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
pairingUrl: "",
|
||||
{
|
||||
name: "created_at",
|
||||
align: "left",
|
||||
label: "Created",
|
||||
field: "created_at",
|
||||
},
|
||||
{
|
||||
name: "expires_at",
|
||||
align: "left",
|
||||
label: "Expires",
|
||||
field: "expires_at",
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10,
|
||||
},
|
||||
connectionInfoDialog: {
|
||||
show: false,
|
||||
data: {},
|
||||
},
|
||||
connectDialog: {
|
||||
show: false,
|
||||
data: {},
|
||||
},
|
||||
pairingDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
pairingUrl: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
pairingQrDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
pairingUrl: "",
|
||||
},
|
||||
},
|
||||
connectionInfoDialog: {
|
||||
show: false,
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
showConnectDialog() {
|
||||
const wallet = this.getWallet();
|
||||
if (!wallet) {
|
||||
methods: {
|
||||
showConnectDialog() {
|
||||
const wallet = this.getWallet();
|
||||
if (!wallet) {
|
||||
Quasar.Notify.create({
|
||||
type: "negative",
|
||||
message: "Please select a wallet first",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
this.connectDialog.show = true;
|
||||
}
|
||||
},
|
||||
openConnectionInfoDialog(data) {
|
||||
this.connectionInfoDialog.data = data;
|
||||
this.connectionInfoDialog.show = true;
|
||||
},
|
||||
closeConnectionInfoDialog() {
|
||||
this.connectionInfoDialog.show = false;
|
||||
},
|
||||
openPairingUrl() {
|
||||
const url = this.pairingDialog.data.pairingUrl;
|
||||
if (url) window.open(url, "_blank");
|
||||
},
|
||||
go(url) {
|
||||
window.open(url, "_blank");
|
||||
},
|
||||
async copyPairingUrl() {
|
||||
const url = this.pairingDialog.data.pairingUrl;
|
||||
if (url) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Please select a wallet first',
|
||||
type: "positive",
|
||||
message: "URL copied to clipboard",
|
||||
});
|
||||
} catch (err) {
|
||||
Quasar.Notify.create({
|
||||
type: "negative",
|
||||
message: "Failed to copy URL.",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
this.connectDialog.show = true;
|
||||
}
|
||||
},
|
||||
openConnectionInfoDialog(data) {
|
||||
this.connectionInfoDialog.data = data;
|
||||
this.connectionInfoDialog.show = true;
|
||||
},
|
||||
closeConnectionInfoDialog() {
|
||||
this.connectionInfoDialog.show = false;
|
||||
},
|
||||
openPairingUrl() {
|
||||
const url = this.pairingDialog.data.pairingUrl;
|
||||
if (url) window.open(url, "_blank");
|
||||
},
|
||||
go(url) {
|
||||
window.open(url, "_blank");
|
||||
},
|
||||
async copyPairingUrl() {
|
||||
const url = this.pairingDialog.data.pairingUrl;
|
||||
if (url) {
|
||||
}
|
||||
},
|
||||
showPairingQR() {
|
||||
this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl;
|
||||
this.pairingQrDialog.show = true;
|
||||
},
|
||||
closePairingQrDialog() {
|
||||
this.pairingQrDialog.show = false;
|
||||
},
|
||||
loadConnectDialogData() {
|
||||
this.connectDialog.data = {
|
||||
description: "",
|
||||
expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7,
|
||||
neverExpires: true,
|
||||
permissions: [],
|
||||
budgets: [],
|
||||
};
|
||||
for (const permission of this.nodePermissions) {
|
||||
this.connectDialog.data.permissions.push({
|
||||
key: permission.key,
|
||||
name: permission.name,
|
||||
value: permission.value,
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteBudget(index) {
|
||||
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",
|
||||
});
|
||||
},
|
||||
closeConnectDialog() {
|
||||
this.connectDialog.show = false;
|
||||
this.loadConnectDialogData();
|
||||
},
|
||||
getWallet: function () {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return wallet;
|
||||
},
|
||||
async generateKeyPair() {
|
||||
while (!window.NobleSecp256k1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
}
|
||||
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;
|
||||
},
|
||||
deleteNWC: async function (pubkey) {
|
||||
Quasar.Dialog.create({
|
||||
title: "Confirm Deletion",
|
||||
message: "Are you sure you want to delete this connection?",
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
})
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
const wallet = this.getWallet();
|
||||
const response = await LNbits.api.request(
|
||||
"DELETE",
|
||||
`/nwcprovider/api/v1/nwc/${pubkey}`,
|
||||
wallet.adminkey,
|
||||
);
|
||||
this.loadNwcs();
|
||||
Quasar.Notify.create({
|
||||
type: "positive",
|
||||
message: "URL copied to clipboard"
|
||||
});
|
||||
} catch (err) {
|
||||
Quasar.Notify.create({
|
||||
type: "negative",
|
||||
message: "Failed to copy URL."
|
||||
message: "Deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
showPairingQR() {
|
||||
this.pairingQrDialog.data.pairingUrl =
|
||||
this.pairingDialog.data.pairingUrl;
|
||||
this.pairingQrDialog.show = true;
|
||||
},
|
||||
closePairingQrDialog() {
|
||||
this.pairingQrDialog.show = false;
|
||||
},
|
||||
loadConnectDialogData() {
|
||||
this.connectDialog.data = {
|
||||
description: "",
|
||||
expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7,
|
||||
neverExpires: true,
|
||||
permissions: [],
|
||||
budgets: [],
|
||||
};
|
||||
for (const permission of this.nodePermissions) {
|
||||
this.connectDialog.data.permissions.push({
|
||||
key: permission.key,
|
||||
name: permission.name,
|
||||
value: permission.value,
|
||||
})
|
||||
.onCancel(() => {
|
||||
// User canceled the operation
|
||||
});
|
||||
},
|
||||
loadNwcs: async function () {
|
||||
const wallet = this.getWallet();
|
||||
if (!wallet) {
|
||||
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;
|
||||
} catch (error) {
|
||||
this.nwcs = [];
|
||||
}
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
"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,
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteBudget(index) {
|
||||
this.connectDialog.data.budgets.splice(index, 1);
|
||||
},
|
||||
addBudget() {
|
||||
this.connectDialog.data.budgets.push({
|
||||
budget_sats: 1000,
|
||||
used_budget_sats: 0,
|
||||
this.nodePermissions = permissions;
|
||||
} catch (error) {
|
||||
Lnbits.utils.notifyApiError(error);
|
||||
}
|
||||
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",
|
||||
);
|
||||
const e =
|
||||
nwc.data.expires_at > 0
|
||||
? Quasar.date.formatDate(
|
||||
new Date(nwc.data.expires_at * 1000),
|
||||
"YYYY-MM-DD HH:mm",
|
||||
)
|
||||
: "Never";
|
||||
const l = Quasar.date.formatDate(
|
||||
new Date(nwc.data.last_used * 1000),
|
||||
"YYYY-MM-DD HH:mm",
|
||||
);
|
||||
const nwcTableEntry = {
|
||||
description: nwc.data.description,
|
||||
created_at: t,
|
||||
expires_at: e,
|
||||
last_used: l,
|
||||
pubkey: nwc.data.pubkey,
|
||||
permissions: nwc.data.permissions,
|
||||
budgets: [],
|
||||
status: "Active",
|
||||
};
|
||||
if (
|
||||
nwc.data.expires_at > 0 &&
|
||||
nwc.data.expires_at < new Date().getTime() / 1000
|
||||
) {
|
||||
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;
|
||||
if (refreshWindow <= 0) {
|
||||
refreshWindow = "Never";
|
||||
} else if (refreshWindow == 60 * 60 * 24) {
|
||||
refreshWindow = "Daily";
|
||||
} else if (refreshWindow == 60 * 60 * 24 * 7) {
|
||||
refreshWindow = "Weekly";
|
||||
} else if (refreshWindow == 60 * 60 * 24 * 30) {
|
||||
refreshWindow = "Monthly";
|
||||
} else if (refreshWindow == 60 * 60 * 24 * 365) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
newTableEntries.push(nwcTableEntry);
|
||||
}
|
||||
this.nwcEntries = newTableEntries;
|
||||
},
|
||||
closePairingDialog() {
|
||||
this.pairingDialog.show = false;
|
||||
},
|
||||
async showPairingDialog(secret) {
|
||||
let response = await LNbits.api.request(
|
||||
"GET",
|
||||
"/nwcprovider/api/v1/pairing/{SECRET}",
|
||||
);
|
||||
response = response.data;
|
||||
response = response.replace("{SECRET}", secret);
|
||||
this.pairingDialog.data.pairingUrl = response;
|
||||
this.pairingDialog.show = true;
|
||||
},
|
||||
async confirmConnectDialog() {
|
||||
const keyPair = await this.generateKeyPair();
|
||||
// timestamp
|
||||
let expires_at = 0;
|
||||
if (!this.connectDialog.data.neverExpires) {
|
||||
expires_at =
|
||||
new Date(this.connectDialog.data.expires_at).getTime() / 1000;
|
||||
}
|
||||
const data = {
|
||||
permissions: [],
|
||||
description: this.connectDialog.data.description,
|
||||
expires_at: expires_at,
|
||||
budgets: [],
|
||||
};
|
||||
for (const permission of this.connectDialog.data.permissions) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
data.budgets.push({
|
||||
budget_msats: budget_msats,
|
||||
refresh_window: refresh_window,
|
||||
created_at:
|
||||
new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
|
||||
expiration: "never",
|
||||
});
|
||||
},
|
||||
closeConnectDialog() {
|
||||
this.connectDialog.show = false;
|
||||
this.loadConnectDialogData();
|
||||
},
|
||||
getWallet: function () {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return wallet;
|
||||
},
|
||||
async generateKeyPair() {
|
||||
while (!window.NobleSecp256k1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
}
|
||||
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;
|
||||
},
|
||||
deleteNWC: async function (pubkey) {
|
||||
Quasar
|
||||
.Dialog.create({
|
||||
title: "Confirm Deletion",
|
||||
message: "Are you sure you want to delete this connection?",
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
})
|
||||
.onOk(async () => {
|
||||
try {
|
||||
const wallet = this.getWallet();
|
||||
const response = await LNbits.api.request(
|
||||
"DELETE",
|
||||
`/nwcprovider/api/v1/nwc/${pubkey}`,
|
||||
wallet.adminkey,
|
||||
);
|
||||
this.loadNwcs();
|
||||
Quasar.Notify.create({
|
||||
type: "positive",
|
||||
message: "Deleted successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
}
|
||||
})
|
||||
.onCancel(() => {
|
||||
// User canceled the operation
|
||||
});
|
||||
},
|
||||
loadNwcs: async function () {
|
||||
const wallet = this.getWallet();
|
||||
if (!wallet) {
|
||||
this.nwcs = [];
|
||||
}
|
||||
const wallet = this.getWallet();
|
||||
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
"PUT",
|
||||
"/nwcprovider/api/v1/nwc/" + keyPair.pubKey,
|
||||
wallet.adminkey,
|
||||
data,
|
||||
);
|
||||
this.closeConnectDialog();
|
||||
if (
|
||||
!response.data ||
|
||||
!response.data.data ||
|
||||
!response.data.data.pubkey
|
||||
) {
|
||||
LNbits.utils.notifyApiError("Error creating nwc pairing");
|
||||
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;
|
||||
} catch (error) {
|
||||
this.nwcs = [];
|
||||
}
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
"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,
|
||||
});
|
||||
}
|
||||
this.nodePermissions = permissions;
|
||||
} catch (error) {
|
||||
Lnbits.utils.notifyApiError(error);
|
||||
}
|
||||
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",
|
||||
);
|
||||
const e =
|
||||
nwc.data.expires_at > 0
|
||||
? Quasar.date.formatDate(
|
||||
new Date(nwc.data.expires_at * 1000),
|
||||
"YYYY-MM-DD HH:mm",
|
||||
)
|
||||
: "Never";
|
||||
const l = Quasar.date.formatDate(
|
||||
new Date(nwc.data.last_used * 1000),
|
||||
"YYYY-MM-DD HH:mm",
|
||||
);
|
||||
const nwcTableEntry = {
|
||||
description: nwc.data.description,
|
||||
created_at: t,
|
||||
expires_at: e,
|
||||
last_used: l,
|
||||
pubkey: nwc.data.pubkey,
|
||||
permissions: nwc.data.permissions,
|
||||
budgets: [],
|
||||
status: "Active",
|
||||
};
|
||||
if (
|
||||
nwc.data.expires_at > 0 &&
|
||||
nwc.data.expires_at < new Date().getTime() / 1000
|
||||
) {
|
||||
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;
|
||||
if (refreshWindow <= 0) {
|
||||
refreshWindow = "Never";
|
||||
} else if (refreshWindow == 60 * 60 * 24) {
|
||||
refreshWindow = "Daily";
|
||||
} else if (refreshWindow == 60 * 60 * 24 * 7) {
|
||||
refreshWindow = "Weekly";
|
||||
} else if (refreshWindow == 60 * 60 * 24 * 30) {
|
||||
refreshWindow = "Monthly";
|
||||
} else if (refreshWindow == 60 * 60 * 24 * 365) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
newTableEntries.push(nwcTableEntry);
|
||||
}
|
||||
this.nwcEntries = newTableEntries;
|
||||
},
|
||||
closePairingDialog() {
|
||||
this.pairingDialog.show = false;
|
||||
},
|
||||
async showPairingDialog(secret) {
|
||||
let response = await LNbits.api.request(
|
||||
"GET",
|
||||
"/nwcprovider/api/v1/pairing/{SECRET}",
|
||||
);
|
||||
response = response.data;
|
||||
response = response.replace("{SECRET}", secret);
|
||||
this.pairingDialog.data.pairingUrl = response;
|
||||
this.pairingDialog.show = true;
|
||||
},
|
||||
async confirmConnectDialog() {
|
||||
const keyPair = await this.generateKeyPair();
|
||||
// timestamp
|
||||
let expires_at = 0;
|
||||
if (!this.connectDialog.data.neverExpires) {
|
||||
expires_at =
|
||||
new Date(this.connectDialog.data.expires_at).getTime() / 1000;
|
||||
}
|
||||
const data = {
|
||||
permissions: [],
|
||||
description: this.connectDialog.data.description,
|
||||
expires_at: expires_at,
|
||||
budgets: [],
|
||||
};
|
||||
for (const permission of this.connectDialog.data.permissions) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
data.budgets.push({
|
||||
budget_msats: budget_msats,
|
||||
refresh_window: refresh_window,
|
||||
created_at:
|
||||
new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
|
||||
});
|
||||
}
|
||||
const wallet = this.getWallet();
|
||||
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
"PUT",
|
||||
"/nwcprovider/api/v1/nwc/" + keyPair.pubKey,
|
||||
wallet.adminkey,
|
||||
data,
|
||||
);
|
||||
this.closeConnectDialog();
|
||||
if (
|
||||
!response.data ||
|
||||
!response.data.data ||
|
||||
!response.data.data.pubkey
|
||||
) {
|
||||
LNbits.utils.notifyApiError("Error creating nwc pairing");
|
||||
return;
|
||||
}
|
||||
this.showPairingDialog(keyPair.privKey);
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
}
|
||||
this.loadNwcs();
|
||||
},
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.showPairingDialog(keyPair.privKey);
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
}
|
||||
this.loadNwcs();
|
||||
},
|
||||
watch: {
|
||||
selectedWallet(newValue, oldValue) {
|
||||
this.loadNwcs();
|
||||
},
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.loadNwcs();
|
||||
},
|
||||
watch: {
|
||||
selectedWallet(newValue, oldValue) {
|
||||
this.loadNwcs();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
61
tasks.py
61
tasks.py
|
|
@ -19,10 +19,20 @@ from loguru import logger
|
|||
|
||||
from .crud import get_config_nwc, get_nwc, tracked_spend_nwc
|
||||
from .execution_queue import execution_queue
|
||||
from .models import NWCKey, TrackedSpendNWC, GetNWC
|
||||
from .models import GetNWC, NWCKey, TrackedSpendNWC
|
||||
from .nwcp import NWCServiceProvider
|
||||
from .paranoia import (
|
||||
assert_boolean,
|
||||
assert_sane_string,
|
||||
assert_valid_bolt11,
|
||||
assert_valid_expiration_seconds,
|
||||
assert_valid_msats,
|
||||
assert_valid_positive_int,
|
||||
assert_valid_pubkey,
|
||||
assert_valid_sha256,
|
||||
assert_valid_wallet_id,
|
||||
)
|
||||
from .permission import nwc_permissions
|
||||
from .paranoia import assert_valid_wallet_id, assert_valid_pubkey, assert_boolean, assert_sane_string, assert_valid_expiration_seconds, assert_valid_msats, assert_valid_positive_int, assert_valid_bolt11, assert_valid_sha256
|
||||
|
||||
|
||||
async def _check(nwc: Optional[NWCKey], method: str) -> Optional[Dict]:
|
||||
|
|
@ -66,7 +76,7 @@ async def _process_invoice(
|
|||
assert_sane_string(description)
|
||||
# ## #
|
||||
|
||||
async def execute_payment() -> str :
|
||||
async def execute_payment() -> str:
|
||||
payment = await pay_invoice(
|
||||
wallet_id=wallet_id,
|
||||
payment_request=invoice,
|
||||
|
|
@ -78,10 +88,7 @@ async def _process_invoice(
|
|||
payment_hash = None
|
||||
try:
|
||||
in_budget, payment_hash = await tracked_spend_nwc(
|
||||
TrackedSpendNWC(
|
||||
pubkey=pubkey,
|
||||
amount_msats=amount_msats
|
||||
), execute_payment
|
||||
TrackedSpendNWC(pubkey=pubkey, amount_msats=amount_msats), execute_payment
|
||||
)
|
||||
if not in_budget:
|
||||
error = {
|
||||
|
|
@ -121,19 +128,14 @@ async def _process_invoice(
|
|||
|
||||
|
||||
async def _on_pay_invoice(
|
||||
sp: NWCServiceProvider,
|
||||
pubkey: str,
|
||||
payload: Dict
|
||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||
|
||||
# hardening #
|
||||
assert_valid_pubkey(pubkey)
|
||||
# ## #
|
||||
|
||||
nwc = await get_nwc(GetNWC(
|
||||
pubkey=pubkey,
|
||||
refresh_last_used=True
|
||||
))
|
||||
nwc = await get_nwc(GetNWC(pubkey=pubkey, refresh_last_used=True))
|
||||
error = await _check(nwc, "pay_invoice")
|
||||
if error:
|
||||
return [(None, error, [])]
|
||||
|
|
@ -167,9 +169,7 @@ async def _on_pay_invoice(
|
|||
|
||||
|
||||
async def _on_multi_pay_invoice(
|
||||
sp: NWCServiceProvider,
|
||||
pubkey: str,
|
||||
payload: Dict
|
||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||
|
||||
# hardening #
|
||||
|
|
@ -228,9 +228,7 @@ async def _on_multi_pay_invoice(
|
|||
|
||||
|
||||
async def _on_make_invoice(
|
||||
sp: NWCServiceProvider,
|
||||
pubkey: str,
|
||||
payload: Dict
|
||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||
|
||||
# hardening #
|
||||
|
|
@ -300,9 +298,7 @@ async def _on_make_invoice(
|
|||
|
||||
|
||||
async def _on_lookup_invoice(
|
||||
sp: NWCServiceProvider,
|
||||
pubkey: str,
|
||||
payload: Dict
|
||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||
|
||||
# hardening #
|
||||
|
|
@ -339,7 +335,10 @@ async def _on_lookup_invoice(
|
|||
is_settled = not payment.pending
|
||||
timestamp = int(payment.time.timestamp()) or int(invoice_data.date)
|
||||
expiry = int(payment.expiry.timestamp()) or timestamp + 3600
|
||||
preimage = payment.preimage or "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
preimage = (
|
||||
payment.preimage
|
||||
or "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
)
|
||||
res: Dict = {
|
||||
"type": "outgoing" if payment.is_out else "incoming",
|
||||
"invoice": payment.bolt11,
|
||||
|
|
@ -362,9 +361,7 @@ async def _on_lookup_invoice(
|
|||
|
||||
|
||||
async def _on_list_transactions(
|
||||
sp: NWCServiceProvider,
|
||||
pubkey: str,
|
||||
payload: Dict
|
||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||
# hardening #
|
||||
assert_valid_pubkey(pubkey)
|
||||
|
|
@ -435,16 +432,14 @@ async def _on_list_transactions(
|
|||
|
||||
|
||||
async def _on_get_balance(
|
||||
sp: NWCServiceProvider,
|
||||
pubkey: str,
|
||||
payload: Dict
|
||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||
|
||||
# hardening #
|
||||
assert_valid_pubkey(pubkey)
|
||||
# ## #
|
||||
|
||||
nwc = await get_nwc(GetNWC(pubkey=pubkey, refresh_last_used= True))
|
||||
nwc = await get_nwc(GetNWC(pubkey=pubkey, refresh_last_used=True))
|
||||
error = await _check(nwc, "get_balance")
|
||||
if error:
|
||||
return [(None, error, [])]
|
||||
|
|
@ -460,9 +455,7 @@ async def _on_get_balance(
|
|||
|
||||
|
||||
async def _on_get_info(
|
||||
sp: NWCServiceProvider,
|
||||
pubkey: str,
|
||||
payload: Dict
|
||||
sp: NWCServiceProvider, pubkey: str, payload: Dict
|
||||
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
|
||||
|
||||
# hardening #
|
||||
|
|
|
|||
|
|
@ -852,7 +852,6 @@ async def test_budget_refresh():
|
|||
await wallet1.close()
|
||||
|
||||
|
||||
|
||||
# Mostly AI generated pentests
|
||||
|
||||
|
||||
|
|
@ -882,7 +881,7 @@ async def test_idor_vulnerability():
|
|||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
f"http://localhost:5002/nwcprovider/api/v1/nwc/{nwc_wallet1['pubkey']}",
|
||||
headers={"X-Api-Key": wallets["wallet2"]["admin_key"]}
|
||||
headers={"X-Api-Key": wallets["wallet2"]["admin_key"]},
|
||||
)
|
||||
assert resp.status_code == 500
|
||||
assert "Pubkey has no associated wallet" in resp.text
|
||||
|
|
@ -896,7 +895,7 @@ async def test_sql_injection():
|
|||
resp = await client.put(
|
||||
f"http://localhost:5002/nwcprovider/api/v1/nwc/{malicious_pubkey}",
|
||||
headers={"X-Api-Key": wallets["wallet1"]["admin_key"]},
|
||||
json={"permissions": ["pay"], "description": "test"}
|
||||
json={"permissions": ["pay"], "description": "test"},
|
||||
)
|
||||
# Should be rejected by input validation
|
||||
assert resp.status_code == 400
|
||||
|
|
@ -941,8 +940,14 @@ async def test_budget_bypass():
|
|||
"wallet1",
|
||||
"test_budget_bypass",
|
||||
["pay", "invoice"],
|
||||
[{"budget_msats": 100000, "refresh_window": 3600, "created_at": int(time.time())}],
|
||||
0
|
||||
[
|
||||
{
|
||||
"budget_msats": 100000,
|
||||
"refresh_window": 3600,
|
||||
"created_at": int(time.time()),
|
||||
}
|
||||
],
|
||||
0,
|
||||
)
|
||||
wallet = NWCWallet(nwc["pairing"])
|
||||
await wallet.start()
|
||||
|
|
@ -964,14 +969,16 @@ async def test_budget_bypass():
|
|||
async def test_unauthorized_config():
|
||||
"""Test unauthorized access to config endpoint"""
|
||||
malicious_relay = "ws://attacker-relay.example"
|
||||
|
||||
async def set_config_nwc(key: str, value: str):
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
"http://localhost:5002/nwcprovider/api/v1/config",
|
||||
json={key: value},
|
||||
headers={"X-Api-Key": "lnbitsadmin"} # Assuming admin key
|
||||
headers={"X-Api-Key": "lnbitsadmin"}, # Assuming admin key
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
await set_config_nwc("relay", malicious_relay)
|
||||
|
||||
|
||||
|
|
|
|||
93
views_api.py
93
views_api.py
|
|
@ -2,7 +2,7 @@ from http import HTTPStatus
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
import secp256k1
|
||||
from fastapi import APIRouter, Depends, Request, Query
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from lnbits.core.models import WalletTypeInfo
|
||||
from lnbits.decorators import check_admin, require_admin_key
|
||||
|
|
@ -18,35 +18,34 @@ from .crud import (
|
|||
set_config_nwc,
|
||||
)
|
||||
from .models import (
|
||||
NWCRegistrationRequest,
|
||||
GetBudgetsNWC,
|
||||
GetWalletNWC,
|
||||
NWCGetResponse,
|
||||
CreateNWCKey,
|
||||
DeleteNWC,
|
||||
GetBudgetsNWC,
|
||||
GetNWC,
|
||||
GetBudgetsNWC
|
||||
GetWalletNWC,
|
||||
NWCGetResponse,
|
||||
NWCRegistrationRequest,
|
||||
)
|
||||
from .paranoia import (
|
||||
assert_boolean,
|
||||
assert_sane_string,
|
||||
assert_valid_pubkey,
|
||||
assert_valid_wallet_id,
|
||||
)
|
||||
from .permission import nwc_permissions
|
||||
from .paranoia import assert_valid_wallet_id, assert_valid_pubkey, assert_sane_string, assert_boolean
|
||||
|
||||
nwcprovider_api_router = APIRouter()
|
||||
|
||||
|
||||
# Get supported permissions
|
||||
@nwcprovider_api_router.get(
|
||||
"/api/v1/permissions",
|
||||
status_code=HTTPStatus.OK
|
||||
)
|
||||
@nwcprovider_api_router.get("/api/v1/permissions", status_code=HTTPStatus.OK)
|
||||
async def api_get_permissions() -> Dict:
|
||||
return nwc_permissions
|
||||
|
||||
|
||||
## Get nwc keys associated with the wallet
|
||||
@nwcprovider_api_router.get(
|
||||
"/api/v1/nwc",
|
||||
status_code=HTTPStatus.OK,
|
||||
response_model=List[NWCGetResponse]
|
||||
"/api/v1/nwc", status_code=HTTPStatus.OK, response_model=List[NWCGetResponse]
|
||||
)
|
||||
async def api_get_nwcs(
|
||||
include_expired: bool = False,
|
||||
|
|
@ -61,16 +60,12 @@ async def api_get_nwcs(
|
|||
assert_boolean(calculate_spent_budget)
|
||||
# ## #
|
||||
|
||||
wallet_nwcs = GetWalletNWC(
|
||||
wallet=wallet_id,
|
||||
include_expired=include_expired
|
||||
)
|
||||
wallet_nwcs = GetWalletNWC(wallet=wallet_id, include_expired=include_expired)
|
||||
nwcs = await get_wallet_nwcs(wallet_nwcs)
|
||||
out = []
|
||||
for nwc in nwcs:
|
||||
budgets_nwc = GetBudgetsNWC(
|
||||
pubkey=nwc.pubkey,
|
||||
calculate_spent=calculate_spent_budget
|
||||
pubkey=nwc.pubkey, calculate_spent=calculate_spent_budget
|
||||
)
|
||||
budgets = await get_budgets_nwc(budgets_nwc)
|
||||
res = NWCGetResponse(data=nwc, budgets=budgets)
|
||||
|
|
@ -80,14 +75,12 @@ async def api_get_nwcs(
|
|||
|
||||
# Get a nwc key
|
||||
@nwcprovider_api_router.get(
|
||||
"/api/v1/nwc/{pubkey}",
|
||||
status_code=HTTPStatus.OK,
|
||||
response_model=NWCGetResponse
|
||||
"/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK, response_model=NWCGetResponse
|
||||
)
|
||||
async def api_get_nwc(
|
||||
pubkey: str,
|
||||
include_expired: bool = False,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> NWCGetResponse:
|
||||
wallet_id = wallet.wallet.id
|
||||
|
||||
|
|
@ -97,27 +90,22 @@ async def api_get_nwc(
|
|||
assert_valid_wallet_id(wallet_id)
|
||||
# ## #
|
||||
|
||||
nwc = await get_nwc(GetNWC(pubkey=pubkey, wallet=wallet_id, include_expired=include_expired))
|
||||
nwc = await get_nwc(
|
||||
GetNWC(pubkey=pubkey, wallet=wallet_id, include_expired=include_expired)
|
||||
)
|
||||
if not nwc:
|
||||
raise Exception("Pubkey has no associated wallet")
|
||||
res = NWCGetResponse(data=nwc, budgets=await get_budgets_nwc(
|
||||
GetBudgetsNWC(
|
||||
pubkey=pubkey
|
||||
)
|
||||
))
|
||||
res = NWCGetResponse(
|
||||
data=nwc, budgets=await get_budgets_nwc(GetBudgetsNWC(pubkey=pubkey))
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
# Get pairing url for given secret
|
||||
@nwcprovider_api_router.get(
|
||||
"/api/v1/pairing/{secret}",
|
||||
status_code=HTTPStatus.OK,
|
||||
response_model=str
|
||||
"/api/v1/pairing/{secret}", status_code=HTTPStatus.OK, response_model=str
|
||||
)
|
||||
async def api_get_pairing_url(
|
||||
req: Request,
|
||||
secret: str
|
||||
) -> str:
|
||||
async def api_get_pairing_url(req: Request, secret: str) -> str:
|
||||
|
||||
# hardening #
|
||||
assert_sane_string(secret)
|
||||
|
|
@ -183,23 +171,15 @@ async def api_register_nwc(
|
|||
budgets=data.budgets,
|
||||
)
|
||||
)
|
||||
budgets = await get_budgets_nwc(
|
||||
GetBudgetsNWC(
|
||||
pubkey=pubkey
|
||||
)
|
||||
)
|
||||
budgets = await get_budgets_nwc(GetBudgetsNWC(pubkey=pubkey))
|
||||
res = NWCGetResponse(data=nwc, budgets=budgets)
|
||||
return res
|
||||
|
||||
|
||||
# Delete a nwc key
|
||||
@nwcprovider_api_router.delete(
|
||||
"/api/v1/nwc/{pubkey}",
|
||||
status_code=HTTPStatus.OK
|
||||
)
|
||||
@nwcprovider_api_router.delete("/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK)
|
||||
async def api_delete_nwc(
|
||||
pubkey: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
pubkey: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
wallet_id = wallet.wallet.id
|
||||
|
||||
|
|
@ -208,20 +188,13 @@ async def api_delete_nwc(
|
|||
assert_valid_wallet_id(wallet_id)
|
||||
# ## #
|
||||
|
||||
await delete_nwc(DeleteNWC(
|
||||
pubkey=pubkey,
|
||||
wallet=wallet_id
|
||||
))
|
||||
return JSONResponse(
|
||||
content={"message": f"NWC key {pubkey} deleted successfully."}
|
||||
)
|
||||
await delete_nwc(DeleteNWC(pubkey=pubkey, wallet=wallet_id))
|
||||
return JSONResponse(content={"message": f"NWC key {pubkey} deleted successfully."})
|
||||
|
||||
|
||||
# Get config
|
||||
@nwcprovider_api_router.get(
|
||||
"/api/v1/config",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)]
|
||||
"/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
|
||||
)
|
||||
async def api_get_all_config_nwc():
|
||||
config = await get_all_config_nwc()
|
||||
|
|
@ -243,9 +216,7 @@ async def api_get_config_nwc(key: str):
|
|||
|
||||
# Set config
|
||||
@nwcprovider_api_router.post(
|
||||
"/api/v1/config",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)]
|
||||
"/api/v1/config", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
|
||||
)
|
||||
async def api_set_config_nwc(req: Request):
|
||||
data = await req.json()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue