Browse Source

Merge pull request #5809 from SomberNight/201911_invoice_paid_detection

wallet: better (outgoing) invoice "paid" detection
hard-fail-on-bad-server-string
ThomasV 5 years ago
committed by GitHub
parent
commit
7a080352f8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      electrum/address_synchronizer.py
  2. 8
      electrum/gui/kivy/main_window.py
  3. 6
      electrum/gui/kivy/uix/screens.py
  4. 3
      electrum/gui/qt/invoice_list.py
  5. 37
      electrum/gui/qt/main_window.py
  6. 17
      electrum/gui/qt/transaction_dialog.py
  7. 51
      electrum/json_db.py
  8. 77
      electrum/wallet.py

19
electrum/address_synchronizer.py

@ -26,7 +26,7 @@ import threading
import asyncio
import itertools
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence, List
from . import bitcoin
from .bitcoin import COINBASE_MATURITY
@ -207,7 +207,7 @@ class AddressSynchronizer(Logger):
conflicting_txns -= {tx_hash}
return conflicting_txns
def add_transaction(self, tx: Transaction, allow_unrelated=False) -> bool:
def add_transaction(self, tx: Transaction, *, allow_unrelated=False) -> bool:
"""Returns whether the tx was successfully added to the wallet history."""
assert tx, tx
assert tx.is_complete()
@ -283,6 +283,8 @@ class AddressSynchronizer(Logger):
for n, txo in enumerate(tx.outputs()):
v = txo.value
ser = tx_hash + ':%d'%n
scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey.hex())
self.db.add_prevout_by_scripthash(scripthash, prevout=TxOutpoint.from_str(ser), value=v)
addr = self.get_txout_address(txo)
if addr and self.is_mine(addr):
self.db.add_txo_addr(tx_hash, addr, n, v, is_coinbase)
@ -299,7 +301,7 @@ class AddressSynchronizer(Logger):
self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs()))
return True
def remove_transaction(self, tx_hash):
def remove_transaction(self, tx_hash: str) -> None:
def remove_from_spent_outpoints():
# undo spends in spent_outpoints
if tx is not None:
@ -317,7 +319,7 @@ class AddressSynchronizer(Logger):
if spending_txid == tx_hash:
self.db.remove_spent_outpoint(prevout_hash, prevout_n)
with self.transaction_lock:
with self.lock, self.transaction_lock:
self.logger.info(f"removing tx from history {tx_hash}")
tx = self.db.remove_transaction(tx_hash)
remove_from_spent_outpoints()
@ -327,6 +329,13 @@ class AddressSynchronizer(Logger):
self.db.remove_txi(tx_hash)
self.db.remove_txo(tx_hash)
self.db.remove_tx_fee(tx_hash)
self.db.remove_verified_tx(tx_hash)
self.unverified_tx.pop(tx_hash, None)
if tx:
for idx, txo in enumerate(tx.outputs()):
scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey.hex())
prevout = TxOutpoint(bfh(tx_hash), idx)
self.db.remove_prevout_by_scripthash(scripthash, prevout=prevout, value=txo.value)
def get_depending_transactions(self, tx_hash):
"""Returns all (grand-)children of tx_hash in this wallet."""
@ -338,7 +347,7 @@ class AddressSynchronizer(Logger):
children |= self.get_depending_transactions(other_hash)
return children
def receive_tx_callback(self, tx_hash, tx, tx_height):
def receive_tx_callback(self, tx_hash: str, tx: Transaction, tx_height: int) -> None:
self.add_unverified_tx(tx_hash, tx_height)
self.add_transaction(tx, allow_unrelated=True)

8
electrum/gui/kivy/main_window.py

@ -1028,18 +1028,12 @@ class ElectrumWindow(App):
status, msg = True, tx.txid()
Clock.schedule_once(lambda dt: on_complete(status, msg))
def broadcast(self, tx, invoice=None):
def broadcast(self, tx):
def on_complete(ok, msg):
if ok:
self.show_info(_('Payment sent.'))
if self.send_screen:
self.send_screen.do_clear()
if invoice:
key = invoice['id']
txid = tx.txid()
self.wallet.set_label(txid, invoice['message'])
self.wallet.set_paid(key, txid)
self.update_tab('invoices')
else:
msg = msg or ''
self.show_error(msg)

