Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
gruve-p committed Oct 31, 2024
2 parents 8779c15 + 3ee2d6a commit ada4d99
Show file tree
Hide file tree
Showing 19 changed files with 186 additions and 121 deletions.
8 changes: 8 additions & 0 deletions electrum_grs/gui/qml/components/WalletMainView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,14 @@ Item {
}
_confirmPaymentDialog.destroy()
}
onSignError: (message) => {
var dialog = app.messageDialog.createObject(mainView, {
title: qsTr('Error'),
text: [qsTr('Could not sign tx'), message].join('\n\n'),
iconSource: '../../../icons/warning.png'
})
dialog.open()
}
}
// TODO: lingering confirmPaymentDialogs can raise exceptions in
// the child finalizer when currentWallet disappears, but we need
Expand Down
4 changes: 3 additions & 1 deletion electrum_grs/gui/qml/qetxdetails.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,10 @@ def _sign(self, broadcast):
if broadcast:
self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded)
self._wallet.broadcastFailed.connect(self.onBroadcastFailed)
self._wallet.sign_and_broadcast(self._tx, on_success=self.on_signed_tx)
else:
self._wallet.sign(self._tx, on_success=self.on_signed_tx)

self._wallet.sign(self._tx, broadcast=broadcast, on_success=self.on_signed_tx)
# side-effect: signing updates self._tx
# we rely on this for broadcast

Expand Down
9 changes: 7 additions & 2 deletions electrum_grs/gui/qml/qetxfinalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ class QETxFinalizer(TxFeeSlider):
_logger = get_logger(__name__)

finished = pyqtSignal([bool, bool, bool], arguments=['signed', 'saved', 'complete'])
signError = pyqtSignal([str], arguments=['message'])

def __init__(self, parent=None, *, make_tx=None, accept=None):
super().__init__(parent)
Expand Down Expand Up @@ -426,15 +427,15 @@ def signAndSend(self):
self.f_accept(self._tx)
return

self._wallet.sign(self._tx, broadcast=True, on_success=partial(self.on_signed_tx, False))
self._wallet.sign_and_broadcast(self._tx, on_success=partial(self.on_signed_tx, False), on_failure=self.on_sign_failed)

@pyqtSlot()
def sign(self):
if not self._valid or not self._tx:
self._logger.error('no valid tx')
return

self._wallet.sign(self._tx, broadcast=False, on_success=partial(self.on_signed_tx, True))
self._wallet.sign(self._tx, on_success=partial(self.on_signed_tx, True), on_failure=self.on_sign_failed)

def on_signed_tx(self, save: bool, tx: Transaction):
self._logger.debug('on_signed_tx')
Expand All @@ -446,6 +447,10 @@ def on_signed_tx(self, save: bool, tx: Transaction):
self._logger.error('Could not save tx')
self.finished.emit(True, saved, tx.is_complete())

def on_sign_failed(self, msg: str = None):
self._logger.debug('on_sign_failed')
self.signError.emit(msg)

@pyqtSlot(result='QVariantList')
def getSerializedTx(self):
txqr = self._tx.to_qr_data()
Expand Down
59 changes: 31 additions & 28 deletions electrum_grs/gui/qml/qewallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import queue
import threading
import time
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING, Callable, Optional, Any
from functools import partial

from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer
Expand Down Expand Up @@ -65,8 +65,6 @@ def getInstanceFor(cls, wallet):
paymentSucceeded = pyqtSignal([str], arguments=['key'])
paymentFailed = pyqtSignal([str, str], arguments=['key', 'reason'])
requestNewPassword = pyqtSignal()
signSucceeded = pyqtSignal([str], arguments=['txid'])
signFailed = pyqtSignal([str], arguments=['message'])
broadcastSucceeded = pyqtSignal([str], arguments=['txid'])
broadcastFailed = pyqtSignal([str, str, str], arguments=['txid', 'code', 'reason'])
saveTxSuccess = pyqtSignal([str], arguments=['txid'])
Expand Down Expand Up @@ -518,42 +516,46 @@ def enableLightning(self):
self.isLightningChanged.emit()
self.dataChanged.emit()

