mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #571 from kdmukai/electrum_seed_cleanup
Light refactor of Electrum seed support
This commit is contained in:
@@ -19,6 +19,7 @@ from seedsigner.models.settings_definition import SettingsConstants
|
||||
# TODO: PR these directly into `embit`? Or replace with new/existing methods already in `embit`?
|
||||
|
||||
|
||||
# TODO: Refactor `wallet_type` to conform to our `sig_type` naming convention
|
||||
def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, script_type: str = SettingsConstants.NATIVE_SEGWIT) -> str:
|
||||
if network == SettingsConstants.MAINNET:
|
||||
network_path = "0'"
|
||||
|
||||
@@ -85,15 +85,17 @@ class BaseStaticQrEncoder(BaseQrEncoder):
|
||||
|
||||
@dataclass
|
||||
class SeedQrEncoder(BaseStaticQrEncoder):
|
||||
seed : Seed = None
|
||||
mnemonic: List[str] = None
|
||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code)
|
||||
self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
|
||||
super().__post_init__()
|
||||
|
||||
self.data = ""
|
||||
# Output as Numeric data format
|
||||
for word in self.seed.mnemonic_list:
|
||||
for word in self.mnemonic:
|
||||
index = self.wordlist.index(word)
|
||||
self.data += str("%04d" % index)
|
||||
|
||||
@@ -108,18 +110,18 @@ class CompactSeedQrEncoder(SeedQrEncoder):
|
||||
def next_part(self):
|
||||
# Output as binary data format
|
||||
binary_str = ""
|
||||
for word in self.seed.mnemonic_list:
|
||||
for word in self.mnemonic:
|
||||
index = self.wordlist.index(word)
|
||||
|
||||
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
|
||||
binary_str += bin(index).split('b')[1].zfill(11)
|
||||
|
||||
# We can exclude the checksum bits at the end
|
||||
if len(self.seed.mnemonic_list) == 24:
|
||||
if len(self.mnemonic) == 24:
|
||||
# 8 checksum bits in a 24-word seed
|
||||
binary_str = binary_str[:-8]
|
||||
|
||||
elif len(self.seed.mnemonic_list) == 12:
|
||||
elif len(self.mnemonic) == 12:
|
||||
# 4 checksum bits in a 12-word seed
|
||||
binary_str = binary_str[:-4]
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class Seed:
|
||||
raise Exception(f"Unrecognized wordlist_language_code {wordlist_language_code}")
|
||||
|
||||
|
||||
def _generate_seed(self) -> bool:
|
||||
def _generate_seed(self):
|
||||
try:
|
||||
self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist)
|
||||
except Exception as e:
|
||||
@@ -64,6 +64,7 @@ class Seed:
|
||||
def wordlist_language_code(self) -> str:
|
||||
return self._wordlist_language_code
|
||||
|
||||
|
||||
@property
|
||||
def mnemonic_display_str(self) -> str:
|
||||
return unicodedata.normalize("NFC", " ".join(self._mnemonic))
|
||||
@@ -106,29 +107,36 @@ class Seed:
|
||||
# TODO: Support other BIP-39 wordlist languages!
|
||||
raise Exception("Not yet implemented!")
|
||||
|
||||
|
||||
@property
|
||||
def script_override(self) -> list:
|
||||
return None
|
||||
|
||||
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||
def script_override(self) -> str:
|
||||
return None
|
||||
|
||||
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||
|
||||
def derivation_override(self, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||
return None
|
||||
|
||||
|
||||
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
|
||||
return bip32.detect_version(derivation_path, default="xpub", network=embit_network)
|
||||
|
||||
|
||||
@property
|
||||
def passphrase_label(self) -> str:
|
||||
return SettingsConstants.LABEL__BIP39_PASSPHRASE
|
||||
|
||||
|
||||
@property
|
||||
def seedqr_supported(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@property
|
||||
def bip85_supported(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def get_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str:
|
||||
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"])
|
||||
return hexlify(root.child(0).fingerprint).decode('utf-8')
|
||||
@@ -158,19 +166,20 @@ class Seed:
|
||||
|
||||
class ElectrumSeed(Seed):
|
||||
|
||||
|
||||
def _generate_seed(self) -> bool:
|
||||
def _generate_seed(self):
|
||||
if len(self._mnemonic) != 12:
|
||||
return False
|
||||
raise InvalidSeedException(f"Unsupported Electrum seed length: {len(self._mnemonic)}")
|
||||
|
||||
s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex()
|
||||
prefix = s[0:3]
|
||||
|
||||
# only support Electrum Segwit version for now
|
||||
if SettingsConstants.ELECTRUM_SEED_SEGWIT == prefix:
|
||||
self.seed_bytes=hashlib.pbkdf2_hmac('sha512', self.mnemonic_str.encode('utf-8'), b'electrum' + self._passphrase.encode('utf-8'), iterations = SettingsConstants.ELECTRUM_PBKDF2_ROUNDS)
|
||||
return True
|
||||
|
||||
else:
|
||||
raise InvalidSeedException("Unsupported electrum seed input")
|
||||
return False
|
||||
raise InvalidSeedException(f"Unsupported Electrum seed format: {prefix}")
|
||||
|
||||
|
||||
def set_passphrase(self, passphrase: str, regenerate_seed: bool = True):
|
||||
if passphrase:
|
||||
@@ -184,6 +193,7 @@ class ElectrumSeed(Seed):
|
||||
# Regenerate the internal seed since passphrase changes the result
|
||||
self._generate_seed()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def normalize_electrum_passphrase(passphrase : str) -> str:
|
||||
passphrase = unicodedata.normalize('NFKD', passphrase)
|
||||
@@ -193,25 +203,31 @@ class ElectrumSeed(Seed):
|
||||
passphrase = u' '.join(passphrase.split())
|
||||
return passphrase
|
||||
|
||||
|
||||
@property
|
||||
def script_override(self) -> list:
|
||||
return [SettingsConstants.NATIVE_SEGWIT]
|
||||
def script_override(self) -> str:
|
||||
return SettingsConstants.NATIVE_SEGWIT
|
||||
|
||||
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||
return "m/0h" if SettingsConstants.SINGLE_SIG == wallet_type else "m/1h"
|
||||
|
||||
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||
def derivation_override(self, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||
return "m/0h" if sig_type == SettingsConstants.SINGLE_SIG else "m/1h"
|
||||
|
||||
|
||||
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
|
||||
return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"]
|
||||
return embit_network["zpub"] if sig_type == SettingsConstants.SINGLE_SIG else embit_network["Zpub"]
|
||||
|
||||
|
||||
@property
|
||||
def passphrase_label(self) -> str:
|
||||
return SettingsConstants.LABEL__CUSTOM_EXTENSION
|
||||
|
||||
|
||||
@property
|
||||
def seedqr_supported(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@property
|
||||
def bip85_supported(self) -> bool:
|
||||
return False
|
||||
|
||||
@@ -207,7 +207,9 @@ class SettingsConstants:
|
||||
|
||||
# Label strings
|
||||
LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase"
|
||||
LABEL__CUSTOM_EXTENSION = "Custom Extension"
|
||||
LABEL__CUSTOM_EXTENSION = "Custom Extension" # Terminology used by Electrum seeds
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingsEntry:
|
||||
@@ -470,8 +472,9 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
abbreviated_name="Electrum",
|
||||
display_name="Electrum seed support (Native Segwit only)",
|
||||
abbreviated_name="electrum",
|
||||
display_name="Electrum seeds",
|
||||
help_text="Native Segwit only",
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
|
||||
@@ -79,16 +79,8 @@ class PSBTSelectSeedView(View):
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features disabled for Electrum seeds",
|
||||
show_back_button=False,
|
||||
)
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
from seedsigner.views.seed_views import SeedElectrumMnemonicStartView
|
||||
return Destination(SeedElectrumMnemonicStartView)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ class SeedSelectSeedView(View):
|
||||
button_data.append(self.SCAN_SEED)
|
||||
button_data.append(self.TYPE_12WORD)
|
||||
button_data.append(self.TYPE_24WORD)
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(self.TYPE_ELECTRUM)
|
||||
|
||||
@@ -158,17 +159,7 @@ class SeedSelectSeedView(View):
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features disabled for Electrum seeds",
|
||||
show_back_button=False,
|
||||
)
|
||||
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
return Destination(SeedElectrumMnemonicStartView)
|
||||
|
||||
|
||||
|
||||
@@ -187,10 +178,12 @@ class LoadSeedView(View):
|
||||
self.SEED_QR,
|
||||
self.TYPE_12WORD,
|
||||
self.TYPE_24WORD,
|
||||
self.CREATE,
|
||||
]
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.insert(len(button_data)-1, self.TYPE_ELECTRUM)
|
||||
button_data.append(self.TYPE_ELECTRUM)
|
||||
|
||||
button_data.append(self.CREATE)
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
@@ -215,15 +208,7 @@ class LoadSeedView(View):
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features disabled for Electrum seeds",
|
||||
show_back_button=False,
|
||||
)
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
return Destination(SeedElectrumMnemonicStartView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.CREATE:
|
||||
from .tools_views import ToolsMenuView
|
||||
@@ -489,6 +474,27 @@ class SeedDiscardView(View):
|
||||
|
||||
|
||||
|
||||
class SeedElectrumMnemonicStartView(View):
|
||||
"""
|
||||
Currently just a warning display before entering an Electrum seed.
|
||||
|
||||
Could be expanded with a follow-up View to specify Electrum seed type.
|
||||
"""
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features are disabled for Electrum seeds.",
|
||||
show_back_button=False,
|
||||
)
|
||||
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
|
||||
|
||||
"""****************************************************************************
|
||||
Views for actions on individual seeds:
|
||||
****************************************************************************"""
|
||||
@@ -614,6 +620,7 @@ class SeedBackupView(View):
|
||||
|
||||
def run(self):
|
||||
button_data = [self.VIEW_WORDS]
|
||||
|
||||
if self.seed.seedqr_supported:
|
||||
button_data.append(self.EXPORT_SEEDQR)
|
||||
|
||||
@@ -681,8 +688,16 @@ class SeedExportXpubScriptTypeView(View):
|
||||
def run(self):
|
||||
from .tools_views import ToolsAddressExplorerAddressTypeView
|
||||
args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
|
||||
|
||||
script_types = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)
|
||||
|
||||
seed = self.controller.storage.seeds[self.seed_num]
|
||||
script_types = seed.script_override if seed.script_override else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)
|
||||
if seed.script_override:
|
||||
# This seed only allows one script type
|
||||
# TODO: Does it matter if the Settings don't have the override script type
|
||||
# enabled?
|
||||
script_types = [seed.script_override]
|
||||
|
||||
if len(script_types) == 1:
|
||||
# Nothing to select; skip this screen
|
||||
args["script_type"] = script_types[0]
|
||||
@@ -1456,7 +1471,8 @@ class SeedTranscribeSeedQRWholeQRView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
encoder_args = dict(seed=self.seed)
|
||||
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
if self.seedqr_format == QRType.SEED__SEEDQR:
|
||||
e = SeedQrEncoder(**encoder_args)
|
||||
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
||||
@@ -1492,7 +1508,8 @@ class SeedTranscribeSeedQRZoomedInView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
encoder_args = dict(seed=self.seed)
|
||||
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
if self.seedqr_format == QRType.SEED__SEEDQR:
|
||||
e = SeedQrEncoder(**encoder_args)
|
||||
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
||||
@@ -1720,7 +1737,7 @@ class SeedAddressVerificationView(View):
|
||||
raise Exception("Can't validate a single sig addr without specifying a seed")
|
||||
self.seed_num = seed_num
|
||||
self.seed = self.controller.get_seed(seed_num)
|
||||
self.seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG)
|
||||
self.seed_derivation_override = self.seed.derivation_override(sig_type=SettingsConstants.SINGLE_SIG)
|
||||
else:
|
||||
self.seed = None
|
||||
self.address = self.controller.unverified_address["address"]
|
||||
|
||||
@@ -496,16 +496,8 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features disabled for Electrum seeds",
|
||||
show_back_button=False,
|
||||
)
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
from seedsigner.views.seed_views import SeedElectrumMnemonicStartView
|
||||
return Destination(SeedElectrumMnemonicStartView)
|
||||
|
||||
|
||||
|
||||
@@ -540,7 +532,7 @@ class ToolsAddressExplorerAddressTypeView(View):
|
||||
if self.seed_num is not None:
|
||||
self.seed = self.controller.storage.seeds[seed_num]
|
||||
data["seed_num"] = self.seed
|
||||
seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG)
|
||||
seed_derivation_override = self.seed.derivation_override(sig_type=SettingsConstants.SINGLE_SIG)
|
||||
|
||||
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
|
||||
derivation_path = self.custom_derivation
|
||||
|
||||
@@ -134,6 +134,16 @@ def test_generate_screenshots(target_locale):
|
||||
# Automatically populate all Settings options Views
|
||||
settings_views_list = []
|
||||
settings_views_list.append(settings_views.SettingsMenuView)
|
||||
settings_views_list.append((
|
||||
settings_views.SettingsMenuView,
|
||||
dict(
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
initial_scroll=240, # Just guessing how many pixels to scroll down
|
||||
),
|
||||
"SettingsMenuView__Advanced"
|
||||
))
|
||||
|
||||
# so we get a choice for transcribe seed qr format
|
||||
controller.settings.set_value(
|
||||
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
||||
@@ -215,6 +225,8 @@ def test_generate_screenshots(target_locale):
|
||||
|
||||
seed_views.SeedSignMessageConfirmMessageView,
|
||||
seed_views.SeedSignMessageConfirmAddressView,
|
||||
|
||||
seed_views.SeedElectrumMnemonicStartView,
|
||||
],
|
||||
"PSBT Views": [
|
||||
psbt_views.PSBTSelectSeedView, # this will fail, be rerun below
|
||||
|
||||
@@ -25,19 +25,18 @@ def test_ur_psbt_qr_encode():
|
||||
def test_seedsigner_qr():
|
||||
# test vector 1 from the SeedQR docs
|
||||
mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split()
|
||||
e = SeedQrEncoder(seed=Seed(mnemonic))
|
||||
e = SeedQrEncoder(mnemonic=mnemonic)
|
||||
assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643"
|
||||
|
||||
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
|
||||
e = CompactSeedQrEncoder(mnemonic=mnemonic)
|
||||
assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n'
|
||||
|
||||
# test vector 4 from the SeedQR docs
|
||||
mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split()
|
||||
seed = Seed(mnemonic)
|
||||
e = SeedQrEncoder(seed=seed)
|
||||
e = SeedQrEncoder(mnemonic=mnemonic)
|
||||
assert e.next_part() == "073318950739065415961602009907670428187212261116"
|
||||
|
||||
e = CompactSeedQrEncoder(seed=seed)
|
||||
e = CompactSeedQrEncoder(mnemonic=mnemonic)
|
||||
assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE'
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import scan_views, seed_views, psbt_views
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
@@ -57,7 +58,40 @@ class TestPSBTFlows(FlowTest):
|
||||
FlowStep(MainMenuView)
|
||||
])
|
||||
|
||||
|
||||
|
||||
def test_scan_psbt_first_then_load_electrum_seed(self):
|
||||
"""
|
||||
Should be able to load an Electrum mnemonic after first loading in a psbt.
|
||||
"""
|
||||
def load_psbt_into_decoder(view: scan_views.ScanView):
|
||||
# Single sig psbt for the below Electrum mnemonic
|
||||
view.decoder.add_data("cHNidP8BAHECAAAAAX9/d6VyI7nvVTyhLBfqu05za2AJ2Z0dKMC0cUX+S2U7AQAAAAD9////AgeHAAAAAAAAFgAUOnNPuZMD1sQudt3+7LvHBUvGhyd//gAAAAAAABYAFGO9QLvu4V9/hz6ZjbIGMrqsEiIYAjQTAAABAR+ghgEAAAAAABYAFKawrgcT62jmIVQwyHPCV0thmJWbAQDBAQAAAAABAYeHL9UQlz/jEKUuNNY3LTeQRjudjBinsP2L0ppvgRt0AAAAAAD/////AnbP3rsPAAAAIlEgtgmCioGjfKwp6f8rOoI4OPb+ZV8db581J9IizZPskl2ghgEAAAAAABYAFKawrgcT62jmIVQwyHPCV0thmJWbAUDCBlMh9VjZN2NdU9Wabi0o3Ct1q9YHTsJRLAkLfUuIHB+BE+ucR4bdGAJG5nBhCWOmCXbpRwKP1INRYvkuQ2fHAAAAACIGA2+PEYHyVy6nhYwAx5SJKBIWXjsWgjhhf/2FEWqXgxnoEKNOC3gAAACAAAAAAAAAAAAAACICA0SBeeHxfHdny6rUnQJuteAnQ7shSydexjJCkSJarn3mEKNOC3gAAACAAQAAAAEAAAAA")
|
||||
|
||||
self.settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
sequence = [
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored
|
||||
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.TYPE_ELECTRUM),
|
||||
FlowStep(seed_views.SeedElectrumMnemonicStartView),
|
||||
]
|
||||
|
||||
# Load the associated Electrum mnemonic during the flow
|
||||
for word in "apple drip silly junior language resource unaware whale snake copy gravity tank".split():
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word),
|
||||
]
|
||||
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(psbt_views.PSBTOverviewView),
|
||||
FlowStep(psbt_views.PSBTMathView),
|
||||
]
|
||||
|
||||
self.run_sequence(sequence)
|
||||
|
||||
|
||||
def test_scan_multisig_psbt_seed_already_signed_flow(self):
|
||||
|
||||
def load_psbt_into_decoder(view: scan_views.ScanView):
|
||||
|
||||
+102
-11
@@ -3,11 +3,11 @@ import pytest
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import BaseTest, FlowTest, FlowStep
|
||||
from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
|
||||
from base import FlowTestInvalidButtonDataSelectionException
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.seed import ElectrumSeed, Seed
|
||||
from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView, NotYetImplementedView
|
||||
from seedsigner.views import seed_views, scan_views, settings_views, tools_views
|
||||
|
||||
@@ -119,6 +119,66 @@ class TestSeedFlows(FlowTest):
|
||||
self.run_sequence(sequence)
|
||||
|
||||
|
||||
def test_electrum_mnemonic_entry_flow(self):
|
||||
"""
|
||||
Manually entering an Electrum mnemonic should land at the Finalize Seed flow and end at
|
||||
the SeedOptionsView.
|
||||
|
||||
Most BIP-39 mnemonics should generate an error if entered as Electrum seeds.
|
||||
"""
|
||||
def test_with_mnemonic(mnemonic: list[str], custom_extension: str = None, expects_electrum_seed_is_valid: bool = True):
|
||||
Settings.HOSTNAME = "not seedsigner-os"
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
sequence = [
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
|
||||
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
|
||||
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_ELECTRUM),
|
||||
FlowStep(seed_views.SeedElectrumMnemonicStartView), # Warning screen; no relevant button data selection.
|
||||
]
|
||||
|
||||
# Now add each manual word entry step
|
||||
for word in mnemonic:
|
||||
sequence.append(
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word)
|
||||
)
|
||||
|
||||
if expects_electrum_seed_is_valid:
|
||||
# With the mnemonic completely entered, we land on the SeedFinalizeView
|
||||
if custom_extension:
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedFinalizeView, screen_return_value=1), # The passphrase / custom extension button is dynamic so there's no constant to refer to here
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase=custom_extension)), # This is a one-off oddity where the Screen returns dict instead of int | str
|
||||
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
|
||||
FlowStep(seed_views.SeedOptionsView),
|
||||
]
|
||||
else:
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView),
|
||||
]
|
||||
|
||||
else:
|
||||
# Or we bomb out if the mnemonic is invalid for Electrum
|
||||
sequence.append(FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD))
|
||||
|
||||
self.run_sequence(sequence)
|
||||
|
||||
BaseTest.reset_controller()
|
||||
|
||||
|
||||
# Test seeds generated by Electrum v4.5.5
|
||||
test_with_mnemonic("bomb congress scorpion mutual word stamp tongue valid permit salmon yellow spy".split())
|
||||
test_with_mnemonic("morning pretty hobby click extend color wait joke define sausage boost salmon".split())
|
||||
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split())
|
||||
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split(), custom_extension="test")
|
||||
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split(), custom_extension="monkey fling orange coin good")
|
||||
|
||||
# Most BIP-39 seeds should fail; test seed generated by bitcoiner.guide
|
||||
test_with_mnemonic("pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split(), expects_electrum_seed_is_valid=False)
|
||||
|
||||
|
||||
def test_export_xpub_standard_flow(self):
|
||||
"""
|
||||
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
|
||||
@@ -193,15 +253,15 @@ class TestSeedFlows(FlowTest):
|
||||
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types if x!=disabled_script])
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators if x!=disabled_coord])
|
||||
|
||||
# test that multisig is not an option via exception raised when redirected to next step instead of having a choice
|
||||
with pytest.raises(FlowTestRunScreenNotExecutedException) as e:
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=disabled_sig),
|
||||
]
|
||||
)
|
||||
# If multisig isn't an option, then the sig type selection is skipped altogether
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView),
|
||||
]
|
||||
)
|
||||
|
||||
# test that taproot is not an option via exception raised when choice is taproot
|
||||
with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e:
|
||||
@@ -295,6 +355,37 @@ class TestSeedFlows(FlowTest):
|
||||
)
|
||||
|
||||
|
||||
def test_export_xpub_electrum_seed_flow(self):
|
||||
"""
|
||||
Electrum seeds should skip script type selection
|
||||
"""
|
||||
# Load a finalized Seed into the Controller
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
self.controller.storage.set_pending_seed(ElectrumSeed("regular reject rare profit once math fringe chase until ketchup century escape".split()))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
# Make sure all options are enabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in SettingsConstants.ALL_SIG_TYPES])
|
||||
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in SettingsConstants.ALL_SCRIPT_TYPES])
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in SettingsConstants.ALL_COORDINATORS])
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=seed_views.SeedExportXpubSigTypeView.SINGLE_SIG),
|
||||
|
||||
# Skips past the script type options via redirect
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0]),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
FlowStep(MainMenuView),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_discard_seed_flow(self):
|
||||
"""
|
||||
Selecting "Discard Seed" from the SeedOptionsView should enter the Discard Seed flow and
|
||||
|
||||
@@ -39,8 +39,6 @@ class TestToolsFlows(FlowTest):
|
||||
Finalizing a seed during the Address Explorer flow should return to the next
|
||||
Address Explorer step upon completion.
|
||||
"""
|
||||
controller = Controller.get_instance()
|
||||
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("0000" * 11 + "0003")
|
||||
|
||||
@@ -55,11 +53,11 @@ class TestToolsFlows(FlowTest):
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView),
|
||||
])
|
||||
|
||||
assert controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER
|
||||
assert self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER
|
||||
|
||||
# Reset
|
||||
controller.storage.seeds.clear()
|
||||
controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
|
||||
self.controller.storage.seeds.clear()
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
|
||||
|
||||
# Finalize the new seed w/passphrase
|
||||
self.run_sequence(
|
||||
@@ -73,6 +71,39 @@ class TestToolsFlows(FlowTest):
|
||||
)
|
||||
|
||||
|
||||
def test__address_explorer__load_electrum_seed__sideflow(self):
|
||||
"""
|
||||
Loading an Electrum seed during the Address Explorer flow should return to
|
||||
the Address Explorer flow upon completion, skip the script type selection,
|
||||
and successfully generate receive or change addresses.
|
||||
"""
|
||||
self.settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
sequence = [
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.ADDRESS_EXPLORER),
|
||||
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.TYPE_ELECTRUM),
|
||||
FlowStep(seed_views.SeedElectrumMnemonicStartView),
|
||||
]
|
||||
|
||||
# Load an Electrum mnemonic during the flow (same one used in test_seed.py)
|
||||
for word in "regular reject rare profit once math fringe chase until ketchup century escape".split():
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word),
|
||||
]
|
||||
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView),
|
||||
]
|
||||
|
||||
self.run_sequence(sequence)
|
||||
|
||||
|
||||
|
||||
def test__address_explorer__scan_wrong_qrtype__flow(self):
|
||||
"""
|
||||
Scanning the wrong type of QR code when a SeedQR is expected should route to ErrorView
|
||||
|
||||
+41
-1
@@ -1,8 +1,10 @@
|
||||
from seedsigner.models.seed import Seed, ElectrumSeed
|
||||
import pytest
|
||||
from seedsigner.models.seed import InvalidSeedException, Seed, ElectrumSeed
|
||||
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
|
||||
# TODO: Change TAB indents to SPACE
|
||||
|
||||
def test_seed():
|
||||
seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split())
|
||||
@@ -37,9 +39,47 @@ def test_seed():
|
||||
|
||||
|
||||
def test_electrum_seed():
|
||||
"""
|
||||
ElectrumSeed should correctly parse a modern Electrum mnemonic.
|
||||
"""
|
||||
seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split())
|
||||
|
||||
intended_seed = b'\xcan|\xf8\x8a\x8d\xf78=Pq\xc4_\xe6\x02\x91\xfcs\xb2[\xed*\xdc\xc7%\xb6[_-(~D\xe5\x1e\x85%N\x9c\x03\x9dh\xafX}\x16\xb1\x99,\xbe\xc4\x11\xfaW\x0f\xb0\x89yD\xf4\x0f\xd5?\x8eA'
|
||||
|
||||
assert seed.seed_bytes == intended_seed
|
||||
|
||||
|
||||
def test_electrum_mnemonic_format():
|
||||
"""
|
||||
ElectrumSeed should reject mnemonics that are not 12 words long.
|
||||
"""
|
||||
with pytest.raises(InvalidSeedException):
|
||||
ElectrumSeed(mnemonic=["regular"] * 11)
|
||||
|
||||
with pytest.raises(InvalidSeedException):
|
||||
ElectrumSeed(mnemonic=["regular"] * 13)
|
||||
|
||||
with pytest.raises(InvalidSeedException):
|
||||
ElectrumSeed(mnemonic=["regular"] * 24)
|
||||
|
||||
|
||||
def test_electrum_seed_rejects_most_bip39_mnemonics():
|
||||
"""
|
||||
ElectrumSeed should throw an exception for most BIP-39 mnemonics.
|
||||
|
||||
There are 1/16 odds that a seed will be valid for both formats.
|
||||
"""
|
||||
# Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide
|
||||
with pytest.raises(InvalidSeedException):
|
||||
ElectrumSeed(mnemonic="pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split())
|
||||
|
||||
with pytest.raises(InvalidSeedException):
|
||||
ElectrumSeed(mnemonic="gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split())
|
||||
|
||||
with pytest.raises(InvalidSeedException):
|
||||
ElectrumSeed(mnemonic="enough board blossom stamp fire buffalo digital solution sadness random number stone".split())
|
||||
|
||||
# This one is valid for both formats
|
||||
mnemonic = "only gain spot output unknown craft simple cram absorb suggest ridge famous".split()
|
||||
Seed(mnemonic)
|
||||
ElectrumSeed(mnemonic)
|
||||
|
||||
@@ -14,12 +14,11 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
|
||||
assert len(mnemonic) == mnemonic_length
|
||||
|
||||
if qr_type == QRType.SEED__SEEDQR:
|
||||
e = SeedQrEncoder(seed=Seed(mnemonic))
|
||||
e = SeedQrEncoder(mnemonic=mnemonic)
|
||||
elif qr_type == QRType.SEED__COMPACTSEEDQR:
|
||||
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
|
||||
e = CompactSeedQrEncoder(mnemonic=mnemonic)
|
||||
|
||||
data = e.next_part()
|
||||
print(data)
|
||||
|
||||
qr = QR()
|
||||
image = qr.qrimage(
|
||||
|
||||
Reference in New Issue
Block a user