6
electrum/gui/kivy/uix/screens.py

@ -380,14 +380,14 @@ class SendScreen(CScreen):
if fee > feerate_warning * tx.estimated_size() / 1000:
msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
msg.append(_("Enter your PIN code to proceed"))
self.app.protected('\n'.join(msg), self.send_tx, (tx, invoice))
self.app.protected('\n'.join(msg), self.send_tx, (tx,))
def send_tx(self, tx, invoice, password):
def send_tx(self, tx, password):
if self.app.wallet.has_password() and password is None:
return
def on_success(tx):
if tx.is_complete():
self.app.broadcast(tx, invoice)
self.app.broadcast(tx)
else:
self.app.tx_dialog(tx)
def on_failure(error):

3
electrum/gui/qt/invoice_list.py

@ -96,7 +96,8 @@ class InvoiceList(MyTreeView):
_list = self.parent.wallet.get_invoices()
# filter out paid invoices unless we have the log
lnworker_logs = self.parent.wallet.lnworker.logs if self.parent.wallet.lnworker else {}
_list = [x for x in _list if x and x.get('status') != PR_PAID or x.get('rhash') in lnworker_logs]
_list = [x for x in _list
if x and (x.get('status') != PR_PAID or x.get('rhash') in lnworker_logs)]
self.model().clear()
self.update_headers(self.__class__.headers)
for idx, item in enumerate(_list):

37
electrum/gui/qt/main_window.py

@ -928,9 +928,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
d = address_dialog.AddressDialog(self, addr)
d.exec_()
def show_transaction(self, tx, *, invoice=None, tx_desc=None):
def show_transaction(self, tx, *, tx_desc=None):
'''tx_desc is set only for txs created in the Send tab'''
show_transaction(tx, parent=self, invoice=invoice, desc=tx_desc)
show_transaction(tx, parent=self, desc=tx_desc)
def create_receive_tab(self):
# A 4-column grid layout. All the stretch is in the last column.
@ -1472,7 +1472,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.pay_lightning_invoice(invoice['invoice'], amount_sat=invoice['amount'])
elif invoice['type'] == PR_TYPE_ONCHAIN:
outputs = invoice['outputs']
self.pay_onchain_dialog(self.get_coins(), outputs, invoice=invoice)
self.pay_onchain_dialog(self.get_coins(), outputs)
else:
raise Exception('unknown invoice type')
@ -1492,7 +1492,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def pay_onchain_dialog(self, inputs: Sequence[PartialTxInput],
outputs: List[PartialTxOutput], *,
invoice=None, external_keypairs=None) -> None:
external_keypairs=None) -> None:
# trustedcoin requires this
if run_hook('abort_send', self):
return
@ -1508,8 +1508,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return
if self.config.get('advanced_preview'):
self.preview_tx_dialog(make_tx=make_tx,
external_keypairs=external_keypairs,
invoice=invoice)
external_keypairs=external_keypairs)
return
output_value = '!' if '!' in output_values else sum(output_values)
@ -1524,27 +1523,26 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if is_send:
def sign_done(success):
if success:
self.broadcast_or_show(tx, invoice=invoice)
self.broadcast_or_show(tx)
self.sign_tx_with_password(tx, callback=sign_done, password=password,
external_keypairs=external_keypairs)
else:
self.preview_tx_dialog(make_tx=make_tx,
external_keypairs=external_keypairs,
invoice=invoice)
external_keypairs=external_keypairs)
def preview_tx_dialog(self, *, make_tx, external_keypairs=None, invoice=None):
def preview_tx_dialog(self, *, make_tx, external_keypairs=None):
d = PreviewTxDialog(make_tx=make_tx, external_keypairs=external_keypairs,
window=self, invoice=invoice)
window=self)
d.show()
def broadcast_or_show(self, tx, *, invoice=None):
def broadcast_or_show(self, tx: Transaction):
if not self.network:
self.show_error(_("You can't broadcast a transaction without a live network connection."))
self.show_transaction(tx, invoice=invoice)
self.show_transaction(tx)
elif not tx.is_complete():
self.show_transaction(tx, invoice=invoice)
self.show_transaction(tx)
else:
self.broadcast_transaction(tx, invoice=invoice)
self.broadcast_transaction(tx)
@protected
def sign_tx(self, tx, *, callback, external_keypairs, password):
@ -1568,7 +1566,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
msg = _('Signing transaction...')
WaitingDialog(self, msg, task, on_success, on_failure)
def broadcast_transaction(self, tx: Transaction, *, invoice=None, tx_desc=None):
def broadcast_transaction(self, tx: Transaction):
def broadcast_thread():
# non-GUI thread
@ -1584,11 +1582,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return False, repr(e)
# success
txid = tx.txid()
if tx_desc:
self.wallet.set_label(txid, tx_desc)
if invoice:
self.wallet.set_paid(invoice['id'], txid)
self.wallet.set_label(txid, invoice['message'])
if pr:
self.payment_request = None
refund_address = self.wallet.get_receiving_address()
@ -2709,7 +2702,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
scriptpubkey = bfh(bitcoin.address_to_script(addr))
outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value='!')]
self.warn_if_watching_only()
self.pay_onchain_dialog(coins, outputs, invoice=None, external_keypairs=keypairs)
self.pay_onchain_dialog(coins, outputs, external_keypairs=keypairs)
def _do_import(self, title, header_layout, func):
text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True)

