plugin: nwc: do budget accounting in msat

Keep track of the spent amount in msat instead of sat to prevent
issues due to rounding.
The budget is still specified in sat.
This commit is contained in:
f321x
2026-03-03 13:58:59 +01:00
parent 0dc08fdf24
commit 3956bff068
3 changed files with 31 additions and 14 deletions
+12 -12
View File
@@ -811,7 +811,7 @@ class NWCServer(Logger, EventListener):
elif invoice.get_amount_msat() is None:
invoice.set_amount_msat(amount_msat)
if not self.budget_allows_spend(request_pub, invoice.get_amount_sat()):
if not self.budget_allows_spend(request_pub, msat_requested=amount_msat or invoice.get_amount_msat()):
return self.get_error_response("QUOTA_EXCEEDED", "Payment exceeds daily limit")
self.wallet.save_invoice(invoice)
@@ -828,7 +828,7 @@ class NWCServer(Logger, EventListener):
if not success or not preimage:
return self.get_error_response("PAYMENT_FAILED", str(log))
else:
self.add_to_budget(request_pub, invoice.get_amount_sat())
self.add_to_budget(request_pub, amount_msat=amount_msat or invoice.get_amount_msat())
response['result'] = {
'preimage': preimage.hex(),
}
@@ -838,7 +838,7 @@ class NWCServer(Logger, EventListener):
self.logger.info(f"failed to pay invoice request from NWC: {log}")
return response
def add_to_budget(self, client_pub: str, amount_sat: int) -> None:
def add_to_budget(self, client_pub: str, *, amount_msat: int) -> None:
"""
If client_pub has a budget, check if the amount is within the budget and add it to the budget.
Return True if the payment is allowed (within the budget)
@@ -846,34 +846,34 @@ class NWCServer(Logger, EventListener):
if 'budget_spends' not in self.connections[client_pub]:
self.connections[client_pub]['budget_spends'] = []
# tuples don't work because jsondb converts them to lists on reload
self.connections[client_pub]['budget_spends'].append([amount_sat, int(time.time())])
self.connections[client_pub]['budget_spends'].append([amount_msat, int(time.time())])
def get_used_budget(self, client_pub: str) -> int:
def get_used_budget_msat(self, client_pub: str) -> int:
"""
Returns the used budget for the given client_pubkey.
Returns the used budget for the given client_pubkey in millisatoshi.
"""
if 'budget_spends' not in self.connections[client_pub]:
return 0
used_budget: int = 0
budget_spends = self.connections[client_pub]['budget_spends']
for amount, timestamp in list(budget_spends):
for amount_msat, timestamp in list(budget_spends):
if timestamp > int(time.time()) - 24 * 3600:
used_budget += amount
used_budget += amount_msat
elif timestamp < int(time.time()) - 24 * 3600:
# remove old expense
try:
budget_spends.remove([amount, timestamp])
budget_spends.remove([amount_msat, timestamp])
except ValueError:
self.logger.debug("", exc_info=True)
continue # could happen if there is a race
return used_budget
def budget_allows_spend(self, client_pub: str, sats_to_spend: int) -> bool:
def budget_allows_spend(self, client_pub: str, *, msat_requested: int) -> bool:
client_budget_sat: Optional[int] = self.connections[client_pub].get('daily_limit_sat')
if client_budget_sat is None:
return True # unlimited budget
used_budget: int = self.get_used_budget(client_pub)
if used_budget + sats_to_spend > client_budget_sat:
used_budget_msat: int = self.get_used_budget_msat(client_pub)
if used_budget_msat + msat_requested > client_budget_sat * 1000:
return False
return True
+3 -1
View File
@@ -25,6 +25,7 @@
from typing import TYPE_CHECKING, Optional
from functools import partial
from datetime import datetime
from decimal import Decimal
from PyQt6.QtWidgets import (
QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTreeWidget, QTreeWidgetItem,
@@ -117,7 +118,8 @@ class Plugin(NWCServerPlugin):
else:
budget = self.config.format_amount(conn['daily_limit_sat'])
used = self.config.format_amount(
self.nwc_server.get_used_budget(conn['client_pub']))
amount_sat=round(Decimal(self.nwc_server.get_used_budget_msat(conn['client_pub'])) / 1000)
)
limit = f"{used}/{budget}"
item = QTreeWidgetItem(
[
+16 -1
View File
@@ -69,7 +69,7 @@ class WalletUnfinished(WalletFileException):
# seed_version is now used for the version of the wallet file
OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 69 # electrum >= 2.7 will set this to prevent
FINAL_SEED_VERSION = 70 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format
@@ -244,6 +244,7 @@ class WalletDBUpgrader(Logger):
self._convert_version_67()
self._convert_version_68()
self._convert_version_69()
self._convert_version_70()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
def _convert_wallet_type(self):
@@ -1385,6 +1386,20 @@ class WalletDBUpgrader(Logger):
self.data['lightning_payments'] = new_payment_infos
self.data['seed_version'] = 69
def _convert_version_70(self):
"""
Converts spending budget values of nwc plugin from sat to msat.
"""
if not self._is_upgrade_method_needed(69, 69):
return
nwc_connections = self.data.get('plugin_data', {}).get('nwc', {}).get('connections', {})
for pubkey, connection in nwc_connections.items():
new_budget_spends = []
for amount_sat, timestamp in connection.get('budget_spends', []):
new_budget_spends.append([amount_sat * 1000, timestamp])
connection['budget_spends'] = new_budget_spends
self.data['seed_version'] = 70
def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return