mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Light refactor of Electrum seed support
This commit is contained in:
@@ -85,15 +85,17 @@ class BaseStaticQrEncoder(BaseQrEncoder):
|
||||
|
||||
@dataclass
|
||||
class SeedQrEncoder(BaseStaticQrEncoder):
|
||||
seed : Seed = None
|
||||
mnemonic: List[str] = None
|
||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code)
|
||||
self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
|
||||
super().__post_init__()
|
||||
|
||||
self.data = ""
|
||||
# Output as Numeric data format
|
||||
for word in self.seed.mnemonic_list:
|
||||
for word in self.mnemonic:
|
||||
index = self.wordlist.index(word)
|
||||
self.data += str("%04d" % index)
|
||||
|
||||
@@ -108,18 +110,18 @@ class CompactSeedQrEncoder(SeedQrEncoder):
|
||||
def next_part(self):
|
||||
# Output as binary data format
|
||||
binary_str = ""
|
||||
for word in self.seed.mnemonic_list:
|
||||
for word in self.mnemonic:
|
||||
index = self.wordlist.index(word)
|
||||
|
||||
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
|
||||
binary_str += bin(index).split('b')[1].zfill(11)
|
||||
|
||||
# We can exclude the checksum bits at the end
|
||||
if len(self.seed.mnemonic_list) == 24:
|
||||
if len(self.mnemonic) == 24:
|
||||
# 8 checksum bits in a 24-word seed
|
||||
binary_str = binary_str[:-8]
|
||||
|
||||
elif len(self.seed.mnemonic_list) == 12:
|
||||
elif len(self.mnemonic) == 12:
|
||||
# 4 checksum bits in a 12-word seed
|
||||
binary_str = binary_str[:-4]
|
||||
|
||||
|
||||
@@ -470,8 +470,9 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
abbreviated_name="Electrum",
|
||||
display_name="Electrum seed support (Native Segwit only)",
|
||||
abbreviated_name="electrum",
|
||||
display_name="Electrum seeds",
|
||||
help_text="Native Segwit only",
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
|
||||
@@ -79,13 +79,6 @@ class PSBTSelectSeedView(View):
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features disabled for Electrum seeds",
|
||||
show_back_button=False,
|
||||
)
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
@@ -119,6 +119,7 @@ class SeedSelectSeedView(View):
|
||||
button_data.append(self.SCAN_SEED)
|
||||
button_data.append(self.TYPE_12WORD)
|
||||
button_data.append(self.TYPE_24WORD)
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(self.TYPE_ELECTRUM)
|
||||
|
||||
@@ -158,17 +159,7 @@ class SeedSelectSeedView(View):
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features disabled for Electrum seeds",
|
||||
show_back_button=False,
|
||||
)
|
||||
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
return Destination(SeedElectrumMnemonicStartView)
|
||||
|
||||
|
||||
|
||||
@@ -187,10 +178,12 @@ class LoadSeedView(View):
|
||||
self.SEED_QR,
|
||||
self.TYPE_12WORD,
|
||||
self.TYPE_24WORD,
|
||||
self.CREATE,
|
||||
]
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.insert(len(button_data)-1, self.TYPE_ELECTRUM)
|
||||
button_data.append(self.TYPE_ELECTRUM)
|
||||
|
||||
button_data.append(self.CREATE)
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
@@ -215,15 +208,7 @@ class LoadSeedView(View):
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features disabled for Electrum seeds",
|
||||
show_back_button=False,
|
||||
)
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
return Destination(SeedElectrumMnemonicStartView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.CREATE:
|
||||
from .tools_views import ToolsMenuView
|
||||
@@ -489,6 +474,27 @@ class SeedDiscardView(View):
|
||||
|
||||
|
||||
|
||||
class SeedElectrumMnemonicStartView(View):
|
||||
"""
|
||||
Currently just a warning display before entering an Electrum seed.
|
||||
|
||||
Could be expanded with a follow-up View to specify Electrum seed type.
|
||||
"""
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features are disabled for Electrum seeds.",
|
||||
show_back_button=False,
|
||||
)
|
||||
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
|
||||
|
||||
"""****************************************************************************
|
||||
Views for actions on individual seeds:
|
||||
****************************************************************************"""
|
||||
@@ -1456,7 +1462,8 @@ class SeedTranscribeSeedQRWholeQRView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
encoder_args = dict(seed=self.seed)
|
||||
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
if self.seedqr_format == QRType.SEED__SEEDQR:
|
||||
e = SeedQrEncoder(**encoder_args)
|
||||
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
||||
@@ -1492,7 +1499,8 @@ class SeedTranscribeSeedQRZoomedInView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
encoder_args = dict(seed=self.seed)
|
||||
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
if self.seedqr_format == QRType.SEED__SEEDQR:
|
||||
e = SeedQrEncoder(**encoder_args)
|
||||
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
||||
|
||||
@@ -496,16 +496,8 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Electrum warning",
|
||||
status_headline=None,
|
||||
text=f"Some features disabled for Electrum seeds",
|
||||
show_back_button=False,
|
||||
)
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
from seedsigner.views.seed_views import SeedElectrumWarningView
|
||||
return Destination(SeedElectrumWarningView)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -215,6 +215,8 @@ def test_generate_screenshots(target_locale):
|
||||
|
||||
seed_views.SeedSignMessageConfirmMessageView,
|
||||
seed_views.SeedSignMessageConfirmAddressView,
|
||||
|
||||
seed_views.SeedElectrumMnemonicStartView,
|
||||
],
|
||||
"PSBT Views": [
|
||||
psbt_views.PSBTSelectSeedView, # this will fail, be rerun below
|
||||
|
||||
@@ -25,19 +25,18 @@ def test_ur_psbt_qr_encode():
|
||||
def test_seedsigner_qr():
|
||||
# test vector 1 from the SeedQR docs
|
||||
mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split()
|
||||
e = SeedQrEncoder(seed=Seed(mnemonic))
|
||||
e = SeedQrEncoder(mnemonic=mnemonic)
|
||||
assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643"
|
||||
|
||||
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
|
||||
e = CompactSeedQrEncoder(mnemonic=mnemonic)
|
||||
assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n'
|
||||
|
||||
# test vector 4 from the SeedQR docs
|
||||
mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split()
|
||||
seed = Seed(mnemonic)
|
||||
e = SeedQrEncoder(seed=seed)
|
||||
e = SeedQrEncoder(mnemonic=mnemonic)
|
||||
assert e.next_part() == "073318950739065415961602009907670428187212261116"
|
||||
|
||||
e = CompactSeedQrEncoder(seed=seed)
|
||||
e = CompactSeedQrEncoder(mnemonic=mnemonic)
|
||||
assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE'
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,57 @@ class TestSeedFlows(FlowTest):
|
||||
self.run_sequence(sequence)
|
||||
|
||||
|
||||
def test_electrum_mnemonic_entry_flow(self):
|
||||
"""
|
||||
Manually entering an Electrum mnemonic should land at the Finalize Seed flow and end at
|
||||
the SeedOptionsView.
|
||||
|
||||
Most BIP-39 mnemonics should generate an error if entered as Electrum seeds.
|
||||
"""
|
||||
def test_with_mnemonic(mnemonic: list[str], expects_electrum_seed_is_valid: bool = True):
|
||||
Settings.HOSTNAME = "not seedsigner-os"
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
sequence = [
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
|
||||
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
|
||||
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_ELECTRUM),
|
||||
FlowStep(seed_views.SeedElectrumMnemonicStartView), # Warning screen; no relevant button data selection.
|
||||
]
|
||||
|
||||
# Now add each manual word entry step
|
||||
for word in mnemonic:
|
||||
sequence.append(
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word)
|
||||
)
|
||||
|
||||
if expects_electrum_seed_is_valid:
|
||||
# With the mnemonic completely entered, we land on the SeedFinalizeView
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView),
|
||||
]
|
||||
|
||||
else:
|
||||
sequence.append(FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD))
|
||||
|
||||
self.run_sequence(sequence)
|
||||
|
||||
BaseTest.reset_controller()
|
||||
|
||||
|
||||
# Test seeds generated by Electrum v4.5.5
|
||||
test_with_mnemonic("bomb congress scorpion mutual word stamp tongue valid permit salmon yellow spy".split())
|
||||
test_with_mnemonic("morning pretty hobby click extend color wait joke define sausage boost salmon".split())
|
||||
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split())
|
||||
|
||||
# Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide
|
||||
test_with_mnemonic("pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split(), expects_electrum_seed_is_valid=False)
|
||||
test_with_mnemonic("gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split(), expects_electrum_seed_is_valid=False)
|
||||
test_with_mnemonic("enough board blossom stamp fire buffalo digital solution sadness random number stone".split(), expects_electrum_seed_is_valid=False)
|
||||
|
||||
|
||||
def test_export_xpub_standard_flow(self):
|
||||
"""
|
||||
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
|
||||
|
||||
@@ -14,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(seed=Seed(mnemonic))
|
||||
e = SeedQrEncoder(mnemonic=mnemonic)
|
||||
elif qr_type == QRType.SEED__COMPACTSEEDQR:
|
||||
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
|
||||
e = CompactSeedQrEncoder(mnemonic=mnemonic)
|
||||
|
||||
data = e.next_part()
|
||||
print(data)
|
||||
|
||||
Reference in New Issue
Block a user