17
electrum/gui/qt/transaction_dialog.py

@ -75,9 +75,9 @@ _logger = get_logger(__name__)
dialogs = [] # Otherwise python randomly garbage collects the dialogs...
def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', invoice=None, desc=None, prompt_if_unsaved=False):
def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, prompt_if_unsaved=False):
try:
d = TxDialog(tx, parent=parent, invoice=invoice, desc=desc, prompt_if_unsaved=prompt_if_unsaved)
d = TxDialog(tx, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved)
except SerializationError as e:
_logger.exception('unable to deserialize the transaction')
parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
@ -88,7 +88,7 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', invoice=None,
class BaseTxDialog(QDialog, MessageBoxMixin):
def __init__(self, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved, finalized: bool, external_keypairs=None):
def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finalized: bool, external_keypairs=None):
'''Transactions in the wallet will show their description.
Pass desc to give a description for txs not yet in the wallet.
'''
@ -103,7 +103,6 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
self.prompt_if_unsaved = prompt_if_unsaved
self.saved = False
self.desc = desc
self.invoice = invoice
self.setMinimumWidth(950)
self.set_title()
@ -213,7 +212,7 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
def do_broadcast(self):
self.main_window.push_top_level_window(self)
try:
self.main_window.broadcast_transaction(self.tx, invoice=self.invoice, tx_desc=self.desc)
self.main_window.broadcast_transaction(self.tx)
finally:
self.main_window.pop_top_level_window(self)
self.saved = True
@ -592,8 +591,8 @@ class TxDetailLabel(QLabel):
class TxDialog(BaseTxDialog):
def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved):
BaseTxDialog.__init__(self, parent=parent, invoice=invoice, desc=desc, prompt_if_unsaved=prompt_if_unsaved, finalized=True)
def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved):
BaseTxDialog.__init__(self, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved, finalized=True)
self.set_tx(tx)
self.update()
@ -601,9 +600,9 @@ class TxDialog(BaseTxDialog):
class PreviewTxDialog(BaseTxDialog, TxEditor):
def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow', invoice):
def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow'):
TxEditor.__init__(self, window=window, make_tx=make_tx, is_sweep=bool(external_keypairs))
BaseTxDialog.__init__(self, parent=window, invoice=invoice, desc='', prompt_if_unsaved=False,
BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False,
finalized=False, external_keypairs=external_keypairs)
self.update_tx()
self.update()

51
electrum/json_db.py

