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: