URI: 
       ttransaction_dialog.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       ttransaction_dialog.py (43064B)
       ---
            1 #!/usr/bin/env python
            2 #
            3 # Electrum - lightweight Bitcoin client
            4 # Copyright (C) 2012 thomasv@gitorious
            5 #
            6 # Permission is hereby granted, free of charge, to any person
            7 # obtaining a copy of this software and associated documentation files
            8 # (the "Software"), to deal in the Software without restriction,
            9 # including without limitation the rights to use, copy, modify, merge,
           10 # publish, distribute, sublicense, and/or sell copies of the Software,
           11 # and to permit persons to whom the Software is furnished to do so,
           12 # subject to the following conditions:
           13 #
           14 # The above copyright notice and this permission notice shall be
           15 # included in all copies or substantial portions of the Software.
           16 #
           17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
           18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
           19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
           20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
           21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
           22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
           23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
           24 # SOFTWARE.
           25 
           26 import sys
           27 import copy
           28 import datetime
           29 import traceback
           30 import time
           31 from typing import TYPE_CHECKING, Callable, Optional, List, Union
           32 from functools import partial
           33 from decimal import Decimal
           34 
           35 from PyQt5.QtCore import QSize, Qt
           36 from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap
           37 from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget, QGridLayout,
           38                              QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox)
           39 import qrcode
           40 from qrcode import exceptions
           41 
           42 from electrum.simple_config import SimpleConfig
           43 from electrum.util import quantize_feerate
           44 from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX
           45 from electrum.i18n import _
           46 from electrum.plugin import run_hook
           47 from electrum import simple_config
           48 from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput
           49 from electrum.logging import get_logger
           50 
           51 from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
           52                    MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog,
           53                    char_width_in_lineedit, TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE,
           54                    TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX,
           55                    TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX,
           56                    BlockingWaitingDialog, getSaveFileName, ColorSchemeItem)
           57 
           58 from .fee_slider import FeeSlider, FeeComboBox
           59 from .confirm_tx_dialog import TxEditor
           60 from .amountedit import FeerateEdit, BTCAmountEdit
           61 from .locktimeedit import LockTimeEdit
           62 
           63 if TYPE_CHECKING:
           64     from .main_window import ElectrumWindow
           65 
           66 
           67 class TxSizeLabel(QLabel):
           68     def setAmount(self, byte_size):
           69         self.setText(('x   %s bytes   =' % byte_size) if byte_size else '')
           70 
           71 class TxFiatLabel(QLabel):
           72     def setAmount(self, fiat_fee):
           73         self.setText(('≈  %s' % fiat_fee) if fiat_fee else '')
           74 
           75 class QTextEditWithDefaultSize(QTextEdit):
           76     def sizeHint(self):
           77         return QSize(0, 100)
           78 
           79 
           80 
           81 _logger = get_logger(__name__)
           82 dialogs = []  # Otherwise python randomly garbage collects the dialogs...
           83 
           84 
           85 def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, prompt_if_unsaved=False):
           86     try:
           87         d = TxDialog(tx, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved)
           88     except SerializationError as e:
           89         _logger.exception('unable to deserialize the transaction')
           90         parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
           91     else:
           92         d.show()
           93 
           94 
           95 
           96 class BaseTxDialog(QDialog, MessageBoxMixin):
           97 
           98     def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finalized: bool, external_keypairs=None):
           99         '''Transactions in the wallet will show their description.
          100         Pass desc to give a description for txs not yet in the wallet.
          101         '''
          102         # We want to be a top-level window
          103         QDialog.__init__(self, parent=None)
          104         self.tx = None  # type: Optional[Transaction]
          105         self.external_keypairs = external_keypairs
          106         self.finalized = finalized
          107         self.main_window = parent
          108         self.config = parent.config
          109         self.wallet = parent.wallet
          110         self.prompt_if_unsaved = prompt_if_unsaved
          111         self.saved = False
          112         self.desc = desc
          113         self.setMinimumWidth(950)
          114         self.set_title()
          115 
          116         self.psbt_only_widgets = []  # type: List[QWidget]
          117 
          118         vbox = QVBoxLayout()
          119         self.setLayout(vbox)
          120 
          121         vbox.addWidget(QLabel(_("Transaction ID:")))
          122         self.tx_hash_e  = ButtonsLineEdit()
          123         qr_show = lambda: parent.show_qrcode(str(self.tx_hash_e.text()), 'Transaction ID', parent=self)
          124         qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png"
          125         self.tx_hash_e.addButton(qr_icon, qr_show, _("Show as QR code"))
          126         self.tx_hash_e.setReadOnly(True)
          127         vbox.addWidget(self.tx_hash_e)
          128 
          129         self.add_tx_stats(vbox)
          130 
          131         vbox.addSpacing(10)
          132 
          133         self.inputs_header = QLabel()
          134         vbox.addWidget(self.inputs_header)
          135         self.inputs_textedit = QTextEditWithDefaultSize()
          136         vbox.addWidget(self.inputs_textedit)
          137 
          138         self.txo_color_recv = TxOutputColoring(
          139             legend=_("Receiving Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receive address"))
          140         self.txo_color_change = TxOutputColoring(
          141             legend=_("Change Address"), color=ColorScheme.YELLOW, tooltip=_("Wallet change address"))
          142         self.txo_color_2fa = TxOutputColoring(
          143             legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions"))
          144 
          145         outheader_hbox = QHBoxLayout()
          146         outheader_hbox.setContentsMargins(0, 0, 0, 0)
          147         vbox.addLayout(outheader_hbox)
          148         self.outputs_header = QLabel()
          149         outheader_hbox.addWidget(self.outputs_header)
          150         outheader_hbox.addStretch(2)
          151         outheader_hbox.addWidget(self.txo_color_recv.legend_label)
          152         outheader_hbox.addWidget(self.txo_color_change.legend_label)
          153         outheader_hbox.addWidget(self.txo_color_2fa.legend_label)
          154 
          155         self.outputs_textedit = QTextEditWithDefaultSize()
          156         vbox.addWidget(self.outputs_textedit)
          157 
          158         self.sign_button = b = QPushButton(_("Sign"))
          159         b.clicked.connect(self.sign)
          160 
          161         self.broadcast_button = b = QPushButton(_("Broadcast"))
          162         b.clicked.connect(self.do_broadcast)
          163 
          164         self.save_button = b = QPushButton(_("Save"))
          165         b.clicked.connect(self.save)
          166 
          167         self.cancel_button = b = QPushButton(_("Close"))
          168         b.clicked.connect(self.close)
          169         b.setDefault(True)
          170 
          171         self.export_actions_menu = export_actions_menu = QMenu()
          172         self.add_export_actions_to_menu(export_actions_menu)
          173         export_actions_menu.addSeparator()
          174         export_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates"))
          175         self.add_export_actions_to_menu(export_submenu, gettx=self._gettx_for_coinjoin)
          176         self.psbt_only_widgets.append(export_submenu)
          177         export_submenu = export_actions_menu.addMenu(_("For hardware device; include xpubs"))
          178         self.add_export_actions_to_menu(export_submenu, gettx=self._gettx_for_hardware_device)
          179         self.psbt_only_widgets.append(export_submenu)
          180 
          181         self.export_actions_button = QToolButton()
          182         self.export_actions_button.setText(_("Export"))
          183         self.export_actions_button.setMenu(export_actions_menu)
          184         self.export_actions_button.setPopupMode(QToolButton.InstantPopup)
          185 
          186         self.finalize_button = QPushButton(_('Finalize'))
          187         self.finalize_button.clicked.connect(self.on_finalize)
          188 
          189         partial_tx_actions_menu = QMenu()
          190         ptx_merge_sigs_action = QAction(_("Merge signatures from"), self)
          191         ptx_merge_sigs_action.triggered.connect(self.merge_sigs)
          192         partial_tx_actions_menu.addAction(ptx_merge_sigs_action)
          193         self._ptx_join_txs_action = QAction(_("Join inputs/outputs"), self)
          194         self._ptx_join_txs_action.triggered.connect(self.join_tx_with_another)
          195         partial_tx_actions_menu.addAction(self._ptx_join_txs_action)
          196         self.partial_tx_actions_button = QToolButton()
          197         self.partial_tx_actions_button.setText(_("Combine"))
          198         self.partial_tx_actions_button.setMenu(partial_tx_actions_menu)
          199         self.partial_tx_actions_button.setPopupMode(QToolButton.InstantPopup)
          200         self.psbt_only_widgets.append(self.partial_tx_actions_button)
          201 
          202         # Action buttons
          203         self.buttons = [self.partial_tx_actions_button, self.sign_button, self.broadcast_button, self.cancel_button]
          204         # Transaction sharing buttons
          205         self.sharing_buttons = [self.finalize_button, self.export_actions_button, self.save_button]
          206         run_hook('transaction_dialog', self)
          207         if not self.finalized:
          208             self.create_fee_controls()
          209             vbox.addWidget(self.feecontrol_fields)
          210         self.hbox = hbox = QHBoxLayout()
          211         hbox.addLayout(Buttons(*self.sharing_buttons))
          212         hbox.addStretch(1)
          213         hbox.addLayout(Buttons(*self.buttons))
          214         vbox.addLayout(hbox)
          215         self.set_buttons_visibility()
          216 
          217         dialogs.append(self)
          218 
          219     def set_buttons_visibility(self):
          220         for b in [self.export_actions_button, self.save_button, self.sign_button, self.broadcast_button, self.partial_tx_actions_button]:
          221             b.setVisible(self.finalized)
          222         for b in [self.finalize_button]:
          223             b.setVisible(not self.finalized)
          224 
          225     def set_tx(self, tx: 'Transaction'):
          226         # Take a copy; it might get updated in the main window by
          227         # e.g. the FX plugin.  If this happens during or after a long
          228         # sign operation the signatures are lost.
          229         self.tx = tx = copy.deepcopy(tx)
          230         try:
          231             self.tx.deserialize()
          232         except BaseException as e:
          233             raise SerializationError(e)
          234         # If the wallet can populate the inputs with more info, do it now.
          235         # As a result, e.g. we might learn an imported address tx is segwit,
          236         # or that a beyond-gap-limit address is is_mine.
          237         # note: this might fetch prev txs over the network.
          238         BlockingWaitingDialog(
          239             self,
          240             _("Adding info to tx, from wallet and network..."),
          241             lambda: tx.add_info_from_wallet(self.wallet),
          242         )
          243 
          244     def do_broadcast(self):
          245         self.main_window.push_top_level_window(self)
          246         self.main_window.save_pending_invoice()
          247         try:
          248             self.main_window.broadcast_transaction(self.tx)
          249         finally:
          250             self.main_window.pop_top_level_window(self)
          251         self.saved = True
          252         self.update()
          253 
          254     def closeEvent(self, event):
          255         if (self.prompt_if_unsaved and not self.saved
          256                 and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
          257             event.ignore()
          258         else:
          259             event.accept()
          260             try:
          261                 dialogs.remove(self)
          262             except ValueError:
          263                 pass  # was not in list already
          264 
          265     def reject(self):
          266         # Override escape-key to close normally (and invoke closeEvent)
          267         self.close()
          268 
          269     def add_export_actions_to_menu(self, menu: QMenu, *, gettx: Callable[[], Transaction] = None) -> None:
          270         if gettx is None:
          271             gettx = lambda: None
          272 
          273         action = QAction(_("Copy to clipboard"), self)
          274         action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx()))
          275         menu.addAction(action)
          276 
          277         qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png"
          278         action = QAction(read_QIcon(qr_icon), _("Show as QR code"), self)
          279         action.triggered.connect(lambda: self.show_qr(tx=gettx()))
          280         menu.addAction(action)
          281 
          282         action = QAction(_("Export to file"), self)
          283         action.triggered.connect(lambda: self.export_to_file(tx=gettx()))
          284         menu.addAction(action)
          285 
          286     def _gettx_for_coinjoin(self) -> PartialTransaction:
          287         if not isinstance(self.tx, PartialTransaction):
          288             raise Exception("Can only export partial transactions for coinjoins.")
          289         tx = copy.deepcopy(self.tx)
          290         tx.prepare_for_export_for_coinjoin()
          291         return tx
          292 
          293     def _gettx_for_hardware_device(self) -> PartialTransaction:
          294         if not isinstance(self.tx, PartialTransaction):
          295             raise Exception("Can only export partial transactions for hardware device.")
          296         tx = copy.deepcopy(self.tx)
          297         tx.add_info_from_wallet(self.wallet, include_xpubs=True)
          298         # log warning if PSBT_*_BIP32_DERIVATION fields cannot be filled with full path due to missing info
          299         from electrum.keystore import Xpub
          300         def is_ks_missing_info(ks):
          301             return (isinstance(ks, Xpub) and (ks.get_root_fingerprint() is None
          302                                               or ks.get_derivation_prefix() is None))
          303         if any([is_ks_missing_info(ks) for ks in self.wallet.get_keystores()]):
          304             _logger.warning('PSBT was requested to be filled with full bip32 paths but '
          305                             'some keystores lacked either the derivation prefix or the root fingerprint')
          306         return tx
          307 
          308     def copy_to_clipboard(self, *, tx: Transaction = None):
          309         if tx is None:
          310             tx = self.tx
          311         self.main_window.do_copy(str(tx), title=_("Transaction"))
          312 
          313     def show_qr(self, *, tx: Transaction = None):
          314         if tx is None:
          315             tx = self.tx
          316         qr_data = tx.to_qr_data()
          317         try:
          318             self.main_window.show_qrcode(qr_data, 'Transaction', parent=self)
          319         except qrcode.exceptions.DataOverflowError:
          320             self.show_error(_('Failed to display QR code.') + '\n' +
          321                             _('Transaction is too large in size.'))
          322         except Exception as e:
          323             self.show_error(_('Failed to display QR code.') + '\n' + repr(e))
          324 
          325     def sign(self):
          326         def sign_done(success):
          327             if self.tx.is_complete():
          328                 self.prompt_if_unsaved = True
          329                 self.saved = False
          330             self.update()
          331             self.main_window.pop_top_level_window(self)
          332 
          333         self.sign_button.setDisabled(True)
          334         self.main_window.push_top_level_window(self)
          335         self.main_window.sign_tx(self.tx, callback=sign_done, external_keypairs=self.external_keypairs)
          336 
          337     def save(self):
          338         self.main_window.push_top_level_window(self)
          339         if self.main_window.save_transaction_into_wallet(self.tx):
          340             self.save_button.setDisabled(True)
          341             self.saved = True
          342         self.main_window.pop_top_level_window(self)
          343 
          344     def export_to_file(self, *, tx: Transaction = None):
          345         if tx is None:
          346             tx = self.tx
          347         if isinstance(tx, PartialTransaction):
          348             tx.finalize_psbt()
          349         if tx.is_complete():
          350             name = 'signed_%s' % (tx.txid()[0:8])
          351             extension = 'txn'
          352             default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX
          353         else:
          354             name = self.wallet.basename() + time.strftime('-%Y%m%d-%H%M')
          355             extension = 'psbt'
          356             default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX
          357         name = f'{name}.{extension}'
          358         fileName = getSaveFileName(
          359             parent=self,
          360             title=_("Select where to save your transaction"),
          361             filename=name,
          362             filter=TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE,
          363             default_extension=extension,
          364             default_filter=default_filter,
          365             config=self.config,
          366         )
          367         if not fileName:
          368             return
          369         if tx.is_complete():  # network tx hex
          370             with open(fileName, "w+") as f:
          371                 network_tx_hex = tx.serialize_to_network()
          372                 f.write(network_tx_hex + '\n')
          373         else:  # if partial: PSBT bytes
          374             assert isinstance(tx, PartialTransaction)
          375             with open(fileName, "wb+") as f:
          376                 f.write(tx.serialize_as_bytes())
          377 
          378         self.show_message(_("Transaction exported successfully"))
          379         self.saved = True
          380 
          381     def merge_sigs(self):
          382         if not isinstance(self.tx, PartialTransaction):
          383             return
          384         text = text_dialog(
          385             parent=self,
          386             title=_('Input raw transaction'),
          387             header_layout=_("Transaction to merge signatures from") + ":",
          388             ok_label=_("Load transaction"),
          389             config=self.config,
          390         )
          391         if not text:
          392             return
          393         tx = self.main_window.tx_from_text(text)
          394         if not tx:
          395             return
          396         try:
          397             self.tx.combine_with_other_psbt(tx)
          398         except Exception as e:
          399             self.show_error(_("Error combining partial transactions") + ":\n" + repr(e))
          400             return
          401         self.update()
          402 
          403     def join_tx_with_another(self):
          404         if not isinstance(self.tx, PartialTransaction):
          405             return
          406         text = text_dialog(
          407             parent=self,
          408             title=_('Input raw transaction'),
          409             header_layout=_("Transaction to join with") + " (" + _("add inputs and outputs") + "):",
          410             ok_label=_("Load transaction"),
          411             config=self.config,
          412         )
          413         if not text:
          414             return
          415         tx = self.main_window.tx_from_text(text)
          416         if not tx:
          417             return
          418         try:
          419             self.tx.join_with_other_psbt(tx)
          420         except Exception as e:
          421             self.show_error(_("Error joining partial transactions") + ":\n" + repr(e))
          422             return
          423         self.update()
          424 
          425     def update(self):
          426         if not self.finalized:
          427             self.update_fee_fields()
          428             self.finalize_button.setEnabled(self.can_finalize())
          429         if self.tx is None:
          430             return
          431         self.update_io()
          432         desc = self.desc
          433         base_unit = self.main_window.base_unit()
          434         format_amount = self.main_window.format_amount
          435         format_fiat_and_units = self.main_window.format_fiat_and_units
          436         tx_details = self.wallet.get_tx_info(self.tx)
          437         tx_mined_status = tx_details.tx_mined_status
          438         exp_n = tx_details.mempool_depth_bytes
          439         amount, fee = tx_details.amount, tx_details.fee
          440         size = self.tx.estimated_size()
          441         txid = self.tx.txid()
          442         fx = self.main_window.fx
          443         tx_item_fiat = None
          444         if (self.finalized  # ensures we don't use historical rates for tx being constructed *now*
          445                 and txid is not None and fx.is_enabled() and amount is not None):
          446             tx_item_fiat = self.wallet.get_tx_item_fiat(
          447                 tx_hash=txid, amount_sat=abs(amount), fx=fx, tx_fee=fee)
          448         lnworker_history = self.wallet.lnworker.get_onchain_history() if self.wallet.lnworker else {}
          449         if txid in lnworker_history:
          450             item = lnworker_history[txid]
          451             ln_amount = item['amount_msat'] / 1000
          452             if amount is None:
          453                 tx_mined_status = self.wallet.lnworker.lnwatcher.get_tx_height(txid)
          454         else:
          455             ln_amount = None
          456         self.broadcast_button.setEnabled(tx_details.can_broadcast)
          457         can_sign = not self.tx.is_complete() and \
          458             (self.wallet.can_sign(self.tx) or bool(self.external_keypairs))
          459         self.sign_button.setEnabled(can_sign)
          460         if self.finalized and tx_details.txid:
          461             self.tx_hash_e.setText(tx_details.txid)
          462         else:
          463             # note: when not finalized, RBF and locktime changes do not trigger
          464             #       a make_tx, so the txid is unreliable, hence:
          465             self.tx_hash_e.setText(_('Unknown'))
          466         if not desc:
          467             self.tx_desc.hide()
          468         else:
          469             self.tx_desc.setText(_("Description") + ': ' + desc)
          470             self.tx_desc.show()
          471         self.status_label.setText(_('Status:') + ' ' + tx_details.status)
          472 
          473         if tx_mined_status.timestamp:
          474             time_str = datetime.datetime.fromtimestamp(tx_mined_status.timestamp).isoformat(' ')[:-3]
          475             self.date_label.setText(_("Date: {}").format(time_str))
          476             self.date_label.show()
          477         elif exp_n is not None:
          478             text = '%.2f MB'%(exp_n/1000000)
          479             self.date_label.setText(_('Position in mempool: {} from tip').format(text))
          480             self.date_label.show()
          481         else:
          482             self.date_label.hide()
          483         if self.tx.locktime <= NLOCKTIME_BLOCKHEIGHT_MAX:
          484             locktime_final_str = f"LockTime: {self.tx.locktime} (height)"
          485         else:
          486             locktime_final_str = f"LockTime: {self.tx.locktime} ({datetime.datetime.fromtimestamp(self.tx.locktime)})"
          487         self.locktime_final_label.setText(locktime_final_str)
          488         if self.locktime_e.get_locktime() is None:
          489             self.locktime_e.set_locktime(self.tx.locktime)
          490         self.rbf_label.setText(_('Replace by fee') + f": {not self.tx.is_final()}")
          491 
          492         if tx_mined_status.header_hash:
          493             self.block_hash_label.setText(_("Included in block: {}")
          494                                           .format(tx_mined_status.header_hash))
          495             self.block_height_label.setText(_("At block height: {}")
          496                                             .format(tx_mined_status.height))
          497         else:
          498             self.block_hash_label.hide()
          499             self.block_height_label.hide()
          500         if amount is None and ln_amount is None:
          501             amount_str = _("Transaction unrelated to your wallet")
          502         elif amount is None:
          503             amount_str = ''
          504         else:
          505             if amount > 0:
          506                 amount_str = _("Amount received:") + ' %s'% format_amount(amount) + ' ' + base_unit
          507             else:
          508                 amount_str = _("Amount sent:") + ' %s' % format_amount(-amount) + ' ' + base_unit
          509             if fx.is_enabled():
          510                 if tx_item_fiat:
          511                     amount_str += ' (%s)' % tx_item_fiat['fiat_value'].to_ui_string()
          512                 else:
          513                     amount_str += ' (%s)' % format_fiat_and_units(abs(amount))
          514         if amount_str:
          515             self.amount_label.setText(amount_str)
          516         else:
          517             self.amount_label.hide()
          518         size_str = _("Size:") + ' %d bytes'% size
          519         if fee is None:
          520             fee_str = _("Fee") + ': ' + _("unknown")
          521         else:
          522             fee_str = _("Fee") + f': {format_amount(fee)} {base_unit}'
          523             if fx.is_enabled():
          524                 if tx_item_fiat:
          525                     fiat_fee_str = tx_item_fiat['fiat_fee'].to_ui_string()
          526                 else:
          527                     fiat_fee_str = format_fiat_and_units(fee)
          528                 fee_str += f' ({fiat_fee_str})'
          529         if fee is not None:
          530             fee_rate = Decimal(fee) / size  # sat/byte
          531             fee_str += '  ( %s ) ' % self.main_window.format_fee_rate(fee_rate * 1000)
          532             if isinstance(self.tx, PartialTransaction):
          533                 if isinstance(self, PreviewTxDialog):
          534                     invoice_amt = self.tx.output_value() if self.output_value == '!' else self.output_value
          535                 else:
          536                     invoice_amt = amount
          537                 fee_warning_tuple = self.wallet.get_tx_fee_warning(
          538                     invoice_amt=invoice_amt, tx_size=size, fee=fee)
          539                 if fee_warning_tuple:
          540                     allow_send, long_warning, short_warning = fee_warning_tuple
          541                     fee_str += " - <font color={color}>{header}: {body}</font>".format(
          542                         header=_('Warning'),
          543                         body=short_warning,
          544                         color=ColorScheme.RED.as_color().name(),
          545                     )
          546         if isinstance(self.tx, PartialTransaction):
          547             risk_of_burning_coins = (can_sign and fee is not None
          548                                      and self.wallet.get_warning_for_risk_of_burning_coins_as_fees(self.tx))
          549             self.fee_warning_icon.setToolTip(str(risk_of_burning_coins))
          550             self.fee_warning_icon.setVisible(bool(risk_of_burning_coins))
          551         self.fee_label.setText(fee_str)
          552         self.size_label.setText(size_str)
          553         if ln_amount is None or ln_amount == 0:
          554             ln_amount_str = ''
          555         elif ln_amount > 0:
          556             ln_amount_str = _('Amount received in channels') + ': ' + format_amount(ln_amount) + ' ' + base_unit
          557         else:
          558             assert ln_amount < 0, f"{ln_amount!r}"
          559             ln_amount_str = _('Amount withdrawn from channels') + ': ' + format_amount(-ln_amount) + ' ' + base_unit
          560         if ln_amount_str:
          561             self.ln_amount_label.setText(ln_amount_str)
          562         else:
          563             self.ln_amount_label.hide()
          564         show_psbt_only_widgets = self.finalized and isinstance(self.tx, PartialTransaction)
          565         for widget in self.psbt_only_widgets:
          566             if isinstance(widget, QMenu):
          567                 widget.menuAction().setVisible(show_psbt_only_widgets)
          568             else:
          569                 widget.setVisible(show_psbt_only_widgets)
          570         if tx_details.is_lightning_funding_tx:
          571             self._ptx_join_txs_action.setEnabled(False)  # would change txid
          572 
          573         self.save_button.setEnabled(tx_details.can_save_as_local)
          574         if tx_details.can_save_as_local:
          575             self.save_button.setToolTip(_("Save transaction offline"))
          576         else:
          577             self.save_button.setToolTip(_("Transaction already saved or not yet signed."))
          578 
          579         run_hook('transaction_dialog_update', self)
          580 
          581     def update_io(self):
          582         inputs_header_text = _("Inputs") + ' (%d)'%len(self.tx.inputs())
          583         if not self.finalized:
          584             selected_coins = self.main_window.get_manually_selected_coins()
          585             if selected_coins is not None:
          586                 inputs_header_text += f"  -  " + _("Coin selection active ({} UTXOs selected)").format(len(selected_coins))
          587         self.inputs_header.setText(inputs_header_text)
          588 
          589         ext = QTextCharFormat()
          590         tf_used_recv, tf_used_change, tf_used_2fa = False, False, False
          591         def text_format(addr):
          592             nonlocal tf_used_recv, tf_used_change, tf_used_2fa
          593             if self.wallet.is_mine(addr):
          594                 if self.wallet.is_change(addr):
          595                     tf_used_change = True
          596                     return self.txo_color_change.text_char_format
          597                 else:
          598                     tf_used_recv = True
          599                     return self.txo_color_recv.text_char_format
          600             elif self.wallet.is_billing_address(addr):
          601                 tf_used_2fa = True
          602                 return self.txo_color_2fa.text_char_format
          603             return ext
          604 
          605         def format_amount(amt):
          606             return self.main_window.format_amount(amt, whitespaces=True)
          607 
          608         i_text = self.inputs_textedit
          609         i_text.clear()
          610         i_text.setFont(QFont(MONOSPACE_FONT))
          611         i_text.setReadOnly(True)
          612         cursor = i_text.textCursor()
          613         for txin in self.tx.inputs():
          614             if txin.is_coinbase_input():
          615                 cursor.insertText('coinbase')
          616             else:
          617                 prevout_hash = txin.prevout.txid.hex()
          618                 prevout_n = txin.prevout.out_idx
          619                 cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext)
          620                 addr = self.wallet.get_txin_address(txin)
          621                 if addr is None:
          622                     addr = ''
          623                 cursor.insertText(addr, text_format(addr))
          624                 txin_value = self.wallet.get_txin_value(txin)
          625                 if txin_value is not None:
          626                     cursor.insertText(format_amount(txin_value), ext)
          627             cursor.insertBlock()
          628 
          629         self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs()))
          630         o_text = self.outputs_textedit
          631         o_text.clear()
          632         o_text.setFont(QFont(MONOSPACE_FONT))
          633         o_text.setReadOnly(True)
          634         cursor = o_text.textCursor()
          635         for o in self.tx.outputs():
          636             addr, v = o.get_ui_address_str(), o.value
          637             cursor.insertText(addr, text_format(addr))
          638             if v is not None:
          639                 cursor.insertText('\t', ext)
          640                 cursor.insertText(format_amount(v), ext)
          641             cursor.insertBlock()
          642 
          643         self.txo_color_recv.legend_label.setVisible(tf_used_recv)
          644         self.txo_color_change.legend_label.setVisible(tf_used_change)
          645         self.txo_color_2fa.legend_label.setVisible(tf_used_2fa)
          646 
          647     def add_tx_stats(self, vbox):
          648         hbox_stats = QHBoxLayout()
          649 
          650         # left column
          651         vbox_left = QVBoxLayout()
          652         self.tx_desc = TxDetailLabel(word_wrap=True)
          653         vbox_left.addWidget(self.tx_desc)
          654         self.status_label = TxDetailLabel()
          655         vbox_left.addWidget(self.status_label)
          656         self.date_label = TxDetailLabel()
          657         vbox_left.addWidget(self.date_label)
          658         self.amount_label = TxDetailLabel()
          659         vbox_left.addWidget(self.amount_label)
          660         self.ln_amount_label = TxDetailLabel()
          661         vbox_left.addWidget(self.ln_amount_label)
          662 
          663         fee_hbox = QHBoxLayout()
          664         self.fee_label = TxDetailLabel()
          665         fee_hbox.addWidget(self.fee_label)
          666         self.fee_warning_icon = QLabel()
          667         pixmap = QPixmap(icon_path("warning"))
          668         pixmap_size = round(2 * char_width_in_lineedit())
          669         pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
          670         self.fee_warning_icon.setPixmap(pixmap)
          671         self.fee_warning_icon.setVisible(False)
          672         fee_hbox.addWidget(self.fee_warning_icon)
          673         fee_hbox.addStretch(1)
          674         vbox_left.addLayout(fee_hbox)
          675 
          676         vbox_left.addStretch(1)
          677         hbox_stats.addLayout(vbox_left, 50)
          678 
          679         # vertical line separator
          680         line_separator = QFrame()
          681         line_separator.setFrameShape(QFrame.VLine)
          682         line_separator.setFrameShadow(QFrame.Sunken)
          683         line_separator.setLineWidth(1)
          684         hbox_stats.addWidget(line_separator)
          685 
          686         # right column
          687         vbox_right = QVBoxLayout()
          688         self.size_label = TxDetailLabel()
          689         vbox_right.addWidget(self.size_label)
          690         self.rbf_label = TxDetailLabel()
          691         vbox_right.addWidget(self.rbf_label)
          692         self.rbf_cb = QCheckBox(_('Replace by fee'))
          693         self.rbf_cb.setChecked(bool(self.config.get('use_rbf', True)))
          694         vbox_right.addWidget(self.rbf_cb)
          695 
          696         self.locktime_final_label = TxDetailLabel()
          697         vbox_right.addWidget(self.locktime_final_label)
          698 
          699         locktime_setter_hbox = QHBoxLayout()
          700         locktime_setter_hbox.setContentsMargins(0, 0, 0, 0)
          701         locktime_setter_hbox.setSpacing(0)
          702         locktime_setter_label = TxDetailLabel()
          703         locktime_setter_label.setText("LockTime: ")
          704         self.locktime_e = LockTimeEdit(self)
          705         locktime_setter_hbox.addWidget(locktime_setter_label)
          706         locktime_setter_hbox.addWidget(self.locktime_e)
          707         locktime_setter_hbox.addStretch(1)
          708         self.locktime_setter_widget = QWidget()
          709         self.locktime_setter_widget.setLayout(locktime_setter_hbox)
          710         vbox_right.addWidget(self.locktime_setter_widget)
          711 
          712         self.block_height_label = TxDetailLabel()
          713         vbox_right.addWidget(self.block_height_label)
          714         vbox_right.addStretch(1)
          715         hbox_stats.addLayout(vbox_right, 50)
          716 
          717         vbox.addLayout(hbox_stats)
          718 
          719         # below columns
          720         self.block_hash_label = TxDetailLabel(word_wrap=True)
          721         vbox.addWidget(self.block_hash_label)
          722 
          723         # set visibility after parenting can be determined by Qt
          724         self.rbf_label.setVisible(self.finalized)
          725         self.rbf_cb.setVisible(not self.finalized)
          726         self.locktime_final_label.setVisible(self.finalized)
          727         self.locktime_setter_widget.setVisible(not self.finalized)
          728 
          729     def set_title(self):
          730         self.setWindowTitle(_("Create transaction") if not self.finalized else _("Transaction"))
          731 
          732     def can_finalize(self) -> bool:
          733         return False
          734 
          735     def on_finalize(self):
          736         pass  # overridden in subclass
          737 
          738     def update_fee_fields(self):
          739         pass  # overridden in subclass
          740 
          741 
          742 class TxDetailLabel(QLabel):
          743     def __init__(self, *, word_wrap=None):
          744         super().__init__()
          745         self.setTextInteractionFlags(Qt.TextSelectableByMouse)
          746         if word_wrap is not None:
          747             self.setWordWrap(word_wrap)
          748 
          749 
          750 class TxOutputColoring:
          751     # used for both inputs and outputs
          752 
          753     def __init__(
          754             self,
          755             *,
          756             legend: str,
          757             color: ColorSchemeItem,
          758             tooltip: str,
          759     ):
          760         self.color = color.as_color(background=True)
          761         self.legend_label = QLabel("<font color={color}>{box_char}</font> = {label}".format(
          762             color=self.color.name(),
          763             box_char="█",
          764             label=legend,
          765         ))
          766         font = self.legend_label.font()
          767         font.setPointSize(font.pointSize() - 1)
          768         self.legend_label.setFont(font)
          769         self.legend_label.setVisible(False)
          770         self.text_char_format = QTextCharFormat()
          771         self.text_char_format.setBackground(QBrush(self.color))
          772         self.text_char_format.setToolTip(tooltip)
          773 
          774 
          775 class TxDialog(BaseTxDialog):
          776     def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved):
          777         BaseTxDialog.__init__(self, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved, finalized=True)
          778         self.set_tx(tx)
          779         self.update()
          780 
          781 
          782 class PreviewTxDialog(BaseTxDialog, TxEditor):
          783 
          784     def __init__(
          785             self,
          786             *,
          787             make_tx,
          788             external_keypairs,
          789             window: 'ElectrumWindow',
          790             output_value: Union[int, str],
          791     ):
          792         TxEditor.__init__(
          793             self,
          794             window=window,
          795             make_tx=make_tx,
          796             is_sweep=bool(external_keypairs),
          797             output_value=output_value,
          798         )
          799         BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False,
          800                               finalized=False, external_keypairs=external_keypairs)
          801         BlockingWaitingDialog(window, _("Preparing transaction..."),
          802                               lambda: self.update_tx(fallback_to_zero_fee=True))
          803         self.update()
          804 
          805     def create_fee_controls(self):
          806 
          807         self.size_e = TxSizeLabel()
          808         self.size_e.setAlignment(Qt.AlignCenter)
          809         self.size_e.setAmount(0)
          810         self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
          811 
          812         self.fiat_fee_label = TxFiatLabel()
          813         self.fiat_fee_label.setAlignment(Qt.AlignCenter)
          814         self.fiat_fee_label.setAmount(0)
          815         self.fiat_fee_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
          816 
          817         self.feerate_e = FeerateEdit(lambda: 0)
          818         self.feerate_e.setAmount(self.config.fee_per_byte())
          819         self.feerate_e.textEdited.connect(partial(self.on_fee_or_feerate, self.feerate_e, False))
          820         self.feerate_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.feerate_e, True))
          821 
          822         self.fee_e = BTCAmountEdit(self.main_window.get_decimal_point)
          823         self.fee_e.textEdited.connect(partial(self.on_fee_or_feerate, self.fee_e, False))
          824         self.fee_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.fee_e, True))
          825 
          826         self.fee_e.textChanged.connect(self.entry_changed)
          827         self.feerate_e.textChanged.connect(self.entry_changed)
          828 
          829         self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback)
          830         self.fee_combo = FeeComboBox(self.fee_slider)
          831         self.fee_slider.setFixedWidth(self.fee_e.width())
          832 
          833         def feerounding_onclick():
          834             text = (self.feerounding_text + '\n\n' +
          835                     _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' +
          836                     _('At most 100 satoshis might be lost due to this rounding.') + ' ' +
          837                     _("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' +
          838                     _('Also, dust is not kept as change, but added to the fee.')  + '\n' +
          839                     _('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.'))
          840             self.show_message(title=_('Fee rounding'), msg=text)
          841 
          842         self.feerounding_icon = QToolButton()
          843         self.feerounding_icon.setIcon(read_QIcon('info.png'))
          844         self.feerounding_icon.setAutoRaise(True)
          845         self.feerounding_icon.clicked.connect(feerounding_onclick)
          846         self.feerounding_icon.setVisible(False)
          847 
          848         self.feecontrol_fields = QWidget()
          849         hbox = QHBoxLayout(self.feecontrol_fields)
          850         hbox.setContentsMargins(0, 0, 0, 0)
          851         grid = QGridLayout()
          852         grid.addWidget(QLabel(_("Target fee:")), 0, 0)
          853         grid.addWidget(self.feerate_e, 0, 1)
          854         grid.addWidget(self.size_e, 0, 2)
          855         grid.addWidget(self.fee_e, 0, 3)
          856         grid.addWidget(self.feerounding_icon, 0, 4)
          857         grid.addWidget(self.fiat_fee_label, 0, 5)
          858         grid.addWidget(self.fee_slider, 1, 1)
          859         grid.addWidget(self.fee_combo, 1, 2)
          860         hbox.addLayout(grid)
          861         hbox.addStretch(1)
          862 
          863     def fee_slider_callback(self, dyn, pos, fee_rate):
          864         super().fee_slider_callback(dyn, pos, fee_rate)
          865         self.fee_slider.activate()
          866         if fee_rate:
          867             fee_rate = Decimal(fee_rate)
          868             self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000))
          869         else:
          870             self.feerate_e.setAmount(None)
          871         self.fee_e.setModified(False)
          872 
          873     def on_fee_or_feerate(self, edit_changed, editing_finished):
          874         edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e
          875         if editing_finished:
          876             if edit_changed.get_amount() is None:
          877                 # This is so that when the user blanks the fee and moves on,
          878                 # we go back to auto-calculate mode and put a fee back.
          879                 edit_changed.setModified(False)
          880         else:
          881             # edit_changed was edited just now, so make sure we will
          882             # freeze the correct fee setting (this)
          883             edit_other.setModified(False)
          884         self.fee_slider.deactivate()
          885         self.update()
          886 
          887     def is_send_fee_frozen(self):
          888         return self.fee_e.isVisible() and self.fee_e.isModified() \
          889                and (self.fee_e.text() or self.fee_e.hasFocus())
          890 
          891     def is_send_feerate_frozen(self):
          892         return self.feerate_e.isVisible() and self.feerate_e.isModified() \
          893                and (self.feerate_e.text() or self.feerate_e.hasFocus())
          894 
          895     def set_feerounding_text(self, num_satoshis_added):
          896         self.feerounding_text = (_('Additional {} satoshis are going to be added.')
          897                                  .format(num_satoshis_added))
          898 
          899     def get_fee_estimator(self):
          900         if self.is_send_fee_frozen() and self.fee_e.get_amount() is not None:
          901             fee_estimator = self.fee_e.get_amount()
          902         elif self.is_send_feerate_frozen() and self.feerate_e.get_amount() is not None:
          903             amount = self.feerate_e.get_amount()  # sat/byte feerate
          904             amount = 0 if amount is None else amount * 1000  # sat/kilobyte feerate
          905             fee_estimator = partial(
          906                 SimpleConfig.estimate_fee_for_feerate, amount)
          907         else:
          908             fee_estimator = None
          909         return fee_estimator
          910 
          911     def entry_changed(self):
          912         # blue color denotes auto-filled values
          913         text = ""
          914         fee_color = ColorScheme.DEFAULT
          915         feerate_color = ColorScheme.DEFAULT
          916         if self.not_enough_funds:
          917             fee_color = ColorScheme.RED
          918             feerate_color = ColorScheme.RED
          919         elif self.fee_e.isModified():
          920             feerate_color = ColorScheme.BLUE
          921         elif self.feerate_e.isModified():
          922             fee_color = ColorScheme.BLUE
          923         else:
          924             fee_color = ColorScheme.BLUE
          925             feerate_color = ColorScheme.BLUE
          926         self.fee_e.setStyleSheet(fee_color.as_stylesheet())
          927         self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
          928         #
          929         self.needs_update = True
          930 
          931     def update_fee_fields(self):
          932         freeze_fee = self.is_send_fee_frozen()
          933         freeze_feerate = self.is_send_feerate_frozen()
          934         tx = self.tx
          935         if self.no_dynfee_estimates and tx:
          936             size = tx.estimated_size()
          937             self.size_e.setAmount(size)
          938         if self.not_enough_funds or self.no_dynfee_estimates:
          939             if not freeze_fee:
          940                 self.fee_e.setAmount(None)
          941             if not freeze_feerate:
          942                 self.feerate_e.setAmount(None)
          943             self.feerounding_icon.setVisible(False)
          944             return
          945 
          946         assert tx is not None
          947         size = tx.estimated_size()
          948         fee = tx.get_fee()
          949 
          950         self.size_e.setAmount(size)
          951         fiat_fee = self.main_window.format_fiat_and_units(fee)
          952         self.fiat_fee_label.setAmount(fiat_fee)
          953 
          954         # Displayed fee/fee_rate values are set according to user input.
          955         # Due to rounding or dropping dust in CoinChooser,
          956         # actual fees often differ somewhat.
          957         if freeze_feerate or self.fee_slider.is_active():
          958             displayed_feerate = self.feerate_e.get_amount()
          959             if displayed_feerate is not None:
          960                 displayed_feerate = quantize_feerate(displayed_feerate)
          961             elif self.fee_slider.is_active():
          962                 # fallback to actual fee
          963                 displayed_feerate = quantize_feerate(fee / size) if fee is not None else None
          964                 self.feerate_e.setAmount(displayed_feerate)
          965             displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None
          966             self.fee_e.setAmount(displayed_fee)
          967         else:
          968             if freeze_fee:
          969                 displayed_fee = self.fee_e.get_amount()
          970             else:
          971                 # fallback to actual fee if nothing is frozen
          972                 displayed_fee = fee
          973                 self.fee_e.setAmount(displayed_fee)
          974             displayed_fee = displayed_fee if displayed_fee else 0
          975             displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None
          976             self.feerate_e.setAmount(displayed_feerate)
          977 
          978         # show/hide fee rounding icon
          979         feerounding = (fee - displayed_fee) if (fee and displayed_fee is not None) else 0
          980         self.set_feerounding_text(int(feerounding))
          981         self.feerounding_icon.setToolTip(self.feerounding_text)
          982         self.feerounding_icon.setVisible(abs(feerounding) >= 1)
          983 
          984     def can_finalize(self):
          985         return (self.tx is not None
          986                 and not self.not_enough_funds)
          987 
          988     def on_finalize(self):
          989         if not self.can_finalize():
          990             return
          991         assert self.tx
          992         self.finalized = True
          993         self.tx.set_rbf(self.rbf_cb.isChecked())
          994         locktime = self.locktime_e.get_locktime()
          995         if locktime is not None:
          996             self.tx.locktime = locktime
          997         for widget in [self.fee_slider, self.fee_combo, self.feecontrol_fields, self.rbf_cb,
          998                        self.locktime_setter_widget, self.locktime_e]:
          999             widget.setEnabled(False)
         1000             widget.setVisible(False)
         1001         for widget in [self.rbf_label, self.locktime_final_label]:
         1002             widget.setVisible(True)
         1003         self.set_title()
         1004         self.set_buttons_visibility()
         1005         self.update()