@auth_protect(message=_('Sign on-chain transaction?')) # FIXME auth msg cannot be explicit due to "broadcast" param
def sign(self, tx, *, broadcast: bool = False, on_success: Callable[[Transaction], None] = None, on_failure: Callable[[], None] = None):
sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast, on_success), partial(self.on_sign_failed, on_failure))
if sign_hook:
success = self.do_sign(tx, False)
if success:
self._logger.debug('plugin needs to sign tx too')
sign_hook(tx)
return
else:
success = self.do_sign(tx, broadcast)

if success:
if on_success:
on_success(tx)
else:
if on_failure:
on_failure()

def do_sign(self, tx, broadcast):
@auth_protect(message=_('Sign and send on-chain transaction?'))
def sign_and_broadcast(self, tx, *,
on_success: Callable[[Transaction], None] = None,
on_failure: Callable[[Optional[Any]], None] = None) -> None:
self.do_sign(tx, True, on_success, on_failure)

@auth_protect(message=_('Sign on-chain transaction?'))
def sign(self, tx, *,
on_success: Callable[[Transaction], None] = None,
on_failure: Callable[[Optional[Any]], None] = None) -> None:
self.do_sign(tx, False, on_success, on_failure)

def do_sign(self, tx, broadcast, on_success: Callable[[Transaction], None] = None, on_failure: Callable[[Optional[Any]], None] = None):
# tc_sign_wrapper is only used by 2fa. don't pass on_failure handler, it is handled via otpFailed signal
sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx,
partial(self.on_sign_complete, broadcast, on_success),
partial(self.on_sign_failed, None))
try:
# ignore_warnings=True, because UI checks and asks user confirmation itself
tx = self.wallet.sign_transaction(tx, self.password, ignore_warnings=True)
except BaseException as e:
self._logger.error(f'{e!r}')
self.signFailed.emit(str(e))
if on_failure:
on_failure(str(e))
return

if tx is None:
self._logger.info('did not sign')
return False
if on_failure:
on_failure()
return

if sign_hook:
self._logger.debug('plugin needs to sign tx too')
sign_hook(tx)
return

txid = tx.txid()
self._logger.debug(f'do_sign(), txid={txid}')

self.signSucceeded.emit(txid)

if not tx.is_complete():
self._logger.debug('tx not complete')
broadcast = False
Expand All @@ -564,7 +566,8 @@ def do_sign(self, tx, broadcast):
# not broadcasted, so refresh history here
self.historyModel.initModel(True)

return True
if on_success:
on_success(tx)

# this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok
def on_sign_complete(self, broadcast, cb: Callable[[Transaction], None] = None, tx: Transaction = None):
Expand Down
13 changes: 10 additions & 3 deletions electrum_grs/gui/qt/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1259,10 +1259,12 @@ def read_QIcon_from_bytes(b: bytes) -> QIcon:
qp = read_QPixmap_from_bytes(b)
return QIcon(qp)


class IconLabel(QWidget):
HorizontalSpacing = 2
def __init__(self, *, text='', final_stretch=True):
def __init__(self, *, text='', final_stretch=True, reverse=False, hide_if_empty=False):
super(QWidget, self).__init__()
self.hide_if_empty = hide_if_empty
size = max(16, font_height())
self.icon_size = QSize(size, size)
layout = QHBoxLayout()
Expand All @@ -1271,13 +1273,18 @@ def __init__(self, *, text='', final_stretch=True):
self.icon = QLabel()
self.label = QLabel(text)
self.label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
layout.addWidget(self.label)
layout.addWidget(self.icon if reverse else self.label)
layout.addSpacing(self.HorizontalSpacing)
layout.addWidget(self.icon)
layout.addWidget(self.label if reverse else self.icon)
if final_stretch:
layout.addStretch()
self.setText(text)

