@ -23,7 +23,9 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# SOFTWARE.
import asyncio
import sys
import sys
import concurrent . futures
import copy
import copy
import datetime
import datetime
import traceback
import traceback
@ -32,7 +34,7 @@ from typing import TYPE_CHECKING, Callable, Optional, List, Union, Tuple
from functools import partial
from functools import partial
from decimal import Decimal
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 . QtGui import QTextCharFormat , QBrush , QFont , QPixmap , QCursor
from PyQt5 . QtWidgets import ( QDialog , QLabel , QPushButton , QHBoxLayout , QVBoxLayout , QWidget , QGridLayout ,
from PyQt5 . QtWidgets import ( QDialog , QLabel , QPushButton , QHBoxLayout , QVBoxLayout , QWidget , QGridLayout ,
QTextEdit , QFrame , QAction , QToolButton , QMenu , QCheckBox , QTextBrowser , QToolTip ,
QTextEdit , QFrame , QAction , QToolButton , QMenu , QCheckBox , QTextBrowser , QToolTip ,
@ -49,8 +51,9 @@ from electrum.i18n import _
from electrum . plugin import run_hook
from electrum . plugin import run_hook
from electrum import simple_config
from electrum import simple_config
from electrum . transaction import SerializationError , Transaction , PartialTransaction , PartialTxInput , TxOutpoint
from electrum . transaction import SerializationError , Transaction , PartialTransaction , PartialTxInput , TxOutpoint
from electrum . transaction import TxinDataFetchProgress
from electrum . logging import get_logger
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 electrum . network import Network
from . util import ( MessageBoxMixin , read_QIcon , Buttons , icon_path ,
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 ,
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX ,
BlockingWaitingDialog , getSaveFileName , ColorSchemeItem ,
BlockingWaitingDialog , getSaveFileName , ColorSchemeItem ,
get_iconname_qrcode )
get_iconname_qrcode )
from . rate_limiter import rate_limited
if TYPE_CHECKING :
if TYPE_CHECKING :
@ -106,6 +110,11 @@ class TxInOutWidget(QWidget):
self . inputs_textedit . textInteractionFlags ( ) | Qt . LinksAccessibleByMouse | Qt . LinksAccessibleByKeyboard )
self . inputs_textedit . textInteractionFlags ( ) | Qt . LinksAccessibleByMouse | Qt . LinksAccessibleByKeyboard )
self . inputs_textedit . setContextMenuPolicy ( Qt . CustomContextMenu )
self . inputs_textedit . setContextMenuPolicy ( Qt . CustomContextMenu )
self . inputs_textedit . customContextMenuRequested . connect ( self . on_context_menu_for_inputs )
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 (
self . txo_color_recv = TxOutputColoring (
legend = _ ( " Receiving Address " ) , color = ColorScheme . GREEN , tooltip = _ ( " Wallet receive address " ) )
legend = _ ( " Receiving Address " ) , color = ColorScheme . GREEN , tooltip = _ ( " Wallet receive address " ) )
self . txo_color_change = TxOutputColoring (
self . txo_color_change = TxOutputColoring (
@ -130,7 +139,7 @@ class TxInOutWidget(QWidget):
outheader_hbox . addWidget ( self . txo_color_2fa . legend_label )
outheader_hbox . addWidget ( self . txo_color_2fa . legend_label )
vbox = QVBoxLayout ( )
vbox = QVBoxLayout ( )
vbox . addWidge t ( self . inputs_ header )
vbox . addLayou t ( self . inheader_hbox )
vbox . addWidget ( self . inputs_textedit )
vbox . addWidget ( self . inputs_textedit )
vbox . addLayout ( outheader_hbox )
vbox . addLayout ( outheader_hbox )
vbox . addWidget ( self . outputs_textedit )
vbox . addWidget ( self . outputs_textedit )
@ -374,6 +383,8 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_uns
class TxDialog ( QDialog , MessageBoxMixin ) :
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 ) :
def __init__ ( self , tx : Transaction , * , parent : ' ElectrumWindow ' , prompt_if_unsaved , external_keypairs = None ) :
''' Transactions in the wallet will show their description.
''' Transactions in the wallet will show their description.
Pass desc to give a description for txs not yet in the wallet .
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 )
self . io_widget = TxInOutWidget ( self . main_window , self . wallet )
vbox . addWidget ( self . io_widget )
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 " ) )
self . sign_button = b = QPushButton ( _ ( " Sign " ) )
b . clicked . connect ( self . sign )
b . clicked . connect ( self . sign )
@ -461,6 +486,10 @@ class TxDialog(QDialog, MessageBoxMixin):
vbox . addLayout ( hbox )
vbox . addLayout ( hbox )
dialogs . append ( self )
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 . set_tx ( tx )
self . update ( )
self . update ( )
self . set_title ( )
self . set_title ( )
@ -479,13 +508,17 @@ class TxDialog(QDialog, MessageBoxMixin):
# or that a beyond-gap-limit address is is_mine.
# or that a beyond-gap-limit address is is_mine.
# note: this might fetch prev txs over the network.
# note: this might fetch prev txs over the network.
tx . add_info_from_wallet ( self . wallet )
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 ( ) :
if not tx . is_complete ( ) and tx . is_missing_info_from_network ( ) :
BlockingWaitingDialog (
BlockingWaitingDialog (
self ,
self ,
_ ( " Adding info to tx, from network... " ) ,
_ ( " 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 ) :
def do_broadcast ( self ) :
self . main_window . push_top_level_window ( self )
self . main_window . push_top_level_window ( self )
@ -507,6 +540,9 @@ class TxDialog(QDialog, MessageBoxMixin):
dialogs . remove ( self )
dialogs . remove ( self )
except ValueError :
except ValueError :
pass # was not in list already
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 ) :
def reject ( self ) :
# Override escape-key to close normally (and invoke closeEvent)
# Override escape-key to close normally (and invoke closeEvent)
@ -660,6 +696,10 @@ class TxDialog(QDialog, MessageBoxMixin):
return
return
self . update ( )
self . update ( )
@rate_limited ( 0.5 , ts_after = True )
def _throttled_update ( self ) :
self . update ( )
def update ( self ) :
def update ( self ) :
if self . tx is None :
if self . tx is None :
return
return
@ -742,25 +782,30 @@ class TxDialog(QDialog, MessageBoxMixin):
else :
else :
amount_str = _ ( " Amount sent: " ) + ' %s ' % format_amount ( - amount ) + ' ' + base_unit
amount_str = _ ( " Amount sent: " ) + ' %s ' % format_amount ( - amount ) + ' ' + base_unit
if fx . is_enabled ( ) :
if fx . is_enabled ( ) :
if tx_item_fiat :
if tx_item_fiat : # historical tx -> using historical price
amount_str + = ' ( %s ) ' % tx_item_fiat [ ' fiat_value ' ] . to_ui_string ( )
amount_str + = ' ( {} ) ' . format ( tx_item_fiat [ ' fiat_value ' ] . to_ui_string ( ) )
els e :
elif tx_d etails . is_related_to_wallet : # probably "tx preview" -> using current price
amount_str + = ' ( %s ) ' % format_fiat_and_units ( abs ( amount ) )
amount_str + = ' ( {} ) ' . format ( format_fiat_and_units ( abs ( amount ) ) )
if amount_str :
if amount_str :
self . amount_label . setText ( amount_str )
self . amount_label . setText ( amount_str )
else :
else :
self . amount_label . hide ( )
self . amount_label . hide ( )
size_str = _ ( " Size: " ) + ' %d bytes ' % size
size_str = _ ( " Size: " ) + ' %d bytes ' % size
if fee is None :
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 :
else :
fee_str = _ ( " Fee " ) + f ' : { format_amount ( fee ) } { base_unit } '
fee_str = _ ( " Fee " ) + f ' : { format_amount ( fee ) } { base_unit } '
if fx . is_enabled ( ) :
if fx . is_enabled ( ) :
if tx_item_fiat :
if tx_item_fiat : # historical tx -> using historical price
fiat_fee_str = tx_item_fiat [ ' fiat_fee ' ] . to_ui_string ( )
fee_str + = ' ( {} ) ' . format ( tx_item_fiat [ ' fiat_fee ' ] . to_ui_string ( ) )
else :
elif tx_details . is_related_to_wallet : # probably "tx preview" -> using current price
fiat_fee_str = format_fiat_and_units ( fee )
fee_str + = ' ( {} ) ' . format ( format_fiat_and_units ( fee ) )
fee_str + = f ' ( { fiat_fee_str } ) '
if fee is not None :
if fee is not None :
fee_rate = Decimal ( fee ) / size # sat/byte
fee_rate = Decimal ( fee ) / size # sat/byte
fee_str + = ' ( %s ) ' % self . main_window . format_fee_rate ( fee_rate * 1000 )
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 ) :
def update_fee_fields ( self ) :
pass # overridden in subclass
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 ) :
class TxDetailLabel ( QLabel ) :
def __init__ ( self , * , word_wrap = None ) :
def __init__ ( self , * , word_wrap = None ) :