From c1d320fa1976ac3c23de94872aa11ab6e99dd415 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Mon, 4 Dec 2023 13:48:07 -0600 Subject: [PATCH 01/19] Add limited support for electrum segwit seeds --- src/seedsigner/gui/screens/seed_screens.py | 15 ++++++ src/seedsigner/helpers/embit_utils.py | 6 ++- src/seedsigner/models/seed.py | 49 +++++++++++++++++++- src/seedsigner/models/settings_definition.py | 5 ++ src/seedsigner/views/seed_views.py | 38 ++++++++++++--- src/seedsigner/views/tools_views.py | 1 + tests/test_seed.py | 25 ++++++++-- 7 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index e1b88db09..044d3d124 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -427,6 +427,21 @@ def __post_init__(self): ) self.components.append(self.fingerprint_icontl) +class SeedSwitchElectrumModeScreen(ButtonListScreen): + button_data: list = None + + def __post_init__(self): + self.show_back_button = False + self.title = "Switch to Electrum?" + self.is_bottom_list: bool = True + + super().__post_init__() + + self.components.append(TextArea( + text="It appears this is an Electrum-style seed, switch it to be so? Some functions are not supported for Electrum seeds", + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, + is_text_centered=True, + )) @dataclass diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index da8f827ac..327a564f0 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -19,7 +19,7 @@ # TODO: PR these directly into `embit`? Or replace with new/existing methods already in `embit`? -def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, script_type: str = SettingsConstants.NATIVE_SEGWIT) -> str: +def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, script_type: str = SettingsConstants.NATIVE_SEGWIT, is_electrum : bool = False) -> str: if network == SettingsConstants.MAINNET: network_path = "0'" elif network == SettingsConstants.TESTNET: @@ -30,6 +30,8 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle raise Exception("Unexpected network") if wallet_type == SettingsConstants.SINGLE_SIG: + if is_electrum: + return f"m/0h" if script_type == SettingsConstants.NATIVE_SEGWIT: return f"m/84'/{network_path}/0'" elif script_type == SettingsConstants.NESTED_SEGWIT: @@ -40,6 +42,8 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle raise Exception("Unexpected script type") elif wallet_type == SettingsConstants.MULTISIG: + if is_electrum: + return f"m/1h" if script_type == SettingsConstants.NATIVE_SEGWIT: return f"m/48'/{network_path}/0'/2'" elif script_type == SettingsConstants.NESTED_SEGWIT: diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 51eb85553..e84111413 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -1,4 +1,6 @@ import unicodedata +import hashlib +import hmac from binascii import hexlify from embit import bip39, bip32, bip85 @@ -25,9 +27,11 @@ def __init__(self, self._mnemonic: List[str] = unicodedata.normalize("NFKD", " ".join(mnemonic).strip()).split() self._passphrase: str = "" + self._is_electrum = False self.set_passphrase(passphrase, regenerate_seed=False) self.seed_bytes: bytes = None + self.electrum_seed_bytes: bytes = None self._generate_seed() @@ -42,11 +46,42 @@ def get_wordlist(wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUA def _generate_seed(self) -> bool: try: - self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) + self._generate_electrum_seed() + if not self.is_electrum: + # only do regular bip39 if not already confirmed this seed is_electrum (i.e. during passphrase change) + self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) except Exception as e: print(repr(e)) - raise InvalidSeedException(repr(e)) + # only re-raise if we didn't get valid electrum seed + # we can raise in the electrum confirmation if they didn't mean to enter an electrum seed + if not self.electrum_seed_bytes: + raise InvalidSeedException(repr(e)) + + def _generate_electrum_seed(self) -> bool: + if len(self._mnemonic) != 12: + return False + s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex() + if s[0] != '0' and s[0] != '1': + return False + length = int(s[0]) + 2 + prefix = s[0:length]; + # only support Electrum Segwit version for now + if SettingsConstants.ELECTRUM_SEED_SEGWIT == prefix: + self.electrum_seed_bytes=hashlib.pbkdf2_hmac('sha512', self.mnemonic_str.encode('utf-8'), b'electrum' + self._passphrase.encode('utf-8'), iterations = SettingsConstants.ELECTRUM_PBKDF2_ROUNDS) + if self.is_electrum: + # if already is_electrum (passphrase change), make sure to set main seed_bytes + self.seed_bytes = self.electrum_seed_bytes + return True + else: + return False + + def switch_to_electrum(self): + self.seed_bytes = self.electrum_seed_bytes + self._is_electrum = True + @property + def is_electrum(self) -> bool: + return self._is_electrum @property def mnemonic_str(self) -> str: @@ -81,6 +116,8 @@ def passphrase_display(self): def set_passphrase(self, passphrase: str, regenerate_seed: bool = True): if passphrase: self._passphrase = unicodedata.normalize("NFKD", passphrase) + if self.is_electrum: + self._passphrase = Seed.normalize_electrum_passphrase(passphrase) else: # Passphrase must always have a string value, even if it's just the empty # string. @@ -90,6 +127,14 @@ def set_passphrase(self, passphrase: str, regenerate_seed: bool = True): # 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) + # lower + passphrase = passphrase.lower() + # normalize whitespaces + passphrase = u' '.join(passphrase.split()) + return passphrase @property def wordlist(self) -> List[str]: diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 76534cecd..ac31f0f85 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -198,6 +198,11 @@ def map_network_to_embit(cls, network) -> str: TYPE__ENABLED_DISABLED_PROMPT_REQUIRED, ] + # Electrum seed constants + ELECTRUM_SEED_LEGACY = "01" + ELECTRUM_SEED_SEGWIT = "100" + ELECTRUM_SEED_2FA = "101" + ELECTRUM_PBKDF2_ROUNDS=2048 @dataclass class SettingsEntry: diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 15c3fde97..5bc62ad57 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -289,6 +289,9 @@ def run(self): class SeedFinalizeView(View): FINALIZE = "Done" PASSPHRASE = "BIP-39 Passphrase" + CUSTOM_EXTENSION = "Custom Extension" + SWITCH = "Switch" + CANCEL = "Cancel" def __init__(self): super().__init__() @@ -297,9 +300,26 @@ def __init__(self): def run(self): + if self.seed.electrum_seed_bytes: + # see if user wants to enter electrum mode since it's a valid electrum seed + button_data = [self.SWITCH, self.CANCEL] + selected_menu_num = self.run_screen( + seed_screens.SeedSwitchElectrumModeScreen, + button_data=button_data + ) + if button_data[selected_menu_num] == self.SWITCH: + self.seed.switch_to_electrum() + # recalculate fingerprint + self.fingerprint = self.seed.get_fingerprint(network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)) + elif not self.seed.seed_bytes: + # we only got here because it was a valid electrum seed + # thus if user didn't mean to enter an electrum seed, it's invalid + raise InvalidSeedException(f"Invalid seed entered") + button_data = [self.FINALIZE] + passphrase_button = self.CUSTOM_EXTENSION if self.seed.is_electrum else self.PASSPHRASE if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED: - button_data.append(self.PASSPHRASE) + button_data.append(passphrase_button) selected_menu_num = self.run_screen( seed_screens.SeedFinalizeScreen, @@ -311,7 +331,7 @@ def run(self): seed_num = self.controller.storage.finalize_pending_seed() return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True) - elif button_data[selected_menu_num] == self.PASSPHRASE: + elif button_data[selected_menu_num] == passphrase_button: return Destination(SeedAddPassphraseView) @@ -323,7 +343,8 @@ def __init__(self): def run(self): - ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase) + passphrase_title="Custom Extension" if self.seed.is_electrum else "BIP-39 Passphrase" + ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase, title=passphrase_title) if ret == RET_CODE__BACK_BUTTON: return Destination(BackStackView) @@ -612,7 +633,9 @@ def __init__(self, seed_num: int, sig_type: str): def run(self): from .tools_views import ToolsAddressExplorerAddressTypeView args = {"seed_num": self.seed_num, "sig_type": self.sig_type} - if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1: + seed = self.controller.storage.seeds[self.seed_num] + script_types = [SettingsConstants.NATIVE_SEGWIT] if seed.is_electrum else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) + if len(script_types) == 1: # Nothing to select; skip this screen args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0] @@ -629,6 +652,8 @@ def run(self): button_data = [] for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES): button_data.append(script_type) + if seed.is_electrum: + button_data = [SettingsConstants.NATIVE_SEGWIT] selected_menu_num = self.run_screen( ButtonListScreen, @@ -799,7 +824,8 @@ def run(self): derivation_path = embit_utils.get_standard_derivation_path( network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), wallet_type=self.sig_type, - script_type=self.script_type + script_type=self.script_type, + is_electrum=self.seed.is_electrum ) if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__DISABLED: @@ -1299,7 +1325,7 @@ def run(self): num_modules_standard = 29 num_modules_compact = 25 - if self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED: + if seed.is_electrum or self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED: # Only configured for standard SeedQR return Destination( SeedTranscribeSeedQRWarningView, diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 12a4493b3..6c71637d3 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -534,6 +534,7 @@ def __init__(self, seed_num: int = None, script_type: str = None, custom_derivat network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), wallet_type=SettingsConstants.SINGLE_SIG, script_type=self.script_type, + is_electrum = self.seed.is_electrum ) data["derivation_path"] = derivation_path diff --git a/tests/test_seed.py b/tests/test_seed.py index a7e082eee..8761e12cb 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -36,6 +36,25 @@ def test_seed(): # assert seed.passphrase == "test" - - - \ No newline at end of file +def test_electrum_seed(): + seed = Seed(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 not seed.seed_bytes + + assert seed.electrum_seed_bytes == intended_seed + + seed.switch_to_electrum() + + assert seed.seed_bytes == intended_seed + + assert seed.is_electrum + +def test_seed_bip39_electrum_both_valid(): + seed = Seed(mnemonic="frog cricket convince battle film mistake survey normal frequent magnet park cheap".split()) + + assert seed.seed_bytes == b'\x05-\x0c\xc2\x97\xfbw\x17b}U[\x80o\x94\x9c\xaa\x0f\xfc\xb2\xac\x08\xa3Q=\xbd\xf6\xcf] I\xfd9\x9a\x7f/\xa3OA\xe21.5^*\xa7e\xcd\xf7$h\x04\x02\xf6\xdf1\xc6\xa9{\x8c\xcc\xcc\xce)' + + assert seed.electrum_seed_bytes == b'<\x03y\xb7\xd6Q\x80I\xf5n\x8e\x9ek\x03!\xea\xbf\xe1\xc4f\x04\xcd\xde\x15\xe9\xfcG\xdd\xf3\x86\xc3\xc4R\xa1\xf0\xeb\xeb\x1f\'\xdd\x84\x9b\xa8\\\xd5\xfc\x9f\xd4q<\xd8\x0b\xb4\x04\x03\x9b8\x937\xb3B\x1e"\xf2' + From 89f6cec897440ecfba0efbbbad339b6043497927 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Mon, 18 Dec 2023 07:12:03 -0600 Subject: [PATCH 02/19] making suggested changes for electrum derivation path and removing one unnecessary is_electrum check --- src/seedsigner/helpers/embit_utils.py | 8 ++++---- src/seedsigner/views/seed_views.py | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index 327a564f0..754d5867d 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -30,9 +30,9 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle raise Exception("Unexpected network") if wallet_type == SettingsConstants.SINGLE_SIG: - if is_electrum: - return f"m/0h" if script_type == SettingsConstants.NATIVE_SEGWIT: + if is_electrum: + return f"m/0h" return f"m/84'/{network_path}/0'" elif script_type == SettingsConstants.NESTED_SEGWIT: return f"m/49'/{network_path}/0'" @@ -42,9 +42,9 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle raise Exception("Unexpected script type") elif wallet_type == SettingsConstants.MULTISIG: - if is_electrum: - return f"m/1h" if script_type == SettingsConstants.NATIVE_SEGWIT: + if is_electrum: + return f"m/1h" return f"m/48'/{network_path}/0'/2'" elif script_type == SettingsConstants.NESTED_SEGWIT: return f"m/48'/{network_path}/0'/1'" diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 5bc62ad57..6580742c5 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -652,8 +652,6 @@ def run(self): button_data = [] for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES): button_data.append(script_type) - if seed.is_electrum: - button_data = [SettingsConstants.NATIVE_SEGWIT] selected_menu_num = self.run_screen( ButtonListScreen, From eaa29ab339ef3870c4633d3aa13b1ee990da19e4 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Mon, 18 Dec 2023 08:57:37 -0600 Subject: [PATCH 03/19] derivation path override to make address verifiction work for electrum seeds --- src/seedsigner/views/seed_views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 6580742c5..5f0b6b9a9 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1674,6 +1674,9 @@ def __init__(self, seed_num: int = None): else: self.seed = None self.address = self.controller.unverified_address["address"] + # override derivation path if it's electrum as bip-39 path was set by default + if self.seed and self.seed.is_electrum: + self.controller.unverified_address["derivation_path"] = 'm/0h' self.derivation_path = self.controller.unverified_address["derivation_path"] self.script_type = self.controller.unverified_address["script_type"] self.sig_type = self.controller.unverified_address["sig_type"] From 1f0a4dda7e9306574d2c7b2733f5891fea2e7740 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Mon, 18 Dec 2023 14:12:09 -0600 Subject: [PATCH 04/19] since EncodeQR generates the seed internally for xpub export, we have to pass the is_electrum status to it to export xpub for electrum seeds properly --- src/seedsigner/models/encode_qr.py | 14 ++++++++++---- src/seedsigner/views/seed_views.py | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 6efa715db..1f3b208f5 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -40,6 +40,7 @@ class EncodeQR: wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH bitcoin_address: str = None signed_message: str = None + is_electrum : bool = False def __post_init__(self): self.qr = QR() @@ -66,7 +67,8 @@ def __post_init__(self): passphrase=self.passphrase, derivation=self.derivation, network=self.network, - wordlist_language_code=self.wordlist_language_code + wordlist_language_code=self.wordlist_language_code, + is_electrum=self.is_electrum ) elif self.qr_type == QRType.XPUB__UR: @@ -76,7 +78,8 @@ def __post_init__(self): passphrase=self.passphrase, derivation=self.derivation, network=self.network, - wordlist_language_code=self.wordlist_language_code + wordlist_language_code=self.wordlist_language_code, + is_electrum=self.is_electrum ) elif self.qr_type == QRType.XPUB__SPECTER: @@ -86,7 +89,8 @@ def __post_init__(self): passphrase=self.passphrase, derivation=self.derivation, network=self.network, - wordlist_language_code=self.wordlist_language_code + wordlist_language_code=self.wordlist_language_code, + is_electrum=self.is_electrum ) @@ -356,7 +360,7 @@ def next_part(self): class XpubQrEncoder(BaseQrEncoder): - def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code): + def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code, is_electrum : bool = False): self.seed_phrase = seed_phrase self.passphrase = passphrase self.derivation = derivation @@ -373,6 +377,8 @@ def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_langua self.seed = Seed(mnemonic=self.seed_phrase, passphrase=self.passphrase, wordlist_language_code=wordlist_language_code) + if is_electrum: + self.seed.switch_to_electrum() self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.xprv = self.root.derive(self.derivation) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 5f0b6b9a9..295a9bf60 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -908,7 +908,8 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str): network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), qr_type=qr_type, qr_density=qr_density, - wordlist_language_code=self.seed.wordlist_language_code + wordlist_language_code=self.seed.wordlist_language_code, + is_electrum=self.seed.is_electrum ) From 289b382720bfce4bcdf2374c691657bba7209b60 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Tue, 19 Dec 2023 11:10:08 -0600 Subject: [PATCH 05/19] pass seed to EncodeQR instead of mnemonic and passphrase to simplify --- src/seedsigner/models/encode_qr.py | 30 ++++++++---------------------- src/seedsigner/views/seed_views.py | 8 +++----- tests/test_encodepsbtqr.py | 14 +++++++++----- tests/test_seedqr.py | 5 ++++- 4 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 1f3b208f5..0cc283bb5 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -31,8 +31,7 @@ class EncodeQR: # Dataclass input vars on __init__() psbt: PSBT = None - seed_phrase: List[str] = None - passphrase: str = None + seed: Seed = None derivation: str = None network: str = SettingsConstants.MAINNET qr_type: str = None @@ -40,7 +39,6 @@ class EncodeQR: wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH bitcoin_address: str = None signed_message: str = None - is_electrum : bool = False def __post_init__(self): self.qr = QR() @@ -63,44 +61,38 @@ def __post_init__(self): # XPUB formats elif self.qr_type == QRType.XPUB: self.encoder = XpubQrEncoder( - seed_phrase=self.seed_phrase, - passphrase=self.passphrase, + seed=self.seed, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum ) elif self.qr_type == QRType.XPUB__UR: self.encoder = UrXpubQrEncoder( qr_density=self.qr_density, - seed_phrase=self.seed_phrase, - passphrase=self.passphrase, + seed=self.seed, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum ) elif self.qr_type == QRType.XPUB__SPECTER: self.encoder = SpecterXPubQrEncoder( qr_density=self.qr_density, - seed_phrase=self.seed_phrase, - passphrase=self.passphrase, + seed=self.seed, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum ) # SeedQR formats elif self.qr_type == QRType.SEED__SEEDQR: - self.encoder = SeedQrEncoder(seed_phrase=self.seed_phrase, + self.encoder = SeedQrEncoder(seed_phrase=self.seed.mnemonic_list, wordlist_language_code=self.wordlist_language_code) elif self.qr_type == QRType.SEED__COMPACTSEEDQR: - self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase, + self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed.mnemonic_list, wordlist_language_code=self.wordlist_language_code) # Misc formats @@ -360,9 +352,8 @@ def next_part(self): class XpubQrEncoder(BaseQrEncoder): - def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code, is_electrum : bool = False): - self.seed_phrase = seed_phrase - self.passphrase = passphrase + def __init__(self, seed, derivation, network, wordlist_language_code): + self.seed = seed self.derivation = derivation self.network = network self.wordlist = Seed.get_wordlist(wordlist_language_code) @@ -374,11 +365,6 @@ def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_langua raise Exception('Wordlist Required') version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) - self.seed = Seed(mnemonic=self.seed_phrase, - passphrase=self.passphrase, - wordlist_language_code=wordlist_language_code) - if is_electrum: - self.seed.switch_to_electrum() self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.xprv = self.root.derive(self.derivation) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 295a9bf60..8e394ace2 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -902,14 +902,12 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str): qr_type = QRType.XPUB__UR self.qr_encoder = EncodeQR( - seed_phrase=self.seed.mnemonic_list, - passphrase=self.seed.passphrase, + seed=self.seed, derivation=derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), qr_type=qr_type, qr_density=qr_density, wordlist_language_code=self.seed.wordlist_language_code, - is_electrum=self.seed.is_electrum ) @@ -1412,7 +1410,7 @@ def __init__(self, seed_num: int, seedqr_format: str, num_modules: int): def run(self): e = EncodeQR( - seed_phrase=self.seed.mnemonic_list, + seed=self.seed, qr_type=self.seedqr_format, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) ) @@ -1447,7 +1445,7 @@ def __init__(self, seed_num: int, seedqr_format: str): def run(self): e = EncodeQR( - seed_phrase=self.seed.mnemonic_list, + seed=self.seed, qr_type=self.seedqr_format, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) ) diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 75ed796fb..e18cd968e 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -4,6 +4,7 @@ from binascii import a2b_base64 from seedsigner.models.settings import SettingsConstants +from seedsigner.models.seed import Seed @@ -62,8 +63,9 @@ def test_specter_qr_encode(): def test_seedsigner_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + seed = Seed(mnemonic.split()) - e = EncodeQR(seed_phrase=mnemonic.split(), qr_type=QRType.SEED__SEEDQR) + e = EncodeQR(seed=seed, qr_type=QRType.SEED__SEEDQR) print(e.next_part()) @@ -73,8 +75,9 @@ def test_seedsigner_qr(): def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + seed = Seed(mnemonic.split(), passphrase="pass") - e = EncodeQR(seed_phrase=mnemonic.split(), passphrase="pass", qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") + e = EncodeQR(seed=seed, qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5" @@ -82,8 +85,9 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + seed = Seed(mnemonic.split(), passphrase="pass") - e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUB__SPECTER, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = EncodeQR(seed=seed, qr_type=QRType.XPUB__SPECTER, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" @@ -94,10 +98,10 @@ def test_specter_xpub_qr(): def test_ur_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + seed = Seed(mnemonic.split(), passphrase="pass") e = EncodeQR( - seed_phrase=mnemonic.split(), - passphrase="pass", + seed=seed, qr_type=QRType.XPUB__UR, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 7e2d63856..6a2f7c944 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -7,6 +7,7 @@ from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.qr_type import QRType from seedsigner.models.settings import SettingsConstants +from seedsigner.models.seed import Seed @@ -16,8 +17,10 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): seed_phrase = bip39.mnemonic_from_bytes(entropy).split() print(seed_phrase) assert len(seed_phrase) == mnemonic_length + seed = Seed(seed_phrase) - e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type) + + e = EncodeQR(seed=seed, qr_type=qr_type) data = e.next_part() print(data) From 656110ba461a2d70ae5e2063165ddcb0675724aa Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Tue, 19 Dec 2023 12:08:39 -0600 Subject: [PATCH 06/19] Revert "pass seed to EncodeQR instead of mnemonic and passphrase to simplify" This reverts commit 289b382720bfce4bcdf2374c691657bba7209b60. --- src/seedsigner/models/encode_qr.py | 30 ++++++++++++++++++++++-------- src/seedsigner/views/seed_views.py | 8 +++++--- tests/test_encodepsbtqr.py | 14 +++++--------- tests/test_seedqr.py | 5 +---- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 0cc283bb5..1f3b208f5 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -31,7 +31,8 @@ class EncodeQR: # Dataclass input vars on __init__() psbt: PSBT = None - seed: Seed = None + seed_phrase: List[str] = None + passphrase: str = None derivation: str = None network: str = SettingsConstants.MAINNET qr_type: str = None @@ -39,6 +40,7 @@ class EncodeQR: wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH bitcoin_address: str = None signed_message: str = None + is_electrum : bool = False def __post_init__(self): self.qr = QR() @@ -61,38 +63,44 @@ def __post_init__(self): # XPUB formats elif self.qr_type == QRType.XPUB: self.encoder = XpubQrEncoder( - seed=self.seed, + seed_phrase=self.seed_phrase, + passphrase=self.passphrase, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, + is_electrum=self.is_electrum ) elif self.qr_type == QRType.XPUB__UR: self.encoder = UrXpubQrEncoder( qr_density=self.qr_density, - seed=self.seed, + seed_phrase=self.seed_phrase, + passphrase=self.passphrase, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, + is_electrum=self.is_electrum ) elif self.qr_type == QRType.XPUB__SPECTER: self.encoder = SpecterXPubQrEncoder( qr_density=self.qr_density, - seed=self.seed, + seed_phrase=self.seed_phrase, + passphrase=self.passphrase, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, + is_electrum=self.is_electrum ) # SeedQR formats elif self.qr_type == QRType.SEED__SEEDQR: - self.encoder = SeedQrEncoder(seed_phrase=self.seed.mnemonic_list, + self.encoder = SeedQrEncoder(seed_phrase=self.seed_phrase, wordlist_language_code=self.wordlist_language_code) elif self.qr_type == QRType.SEED__COMPACTSEEDQR: - self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed.mnemonic_list, + self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase, wordlist_language_code=self.wordlist_language_code) # Misc formats @@ -352,8 +360,9 @@ def next_part(self): class XpubQrEncoder(BaseQrEncoder): - def __init__(self, seed, derivation, network, wordlist_language_code): - self.seed = seed + def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code, is_electrum : bool = False): + self.seed_phrase = seed_phrase + self.passphrase = passphrase self.derivation = derivation self.network = network self.wordlist = Seed.get_wordlist(wordlist_language_code) @@ -365,6 +374,11 @@ def __init__(self, seed, derivation, network, wordlist_language_code): raise Exception('Wordlist Required') version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) + self.seed = Seed(mnemonic=self.seed_phrase, + passphrase=self.passphrase, + wordlist_language_code=wordlist_language_code) + if is_electrum: + self.seed.switch_to_electrum() self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.xprv = self.root.derive(self.derivation) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 8e394ace2..295a9bf60 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -902,12 +902,14 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str): qr_type = QRType.XPUB__UR self.qr_encoder = EncodeQR( - seed=self.seed, + seed_phrase=self.seed.mnemonic_list, + passphrase=self.seed.passphrase, derivation=derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), qr_type=qr_type, qr_density=qr_density, wordlist_language_code=self.seed.wordlist_language_code, + is_electrum=self.seed.is_electrum ) @@ -1410,7 +1412,7 @@ def __init__(self, seed_num: int, seedqr_format: str, num_modules: int): def run(self): e = EncodeQR( - seed=self.seed, + seed_phrase=self.seed.mnemonic_list, qr_type=self.seedqr_format, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) ) @@ -1445,7 +1447,7 @@ def __init__(self, seed_num: int, seedqr_format: str): def run(self): e = EncodeQR( - seed=self.seed, + seed_phrase=self.seed.mnemonic_list, qr_type=self.seedqr_format, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) ) diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index e18cd968e..75ed796fb 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -4,7 +4,6 @@ from binascii import a2b_base64 from seedsigner.models.settings import SettingsConstants -from seedsigner.models.seed import Seed @@ -63,9 +62,8 @@ def test_specter_qr_encode(): def test_seedsigner_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - seed = Seed(mnemonic.split()) - e = EncodeQR(seed=seed, qr_type=QRType.SEED__SEEDQR) + e = EncodeQR(seed_phrase=mnemonic.split(), qr_type=QRType.SEED__SEEDQR) print(e.next_part()) @@ -75,9 +73,8 @@ def test_seedsigner_qr(): def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - seed = Seed(mnemonic.split(), passphrase="pass") - e = EncodeQR(seed=seed, qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") + e = EncodeQR(seed_phrase=mnemonic.split(), passphrase="pass", qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5" @@ -85,9 +82,8 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - seed = Seed(mnemonic.split(), passphrase="pass") - e = EncodeQR(seed=seed, qr_type=QRType.XPUB__SPECTER, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUB__SPECTER, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" @@ -98,10 +94,10 @@ def test_specter_xpub_qr(): def test_ur_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - seed = Seed(mnemonic.split(), passphrase="pass") e = EncodeQR( - seed=seed, + seed_phrase=mnemonic.split(), + passphrase="pass", qr_type=QRType.XPUB__UR, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 6a2f7c944..7e2d63856 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -7,7 +7,6 @@ from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.qr_type import QRType from seedsigner.models.settings import SettingsConstants -from seedsigner.models.seed import Seed @@ -17,10 +16,8 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): seed_phrase = bip39.mnemonic_from_bytes(entropy).split() print(seed_phrase) assert len(seed_phrase) == mnemonic_length - seed = Seed(seed_phrase) - - e = EncodeQR(seed=seed, qr_type=qr_type) + e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type) data = e.next_part() print(data) From f8aca10107ebebc10cc8bbc573284e3a6bb53e17 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Sun, 24 Dec 2023 15:49:39 -0600 Subject: [PATCH 07/19] export correct xpub version bytes for electrum seeds --- src/seedsigner/helpers/embit_utils.py | 6 ++++++ src/seedsigner/models/encode_qr.py | 15 ++++++++++----- src/seedsigner/views/seed_views.py | 15 +++++++++------ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index 754d5867d..5d9232f14 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -56,6 +56,12 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle raise Exception("Unexpected wallet type") # checks that all inputs are from the same wallet +def detect_version(derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, is_electrum : bool = False) -> str: + embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] + if is_electrum: + return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"] + else: + return embit.bip32.detect_version(derivation_path, default="xpub", network=embit_network) def get_xpub(seed_bytes, derivation_path: str, embit_network: str = "main") -> HDKey: root = bip32.HDKey.from_seed(seed_bytes, version=NETWORKS[embit_network]["xprv"]) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 1f3b208f5..1baff2145 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -11,6 +11,7 @@ from seedsigner.helpers.ur2.ur_encoder import UREncoder from seedsigner.helpers.ur2.ur import UR from seedsigner.helpers.qr import QR +from seedsigner.helpers import embit_utils from seedsigner.models.qr_type import QRType from seedsigner.models.seed import Seed from seedsigner.models.settings import SettingsConstants @@ -41,6 +42,7 @@ class EncodeQR: bitcoin_address: str = None signed_message: str = None is_electrum : bool = False + sig_type : str = SettingsConstants.SINGLE_SIG def __post_init__(self): self.qr = QR() @@ -68,7 +70,8 @@ def __post_init__(self): derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum + is_electrum=self.is_electrum, + sig_type=self.sig_type ) elif self.qr_type == QRType.XPUB__UR: @@ -79,7 +82,8 @@ def __post_init__(self): derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum + is_electrum=self.is_electrum, + sig_type=self.sig_type ) elif self.qr_type == QRType.XPUB__SPECTER: @@ -90,7 +94,8 @@ def __post_init__(self): derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum + is_electrum=self.is_electrum, + sig_type=self.sig_type ) @@ -360,7 +365,7 @@ def next_part(self): class XpubQrEncoder(BaseQrEncoder): - def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code, is_electrum : bool = False): + def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code, is_electrum : bool = False, sig_type : str = SettingsConstants.SINGLE_SIG): self.seed_phrase = seed_phrase self.passphrase = passphrase self.derivation = derivation @@ -373,12 +378,12 @@ def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_langua if self.wordlist == None: raise Exception('Wordlist Required') - version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) self.seed = Seed(mnemonic=self.seed_phrase, passphrase=self.passphrase, wordlist_language_code=wordlist_language_code) if is_electrum: self.seed.switch_to_electrum() + version = embit_utils.detect_version(self.derivation, network, sig_type, self.seed.is_electrum) self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.xprv = self.root.derive(self.derivation) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 295a9bf60..3d1f46aef 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -838,10 +838,11 @@ def run(self): try: embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))] - version = embit.bip32.detect_version( - derivation_path, - default="xpub", - network=embit_network + version = embit_utils.detect_version( + derivation_path, + self.settings.get_value(SettingsConstants.SETTING__NETWORK), + self.sig_type, + self.seed.is_electrum ) root = embit.bip32.HDKey.from_seed( self.seed.seed_bytes, @@ -869,6 +870,7 @@ def run(self): dict(seed_num=self.seed_num, coordinator=self.coordinator, derivation_path=derivation_path, + sig_type=self.sig_type ) ) @@ -878,7 +880,7 @@ def run(self): class SeedExportXpubQRDisplayView(View): - def __init__(self, seed_num: int, coordinator: str, derivation_path: str): + def __init__(self, seed_num: int, coordinator: str, derivation_path: str, sig_type: str = SettingsConstants.SINGLE_SIG): super().__init__() self.seed = self.controller.get_seed(seed_num) @@ -909,7 +911,8 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str): qr_type=qr_type, qr_density=qr_density, wordlist_language_code=self.seed.wordlist_language_code, - is_electrum=self.seed.is_electrum + is_electrum=self.seed.is_electrum, + sig_type=sig_type ) From 40520f8b35f8b74cc9d972db0b7028684c0a9a68 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Tue, 19 Mar 2024 11:24:57 -0500 Subject: [PATCH 08/19] integrating PR review recommendations for electrum seed support --- src/seedsigner/gui/screens/seed_screens.py | 15 --- src/seedsigner/helpers/embit_utils.py | 12 +- src/seedsigner/models/encode_qr.py | 32 ++--- src/seedsigner/models/seed.py | 123 ++++++++++++------- src/seedsigner/models/seed_storage.py | 17 ++- src/seedsigner/models/settings_definition.py | 14 ++- src/seedsigner/views/psbt_views.py | 8 ++ src/seedsigner/views/seed_views.py | 78 ++++++------ src/seedsigner/views/tools_views.py | 12 +- tests/test_encodepsbtqr.py | 10 +- tests/test_flows_psbt.py | 3 +- tests/test_flows_seed.py | 2 +- tests/test_flows_tools.py | 4 +- tests/test_seed.py | 19 +-- tests/test_seedqr.py | 3 +- 15 files changed, 184 insertions(+), 168 deletions(-) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 2c1d0c13e..9fe35c1c7 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -427,21 +427,6 @@ def __post_init__(self): ) self.components.append(self.fingerprint_icontl) -class SeedSwitchElectrumModeScreen(ButtonListScreen): - button_data: list = None - - def __post_init__(self): - self.show_back_button = False - self.title = "Switch to Electrum?" - self.is_bottom_list: bool = True - - super().__post_init__() - - self.components.append(TextArea( - text="It appears this is an Electrum-style seed, switch it to be so? Some functions are not supported for Electrum seeds", - screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, - is_text_centered=True, - )) @dataclass diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index 5d9232f14..da8f827ac 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -19,7 +19,7 @@ # TODO: PR these directly into `embit`? Or replace with new/existing methods already in `embit`? -def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, script_type: str = SettingsConstants.NATIVE_SEGWIT, is_electrum : bool = False) -> str: +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'" elif network == SettingsConstants.TESTNET: @@ -31,8 +31,6 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle if wallet_type == SettingsConstants.SINGLE_SIG: if script_type == SettingsConstants.NATIVE_SEGWIT: - if is_electrum: - return f"m/0h" return f"m/84'/{network_path}/0'" elif script_type == SettingsConstants.NESTED_SEGWIT: return f"m/49'/{network_path}/0'" @@ -43,8 +41,6 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle elif wallet_type == SettingsConstants.MULTISIG: if script_type == SettingsConstants.NATIVE_SEGWIT: - if is_electrum: - return f"m/1h" return f"m/48'/{network_path}/0'/2'" elif script_type == SettingsConstants.NESTED_SEGWIT: return f"m/48'/{network_path}/0'/1'" @@ -56,12 +52,6 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle raise Exception("Unexpected wallet type") # checks that all inputs are from the same wallet -def detect_version(derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, is_electrum : bool = False) -> str: - embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] - if is_electrum: - return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"] - else: - return embit.bip32.detect_version(derivation_path, default="xpub", network=embit_network) def get_xpub(seed_bytes, derivation_path: str, embit_network: str = "main") -> HDKey: root = bip32.HDKey.from_seed(seed_bytes, version=NETWORKS[embit_network]["xprv"]) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 1baff2145..e929c6796 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -32,8 +32,7 @@ class EncodeQR: # Dataclass input vars on __init__() psbt: PSBT = None - seed_phrase: List[str] = None - passphrase: str = None + seed: Seed = None derivation: str = None network: str = SettingsConstants.MAINNET qr_type: str = None @@ -41,7 +40,6 @@ class EncodeQR: wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH bitcoin_address: str = None signed_message: str = None - is_electrum : bool = False sig_type : str = SettingsConstants.SINGLE_SIG def __post_init__(self): @@ -65,47 +63,41 @@ def __post_init__(self): # XPUB formats elif self.qr_type == QRType.XPUB: self.encoder = XpubQrEncoder( - seed_phrase=self.seed_phrase, - passphrase=self.passphrase, + seed=self.seed, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum, sig_type=self.sig_type ) elif self.qr_type == QRType.XPUB__UR: self.encoder = UrXpubQrEncoder( qr_density=self.qr_density, - seed_phrase=self.seed_phrase, - passphrase=self.passphrase, + seed=self.seed, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum, sig_type=self.sig_type ) elif self.qr_type == QRType.XPUB__SPECTER: self.encoder = SpecterXPubQrEncoder( qr_density=self.qr_density, - seed_phrase=self.seed_phrase, - passphrase=self.passphrase, + seed=self.seed, derivation=self.derivation, network=self.network, wordlist_language_code=self.wordlist_language_code, - is_electrum=self.is_electrum, sig_type=self.sig_type ) # SeedQR formats elif self.qr_type == QRType.SEED__SEEDQR: - self.encoder = SeedQrEncoder(seed_phrase=self.seed_phrase, + self.encoder = SeedQrEncoder(seed_phrase=self.seed.mnemonic_list, wordlist_language_code=self.wordlist_language_code) elif self.qr_type == QRType.SEED__COMPACTSEEDQR: - self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase, + self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed.mnemonic_list, wordlist_language_code=self.wordlist_language_code) # Misc formats @@ -365,9 +357,8 @@ def next_part(self): class XpubQrEncoder(BaseQrEncoder): - def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code, is_electrum : bool = False, sig_type : str = SettingsConstants.SINGLE_SIG): - self.seed_phrase = seed_phrase - self.passphrase = passphrase + def __init__(self, seed, derivation, network, wordlist_language_code, sig_type : str = SettingsConstants.SINGLE_SIG): + self.seed = seed self.derivation = derivation self.network = network self.wordlist = Seed.get_wordlist(wordlist_language_code) @@ -378,12 +369,7 @@ def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_langua if self.wordlist == None: raise Exception('Wordlist Required') - self.seed = Seed(mnemonic=self.seed_phrase, - passphrase=self.passphrase, - wordlist_language_code=wordlist_language_code) - if is_electrum: - self.seed.switch_to_electrum() - version = embit_utils.detect_version(self.derivation, network, sig_type, self.seed.is_electrum) + version = self.seed.detect_version(self.derivation, network, sig_type) self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.xprv = self.root.derive(self.derivation) diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index e84111413..9dfdc6460 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -27,11 +27,9 @@ def __init__(self, self._mnemonic: List[str] = unicodedata.normalize("NFKD", " ".join(mnemonic).strip()).split() self._passphrase: str = "" - self._is_electrum = False self.set_passphrase(passphrase, regenerate_seed=False) self.seed_bytes: bytes = None - self.electrum_seed_bytes: bytes = None self._generate_seed() @@ -46,42 +44,11 @@ def get_wordlist(wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUA def _generate_seed(self) -> bool: try: - self._generate_electrum_seed() - if not self.is_electrum: - # only do regular bip39 if not already confirmed this seed is_electrum (i.e. during passphrase change) - self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) + self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) except Exception as e: print(repr(e)) - # only re-raise if we didn't get valid electrum seed - # we can raise in the electrum confirmation if they didn't mean to enter an electrum seed - if not self.electrum_seed_bytes: - raise InvalidSeedException(repr(e)) + raise InvalidSeedException(repr(e)) - def _generate_electrum_seed(self) -> bool: - if len(self._mnemonic) != 12: - return False - s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex() - if s[0] != '0' and s[0] != '1': - return False - length = int(s[0]) + 2 - prefix = s[0:length]; - # only support Electrum Segwit version for now - if SettingsConstants.ELECTRUM_SEED_SEGWIT == prefix: - self.electrum_seed_bytes=hashlib.pbkdf2_hmac('sha512', self.mnemonic_str.encode('utf-8'), b'electrum' + self._passphrase.encode('utf-8'), iterations = SettingsConstants.ELECTRUM_PBKDF2_ROUNDS) - if self.is_electrum: - # if already is_electrum (passphrase change), make sure to set main seed_bytes - self.seed_bytes = self.electrum_seed_bytes - return True - else: - return False - - def switch_to_electrum(self): - self.seed_bytes = self.electrum_seed_bytes - self._is_electrum = True - - @property - def is_electrum(self) -> bool: - return self._is_electrum @property def mnemonic_str(self) -> str: @@ -116,8 +83,6 @@ def passphrase_display(self): def set_passphrase(self, passphrase: str, regenerate_seed: bool = True): if passphrase: self._passphrase = unicodedata.normalize("NFKD", passphrase) - if self.is_electrum: - self._passphrase = Seed.normalize_electrum_passphrase(passphrase) else: # Passphrase must always have a string value, even if it's just the empty # string. @@ -127,14 +92,6 @@ def set_passphrase(self, passphrase: str, regenerate_seed: bool = True): # 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) - # lower - passphrase = passphrase.lower() - # normalize whitespaces - passphrase = u' '.join(passphrase.split()) - return passphrase @property def wordlist(self) -> List[str]: @@ -145,6 +102,24 @@ def set_wordlist_language_code(self, language_code: str): # 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: + return None + + def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_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 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"]) @@ -170,3 +145,61 @@ def __eq__(self, other): if isinstance(other, Seed): return self.seed_bytes == other.seed_bytes return False + + + +class ElectrumSeed(Seed): + + + def _generate_seed(self) -> bool: + if len(self._mnemonic) != 12: + return False + 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 + + def set_passphrase(self, passphrase: str, regenerate_seed: bool = True): + if passphrase: + self._passphrase = ElectrumSeed.normalize_electrum_passphrase(passphrase) + else: + # Passphrase must always have a string value, even if it's just the empty + # string. + self._passphrase = "" + + if regenerate_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) + # lower + passphrase = passphrase.lower() + # normalize whitespaces + passphrase = u' '.join(passphrase.split()) + return passphrase + + @property + def script_override(self) -> list: + 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: + embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] + return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"] + + @property + def passphrase_label(self) -> str: + return SettingsConstants.LABEL__CUSTOM_EXTENSION + + @property + def seedqr_supported(self) -> bool: + return False diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index 5b6c30495..85feb49cb 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -1,5 +1,5 @@ from typing import List -from seedsigner.models.seed import Seed, InvalidSeedException +from seedsigner.models.seed import Seed, ElectrumSeed, InvalidSeedException from seedsigner.models.settings_definition import SettingsConstants @@ -9,6 +9,7 @@ def __init__(self) -> None: self.seeds: List[Seed] = [] self.pending_seed: Seed = None self._pending_mnemonic: List[str] = [] + self._pending_is_electrum : bool = False def set_pending_seed(self, seed: Seed): @@ -58,8 +59,9 @@ def pending_mnemonic_length(self) -> int: return len(self._pending_mnemonic) - def init_pending_mnemonic(self, num_words:int = 12): + def init_pending_mnemonic(self, num_words:int = 12, is_electrum:bool = False): self._pending_mnemonic = [None] * num_words + self._pending_is_electrum = is_electrum def update_pending_mnemonic(self, word: str, index: int): @@ -81,16 +83,23 @@ def get_pending_mnemonic_word(self, index: int) -> str: def get_pending_mnemonic_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str: try: - seed = Seed(self._pending_mnemonic) + if self._pending_is_electrum: + seed = ElectrumSeed(self._pending_mnemonic) + else: + seed = Seed(self._pending_mnemonic) return seed.get_fingerprint(network) except InvalidSeedException: return None def convert_pending_mnemonic_to_pending_seed(self): - self.pending_seed = Seed(self._pending_mnemonic) + if self._pending_is_electrum: + self.pending_seed = ElectrumSeed(self._pending_mnemonic) + else: + self.pending_seed = Seed(self._pending_mnemonic) self.discard_pending_mnemonic() def discard_pending_mnemonic(self): self._pending_mnemonic = [] + self._pending_is_electrum = False diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index ac31f0f85..c576e6d0d 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -160,6 +160,7 @@ def map_network_to_embit(cls, network) -> str: SETTING__CAMERA_ROTATION = "camera_rotation" SETTING__COMPACT_SEEDQR = "compact_seedqr" SETTING__BIP85_CHILD_SEEDS = "bip85_child_seeds" + SETTING__ELECTRUM_SEEDS = "electrum_seeds" SETTING__MESSAGE_SIGNING = "message_signing" SETTING__PRIVACY_WARNINGS = "privacy_warnings" SETTING__DIRE_WARNINGS = "dire_warnings" @@ -199,11 +200,15 @@ def map_network_to_embit(cls, network) -> str: ] # Electrum seed constants - ELECTRUM_SEED_LEGACY = "01" + ELECTRUM_SEED_STANDARD = "01" ELECTRUM_SEED_SEGWIT = "100" ELECTRUM_SEED_2FA = "101" ELECTRUM_PBKDF2_ROUNDS=2048 + # Label strings + LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase" + LABEL__CUSTOM_EXTENSION = "Custom Extension" + @dataclass class SettingsEntry: """ @@ -463,6 +468,13 @@ class SettingsDefinition: visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, + attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS, + abbreviated_name="Electrum", + display_name="Electrum seed support", + visibility=SettingsConstants.VISIBILITY__ADVANCED, + default_value=SettingsConstants.OPTION__DISABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING, display_name="Message signing", diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 9c0d9c60f..b639b50a2 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -18,6 +18,7 @@ class PSBTSelectSeedView(View): SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) def run(self): @@ -45,6 +46,8 @@ def run(self): 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) selected_menu_num = self.run_screen( ButtonListScreen, @@ -76,6 +79,11 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: + from seedsigner.views.seed_views import SeedMnemonicEntryView + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + return Destination(SeedMnemonicEntryView) + class PSBTOverviewView(View): diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 5a3657dcb..dfc2cc1a3 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -79,6 +79,7 @@ class SeedSelectSeedView(View): SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) def __init__(self, flow: str = Controller.FLOW__VERIFY_SINGLESIG_ADDR): @@ -118,6 +119,8 @@ def run(self): 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) selected_menu_num = self.run_screen( seed_screens.SeedSelectSeedScreen, @@ -154,6 +157,11 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: + from seedsigner.views.seed_views import SeedMnemonicEntryView + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + return Destination(SeedMnemonicEntryView) + """**************************************************************************** @@ -163,6 +171,7 @@ class LoadSeedView(View): SEED_QR = (" Scan a SeedQR", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) CREATE = (" Create a seed", SeedSignerIconConstants.PLUS) def run(self): @@ -172,6 +181,9 @@ def run(self): 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) + selected_menu_num = self.run_screen( ButtonListScreen, title="Load A Seed", @@ -194,6 +206,10 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.CREATE: from .tools_views import ToolsMenuView return Destination(ToolsMenuView) @@ -288,10 +304,6 @@ def run(self): class SeedFinalizeView(View): FINALIZE = "Done" - PASSPHRASE = "BIP-39 Passphrase" - CUSTOM_EXTENSION = "Custom Extension" - SWITCH = "Switch" - CANCEL = "Cancel" def __init__(self): super().__init__() @@ -300,24 +312,8 @@ def __init__(self): def run(self): - if self.seed.electrum_seed_bytes: - # see if user wants to enter electrum mode since it's a valid electrum seed - button_data = [self.SWITCH, self.CANCEL] - selected_menu_num = self.run_screen( - seed_screens.SeedSwitchElectrumModeScreen, - button_data=button_data - ) - if button_data[selected_menu_num] == self.SWITCH: - self.seed.switch_to_electrum() - # recalculate fingerprint - self.fingerprint = self.seed.get_fingerprint(network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - elif not self.seed.seed_bytes: - # we only got here because it was a valid electrum seed - # thus if user didn't mean to enter an electrum seed, it's invalid - raise InvalidSeedException(f"Invalid seed entered") - button_data = [self.FINALIZE] - passphrase_button = self.CUSTOM_EXTENSION if self.seed.is_electrum else self.PASSPHRASE + passphrase_button = self.seed.passphrase_label if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED: button_data.append(passphrase_button) @@ -343,7 +339,7 @@ def __init__(self): def run(self): - passphrase_title="Custom Extension" if self.seed.is_electrum else "BIP-39 Passphrase" + passphrase_title=self.seed.passphrase_label ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase, title=passphrase_title) if ret == RET_CODE__BACK_BUTTON: @@ -567,7 +563,9 @@ def __init__(self, seed_num): def run(self): - button_data = [self.VIEW_WORDS, self.EXPORT_SEEDQR] + button_data = [self.VIEW_WORDS] + if self.seed.seedqr_supported: + button_data.append(self.EXPORT_SEEDQR) selected_menu_num = self.run_screen( ButtonListScreen, @@ -634,7 +632,7 @@ def run(self): from .tools_views import ToolsAddressExplorerAddressTypeView args = {"seed_num": self.seed_num, "sig_type": self.sig_type} seed = self.controller.storage.seeds[self.seed_num] - script_types = [SettingsConstants.NATIVE_SEGWIT] if seed.is_electrum else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) + script_types = seed.script_override if seed.script_override else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) if len(script_types) == 1: # Nothing to select; skip this screen args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0] @@ -816,14 +814,16 @@ def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: def run(self): + seed_derivation_override = self.seed.derivation_override(self.sig_type) if self.script_type == SettingsConstants.CUSTOM_DERIVATION: derivation_path = self.custom_derivation + elif seed_derivation_override: + derivation_path = seed_derivation_override else: derivation_path = embit_utils.get_standard_derivation_path( network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), wallet_type=self.sig_type, - script_type=self.script_type, - is_electrum=self.seed.is_electrum + script_type=self.script_type ) if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__DISABLED: @@ -838,11 +838,10 @@ def run(self): try: embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))] - version = embit_utils.detect_version( - derivation_path, - self.settings.get_value(SettingsConstants.SETTING__NETWORK), - self.sig_type, - self.seed.is_electrum + version = self.seed.detect_version( + derivation_path, + self.settings.get_value(SettingsConstants.SETTING__NETWORK), + self.sig_type ) root = embit.bip32.HDKey.from_seed( self.seed.seed_bytes, @@ -904,14 +903,12 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str, sig_ty qr_type = QRType.XPUB__UR self.qr_encoder = EncodeQR( - seed_phrase=self.seed.mnemonic_list, - passphrase=self.seed.passphrase, + seed=self.seed, derivation=derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), qr_type=qr_type, qr_density=qr_density, wordlist_language_code=self.seed.wordlist_language_code, - is_electrum=self.seed.is_electrum, sig_type=sig_type ) @@ -1327,7 +1324,7 @@ def run(self): num_modules_standard = 29 num_modules_compact = 25 - if seed.is_electrum or self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED: + if self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED: # Only configured for standard SeedQR return Destination( SeedTranscribeSeedQRWarningView, @@ -1415,7 +1412,7 @@ def __init__(self, seed_num: int, seedqr_format: str, num_modules: int): def run(self): e = EncodeQR( - seed_phrase=self.seed.mnemonic_list, + seed=self.seed, qr_type=self.seedqr_format, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) ) @@ -1450,7 +1447,7 @@ def __init__(self, seed_num: int, seedqr_format: str): def run(self): e = EncodeQR( - seed_phrase=self.seed.mnemonic_list, + seed=self.seed, qr_type=self.seedqr_format, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) ) @@ -1670,18 +1667,17 @@ def __init__(self, seed_num: int = None): super().__init__() self.seed_num = seed_num self.is_multisig = self.controller.unverified_address["sig_type"] == SettingsConstants.MULTISIG + self.seed_derivation_override = "" if not self.is_multisig: if seed_num is None: 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) else: self.seed = None self.address = self.controller.unverified_address["address"] - # override derivation path if it's electrum as bip-39 path was set by default - if self.seed and self.seed.is_electrum: - self.controller.unverified_address["derivation_path"] = 'm/0h' - self.derivation_path = self.controller.unverified_address["derivation_path"] + self.derivation_path = self.seed_derivation_override if self.seed_derivation_override else self.seed.derivation_self.controller.unverified_address["derivation_path"] self.script_type = self.controller.unverified_address["script_type"] self.sig_type = self.controller.unverified_address["sig_type"] self.network = self.controller.unverified_address["network"] diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 6c71637d3..b2c7be9b0 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -441,6 +441,7 @@ class ToolsAddressExplorerSelectSourceView(View): SCAN_DESCRIPTOR = ("Scan wallet descriptor", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) def run(self): @@ -450,6 +451,8 @@ def run(self): button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT)) button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD] + if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED: + button_data.append(self.TYPE_ELECTRUM) selected_menu_num = self.run_screen( ButtonListScreen, @@ -493,6 +496,11 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: + from seedsigner.views.seed_views import SeedMnemonicEntryView + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + return Destination(SeedMnemonicEntryView) + class ToolsAddressExplorerAddressTypeView(View): @@ -526,15 +534,17 @@ def __init__(self, seed_num: int = None, script_type: str = None, custom_derivat 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) if self.script_type == SettingsConstants.CUSTOM_DERIVATION: derivation_path = self.custom_derivation + elif seed_derivation_override: + derivation_path = seed_derivation_override else: derivation_path = embit_utils.get_standard_derivation_path( network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), wallet_type=SettingsConstants.SINGLE_SIG, script_type=self.script_type, - is_electrum = self.seed.is_electrum ) data["derivation_path"] = derivation_path diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 75ed796fb..3bb89ee16 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -4,6 +4,7 @@ from binascii import a2b_base64 from seedsigner.models.settings import SettingsConstants +from seedsigner.models.seed import Seed @@ -63,7 +64,7 @@ def test_specter_qr_encode(): def test_seedsigner_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(), qr_type=QRType.SEED__SEEDQR) + e = EncodeQR(seed=Seed(mnemonic.split()), qr_type=QRType.SEED__SEEDQR) print(e.next_part()) @@ -74,7 +75,7 @@ def test_seedsigner_qr(): def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(), passphrase="pass", qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") + e = EncodeQR(seed=Seed(mnemonic.split(), passphrase="pass"), qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5" @@ -83,7 +84,7 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUB__SPECTER, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = EncodeQR(seed=Seed(mnemonic.split(), passphrase="pass"), qr_type=QRType.XPUB__SPECTER, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" @@ -96,8 +97,7 @@ def test_ur_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" e = EncodeQR( - seed_phrase=mnemonic.split(), - passphrase="pass", + seed=Seed(mnemonic.split(), passphrase="pass"), qr_type=QRType.XPUB__UR, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index 02b714463..a14abb048 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -3,6 +3,7 @@ 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 class TestPSBTFlows(FlowTest): @@ -78,7 +79,7 @@ def load_seed_into_decoder(view: scan_views.ScanView): FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED), FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED), FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE), FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="abc"), FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), FlowStep(seed_views.SeedOptionsView, is_redirect=True), diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index e71ad0b54..84b18eb81 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -40,7 +40,7 @@ def test_passphrase_entry_flow(self): self.run_sequence([ FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE), FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="muhpassphrase"), FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.EDIT), FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="muhpassphrase2"), diff --git a/tests/test_flows_tools.py b/tests/test_flows_tools.py index 0fb0c4a1c..9b5144b8b 100644 --- a/tests/test_flows_tools.py +++ b/tests/test_flows_tools.py @@ -64,7 +64,7 @@ def load_seed_into_decoder(view: scan_views.ScanView): # Finalize the new seed w/passphrase self.run_sequence( sequence=[ - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE), FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="mypassphrase"), FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), FlowStep(seed_views.SeedOptionsView, is_redirect=True), @@ -157,4 +157,4 @@ def load_seed_into_decoder(view: scan_views.ScanView): FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, screen_return_value=RET_CODE__BACK_BUTTON), FlowStep(MainMenuView), - ]) \ No newline at end of file + ]) diff --git a/tests/test_seed.py b/tests/test_seed.py index 8761e12cb..04291f65f 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -1,4 +1,4 @@ -from seedsigner.models.seed import Seed +from seedsigner.models.seed import Seed, ElectrumSeed from seedsigner.models.settings import SettingsConstants @@ -37,24 +37,9 @@ def test_seed(): def test_electrum_seed(): - seed = Seed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split()) + 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 not seed.seed_bytes - - assert seed.electrum_seed_bytes == intended_seed - - seed.switch_to_electrum() - assert seed.seed_bytes == intended_seed - assert seed.is_electrum - -def test_seed_bip39_electrum_both_valid(): - seed = Seed(mnemonic="frog cricket convince battle film mistake survey normal frequent magnet park cheap".split()) - - assert seed.seed_bytes == b'\x05-\x0c\xc2\x97\xfbw\x17b}U[\x80o\x94\x9c\xaa\x0f\xfc\xb2\xac\x08\xa3Q=\xbd\xf6\xcf] I\xfd9\x9a\x7f/\xa3OA\xe21.5^*\xa7e\xcd\xf7$h\x04\x02\xf6\xdf1\xc6\xa9{\x8c\xcc\xcc\xce)' - - assert seed.electrum_seed_bytes == b'<\x03y\xb7\xd6Q\x80I\xf5n\x8e\x9ek\x03!\xea\xbf\xe1\xc4f\x04\xcd\xde\x15\xe9\xfcG\xdd\xf3\x86\xc3\xc4R\xa1\xf0\xeb\xeb\x1f\'\xdd\x84\x9b\xa8\\\xd5\xfc\x9f\xd4q<\xd8\x0b\xb4\x04\x03\x9b8\x937\xb3B\x1e"\xf2' - diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 7e2d63856..f8146d06e 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -6,6 +6,7 @@ from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.qr_type import QRType +from seedsigner.models.seed import Seed from seedsigner.models.settings import SettingsConstants @@ -17,7 +18,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): print(seed_phrase) assert len(seed_phrase) == mnemonic_length - e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type) + e = EncodeQR(seed=Seed(seed_phrase), qr_type=qr_type) data = e.next_part() print(data) From 8c9fd5c889a4cb256ad52109d66386e9b3620a56 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Tue, 19 Mar 2024 12:24:17 -0500 Subject: [PATCH 09/19] display warning screen before user inputs electrum seed to notify some features disabled --- src/seedsigner/views/psbt_views.py | 7 +++++++ src/seedsigner/views/seed_views.py | 15 +++++++++++++++ src/seedsigner/views/tools_views.py | 7 +++++++ 3 files changed, 29 insertions(+) diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index b639b50a2..09ac597cf 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -80,6 +80,13 @@ def run(self): 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) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index dfc2cc1a3..59975821f 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -158,6 +158,14 @@ def run(self): 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) @@ -207,6 +215,13 @@ def run(self): 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) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index b2c7be9b0..40bfa6d33 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -497,6 +497,13 @@ def run(self): 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 1820997ad3fc2fda7d3822862ce84dd85fe6e460 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Tue, 19 Mar 2024 12:28:39 -0500 Subject: [PATCH 10/19] disabled BIP85 child seeds for electrum seeds since it doesn't apply --- src/seedsigner/models/seed.py | 8 ++++++++ src/seedsigner/views/seed_views.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 9dfdc6460..55a95d1e6 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -121,6 +121,10 @@ def passphrase_label(self) -> str: 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') @@ -203,3 +207,7 @@ def passphrase_label(self) -> str: @property def seedqr_supported(self) -> bool: return False + + @property + def bip85_supported(self) -> bool: + return False diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 59975821f..dd0e55c31 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -519,7 +519,7 @@ def run(self): if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__ENABLED: button_data.append(self.SIGN_MESSAGE) - if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED: + if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED and self.seed.bip85_supported: button_data.append(self.BIP85_CHILD_SEED) button_data.append(self.DISCARD) From 2355c53ce23c25d2f949f8ecaa580ba204f553eb Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Tue, 19 Mar 2024 13:01:27 -0500 Subject: [PATCH 11/19] updating docs to elaborate on current Electrum seed support and limitations --- README.md | 1 + docs/electrum.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/electrum.md diff --git a/README.md b/README.md index 1f6a0a10b..0e4cd3d1d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ If you have specific questions about the project, our [Telegram Group](https://t * Optimized seed word entry interface * Support for Bitcoin Mainnet & Testnet * Support for custom user-defined derivation paths +* Support for loading Electrum Segwit seed phrases with feature limitations: [Electrum support info](docs/electrum.md) * On-demand receive address verification * Address Explorer for single sig and multisig wallets * User-configurable QR code display density diff --git a/docs/electrum.md b/docs/electrum.md new file mode 100644 index 000000000..23581fd0d --- /dev/null +++ b/docs/electrum.md @@ -0,0 +1,14 @@ +# SeedSigner Electrum seed phrase support + +SeedSigner supports loading of Electrum's Segwit seed phrases. This is considered an Advanced feature that is disabled by default. + +To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen. + +Some SeedSigner functionality is deliberately disabled when using an Electrum mnemonic: + +- BIP-85 child seeds + - Not applicable for Electrum seed types +- SeedQR backups + - Since Electrum seeds are not supported by other SeedQR implementations, it would be dangerous to use SeedQR as a backup tool for Electrum seeds and is thus disabled +- Custom derivations + - Like BIP-85 child seeds, custom derivations are not applicable to Electrum seeds and thus disabled From ce9113fe872d4dfe42d61cae7850973b31cb1427 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Tue, 19 Mar 2024 13:37:18 -0500 Subject: [PATCH 12/19] fix bad paste in unverified address deriv path --- src/seedsigner/views/seed_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index dd0e55c31..9a801ea20 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1692,7 +1692,7 @@ def __init__(self, seed_num: int = None): else: self.seed = None self.address = self.controller.unverified_address["address"] - self.derivation_path = self.seed_derivation_override if self.seed_derivation_override else self.seed.derivation_self.controller.unverified_address["derivation_path"] + self.derivation_path = self.seed_derivation_override if self.seed_derivation_override else self.controller.unverified_address["derivation_path"] self.script_type = self.controller.unverified_address["script_type"] self.sig_type = self.controller.unverified_address["sig_type"] self.network = self.controller.unverified_address["network"] From 8a83f32b67811946c803436698dfed6bf151343a Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Fri, 21 Jun 2024 13:45:03 -0500 Subject: [PATCH 13/19] remove unneccessary include --- tests/test_seedqr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index e855252bd..2202af0df 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -5,7 +5,6 @@ from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder from seedsigner.models.qr_type import QRType from seedsigner.models.seed import Seed -from seedsigner.models.settings import SettingsConstants From c0f1ad641e8072ba575908390436b5d061e18885 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Fri, 21 Jun 2024 13:55:57 -0500 Subject: [PATCH 14/19] small cleanup --- src/seedsigner/models/encode_qr.py | 5 ----- src/seedsigner/models/seed.py | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 7e038fbc8..71eb404ce 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -151,13 +151,8 @@ class BaseXpubQrEncoder(BaseQrEncoder): seed: Seed = None derivation: str = None network: str = SettingsConstants.MAINNET - wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH def prep_xpub(self): - self.wordlist = Seed.get_wordlist() - - if self.wordlist == None: - raise Exception('Wordlist Required') version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 4c4066a8b..6164ff84c 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -59,6 +59,7 @@ def mnemonic_str(self) -> str: def mnemonic_list(self) -> List[str]: return self._mnemonic + @property def wordlist_language_code(self) -> str: return self._wordlist_language_code @@ -98,7 +99,7 @@ def set_passphrase(self, passphrase: str, regenerate_seed: bool = True): @property def wordlist(self) -> List[str]: - return Seed.get_wordlist(self._wordlist_language_code) + return Seed.get_wordlist(self.wordlist_language_code) def set_wordlist_language_code(self, language_code: str): From b430d50a6a07c2c174b43c153daaa70af839960b Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Fri, 21 Jun 2024 15:41:25 -0500 Subject: [PATCH 15/19] pass sig_type to BaseXpubQrEncoder so that it can get correct version --- src/seedsigner/models/encode_qr.py | 3 ++- src/seedsigner/views/seed_views.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 71eb404ce..269a8f897 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -151,10 +151,11 @@ class BaseXpubQrEncoder(BaseQrEncoder): seed: Seed = None derivation: str = None network: str = SettingsConstants.MAINNET + sig_type : str = None def prep_xpub(self): - version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) + version = self.seed.detect_version(self.derivation, self.network, self.sig_type) self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.xprv = self.root.derive(self.derivation) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index b299a50f3..13dd38b51 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -907,7 +907,8 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str, sig_ty seed=self.seed, derivation=derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), - qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) + qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY), + sig_type=sig_type ) if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP: From a28f1626af15aa7a319acda4daf6e5060f6c1394 Mon Sep 17 00:00:00 2001 From: BamaHodl <86847733+BamaHodl@users.noreply.github.com> Date: Mon, 8 Jul 2024 09:39:47 -0500 Subject: [PATCH 16/19] Update electrum.md Update to clarify Electrum Segwit derivations and add link to Elecrum seed versioning documentation --- docs/electrum.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/electrum.md b/docs/electrum.md index 23581fd0d..5202ae885 100644 --- a/docs/electrum.md +++ b/docs/electrum.md @@ -1,8 +1,8 @@ # SeedSigner Electrum seed phrase support -SeedSigner supports loading of Electrum's Segwit seed phrases. This is considered an Advanced feature that is disabled by default. +SeedSigner supports loading of [Electrum's Segwit seed phrases](https://electrum.readthedocs.io/en/latest/seedphrase.html#electrum-seed-version-system). This is considered an Advanced feature that is disabled by default. -To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen. +To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum Native Segwit seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen. Some SeedSigner functionality is deliberately disabled when using an Electrum mnemonic: @@ -11,4 +11,5 @@ Some SeedSigner functionality is deliberately disabled when using an Electrum mn - SeedQR backups - Since Electrum seeds are not supported by other SeedQR implementations, it would be dangerous to use SeedQR as a backup tool for Electrum seeds and is thus disabled - Custom derivations - - Like BIP-85 child seeds, custom derivations are not applicable to Electrum seeds and thus disabled + - Hard coded derivation path and script types in SeedSigner to match Electrum wallet software. These are m/0h for single sig and m/1h for multisig + - User-chosen custom derivations are thus not supported for Electrum seeds From 4ea2d72797e65fdad2ab55c9ac2704d40a3e12d2 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Mon, 8 Jul 2024 09:48:33 -0500 Subject: [PATCH 17/19] more explicit about which electrum seeds supported in Advanced options and correct WarningScreen import in tools_views.py --- src/seedsigner/models/settings_definition.py | 2 +- src/seedsigner/views/tools_views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index acedd4f6c..8f33ee6b4 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -471,7 +471,7 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS, abbreviated_name="Electrum", - display_name="Electrum seed support", + display_name="Electrum seed support (Native Segwit only)", visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 39af6a09f..ad2940bea 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -9,7 +9,7 @@ from seedsigner.controller import Controller from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants -from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) +from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen) from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen) From 46061c5d30312b33b5e380a8a83a2af24bb4849d Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Mon, 8 Jul 2024 09:58:48 -0500 Subject: [PATCH 18/19] correcting electrum doc to match change in option name --- docs/electrum.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/electrum.md b/docs/electrum.md index 5202ae885..6c07ca31b 100644 --- a/docs/electrum.md +++ b/docs/electrum.md @@ -2,7 +2,7 @@ SeedSigner supports loading of [Electrum's Segwit seed phrases](https://electrum.readthedocs.io/en/latest/seedphrase.html#electrum-seed-version-system). This is considered an Advanced feature that is disabled by default. -To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum Native Segwit seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen. +To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen. Some SeedSigner functionality is deliberately disabled when using an Electrum mnemonic: From 75d9f5ff511f7e1195922dba2ef863f187b79e13 Mon Sep 17 00:00:00 2001 From: BamaHodl Date: Mon, 8 Jul 2024 13:56:21 -0500 Subject: [PATCH 19/19] make sure to use the correct list for script_types --- src/seedsigner/views/seed_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 097dc7e48..215af14f5 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -650,7 +650,7 @@ def run(self): script_types = seed.script_override if seed.script_override else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) if len(script_types) == 1: # Nothing to select; skip this screen - args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0] + args["script_type"] = script_types[0] if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: del args["sig_type"]