invoices: also run amount-validator on setter
- @amount_msat.validator prevents the creation of invoices with e.g. too large amounts - however the qml gui is mutating invoices by directly setting the `amount_msat` field, and it looks like attrs validators only run during init. We can use `on_setattr` (introduced in attrs==20.1.0). - a wallet db upgrade is added to rm existing insane invoices - btw the qml gui was already doing its own input validation on the textedit (see qeconfig.btcAmountRegex). however that only limits the input to not have more chars than what is needed to represent 21M BTC (e.g. you can still enter 99M BTC, which the invoice logic does not tolerate later on - but is normally caught). fixes https://github.com/spesmilo/electrum/issues/8582
This commit is contained in:
@@ -6,7 +6,7 @@ aiohttp>=3.3.0,<4.0.0
|
||||
aiohttp_socks>=0.3
|
||||
certifi
|
||||
bitstring
|
||||
attrs>=19.2.0
|
||||
attrs>=20.1.0
|
||||
|
||||
# Note that we also need the dnspython[DNSSEC] extra which pulls in cryptography,
|
||||
# but as that is not pure-python it cannot be listed in this file!
|
||||
|
||||
@@ -94,13 +94,18 @@ class BaseInvoice(StoredObject):
|
||||
"""
|
||||
Base class for Invoice and Request
|
||||
In the code, we use 'invoice' for outgoing payments, and 'request' for incoming payments.
|
||||
|
||||
TODO this class is getting too complicated for "attrs"... maybe we should rewrite it without.
|
||||
"""
|
||||
|
||||
# mandatory fields
|
||||
amount_msat = attr.ib(kw_only=True) # type: Optional[Union[int, str]] # can be '!' or None
|
||||
amount_msat = attr.ib( # can be '!' or None
|
||||
kw_only=True, on_setattr=attr.setters.validate) # type: Optional[Union[int, str]]
|
||||
message = attr.ib(type=str, kw_only=True)
|
||||
time = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # timestamp of the invoice
|
||||
exp = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # expiration delay (relative). 0 means never
|
||||
time = attr.ib( # timestamp of the invoice
|
||||
type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)
|
||||
exp = attr.ib( # expiration delay (relative). 0 means never
|
||||
type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)
|
||||
|
||||
# optional fields.
|
||||
# an request (incoming) can be satisfied onchain, using lightning or using a swap
|
||||
@@ -108,7 +113,8 @@ class BaseInvoice(StoredObject):
|
||||
|
||||
# onchain only
|
||||
outputs = attr.ib(kw_only=True, converter=_decode_outputs) # type: Optional[List[PartialTxOutput]]
|
||||
height = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # only for receiving
|
||||
height = attr.ib( # only for receiving
|
||||
type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)
|
||||
bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str]
|
||||
#bip70_requestor = attr.ib(type=str, kw_only=True) # type: Optional[str]
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ from . import ElectrumTestCase
|
||||
|
||||
from electrum.simple_config import SimpleConfig
|
||||
from electrum.wallet import restore_wallet_from_text, Standard_Wallet, Abstract_Wallet
|
||||
from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, BaseInvoice
|
||||
from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, BaseInvoice, Invoice, LN_EXPIRY_NEVER
|
||||
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED
|
||||
from electrum.transaction import Transaction, PartialTxOutput
|
||||
from electrum.util import TxMinedInfo
|
||||
from electrum.util import TxMinedInfo, InvoiceError
|
||||
|
||||
|
||||
class TestWalletPaymentRequests(ElectrumTestCase):
|
||||
@@ -266,3 +266,39 @@ class TestWalletPaymentRequests(ElectrumTestCase):
|
||||
BaseInvoice._get_cur_time = lambda *args: time.time() + 200_000
|
||||
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2))
|
||||
self.assertEqual(pr2, wallet1.get_request_by_addr(addr1))
|
||||
|
||||
|
||||
class TestBaseInvoice(ElectrumTestCase):
|
||||
TESTNET = True
|
||||
|
||||
async def test_arg_validation(self):
|
||||
amount_sat = 10_000
|
||||
outputs = [PartialTxOutput.from_address_and_value("tb1qmjzmg8nd4z56ar4fpngzsr6euktrhnjg9td385", amount_sat)]
|
||||
invoice = Invoice(
|
||||
amount_msat=amount_sat * 1000,
|
||||
message="mymsg",
|
||||
time=1692716965,
|
||||
exp=LN_EXPIRY_NEVER,
|
||||
outputs=outputs,
|
||||
bip70=None,
|
||||
height=0,
|
||||
lightning_invoice=None,
|
||||
)
|
||||
with self.assertRaises(InvoiceError):
|
||||
invoice.amount_msat = 10**20
|
||||
with self.assertRaises(InvoiceError):
|
||||
invoice2 = Invoice(
|
||||
amount_msat=10**20,
|
||||
message="mymsg",
|
||||
time=1692716965,
|
||||
exp=LN_EXPIRY_NEVER,
|
||||
outputs=outputs,
|
||||
bip70=None,
|
||||
height=0,
|
||||
lightning_invoice=None,
|
||||
)
|
||||
with self.assertRaises(TypeError):
|
||||
invoice.time = "asd"
|
||||
with self.assertRaises(TypeError):
|
||||
invoice.exp = "asd"
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ from .version import ELECTRUM_VERSION
|
||||
|
||||
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
||||
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
||||
FINAL_SEED_VERSION = 53 # electrum >= 2.7 will set this to prevent
|
||||
FINAL_SEED_VERSION = 54 # electrum >= 2.7 will set this to prevent
|
||||
# old versions from overwriting new format
|
||||
|
||||
|
||||
@@ -239,6 +239,7 @@ class WalletDB(JsonDB):
|
||||
self._convert_version_51()
|
||||
self._convert_version_52()
|
||||
self._convert_version_53()
|
||||
self._convert_version_54()
|
||||
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
||||
|
||||
self._after_upgrade_tasks()
|
||||
@@ -1076,6 +1077,23 @@ class WalletDB(JsonDB):
|
||||
cb['local_payment_pubkey'] = None
|
||||
self.data['seed_version'] = 53
|
||||
|
||||
def _convert_version_54(self):
|
||||
# note: similar to convert_version_38
|
||||
if not self._is_upgrade_method_needed(53, 53):
|
||||
return
|
||||
from .bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN
|
||||
max_sats = TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN
|
||||
requests = self.data.get('payment_requests', {})
|
||||
invoices = self.data.get('invoices', {})
|
||||
for d in [invoices, requests]:
|
||||
for key, item in list(d.items()):
|
||||
amount_msat = item['amount_msat']
|
||||
if amount_msat == '!':
|
||||
continue
|
||||
if not (isinstance(amount_msat, int) and 0 <= amount_msat <= max_sats * 1000):
|
||||
del d[key]
|
||||
self.data['seed_version'] = 54
|
||||
|
||||
def _convert_imported(self):
|
||||
if not self._is_upgrade_method_needed(0, 13):
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user