mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 23:18:25 +00:00
Merge pull request #513 from BamaHodl/dev
Add limited support for electrum segwit seeds
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,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),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user