Files
pallectrum/electrum/gui/qt/transaction_dialog.py

1102 lines
47 KiB
Python
Raw Normal View History

2013-09-14 21:07:54 +02:00
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2012 thomasv@gitorious
#
2016-02-23 11:36:42 +01:00
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
2013-09-14 21:07:54 +02:00
#
2016-02-23 11:36:42 +01:00
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
2013-09-14 21:07:54 +02:00
#
2016-02-23 11:36:42 +01:00
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
import concurrent.futures
import copy
import datetime
2019-10-23 17:09:41 +02:00
import time
from typing import TYPE_CHECKING, Optional, List, Union, Mapping, Callable
from functools import partial
from decimal import Decimal
2013-09-14 21:07:54 +02:00
from PyQt6.QtCore import QSize, Qt, QUrl, QPoint, pyqtSignal
from PyQt6.QtGui import QTextCharFormat, QBrush, QFont, QPixmap, QTextCursor, QAction
2025-01-23 12:58:28 +01:00
from PyQt6.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget,
QToolButton, QMenu, QTextBrowser,
QSizePolicy)
2018-07-21 23:09:46 +02:00
import qrcode
from qrcode import exceptions
from electrum import bitcoin
2025-01-23 12:58:28 +01:00
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX, DummyAddress
from electrum.i18n import _
from electrum.plugin import run_hook
from electrum.transaction import SerializationError, Transaction, PartialTransaction, TxOutpoint, TxinDataFetchProgress
2019-04-26 18:52:26 +02:00
from electrum.logging import get_logger
from electrum.util import ShortID, get_asyncio_loop, UI_UNIT_NAME_TXSIZE_VBYTES, delta_time_str
from electrum.network import Network
from electrum.wallet import TxSighashRiskLevel, TxSighashDanger
2019-11-18 20:56:49 +01:00
from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog,
char_width_in_lineedit, TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE,
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX,
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX,
getSaveFileName, ColorSchemeItem,
get_icon_qrcode, VLine, WaitingDialog)
from .rate_limiter import rate_limited
from .my_treeview import create_toolbar_with_menu, QMenuWithConfig
if TYPE_CHECKING:
from .main_window import ElectrumWindow
from electrum.wallet import Abstract_Wallet
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
from electrum.payment_identifier import PaymentIdentifier
_logger = get_logger(__name__)
dialogs = [] # Otherwise python randomly garbage collects the dialogs...
2019-11-12 22:00:29 +01:00
class TxSizeLabel(QLabel):
def setAmount(self, byte_size):
text = ""
if byte_size:
text = f"x {byte_size} {UI_UNIT_NAME_TXSIZE_VBYTES} ="
self.setText(text)
class TxFiatLabel(QLabel):
def setAmount(self, fiat_fee):
self.setText(('%s' % fiat_fee) if fiat_fee else '')
2019-11-12 22:00:29 +01:00
class QTextBrowserWithDefaultSize(QTextBrowser):
def __init__(self, width: int = 0, height: int = 0):
self._width = width
self._height = height
QTextBrowser.__init__(self)
self.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap)
def sizeHint(self):
return QSize(self._width, self._height)
class TxInOutWidget(QWidget):
def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'):
QWidget.__init__(self)
self.wallet = wallet
self.main_window = main_window
self.tx = None # type: Optional[Transaction]
self.inputs_header = QLabel()
self.inputs_textedit = QTextBrowserWithDefaultSize(750, 100)
self.inputs_textedit.setOpenLinks(False) # disable automatic link opening
self.inputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler
self.inputs_textedit.setTextInteractionFlags(
self.inputs_textedit.textInteractionFlags() | Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard)
self.inputs_textedit.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.inputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_inputs)
self.sighash_label = QLabel()
self.sighash_label.setStyleSheet('font-weight: bold')
self.sighash_danger = TxSighashDanger()
self.inputs_warning_icon = QLabel()
pixmap = QPixmap(icon_path("warning"))
pixmap_size = round(2 * char_width_in_lineedit())
pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
self.inputs_warning_icon.setPixmap(pixmap)
self.inputs_warning_icon.setVisible(False)
self.inheader_hbox = QHBoxLayout()
self.inheader_hbox.setContentsMargins(0, 0, 0, 0)
self.inheader_hbox.addWidget(self.inputs_header)
self.inheader_hbox.addStretch(2)
self.inheader_hbox.addWidget(self.sighash_label)
self.inheader_hbox.addWidget(self.inputs_warning_icon)
self.txo_color_recv = TxOutputColoring(
legend=_("Wallet Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receiving address"))
self.txo_color_change = TxOutputColoring(
legend=_("Change Address"), color=ColorScheme.YELLOW, tooltip=_("Wallet change address"))
self.txo_color_2fa = TxOutputColoring(
legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions"))
2023-06-16 10:05:02 +02:00
self.txo_color_swap = TxOutputColoring(
legend=_("Submarine swap address"), color=ColorScheme.BLUE, tooltip=_("Submarine swap address"))
self.outputs_header = QLabel()
self.outputs_textedit = QTextBrowserWithDefaultSize(750, 100)
self.outputs_textedit.setOpenLinks(False) # disable automatic link opening
self.outputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler
self.outputs_textedit.setTextInteractionFlags(
self.outputs_textedit.textInteractionFlags() | Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard)
self.outputs_textedit.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.outputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_outputs)
outheader_hbox = QHBoxLayout()
outheader_hbox.setContentsMargins(0, 0, 0, 0)
outheader_hbox.addWidget(self.outputs_header)
outheader_hbox.addStretch(2)
outheader_hbox.addWidget(self.txo_color_recv.legend_label)
outheader_hbox.addWidget(self.txo_color_change.legend_label)
outheader_hbox.addWidget(self.txo_color_2fa.legend_label)
2023-06-16 10:05:02 +02:00
outheader_hbox.addWidget(self.txo_color_swap.legend_label)
vbox = QVBoxLayout()
vbox.addLayout(self.inheader_hbox)
vbox.addWidget(self.inputs_textedit)
vbox.addLayout(outheader_hbox)
vbox.addWidget(self.outputs_textedit)
self.setLayout(vbox)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
def update(self, tx: Optional[Transaction]):
self.tx = tx
if tx is None:
self.inputs_header.setText('')
self.inputs_textedit.setText('')
self.outputs_header.setText('')
self.outputs_textedit.setText('')
return
inputs_header_text = _("Inputs") + ' (%d)'%len(self.tx.inputs())
self.inputs_header.setText(inputs_header_text)
ext = QTextCharFormat() # "external"
lnk = QTextCharFormat()
lnk.setToolTip(_('Click to open, right-click for menu'))
lnk.setAnchor(True)
lnk.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SingleUnderline)
2023-06-16 10:05:02 +02:00
tf_used_recv, tf_used_change, tf_used_2fa, tf_used_swap = False, False, False, False
2025-01-23 12:58:28 +01:00
def addr_text_format(addr: str) -> QTextCharFormat:
2023-06-16 10:05:02 +02:00
nonlocal tf_used_recv, tf_used_change, tf_used_2fa, tf_used_swap
sm = self.wallet.lnworker.swap_manager if self.wallet.lnworker else None
if self.wallet.is_mine(addr):
if self.wallet.is_change(addr):
tf_used_change = True
fmt = QTextCharFormat(self.txo_color_change.text_char_format)
else:
tf_used_recv = True
fmt = QTextCharFormat(self.txo_color_recv.text_char_format)
fmt.setAnchorHref(addr)
fmt.setToolTip(_('Click to open, right-click for menu'))
fmt.setAnchor(True)
fmt.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SingleUnderline)
return fmt
elif sm and sm.is_lockup_address_for_a_swap(addr) or addr == DummyAddress.SWAP:
2023-06-16 10:05:02 +02:00
tf_used_swap = True
return self.txo_color_swap.text_char_format
elif self.wallet.is_billing_address(addr):
tf_used_2fa = True
return self.txo_color_2fa.text_char_format
return ext
def insert_tx_io(
*,
cursor: QTextCursor,
txio_idx: int,
is_coinbase: bool,
tcf_shortid: QTextCharFormat = None,
short_id: str,
addr: Optional[str],
value: Optional[int],
):
tcf_ext = QTextCharFormat(ext)
tcf_addr = addr_text_format(addr)
if tcf_shortid is None:
tcf_shortid = tcf_ext
a_name = f"txio_idx {txio_idx}"
for tcf in (tcf_ext, tcf_shortid, tcf_addr): # used by context menu creation
tcf.setAnchorNames([a_name])
if is_coinbase:
cursor.insertText('coinbase', tcf_ext)
else:
# short_id
cursor.insertText(short_id, tcf_shortid)
cursor.insertText(" " * max(0, 15 - len(short_id)), tcf_ext) # padding
cursor.insertText('\t', tcf_ext)
# addr
if addr is None:
address_str = '<address unknown>'
elif len(addr) <= 42:
address_str = addr
else:
address_str = addr[0:30] + '' + addr[-11:]
cursor.insertText(address_str, tcf_addr)
cursor.insertText(" " * max(0, 42 - len(address_str)), tcf_ext) # padding
cursor.insertText('\t', tcf_ext)
# value
value_str = self.main_window.format_amount(value, whitespaces=True)
cursor.insertText(value_str, tcf_ext)
cursor.insertBlock()
i_text = self.inputs_textedit
i_text.clear()
i_text.setFont(QFont(MONOSPACE_FONT))
i_text.setReadOnly(True)
cursor = i_text.textCursor()
for txin_idx, txin in enumerate(self.tx.inputs()):
addr = self.wallet.adb.get_txin_address(txin)
txin_value = self.wallet.adb.get_txin_value(txin)
tcf_shortid = QTextCharFormat(lnk)
tcf_shortid.setAnchorHref(txin.prevout.txid.hex())
insert_tx_io(
cursor=cursor, is_coinbase=txin.is_coinbase_input(), txio_idx=txin_idx,
tcf_shortid=tcf_shortid,
short_id=str(txin.short_id), addr=addr, value=txin_value,
)
if isinstance(self.tx, PartialTransaction):
self.sighash_danger = self.wallet.check_sighash(self.tx)
if self.sighash_danger.risk_level >= TxSighashRiskLevel.WEIRD_SIGHASH:
self.sighash_label.setText(self.sighash_danger.short_message)
self.inputs_warning_icon.setVisible(True)
self.inputs_warning_icon.setToolTip(self.sighash_danger.get_long_message())
self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs()))
o_text = self.outputs_textedit
o_text.clear()
o_text.setFont(QFont(MONOSPACE_FONT))
o_text.setReadOnly(True)
tx_height, tx_pos = None, None
tx_hash = self.tx.txid()
if tx_hash:
tx_mined_info = self.wallet.adb.get_tx_height(tx_hash)
tx_height = tx_mined_info.height
tx_pos = tx_mined_info.txpos
cursor = o_text.textCursor()
for txout_idx, o in enumerate(self.tx.outputs()):
if tx_height is not None and tx_pos is not None and tx_pos >= 0:
short_id = ShortID.from_components(tx_height, tx_pos, txout_idx)
elif tx_hash:
short_id = TxOutpoint(bytes.fromhex(tx_hash), txout_idx).short_name()
else:
short_id = f"unknown:{txout_idx}"
addr = o.get_ui_address_str()
insert_tx_io(
cursor=cursor, is_coinbase=False, txio_idx=txout_idx,
short_id=str(short_id), addr=addr, value=o.value,
)
self.txo_color_recv.legend_label.setVisible(tf_used_recv)
self.txo_color_change.legend_label.setVisible(tf_used_change)
self.txo_color_2fa.legend_label.setVisible(tf_used_2fa)
2023-06-16 10:05:02 +02:00
self.txo_color_swap.legend_label.setVisible(tf_used_swap)
def _open_internal_link(self, target):
"""Accepts either a str txid, str address, or a QUrl which should be
of the bare form "txid" and/or "address" -- used by the clickable
links in the inputs/outputs QTextBrowsers"""
if isinstance(target, QUrl):
target = target.toString(QUrl.UrlFormattingOption.None_)
assert target
if bitcoin.is_address(target):
# target was an address, open address dialog
self.main_window.show_address(target, parent=self)
else:
# target was a txid, open new tx dialog
self.main_window.do_process_from_txid(txid=target, parent=self)
def on_context_menu_for_inputs(self, pos: QPoint):
i_text = self.inputs_textedit
global_pos = i_text.viewport().mapToGlobal(pos)
cursor = i_text.cursorForPosition(pos)
charFormat = cursor.charFormat()
name = charFormat.anchorNames() and charFormat.anchorNames()[0]
if not name:
menu = i_text.createStandardContextMenu()
menu.exec(global_pos)
return
menu = QMenu()
show_list = []
copy_list = []
# figure out which input they right-clicked on. input lines have an anchor named "txio_idx N"
txin_idx = int(name.split()[1]) # split "txio_idx N", translate N -> int
txin = self.tx.inputs()[txin_idx]
menu.addAction(_("Tx Input #{}").format(txin_idx)).setDisabled(True)
menu.addSeparator()
if txin.is_coinbase_input():
menu.addAction(_("Coinbase Input")).setDisabled(True)
else:
show_list += [(_("Show Prev Tx"), lambda: self._open_internal_link(txin.prevout.txid.hex()))]
copy_list += [(_("Copy Outpoint"), lambda: self.main_window.do_copy(txin.prevout.to_str()))]
addr = self.wallet.adb.get_txin_address(txin)
if addr:
if self.wallet.is_mine(addr):
show_list += [(_("Address Details"), lambda: self.main_window.show_address(addr, parent=self))]
copy_list += [(_("Copy Address"), lambda: self.main_window.do_copy(addr))]
txin_value = self.wallet.adb.get_txin_value(txin)
if txin_value:
value_str = self.main_window.format_amount(txin_value, add_thousands_sep=False)
copy_list += [(_("Copy Amount"), lambda: self.main_window.do_copy(value_str))]
for item in show_list:
menu.addAction(*item)
if show_list and copy_list:
menu.addSeparator()
for item in copy_list:
menu.addAction(*item)
menu.addSeparator()
std_menu = i_text.createStandardContextMenu()
menu.addActions(std_menu.actions())
menu.exec(global_pos)
def on_context_menu_for_outputs(self, pos: QPoint):
o_text = self.outputs_textedit
global_pos = o_text.viewport().mapToGlobal(pos)
cursor = o_text.cursorForPosition(pos)
charFormat = cursor.charFormat()
name = charFormat.anchorNames() and charFormat.anchorNames()[0]
if not name:
menu = o_text.createStandardContextMenu()
menu.exec(global_pos)
return
menu = QMenu()
show_list = []
copy_list = []
# figure out which output they right-clicked on. output lines have an anchor named "txio_idx N"
txout_idx = int(name.split()[1]) # split "txio_idx N", translate N -> int
menu.addAction(_("Tx Output #{}").format(txout_idx)).setDisabled(True)
menu.addSeparator()
if tx_hash := self.tx.txid():
outpoint = TxOutpoint(bytes.fromhex(tx_hash), txout_idx)
copy_list += [(_("Copy Outpoint"), lambda: self.main_window.do_copy(outpoint.to_str()))]
if addr := self.tx.outputs()[txout_idx].address:
if self.wallet.is_mine(addr):
show_list += [(_("Address Details"), lambda: self.main_window.show_address(addr, parent=self))]
copy_list += [(_("Copy Address"), lambda: self.main_window.do_copy(addr))]
else:
spk = self.tx.outputs()[txout_idx].scriptpubkey
copy_list += [(_("Copy scriptPubKey"), lambda: self.main_window.do_copy(spk.hex()))]
txout_value = self.tx.outputs()[txout_idx].value
value_str = self.main_window.format_amount(txout_value, add_thousands_sep=False)
copy_list += [(_("Copy Amount"), lambda: self.main_window.do_copy(value_str))]
for item in show_list:
menu.addAction(*item)
if show_list and copy_list:
menu.addSeparator()
for item in copy_list:
menu.addAction(*item)
menu.addSeparator()
std_menu = o_text.createStandardContextMenu()
menu.addActions(std_menu.actions())
menu.exec(global_pos)
def show_transaction(
tx: Transaction,
*,
parent: 'ElectrumWindow',
prompt_if_unsaved: bool = False,
external_keypairs: Mapping[bytes, bytes] = None,
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
payment_identifier: 'PaymentIdentifier' = None,
on_closed: Callable[[], None] = None,
Timelock Recovery Extension (#9589) * Timelock Recovery Extension * Timelock Recovery Extension tests * Use fee_policy instead of fee_est Following 3f327eea079986fe557b931c52f58f32710db0cf * making tx with base_tx Following ab14c3e1382c1af48baff73b790aecfbd069eb8a * move plugin metadata from __init__.py to manifest.json * removing json large indentation * timelock recovery icon * timelock recovery plugin: fix typos * timelock recovery plugin: use menu instead of status bar. The status bar should be used for displaying status. For example, hardware wallet plugins use it because their connection status is changing and needs to be displayed. * timelock recovery plugin: ask for password only once * timelock recovery plugin: ask whether to create cancellation tx in the initial window * remove unnecessary code. (calling run_hook from a plugin does not make sense) * show alert and cancellation address at the end. skip unnecessary dialog * timelock recovery plugin: do not show transactions one by one. Set the fee policy in the first dialog, and use the same fee policy for all tx. We could add 3 sliders to this dialog, if different fees are needed, but I think this really isn't really necessary. * simplify default_wallet for tests All the lightning-related stuff is irrelevant for this plugin. Also use a different destination address for the test recovery-plan (an address that does not belong to the same wallet). * Fee selection should be above fee calculation also show fee calculation result with "fee: " label. * hide Sign and Broadcast buttons during view * recalculate cancellation transaction The checkbox could be clicked after the fee rate has been set. Calling update_transactions() may seem inefficient, but it's the simplest way to avoid such edge-cases. Also set the context's cancellation transaction to None when the checkbox is unset. * use context.cancellation_tx instead of checkbox value context.cancellation_tx will be None iff the checkbox was unset * hide cancellation address if not used * init monospace font correctly * timelock recovery plugin: add input info at signing time. Fixes trezor exception: 'Missing previous tx' * timelock recovery: remove unused parameters * avoid saving the tx in a separate var fixing the assertions * avoid caching recovery & cancellation inputs * timelock recovery: separate help window from agreement. move agreement at the end of the flow, rephrase it * do not cache alert_tx_outputs * do not crash when not enough funds not enough funds can happen when multiple addresses are specified in payto_e, with an amount larger than the wallet has - so we set the payto_e color to red. It can also happen when the user selects a really high fee, but this is not common in a "recovery" wallet with significant funds. * If files not saved - ask before closing * move the checkbox above the save buttons people read the text from top to bottom and may not understand why the buttons are disabled --------- Co-authored-by: f321x <f321x@tutamail.com> Co-authored-by: ThomasV <thomasv@electrum.org>
2025-04-22 11:02:01 +03:00
show_sign_button: bool = True,
show_broadcast_button: bool = True,
):
try:
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
d = TxDialog(
tx,
parent=parent,
prompt_if_unsaved=prompt_if_unsaved,
external_keypairs=external_keypairs,
payment_identifier=payment_identifier,
on_closed=on_closed,
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
)
Timelock Recovery Extension (#9589) * Timelock Recovery Extension * Timelock Recovery Extension tests * Use fee_policy instead of fee_est Following 3f327eea079986fe557b931c52f58f32710db0cf * making tx with base_tx Following ab14c3e1382c1af48baff73b790aecfbd069eb8a * move plugin metadata from __init__.py to manifest.json * removing json large indentation * timelock recovery icon * timelock recovery plugin: fix typos * timelock recovery plugin: use menu instead of status bar. The status bar should be used for displaying status. For example, hardware wallet plugins use it because their connection status is changing and needs to be displayed. * timelock recovery plugin: ask for password only once * timelock recovery plugin: ask whether to create cancellation tx in the initial window * remove unnecessary code. (calling run_hook from a plugin does not make sense) * show alert and cancellation address at the end. skip unnecessary dialog * timelock recovery plugin: do not show transactions one by one. Set the fee policy in the first dialog, and use the same fee policy for all tx. We could add 3 sliders to this dialog, if different fees are needed, but I think this really isn't really necessary. * simplify default_wallet for tests All the lightning-related stuff is irrelevant for this plugin. Also use a different destination address for the test recovery-plan (an address that does not belong to the same wallet). * Fee selection should be above fee calculation also show fee calculation result with "fee: " label. * hide Sign and Broadcast buttons during view * recalculate cancellation transaction The checkbox could be clicked after the fee rate has been set. Calling update_transactions() may seem inefficient, but it's the simplest way to avoid such edge-cases. Also set the context's cancellation transaction to None when the checkbox is unset. * use context.cancellation_tx instead of checkbox value context.cancellation_tx will be None iff the checkbox was unset * hide cancellation address if not used * init monospace font correctly * timelock recovery plugin: add input info at signing time. Fixes trezor exception: 'Missing previous tx' * timelock recovery: remove unused parameters * avoid saving the tx in a separate var fixing the assertions * avoid caching recovery & cancellation inputs * timelock recovery: separate help window from agreement. move agreement at the end of the flow, rephrase it * do not cache alert_tx_outputs * do not crash when not enough funds not enough funds can happen when multiple addresses are specified in payto_e, with an amount larger than the wallet has - so we set the payto_e color to red. It can also happen when the user selects a really high fee, but this is not common in a "recovery" wallet with significant funds. * If files not saved - ask before closing * move the checkbox above the save buttons people read the text from top to bottom and may not understand why the buttons are disabled --------- Co-authored-by: f321x <f321x@tutamail.com> Co-authored-by: ThomasV <thomasv@electrum.org>
2025-04-22 11:02:01 +03:00
if not show_sign_button:
d.sign_button.setVisible(False)
if not show_broadcast_button:
d.broadcast_button.setVisible(False)
except SerializationError as e:
2019-04-26 18:52:26 +02:00
_logger.exception('unable to deserialize the transaction')
parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
else:
d.show()
2013-09-14 21:07:54 +02:00
class TxDialog(QDialog, MessageBoxMixin):
throttled_update_sig = pyqtSignal() # emit from thread to do update in main thread
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
def __init__(
self,
tx: Transaction,
*,
parent: 'ElectrumWindow',
prompt_if_unsaved: bool,
external_keypairs: Mapping[bytes, bytes] = None,
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
payment_identifier: 'PaymentIdentifier' = None,
on_closed: Callable[[], None] = None,
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
):
'''Transactions in the wallet will show their description.
Pass desc to give a description for txs not yet in the wallet.
'''
# We want to be a top-level window
QDialog.__init__(self, parent=None)
2019-11-12 22:00:29 +01:00
self.tx = None # type: Optional[Transaction]
2019-11-17 13:48:19 +01:00
self.external_keypairs = external_keypairs
2019-09-18 02:10:53 +02:00
self.main_window = parent
self.config = parent.config
2013-09-14 21:07:54 +02:00
self.wallet = parent.wallet
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
self.payment_identifier = payment_identifier
self.prompt_if_unsaved = prompt_if_unsaved
self.on_closed = on_closed
self.saved = False
self.desc = None
if txid := tx.txid():
self.desc = self.wallet.get_label_for_txid(txid) or None
self.setMinimumWidth(640)
2013-09-14 21:07:54 +02:00
self.psbt_only_widgets = [] # type: List[Union[QWidget, QAction]]
2013-09-14 21:07:54 +02:00
vbox = QVBoxLayout()
self.setLayout(vbox)
toolbar, menu = create_toolbar_with_menu(self.config, '')
menu.addConfig(
self.config.cv.GUI_QT_TX_DIALOG_FETCH_TXIN_DATA,
callback=self.maybe_fetch_txin_data)
vbox.addLayout(toolbar)
2013-09-14 21:07:54 +02:00
vbox.addWidget(QLabel(_("Transaction ID:")))
self.tx_hash_e = ShowQRLineEdit('', self.config, title=_('Transaction ID'))
2013-09-14 21:07:54 +02:00
vbox.addWidget(self.tx_hash_e)
self.tx_desc_label = QLabel(_("Description:"))
vbox.addWidget(self.tx_desc_label)
self.tx_desc = ButtonsLineEdit('')
def on_edited():
text = self.tx_desc.text()
if self.wallet.set_label(txid, text):
self.main_window.history_list.update()
self.main_window.utxo_list.update()
self.main_window.labels_changed_signal.emit()
self.tx_desc.editingFinished.connect(on_edited)
self.tx_desc.addCopyButton()
vbox.addWidget(self.tx_desc)
2013-09-14 21:07:54 +02:00
self.add_tx_stats(vbox)
vbox.addSpacing(10)
2019-10-31 06:25:32 +01:00
self.io_widget = TxInOutWidget(self.main_window, self.wallet)
vbox.addWidget(self.io_widget)
2013-09-14 21:07:54 +02:00
self.sign_button = b = QPushButton(_("Sign"))
b.clicked.connect(self.sign)
self.broadcast_button = b = QPushButton(_("Broadcast"))
b.clicked.connect(self.do_broadcast)
2013-09-14 21:07:54 +02:00
self.save_button = b = QPushButton(_("Add to History"))
b.clicked.connect(self.save)
self.cancel_button = b = QPushButton(_("Close"))
b.clicked.connect(self.close)
b.setDefault(True)
self.export_actions_menu = export_actions_menu = QMenuWithConfig(config=self.config)
self.add_export_actions_to_menu(export_actions_menu)
export_actions_menu.addSeparator()
export_option = export_actions_menu.addConfig(
self.config.cv.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA)
self.psbt_only_widgets.append(export_option)
export_option = export_actions_menu.addConfig(
self.config.cv.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS)
self.psbt_only_widgets.append(export_option)
if self.wallet.has_support_for_slip_19_ownership_proofs():
export_option = export_actions_menu.addAction(
_('Include SLIP-19 ownership proofs'),
self._add_slip_19_ownership_proofs_to_tx)
export_option.setToolTip(_("Some cosigners (e.g. Trezor) might require this for coinjoins."))
self._export_option_slip19 = export_option
export_option.setCheckable(True)
export_option.setChecked(False)
self.psbt_only_widgets.append(export_option)
self.export_actions_button = QToolButton()
self.export_actions_button.setText(_("Share"))
self.export_actions_button.setMenu(export_actions_menu)
self.export_actions_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
2015-04-20 14:44:59 +02:00
2019-10-31 06:25:32 +01:00
partial_tx_actions_menu = QMenu()
ptx_merge_sigs_action = QAction(_("Merge signatures from"), self)
ptx_merge_sigs_action.triggered.connect(self.merge_sigs)
partial_tx_actions_menu.addAction(ptx_merge_sigs_action)
self._ptx_join_txs_action = QAction(_("Join inputs/outputs"), self)
self._ptx_join_txs_action.triggered.connect(self.join_tx_with_another)
partial_tx_actions_menu.addAction(self._ptx_join_txs_action)
2019-10-31 06:25:32 +01:00
self.partial_tx_actions_button = QToolButton()
self.partial_tx_actions_button.setText(_("Combine"))
2019-10-31 06:25:32 +01:00
self.partial_tx_actions_button.setMenu(partial_tx_actions_menu)
self.partial_tx_actions_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.psbt_only_widgets.append(self.partial_tx_actions_button)
2019-10-31 06:25:32 +01:00
# Action buttons
self.buttons = [self.partial_tx_actions_button, self.sign_button, self.broadcast_button, self.cancel_button]
# Transaction sharing buttons
self.sharing_buttons = [self.export_actions_button, self.save_button]
2014-06-20 11:55:34 +02:00
run_hook('transaction_dialog', self)
self.hbox = hbox = QHBoxLayout()
hbox.addLayout(Buttons(*self.sharing_buttons))
hbox.addStretch(1)
hbox.addLayout(Buttons(*self.buttons))
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.ConnectionType.QueuedConnection)
self.set_tx(tx)
self.update()
self.set_title()
2019-11-12 22:00:29 +01:00
def set_tx(self, tx: 'Transaction'):
# Take a copy; it might get updated in the main window by
# e.g. the FX plugin. If this happens during or after a long
# sign operation the signatures are lost.
self.tx = tx = copy.deepcopy(tx)
try:
self.tx.deserialize()
except BaseException as e:
raise SerializationError(e)
# If the wallet can populate the inputs with more info, do it now.
# As a result, e.g. we might learn an imported address tx is segwit,
# 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)
# 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():
self.main_window.run_coroutine_dialog(
tx.add_info_from_network(self.wallet.network, timeout=10),
_("Adding info to tx, from network..."),
)
else:
self.maybe_fetch_txin_data()
2014-06-20 11:55:34 +02:00
def do_broadcast(self):
self.main_window.push_top_level_window(self)
self.main_window.send_tab.save_pending_invoice()
try:
qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug.
2023-07-10 18:16:56 +00:00
self.main_window.broadcast_transaction(self.tx, payment_identifier=self.payment_identifier)
finally:
self.main_window.pop_top_level_window(self)
self.saved = True
self.update()
def closeEvent(self, event):
if (self.prompt_if_unsaved and not self.saved
2018-01-31 16:44:50 +01:00
and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
event.ignore()
else:
event.accept()
2018-01-31 16:44:50 +01:00
try:
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
2013-09-14 21:07:54 +02:00
if self.on_closed:
self.on_closed()
def reject(self):
# Override escape-key to close normally (and invoke closeEvent)
self.close()
def add_export_actions_to_menu(self, menu: QMenu) -> None:
def gettx() -> Transaction:
if not isinstance(self.tx, PartialTransaction):
return self.tx
tx = copy.deepcopy(self.tx)
if self.config.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS:
Network.run_from_another_thread(
tx.prepare_for_export_for_hardware_device(self.wallet))
if self.config.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA:
tx.prepare_for_export_for_coinjoin()
return tx
action = QAction(_("Copy to clipboard"), self)
action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx()))
menu.addAction(action)
action = QAction(get_icon_qrcode(), _("Show as QR code"), self)
action.triggered.connect(lambda: self.show_qr(tx=gettx()))
menu.addAction(action)
action = QAction(_("Save to file"), self)
action.triggered.connect(lambda: self.export_to_file(tx=gettx()))
menu.addAction(action)
def _add_slip_19_ownership_proofs_to_tx(self):
assert isinstance(self.tx, PartialTransaction)
2025-01-23 12:58:28 +01:00
def on_success(result):
self._export_option_slip19.setEnabled(False)
self.main_window.pop_top_level_window(self)
2025-01-23 12:58:28 +01:00
def on_failure(exc_info):
self._export_option_slip19.setChecked(False)
self.main_window.on_error(exc_info)
self.main_window.pop_top_level_window(self)
task = partial(self.wallet.add_slip_19_ownership_proofs_to_tx, self.tx)
msg = _('Adding SLIP-19 ownership proofs to transaction...')
self.main_window.push_top_level_window(self)
WaitingDialog(self, msg, task, on_success, on_failure)
def copy_to_clipboard(self, *, tx: Transaction = None):
if tx is None:
tx = self.tx
self.main_window.do_copy(str(tx), title=_("Transaction"))
def show_qr(self, *, tx: Transaction = None):
if tx is None:
tx = self.tx
qr_data, is_complete = tx.to_qr_data()
help_text = None
if not is_complete:
help_text = _(
"""Warning: Some data (prev txs / "full utxos") was left """
"""out of the QR code as it would not fit. This might cause issues if signing offline. """
"""As a workaround, try exporting the tx as file or text instead.""")
2014-06-17 16:24:01 +02:00
try:
self.main_window.show_qrcode(qr_data, _("Transaction"), parent=self, help_text=help_text)
2018-07-21 23:09:46 +02:00
except qrcode.exceptions.DataOverflowError:
self.show_error(_('Failed to display QR code.') + '\n' +
_('Transaction is too large in size.'))
2014-06-17 16:24:01 +02:00
except Exception as e:
self.show_error(_('Failed to display QR code.') + '\n' + repr(e))
2013-09-14 21:07:54 +02:00
def sign(self):
def sign_done(success):
if self.tx.is_complete():
self.prompt_if_unsaved = True
self.saved = False
self.update()
self.main_window.pop_top_level_window(self)
if self.io_widget.sighash_danger.needs_confirm():
if not self.question(
msg='\n'.join([
self.io_widget.sighash_danger.get_long_message(),
'',
_('Are you sure you want to sign this transaction?')
]),
title=self.io_widget.sighash_danger.short_message,
):
return
self.sign_button.setDisabled(True)
self.main_window.push_top_level_window(self)
self.main_window.sign_tx(self.tx, callback=sign_done, external_keypairs=self.external_keypairs)
2013-09-14 21:07:54 +02:00
def save(self):
self.main_window.push_top_level_window(self)
if self.main_window.save_transaction_into_wallet(self.tx):
self.save_button.setDisabled(True)
self.saved = True
self.main_window.pop_top_level_window(self)
def export_to_file(self, *, tx: Transaction = None):
if tx is None:
tx = self.tx
if isinstance(tx, PartialTransaction):
tx.finalize_psbt()
txid = tx.txid()
suffix = txid[0:8] if txid is not None else time.strftime('%Y%m%d-%H%M')
if tx.is_complete():
extension = 'txn'
default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX
2019-10-23 17:09:41 +02:00
else:
extension = 'psbt'
default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX
name = f'{self.wallet.basename()}-{suffix}.{extension}'
fileName = getSaveFileName(
parent=self,
title=_("Select where to save your transaction"),
filename=name,
filter=TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE,
default_extension=extension,
default_filter=default_filter,
config=self.config,
)
2019-10-23 17:09:41 +02:00
if not fileName:
return
if tx.is_complete(): # network tx hex
2013-09-14 21:07:54 +02:00
with open(fileName, "w+") as f:
network_tx_hex = tx.serialize_to_network()
2019-10-23 17:09:41 +02:00
f.write(network_tx_hex + '\n')
else: # if partial: PSBT bytes
assert isinstance(tx, PartialTransaction)
2019-10-23 17:09:41 +02:00
with open(fileName, "wb+") as f:
f.write(tx.serialize_as_bytes())
2019-10-23 17:09:41 +02:00
self.show_message(_("Transaction exported successfully"))
self.saved = True
def merge_sigs(self):
if not isinstance(self.tx, PartialTransaction):
return
text = text_dialog(
parent=self,
title=_('Input raw transaction'),
header_layout=_("Transaction to merge signatures from") + ":",
ok_label=_("Load transaction"),
config=self.config,
)
2019-10-23 17:09:41 +02:00
if not text:
return
tx = self.main_window.tx_from_text(text)
if not tx:
return
try:
self.tx.combine_with_other_psbt(tx)
except Exception as e:
self.show_error(_("Error combining partial transactions") + ":\n" + repr(e))
return
self.update()
2013-09-14 21:07:54 +02:00
2019-10-31 06:25:32 +01:00
def join_tx_with_another(self):
if not isinstance(self.tx, PartialTransaction):
return
text = text_dialog(
parent=self,
title=_('Input raw transaction'),
header_layout=_("Transaction to join with") + " (" + _("add inputs and outputs") + "):",
ok_label=_("Load transaction"),
config=self.config,
)
2019-10-31 06:25:32 +01:00
if not text:
return
tx = self.main_window.tx_from_text(text)
if not tx:
return
try:
self.tx.join_with_other_psbt(tx, config=self.config)
2019-10-31 06:25:32 +01:00
except Exception as e:
self.show_error(_("Error joining partial transactions") + ":\n" + repr(e))
return
self.update()
@rate_limited(0.5, ts_after=True)
def _throttled_update(self):
self.update()
2013-09-14 21:07:54 +02:00
def update(self):
if self.tx is None:
return
self.io_widget.update(self.tx)
2016-06-08 11:06:51 +02:00
desc = self.desc
base_unit = self.main_window.base_unit()
format_amount = self.main_window.format_amount
format_fiat_and_units = self.main_window.format_fiat_and_units
tx_details = self.wallet.get_tx_info(self.tx)
tx_mined_status = tx_details.tx_mined_status
exp_n = tx_details.mempool_depth_bytes
amount, fee = tx_details.amount, tx_details.fee
size = self.tx.estimated_size()
txid = self.tx.txid()
2021-02-26 15:53:01 +01:00
fx = self.main_window.fx
tx_item_fiat = None
if txid is not None and fx.is_enabled() and amount is not None:
tx_item_fiat = self.wallet.get_tx_item_fiat(
tx_hash=txid, amount_sat=abs(amount), fx=fx, tx_fee=fee)
if self.wallet.lnworker:
# if it is a group, collect ln amount
full_history = self.wallet.get_full_history()
item = full_history.get('group:' + txid)
ln_amount = item['ln_value'].value if item else None
else:
ln_amount = None
self.broadcast_button.setEnabled(tx_details.can_broadcast)
can_sign = not self.tx.is_complete() and \
(self.wallet.can_sign(self.tx) or bool(self.external_keypairs))
self.sign_button.setEnabled(can_sign and not self.io_widget.sighash_danger.needs_reject())
if sh_danger_msg := self.io_widget.sighash_danger.get_long_message():
self.sign_button.setToolTip(sh_danger_msg)
if tx_details.txid:
self.tx_hash_e.setText(tx_details.txid)
else:
# note: when not finalized, RBF and locktime changes do not trigger
# a make_tx, so the txid is unreliable, hence:
self.tx_hash_e.setText(_('Unknown'))
if not self.wallet.adb.get_transaction(txid):
self.tx_desc.hide()
self.tx_desc_label.hide()
else:
self.tx_desc.setText(desc)
self.tx_desc.show()
self.tx_desc_label.show()
self.status_label.setText(_('Status: {}').format(tx_details.status))
2013-09-14 21:07:54 +02:00
if tx_mined_status.timestamp:
time_str = datetime.datetime.fromtimestamp(tx_mined_status.timestamp).isoformat(' ')[:-3]
self.date_label.setText(_("Date: {}").format(time_str))
2013-09-14 21:07:54 +02:00
self.date_label.show()
elif exp_n is not None:
from electrum.fee_policy import FeePolicy
self.date_label.setText(_('Position in mempool: {}').format(FeePolicy.depth_tooltip(exp_n)))
self.date_label.show()
2013-09-14 21:07:54 +02:00
else:
self.date_label.hide()
if self.tx.locktime <= NLOCKTIME_BLOCKHEIGHT_MAX:
locktime_str = _('height')
else:
locktime_str = datetime.datetime.fromtimestamp(self.tx.locktime)
locktime_final_str = _("LockTime: {} ({})").format(self.tx.locktime, locktime_str)
self.locktime_final_label.setText(locktime_final_str)
nsequence_time = self.tx.get_time_based_relative_locktime()
nsequence_blocks = self.tx.get_block_based_relative_locktime()
if nsequence_time or nsequence_blocks:
if nsequence_time:
seconds = nsequence_time * 512
time_str = delta_time_str(datetime.timedelta(seconds=seconds))
else:
time_str = '{} blocks'.format(nsequence_blocks)
nsequence_str = _("Relative locktime: {}").format(time_str)
self.nsequence_label.setText(nsequence_str)
else:
self.nsequence_label.hide()
# TODO: 'Yes'/'No' might be better translatable than 'True'/'False'?
self.rbf_label.setText(_('Replace by fee: {}').format(_('True') if self.tx.is_rbf_enabled() else _('False')))
if tx_mined_status.header_hash:
self.block_height_label.setText(_("At block height: {}").format(tx_mined_status.height))
else:
self.block_height_label.hide()
if amount is None and ln_amount is None:
2016-06-02 10:40:16 +02:00
amount_str = _("Transaction unrelated to your wallet")
elif amount is None:
amount_str = ''
2016-06-08 11:06:51 +02:00
else:
amount_str = ''
2021-02-26 15:53:01 +01:00
if fx.is_enabled():
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)))
amount_str = format_amount(abs(amount)) + ' ' + base_unit + amount_str
if amount > 0:
amount_str = _("Amount received: {}").format(amount_str)
else:
amount_str = _("Amount sent: {}").format(amount_str)
if amount_str:
self.amount_label.setText(amount_str)
else:
self.amount_label.hide()
size_str = _("Size: {} {}").format(size, UI_UNIT_NAME_TXSIZE_VBYTES)
2021-02-26 15:53:01 +01:00
if fee is None:
if prog := self._fetch_txin_data_progress:
if not prog.has_errored:
fee_str = _("Downloading input data... {}").format(f"({prog.num_tasks_done}/{prog.num_tasks_total})")
else:
fee_str = _("Downloading input data... {}").format(_("error"))
else:
fee_str = _("Fee: {}").format(_("unknown"))
else:
fee_str = _("Fee: {}").format(f'{format_amount(fee)} {base_unit}')
2021-02-26 15:53:01 +01:00
if fx.is_enabled():
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))
fee_rate = Decimal(fee) / size # sat/byte
fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate * 1000)
if isinstance(self.tx, PartialTransaction):
# 'amount' is zero for self-payments, so in that case we use sum-of-outputs
2024-11-18 08:43:47 +01:00
invoice_amt = abs(amount) if amount else self.tx.output_value()
fee_warning_tuple = self.wallet.get_tx_fee_warning(
invoice_amt=invoice_amt, tx_size=size, fee=fee, txid=self.tx.txid())
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
fee_str += " - <font color={color}>{header}: {body}</font>".format(
header=_('Warning'),
body=short_warning,
color=ColorScheme.RED.as_color().name(),
)
if isinstance(self.tx, PartialTransaction):
sh_warning = self.io_widget.sighash_danger.get_long_message()
self.fee_warning_icon.setToolTip(str(sh_warning))
self.fee_warning_icon.setVisible(can_sign and bool(sh_warning))
2016-06-02 10:40:16 +02:00
self.fee_label.setText(fee_str)
self.size_label.setText(size_str)
2020-05-27 09:59:53 +02:00
if ln_amount is None or ln_amount == 0:
ln_amount_str = ''
elif ln_amount > 0:
ln_amount_str = _('Amount received in channels: {}').format(format_amount(ln_amount) + ' ' + base_unit)
2021-02-26 15:53:01 +01:00
else:
assert ln_amount < 0, f"{ln_amount!r}"
ln_amount_str = _('Amount withdrawn from channels: {}').format(format_amount(-ln_amount) + ' ' + base_unit)
if ln_amount_str:
self.ln_amount_label.setText(ln_amount_str)
else:
self.ln_amount_label.hide()
show_psbt_only_widgets = isinstance(self.tx, PartialTransaction)
for widget in self.psbt_only_widgets:
if isinstance(widget, QMenu):
widget.menuAction().setVisible(show_psbt_only_widgets)
else:
widget.setVisible(show_psbt_only_widgets)
if tx_details.is_lightning_funding_tx:
self._ptx_join_txs_action.setEnabled(False) # would change txid
self.save_button.setEnabled(tx_details.can_save_as_local)
if tx_details.can_save_as_local:
self.save_button.setToolTip(_("Add transaction to history, without broadcasting it"))
else:
self.save_button.setToolTip(_("Transaction already in history or not yet signed."))
2014-06-24 14:48:15 +02:00
run_hook('transaction_dialog_update', self)
2013-09-14 21:07:54 +02:00
def add_tx_stats(self, vbox):
hbox_stats = QHBoxLayout()
hbox_stats.setContentsMargins(0, 0, 0, 0)
hbox_stats_w = QWidget()
hbox_stats_w.setLayout(hbox_stats)
hbox_stats_w.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
# left column
vbox_left = QVBoxLayout()
self.status_label = TxDetailLabel()
vbox_left.addWidget(self.status_label)
self.date_label = TxDetailLabel()
vbox_left.addWidget(self.date_label)
self.amount_label = TxDetailLabel()
vbox_left.addWidget(self.amount_label)
self.ln_amount_label = TxDetailLabel()
vbox_left.addWidget(self.ln_amount_label)
fee_hbox = QHBoxLayout()
self.fee_label = TxDetailLabel()
fee_hbox.addWidget(self.fee_label)
self.fee_warning_icon = QLabel()
pixmap = QPixmap(icon_path("warning"))
pixmap_size = round(2 * char_width_in_lineedit())
pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
self.fee_warning_icon.setPixmap(pixmap)
self.fee_warning_icon.setVisible(False)
fee_hbox.addWidget(self.fee_warning_icon)
fee_hbox.addStretch(1)
vbox_left.addLayout(fee_hbox)
vbox_left.addStretch(1)
hbox_stats.addLayout(vbox_left, 50)
# vertical line separator
hbox_stats.addWidget(VLine())
# right column
vbox_right = QVBoxLayout()
self.size_label = TxDetailLabel()
vbox_right.addWidget(self.size_label)
self.rbf_label = TxDetailLabel()
vbox_right.addWidget(self.rbf_label)
self.locktime_final_label = TxDetailLabel()
vbox_right.addWidget(self.locktime_final_label)
self.nsequence_label = TxDetailLabel()
vbox_right.addWidget(self.nsequence_label)
self.block_height_label = TxDetailLabel()
vbox_right.addWidget(self.block_height_label)
vbox_right.addStretch(1)
hbox_stats.addLayout(vbox_right, 50)
vbox.addWidget(hbox_stats_w)
# set visibility after parenting can be determined by Qt
self.rbf_label.setVisible(True)
self.locktime_final_label.setVisible(True)
def set_title(self):
txid = self.tx.txid() or "<no txid yet>"
self.setWindowTitle(_("Transaction") + ' ' + txid)
def maybe_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.
"""
if not self.config.GUI_QT_TX_DIALOG_FETCH_TXIN_DATA:
return
tx = self.tx
if not tx:
return
if self._fetch_txin_data_fut is not None:
return
network = self.wallet.network
2025-01-23 12:58:28 +01:00
def progress_cb(prog: TxinDataFetchProgress):
self._fetch_txin_data_progress = prog
self.throttled_update_sig.emit()
2025-01-23 12:58:28 +01:00
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):
2019-05-02 12:05:27 +02:00
def __init__(self, *, word_wrap=None):
super().__init__()
self.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
2019-05-02 12:05:27 +02:00
if word_wrap is not None:
self.setWordWrap(word_wrap)
class TxOutputColoring:
# used for both inputs and outputs
def __init__(
self,
*,
legend: str,
color: ColorSchemeItem,
tooltip: str,
):
self.color = color.as_color(background=True)
self.legend_label = QLabel("<font color={color}>{box_char}</font> = {label}".format(
color=self.color.name(),
box_char="",
label=legend,
))
font = self.legend_label.font()
font.setPointSize(font.pointSize() - 1)
self.legend_label.setFont(font)
self.legend_label.setVisible(False)
self.text_char_format = QTextCharFormat()
self.text_char_format.setBackground(QBrush(self.color))
self.text_char_format.setToolTip(tooltip)