Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion electrum/gui/qt/wizard/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ def is_seed(self, x):
# really only used for electrum seeds. bip39 and slip39 are validated in SeedWidget
t = mnemonic.calc_seed_type(x)
if self.wizard_data['wallet_type'] == 'standard':
return mnemonic.is_seed(x) and not mnemonic.is_any_2fa_seed_type(t)
return mnemonic.is_seed(x) and (self.wizard.supports_2fa() or not mnemonic.is_any_2fa_seed_type(t))
elif self.wizard_data['wallet_type'] == '2fa':
return mnemonic.is_any_2fa_seed_type(t)
else:
Expand Down
36 changes: 33 additions & 3 deletions electrum/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,9 @@ def is_multisig(self, wizard_data: dict) -> bool:
def is_hardware(self, wizard_data: dict) -> bool:
return wizard_data['keystore_type'] == 'hardware'

def supports_2fa(self) -> bool:
return False

def wallet_password_view(self, wizard_data: dict) -> str:
if self.is_hardware(wizard_data) and wizard_data['wallet_type'] == 'standard':
return 'wallet_password_hardware'
Expand Down Expand Up @@ -345,6 +348,9 @@ def validate_seed(self, seed: str, seed_variant: str, wallet_type: str) -> Tuple
# check if seed matches wallet type
if wallet_type == '2fa' and not is_any_2fa_seed_type(seed_type):
seed_valid = False
elif wallet_type == 'standard' and is_any_2fa_seed_type(seed_type):
# wizard will redirect
seed_valid = self.supports_2fa()
elif wallet_type == 'standard' and seed_type not in ['old', 'standard', 'segwit', 'bip39', 'slip39']:
seed_valid = False
elif wallet_type == 'multisig' and seed_type not in ['standard', 'segwit', 'bip39', 'slip39']:
Expand Down Expand Up @@ -433,10 +439,10 @@ def __init__(self, daemon: 'Daemon', plugins: 'Plugins'):
'last': lambda d: self.is_single_password() and not self.is_multisig(d)
},
'have_seed': {
'next': lambda d: 'have_ext' if self.wants_ext(d) else self.on_have_or_confirm_seed(d),
'accept': lambda d: None if self.wants_ext(d) else self.maybe_master_pubkey(d),
'next': self.on_have_seed,
'accept': self.on_accept_have_seed,
'last': lambda d: self.is_single_password() and not
(self.needs_derivation_path(d) or self.is_multisig(d) or self.wants_ext(d)),
(self.needs_derivation_path(d) or self.is_multisig(d) or self.wants_ext(d) or self.wants_2fa(d)),
},
'have_ext': {
'next': self.on_have_or_confirm_seed,
Expand Down Expand Up @@ -530,6 +536,30 @@ def on_keystore_type(self, wizard_data: dict) -> str:
'hardware': 'choose_hardware_device'
}.get(t)

def supports_2fa(self) -> bool:
return True

def wants_2fa(self, wizard_data: dict) -> bool:
# True if the user entered a 2fa seed while restoring a wallet of type 'standard'
return (wizard_data['wallet_type'] == 'standard'
and is_any_2fa_seed_type(wizard_data.get('seed_type', ''))
and self.supports_2fa())

def on_have_seed(self, wizard_data: dict) -> str:
if wizard_data['wallet_type'] == '2fa': # redirected by on_accept_have_seed
return 'trustedcoin_have_ext' if self.wants_ext(wizard_data) else 'trustedcoin_keep_disable'
elif self.wants_ext(wizard_data):
return 'have_ext'
else:
return self.on_have_or_confirm_seed(wizard_data)

def on_accept_have_seed(self, wizard_data: dict) -> None:
if self.wants_2fa(wizard_data):
wizard_data['wallet_type'] = '2fa'
return
if not self.wants_ext(wizard_data):
self.maybe_master_pubkey(wizard_data)

def on_have_or_confirm_seed(self, wizard_data: dict) -> str:
if self.needs_derivation_path(wizard_data):
return 'script_and_derivation'
Expand Down
77 changes: 68 additions & 9 deletions tests/test_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ def __init__(self, config: SimpleConfig):
self.network = NetworkMock()


class TNewWalletWizard(NewWalletWizard):
def is_single_password(self):
"""impl abstract reqd"""
return True


class WizardTestCase(ElectrumTestCase):

def setUp(self):
Expand Down Expand Up @@ -133,11 +139,6 @@ def is_single_password(self):
"""impl abstract reqd"""
return True

class TNewWalletWizard(NewWalletWizard):
def is_single_password(self):
"""impl abstract reqd"""
return True

def _wizard_for(self, *, wallet_type: str = 'standard', hww: bool = False) -> tuple[KeystoreWizard, WizardViewState]:
w = KeystoreWizardTestCase.TKeystoreWizard(self.plugins)
start_viewstate = WizardViewState('keystore_type', {'wallet_type': wallet_type}, {})
Expand All @@ -156,7 +157,7 @@ def _wizard_for(self, *, wallet_type: str = 'standard', hww: bool = False) -> tu
return w, v

