Merge pull request #513 from BamaHodl/dev

Add limited support for electrum segwit seeds
This commit is contained in:
Nick Klockenga
2024-07-11 16:57:10 -04:00
committed by GitHub
15 changed files with 265 additions and 63 deletions
+1
View File
@@ -44,6 +44,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
+15
View File
@@ -0,0 +1,15 @@
# SeedSigner Electrum seed phrase support
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.
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
- 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
+9 -18
View File
@@ -85,16 +85,15 @@ class BaseStaticQrEncoder(BaseQrEncoder):
@dataclass
class SeedQrEncoder(BaseStaticQrEncoder):
mnemonic: List[str] = None
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
seed : Seed = None
def __post_init__(self):
self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code)
super().__post_init__()
self.data = ""
# Output as Numeric data format
for word in self.mnemonic:
for word in self.seed.mnemonic_list:
index = self.wordlist.index(word)
self.data += str("%04d" % index)
@@ -109,18 +108,18 @@ class CompactSeedQrEncoder(SeedQrEncoder):
def next_part(self):
# Output as binary data format
binary_str = ""
for word in self.mnemonic:
for word in self.seed.mnemonic_list:
index = self.wordlist.index(word)
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
binary_str += bin(index).split('b')[1].zfill(11)
# We can exclude the checksum bits at the end
if len(self.mnemonic) == 24:
if len(self.seed.mnemonic_list) == 24:
# 8 checksum bits in a 24-word seed
binary_str = binary_str[:-8]
elif len(self.mnemonic) == 12:
elif len(self.seed.mnemonic_list) == 12:
# 4 checksum bits in a 12-word seed
binary_str = binary_str[:-4]
@@ -149,22 +148,14 @@ class BaseXpubQrEncoder(BaseQrEncoder):
"""
Base Xpub QrEncoder for static and animated formats
"""
mnemonic: list = None
passphrase: str = None
seed: Seed = None
derivation: str = None
network: str = SettingsConstants.MAINNET
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
sig_type : str = None
def prep_xpub(self):
self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
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.mnemonic,
passphrase=self.passphrase,
wordlist_language_code=self.wordlist_language_code)
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)
+92 -2
View File
@@ -1,4 +1,6 @@
import unicodedata
import hashlib
import hmac
from binascii import hexlify
from embit import bip39, bip32, bip85
@@ -18,7 +20,7 @@ class Seed:
mnemonic: List[str] = None,
passphrase: str = "",
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None:
self.wordlist_language_code = wordlist_language_code
self._wordlist_language_code = wordlist_language_code
if not mnemonic:
raise Exception("Must initialize a Seed with a mnemonic List[str]")
@@ -56,7 +58,11 @@ class Seed:
@property
def mnemonic_list(self) -> List[str]:
return self._mnemonic
@property
def wordlist_language_code(self) -> str:
return self._wordlist_language_code
@property
def mnemonic_display_str(self) -> str:
@@ -100,6 +106,28 @@ class Seed:
# TODO: Support other BIP-39 wordlist languages!
raise Exception("Not yet implemented!")
@property
def script_override(self) -> list:
return None
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
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
@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"])
@@ -125,3 +153,65 @@ class Seed:
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
@property
def bip85_supported(self) -> bool:
return False
+13 -4
View File
@@ -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 @@ class SeedStorage:
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 @@ class SeedStorage:
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 @@ class SeedStorage:
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
@@ -160,6 +160,7 @@ class SettingsConstants:
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"
@@ -198,6 +199,15 @@ class SettingsConstants:
TYPE__ENABLED_DISABLED_PROMPT_REQUIRED,
]
# Electrum seed constants
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:
@@ -458,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 (Native Segwit only)",
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING,
display_name="Message signing",
+15
View File
@@ -17,6 +17,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):
@@ -44,6 +45,8 @@ class PSBTSelectSeedView(View):
button_data.append(self.SCAN_SEED)
button_data.append(self.TYPE_12WORD)
button_data.append(self.TYPE_24WORD)
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
button_data.append(self.TYPE_ELECTRUM)
selected_menu_num = self.run_screen(
ButtonListScreen,
@@ -75,6 +78,18 @@ class PSBTSelectSeedView(View):
self.controller.storage.init_pending_mnemonic(num_words=24)
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)
class PSBTOverviewView(View):
+60 -20
View File
@@ -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 @@ class SeedSelectSeedView(View):
button_data.append(self.SCAN_SEED)
button_data.append(self.TYPE_12WORD)
button_data.append(self.TYPE_24WORD)
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
button_data.append(self.TYPE_ELECTRUM)
selected_menu_num = self.run_screen(
seed_screens.SeedSelectSeedScreen,
@@ -154,6 +157,19 @@ class SeedSelectSeedView(View):
self.controller.storage.init_pending_mnemonic(num_words=24)
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)
"""****************************************************************************
@@ -163,6 +179,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 +189,9 @@ class LoadSeedView(View):
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 +214,17 @@ class LoadSeedView(View):
self.controller.storage.init_pending_mnemonic(num_words=24)
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)
elif button_data[selected_menu_num] == self.CREATE:
from .tools_views import ToolsMenuView
return Destination(ToolsMenuView)
@@ -288,7 +319,6 @@ class SeedMnemonicInvalidView(View):
class SeedFinalizeView(View):
FINALIZE = "Done"
PASSPHRASE = "BIP-39 Passphrase"
def __init__(self):
super().__init__()
@@ -298,8 +328,9 @@ class SeedFinalizeView(View):
def run(self):
button_data = [self.FINALIZE]
passphrase_button = self.seed.passphrase_label
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 +342,7 @@ class SeedFinalizeView(View):
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 +354,8 @@ class SeedAddPassphraseView(View):
def run(self):
ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.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:
return Destination(BackStackView)
@@ -487,7 +519,7 @@ class SeedOptionsView(View):
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)
@@ -546,7 +578,9 @@ class SeedBackupView(View):
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,
@@ -612,9 +646,11 @@ class SeedExportXpubScriptTypeView(View):
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 = 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"]
@@ -798,8 +834,11 @@ class SeedExportXpubDetailsView(View):
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),
@@ -819,10 +858,10 @@ class SeedExportXpubDetailsView(View):
try:
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
version = embit.bip32.detect_version(
version = self.seed.detect_version(
derivation_path,
default="xpub",
network=embit_network
self.settings.get_value(SettingsConstants.SETTING__NETWORK),
self.sig_type
)
root = embit.bip32.HDKey.from_seed(
self.seed.seed_bytes,
@@ -850,6 +889,7 @@ class SeedExportXpubDetailsView(View):
dict(seed_num=self.seed_num,
coordinator=self.coordinator,
derivation_path=derivation_path,
sig_type=self.sig_type
)
)
@@ -859,16 +899,16 @@ class SeedExportXpubDetailsView(View):
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)
encoder_args = dict(
mnemonic=self.seed.mnemonic_list,
passphrase=self.seed.passphrase,
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:
@@ -1381,8 +1421,7 @@ class SeedTranscribeSeedQRWholeQRView(View):
def run(self):
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
encoder_args = dict(seed=self.seed)
if self.seedqr_format == QRType.SEED__SEEDQR:
e = SeedQrEncoder(**encoder_args)
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
@@ -1418,8 +1457,7 @@ class SeedTranscribeSeedQRZoomedInView(View):
def run(self):
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
encoder_args = dict(seed=self.seed)
if self.seedqr_format == QRType.SEED__SEEDQR:
e = SeedQrEncoder(**encoder_args)
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
@@ -1641,15 +1679,17 @@ class SeedAddressVerificationView(View):
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"]
self.derivation_path = 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"]
+19 -1
View File
@@ -9,7 +9,7 @@ from PIL.ImageOps import autocontrast
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)
@@ -440,6 +440,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):
@@ -449,6 +450,8 @@ class ToolsAddressExplorerSelectSourceView(View):
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,
@@ -492,6 +495,18 @@ class ToolsAddressExplorerSelectSourceView(View):
self.controller.storage.init_pending_mnemonic(num_words=24)
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)
class ToolsAddressExplorerAddressTypeView(View):
@@ -525,9 +540,12 @@ class ToolsAddressExplorerAddressTypeView(View):
if self.seed_num is not None:
self.seed = self.controller.storage.seeds[seed_num]
data["seed_num"] = self.seed
seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG)
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),
+9 -9
View File
@@ -3,6 +3,7 @@ from embit import psbt
from binascii import a2b_base64
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.seed import Seed
@@ -24,18 +25,19 @@ def test_ur_psbt_qr_encode():
def test_seedsigner_qr():
# test vector 1 from the SeedQR docs
mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split()
e = SeedQrEncoder(mnemonic=mnemonic)
e = SeedQrEncoder(seed=Seed(mnemonic))
assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643"
e = CompactSeedQrEncoder(mnemonic=mnemonic)
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n'
# test vector 4 from the SeedQR docs
mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split()
e = SeedQrEncoder(mnemonic=mnemonic)
seed = Seed(mnemonic)
e = SeedQrEncoder(seed=seed)
assert e.next_part() == "073318950739065415961602009907670428187212261116"
e = CompactSeedQrEncoder(mnemonic=mnemonic)
e = CompactSeedQrEncoder(seed=seed)
assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE'
@@ -43,8 +45,7 @@ def test_seedsigner_qr():
def test_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
e = StaticXpubQrEncoder(mnemonic=mnemonic.split(),
passphrase="pass",
e = StaticXpubQrEncoder(seed=Seed(mnemonic.split(), passphrase="pass"),
derivation="m/48h/1h/0h/2h",
network=SettingsConstants.TESTNET)
assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5"
@@ -54,7 +55,7 @@ def test_xpub_qr():
def test_specter_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
e = SpecterXPubQrEncoder(mnemonic=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW)
e = SpecterXPubQrEncoder(seed=Seed(mnemonic.split(" "), passphrase="pass"), 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"
@@ -67,8 +68,7 @@ def test_ur_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
e = UrXpubQrEncoder(
mnemonic=mnemonic.split(),
passphrase="pass",
seed=Seed(mnemonic.split(), passphrase="pass"),
network=SettingsConstants.TESTNET,
derivation="m/48h/1h/0h/2h",
qr_density=SettingsConstants.DENSITY__MEDIUM
+2 -1
View File
@@ -2,6 +2,7 @@ from base import FlowTest, FlowStep
from seedsigner.views.view import MainMenuView
from seedsigner.views import scan_views, seed_views, psbt_views
from seedsigner.models.settings import SettingsConstants
# TODO: Cleanup: convert TAB spacing to SPACE
@@ -80,7 +81,7 @@ class TestPSBTFlows(FlowTest):
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),
+1 -1
View File
@@ -40,7 +40,7 @@ class TestSeedFlows(FlowTest):
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"),
+1 -1
View File
@@ -64,7 +64,7 @@ class TestToolsFlows(FlowTest):
# 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),
+8 -4
View File
@@ -1,4 +1,4 @@
from seedsigner.models.seed import Seed
from seedsigner.models.seed import Seed, ElectrumSeed
from seedsigner.models.settings import SettingsConstants
@@ -36,6 +36,10 @@ def test_seed():
# assert seed.passphrase == "test"
def test_electrum_seed():
seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split())
intended_seed = b'\xcan|\xf8\x8a\x8d\xf78=Pq\xc4_\xe6\x02\x91\xfcs\xb2[\xed*\xdc\xc7%\xb6[_-(~D\xe5\x1e\x85%N\x9c\x03\x9dh\xafX}\x16\xb1\x99,\xbe\xc4\x11\xfaW\x0f\xb0\x89yD\xf4\x0f\xd5?\x8eA'
assert seed.seed_bytes == intended_seed
+3 -2
View File
@@ -4,6 +4,7 @@ from seedsigner.helpers.qr import QR
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder
from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import Seed
@@ -13,9 +14,9 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
assert len(mnemonic) == mnemonic_length
if qr_type == QRType.SEED__SEEDQR:
e = SeedQrEncoder(mnemonic=mnemonic)
e = SeedQrEncoder(seed=Seed(mnemonic))
elif qr_type == QRType.SEED__COMPACTSEEDQR:
e = CompactSeedQrEncoder(mnemonic=mnemonic)
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
data = e.next_part()
print(data)