diff --git a/README.rst b/README.rst
index eb5f90669eca..5e85c4f8f5cd 100644
--- a/README.rst
+++ b/README.rst
@@ -52,6 +52,9 @@ Run install (this should install dependencies)::
python3 -m pip install .[fast]
+In case of problems with btchip library::
+
+ pip install git+https://git@github.com/bitcoinvault/btchip-python.git@v0.1.31-btcv
Compile the protobuf description file::
diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt
index dc1f233a3f55..a1bf4386376f 100644
--- a/contrib/requirements/requirements-hw.txt
+++ b/contrib/requirements/requirements-hw.txt
@@ -1,6 +1,6 @@
trezor[hidapi]>=0.11.5
safet>=0.1.5
keepkey>=6.0.3
-btchip-python>=0.1.26
+btchip-python@git://github.com/bitcoinvault/btchip-python.git@v0.1.31-btcv
ckcc-protocol>=0.7.7
hidapi
diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
index ecbaace339aa..8f27c75b44a7 100755
--- a/electrum/base_wizard.py
+++ b/electrum/base_wizard.py
@@ -28,6 +28,8 @@
import sys
from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional
+from btchip.btchipException import BTChipException
+
from . import bitcoin
from . import keystore
from . import mnemonic
@@ -41,7 +43,7 @@
from .simple_config import SimpleConfig
from .storage import (WalletStorage, StorageEncryptionVersion,
get_derivation_used_for_hw_device_encryption)
-from .three_keys import short_mnemonic
+from .three_keys.pubkey_type import PubkeyType
from .util import UserCancelled, InvalidPassword
from .wallet import (wallet_types)
@@ -49,7 +51,10 @@
from .plugin import DeviceInfo
# hardware device setup purpose
-HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
+HWD_SETUP_NEW_WALLET = 0
+HWD_SETUP_DECRYPT_WALLET = 1
+HWD_SETUP_NEW_BTCV_WALLET = 2
+HWD_SETUP_DECRYPT_BTCV_WALLET = 3
class ScriptTypeNotSupported(Exception): pass
@@ -215,6 +220,17 @@ def process_choice(choice):
action = 'three_keys_2fa' + sub_action
else:
raise Exception('Invalid multikey wallet type: ' + self.wallet_type)
+ elif choice[:11] == 'multikey_hw':
+ self.data['multikey_type'] = 'hw'
+ self.data['wallet_type'] += '-hw'
+ self.wallet_type += '-hw'
+ sub_action = choice[-7:]
+ if self.wallet_type == '2-key-hw':
+ action = 'two_keys_hw' + sub_action
+ elif self.wallet_type == '3-key-hw':
+ action = 'three_keys_hw' + sub_action
+ else:
+ raise Exception('Invalid multikey wallet type: ' + self.wallet_type)
else:
raise Exception('Invalid choice: ' + choice)
self.run(action)
@@ -228,7 +244,9 @@ def process_choice(choice):
choices = [
('multikey_2fa_create', _('Use Gold Wallet and create a new wallet')),
('multikey_2fa_import', _('Use Gold Wallet and import an existing wallet')),
- ('multikey_standalone', _('Do not use Gold Wallet')),
+ ('multikey_hw_create', _('Use Ledger device and create a new wallet')),
+ ('multikey_hw_import', _('Use Ledger device and import an existing wallet')),
+ ('multikey_standalone', _('Do not use Gold Wallet nor Ledger device')),
]
self.choice_dialog(title=title, message=message, choices=choices, run_next=process_choice)
@@ -242,6 +260,13 @@ def two_keys_2fa_create(self):
def two_keys_2fa_import(self):
self.get_authenticator_pubkey(run_next=self.on_two_keys_import)
+ def two_keys_hw_create(self):
+ self.get_hw_password(run_next=self.on_two_keys_hw_create, title=_('Cancel password'))
+
+ def two_keys_hw_import(self):
+ self.check_hw_password(run_next=self.on_two_keys_hw_import, title=_('Cancel password'))
+
+
def on_two_keys_create(self, recovery_pubkey: str):
self.data['recovery_pubkey'] = recovery_pubkey
self.run('choose_keystore')
@@ -250,6 +275,14 @@ def on_two_keys_import(self, recovery_pubkey: str):
self.data['recovery_pubkey'] = recovery_pubkey
self.run('restore_from_seed')
+ def on_two_keys_hw_create(self, recovery_password: str):
+ self.data['recovery_password'] = recovery_password
+ self.run('choose_hw_device', HWD_SETUP_NEW_BTCV_WALLET)
+
+ def on_two_keys_hw_import(self, recovery_password: str):
+ self.data['recovery_password'] = recovery_password
+ self.run('choose_hw_device', HWD_SETUP_DECRYPT_BTCV_WALLET)
+
def three_keys_standalone(self):
def collect_instant_pubkey(instant_pubkey: str):
self.data['instant_pubkey'] = instant_pubkey
@@ -271,6 +304,12 @@ def collect_instant_pubkey(instant_pubkey: str):
self.get_authenticator_pubkey(run_next=collect_instant_pubkey)
+ def three_keys_hw_create(self):
+ self.get_hw_passwords(run_next=self.on_three_keys_hw_create, title=_('Instant and cancel passwords'))
+
+ def three_keys_hw_import(self):
+ self.check_hw_passwords(run_next=self.on_three_keys_hw_import, title=_('Instant and cancel passwords'))
+
def on_three_keys_create(self, recovery_pubkey: str):
self.data['recovery_pubkey'] = recovery_pubkey
self.run('choose_keystore')
@@ -279,8 +318,18 @@ def on_three_keys_import(self, recovery_pubkey: str):
self.data['recovery_pubkey'] = recovery_pubkey
self.run('restore_from_seed')
+ def on_three_keys_hw_create(self, *passwords):
+ self.data['instant_password'] = passwords[0]
+ self.data['recovery_password'] = passwords[1]
+ self.run('choose_hw_device', HWD_SETUP_NEW_BTCV_WALLET)
+
+ def on_three_keys_hw_import(self, *passwords):
+ self.data['instant_password'] = passwords[0]
+ self.data['recovery_password'] = passwords[1]
+ self.run('choose_hw_device', HWD_SETUP_DECRYPT_BTCV_WALLET)
+
def choose_keystore(self):
- assert self.wallet_type in ['standard', 'multisig', '2-key', '3-key']
+ assert self.wallet_type in ['standard', 'multisig', '2-key', '3-key', '2-key-hw', '3-key-hw']
i = len(self.keystores)
title = _('Add cosigner') + ' (%d of %d)' % (i + 1, self.n) if self.wallet_type == 'multisig' else _('Keystore')
if self.wallet_type == 'multisig' and i > 0:
@@ -304,7 +353,7 @@ def choose_keystore(self):
advanced_choices = [
('restore_from_key', _('Use a master key')),
]
- if not self.is_kivy and self.wallet_type not in ['2-key', '3-key']:
+ if not self.is_kivy and self.wallet_type not in ['2-key', '3-key', '2-key-hw', '3-key-hw']:
advanced_choices.append(('choose_hw_device', _('Use a hardware device')))
if self.wallet_type == 'multisig':
self.choice_dialog(title=title, message=message, choices=base_choices + advanced_choices, run_next=self.run)
@@ -489,6 +538,28 @@ def f(derivation, script_type):
if hasattr(client, 'clear_session'): # FIXME not all hw wallet plugins have this
client.clear_session()
raise
+ elif purpose == HWD_SETUP_NEW_BTCV_WALLET:
+ if 'recovery_password' not in self.data:
+ raise Exception('Recovery password not set')
+
+ self.plugin.set_recovery_password(device_info.device.id_, self.data['recovery_password'], self)
+ if 'instant_password' in self.data:
+ self.plugin.set_instant_password(device_info.device.id_, self.data['instant_password'], self)
+ else:
+ self.plugin.set_instant_password(device_info.device.id_, str(0x00), self)
+
+ self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh')
+ elif purpose == HWD_SETUP_DECRYPT_BTCV_WALLET:
+ if 'recovery_password' not in self.data:
+ raise Exception('Invalid recovery password')
+ if 'instant_password' in self.data:
+ self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh',
+ btcv_instant_password_check=self.data['instant_password'],
+ btcv_recovery_password_check=self.data['recovery_password'])
+ else:
+ self.run('on_hw_derivation', name, device_info, bip44_derivation(0), 'p2wsh-p2sh',
+ btcv_recovery_password_check=self.data['recovery_password'],
+ btcv_instant_password_check=str(0x00))
else:
raise Exception('unknown purpose: %s' % purpose)
@@ -526,10 +597,11 @@ def derivation_and_script_type_dialog(self, f):
self.show_error(e)
# let the user choose again
- def on_hw_derivation(self, name, device_info, derivation, xtype):
+ def on_hw_derivation(self, name, device_info, derivation, xtype, xpub_keystore=False, pubkey_type=PubkeyType.PUBKEY_ALERT,
+ btcv_instant_password_check=None, btcv_recovery_password_check=None):
from .keystore import hardware_keystore
try:
- xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
+ xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self, pubkey_type)
root_xpub = self.plugin.get_xpub(device_info.device.id_, 'm', 'standard', self)
except ScriptTypeNotSupported:
raise # this is handled in derivation_dialog
@@ -538,16 +610,24 @@ def on_hw_derivation(self, name, device_info, derivation, xtype):
self.show_error(e)
return
xfp = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower()
- d = {
- 'type': 'hardware',
- 'hw_type': name,
- 'derivation': derivation,
- 'root_fingerprint': xfp,
- 'xpub': xpub,
- 'label': device_info.label,
- }
- k = hardware_keystore(d)
- self.on_keystore(k)
+ if xpub_keystore:
+ k = keystore.from_master_key(xpub)
+ else:
+ d = {
+ 'type': 'hardware',
+ 'hw_type': name,
+ 'derivation': derivation,
+ 'root_fingerprint': xfp,
+ 'xpub': xpub,
+ 'label': device_info.label,
+ }
+ k = hardware_keystore(d)
+ from electrum.plugins.ledger.ledger import Ledger_KeyStore
+ if isinstance(k, Ledger_KeyStore) \
+ and not k.are_3keys_ledger_passwords_correct(self, btcv_instant_password_check, btcv_recovery_password_check):
+ # user already notified about invalid password(s)
+ return self.terminate()
+ self.on_keystore(k, name, device_info)
def passphrase_dialog(self, run_next, is_restoring=False):
title = _('Seed extension')
@@ -609,7 +689,7 @@ def on_bip43(self, seed, passphrase, derivation, script_type):
k = keystore.from_bip39_seed(seed, passphrase, derivation, xtype=script_type)
self.on_keystore(k)
- def on_keystore(self, k):
+ def on_keystore(self, k, name=None, device_info=None):
has_xpub = isinstance(k, keystore.Xpub)
if has_xpub:
t1 = xpub_type(k.xpub)
@@ -620,6 +700,28 @@ def on_keystore(self, k):
return
self.keystores.append(k)
self.run('create_wallet')
+ elif self.wallet_type in ['2-key-hw', '3-key-hw']:
+ if has_xpub and t1 != 'p2wsh-p2sh':
+ self.show_error(_('Wrong key type') + ' %s' % t1)
+ self.run('choose_keystore')
+ return
+ self.keystores.append(k)
+
+ keystores_needed = 2 if self.wallet_type == '2-key-hw' else 3
+ if len(self.keystores) < keystores_needed:
+ if not name or not device_info:
+ self.show_error(_('Missing device info'))
+ self.run('choose_keystore')
+ return
+ script_type = 'p2wsh-p2sh'
+ derivation = bip44_derivation(0)
+ if keystores_needed == 3:
+ pubkey_type = len(self.keystores)
+ elif keystores_needed == 2:
+ pubkey_type = 2 * len(self.keystores)
+ self.run('on_hw_derivation', name, device_info, derivation, script_type, True, pubkey_type)
+ else:
+ self.run('create_wallet')
elif self.wallet_type == 'multisig':
assert has_xpub
if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']:
@@ -694,7 +796,7 @@ def on_password(self, password, *, encrypt_storage: bool,
self.data['seed_type'] = self.seed_type
keys = self.keystores[0].dump()
self.data['keystore'] = keys
- elif self.wallet_type == 'multisig':
+ elif self.wallet_type in ['multisig', '2-key-hw', '3-key-hw']:
for i, k in enumerate(self.keystores):
self.data['x%d/' % (i + 1)] = k.dump()
elif self.wallet_type == 'imported':
diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
index 13e691be05f9..e66279e5d92f 100644
--- a/electrum/gui/qt/__init__.py
+++ b/electrum/gui/qt/__init__.py
@@ -31,7 +31,7 @@
from typing import Optional, TYPE_CHECKING
from .terms_and_conditions_mixin import TermsNotAccepted
-from .three_keys_windows import ElectrumARWindow, ElectrumAIRWindow
+from .three_keys_windows import ElectrumARWindow, ElectrumAIRWindow, ElectrumARHWWindow, ElectrumAIRHWWindow
try:
import PyQt5
@@ -211,6 +211,10 @@ def _create_window_for_wallet(self, wallet):
w = ElectrumARWindow(self, wallet)
elif wallet_type == '3-key':
w = ElectrumAIRWindow(self, wallet)
+ elif wallet_type == '2-key-hw':
+ w = ElectrumARHWWindow(self, wallet)
+ elif wallet_type == '3-key-hw':
+ w = ElectrumAIRHWWindow(self, wallet)
else:
w = ElectrumWindow(self, wallet)
self.windows.append(w)
diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py
index 2aea4408a5b2..ac1e864c7c70 100755
--- a/electrum/gui/qt/installwizard.py
+++ b/electrum/gui/qt/installwizard.py
@@ -21,7 +21,7 @@
from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
from .seed_dialog import SeedLayout, KeysLayout
from .terms_and_conditions_mixin import TermsAndConditionsMixin, PushedButton
-from .three_keys_dialogs import InsertPubKeyDialog, Qr2FaDialog
+from .three_keys_dialogs import InsertPubKeyDialog, InsertHWPasswordDialog, Qr2FaDialog, CheckHWPasswordDialog
from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel,
InfoButton, char_width_in_lineedit, get_default_language)
@@ -651,6 +651,84 @@ def get_authenticator_pubkey(self, run_next, disallowed_key=None):
self.exec_layout(layout, _('Gold Wallet authenticator public key'), next_enabled=False)
return layout.get_compressed_pubkey()
+ @wizard_dialog
+ def get_hw_password(self, run_next, title):
+ label = QLabel()
+ message = _('Please provide password related to your Ledger device')
+ label.setText(message)
+ label.setOpenExternalLinks(True)
+ label.setTextInteractionFlags(Qt.TextBrowserInteraction)
+ label.setWordWrap(True)
+
+ layout = InsertHWPasswordDialog(self, message_label=label)
+ self.exec_layout(layout, title, next_enabled=False)
+ return layout.get_password()
+
+ @wizard_dialog
+ def get_hw_passwords(self, run_next, title):
+ label1 = QLabel()
+ message1 = _('Please provide instant password related to your Ledger device')
+ label1.setText(message1)
+ label1.setOpenExternalLinks(True)
+ label1.setTextInteractionFlags(Qt.TextBrowserInteraction)
+ label1.setWordWrap(True)
+
+ layout1 = InsertHWPasswordDialog(self, message_label=label1)
+ self.exec_layout(layout1, title, next_enabled=False)
+ instant_password = layout1.get_password()
+
+ label2 = QLabel()
+ message2 = _('Please provide cancel password related to your Ledger device')
+ label2.setText(message2)
+ label2.setOpenExternalLinks(True)
+ label2.setTextInteractionFlags(Qt.TextBrowserInteraction)
+ label2.setWordWrap(True)
+
+ layout2 = InsertHWPasswordDialog(self, message_label=label2)
+ self.exec_layout(layout2, title, next_enabled=False)
+ recovery_password = layout2.get_password()
+
+ return (instant_password, recovery_password)
+
+ @wizard_dialog
+ def check_hw_password(self, run_next, title):
+ label = QLabel()
+ message = _('Please provide password related to your Ledger device')
+ label.setText(message)
+ label.setOpenExternalLinks(True)
+ label.setTextInteractionFlags(Qt.TextBrowserInteraction)
+ label.setWordWrap(True)
+
+ layout = CheckHWPasswordDialog(self, message_label=label)
+ self.exec_layout(layout, title, next_enabled=False)
+ return layout.get_password()
+
+ @wizard_dialog
+ def check_hw_passwords(self, run_next, title):
+ label1 = QLabel()
+ message1 = _('Please provide instant password related to your Ledger device')
+ label1.setText(message1)
+ label1.setOpenExternalLinks(True)
+ label1.setTextInteractionFlags(Qt.TextBrowserInteraction)
+ label1.setWordWrap(True)
+
+ layout1 = CheckHWPasswordDialog(self, message_label=label1)
+ self.exec_layout(layout1, title, next_enabled=False)
+ instant_password = layout1.get_password()
+
+ label2 = QLabel()
+ message2 = _('Please provide cancel password related to your Ledger device')
+ label2.setText(message2)
+ label2.setOpenExternalLinks(True)
+ label2.setTextInteractionFlags(Qt.TextBrowserInteraction)
+ label2.setWordWrap(True)
+
+ layout2 = CheckHWPasswordDialog(self, message_label=label2)
+ self.exec_layout(layout2, title, next_enabled=False)
+ recovery_password = layout2.get_password()
+
+ return (instant_password, recovery_password)
+
@wizard_dialog
def display_2fa_pairing_qr(self, run_next, entropy: bytes):
title_label = QLabel()
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index 914aad3fee41..32ded03eb3d1 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -72,7 +72,7 @@
from electrum.version import ELECTRUM_VERSION
from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
sweep_preparations, InternalAddressCorruption,
- ThreeKeysWallet)
+ ThreeKeysWallet, ThreeKeysHWWallet)
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
from .channels_list import ChannelsList
from .confirm_tx_dialog import ConfirmTxDialog
@@ -1541,6 +1541,9 @@ def on_failure(exc_info):
elif external_keypairs:
# can sign directly
task = partial(tx.sign, external_keypairs)
+ elif isinstance(self.wallet, ThreeKeysHWWallet) and self.wallet.is_instant_mode():
+ instant_password = self._get_instant_password()
+ task = partial(self.wallet.sign_instant_transaction, tx, password, None, instant_password)
else:
task = partial(self.wallet.sign_transaction, tx, password)
msg = _('Signing transaction...')
diff --git a/electrum/gui/qt/recovery_list.py b/electrum/gui/qt/recovery_list.py
index e6d588328903..00aa8538d58f 100755
--- a/electrum/gui/qt/recovery_list.py
+++ b/electrum/gui/qt/recovery_list.py
@@ -7,16 +7,17 @@
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QMouseEvent, QValidator, QKeySequence
-from PyQt5.QtWidgets import QPushButton, QLabel, QWidget, QComboBox,\
- QTreeView, QHeaderView, QStyledItemDelegate,\
- QVBoxLayout, QGridLayout,\
- QCompleter, QShortcut
+from PyQt5.QtWidgets import QPushButton, QLabel, QWidget, QComboBox, \
+ QTreeView, QHeaderView, QStyledItemDelegate, \
+ QVBoxLayout, QGridLayout, \
+ QCompleter, QShortcut, QLineEdit
from electrum.i18n import _
from electrum.logging import get_logger
-from electrum.wallet import Abstract_Wallet
+from electrum.wallet import Abstract_Wallet, TwoKeysHWWallet, ThreeKeysHWWallet
from electrum.util import get_request_status, PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum import bitcoin
+from .three_keys_dialogs import MAX_3KEYS_PASSWD_LEN
from .util import read_QIcon, pr_icons, WaitingDialog, filter_non_printable, ColorScheme
from .confirm_tx_dialog import ConfirmTxDialog
@@ -82,6 +83,10 @@ def clear_cache(self):
def onItemChecked(self):
self.selected()
self.tab.update_recovery_button()
+ if (isinstance(self.tab, RecoveryTabAIR) and self.tab.is_hw):
+ self.tab.on_3k_password_line_edit()
+ elif self.tab.is_hw:
+ self.tab.on_recovery_password_line_edit()
def onSelectAll(self):
model = self.model()
@@ -183,6 +188,7 @@ def __init__(self, parent, wallet: Abstract_Wallet, config):
self.config = config
self.wallet = wallet
self.is_2fa = self.wallet.storage.get('multikey_type', '') == '2fa'
+ self.is_hw = self.wallet.storage.get('multikey_type', '') == 'hw'
QWidget.__init__(self)
self.invoice_list = RecoveryView(self.electrum_main_window, self)
@@ -272,12 +278,23 @@ def validate_words(words):
validity_fn(is_valid and len(seed) == 12)
self.update_recovery_button()
+ def on_recovery_password_line_edit(self):
+ password = self._get_recovery_password()
+ is_valid = (len(password) > 0) or len(self.invoice_list.selected()) == 0
+ if is_valid:
+ self.recovery_password_line.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet(True))
+ else:
+ self.recovery_password_line.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
+
def get_recovery_seed(self):
text = self.recovery_privkey_line.text()
words = text.split()
del text
return words
+ def _get_recovery_password(self):
+ return self.recovery_password_line.text()
+
def _get_recovery_keypair(self):
stored_recovery_pubkey = self.wallet.storage.get('recovery_pubkey')
seed = self.get_recovery_seed()
@@ -341,7 +358,12 @@ def on_failure(exc_info):
on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
if self.wallet.is_recovery_mode():
- task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs)
+ if not self.is_hw:
+ task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs)
+ elif isinstance(self.wallet, TwoKeysHWWallet):
+ task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs, self._get_recovery_password())
+ elif isinstance(self.wallet, ThreeKeysHWWallet):
+ task = partial(self.wallet.sign_recovery_transaction, tx, password, external_keypairs, self._get_recovery_password(), self._get_instant_password())
else:
task = partial(self.wallet.sign_transaction, tx, password, external_keypairs)
msg = _('Signing transaction...')
@@ -352,7 +374,7 @@ def recover_action(self):
atxs = self.invoice_list.selected()
address = self.recovery_address_line.currentText()
recovery_keypair = None
- if not self.is_2fa:
+ if not self.is_2fa and not self.is_hw:
recovery_keypair = self._get_recovery_keypair()
if not is_address_valid(address):
@@ -371,9 +393,12 @@ def recover_action(self):
recovery_keypairs=recovery_keypair,
)
- if not self.is_2fa:
+ if not self.is_2fa and not self.is_hw:
self.recovery_privkey_line.setText('')
+ if self.is_hw:
+ self._clean_password_lines()
+
def _create_privkey_line(self, on_edit):
class CompleterDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
@@ -399,6 +424,8 @@ def clear_cache(self):
def update_view(self):
self.invoice_list.update_data()
+ def _clean_password_lines(self):
+ pass
class RecoveryTabAR(RecoveryTab):
@@ -416,11 +443,18 @@ def __init__(self, parent, wallet: Abstract_Wallet, config):
grid_layout.addWidget(self.recovery_address_line, 0, 1)
# Row 2
- if not self.is_2fa:
+ if not self.is_2fa and not self.is_hw:
grid_layout.addWidget(QLabel(_('Cancel seedphrase')), 1, 0)
# complete line edit with suggestions
self.recovery_privkey_line = self._create_privkey_line(self.on_recovery_seed_line_edit)
grid_layout.addWidget(self.recovery_privkey_line, 1, 1)
+ if self.is_hw:
+ grid_layout.addWidget(QLabel(_('Cancel password')), 1, 0)
+ self.recovery_password_line = QLineEdit()
+ self.recovery_password_line.setEchoMode(QLineEdit.Password)
+ self.recovery_password_line.setMaxLength(MAX_3KEYS_PASSWD_LEN)
+ self.recovery_password_line.textChanged.connect(self.on_recovery_password_line_edit)
+ grid_layout.addWidget(self.recovery_password_line, 1, 1)
# Row 3
button = self.recover_button
@@ -434,7 +468,7 @@ def __init__(self, parent, wallet: Abstract_Wallet, config):
self.setLayout(self.main_layout)
def update_recovery_button(self):
- if self.is_2fa:
+ if self.is_2fa or self.is_hw:
enabled = self.is_address_valid and len(self.invoice_list.selected())
else:
enabled = self.is_address_valid \
@@ -447,6 +481,9 @@ def update_recovery_button(self):
else:
self.recover_button.setText('Send cancel transaction')
+ def _clean_password_lines(self):
+ self.recovery_password_line.setText('')
+
class RecoveryTabAIR(RecoveryTab):
@@ -467,19 +504,34 @@ def __init__(self, parent, wallet: Abstract_Wallet, config):
grid_layout.addWidget(self.recovery_address_line, 0, 1)
# Row 2
- if not self.is_2fa:
+ if not self.is_2fa and not self.is_hw:
grid_layout.addWidget(QLabel(_('Secure Fast seedphrase')), 1, 0)
# complete line edit with suggestions
self.instant_privkey_line = self._create_privkey_line(self.on_instant_seed_line_edit)
self.instant_privkey_line.setContextMenuPolicy(Qt.PreventContextMenu)
grid_layout.addWidget(self.instant_privkey_line, 1, 1)
+ elif self.is_hw:
+ grid_layout.addWidget(QLabel(_('Instant password')), 1, 0)
+ self.instant_password_line = QLineEdit()
+ self.instant_password_line.setEchoMode(QLineEdit.Password)
+ self.instant_password_line.setMaxLength(MAX_3KEYS_PASSWD_LEN)
+ self.instant_password_line.textChanged.connect(self.on_3k_password_line_edit)
+ grid_layout.addWidget(self.instant_password_line, 1, 1)
# Row 3
- grid_layout.addWidget(QLabel(_('Cancel seedphrase')), 2, 0)
# complete line edit with suggestions
- self.recovery_privkey_line = self._create_privkey_line(self.on_recovery_seed_line_edit)
- self.recovery_privkey_line.setContextMenuPolicy(Qt.PreventContextMenu)
- grid_layout.addWidget(self.recovery_privkey_line, 2, 1)
+ if self.is_hw:
+ grid_layout.addWidget(QLabel(_('Cancel password')), 2, 0)
+ self.recovery_password_line = QLineEdit()
+ self.recovery_password_line.setEchoMode(QLineEdit.Password)
+ self.recovery_password_line.setMaxLength(MAX_3KEYS_PASSWD_LEN)
+ self.recovery_password_line.textChanged.connect(self.on_3k_password_line_edit)
+ grid_layout.addWidget(self.recovery_password_line, 2, 1)
+ else:
+ grid_layout.addWidget(QLabel(_('Cancel seedphrase')), 2, 0)
+ self.recovery_privkey_line = self._create_privkey_line(self.on_recovery_seed_line_edit)
+ self.recovery_privkey_line.setContextMenuPolicy(Qt.PreventContextMenu)
+ grid_layout.addWidget(self.recovery_privkey_line, 2, 1)
# Row 4
button = self.recover_button
@@ -508,6 +560,10 @@ def _get_instant_keypair(self):
return {pubkey: (privkey, True)}
def recover_action(self):
+ if self.is_hw:
+ RecoveryTab.recover_action(self)
+ return
+ #TODO: design pattern for this(!)
try:
address = self.recovery_address_line.currentText()
instant_keypair = None
@@ -546,6 +602,11 @@ def update_recovery_button(self):
enabled = self.is_address_valid \
and self.is_recovery_seed_valid \
and len(self.invoice_list.selected())
+ elif self.is_hw:
+ enabled = self.is_address_valid \
+ and len(self.invoice_list.selected()) \
+ and len(self.instant_password_line.text()) \
+ and len(self.recovery_password_line.text())
else:
enabled = self.is_address_valid \
and self.is_instant_seed_valid \
@@ -562,3 +623,24 @@ def on_instant_seed_line_edit(self):
return self.on_seed_line_edit(self.instant_privkey_line,
self.get_instant_seed(),
self.set_instant_seed_validity)
+
+ def _get_instant_password(self):
+ return self.instant_password_line.text()
+
+ def set_password_line_style(self, password, password_line):
+ is_valid = (len(password) > 0) or len(self.invoice_list.selected()) == 0
+ if is_valid:
+ password_line.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet(True))
+ else:
+ password_line.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
+
+ def on_3k_password_line_edit(self):
+ password = self._get_recovery_password()
+ self.set_password_line_style(password, self.recovery_password_line)
+ password = self._get_instant_password()
+ self.set_password_line_style(password, self.instant_password_line)
+ self.update_recovery_button()
+
+ def _clean_password_lines(self):
+ self.recovery_password_line.setText('')
+ self.instant_password_line.setText('')
diff --git a/electrum/gui/qt/three_keys_dialogs.py b/electrum/gui/qt/three_keys_dialogs.py
index 3b759dcf986f..c32d8d2a5799 100644
--- a/electrum/gui/qt/three_keys_dialogs.py
+++ b/electrum/gui/qt/three_keys_dialogs.py
@@ -23,6 +23,7 @@ class ValidationState(IntEnum):
INTERMEDIATE = 2
CROPPED = 3
+MAX_3KEYS_PASSWD_LEN = 32
class PubKeyValidator:
COMPRESSED_PREFIXES = ('02', '03')
@@ -138,6 +139,66 @@ def get_compressed_pubkey(self):
return pubkey.get_public_key_hex(compressed=True)
+class InsertHWPasswordDialog(QVBoxLayout):
+ def __init__(self, parent, message_label):
+ super().__init__()
+ self.parent = parent
+ label1 = message_label
+ self.edit1 = QLineEdit()
+ self.edit1.setEchoMode(QLineEdit.Password)
+ self.edit1.setMaxLength(MAX_3KEYS_PASSWD_LEN)
+ label2 = QLabel("Repeat password: ")
+ self.edit2 = QLineEdit()
+ self.edit2.setEchoMode(QLineEdit.Password)
+ self.edit2.setMaxLength(MAX_3KEYS_PASSWD_LEN)
+ self.error_label = ErrorLabel()
+
+ self.edit1.textChanged.connect(self._on_change)
+ self.edit2.textChanged.connect(self._on_change)
+ self.addWidget(label1)
+ self.addWidget(self.edit1)
+ self.addWidget(label2)
+ self.addWidget(self.edit2)
+ self.addWidget(self.error_label)
+
+ def _on_change(self):
+ if(self._get_str(self.edit1) != self._get_str(self.edit2)):
+ self.error_label.setText("Password confirmation does not match")
+ self.parent.next_button.setEnabled(False)
+ else:
+ self.error_label.setText("")
+ self.parent.next_button.setEnabled(self._get_str(self.edit1).strip() != '')
+
+ def _get_str(self, line) -> str:
+ return line.text().replace('\n', '')
+
+ def get_password(self):
+ return self._get_str(self.edit1)
+
+
+class CheckHWPasswordDialog(QVBoxLayout):
+ def __init__(self, parent, message_label):
+ super().__init__()
+ self.parent = parent
+ label = message_label
+ self.edit = QLineEdit()
+ self.edit.setEchoMode(QLineEdit.Password)
+ self.edit.setMaxLength(MAX_3KEYS_PASSWD_LEN)
+
+ self.edit.textChanged.connect(self._on_change)
+ self.addWidget(label)
+ self.addWidget(self.edit)
+
+ def _on_change(self):
+ self.parent.next_button.setEnabled(self._get_str(self.edit).strip() != '')
+
+ def _get_str(self, line) -> str:
+ return line.text().replace('\n', '')
+
+ def get_password(self):
+ return self._get_str(self.edit)
+
+
class Qr2FaDialog(QVBoxLayout):
def __init__(self, parent, title_label: str, pin_label: str, qr_data: dict):
diff --git a/electrum/gui/qt/three_keys_windows.py b/electrum/gui/qt/three_keys_windows.py
index c3a342e6c162..d0558d80a2cb 100644
--- a/electrum/gui/qt/three_keys_windows.py
+++ b/electrum/gui/qt/three_keys_windows.py
@@ -3,7 +3,7 @@
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QWidget, QHBoxLayout, \
QGridLayout, QCompleter, QComboBox, \
- QStyledItemDelegate
+ QStyledItemDelegate, QLineEdit
from electrum.i18n import _
from .amountedit import BTCAmountEdit, MyLineEdit, AmountEdit
@@ -11,7 +11,7 @@
from .confirm_tx_dialog import ConfirmTxDialog
from .main_window import ElectrumWindow
from .recovery_list import RecoveryTabAR, RecoveryTabAIR
-from .three_keys_dialogs import PreviewPsbtTxDialog
+from .three_keys_dialogs import PreviewPsbtTxDialog, MAX_3KEYS_PASSWD_LEN
from .transaction_dialog import PreviewTxDialog
from .util import read_QIcon, HelpLabel, EnterButton, ColorScheme
from ...mnemonic import load_wordlist
@@ -26,6 +26,7 @@ class ElectrumMultikeyWalletWindow(ElectrumWindow):
def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'):
self.is_2fa = wallet.storage.get('multikey_type', '') == '2fa'
+ self.is_hw = wallet.storage.get('multikey_type', '') == 'hw'
super().__init__(gui_object=gui_object, wallet=wallet)
self.recovery_tab = self.create_recovery_tab(wallet, self.config)
self.tabs.addTab(self.recovery_tab, read_QIcon('recovery.png'), _('Cancel'))
@@ -35,7 +36,7 @@ def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'):
self.READY_TO_UPDATE = True
def timer_actions(self):
- # synchronizing the timer thread with end of the __init__ call
+ # synchronizing the timer thread with end of the __init__ call
if self.READY_TO_UPDATE:
super().timer_actions()
@@ -51,8 +52,9 @@ def show_recovery_tab(self):
def update_tabs(self, wallet=None):
super().update_tabs(wallet=wallet)
- self.recovery_tab.update_view()
- self.recovery_tab.update_recovery_button()
+ if hasattr(self, 'recovery_tab'):
+ self.recovery_tab.update_view()
+ self.recovery_tab.update_recovery_button()
class ElectrumARWindow(ElectrumMultikeyWalletWindow):
@@ -305,7 +307,7 @@ def create_send_tab(self):
grid.addWidget(self.max_button, 3, 3)
def on_tx_type(index):
- if not self.is_2fa:
+ if not self.is_2fa and not self.is_hw:
if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure:
self.instant_privkey_line.setEnabled(False)
self.instant_privkey_line.clear()
@@ -313,6 +315,21 @@ def on_tx_type(index):
elif self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure_Fast:
self.instant_privkey_line.setEnabled(True)
self.label_transaction_limitations.hide()
+ elif self.is_hw:
+ if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure:
+ self.instant_password_line.setEnabled(False)
+ self.instant_password_line.clear()
+ self.label_transaction_limitations.show()
+ elif self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure_Fast:
+ self.instant_password_line.setEnabled(True)
+ self.label_transaction_limitations.hide()
+ password = self.instant_password_line.text()
+ is_valid = (len(password) > 0) or not self.instant_password_line.isEnabled()
+ if is_valid:
+ self.instant_password_line.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet(True))
+ else:
+ self.instant_password_line.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
+ self.instant_password_line.textChanged.connect(on_tx_type)
else:
if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure:
description_label.setEnabled(True)
@@ -339,7 +356,7 @@ def on_tx_type(index):
grid.addWidget(tx_type_label, 4, 0)
grid.addWidget(self.tx_type_combo, 4, 1, 1, -1)
- if not self.is_2fa:
+ if not self.is_2fa and not self.is_hw:
instant_privkey_label = HelpLabel(_('Secure Fast Tx seed'), msg)
self.instant_privkey_line = CompletionTextEdit()
self.instant_privkey_line.setTabChangesFocus(False)
@@ -362,6 +379,16 @@ def initStyleOption(self, option, index):
grid.addWidget(instant_privkey_label, 5, 0)
grid.addWidget(self.instant_privkey_line, 5, 1, 1, -1)
+ elif self.is_hw:
+ instant_password_label = HelpLabel(_('Secure Fast Tx password'), msg)
+ self.instant_password_line = QLineEdit()
+ self.instant_password_line.setEchoMode(QLineEdit.Password)
+ self.instant_password_line.setMaxLength(MAX_3KEYS_PASSWD_LEN)
+ self.instant_password_line.setEnabled(False)
+ grid.addWidget(instant_password_label, 5, 0)
+ grid.addWidget(self.instant_password_line, 5, 1, 1, -1)
+
+
self.save_button = EnterButton(_("Save"), self.do_save_invoice)
self.send_button = EnterButton(_("Pay"), self.do_pay)
self.clear_button = EnterButton(_("Clear"), self.do_clear)
@@ -461,7 +488,7 @@ def do_pay_invoice(self, invoice, external_keypairs=None):
self.wallet.set_alert()
if invoice['txtype'] == TxType.INSTANT.name:
try:
- if not self.is_2fa and external_keypairs == None:
+ if not self.is_2fa and not self.is_hw and external_keypairs == None:
external_keypairs = self.get_instant_keypair()
self.wallet.set_instant()
except Exception as e:
@@ -495,7 +522,7 @@ def do_clear(self):
for e in [self.payto_e, self.message_e, self.amount_e]:
e.setText('')
e.setFrozen(False)
- if not self.is_2fa:
+ if not self.is_2fa and not self.is_hw:
self.instant_privkey_line.clear()
self.tx_type_combo.setCurrentIndex(self.TX_TYPES.Secure)
self.update_status()
@@ -543,3 +570,90 @@ def preview_tx_dialog(self, make_tx, outputs, external_keypairs=None, invoice=No
else PreviewTxDialog
d = dialog_class(make_tx, outputs, external_keypairs, window=self, invoice=invoice)
d.show()
+
+
+class ElectrumARHWWindow(ElectrumARWindow):
+
+ def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'):
+ super().__init__(gui_object=gui_object, wallet=wallet)
+
+ def do_pay(self):
+ invoice = self.read_invoice()
+ if not invoice:
+ return
+ invoice['txtype'] = TxType.ALERT_PENDING.name
+ self.wallet.save_invoice(invoice)
+ self.invoice_list.update()
+ self.do_clear()
+ self.do_pay_invoice(invoice)
+
+ def pay_onchain_dialog(self, inputs, outputs, invoice=None, external_keypairs=None):
+ # trustedcoin requires this
+ if run_hook('abort_send', self):
+ return
+ is_sweep = False # Was bool(external_keypairs). Should be good to keep it false, since we do not use trustedcoin
+
+ if invoice['txtype'] == TxType.ALERT_PENDING.name:
+ self.wallet.set_alert()
+ elif invoice['txtype'] == TxType.INSTANT.name:
+ self.wallet.set_recovery()
+ elif invoice['txtype'] == TxType.RECOVERY.name:
+ self.wallet.set_instant()
+
+ make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
+ coins=inputs,
+ outputs=outputs,
+ fee=fee_est,
+ is_sweep=is_sweep)
+ if self.config.get('advanced_preview'):
+ self.preview_tx_dialog(make_tx, outputs, external_keypairs=external_keypairs, invoice=invoice)
+ return
+
+ output_values = [x.value for x in outputs]
+ output_value = '!' if '!' in output_values else sum(output_values)
+ d = ConfirmTxDialog(self, make_tx, output_value, is_sweep)
+ d.update_tx()
+ if d.not_enough_funds:
+ self.show_message(_('Not Enough Funds'))
+ return
+ cancelled, is_send, password, tx = d.run()
+ if cancelled:
+ return
+
+ if is_send:
+ def sign_done(success):
+ if success:
+ self.wallet.multisig_script_generator.set_alert()
+ self.broadcast_or_show(tx, invoice=invoice)
+ self.sign_tx_with_password(tx, sign_done, password, external_keypairs)
+ else:
+ self.preview_tx_dialog(make_tx, outputs, external_keypairs=external_keypairs, invoice=invoice)
+
+
+class ElectrumAIRHWWindow(ElectrumAIRWindow):
+ def __init__(self, gui_object: 'ElectrumGui', wallet: 'Abstract_Wallet'):
+ super().__init__(gui_object=gui_object, wallet=wallet)
+
+ def do_pay(self):
+ invoice = self.read_invoice()
+ if not invoice:
+ return
+
+ try:
+ if self.tx_type_combo.currentIndex() == self.TX_TYPES.Secure_Fast:
+ invoice['txtype'] = TxType.INSTANT.name
+ self.instant_password_line.text()
+ self.wallet.set_instant()
+ else:
+ invoice['txtype'] = TxType.ALERT_PENDING.name
+ self.wallet.set_alert()
+ except Exception as e:
+ self.on_error([0, str(e)])
+ return
+
+ self.wallet.save_invoice(invoice)
+ self.invoice_list.update()
+ self.do_pay_invoice(invoice)
+
+ def _get_instant_password(self):
+ return self.instant_password_line.text()
diff --git a/electrum/keystore.py b/electrum/keystore.py
index 859e74989f8d..2989c944d674 100644
--- a/electrum/keystore.py
+++ b/electrum/keystore.py
@@ -745,6 +745,9 @@ def opportunistically_fill_in_missing_info_from_device(self, client: 'HardwareCl
self.label = client.label()
self.is_requesting_to_be_rewritten_to_wallet_file = True
+ def set_btcv_password_use(self, tx_type, password=None):
+ raise NotImplementedError()
+
def bip39_normalize_passphrase(passphrase):
return normalize('NFKD', passphrase or '')
diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
index ee68dd7945c7..f61c0c660ed4 100644
--- a/electrum/plugins/hw_wallet/plugin.py
+++ b/electrum/plugins/hw_wallet/plugin.py
@@ -32,6 +32,7 @@
from electrum.util import bfh, versiontuple, UserFacingException
from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
from electrum.bip32 import BIP32Node
+from electrum.three_keys.pubkey_type import PubkeyType
if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet
@@ -167,6 +168,12 @@ def can_recognize_device(self, device: Device) -> bool:
"""
return device.product_key in self.DEVICE_IDS
+ def set_instant_password(self, device_id, password: str, wizard) -> bool:
+ raise NotImplementedError()
+
+ def set_recovery_password(self, device_id, password: str, wizard) -> bool:
+ raise NotImplementedError()
+
class HardwareClientBase:
@@ -195,7 +202,7 @@ def label(self) -> str:
def has_usable_connection_with_device(self) -> bool:
raise NotImplementedError()
- def get_xpub(self, bip32_path: str, xtype) -> str:
+ def get_xpub(self, bip32_path: str, xtype, pubkey_type=PubkeyType.PUBKEY_ALERT) -> str:
raise NotImplementedError()
diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
index 9b9d0baf55ac..fbbe91946ae7 100644
--- a/electrum/plugins/ledger/ledger.py
+++ b/electrum/plugins/ledger/ledger.py
@@ -1,8 +1,11 @@
+import enum
from struct import pack, unpack
import hashlib
import sys
import traceback
+from PyQt5.QtWidgets import QMessageBox
+
from electrum import ecc
from electrum import bip32
from electrum.crypto import hash_160
@@ -16,6 +19,7 @@
from electrum.base_wizard import ScriptTypeNotSupported
from electrum.logging import get_logger
from electrum.plugin import Device
+from electrum.three_keys.pubkey_type import PubkeyType
from typing import Tuple, Optional
from ..hw_wallet import HW_PluginBase, HardwareClientBase
@@ -62,6 +66,12 @@ def catch_exception(self, *args, **kwargs):
return catch_exception
+class LedgerBtcvTxType(enum.IntEnum):
+ ALERT = 0x00
+ INSTANT = 0x01
+ RECOVERY = 0x02
+
+
class Ledger_Client(HardwareClientBase):
def __init__(self, hidDevice):
self.dongleObject = btchip(hidDevice)
@@ -90,7 +100,7 @@ def has_usable_connection_with_device(self):
return True
@test_pin_unlocked
- def get_xpub(self, bip32_path, xtype):
+ def get_xpub(self, bip32_path, xtype, pubkey_type=PubkeyType.PUBKEY_ALERT):
self.checkDevice()
# bip32_path is of the form 44'/0'/1'
# S-L-O-W - we don't handle the fingerprint directly, so compute
@@ -107,14 +117,17 @@ def get_xpub(self, bip32_path, xtype):
bip32_path = bip32_path[2:] # cut off "m/"
if len(bip32_intpath) >= 1:
prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:]
- nodeData = self.dongleObject.getWalletPublicKey(prevPath)
+ nodeData = self.dongleObject.getWalletPublicKey(prevPath, btcvAddr=False)
publicKey = compress_public_key(nodeData['publicKey'])
fingerprint_bytes = hash_160(publicKey)[0:4]
childnum_bytes = bip32_intpath[-1].to_bytes(length=4, byteorder="big")
else:
fingerprint_bytes = bytes(4)
childnum_bytes = bytes(4)
- nodeData = self.dongleObject.getWalletPublicKey(bip32_path)
+ if(pubkey_type == LedgerBtcvTxType.ALERT):
+ nodeData = self.dongleObject.getWalletPublicKey(bip32_path, btcvAddr=False)
+ else:
+ nodeData = self.dongleObject.getWalletPublicKey(bip32_path, btcvPubkeyTree=pubkey_type)
publicKey = compress_public_key(nodeData['publicKey'])
depth = len(bip32_intpath)
return BIP32Node(xtype=xtype,
@@ -124,6 +137,14 @@ def get_xpub(self, bip32_path, xtype):
fingerprint=fingerprint_bytes,
child_number=childnum_bytes).to_xpub()
+ @test_pin_unlocked
+ def set_instant_password(self, password):
+ self.dongleObject.setBTCVPassword(password, btchip.BTCV_PASSWORD_INSTANT)
+
+ @test_pin_unlocked
+ def set_recovery_password(self, password):
+ self.dongleObject.setBTCVPassword(password, btchip.BTCV_PASSWORD_RECOVERY)
+
def has_detached_pin_support(self, client):
try:
client.getVerifyPinRemainingAttempts()
@@ -307,11 +328,49 @@ def sign_message(self, sequence, message, password):
# And convert it
return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s
+ @test_pin_unlocked
+ def set_btcv_password_use(self, tx_type=LedgerBtcvTxType.ALERT, password=None, wizard=None):
+ if self.handler == None:
+ self.handler = self.plugin.create_handler(wizard)
+ client = self.get_client()
+ password_hash = bytearray(32)
+ if password:
+ m = hashlib.sha256()
+ m.update(bytearray.fromhex(bytearray(password.encode('utf-8')).hex().ljust(64, '0')))
+ password_hash = m.digest()
+ try:
+ client.setBTCVPasswordUse(password_hash, tx_type)
+ except BTChipException as e:
+ if e.sw == 0x6a80:
+ raise BTChipException(e.message + "\n(Password may be incorrect)")
+
+ def are_3keys_ledger_passwords_correct(self, wizard, btcv_instant_password_check=None, btcv_recovery_password_check=None):
+ try:
+ if btcv_recovery_password_check:
+ self.set_btcv_password_use(tx_type=LedgerBtcvTxType.RECOVERY, password=btcv_recovery_password_check, wizard=wizard)
+ if btcv_instant_password_check:
+ self.set_btcv_password_use(tx_type=LedgerBtcvTxType.INSTANT, password=btcv_instant_password_check, wizard=wizard)
+ except BTChipException as e:
+ msg = _('Hardware wallet import fail') + "\n" + str(e)
+ if e.sw == 0x6a80:
+ msg = msg + "\n" + ('Invalid password(s)')
+ if wizard:
+ wizard.show_error(msg)
+ return False
+ finally:
+ self.set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT, wizard=self)
+ return True
+
@test_pin_unlocked
@set_and_unset_signing
- def sign_transaction(self, tx, password):
+ def sign_transaction(self, tx, password, pubkey_index=None, recovery_password=None, instant_password=False):
+
if tx.is_complete():
return
+ if recovery_password and not self.are_3keys_ledger_passwords_correct(None, btcv_recovery_password_check=recovery_password):
+ raise RuntimeError("Invalid recovery password provided")
+ if instant_password and not self.are_3keys_ledger_passwords_correct(None, btcv_instant_password_check=instant_password):
+ raise RuntimeError("Invalid instant password provided")
inputs = []
inputsPaths = []
chipInputs = []
@@ -341,7 +400,11 @@ def sign_transaction(self, tx, password):
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True
- my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin)
+ if pubkey_index:
+ my_pubkey, = txin.bip32_paths.keys()[pubkey_index]
+ full_path = self.get_pubkey_derivation(my_pubkey, txin, only_der_suffix=False)
+ else:
+ my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin)
if not full_path:
self.give_error("No matching pubkey for sign_transaction") # should never happen
full_path = convert_bip32_intpath_to_strpath(full_path)[2:]
@@ -444,7 +507,7 @@ def sign_transaction(self, tx, password):
while inputIndex < len(inputs):
singleInput = [ chipInputs[inputIndex] ]
self.get_client().startUntrustedTransaction(False, 0,
- singleInput, redeemScripts[inputIndex], version=tx.version)
+ singleInput, redeemScripts[inputIndex], version=tx.version)
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
inputSignature[0] = 0x30 # force for 1.4.9+
my_pubkey = inputs[inputIndex][4]
@@ -452,6 +515,20 @@ def sign_transaction(self, tx, password):
signing_pubkey=my_pubkey.hex(),
sig=inputSignature.hex())
inputIndex = inputIndex + 1
+
+ if recovery_password:
+ three_keys_pubkey_index = 1
+ if instant_password:
+ three_keys_pubkey_index = 2
+ self.add_3k_signatures_to_tx(changePath, chipInputs, inputs, inputsPaths,
+ LedgerBtcvTxType.RECOVERY, output, pin, rawTx, recovery_password,
+ redeemScripts, tx, txOutput, three_keys_pubkey_index)
+ if instant_password:
+ three_keys_pubkey_index = 1
+ self.add_3k_signatures_to_tx(changePath, chipInputs, inputs, inputsPaths,
+ LedgerBtcvTxType.INSTANT, output, pin, rawTx, instant_password,
+ redeemScripts, tx, txOutput, three_keys_pubkey_index)
+
else:
while inputIndex < len(inputs):
self.get_client().startUntrustedTransaction(firstTransaction, inputIndex,
@@ -494,6 +571,53 @@ def sign_transaction(self, tx, password):
finally:
self.handler.finished()
+ def add_3k_signatures_to_tx(self, changePath, chipInputs, inputs, inputsPaths, tx_type, output,
+ pin, rawTx, three_keys_password, redeemScripts, tx, txOutput, three_keys_pubkey_index):
+ inputIndex = 0
+ self.get_client().startUntrustedTransaction(True, inputIndex,
+ chipInputs, redeemScripts[inputIndex], version=tx.version)
+ # we don't set meaningful outputAddress, amount and fees
+ # as we only care about the alternateEncoding==True branch
+ outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx))
+ outputData['outputData'] = txOutput
+ if outputData['confirmationNeeded']:
+ outputData['address'] = output
+ self.handler.finished()
+ pin = self.handler.get_auth(outputData) # does the authenticate dialog and returns pin
+ if not pin:
+ raise UserWarning()
+ self.handler.show_message(_("Confirmed. Signing Transaction..."))
+ while inputIndex < len(inputs):
+ singleInput = [chipInputs[inputIndex]]
+ self.get_client().startUntrustedTransaction(False, 0,
+ singleInput, redeemScripts[inputIndex], version=tx.version)
+ self.set_btcv_password_use(tx_type=tx_type, password=three_keys_password)
+
+ inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
+ inputSignature[0] = 0x30 # force for 1.4.9+
+
+ my_pubkey = list(tx.inputs()[inputIndex].bip32_paths.keys())[three_keys_pubkey_index]
+
+ tx.add_signature_to_txin(txin_idx=inputIndex,
+ signing_pubkey=my_pubkey.hex(),
+ sig=inputSignature.hex())
+ inputIndex += 1
+
+ @test_pin_unlocked
+ @set_and_unset_signing
+ def get_address(self, sequence, txin_type, btcvAddr=False):
+ client = self.get_client()
+ address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence
+ segwit = is_segwit_script_type(txin_type)
+ segwitNative = txin_type == 'p2wpkh'
+
+ try:
+ result = client.getWalletPublicKey(address_path, showOnScreen=False, segwit=segwit, segwitNative=segwitNative, btcvAddr=btcvAddr)
+ return result['address'].decode("utf-8")
+ except Exception as e:
+ self.logger.exception(e)
+
+
@test_pin_unlocked
@set_and_unset_signing
def show_address(self, sequence, txin_type):
@@ -593,14 +717,36 @@ def setup_device(self, device_info, wizard, purpose):
client.handler = self.create_handler(wizard)
client.get_xpub("m/44'/0'", 'standard') # TODO replace by direct derivation once Nano S > 1.1
- def get_xpub(self, device_id, derivation, xtype, wizard):
+ def set_recovery_password(self, device_id, password, wizard):
+ devmgr = self.device_manager()
+ client = devmgr.client_by_id(device_id)
+ client.handler = self.create_handler(wizard)
+ client.checkDevice()
+ try:
+ client.set_recovery_password(password)
+ return True
+ except:
+ return False
+
+ def set_instant_password(self, device_id, password, wizard):
+ devmgr = self.device_manager()
+ client = devmgr.client_by_id(device_id)
+ client.handler = self.create_handler(wizard)
+ client.checkDevice()
+ try:
+ client.set_instant_password(password)
+ return True
+ except:
+ return False
+
+ def get_xpub(self, device_id, derivation, xtype, wizard, pubkey_type=PubkeyType.PUBKEY_ALERT):
if xtype not in self.SUPPORTED_XTYPES:
raise ScriptTypeNotSupported(_('This type of script is not supported with {device}.').format(device=self.device))
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
client.handler = self.create_handler(wizard)
client.checkDevice()
- xpub = client.get_xpub(derivation, xtype)
+ xpub = client.get_xpub(derivation, xtype, pubkey_type)
return xpub
def get_client(self, keystore, force_pair=True):
diff --git a/electrum/three_keys/pubkey_type.py b/electrum/three_keys/pubkey_type.py
new file mode 100644
index 000000000000..0bf96854b94a
--- /dev/null
+++ b/electrum/three_keys/pubkey_type.py
@@ -0,0 +1,6 @@
+from enum import IntEnum
+
+class PubkeyType(IntEnum):
+ PUBKEY_ALERT = 0
+ PUBKEY_INSTANT = 1
+ PUBKEY_RECOVERY = 2
diff --git a/electrum/three_keys/script.py b/electrum/three_keys/script.py
index d7436d5b0a36..1684d80a1ef5 100644
--- a/electrum/three_keys/script.py
+++ b/electrum/three_keys/script.py
@@ -14,39 +14,45 @@ def __init__(self, recovery_pubkey: str):
self._recovery_alert_flag = None
self.witness_flags = []
- def get_redeem_script(self, public_keys: List[str]) -> str:
- if not isinstance(public_keys, list) or len(public_keys) not in [1, 2]:
- raise ThreeKeysError(f"Wrong input type! Expected 1 or 2 elements list not '{public_keys}'")
- # filter out recovery pubkey
- filtered_keys = list(filter(lambda item: item != self.recovery_pubkey, public_keys))
- if len(filtered_keys) != 1:
- raise ThreeKeysError(f'Cannot deduce pubkey from {public_keys}')
-
- pub_key = filtered_keys[0]
+ @staticmethod
+ def create_redeem_script(alert_pubkey, recovery_pubkey):
return (
opcodes.OP_IF.hex() +
opcodes.OP_1.hex() +
opcodes.OP_ELSE.hex() +
opcodes.OP_2.hex() +
opcodes.OP_ENDIF.hex() +
-
- push_script(pub_key) +
- push_script(self.recovery_pubkey) +
-
+ push_script(alert_pubkey) +
+ push_script(recovery_pubkey) +
opcodes.OP_2.hex() +
opcodes.OP_CHECKMULTISIG.hex()
)
+ @staticmethod
+ def _create_script_sig(signatures, flag, redeem_script):
+ return (
+ opcodes.OP_0.hex() +
+ signatures +
+ flag +
+ push_script(redeem_script)
+ )
+
+ def get_redeem_script(self, public_keys: List[str]) -> str:
+ if not isinstance(public_keys, list) or len(public_keys) not in [1, 2]:
+ raise ThreeKeysError(f"Wrong input type! Expected 1 or 2 elements list not '{public_keys}'")
+ # filter out recovery pubkey
+ filtered_keys = list(filter(lambda item: item != self.recovery_pubkey, public_keys))
+ if len(filtered_keys) != 1:
+ raise ThreeKeysError(f'Cannot deduce pubkey from {public_keys}')
+
+ pub_key = filtered_keys[0]
+ return self.create_redeem_script(pub_key, self.recovery_pubkey)
+
def get_script_sig(self, signatures: List[str], public_keys: List[str]) -> str:
if self._recovery_alert_flag is None:
raise ThreeKeysError('Recovery/alert flag not set!')
sigs = ''.join(push_script(sig) for sig in signatures)
- return (
- opcodes.OP_0.hex() +
- sigs +
- self._recovery_alert_flag +
- push_script(self.get_redeem_script(public_keys))
- )
+ return self._create_script_sig(sigs, self._recovery_alert_flag, self.get_redeem_script(public_keys))
def set_alert(self):
# 1 of 2
@@ -72,11 +78,8 @@ def __init__(self, recovery_pubkey: str, instant_pubkey: str):
self._instant_recovery_alert_flag = None
self.witness_flags = []
- def get_redeem_script(self, public_keys: List[str]) -> str:
- if not isinstance(public_keys, list) or len(public_keys) > 3:
- raise ThreeKeysError(f"Wrong input type! Expected list not '{public_keys}'")
-
- pub_key = public_keys[0]
+ @staticmethod
+ def create_redeem_script(alert_pubkey, instant_pubkey, recovery_pubkey):
return (
opcodes.OP_IF.hex() +
opcodes.OP_1.hex() +
@@ -87,25 +90,34 @@ def get_redeem_script(self, public_keys: List[str]) -> str:
opcodes.OP_3.hex() +
opcodes.OP_ENDIF.hex() +
opcodes.OP_ENDIF.hex() +
-
- push_script(pub_key) +
- push_script(self.instant_pubkey) +
- push_script(self.recovery_pubkey) +
-
+ push_script(alert_pubkey) +
+ push_script(instant_pubkey) +
+ push_script(recovery_pubkey) +
opcodes.OP_3.hex() +
opcodes.OP_CHECKMULTISIG.hex()
)
+ @staticmethod
+ def _create_script_sig(signatures, flags, redeem_script):
+ return (
+ opcodes.OP_0.hex() +
+ signatures +
+ flags +
+ push_script(redeem_script)
+ )
+
+ def get_redeem_script(self, public_keys: List[str]) -> str:
+ if not isinstance(public_keys, list) or len(public_keys) > 3:
+ raise ThreeKeysError(f"Wrong input type! Expected list not '{public_keys}'")
+
+ pub_key = public_keys[0]
+ return self.create_redeem_script(pub_key, self.instant_pubkey, self.recovery_pubkey)
+
def get_script_sig(self, signatures: List[str], public_keys: List[str]) -> str:
if self._instant_recovery_alert_flag is None:
raise ThreeKeysError('Recovery/alert/instant flag not set!')
sigs = ''.join(push_script(sig) for sig in signatures)
- return (
- opcodes.OP_0.hex() +
- sigs +
- self._instant_recovery_alert_flag +
- push_script(self.get_redeem_script(public_keys))
- )
+ return self._create_script_sig(sigs, self._instant_recovery_alert_flag, self.get_redeem_script(public_keys))
def set_alert(self):
# 1 of 3
@@ -130,3 +142,28 @@ def is_instant_mode(self):
def is_alert_mode(self):
return self.witness_flags == [1]
+
+
+class TwoKeysHWScriptGenerator(TwoKeysScriptGenerator):
+ def __init__(self, recovery_pubkey: str):
+ self._recovery_alert_flag = None
+ self.recovery_pubkey = recovery_pubkey
+ self.witness_flags = []
+
+ def get_redeem_script(self, public_keys: List[str]) -> str:
+ if not isinstance(public_keys, list) or len(public_keys) != 2:
+ raise ThreeKeysError(f"Wrong input type! Expected 2 elements list not '{public_keys}'")
+ return self.create_redeem_script(public_keys[0], public_keys[1])
+
+
+class ThreeKeysHWScriptGenerator(ThreeKeysScriptGenerator):
+ def __init__(self, instant_pubkey: str, recovery_pubkey: str):
+ self._instant_recovery_alert_flag = None
+ self.instant_pubkey = instant_pubkey
+ self.recovery_pubkey = recovery_pubkey
+ self.witness_flags = []
+
+ def get_redeem_script(self, public_keys: List[str]) -> str:
+ if not isinstance(public_keys, list) or len(public_keys) != 3:
+ raise ThreeKeysError(f"Wrong input type! Expected 3 elements list not '{public_keys}'")
+ return self.create_redeem_script(public_keys[0], public_keys[1], public_keys[2])
diff --git a/electrum/transaction.py b/electrum/transaction.py
index 3a0caeb6ae2a..3616ca9b04dc 100644
--- a/electrum/transaction.py
+++ b/electrum/transaction.py
@@ -521,6 +521,9 @@ def update_inputs(self):
for input in self._inputs:
input.multisig_script_generator = self.multisig_script_generator
+ def update_input_multisig_generator(self, input, multisig_script_generator):
+ input.multisig_script_generator = multisig_script_generator
+
def to_json(self) -> dict:
d = {
'version': self.version,
diff --git a/electrum/wallet.py b/electrum/wallet.py
index ab6fee7c7dcb..aebe7accf01b 100755
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -59,7 +59,8 @@
from .plugin import run_hook
from .simple_config import SimpleConfig
from .storage import StorageEncryptionVersion, WalletStorage
-from .three_keys.script import TwoKeysScriptGenerator, ThreeKeysScriptGenerator
+from .three_keys.script import TwoKeysScriptGenerator, ThreeKeysScriptGenerator, ThreeKeysError, \
+ TwoKeysHWScriptGenerator, ThreeKeysHWScriptGenerator
from .three_keys.transaction import TxType, ThreeKeysTransaction
from .three_keys.utils import filter_spendable_coins, update_tx_status
from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
@@ -2078,14 +2079,16 @@ def get_public_keys_with_deriv_info(self, address: str):
return {k.derive_pubkey(*der_suffix): (k, der_suffix)
for k in self.get_keystores()}
- def _add_input_sig_info(self, txin, address, *, only_der_suffix=True):
- self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
+ def _add_input_sig_info(self, txin, address, *, only_der_suffix=True, sort_pubkeys=True):
+ self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix, sort_pubkeys=sort_pubkeys)
- def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix=True):
+ def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix=True, sort_pubkeys=True):
if not self.is_mine(address):
return
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
- txinout.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)])
+ txinout.pubkeys = [bfh(pk) for pk in list(pubkey_deriv_info)]
+ if sort_pubkeys:
+ txinout.pubkeys = sorted(txinout.pubkeys)
for pubkey_hex in pubkey_deriv_info:
ks, der_suffix = pubkey_deriv_info[pubkey_hex]
fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix,
@@ -2306,6 +2309,7 @@ def __init__(self, storage: WalletStorage, *, config: SimpleConfig, scriptGenera
self.multikey_type = storage.get('multikey_type')
self.multisig_script_generator = scriptGenerator
self.set_alert()
+
# super has to be at the end otherwise wallet breaks
super().__init__(storage=storage, config=config)
self.multiple_change = storage.get('multiple_change', True)
@@ -2341,7 +2345,7 @@ def derive_txin_type_from_keystore(keystore):
if txin_type == 'standard':
return 'p2sh'
- if 'p2wpkh' in txin_type:
+ if 'p2wpkh' in txin_type or 'p2wsh-p2sh' == txin_type:
return 'p2wsh-p2sh'
raise UnknownTxinType(f'Cannot derive txin_type from {txin_type}')
@@ -2508,7 +2512,7 @@ def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_k
tx.finalize_psbt()
return tx
-
+
def get_wallet_label(self):
return '2-Key Vault'
@@ -2574,6 +2578,296 @@ def get_wallet_label(self):
return '3-Key Vault'
+class MultikeyHWWallet(Multisig_Wallet):
+
+ def __init__(self, storage: WalletStorage, *, config: SimpleConfig, scriptGenerator: MultiKeyScriptGenerator):
+ self.wallet_type = storage.get('wallet_type')
+ self.multikey_type = storage.get('multikey_type')
+ self.multisig_script_generator = scriptGenerator
+ if isinstance(self.multisig_script_generator, ThreeKeysHWScriptGenerator):
+ self.n = 3
+ else:
+ self.n = 2
+ self.m = 1
+ Deterministic_Wallet.__init__(self, storage, config=config)
+ # super has to be at the end otherwise wallet breaks
+ self.multiple_change = storage.get('multiple_change', False)
+
+ def load_keystore(self):
+ self.keystores = {}
+ for i in range(self.n):
+ name = 'x%d/'%(i+1)
+ self.keystores[name] = load_keystore(self.storage, name)
+ self.keystore = self.keystores['x1/']
+ self.txin_type = 'p2wsh-p2sh'
+
+ def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
+ outputs: List[PartialTxOutput], fee=None,
+ change_addr: str = None, is_sweep=False) -> PartialTransaction:
+ self.update_tx_input_multisig_generator(coins)
+ tx = super().make_unsigned_transaction(
+ coins=coins,
+ outputs=outputs,
+ fee=fee,
+ change_addr=change_addr,
+ is_sweep=is_sweep,
+ )
+ self.update_transaction_multisig_generator(tx)
+ return tx
+
+ def update_transaction_multisig_generator(self, tx: Transaction):
+ tx.multisig_script_generator = self.multisig_script_generator
+ tx.update_inputs()
+
+ def update_tx_input_multisig_generator(self, inputs: Sequence[PartialTxInput]):
+ for txin in inputs:
+ txin.multisig_script_generator = self.multisig_script_generator
+
+ def get_atxs_to_recovery(self):
+ txi_list = self.db.list_txi()
+ recovery_mempool_transactions = {
+ history_item.txid: self.db.get_transaction(history_item.txid)
+ for history_item in self.get_history() if history_item.tx_mined_status.conf == 0 and history_item.tx_mined_status.txtype == TxType.RECOVERY.name
+ }
+ with self.transaction_lock:
+ conflicting_alert_inputs = set([
+ txin.prevout.txid.hex()
+ for tx in recovery_mempool_transactions.values() for txin in tx.inputs()
+ ])
+ for tx_hash, tx in self.db.transactions.items():
+ mined_info = self.get_tx_height(tx_hash)
+ # skip incoming alerts, mempool alerts and alerts conflicted with recovery mempool
+ if tx.tx_type == TxType.ALERT_PENDING and mined_info.conf > 0 and tx_hash in txi_list:
+ if not set([txin.prevout.txid.hex() for txin in tx.inputs()]).issubset(conflicting_alert_inputs):
+ yield tx
+
+ def get_inputs_and_output_for_recovery(self, alert_transactions: ThreeKeysTransaction, destination_address: str):
+ inputs = [PartialTxInput.from_txin(txin) for atx in alert_transactions for txin in atx.inputs()]
+ scriptpubkey = bfh(bitcoin.address_to_script(destination_address))
+ # ! sign sets max value to output
+ output = PartialTxOutput(scriptpubkey=scriptpubkey, value='!')
+ return inputs, output
+
+ def prepare_inputs_for_recovery(self, inputs: list):
+ """Methods for modification tx inputs coming from alert transaction to work with recovery tx.
+ Method adds missing address, satoshi and height value from db storage"""
+ updated_inputs = copy.deepcopy(inputs)
+ # cache for not doubling fetching data for repeating address
+ db_address_satoshi_height_cache = {}
+ for input in updated_inputs:
+ tx_hash = input.prevout.txid.hex()
+ prevout_index = input.prevout.out_idx
+ key = (tx_hash, prevout_index)
+ if key not in db_address_satoshi_height_cache:
+ fetched_data = self.db.get_address_satoshi_height_for_tx(tx_hash)
+ db_address_satoshi_height_cache.update(fetched_data)
+ fetched_data = fetched_data[key]
+ else:
+ fetched_data = db_address_satoshi_height_cache[key]
+
+ input._trusted_address = fetched_data['address']
+ input._trusted_value_sats = fetched_data['satoshi']
+ input.block_height = fetched_data['height']
+ return updated_inputs
+
+ def set_alert(self):
+ from .plugins.ledger.ledger import LedgerBtcvTxType
+ self.multisig_script_generator.set_alert()
+ self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT)
+
+ def set_recovery(self):
+ self.multisig_script_generator.set_recovery()
+
+ def set_instant(self):
+ self.multisig_script_generator.set_instant()
+
+ def sign_transaction(self, tx: Transaction, password, update_pubkeys_fn=None, recovery_password=None, instant_password=None) -> Optional[PartialTransaction]:
+ if self.is_watching_only():
+ return
+ if not isinstance(tx, PartialTransaction):
+ return
+ tmp_tx = copy.deepcopy(tx)
+ for input in tmp_tx.inputs():
+ tmp_pubkeys = self.get_public_keys_with_deriv_info(input.address)
+ self.multisig_script_generator.recovery_pubkey = list(tmp_pubkeys)[1]
+ if isinstance(self.multisig_script_generator, ThreeKeysHWScriptGenerator):
+ self.multisig_script_generator.instant_pubkey = list(tmp_pubkeys)[2]
+ tmp_tx.multisig_script_generator = self.multisig_script_generator
+ tmp_tx.update_input_multisig_generator(input, copy.deepcopy(self.multisig_script_generator))
+
+ tmp_tx.add_info_from_wallet(self, include_xpubs_and_full_paths=True)
+ if update_pubkeys_fn:
+ update_pubkeys_fn(tmp_tx)
+
+ for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
+ try:
+ if k.can_sign(tmp_tx):
+ from electrum.plugins.ledger.ledger import Ledger_KeyStore
+ if isinstance(k, Ledger_KeyStore):
+ from electrum.plugins.ledger.ledger import LedgerBtcvTxType
+ self._get_hw_keystore().set_btcv_password_use(tx_type=LedgerBtcvTxType.ALERT)
+
+ k.sign_transaction(tmp_tx, password, None, recovery_password, instant_password)
+
+ else:
+ raise NotImplementedError
+ except UserCancelled:
+ continue
+
+ tmp_tx.remove_xpubs_and_bip32_paths()
+ tx.combine_with_other_psbt(tmp_tx, True)
+ tx.add_info_from_wallet(self, include_xpubs_and_full_paths=False)
+ return tx
+
+ def get_coin_chooser(self):
+ if self.multisig_script_generator.is_alert_mode():
+ return coinchooser.get_coin_chooser_alert(self.config)
+ else:
+ return coinchooser.get_coin_chooser(self.config)
+
+ def add_input_info(self, txin: PartialTxInput, *, only_der_suffix: bool = True) -> None:
+ address = self.get_txin_address(txin)
+ if not self.is_mine(address):
+ is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
+ if not is_mine:
+ return
+ # set script_type first, as later checks might rely on it:
+ txin.script_type = self.get_txin_type(address)
+ self._add_input_utxo_info(txin, address)
+ txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1
+ if txin.redeem_script is None:
+ try:
+ redeem_script_hex = self.get_redeem_script(address)
+ txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
+ except UnknownTxinType:
+ pass
+ if txin.witness_script is None:
+ try:
+ witness_script_hex = self.get_witness_script(address)
+ txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None
+ except UnknownTxinType:
+ pass
+ self._add_input_sig_info(txin, address, only_der_suffix=only_der_suffix, sort_pubkeys=False)
+
+ def _get_hw_keystore(self):
+ for k in self.get_keystores():
+ if isinstance(k, Hardware_KeyStore):
+ return k
+ raise AssertionError("Hardware keystore not found")
+
+ def is_recovery_mode(self):
+ return self.multisig_script_generator.is_recovery_mode()
+
+class TwoKeysHWWallet(MultikeyHWWallet):
+
+ def __init__(self, storage: WalletStorage, *, config: SimpleConfig):
+ script_generator = TwoKeysHWScriptGenerator(recovery_pubkey=storage.get('recovery_pubkey'))
+ super().__init__(storage, config=config, scriptGenerator=script_generator)
+
+ def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str:
+ if not isinstance(pubkeys, list) or len(pubkeys) != 2:
+ raise ThreeKeysError(f"Wrong input type! Expected list not '{pubkeys}'")
+
+ return TwoKeysScriptGenerator.create_redeem_script(pubkeys[0], pubkeys[1])
+
+ def _add_recovery_pubkey_to_transaction(self, tx):
+ for input in tx.inputs():
+ recovery_pubkey = bytes.fromhex(input.multisig_script_generator.recovery_pubkey)
+ if recovery_pubkey not in input.pubkeys:
+ input.pubkeys.append(recovery_pubkey)
+ input.num_sig = 2
+ assert len(input.pubkeys) == 2, 'Wrong number of pubkeys for performing recovery tx'
+ return tx
+
+ def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs, recovery_password=None)-> Optional[
+ PartialTransaction]:
+
+ if not isinstance(tx, PartialTransaction):
+ return
+
+ tx = self.sign_transaction(tx, password, self._add_recovery_pubkey_to_transaction, recovery_password)
+
+ if not tx.is_complete():
+ _logger.error(f'Recovery transaction not completed')
+ tx.finalize_psbt()
+
+ return tx
+
+ def get_wallet_label(self):
+ return '2-Key Vault'
+
+
+class ThreeKeysHWWallet(MultikeyHWWallet):
+
+ def __init__(self, storage: WalletStorage, *, config: SimpleConfig):
+
+ script_generator = ThreeKeysHWScriptGenerator(instant_pubkey=storage.get('instant_pubkey'),
+ recovery_pubkey=storage.get('recovery_pubkey'))
+ super().__init__(storage, config=config, scriptGenerator=script_generator)
+ self.n = 3
+ self.m = 1
+
+ def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str:
+ if not isinstance(pubkeys, list) or len(pubkeys) != 3:
+ raise ThreeKeysError(f"Wrong input type! Expected list not '{pubkeys}'")
+
+ return ThreeKeysScriptGenerator.create_redeem_script(pubkeys[0], pubkeys[1], pubkeys[2])
+
+ def sign_instant_transaction(self, tx: PartialTransaction, password, instant_keypairs, instant_password) -> Optional[PartialTransaction]:
+ if not isinstance(tx, PartialTransaction):
+ return
+
+ tx = self.sign_transaction(tx, password, self._add_instant_pubkey_to_transaction, None, instant_password)
+
+ if not tx.is_complete():
+ _logger.error(f'Recovery transaction not completed')
+ tx.finalize_psbt()
+
+ return tx
+
+ def _add_instant_pubkey_to_transaction(self, tx):
+ for input in tx.inputs():
+ instant_pubkey = bytes.fromhex(self.multisig_script_generator.instant_pubkey)
+ if instant_pubkey not in input.pubkeys:
+ input.pubkeys.append(instant_pubkey)
+ input.num_sig = 2
+ assert len(input.pubkeys) == 3, 'Wrong number of pubkeys for performing recovery tx'
+ return tx
+
+ def _add_recovery_pubkey_to_transaction(self, tx):
+ for input in tx.inputs():
+ instant_pubkey = bytes.fromhex(input.multisig_script_generator.instant_pubkey)
+ if instant_pubkey not in input.pubkeys:
+ input.pubkeys.append(instant_pubkey)
+ recovery_pubkey = bytes.fromhex(input.multisig_script_generator.recovery_pubkey)
+ if recovery_pubkey not in input.pubkeys:
+ input.pubkeys.append(recovery_pubkey)
+ input.num_sig = 3
+ assert len(input.pubkeys) == 3, 'Wrong number of pubkeys for performing recovery tx'
+ return tx
+
+
+ def sign_recovery_transaction(self, tx: PartialTransaction, password, recovery_keypairs, recovery_password, instant_password)-> Optional[
+ PartialTransaction]:
+
+ if not isinstance(tx, PartialTransaction):
+ return
+
+ tx = self.sign_transaction(tx, password, self._add_recovery_pubkey_to_transaction, recovery_password, instant_password)
+
+ if not tx.is_complete():
+ _logger.error(f'Recovery transaction not completed')
+ tx.finalize_psbt()
+
+ return tx
+
+ def get_wallet_label(self):
+ return '3-Key Vault'
+
+ def is_instant_mode(self):
+ return self.multisig_script_generator.is_instant_mode()
+
+
wallet_types = [
'2-key',
'3-key',
@@ -2593,12 +2887,16 @@ def register_wallet_type(category):
'xpub': Standard_Wallet,
'imported': Imported_Wallet,
'2-key': TwoKeysWallet,
- '3-key': ThreeKeysWallet
+ '3-key': ThreeKeysWallet,
+ '2-key-hw': TwoKeysHWWallet,
+ '3-key-hw': ThreeKeysHWWallet
}
+
def register_constructor(wallet_type, constructor):
wallet_constructors[wallet_type] = constructor
+
# former WalletFactory
class Wallet(object):
"""The main wallet "entry point".