def setText(self, text):
self.label.setText(text)
if self.hide_if_empty:
self.setVisible(bool(text))

def setIcon(self, icon):
self.icon.setPixmap(icon.pixmap(self.icon_size))
self.icon.repaint() # macOS hack for #6269
Expand Down
78 changes: 43 additions & 35 deletions electrum_grs/gui/qt/wizard/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from electrum_grs.gui.qt.password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD, PasswordLayoutForHW
from electrum_grs.gui.qt.seed_dialog import SeedWidget, MSG_PASSPHRASE_WARN_ISSUE4566, KeysWidget
from electrum_grs.gui.qt.util import (PasswordLineEdit, char_width_in_lineedit, WWLabel, InfoButton, font_height,
ChoiceWidget, MessageBoxMixin, icon_path)
ChoiceWidget, MessageBoxMixin, icon_path, IconLabel, read_QIcon)

if TYPE_CHECKING:
from electrum_grs.simple_config import SimpleConfig
Expand Down Expand Up @@ -450,7 +450,6 @@ def apply(self):
self.wizard_data['seed_type'] = self.seed_type
self.wizard_data['seed_extend'] = self.seed_widget.is_ext
self.wizard_data['seed_variant'] = 'electrum'
self.wizard_data['seed_extra_words'] = '' # empty default

def create_seed(self):
self.busy = True
Expand Down Expand Up @@ -516,6 +515,12 @@ def __init__(self, parent, wizard):
self.ext_edit.textEdited.connect(self.on_text_edited)
self.layout().addWidget(self.ext_edit)
self.layout().addStretch(1)
self.warn_label = IconLabel(reverse=True, hide_if_empty=True)
self.warn_label.setIcon(read_QIcon('warning.png'))
self.layout().addWidget(self.warn_label)

def on_ready(self):
self.validate()

def on_text_edited(self, text):
# TODO also for cosigners?
Expand All @@ -525,30 +530,10 @@ def on_text_edited(self, text):

def validate(self):
self.apply()
text = self.ext_edit.text()
if len(text) == 0:
self.valid = False
return

cosigner_data = self.wizard.current_cosigner(self.wizard_data)

if self.wizard_data['wallet_type'] == 'multisig':
if 'seed_variant' in cosigner_data and cosigner_data['seed_variant'] in ['bip39', 'slip39']:
# defer validation to when derivation path is known
self.valid = True
else:
if self.wizard.has_duplicate_masterkeys(self.wizard_data):
self.logger.debug('Duplicate master keys!')
# TODO: user feedback
self.valid = False
elif self.wizard.has_heterogeneous_masterkeys(self.wizard_data):
self.logger.debug('Heterogenous master keys!')
# TODO: user feedback
self.valid = False
else:
self.valid = True
else:
self.valid = True
musig_valid, errortext = self.wizard.check_multisig_constraints(self.wizard_data)
self.valid = musig_valid
self.warn_label.setText(errortext)

def apply(self):
cosigner_data = self.wizard.current_cosigner(self.wizard_data)
Expand All @@ -567,8 +552,14 @@ def __init__(self, parent, wizard):
self.layout().addWidget(self.ext_edit)
self.layout().addStretch(1)

def on_text_edited(self, text):
self.valid = text == self.wizard_data['seed_extra_words']
def on_ready(self):
self.validate()

def on_text_edited(self, *args):
self.validate()

def validate(self):
self.valid = self.ext_edit.text() == self.wizard_data['seed_extra_words']

def apply(self):
pass
Expand All @@ -580,6 +571,8 @@ def __init__(self, parent, wizard):
Logger.__init__(self)

self.layout().addWidget(WWLabel(_('Please enter your seed phrase in order to restore your wallet.')))
self.warn_label = IconLabel(reverse=True, hide_if_empty=True)
self.warn_label.setIcon(read_QIcon('warning.png'))

