tinvoices: follow-up fixes re clean-up - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit 309ba15745cc60c01f660c10e30bbe3ac4fb9604
 (DIR) parent e058ee29575ddd69afeefe4a0319360163750c53
 (HTM) Author: SomberNight <somber.night@protonmail.com>
       Date:   Wed,  3 Jun 2020 21:00:03 +0200
       
       invoices: follow-up fixes re clean-up
       
       follow-up 6058829870fde0ef17b2e08a567110ecc381ab94 and related
       
       Diffstat:
         M electrum/gui/kivy/uix/screens.py    |       6 +++++-
         M electrum/gui/qt/main_window.py      |       8 ++++++--
         M electrum/invoices.py                |      33 +++++++++++++++++++++++++++----
         M electrum/plugins/email_requests/qt… |       8 ++++++--
         M electrum/wallet.py                  |      58 +++++++++++++------------------
       
       5 files changed, 71 insertions(+), 42 deletions(-)
       ---
 (DIR) diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -310,7 +310,11 @@ class SendScreen(CScreen):
                            self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
                            return
                        outputs = [PartialTxOutput.from_address_and_value(address, amount)]
       -            return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI)
       +            return self.app.wallet.create_invoice(
       +                outputs=outputs,
       +                message=message,
       +                pr=self.payment_request,
       +                URI=self.parsed_URI)
        
            def do_save(self):
                invoice = self.read_invoice()
 (DIR) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -1523,7 +1523,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    if self.check_send_tab_onchain_outputs_and_show_errors(outputs):
                        return
                    message = self.message_e.text()
       -            return self.wallet.create_invoice(outputs, message, self.payment_request, self.payto_URI)
       +            return self.wallet.create_invoice(
       +                outputs=outputs,
       +                message=message,
       +                pr=self.payment_request,
       +                URI=self.payto_URI)
        
            def do_save_invoice(self):
                invoice = self.read_invoice()
       t@@ -1772,7 +1776,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    return
                key = pr.get_id()
                invoice = self.wallet.get_invoice(key)
       -        if invoice and self.wallet.get_invoice_status() == PR_PAID:
       +        if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID:
                    self.show_message("invoice already paid")
                    self.do_clear()
                    self.payment_request = None
 (DIR) diff --git a/electrum/invoices.py b/electrum/invoices.py
       t@@ -1,5 +1,6 @@
        import attr
        import time
       +from typing import TYPE_CHECKING, List
        
        from .json_db import StoredObject
        from .i18n import _
       t@@ -9,6 +10,9 @@ from . import constants
        from .bitcoin import COIN
        from .transaction import PartialTxOutput
        
       +if TYPE_CHECKING:
       +    from .paymentrequest import PaymentRequest
       +
        # convention: 'invoices' = outgoing , 'request' = incoming
        
        # types of payment requests
       t@@ -54,7 +58,14 @@ pr_expiration_values = {
        }
        assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
        
       -outputs_decoder = lambda _list: [PartialTxOutput.from_legacy_tuple(*x) for x in _list]
       +
       +def _decode_outputs(outputs) -> List[PartialTxOutput]:
       +    ret = []
       +    for output in outputs:
       +        if not isinstance(output, PartialTxOutput):
       +            output = PartialTxOutput.from_legacy_tuple(*output)
       +        ret.append(output)
       +    return ret
        
        # hack: BOLT-11 is not really clear on what an expiry of 0 means.
        # It probably interprets it as 0 seconds, so already expired...
       t@@ -86,21 +97,35 @@ class Invoice(StoredObject):
        @attr.s
        class OnchainInvoice(Invoice):
            id = attr.ib(type=str)
       -    outputs = attr.ib(type=list, converter=outputs_decoder)
       +    outputs = attr.ib(type=list, converter=_decode_outputs)
            bip70 = attr.ib(type=str) # may be None
            requestor = attr.ib(type=str) # may be None
        
       -    def get_address(self):
       +    def get_address(self) -> str:
                assert len(self.outputs) == 1
                return self.outputs[0].address
        
       +    @classmethod
       +    def from_bip70_payreq(cls, pr: 'PaymentRequest') -> 'OnchainInvoice':
       +        return OnchainInvoice(
       +            type=PR_TYPE_ONCHAIN,
       +            amount=pr.get_amount(),
       +            outputs=pr.get_outputs(),
       +            message=pr.get_memo(),
       +            id=pr.get_id(),
       +            time=pr.get_time(),
       +            exp=pr.get_expiration_date() - pr.get_time(),
       +            bip70=pr.raw.hex() if pr else None,
       +            requestor=pr.get_requestor(),
       +        )
       +
        @attr.s
        class LNInvoice(Invoice):
            rhash = attr.ib(type=str)
            invoice = attr.ib(type=str)
        
            @classmethod
       -    def from_bech32(klass, invoice: str):
       +    def from_bech32(klass, invoice: str) -> 'LNInvoice':
                lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
                amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
                return LNInvoice(
 (DIR) diff --git a/electrum/plugins/email_requests/qt.py b/electrum/plugins/email_requests/qt.py
       t@@ -29,6 +29,7 @@ import base64
        from functools import partial
        import traceback
        import sys
       +from typing import Set
        
        import smtplib
        import imaplib
       t@@ -48,6 +49,8 @@ from electrum.plugin import BasePlugin, hook
        from electrum.paymentrequest import PaymentRequest
        from electrum.i18n import _
        from electrum.logging import Logger
       +from electrum.wallet import Abstract_Wallet
       +from electrum.invoices import OnchainInvoice
        
        
        class Processor(threading.Thread, Logger):
       t@@ -150,7 +153,7 @@ class Plugin(BasePlugin):
                    self.processor.start()
                self.obj = QEmailSignalObject()
                self.obj.email_new_invoice_signal.connect(self.new_invoice)
       -        self.wallets = set()
       +        self.wallets = set()  # type: Set[Abstract_Wallet]
        
            def on_receive(self, pr_str):
                self.logger.info('received payment request')
       t@@ -166,8 +169,9 @@ class Plugin(BasePlugin):
                self.wallets -= {wallet}
        
            def new_invoice(self):
       +        invoice = OnchainInvoice.from_bip70_payreq(self.pr)
                for wallet in self.wallets:
       -            wallet.invoices.add(self.pr)
       +            wallet.save_invoice(invoice)
                #main_window.invoice_list.update()
        
            @hook
 (DIR) diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -68,7 +68,7 @@ from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
        from .plugin import run_hook
        from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
                                           TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
       -from .invoices import Invoice, OnchainInvoice, invoice_from_json
       +from .invoices import Invoice, OnchainInvoice, invoice_from_json, LNInvoice
        from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_TYPE_ONCHAIN, PR_TYPE_LN
        from .contacts import Contacts
        from .interface import NetworkException
       t@@ -248,7 +248,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                self.frozen_coins          = set(db.get('frozen_coins', []))  # set of txid:vout strings
                self.fiat_value            = db.get_dict('fiat_value')
                self.receive_requests      = db.get_dict('payment_requests')
       -        self.invoices              = db.get_dict('invoices')
       +        self.invoices              = db.get_dict('invoices')  # type: Dict[str, Invoice]
                self._reserved_addresses   = set(db.get('reserved_addresses', []))
        
                self._prepare_onchain_invoice_paid_detection()
       t@@ -656,43 +656,33 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                        'txpos_in_block': hist_item.tx_mined_status.txpos,
                    }
        
       -    def create_invoice(self, outputs: List[PartialTxOutput], message, pr, URI):
       +    def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
       +        if pr:
       +            return OnchainInvoice.from_bip70_payreq(pr)
                if '!' in (x.value for x in outputs):
                    amount = '!'
                else:
                    amount = sum(x.value for x in outputs)
       -        outputs = [x.to_legacy_tuple() for x in outputs]
       -        if pr:
       -            invoice = OnchainInvoice(
       -                type = PR_TYPE_ONCHAIN,
       -                amount = amount,
       -                outputs = outputs,
       -                message = pr.get_memo(),
       -                id = pr.get_id(),
       -                time = pr.get_time(),
       -                exp = pr.get_expiration_date() - pr.get_time(),
       -                bip70 = pr.raw.hex() if pr else None,
       -                requestor = pr.get_requestor(),
       -            )
       -        else:
       -            invoice = OnchainInvoice(
       -                type = PR_TYPE_ONCHAIN,
       -                amount = amount,
       -                outputs = outputs,
       -                message = message,
       -                id = bh2u(sha256(repr(outputs))[0:16]),
       -                time = URI.get('time') if URI else int(time.time()),
       -                exp = URI.get('exp') if URI else 0,
       -                bip70 = None,
       -                requestor = None,
       -            )
       +        invoice = OnchainInvoice(
       +            type=PR_TYPE_ONCHAIN,
       +            amount=amount,
       +            outputs=outputs,
       +            message=message,
       +            id=bh2u(sha256(repr(outputs))[0:16]),
       +            time=URI.get('time') if URI else int(time.time()),
       +            exp=URI.get('exp') if URI else 0,
       +            bip70=None,
       +            requestor=None,
       +        )
                return invoice
        
       -    def save_invoice(self, invoice: Invoice):
       +    def save_invoice(self, invoice: Invoice) -> None:
                invoice_type = invoice.type
                if invoice_type == PR_TYPE_LN:
       +            assert isinstance(invoice, LNInvoice)
                    key = invoice.rhash
                elif invoice_type == PR_TYPE_ONCHAIN:
       +            assert isinstance(invoice, OnchainInvoice)
                    key = invoice.id
                    if self.is_onchain_invoice_paid(invoice):
                        self.logger.info("saving invoice... but it is already paid!")
       t@@ -729,12 +719,14 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                self._invoices_from_scriptpubkey_map = defaultdict(set)  # type: Dict[bytes, Set[str]]
                for invoice_key, invoice in self.invoices.items():
                    if invoice.type == PR_TYPE_ONCHAIN:
       +                assert isinstance(invoice, OnchainInvoice)
                        for txout in invoice.outputs:
                            self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
        
            def _is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Sequence[str]]:
                """Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
                assert invoice.type == PR_TYPE_ONCHAIN
       +        assert isinstance(invoice, OnchainInvoice)
                invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats
                for txo in invoice.outputs:  # type: PartialTxOutput
                    invoice_amounts[txo.scriptpubkey] += 1 if txo.value == '!' else txo.value
       t@@ -763,9 +755,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                    for invoice_key in self._get_relevant_invoice_keys_for_tx(tx):
                        invoice = self.invoices.get(invoice_key)
                        if invoice is None: continue
       -                assert invoice.get('type') == PR_TYPE_ONCHAIN
       -                if invoice['message']:
       -                    labels.append(invoice['message'])
       +                assert isinstance(invoice, OnchainInvoice)
       +                if invoice.message:
       +                    labels.append(invoice.message)
                if labels:
                    self.set_label(tx_hash, "; ".join(labels))
                return bool(labels)
       t@@ -1610,7 +1602,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
                    status = PR_EXPIRED
                return status
        
       -    def get_invoice_status(self, invoice):
       +    def get_invoice_status(self, invoice: Invoice):
                if invoice.is_lightning():
                    status = self.lnworker.get_invoice_status(invoice)
                else: