diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index 820e9cca0..47753c4b2 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -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! diff --git a/electrum/invoices.py b/electrum/invoices.py index d5bf7bfdc..9f790ea77 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -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] diff --git a/electrum/tests/test_invoices.py b/electrum/tests/test_invoices.py index 06dafd494..a5ea83fca 100644 --- a/electrum/tests/test_invoices.py +++ b/electrum/tests/test_invoices.py @@ -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" + diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index d148a0a9c..01a4c6f49 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -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