1
0

qml: refactor invoice amount checks, msat precision for lightning.

add qeconfig unit tests for conversion methods.
This commit is contained in:
Sander van Grieken
2025-11-25 12:31:01 +01:00
parent 0f2a41e078
commit 93c9dd9d12
3 changed files with 93 additions and 51 deletions

View File

@@ -358,13 +358,6 @@ class QEConfig(AuthMixin, QObject):
else:
return self.config.format_amount(msats/1000, precision=precision)
# TODO delegate all this to config.py/util.py
def decimal_point(self):
return self.config.BTC_AMOUNTS_DECIMAL_POINT
def max_precision(self):
return self.decimal_point() + 0 # self.extra_precision
@pyqtSlot(str, result=QEAmount)
def unitsToSats(self, unitAmount):
self._amount = QEAmount()
@@ -373,16 +366,11 @@ class QEConfig(AuthMixin, QObject):
except Exception:
return self._amount
# scale it to max allowed precision, make it an int
max_prec_amount = int(pow(10, self.max_precision()) * x)
# if the max precision is simply what unit conversion allows, just return
if self.max_precision() == self.decimal_point():
self._amount = QEAmount(amount_sat=max_prec_amount)
return self._amount
self._logger.debug('fallthrough')
# otherwise, scale it back to the expected unit
#amount = Decimal(max_prec_amount) / Decimal(pow(10, self.max_precision()-self.decimal_point()))
#return int(amount) #Decimal(amount) if not self.is_int else int(amount)
sat_max_precision = self.config.BTC_AMOUNTS_DECIMAL_POINT
msat_max_precision = self.config.BTC_AMOUNTS_DECIMAL_POINT + 3
sat_max_prec_amount = int(pow(10, sat_max_precision) * x)
msat_max_prec_amount = int(pow(10, msat_max_precision) * x)
self._amount = QEAmount(amount_sat=sat_max_prec_amount, amount_msat=msat_max_prec_amount)
return self._amount
@pyqtSlot('quint64', result=float)

View File

@@ -1,7 +1,7 @@
import copy
import threading
from enum import IntEnum
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, Tuple
from urllib.parse import urlparse
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtEnum, QTimer
@@ -327,23 +327,10 @@ class QEInvoice(QObject, QtEventListener):
PR_UNKNOWN: _('Invoice has unknown status'),
}[_status]
if self.invoiceType == QEInvoice.Type.LightningInvoice:
if status in [PR_UNPAID, PR_FAILED]:
if self.get_max_spendable_lightning() >= amount.satsInt:
lnaddr = self._effectiveInvoice._lnaddr
if lnaddr.amount and amount.satsInt < lnaddr.amount * COIN:
self.userinfo = _('Cannot pay less than the amount specified in the invoice')
elif not self.address or self.get_max_spendable_onchain() < amount.satsInt:
# TODO: for onchain: validate address? subtract fee?
self.userinfo = _('Insufficient balance')
else:
self.userinfo = userinfo_for_invoice_status(status)
elif self.invoiceType == QEInvoice.Type.OnchainInvoice:
if status in [PR_UNPAID, PR_FAILED]:
if not ((amount.isMax and self.get_max_spendable_onchain() > 0) or (self.get_max_spendable_onchain() >= amount.satsInt)):
self.userinfo = _('Insufficient balance')
else:
self.userinfo = userinfo_for_invoice_status(status)
if status in [PR_UNPAID, PR_FAILED]:
x, self.userinfo = self.check_can_pay_amount(amount)
else:
self.userinfo = userinfo_for_invoice_status(status)
def determine_can_pay(self):
self.canPay = False
@@ -364,24 +351,25 @@ class QEInvoice(QObject, QtEventListener):
if amount.isEmpty and status == PR_UNPAID: # unspecified amount
return
if status in [PR_UNPAID, PR_FAILED]:
self.canPay, x = self.check_can_pay_amount(amount)
def check_can_pay_amount(self, amount: QEAmount) -> Tuple[bool, Optional[str]]:
assert self.status in [PR_UNPAID, PR_FAILED]
if self.invoiceType == QEInvoice.Type.LightningInvoice:
if status in [PR_UNPAID, PR_FAILED]:
if self.get_max_spendable_lightning() >= amount.satsInt:
lnaddr = self._effectiveInvoice._lnaddr
if not (lnaddr.amount and amount.satsInt < lnaddr.amount * COIN):
self.canPay = True
elif self.address and self.get_max_spendable_onchain() > amount.satsInt:
# TODO: validate address?
# TODO: subtract fee?
self.canPay = True
if self.get_max_spendable_lightning() * 1000 >= amount.msatsInt:
lnaddr = self._effectiveInvoice._lnaddr
if lnaddr.amount and amount.msatsInt < lnaddr.amount * COIN * 1000:
return False, _('Cannot pay less than the amount specified in the invoice')
else:
return True, None
elif self.address and self.get_max_spendable_onchain() > amount.satsInt:
return True, None
elif self.invoiceType == QEInvoice.Type.OnchainInvoice:
if status in [PR_UNPAID, PR_FAILED]:
if amount.isMax and self.get_max_spendable_onchain() > 0:
# TODO: dust limit?
self.canPay = True
elif self.get_max_spendable_onchain() >= amount.satsInt:
# TODO: subtract fee?
self.canPay = True
if (amount.isMax and self.get_max_spendable_onchain() > 0) or (self.get_max_spendable_onchain() >= amount.satsInt):
return True, None
return False, _('Insufficient balance')
@pyqtSlot()
def payLightningInvoice(self):

