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:
@@ -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()
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user