@ -31,16 +31,16 @@ from collections import defaultdict
from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence
from . import util, bitcoin
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
from .keystore import bip44_derivation
from .transaction import Transaction
from .transaction import Transaction, TxOutpoint
from .logging import Logger
# seed_version is now used for the version of the wallet file
OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 21 # electrum >= 2.7 will set this to prevent
FINAL_SEED_VERSION = 22 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format
@ -215,6 +215,7 @@ class JsonDB(Logger):
self._convert_version_19()
self._convert_version_20()
self._convert_version_21()
self._convert_version_22()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks()
@ -496,6 +497,24 @@ class JsonDB(Logger):
self.put('channels', channels)
self.put('seed_version', 21)
def _convert_version_22(self):
# construct prevouts_by_scripthash
if not self._is_upgrade_method_needed(21, 21):
return
from .bitcoin import script_to_scripthash
transactions = self.get('transactions', {}) # txid -> raw_tx
prevouts_by_scripthash = defaultdict(list)
for txid, raw_tx in transactions.items():
tx = Transaction(raw_tx)
for idx, txout in enumerate(tx.outputs()):
outpoint = f"{txid}:{idx}"
scripthash = script_to_scripthash(txout.scriptpubkey.hex())
prevouts_by_scripthash[scripthash].append((outpoint, txout.value))
self.put('prevouts_by_scripthash', prevouts_by_scripthash)
self.put('seed_version', 22)
def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
@ -660,6 +679,25 @@ class JsonDB(Logger):
self.spent_outpoints[prevout_hash] = {}
self.spent_outpoints[prevout_hash][prevout_n] = tx_hash
@modifier
def add_prevout_by_scripthash(self, scripthash: str, *, prevout: TxOutpoint, value: int) -> None:
assert isinstance(prevout, TxOutpoint)
if scripthash not in self._prevouts_by_scripthash:
self._prevouts_by_scripthash[scripthash] = set()
self._prevouts_by_scripthash[scripthash].add((prevout.to_str(), value))
@modifier
def remove_prevout_by_scripthash(self, scripthash: str, *, prevout: TxOutpoint, value: int) -> None:
assert isinstance(prevout, TxOutpoint)
self._prevouts_by_scripthash[scripthash].discard((prevout.to_str(), value))
if not self._prevouts_by_scripthash[scripthash]:
self._prevouts_by_scripthash.pop(scripthash)
@locked
def get_prevouts_by_scripthash(self, scripthash: str) -> Set[Tuple[TxOutpoint, int]]:
prevouts_and_values = self._prevouts_by_scripthash.get(scripthash, set())
return {(TxOutpoint.from_str(prevout), value) for prevout, value in prevouts_and_values}
@modifier
def add_transaction(self, tx_hash: str, tx: Transaction) -> None:
assert isinstance(tx, Transaction)
@ -863,14 +901,19 @@ class JsonDB(Logger):
self.history = self.get_data_ref('addr_history') # address -> list of (txid, height)
self.verified_tx = self.get_data_ref('verified_tx3') # txid -> (height, timestamp, txpos, header_hash)
self.tx_fees = self.get_data_ref('tx_fees') # type: Dict[str, TxFeesValue]
# scripthash -> set of (outpoint, value)
self._prevouts_by_scripthash = self.get_data_ref('prevouts_by_scripthash') # type: Dict[str, Set[Tuple[str, int]]]
# convert raw hex transactions to Transaction objects
for tx_hash, raw_tx in self.transactions.items():
self.transactions[tx_hash] = Transaction(raw_tx)
# convert list to set
# convert txi, txo: list to set
for t in self.txi, self.txo:
for d in t.values():
for addr, lst in d.items():
d[addr] = set([tuple(x) for x in lst])
# convert prevouts_by_scripthash: list to set, list to tuple
for scripthash, lst in self._prevouts_by_scripthash.items():
self._prevouts_by_scripthash[scripthash] = {(prevout, value) for prevout, value in lst}
# remove unreferenced tx
for tx_hash in list(self.transactions.keys()):
if not self.get_txi_addresses(tx_hash) and not self.get_txo_addresses(tx_hash):

77
electrum/wallet.py

