From 44d3e28182e7c551c718d63edde1fd999a2214ad Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 23 Feb 2025 16:52:03 +0000 Subject: [PATCH] format --- README.md | 16 +- __init__.py | 1 + crud.py | 65 +-- models.py | 7 +- nwcp.py | 20 +- paranoia.py | 78 ++-- static/js/admin.js | 165 ++++---- static/js/index.js | 727 +++++++++++++++++----------------- tasks.py | 79 ++-- tests/integration/test_all.py | 25 +- views_api.py | 99 ++--- 11 files changed, 637 insertions(+), 645 deletions(-) diff --git a/README.md b/README.md index 7e8ba03..24eb404 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,21 @@ 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] > > Do not change `handle_missed_events` from its default value of `0` unless you fully understand its implications. -> While a non-zero value may improve service quality under unstable conditions (e.g., poor connectivity or unreliable power), it can also lead to unexpected behavior. +> While a non-zero value may improve service quality under unstable conditions (e.g., poor connectivity or unreliable power), it can also lead to unexpected behavior. > For example, in shared or community lnbits instances, where users are unaware of this functionality, they might assume a payment has failed and attempt to pay a new invoice with a different wallet, only for the instance to come back online and process the original payment request, potentially leading to duplicate payments. > > 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. diff --git a/__init__.py b/__init__.py index d7f2a3b..86e59d7 100644 --- a/__init__.py +++ b/__init__.py @@ -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) diff --git a/crud.py b/crud.py index dd87609..b41e71c 100644 --- a/crud.py +++ b/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,8 +81,8 @@ 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) assert_valid_expiration_seconds(expires) @@ -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) @@ -117,7 +125,7 @@ async def get_nwc(data: GetNWC) -> Optional[NWCKey]: }, NWCKey, ) - else: + else: row = await db.fetchone( """ SELECT * FROM nwcprovider.keys @@ -143,7 +151,7 @@ async def get_nwc(data: GetNWC) -> Optional[NWCKey]: async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]: - + # hardening # assert_valid_pubkey(data.pubkey) # ## # @@ -156,7 +164,7 @@ async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]: if data.calculate_spent: for budget in budgets: last_cycle, next_cycle = budget.get_timestamp_range() - + # hardening # assert_valid_timestamp_seconds(last_cycle) assert_valid_timestamp_seconds(next_cycle) @@ -173,8 +181,10 @@ async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]: "last_cycle": last_cycle, "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,23 +214,30 @@ 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 # assert_valid_msats(tot_spent_in_range_msats) - assert_valid_msats(budget.budget_msats) + assert_valid_msats(budget.budget_msats) # ## # if tot_spent_in_range_msats + data.amount_msats > budget.budget_msats: @@ -243,7 +258,7 @@ async def tracked_spend_nwc(data: TrackedSpendNWC, action): }, ) return True, out - + return await enqueue(r) @@ -262,7 +277,7 @@ async def set_config_nwc(key: str, value: str): assert_sane_string(key) assert_sane_string(value) # ## # - + await db.execute( """ INSERT OR REPLACE INTO nwcprovider.config (key, value) diff --git a/models.py b/models.py index 5884d4c..2208d98 100644 --- a/models.py +++ b/models.py @@ -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 @@ -61,7 +57,7 @@ class NWCNewBudget(BaseModel): budget_msats: int refresh_window: int created_at: int - + # CRUD models class CreateNWCKey(BaseModel): @@ -111,4 +107,3 @@ class NWCRegistrationRequest(BaseModel): class NWCGetResponse(BaseModel): data: NWCKey budgets: List[NWCBudget] - diff --git a/nwcp.py b/nwcp.py index e5d3bf0..22358dc 100644 --- a/nwcp.py +++ b/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": @@ -126,8 +131,8 @@ class NWCServiceProvider: # if True the instance is shutting down self.shutdown = False - - # process missed events that are not older than + + # process missed events that are not older than # handle_missed_events seconds (0 to disable) # (handles reboots) self.handle_missed_events = handle_missed_events @@ -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) diff --git a/paranoia.py b/paranoia.py index 17829e8..815ae4e 100644 --- a/paranoia.py +++ b/paranoia.py @@ -3,85 +3,93 @@ 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) - max_sats_value = 10_000_000 + max_sats_value = 10_000_000 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) max_msats_value = 10_000_000 * 1000 if v >= max_msats_value: panic("msats amount looks too high") - + # 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) assert_no_badhash(v) - + # 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,44 +120,46 @@ 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) if len(v) > 1024: panic("string is too long") - + # 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") - diff --git a/static/js/admin.js b/static/js/admin.js index b6fc801..afacb7e 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -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(); - }, - }); \ No newline at end of file + created: function () { + this.fetchConfig(); + }, +}); diff --git a/static/js/index.js b/static/js/index.js index 3141357..e8a4ec1 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -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(); }, - }); \ No newline at end of file + }, +}); diff --git a/tasks.py b/tasks.py index 1cc1f1a..d06a80f 100644 --- a/tasks.py +++ b/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]: @@ -56,7 +66,7 @@ async def _process_invoice( amount_msats: int, description: Optional[str] = None, ): - + # hardening # assert_valid_wallet_id(wallet_id) assert_valid_pubkey(pubkey) @@ -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,11 +169,9 @@ 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 # assert_valid_pubkey(pubkey) # ## # @@ -228,11 +228,9 @@ 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 # assert_valid_pubkey(pubkey) # ## # @@ -261,7 +259,7 @@ async def _on_make_invoice( if expiry: assert_valid_expiration_seconds(expiry) # ## # - + payment = await create_invoice( wallet_id=nwc.wallet, amount=int(amount_msats / 1000), @@ -300,11 +298,9 @@ 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 # assert_valid_pubkey(pubkey) # ## # @@ -330,7 +326,7 @@ async def _on_lookup_invoice( assert_valid_sha256(payment_hash) assert_valid_bolt11(invoice) # ## # - + # Get payment data payment = await get_wallet_payment(nwc.wallet, payment_hash) if not payment: @@ -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,11 +455,9 @@ 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 # assert_valid_pubkey(pubkey) # ## # diff --git a/tests/integration/test_all.py b/tests/integration/test_all.py index cc785b5..e965b0d 100644 --- a/tests/integration/test_all.py +++ b/tests/integration/test_all.py @@ -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() @@ -963,17 +968,19 @@ async def test_budget_bypass(): @pytest.mark.asyncio async def test_unauthorized_config(): """Test unauthorized access to config endpoint""" - malicious_relay = "ws://attacker-relay.example" + 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) - + async def create_valid_invoice(wallet, amount=1000): """Helper function to create valid test invoice""" @@ -983,4 +990,4 @@ async def create_valid_invoice(wallet, amount=1000): result, tags, error = await wallet.wait_for("make_invoice") if error: raise Exception(f"Failed to create invoice: {error}") - return result["invoice"] \ No newline at end of file + return result["invoice"] diff --git a/views_api.py b/views_api.py index b7ef1ad..c29c60c 100644 --- a/views_api.py +++ b/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, @@ -59,18 +58,14 @@ async def api_get_nwcs( assert_valid_wallet_id(wallet_id) assert_boolean(include_expired) 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,28 +90,23 @@ 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) # ## # @@ -163,7 +151,7 @@ async def api_get_pairing_url( ) async def api_register_nwc( pubkey: str, - data: NWCRegistrationRequest, + data: NWCRegistrationRequest, wallet: WalletTypeInfo = Depends(require_admin_key), ): wallet_id = wallet.wallet.id @@ -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()