qml: refactor invoice amount checks, msat precision for lightning.
add qeconfig unit tests for conversion methods.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
66
tests/test_qml_qeconfig.py
Normal file
66
tests/test_qml_qeconfig.py
Normal 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)
|
||||
Reference in New Issue
Block a user