format
This commit is contained in:
parent
ac69ae9846
commit
44d3e28182
11 changed files with 630 additions and 638 deletions
|
|
@ -13,13 +13,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 |
|
||||
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> Do not change `handle_missed_events` from its default value of `0` unless you fully understand its implications.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
33
crud.py
33
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")
|
||||
|
||||
|
|
@ -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,7 +214,10 @@ 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 >=
|
||||
|
|
@ -217,7 +228,11 @@ async def tracked_spend_nwc(data: TrackedSpendNWC, action):
|
|||
"last_cycle": last_cycle,
|
||||
"next_cycle": next_cycle,
|
||||
},
|
||||
)).values())) or 0
|
||||
)
|
||||
).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)
|
||||
|
|
|
|||
46
paranoia.py
46
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,15 +59,16 @@ 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
|
||||
|
|
@ -72,8 +79,9 @@ def assert_no_badhash(v:str):
|
|||
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")
|
||||
|
||||
|
|
|
|||
|
|
@ -63,18 +63,19 @@ window.app = Vue.createApp({
|
|||
data,
|
||||
);
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Config saved!',
|
||||
type: "positive",
|
||||
message: "Config saved!",
|
||||
});
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'You need to restart the server for the changes to take effect!',
|
||||
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),
|
||||
})
|
||||
type: "negative",
|
||||
message: "Error saving config: " + String(error),
|
||||
});
|
||||
console.error("Error saving config:", error);
|
||||
}
|
||||
},
|
||||
|
|
@ -83,4 +84,4 @@ window.app = Vue.createApp({
|
|||
created: function () {
|
||||
this.fetchConfig();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
window.app = Vue.createApp({
|
||||
window.app = Vue.createApp({
|
||||
el: "#vue",
|
||||
mixins: [windowMixin],
|
||||
delimiters: ["${", "}"],
|
||||
|
|
@ -67,8 +67,8 @@
|
|||
const wallet = this.getWallet();
|
||||
if (!wallet) {
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Please select a wallet first',
|
||||
type: "negative",
|
||||
message: "Please select a wallet first",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
|
|
@ -96,19 +96,18 @@
|
|||
await navigator.clipboard.writeText(url);
|
||||
Quasar.Notify.create({
|
||||
type: "positive",
|
||||
message: "URL copied to clipboard"
|
||||
message: "URL copied to clipboard",
|
||||
});
|
||||
} catch (err) {
|
||||
Quasar.Notify.create({
|
||||
type: "negative",
|
||||
message: "Failed to copy URL."
|
||||
message: "Failed to copy URL.",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
showPairingQR() {
|
||||
this.pairingQrDialog.data.pairingUrl =
|
||||
this.pairingDialog.data.pairingUrl;
|
||||
this.pairingQrDialog.data.pairingUrl = this.pairingDialog.data.pairingUrl;
|
||||
this.pairingQrDialog.show = true;
|
||||
},
|
||||
closePairingQrDialog() {
|
||||
|
|
@ -137,8 +136,7 @@
|
|||
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,
|
||||
created_at: new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000,
|
||||
expiration: "never",
|
||||
});
|
||||
},
|
||||
|
|
@ -171,8 +169,7 @@
|
|||
return out;
|
||||
},
|
||||
deleteNWC: async function (pubkey) {
|
||||
Quasar
|
||||
.Dialog.create({
|
||||
Quasar.Dialog.create({
|
||||
title: "Confirm Deletion",
|
||||
message: "Are you sure you want to delete this connection?",
|
||||
cancel: true,
|
||||
|
|
@ -189,7 +186,7 @@
|
|||
this.loadNwcs();
|
||||
Quasar.Notify.create({
|
||||
type: "positive",
|
||||
message: "Deleted successfully"
|
||||
message: "Deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error);
|
||||
|
|
@ -386,4 +383,4 @@
|
|||
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)
|
||||
|
||||
|
||||
|
|
|
|||
91
views_api.py
91
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