wizard: extend derivation dialog to also let user select script type.
enable segwit multisig for bip39/hw wallets.
This commit is contained in:
@@ -519,6 +519,34 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||
self.exec_layout(vbox, '')
|
||||
return clayout.selected_index()
|
||||
|
||||
@wizard_dialog
|
||||
def choice_and_line_dialog(self, title, message1, choices, message2,
|
||||
test_text, run_next) -> (str, str):
|
||||
vbox = QVBoxLayout()
|
||||
|
||||
c_values = [x[0] for x in choices]
|
||||
c_titles = [x[1] for x in choices]
|
||||
c_default_text = [x[2] for x in choices]
|
||||
def on_choice_click(clayout):
|
||||
idx = clayout.selected_index()
|
||||
line.setText(c_default_text[idx])
|
||||
clayout = ChoicesLayout(message1, c_titles, on_choice_click)
|
||||
vbox.addLayout(clayout.layout())
|
||||
|
||||
vbox.addSpacing(50)
|
||||
vbox.addWidget(WWLabel(message2))
|
||||
|
||||
line = QLineEdit()
|
||||
def on_text_change(text):
|
||||
self.next_button.setEnabled(test_text(text))
|
||||
line.textEdited.connect(on_text_change)
|
||||
on_choice_click(clayout) # set default text for "line"
|
||||
vbox.addWidget(line)
|
||||
|
||||
self.exec_layout(vbox, title)
|
||||
choice = c_values[clayout.selected_index()]
|
||||
return str(line.text()), choice
|
||||
|
||||
@wizard_dialog
|
||||
def line_dialog(self, run_next, title, message, default, test, warning='',
|
||||
presets=()):
|
||||
@@ -535,9 +563,9 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||
for preset in presets:
|
||||
button = QPushButton(preset[0])
|
||||
button.clicked.connect(lambda __, text=preset[1]: line.setText(text))
|
||||
button.setMaximumWidth(150)
|
||||
button.setMinimumWidth(150)
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addWidget(button, Qt.AlignCenter)
|
||||
hbox.addWidget(button, alignment=Qt.AlignCenter)
|
||||
vbox.addLayout(hbox)
|
||||
|
||||
self.exec_layout(vbox, title, next_enabled=test(default))
|
||||
|
||||
@@ -30,7 +30,7 @@ from functools import partial
|
||||
|
||||
from . import bitcoin
|
||||
from . import keystore
|
||||
from .keystore import bip44_derivation
|
||||
from .keystore import bip44_derivation, purpose48_derivation
|
||||
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet
|
||||
from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
|
||||
from .i18n import _
|
||||
@@ -279,13 +279,9 @@ class BaseWizard(object):
|
||||
self.choose_hw_device(purpose)
|
||||
return
|
||||
if purpose == HWD_SETUP_NEW_WALLET:
|
||||
if self.wallet_type=='multisig':
|
||||
# There is no general standard for HD multisig.
|
||||
# This is partially compatible with BIP45; assumes index=0
|
||||
self.on_hw_derivation(name, device_info, "m/45'/0")
|
||||
else:
|
||||
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
|
||||
self.derivation_dialog(f)
|
||||
def f(derivation, script_type):
|
||||
self.run('on_hw_derivation', name, device_info, derivation, script_type)
|
||||
self.derivation_and_script_type_dialog(f)
|
||||
elif purpose == HWD_SETUP_DECRYPT_WALLET:
|
||||
derivation = get_derivation_used_for_hw_device_encryption()
|
||||
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self)
|
||||
@@ -302,30 +298,39 @@ class BaseWizard(object):
|
||||
else:
|
||||
raise Exception('unknown purpose: %s' % purpose)
|
||||
|
||||
def derivation_dialog(self, f):
|
||||
default = bip44_derivation(0, bip43_purpose=44)
|
||||
message = '\n'.join([
|
||||
_('Enter your wallet derivation here.'),
|
||||
def derivation_and_script_type_dialog(self, f):
|
||||
message1 = _('Choose the type of addresses in your wallet.')
|
||||
message2 = '\n'.join([
|
||||
_('You can override the suggested derivation path.'),
|
||||
_('If you are not sure what this is, leave this field unchanged.')
|
||||
])
|
||||
presets = (
|
||||
('legacy BIP44', bip44_derivation(0, bip43_purpose=44)),
|
||||
('p2sh-segwit BIP49', bip44_derivation(0, bip43_purpose=49)),
|
||||
('native-segwit BIP84', bip44_derivation(0, bip43_purpose=84)),
|
||||
)
|
||||
if self.wallet_type == 'multisig':
|
||||
# There is no general standard for HD multisig.
|
||||
# For legacy, this is partially compatible with BIP45; assumes index=0
|
||||
# For segwit, a custom path is used, as there is no standard at all.
|
||||
choices = [
|
||||
('standard', 'legacy multisig (p2sh)', "m/45'/0"),
|
||||
('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')),
|
||||
('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')),
|
||||
]
|
||||
else:
|
||||
choices = [
|
||||
('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)),
|
||||
('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)),
|
||||
('p2wpkh', 'native segwit (p2wpkh)', bip44_derivation(0, bip43_purpose=84)),
|
||||
]
|
||||
while True:
|
||||
try:
|
||||
self.line_dialog(run_next=f, title=_('Derivation'), message=message,
|
||||
default=default, test=bitcoin.is_bip32_derivation,
|
||||
presets=presets)
|
||||
self.choice_and_line_dialog(
|
||||
run_next=f, title=_('Script type and Derivation path'), message1=message1,
|
||||
message2=message2, choices=choices, test_text=bitcoin.is_bip32_derivation)
|
||||
return
|
||||
except ScriptTypeNotSupported as e:
|
||||
self.show_error(e)
|
||||
# let the user choose again
|
||||
|
||||
def on_hw_derivation(self, name, device_info, derivation):
|
||||
def on_hw_derivation(self, name, device_info, derivation, xtype):
|
||||
from .keystore import hardware_keystore
|
||||
xtype = keystore.xtype_from_derivation(derivation)
|
||||
try:
|
||||
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
|
||||
except ScriptTypeNotSupported:
|
||||
@@ -379,15 +384,16 @@ class BaseWizard(object):
|
||||
raise Exception('Unknown seed type', self.seed_type)
|
||||
|
||||
def on_restore_bip39(self, seed, passphrase):
|
||||
f = lambda x: self.run('on_bip43', seed, passphrase, str(x))
|
||||
self.derivation_dialog(f)
|
||||
def f(derivation, script_type):
|
||||
self.run('on_bip43', seed, passphrase, derivation, script_type)
|
||||
self.derivation_and_script_type_dialog(f)
|
||||
|
||||
def create_keystore(self, seed, passphrase):
|
||||
k = keystore.from_seed(seed, passphrase, self.wallet_type == 'multisig')
|
||||
self.on_keystore(k)
|
||||
|
||||
def on_bip43(self, seed, passphrase, derivation):
|
||||
k = keystore.from_bip39_seed(seed, passphrase, derivation)
|
||||
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):
|
||||
|
||||
@@ -398,7 +398,7 @@ def DecodeBase58Check(psz):
|
||||
# backwards compat
|
||||
# extended WIF for segwit (used in 3.0.x; but still used internally)
|
||||
# the keys in this dict should be a superset of what Imported Wallets can import
|
||||
SCRIPT_TYPES = {
|
||||
WIF_SCRIPT_TYPES = {
|
||||
'p2pkh':0,
|
||||
'p2wpkh':1,
|
||||
'p2wpkh-p2sh':2,
|
||||
@@ -406,6 +406,14 @@ SCRIPT_TYPES = {
|
||||
'p2wsh':6,
|
||||
'p2wsh-p2sh':7
|
||||
}
|
||||
WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES)
|
||||
|
||||
|
||||
PURPOSE48_SCRIPT_TYPES = {
|
||||
'p2wsh-p2sh': 1, # specifically multisig
|
||||
'p2wsh': 2, # specifically multisig
|
||||
}
|
||||
PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES)
|
||||
|
||||
|
||||
def serialize_privkey(secret: bytes, compressed: bool, txin_type: str,
|
||||
@@ -413,7 +421,7 @@ def serialize_privkey(secret: bytes, compressed: bool, txin_type: str,
|
||||
# we only export secrets inside curve range
|
||||
secret = ecc.ECPrivkey.normalize_secret_bytes(secret)
|
||||
if internal_use:
|
||||
prefix = bytes([(SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255])
|
||||
prefix = bytes([(WIF_SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255])
|
||||
else:
|
||||
prefix = bytes([constants.net.WIF_PREFIX])
|
||||
suffix = b'\01' if compressed else b''
|
||||
@@ -432,7 +440,7 @@ def deserialize_privkey(key: str) -> (str, bytes, bool):
|
||||
txin_type = None
|
||||
if ':' in key:
|
||||
txin_type, key = key.split(sep=':', maxsplit=1)
|
||||
if txin_type not in SCRIPT_TYPES:
|
||||
if txin_type not in WIF_SCRIPT_TYPES:
|
||||
raise BitcoinException('unknown script type: {}'.format(txin_type))
|
||||
try:
|
||||
vch = DecodeBase58Check(key)
|
||||
@@ -444,9 +452,8 @@ def deserialize_privkey(key: str) -> (str, bytes, bool):
|
||||
if txin_type is None:
|
||||
# keys exported in version 3.0.x encoded script type in first byte
|
||||
prefix_value = vch[0] - constants.net.WIF_PREFIX
|
||||
inverse_script_types = inv_dict(SCRIPT_TYPES)
|
||||
try:
|
||||
txin_type = inverse_script_types[prefix_value]
|
||||
txin_type = WIF_SCRIPT_TYPES_INV[prefix_value]
|
||||
except KeyError:
|
||||
raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0]))
|
||||
else:
|
||||
|
||||
@@ -600,14 +600,26 @@ def from_bip39_seed(seed, passphrase, derivation, xtype=None):
|
||||
return k
|
||||
|
||||
|
||||
def xtype_from_derivation(derivation):
|
||||
def xtype_from_derivation(derivation: str) -> str:
|
||||
"""Returns the script type to be used for this derivation."""
|
||||
if derivation.startswith("m/84'"):
|
||||
return 'p2wpkh'
|
||||
elif derivation.startswith("m/49'"):
|
||||
return 'p2wpkh-p2sh'
|
||||
else:
|
||||
elif derivation.startswith("m/44'"):
|
||||
return 'standard'
|
||||
elif derivation.startswith("m/45'"):
|
||||
return 'standard'
|
||||
|
||||
bip32_indices = list(bip32_derivation(derivation))
|
||||
if len(bip32_indices) >= 4:
|
||||
if bip32_indices[0] == 48 + BIP32_PRIME:
|
||||
# m / purpose' / coin_type' / account' / script_type' / change / address_index
|
||||
script_type_int = bip32_indices[3] - BIP32_PRIME
|
||||
script_type = PURPOSE48_SCRIPT_TYPES_INV.get(script_type_int)
|
||||
if script_type is not None:
|
||||
return script_type
|
||||
return 'standard'
|
||||
|
||||
|
||||
# extended pubkeys
|
||||
@@ -719,6 +731,18 @@ def bip44_derivation(account_id, bip43_purpose=44):
|
||||
coin = constants.net.BIP44_COIN_TYPE
|
||||
return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
|
||||
|
||||
|
||||
def purpose48_derivation(account_id: int, xtype: str) -> str:
|
||||
# m / purpose' / coin_type' / account' / script_type' / change / address_index
|
||||
bip43_purpose = 48
|
||||
coin = constants.net.BIP44_COIN_TYPE
|
||||
account_id = int(account_id)
|
||||
script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype)
|
||||
if script_type_int is None:
|
||||
raise Exception('unknown xtype: {}'.format(xtype))
|
||||
return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int)
|
||||
|
||||
|
||||
def from_seed(seed, passphrase, is_p2sh):
|
||||
t = seed_type(seed)
|
||||
if t == 'old':
|
||||
|
||||
@@ -19,6 +19,7 @@ from lib.transaction import opcodes
|
||||
from lib.util import bfh, bh2u
|
||||
from lib import constants
|
||||
from lib.storage import WalletStorage
|
||||
from lib.keystore import xtype_from_derivation
|
||||
|
||||
from . import SequentialTestCase
|
||||
from . import TestCaseForTestnet
|
||||
@@ -469,6 +470,23 @@ class Test_xprv_xpub(SequentialTestCase):
|
||||
self.assertFalse(is_bip32_derivation(""))
|
||||
self.assertFalse(is_bip32_derivation("m/q8462"))
|
||||
|
||||
def test_xtype_from_derivation(self):
|
||||
self.assertEqual('standard', xtype_from_derivation("m/44'"))
|
||||
self.assertEqual('standard', xtype_from_derivation("m/44'/"))
|
||||
self.assertEqual('standard', xtype_from_derivation("m/44'/0'/0'"))
|
||||
self.assertEqual('standard', xtype_from_derivation("m/44'/5241'/221"))
|
||||
self.assertEqual('standard', xtype_from_derivation("m/45'"))
|
||||
self.assertEqual('standard', xtype_from_derivation("m/45'/56165/271'"))
|
||||
self.assertEqual('p2wpkh-p2sh', xtype_from_derivation("m/49'"))
|
||||
self.assertEqual('p2wpkh-p2sh', xtype_from_derivation("m/49'/134"))
|
||||
self.assertEqual('p2wpkh', xtype_from_derivation("m/84'"))
|
||||
self.assertEqual('p2wpkh', xtype_from_derivation("m/84'/112'/992/112/33'/0/2"))
|
||||
self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/0'/0'/1'"))
|
||||
self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/0'/0'/1'/52112/52'"))
|
||||
self.assertEqual('p2wsh-p2sh', xtype_from_derivation("m/48'/9'/2'/1'"))
|
||||
self.assertEqual('p2wsh', xtype_from_derivation("m/48'/0'/0'/2'"))
|
||||
self.assertEqual('p2wsh', xtype_from_derivation("m/48'/1'/0'/2'/77'/0"))
|
||||
|
||||
def test_version_bytes(self):
|
||||
xprv_headers_b58 = {
|
||||
'standard': 'xprv',
|
||||
|
||||
Reference in New Issue
Block a user