self.seed_widget = None
self.can_passphrase = True
Expand Down Expand Up @@ -610,6 +603,8 @@ def seed_valid_changed(valid):
self.layout().addWidget(self.seed_widget)
self.layout().addStretch(1)

self.layout().addWidget(self.warn_label)

def is_seed(self, x):
# really only used for electrum seeds. bip39 and slip39 are validated in SeedWidget
t = mnemonic.calc_seed_type(x)
Expand All @@ -635,10 +630,11 @@ def validate(self):
return

self.apply()
if not self.wizard.check_multisig_constraints(self.wizard_data)[0]:
# TODO: user feedback
musig_valid, errortext = self.wizard.check_multisig_constraints(self.wizard_data)
if not musig_valid:
seed_valid = False

self.warn_label.setText(errortext)
self.valid = seed_valid

def apply(self):
Expand All @@ -651,7 +647,6 @@ def apply(self):
else:
cosigner_data['seed_type'] = self.seed_widget.seed_type
cosigner_data['seed_extend'] = self.seed_widget.is_ext if self.can_passphrase else False
cosigner_data['seed_extra_words'] = '' # empty default


class WCScriptAndDerivation(WalletWizardComponent, Logger):
Expand All @@ -662,6 +657,9 @@ def __init__(self, parent, wizard):
self.choice_w = None
self.derivation_path_edit = None

self.warn_label = IconLabel(reverse=True, hide_if_empty=True)
self.warn_label.setIcon(read_QIcon('warning.png'))

def on_ready(self):
message1 = _('Choose the type of addresses in your wallet.')
message2 = ' '.join([
Expand Down Expand Up @@ -736,6 +734,7 @@ def on_choice_click(index):
on_choice_click(self.choice_w.selected_index) # set default value for derivation path

self.layout().addStretch(1)
self.layout().addWidget(self.warn_label)

def validate(self):
self.apply()
Expand All @@ -744,10 +743,12 @@ def validate(self):
valid = is_bip32_derivation(cosigner_data['derivation_path'])

if valid:
valid, error = self.wizard.check_multisig_constraints(self.wizard_data)
valid, errortext = self.wizard.check_multisig_constraints(self.wizard_data)
if not valid:
# TODO: user feedback
self.logger.error(error)
self.logger.error(errortext)
self.warn_label.setText(errortext)
else:
self.warn_label.setText(_('Invalid derivation path'))

self.valid = valid

Expand Down Expand Up @@ -825,6 +826,9 @@ def __init__(self, parent, wizard):
self.label.setMinimumWidth(400)
self.header_layout.addWidget(self.label)

self.warn_label = IconLabel(reverse=True, hide_if_empty=True)
self.warn_label.setIcon(read_QIcon('warning.png'))

def on_ready(self):
if self.wizard_data['wallet_type'] == 'standard':
self.label.setText(self.message_create)
Expand All @@ -840,10 +844,12 @@ def is_valid(x) -> bool:

def is_valid(x) -> bool:
if not keystore.is_bip32_key(x):
self.warn_label.setText(_('Invalid key'))
return False
self.apply()
if not self.wizard.check_multisig_constraints(self.wizard_data)[0]:
# TODO: user feedback
musig_valid, errortext = self.wizard.check_multisig_constraints(self.wizard_data)
self.warn_label.setText(errortext)
if not musig_valid:
return False
return True
else:
Expand All @@ -858,6 +864,8 @@ def key_valid_changed(valid):
self.keys_widget.validChanged.connect(key_valid_changed)

self.layout().addWidget(self.keys_widget)
self.layout().addStretch()
self.layout().addWidget(self.warn_label)

def apply(self):
text = self.keys_widget.get_text()
Expand Down
Loading

0 comments on commit ada4d99

Please sign in to comment.