1
0

qt tx dialog: add checkbox "Download input data"

If checked, we download prev (parent) txs from the network, asynchronously.
This allows calculating the fee and showing "input addresses".

We could also SPV-verify the tx, to fill in missing tx_mined_status
(block height, blockhash, timestamp, short ids), but this is not done currently.
Note that there is no clean way to do this with electrum protocol 1.4:
`blockchain.transaction.get_merkle(tx_hash, height)` requires knowledge of the block height.

Loosely based on 6112fe0e51
This commit is contained in:
SomberNight
2023-03-12 00:19:39 +00:00
parent c79074c4d8
commit d83863cc52
5 changed files with 140 additions and 24 deletions

View File

@@ -134,7 +134,7 @@ class TxDialog(Factory.Popup):
tx.add_info_from_wallet(self.wallet)
if not tx.is_complete() and tx.is_missing_info_from_network():
Network.run_from_another_thread(
tx.add_info_from_network(self.wallet.network)) # FIXME is this needed?...
tx.add_info_from_network(self.wallet.network, timeout=10)) # FIXME is this needed?...
def on_open(self):
self.update()

View File

@@ -241,7 +241,7 @@ class QETxDetails(QObject, QtEventListener):
self._tx.add_info_from_wallet(self._wallet.wallet)
if not self._tx.is_complete() and self._tx.is_missing_info_from_network():
Network.run_from_another_thread(
self._tx.add_info_from_network(self._wallet.wallet.network)) # FIXME is this needed?...
self._tx.add_info_from_network(self._wallet.wallet.network, timeout=10)) # FIXME is this needed?...
self._inputs = list(map(lambda x: x.to_json(), self._tx.inputs()))
self._outputs = list(map(lambda x: {

View File

@@ -23,7 +23,9 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
import sys
import concurrent.futures
import copy
import datetime
import traceback
@@ -32,7 +34,7 @@ from typing import TYPE_CHECKING, Callable, Optional, List, Union, Tuple
from functools import partial
from decimal import Decimal
from PyQt5.QtCore import QSize, Qt, QUrl, QPoint
from PyQt5.QtCore import QSize, Qt, QUrl, QPoint, pyqtSignal
from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap, QCursor
from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget, QGridLayout,
QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox, QTextBrowser, QToolTip,
@@ -49,8 +51,9 @@ from electrum.i18n import _
from electrum.plugin import run_hook
from electrum import simple_config
from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, TxOutpoint
from electrum.transaction import TxinDataFetchProgress
from electrum.logging import get_logger
from electrum.util import ShortID
from electrum.util import ShortID, get_asyncio_loop
from electrum.network import Network
from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
@@ -60,6 +63,7 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX,
BlockingWaitingDialog, getSaveFileName, ColorSchemeItem,
get_iconname_qrcode)
from .rate_limiter import rate_limited
if TYPE_CHECKING:
@@ -106,6 +110,11 @@ class TxInOutWidget(QWidget):
self.inputs_textedit.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
self.inputs_textedit.setContextMenuPolicy(Qt.CustomContextMenu)
self.inputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_inputs)
self.inheader_hbox = QHBoxLayout()
self.inheader_hbox.setContentsMargins(0, 0, 0, 0)
self.inheader_hbox.addWidget(self.inputs_header)
self.txo_color_recv = TxOutputColoring(
legend=_("Receiving Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receive address"))
self.txo_color_change = TxOutputColoring(
@@ -130,7 +139,7 @@ class TxInOutWidget(QWidget):
outheader_hbox.addWidget(self.txo_color_2fa.legend_label)
vbox = QVBoxLayout()
vbox.addWidget(self.inputs_header)
vbox.addLayout(self.inheader_hbox)
vbox.addWidget(self.inputs_textedit)
vbox.addLayout(outheader_hbox)
vbox.addWidget(self.outputs_textedit)
@@ -374,6 +383,8 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_uns
class TxDialog(QDialog, MessageBoxMixin):
throttled_update_sig = pyqtSignal() # emit from thread to do update in main thread
def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsaved, external_keypairs=None):
'''Transactions in the wallet will show their description.
Pass desc to give a description for txs not yet in the wallet.
@@ -408,6 +419,20 @@ class TxDialog(QDialog, MessageBoxMixin):
self.io_widget = TxInOutWidget(self.main_window, self.wallet)
vbox.addWidget(self.io_widget)
# add "fetch_txin_data" checkbox to io_widget
fetch_txin_data_cb = QCheckBox(_('Download input data'))
fetch_txin_data_cb.setChecked(bool(self.config.get('tx_dialog_fetch_txin_data', False)))
fetch_txin_data_cb.setToolTip(_('Download parent transactions from the network.\n'
'Allows filling in missing fee and address details.'))
def on_fetch_txin_data_cb(x):
self.config.set_key('tx_dialog_fetch_txin_data', bool(x))
if x:
self.initiate_fetch_txin_data()
fetch_txin_data_cb.stateChanged.connect(on_fetch_txin_data_cb)
self.io_widget.inheader_hbox.addStretch(1)
self.io_widget.inheader_hbox.addWidget(fetch_txin_data_cb)
self.io_widget.inheader_hbox.addStretch(10)
self.sign_button = b = QPushButton(_("Sign"))
b.clicked.connect(self.sign)
@@ -461,6 +486,10 @@ class TxDialog(QDialog, MessageBoxMixin):
vbox.addLayout(hbox)
dialogs.append(self)
self._fetch_txin_data_fut = None # type: Optional[concurrent.futures.Future]
self._fetch_txin_data_progress = None # type: Optional[TxinDataFetchProgress]
self.throttled_update_sig.connect(self._throttled_update, Qt.QueuedConnection)
self.set_tx(tx)
self.update()
self.set_title()
@@ -479,13 +508,17 @@ class TxDialog(QDialog, MessageBoxMixin):
# or that a beyond-gap-limit address is is_mine.
# note: this might fetch prev txs over the network.
tx.add_info_from_wallet(self.wallet)
# TODO fetch prev txs for any tx; guarded with a config key
# FIXME for PSBTs, we do a blocking fetch, as the missing data might be needed for e.g. signing
# - otherwise, the missing data is for display-completeness only, e.g. fee, input addresses (we do it async)
if not tx.is_complete() and tx.is_missing_info_from_network():
BlockingWaitingDialog(
self,
_("Adding info to tx, from network..."),
lambda: Network.run_from_another_thread(tx.add_info_from_network(self.wallet.network)),
lambda: Network.run_from_another_thread(
tx.add_info_from_network(self.wallet.network, timeout=10)),
)
elif self.config.get('tx_dialog_fetch_txin_data', False):
self.initiate_fetch_txin_data()
def do_broadcast(self):
self.main_window.push_top_level_window(self)
@@ -507,6 +540,9 @@ class TxDialog(QDialog, MessageBoxMixin):
dialogs.remove(self)
except ValueError:
pass # was not in list already
if self._fetch_txin_data_fut:
self._fetch_txin_data_fut.cancel()
self._fetch_txin_data_fut = None
def reject(self):
# Override escape-key to close normally (and invoke closeEvent)
@@ -660,6 +696,10 @@ class TxDialog(QDialog, MessageBoxMixin):
return
self.update()
@rate_limited(0.5, ts_after=True)
def _throttled_update(self):
self.update()
def update(self):
if self.tx is None:
return
@@ -742,25 +782,30 @@ class TxDialog(QDialog, MessageBoxMixin):
else:
amount_str = _("Amount sent:") + ' %s' % format_amount(-amount) + ' ' + base_unit
if fx.is_enabled():
if tx_item_fiat:
amount_str += ' (%s)' % tx_item_fiat['fiat_value'].to_ui_string()
else:
amount_str += ' (%s)' % format_fiat_and_units(abs(amount))
if tx_item_fiat: # historical tx -> using historical price
amount_str += ' ({})'.format(tx_item_fiat['fiat_value'].to_ui_string())
elif tx_details.is_related_to_wallet: # probably "tx preview" -> using current price
amount_str += ' ({})'.format(format_fiat_and_units(abs(amount)))
if amount_str:
self.amount_label.setText(amount_str)
else:
self.amount_label.hide()
size_str = _("Size:") + ' %d bytes'% size
if fee is None:
fee_str = _("Fee") + ': ' + _("unknown")
if prog := self._fetch_txin_data_progress:
if not prog.has_errored:
fee_str = _("Downloading input data...") + f" ({prog.num_tasks_done}/{prog.num_tasks_total})"
else:
fee_str = _("Downloading input data...") + f" error."
else:
fee_str = _("Fee") + ': ' + _("unknown")
else:
fee_str = _("Fee") + f': {format_amount(fee)} {base_unit}'
if fx.is_enabled():
if tx_item_fiat:
fiat_fee_str = tx_item_fiat['fiat_fee'].to_ui_string()
else:
fiat_fee_str = format_fiat_and_units(fee)
fee_str += f' ({fiat_fee_str})'
if tx_item_fiat: # historical tx -> using historical price
fee_str += ' ({})'.format(tx_item_fiat['fiat_fee'].to_ui_string())
elif tx_details.is_related_to_wallet: # probably "tx preview" -> using current price
fee_str += ' ({})'.format(format_fiat_and_units(fee))
if fee is not None:
fee_rate = Decimal(fee) / size # sat/byte
fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate * 1000)
@@ -887,6 +932,30 @@ class TxDialog(QDialog, MessageBoxMixin):
def update_fee_fields(self):
pass # overridden in subclass
def initiate_fetch_txin_data(self):
"""Download missing input data from the network, asynchronously.
Note: we fetch the prev txs, which allows calculating the fee and showing "input addresses".
We could also SPV-verify the tx, to fill in missing tx_mined_status (block height, blockhash, timestamp),
but this is not done currently.
"""
tx = self.tx
if not tx:
return
if self._fetch_txin_data_fut is not None:
return
network = self.wallet.network
def progress_cb(prog: TxinDataFetchProgress):
self._fetch_txin_data_progress = prog
self.throttled_update_sig.emit()
async def wrapper():
try:
await tx.add_info_from_network(network, progress_cb=progress_cb)
finally:
self._fetch_txin_data_fut = None
self._fetch_txin_data_progress = None
self._fetch_txin_data_fut = asyncio.run_coroutine_threadsafe(wrapper(), get_asyncio_loop())
class TxDetailLabel(QLabel):
def __init__(self, *, word_wrap=None):