def _create_xpub_keystore_wallet(self, *, wallet_type: str = 'standard', xpub):
w = KeystoreWizardTestCase.TNewWalletWizard(DaemonMock(self.config), self.plugins)
w = TNewWalletWizard(DaemonMock(self.config), self.plugins)
wallet_path = self.wallet_path
d = {
'wallet_type': wallet_type,
Expand Down Expand Up @@ -262,6 +263,14 @@ async def test_haveseed_electrum__mismatching_seed(self):
wallet.enable_keystore(ks, ishww, None)
self.assertTrue("mismatching xpubs" in ctx.exception.args[0])

async def test_haveseed_electrum__2fa_seed(self):
"""unlike NewWalletWizard, KeystoreWizard cannot redirect to the 2fa flow, so 2fa seeds are invalid"""
w, v = self._wizard_for()
seed_valid, seed_type, *_ = w.validate_seed(
'oblige basket safe educate whale bacon celery demand novel slice various awkward', 'electrum', 'standard')
self.assertFalse(seed_valid)
self.assertEqual('2fa_segwit', seed_type)

async def test_haveseed_electrum_oldseed(self):
w, v = self._wizard_for()
d = v.wizard_data
Expand Down Expand Up @@ -439,9 +448,8 @@ def _wizard_for(
name: str = "mywallet",
wallet_type: str,
) -> NewWalletWizard:
w = NewWalletWizard(DaemonMock(self.config), self.plugins)
if wallet_type == '2fa':
w.plugins.get_plugin('trustedcoin').extend_wizard(w)
w = TNewWalletWizard(DaemonMock(self.config), self.plugins)
w.plugins.get_plugin('trustedcoin').extend_wizard(w)
v_init = w.start()
self.assertEqual('wallet_name', v_init.view)
d = {'wallet_name': name}
Expand Down Expand Up @@ -895,6 +903,57 @@ async def test_2fa_haveseed_passphrase(self):
v = w.resolve_next(v.view, d)
self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qcnu9ay4v3w0tawuxe6wlh6mh33rrpauqnufdgkxx7we8vpx3e6wqa25qud")

async def test_2fa_haveseed_redirected_from_standard(self):
w = self._wizard_for(wallet_type='standard')
v = w._current
d = v.wizard_data
self.assertEqual('keystore_type', v.view)

d.update({'keystore_type': 'haveseed'})
v = w.resolve_next(v.view, d)
self.assertEqual('have_seed', v.view)
myseed = 'oblige basket safe educate whale bacon celery demand novel slice various awkward'
self.assertTrue(w.validate_seed(myseed, 'electrum', 'standard')[0])
d.update({
'seed': myseed,
'seed_type': '2fa_segwit', 'seed_extend': False, 'seed_variant': 'electrum',
})
self.assertFalse(w.is_last_view(v.view, d))
v = w.resolve_next(v.view, d)
self.assertEqual('trustedcoin_keep_disable', v.view)
self.assertEqual('2fa', v.wizard_data['wallet_type'])
d.update({'trustedcoin_keepordisable': 'keep'})
v = w.resolve_next(v.view, d)
self.assertEqual('trustedcoin_tos', v.view)
v = w.resolve_next(v.view, d)
self.assertEqual('trustedcoin_show_confirm_otp', v.view)
v = w.resolve_next(v.view, d)
wallet = self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qnf5qafvpx0afk47433j3tt30pqkxp5wa263m77wt0pvyqq67rmfs522m94")
self.assertEqual('2fa', wallet.wallet_type)

async def test_2fa_haveseed_redirected_from_standard_passphrase(self):
w = self._wizard_for(wallet_type='standard')
v = w._current
d = v.wizard_data
self.assertEqual('keystore_type', v.view)

d.update({'keystore_type': 'haveseed'})
v = w.resolve_next(v.view, d)
self.assertEqual('have_seed', v.view)
d.update({
'seed': 'oblige basket safe educate whale bacon celery demand novel slice various awkward',
'seed_type': '2fa_segwit', 'seed_extend': True, 'seed_variant': 'electrum',
})
v = w.resolve_next(v.view, d)
self.assertEqual('trustedcoin_have_ext', v.view)
d.update({'seed_extra_words': UNICODE_HORROR})
v = w.resolve_next(v.view, d)
self.assertEqual('trustedcoin_keep_disable', v.view)
d.update({'trustedcoin_keepordisable': 'disable'})
v = w.resolve_next(v.view, d)
wallet = self._set_password_and_check_address(v=v, w=w, recv_addr="bc1qcnu9ay4v3w0tawuxe6wlh6mh33rrpauqnufdgkxx7we8vpx3e6wqa25qud")
self.assertEqual('2fa', wallet.wallet_type)

async def test_create_standard_wallet_trezor(self):
# bip39 seed for trezor: "history six okay anchor sheriff flock atom tomorrow foster aerobic eternal foam"
w = self._wizard_for(wallet_type='standard')
Expand Down