@ -36,9 +36,10 @@ import errno
import traceback
import operator
from functools import partial
from collections import defaultdict
from numbers import Number
from decimal import Decimal
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any, Set
from .i18n import _
from .bip32 import BIP32Node
@ -252,6 +253,7 @@ class Abstract_Wallet(AddressSynchronizer):
if invoice.get('type') == PR_TYPE_ONCHAIN:
outputs = [PartialTxOutput.from_legacy_tuple(*output) for output in invoice.get('outputs')]
invoice['outputs'] = outputs
self._prepare_onchain_invoice_paid_detection()
self.calc_unused_change_addresses()
# save wallet type the first time
if self.storage.get('wallet_type') is None:
@ -614,7 +616,10 @@ class Abstract_Wallet(AddressSynchronizer):
elif invoice_type == PR_TYPE_ONCHAIN:
key = bh2u(sha256(repr(invoice))[0:16])
invoice['id'] = key
invoice['txid'] = None
outputs = invoice['outputs'] # type: List[PartialTxOutput]
with self.transaction_lock:
for txout in outputs:
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
else:
raise Exception('Unsupported invoice type')
self.invoices[key] = invoice
@ -632,27 +637,73 @@ class Abstract_Wallet(AddressSynchronizer):
out.sort(key=operator.itemgetter('time'))
return out
def set_paid(self, key, txid):
if key not in self.invoices:
return
invoice = self.invoices[key]
assert invoice.get('type') == PR_TYPE_ONCHAIN
invoice['txid'] = txid
self.storage.put('invoices', self.invoices)
def get_invoice(self, key):
if key not in self.invoices:
return
item = copy.copy(self.invoices[key])
request_type = item.get('type')
if request_type == PR_TYPE_ONCHAIN:
item['status'] = PR_PAID if item.get('txid') is not None else PR_UNPAID
item['status'] = PR_PAID if self._is_onchain_invoice_paid(item)[0] else PR_UNPAID
elif self.lnworker and request_type == PR_TYPE_LN:
item['status'] = self.lnworker.get_payment_status(bfh(item['rhash']))
else:
return
return item
def _get_relevant_invoice_keys_for_tx(self, tx: Transaction) -> Set[str]:
relevant_invoice_keys = set()
for txout in tx.outputs():
for invoice_key in self._invoices_from_scriptpubkey_map.get(txout.scriptpubkey, set()):
relevant_invoice_keys.add(invoice_key)
return relevant_invoice_keys
def _prepare_onchain_invoice_paid_detection(self):
# scriptpubkey -> list(invoice_keys)
self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]]
for invoice_key, invoice in self.invoices.items():
if invoice.get('type') == PR_TYPE_ONCHAIN:
outputs = invoice['outputs'] # type: List[PartialTxOutput]
for txout in outputs:
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
def _is_onchain_invoice_paid(self, invoice) -> Tuple[bool, Sequence[str]]:
"""Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
assert invoice.get('type') == PR_TYPE_ONCHAIN
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
relevant_txs = []
with self.transaction_lock:
for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():
scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey.hex())
prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)
relevant_txs += [prevout.txid.hex() for prevout, v in prevouts_and_values]
total_received = sum([v for prevout, v in prevouts_and_values])
if total_received < invoice_amt:
return False, []
return True, relevant_txs
def _maybe_set_tx_label_based_on_invoices(self, tx: Transaction) -> bool:
tx_hash = tx.txid()
with self.transaction_lock:
labels = []
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'])
if labels:
self.set_label(tx_hash, "; ".join(labels))
return bool(labels)
def add_transaction(self, tx, *, allow_unrelated=False):
tx_was_added = super().add_transaction(tx, allow_unrelated=allow_unrelated)
if tx_was_added:
self._maybe_set_tx_label_based_on_invoices(tx)
return tx_was_added
@profiler
def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True):
transactions = OrderedDictWithIndex()
@ -1877,10 +1928,6 @@ class Imported_Wallet(Simple_Wallet):
self.db.remove_addr_history(address)
for tx_hash in transactions_to_remove:
self.remove_transaction(tx_hash)
self.db.remove_tx_fee(tx_hash)
self.db.remove_verified_tx(tx_hash)
self.unverified_tx.pop(tx_hash, None)
self.db.remove_transaction(tx_hash)
self.set_label(address, None)
self.remove_payment_request(address)
self.set_frozen_state_of_addresses([address], False)

Loading…
Cancel
Save