View File

@@ -0,0 +1,66 @@
from electrum import SimpleConfig
from electrum.gui.qml.qeconfig import QEConfig
from tests.qt_util import QETestCase, qt_test
class TestConfig(QETestCase):
@classmethod
def setUpClass(cls):
QEConfig(SimpleConfig())
def setUp(self):
super().setUp()
self.q: QEConfig = QEConfig.instance
# raise Exception() # NOTE: exceptions in setUp() will block the test
@qt_test
def test_satstounits(self):
self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 5
self.assertEqual(self.q.satsToUnits(100_000), 1.0)
self.assertEqual(self.q.satsToUnits(1), 0.00001)
self.assertEqual(self.q.satsToUnits(0.001), 0.00000001)
@qt_test
def test_unitstosats(self):
qa = self.q.unitsToSats('')
self.assertTrue(qa.isEmpty)
qa = self.q.unitsToSats('0')
self.assertTrue(qa.isEmpty)
qa = self.q.unitsToSats('0.000')
self.assertTrue(qa.isEmpty)
self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 5
qa = self.q.unitsToSats('1')
self.assertFalse(qa.isEmpty)
self.assertEqual(qa.satsInt, 100_000)
self.assertEqual(qa.msatsInt, 100_000_000)
qa = self.q.unitsToSats('1.001')
self.assertFalse(qa.isEmpty)
self.assertEqual(qa.satsInt, 100_100)
self.assertEqual(qa.msatsInt, 100_100_000)
qa = self.q.unitsToSats('1.000001')
self.assertFalse(qa.isEmpty)
self.assertEqual(qa.satsInt, 100_000)
self.assertEqual(qa.msatsInt, 100_000_100)
self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 0
qa = self.q.unitsToSats('1.001')
self.assertFalse(qa.isEmpty)
self.assertEqual(qa.satsInt, 1)
self.assertEqual(qa.msatsInt, 1001)
qa = self.q.unitsToSats('1.0001') # outside msat precision
self.assertFalse(qa.isEmpty)
self.assertEqual(qa.satsInt, 1)
self.assertEqual(qa.msatsInt, 1000)
self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 8
qa = self.q.unitsToSats('0.00000001001')
self.assertFalse(qa.isEmpty)
self.assertEqual(qa.satsInt, 1)
self.assertEqual(qa.msatsInt, 1001)