mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
1799 lines
70 KiB
Python
1799 lines
70 KiB
Python
import embit
|
|
import random
|
|
import time
|
|
|
|
from binascii import hexlify
|
|
from embit import bip39
|
|
from embit.descriptor import Descriptor
|
|
from embit.networks import NETWORKS
|
|
from typing import List
|
|
|
|
from seedsigner.controller import Controller
|
|
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
|
|
from seedsigner.helpers import embit_utils
|
|
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
|
WarningScreen, DireWarningScreen, seed_screens)
|
|
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
|
|
from seedsigner.models.decode_qr import DecodeQR
|
|
from seedsigner.models.encode_qr import EncodeQR
|
|
from seedsigner.models.psbt_parser import PSBTParser
|
|
from seedsigner.models.qr_type import QRType
|
|
from seedsigner.models.seed import InvalidSeedException, Seed
|
|
from seedsigner.models.settings import Settings, SettingsConstants
|
|
from seedsigner.models.settings_definition import SettingsDefinition
|
|
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
|
from seedsigner.views.psbt_views import PSBTChangeDetailsView
|
|
from seedsigner.views.scan_views import ScanView
|
|
|
|
from .view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView
|
|
|
|
|
|
|
|
|
|
class SeedsMenuView(View):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.seeds = []
|
|
for seed in self.controller.storage.seeds:
|
|
self.seeds.append({
|
|
"fingerprint": seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
|
})
|
|
|
|
|
|
def run(self):
|
|
if not self.seeds:
|
|
# Nothing to do here unless we have a seed loaded
|
|
return Destination(LoadSeedView, clear_history=True)
|
|
|
|
button_data = []
|
|
for seed in self.seeds:
|
|
button_data.append((seed["fingerprint"], SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
|
button_data.append("Load a seed")
|
|
|
|
selected_menu_num = ButtonListScreen(
|
|
title="In-Memory Seeds",
|
|
is_button_text_centered=False,
|
|
button_data=button_data
|
|
).display()
|
|
|
|
if len(self.seeds) > 0 and selected_menu_num < len(self.seeds):
|
|
return Destination(SeedOptionsView, view_args={"seed_num": selected_menu_num})
|
|
|
|
elif selected_menu_num == len(self.seeds):
|
|
return Destination(LoadSeedView)
|
|
|
|
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
|
|
|
|
"""****************************************************************************
|
|
Loading seeds, passphrases, etc
|
|
****************************************************************************"""
|
|
class LoadSeedView(View):
|
|
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
|
|
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
|
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
|
CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS)
|
|
button_data=[
|
|
SEED_QR,
|
|
TYPE_12WORD,
|
|
TYPE_24WORD,
|
|
CREATE,
|
|
]
|
|
|
|
def run(self):
|
|
selected_menu_num = self.run_screen(
|
|
ButtonListScreen,
|
|
title="Load A Seed",
|
|
is_button_text_centered=False,
|
|
button_data=self.button_data
|
|
)
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
if self.button_data[selected_menu_num] == self.SEED_QR:
|
|
from .scan_views import ScanView
|
|
return Destination(ScanView)
|
|
|
|
elif self.button_data[selected_menu_num] == self.TYPE_12WORD:
|
|
self.controller.storage.init_pending_mnemonic(num_words=12)
|
|
return Destination(SeedMnemonicEntryView)
|
|
|
|
elif self.button_data[selected_menu_num] == self.TYPE_24WORD:
|
|
self.controller.storage.init_pending_mnemonic(num_words=24)
|
|
return Destination(SeedMnemonicEntryView)
|
|
|
|
elif self.button_data[selected_menu_num] == self.CREATE:
|
|
from .tools_views import ToolsMenuView
|
|
return Destination(ToolsMenuView)
|
|
|
|
|
|
|
|
class SeedMnemonicEntryView(View):
|
|
def __init__(self, cur_word_index: int = 0, is_calc_final_word: bool=False):
|
|
super().__init__()
|
|
self.cur_word_index = cur_word_index
|
|
self.cur_word = self.controller.storage.get_pending_mnemonic_word(cur_word_index)
|
|
self.is_calc_final_word = is_calc_final_word
|
|
|
|
|
|
def run(self):
|
|
ret = self.run_screen(
|
|
seed_screens.SeedMnemonicEntryScreen,
|
|
title=f"Seed Word #{self.cur_word_index + 1}", # Human-readable 1-indexing!
|
|
initial_letters=list(self.cur_word) if self.cur_word else ["a"],
|
|
wordlist=Seed.get_wordlist(wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)),
|
|
)
|
|
|
|
if ret == RET_CODE__BACK_BUTTON:
|
|
if self.cur_word_index > 0:
|
|
return Destination(BackStackView)
|
|
else:
|
|
self.controller.storage.discard_pending_mnemonic()
|
|
return Destination(MainMenuView)
|
|
|
|
# ret will be our new mnemonic word
|
|
self.controller.storage.update_pending_mnemonic(ret, self.cur_word_index)
|
|
|
|
if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 2:
|
|
# Time to calculate the last word. User must decide how they want to specify
|
|
# the last bits of entropy for the final word.
|
|
from seedsigner.views.tools_views import ToolsCalcFinalWordFinalizePromptView
|
|
return Destination(ToolsCalcFinalWordFinalizePromptView)
|
|
|
|
if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 1:
|
|
# Time to calculate the last word. User must either select a final word to
|
|
# contribute entropy to the checksum word OR we assume 0 ("abandon").
|
|
from seedsigner.views.tools_views import ToolsCalcFinalWordShowFinalWordView
|
|
return Destination(ToolsCalcFinalWordShowFinalWordView)
|
|
|
|
if self.cur_word_index < self.controller.storage.pending_mnemonic_length - 1:
|
|
return Destination(
|
|
SeedMnemonicEntryView,
|
|
view_args={
|
|
"cur_word_index": self.cur_word_index + 1,
|
|
"is_calc_final_word": self.is_calc_final_word
|
|
}
|
|
)
|
|
else:
|
|
# Attempt to finalize the mnemonic
|
|
try:
|
|
self.controller.storage.convert_pending_mnemonic_to_pending_seed()
|
|
except InvalidSeedException:
|
|
return Destination(SeedMnemonicInvalidView)
|
|
|
|
return Destination(SeedFinalizeView)
|
|
|
|
|
|
|
|
class SeedMnemonicInvalidView(View):
|
|
EDIT = "Review & Edit"
|
|
DISCARD = ("Discard", None, None, "red")
|
|
button_data = [EDIT, DISCARD]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.mnemonic: List[str] = self.controller.storage.pending_mnemonic
|
|
|
|
|
|
def run(self):
|
|
selected_menu_num = self.run_screen(
|
|
WarningScreen,
|
|
title="Invalid Mnemonic!",
|
|
status_headline=None,
|
|
text=f"Checksum failure; not a valid seed phrase.",
|
|
show_back_button=False,
|
|
button_data=self.button_data,
|
|
)
|
|
|
|
if self.button_data[selected_menu_num] == self.EDIT:
|
|
return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0})
|
|
|
|
elif self.button_data[selected_menu_num] == self.DISCARD:
|
|
self.controller.storage.discard_pending_mnemonic()
|
|
return Destination(MainMenuView)
|
|
|
|
|
|
|
|
class SeedFinalizeView(View):
|
|
FINALIZE = "Done"
|
|
PASSPHRASE = "BIP-39 Passphrase"
|
|
button_data = [FINALIZE]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.seed = self.controller.storage.get_pending_seed()
|
|
self.fingerprint = self.seed.get_fingerprint(network=self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
|
|
|
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED:
|
|
self.button_data.append(self.PASSPHRASE)
|
|
|
|
|
|
def run(self):
|
|
selected_menu_num = self.run_screen(
|
|
seed_screens.SeedFinalizeScreen,
|
|
fingerprint=self.fingerprint,
|
|
button_data=self.button_data,
|
|
)
|
|
|
|
if self.button_data[selected_menu_num] == self.FINALIZE:
|
|
seed_num = self.controller.storage.finalize_pending_seed()
|
|
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
|
|
|
|
elif self.button_data[selected_menu_num] == self.PASSPHRASE:
|
|
return Destination(SeedAddPassphraseView)
|
|
|
|
|
|
|
|
class SeedAddPassphraseView(View):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.seed = self.controller.storage.get_pending_seed()
|
|
|
|
|
|
def run(self):
|
|
ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase)
|
|
|
|
if ret == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
# The new passphrase will be the return value; it might be empty.
|
|
self.seed.set_passphrase(ret)
|
|
if len(self.seed.passphrase) > 0:
|
|
return Destination(SeedReviewPassphraseView)
|
|
else:
|
|
return Destination(SeedFinalizeView)
|
|
|
|
|
|
|
|
class SeedReviewPassphraseView(View):
|
|
"""
|
|
Display the completed passphrase back to the user.
|
|
"""
|
|
EDIT = "Edit passphrase"
|
|
DONE = "Done"
|
|
button_data = [EDIT, DONE]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.seed = self.controller.storage.get_pending_seed()
|
|
|
|
|
|
def run(self):
|
|
# Get the before/after fingerprints
|
|
network = self.settings.get_value(SettingsConstants.SETTING__NETWORK)
|
|
passphrase = self.seed.passphrase
|
|
fingerprint_with = self.seed.get_fingerprint(network=network)
|
|
self.seed.set_passphrase("")
|
|
fingerprint_without = self.seed.get_fingerprint(network=network)
|
|
self.seed.set_passphrase(passphrase)
|
|
|
|
# Because we have an explicit "Edit" button, we disable "BACK" to keep the
|
|
# routing options sane.
|
|
selected_menu_num = self.run_screen(
|
|
seed_screens.SeedReviewPassphraseScreen,
|
|
fingerprint_without=fingerprint_without,
|
|
fingerprint_with=fingerprint_with,
|
|
passphrase=self.seed.passphrase,
|
|
button_data=self.button_data,
|
|
show_back_button=False,
|
|
)
|
|
|
|
if self.button_data[selected_menu_num] == self.EDIT:
|
|
return Destination(SeedAddPassphraseView)
|
|
|
|
elif self.button_data[selected_menu_num] == self.DONE:
|
|
seed_num = self.controller.storage.finalize_pending_seed()
|
|
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
|
|
|
|
|
|
|
|
class SeedDiscardView(View):
|
|
def __init__(self, seed_num: int = None):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
if self.seed_num is not None:
|
|
self.seed = self.controller.get_seed(self.seed_num)
|
|
else:
|
|
self.seed = self.controller.storage.pending_seed
|
|
|
|
|
|
def run(self):
|
|
KEEP = "Keep Seed"
|
|
DISCARD = ("Discard", None, None, "red")
|
|
button_data = [KEEP, DISCARD]
|
|
|
|
fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
|
selected_menu_num = WarningScreen(
|
|
title="Discard Seed?",
|
|
status_headline=None,
|
|
text=f"Wipe seed {fingerprint} from the device?",
|
|
show_back_button=False,
|
|
button_data=button_data,
|
|
).display()
|
|
|
|
if button_data[selected_menu_num] == KEEP:
|
|
# Use skip_current_view=True to prevent BACK from landing on this warning screen
|
|
if self.seed_num is not None:
|
|
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, skip_current_view=True)
|
|
else:
|
|
return Destination(SeedFinalizeView, skip_current_view=True)
|
|
|
|
elif button_data[selected_menu_num] == DISCARD:
|
|
if self.seed_num is not None:
|
|
self.controller.discard_seed(self.seed_num)
|
|
else:
|
|
self.controller.storage.clear_pending_seed()
|
|
return Destination(MainMenuView, clear_history=True)
|
|
|
|
|
|
|
|
"""****************************************************************************
|
|
Views for actions on individual seeds:
|
|
****************************************************************************"""
|
|
class SeedOptionsView(View):
|
|
def __init__(self, seed_num: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.seed = self.controller.get_seed(self.seed_num)
|
|
|
|
|
|
def run(self):
|
|
from seedsigner.views.psbt_views import PSBTOverviewView
|
|
|
|
SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE)
|
|
VERIFY_ADDRESS = "Verify Addr"
|
|
EXPORT_XPUB = "Export Xpub"
|
|
EXPLORER = "Address Explorer"
|
|
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
|
BIP85_CHILD_SEED = "BIP-85 Child Seed"
|
|
DISCARD = ("Discard Seed", None, None, "red")
|
|
|
|
button_data = []
|
|
|
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
|
# Jump straight back into the address explorer script type selection flow
|
|
# But do ont cancel the `resume_main_flow` as we'll still need that after
|
|
# derivation path is specified.
|
|
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
|
|
|
|
if self.controller.unverified_address:
|
|
if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
|
|
# Jump straight back into the single sig addr verification flow
|
|
self.controller.resume_main_flow = None
|
|
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num), skip_current_view=True)
|
|
|
|
addr = self.controller.unverified_address["address"][:7]
|
|
VERIFY_ADDRESS += f" {addr}"
|
|
button_data.append(VERIFY_ADDRESS)
|
|
|
|
if self.controller.psbt:
|
|
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
|
if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
|
|
# Re-route us directly back to the start of the PSBT flow
|
|
self.controller.resume_main_flow = None
|
|
self.controller.psbt_seed = self.seed
|
|
return Destination(PSBTOverviewView, skip_current_view=True)
|
|
|
|
button_data.append(SCAN_PSBT)
|
|
|
|
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
|
|
button_data.append(EXPORT_XPUB)
|
|
|
|
button_data.append(EXPLORER)
|
|
button_data.append(BACKUP)
|
|
|
|
if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
|
button_data.append(BIP85_CHILD_SEED)
|
|
|
|
button_data.append(DISCARD)
|
|
|
|
selected_menu_num = self.run_screen(
|
|
seed_screens.SeedOptionsScreen,
|
|
button_data=button_data,
|
|
fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
|
|
has_passphrase=self.seed.passphrase is not None,
|
|
)
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
# Force BACK to always return to the Main Menu
|
|
return Destination(MainMenuView)
|
|
|
|
if button_data[selected_menu_num] == SCAN_PSBT:
|
|
from seedsigner.views.scan_views import ScanView
|
|
self.controller.psbt_seed = self.controller.get_seed(self.seed_num)
|
|
return Destination(ScanView)
|
|
|
|
elif button_data[selected_menu_num] == VERIFY_ADDRESS:
|
|
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num))
|
|
|
|
elif button_data[selected_menu_num] == EXPORT_XPUB:
|
|
return Destination(SeedExportXpubSigTypeView, view_args=dict(seed_num=self.seed_num))
|
|
|
|
elif button_data[selected_menu_num] == EXPLORER:
|
|
self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
|
|
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG))
|
|
|
|
elif button_data[selected_menu_num] == BACKUP:
|
|
return Destination(SeedBackupView, view_args=dict(seed_num=self.seed_num))
|
|
|
|
elif button_data[selected_menu_num] == BIP85_CHILD_SEED:
|
|
return Destination(SeedBIP85ApplicationModeView, view_args={"seed_num": self.seed_num})
|
|
|
|
elif button_data[selected_menu_num] == DISCARD:
|
|
return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num))
|
|
|
|
|
|
|
|
class SeedBackupView(View):
|
|
def __init__(self, seed_num):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.seed = self.controller.get_seed(self.seed_num)
|
|
|
|
|
|
def run(self):
|
|
VIEW_WORDS = "View Seed Words"
|
|
EXPORT_SEEDQR = "Export as SeedQR"
|
|
button_data = [VIEW_WORDS, EXPORT_SEEDQR]
|
|
|
|
selected_menu_num = ButtonListScreen(
|
|
title="Backup Seed",
|
|
button_data=button_data,
|
|
is_bottom_list=True,
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
elif button_data[selected_menu_num] == VIEW_WORDS:
|
|
return Destination(SeedWordsWarningView, view_args={"seed_num": self.seed_num})
|
|
|
|
elif button_data[selected_menu_num] == EXPORT_SEEDQR:
|
|
return Destination(SeedTranscribeSeedQRFormatView, view_args={"seed_num": self.seed_num})
|
|
|
|
|
|
|
|
"""****************************************************************************
|
|
Export Xpub flow
|
|
****************************************************************************"""
|
|
class SeedExportXpubSigTypeView(View):
|
|
def __init__(self, seed_num: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
|
|
|
|
def run(self):
|
|
if len(self.settings.get_value(SettingsConstants.SETTING__SIG_TYPES)) == 1:
|
|
# Nothing to select; skip this screen
|
|
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": self.settings.get_value(SettingsConstants.SETTING__SIG_TYPES)[0]}, skip_current_view=True)
|
|
|
|
SINGLE_SIG = "Single Sig"
|
|
MULTISIG = "Multisig"
|
|
button_data=[SINGLE_SIG, MULTISIG]
|
|
|
|
selected_menu_num = ButtonListScreen(
|
|
title="Export Xpub",
|
|
button_data=button_data
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
if button_data[selected_menu_num] == SINGLE_SIG:
|
|
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.SINGLE_SIG})
|
|
|
|
elif button_data[selected_menu_num] == MULTISIG:
|
|
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.MULTISIG})
|
|
|
|
|
|
|
|
class SeedExportXpubScriptTypeView(View):
|
|
def __init__(self, seed_num: int, sig_type: str):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.sig_type = sig_type
|
|
|
|
self.button_data = []
|
|
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
|
|
self.button_data.append(script_type)
|
|
|
|
|
|
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:
|
|
# Nothing to select; skip this screen
|
|
args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0]
|
|
|
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
|
del args["sig_type"]
|
|
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True)
|
|
else:
|
|
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
|
|
|
|
title = "Export Xpub"
|
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
|
title = "Address Explorer"
|
|
|
|
selected_menu_num = self.run_screen(
|
|
ButtonListScreen,
|
|
title=title,
|
|
is_button_text_centered=False,
|
|
button_data=self.button_data,
|
|
is_bottom_list=True,
|
|
)
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
# If previous view is SeedOptionsView then that should be where resume_main_flow started (otherwise it would have been skipped).
|
|
if len(self.controller.back_stack) >= 2 and self.controller.back_stack[-2].View_cls == SeedOptionsView:
|
|
self.controller.resume_main_flow = None
|
|
return Destination(BackStackView)
|
|
|
|
else:
|
|
script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES)
|
|
selected_display_name = self.button_data[selected_menu_num]
|
|
args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name)
|
|
|
|
if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION:
|
|
return Destination(SeedExportXpubCustomDerivationView, view_args=args)
|
|
|
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
|
del args["sig_type"]
|
|
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args)
|
|
else:
|
|
return Destination(SeedExportXpubCoordinatorView, view_args=args)
|
|
|
|
|
|
|
|
class SeedExportXpubCustomDerivationView(View):
|
|
def __init__(self, seed_num: int, sig_type: str, script_type: str):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.sig_type = sig_type
|
|
self.script_type = script_type
|
|
self.custom_derivation_path = "m/"
|
|
|
|
def run(self):
|
|
ret = seed_screens.SeedExportXpubCustomDerivationScreen(
|
|
initial_value=self.custom_derivation_path,
|
|
).display()
|
|
|
|
if ret == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
# ret will be the custom derivation path
|
|
custom_derivation = ret
|
|
|
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
|
from .tools_views import ToolsAddressExplorerAddressTypeView
|
|
return Destination(ToolsAddressExplorerAddressTypeView, view_args=dict(seed_num=self.seed_num, script_type=self.script_type, custom_derivation=custom_derivation))
|
|
|
|
return Destination(
|
|
SeedExportXpubCoordinatorView,
|
|
view_args={
|
|
"seed_num": self.seed_num,
|
|
"sig_type": self.sig_type,
|
|
"script_type": self.script_type,
|
|
"custom_derivation": custom_derivation,
|
|
}
|
|
)
|
|
|
|
|
|
|
|
class SeedExportXpubCoordinatorView(View):
|
|
def __init__(self, seed_num: int, sig_type: str, script_type: str, custom_derivation: str = None):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.sig_type = sig_type
|
|
self.script_type = script_type
|
|
self.custom_derivation = custom_derivation
|
|
|
|
|
|
def run(self):
|
|
args = {
|
|
"seed_num": self.seed_num,
|
|
"sig_type": self.sig_type,
|
|
"script_type": self.script_type,
|
|
"custom_derivation": self.custom_derivation,
|
|
}
|
|
if len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)) == 1:
|
|
# Nothing to select; skip this screen
|
|
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
|
|
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
|
|
|
|
selected_menu_num = ButtonListScreen(
|
|
title="Export Xpub",
|
|
is_button_text_centered=False,
|
|
button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS),
|
|
).display()
|
|
|
|
if selected_menu_num < len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)):
|
|
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[selected_menu_num]
|
|
return Destination(SeedExportXpubWarningView, view_args=args)
|
|
|
|
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
|
|
|
|
class SeedExportXpubWarningView(View):
|
|
def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.sig_type = sig_type
|
|
self.script_type = script_type
|
|
self.coordinator = coordinator
|
|
self.custom_derivation = custom_derivation
|
|
|
|
|
|
def run(self):
|
|
destination = Destination(
|
|
SeedExportXpubDetailsView,
|
|
view_args={
|
|
"seed_num": self.seed_num,
|
|
"sig_type": self.sig_type,
|
|
"script_type": self.script_type,
|
|
"coordinator": self.coordinator,
|
|
"custom_derivation": self.custom_derivation,
|
|
},
|
|
skip_current_view=True, # Prevent going BACK to WarningViews
|
|
)
|
|
|
|
if self.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__DISABLED:
|
|
# Skip the WarningView entirely
|
|
return destination
|
|
|
|
selected_menu_num = WarningScreen(
|
|
status_headline="Privacy Leak!",
|
|
text="""Xpub can be used to view all future transactions.""",
|
|
).display()
|
|
|
|
if selected_menu_num == 0:
|
|
# User clicked "I Understand"
|
|
return destination
|
|
|
|
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
|
|
|
|
class SeedExportXpubDetailsView(View):
|
|
"""
|
|
Collects the user input from all the previous screens leading up to this and
|
|
finally calculates the xpub and displays the summary view to the user.
|
|
"""
|
|
def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str):
|
|
super().__init__()
|
|
self.sig_type = sig_type
|
|
self.script_type = script_type
|
|
self.coordinator = coordinator
|
|
self.custom_derivation = custom_derivation
|
|
|
|
self.seed_num = seed_num
|
|
self.seed = self.controller.get_seed(self.seed_num)
|
|
|
|
|
|
def run(self):
|
|
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
|
|
derivation_path = self.custom_derivation
|
|
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
|
|
)
|
|
|
|
if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__DISABLED:
|
|
# We're just skipping right past this screen
|
|
selected_menu_num = 0
|
|
|
|
else:
|
|
# The derivation calc takes a few moments. Run the loading screen while we wait.
|
|
self.loading_screen = LoadingScreenThread(text="Generating xpub...")
|
|
self.loading_screen.start()
|
|
|
|
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
|
|
)
|
|
root = embit.bip32.HDKey.from_seed(
|
|
self.seed.seed_bytes,
|
|
version=embit_network["xprv"]
|
|
)
|
|
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
|
|
xprv = root.derive(derivation_path)
|
|
xpub = xprv.to_public()
|
|
xpub_base58 = xpub.to_string(version=version)
|
|
|
|
finally:
|
|
self.loading_screen.stop()
|
|
|
|
selected_menu_num = seed_screens.SeedExportXpubDetailsScreen(
|
|
fingerprint=fingerprint,
|
|
has_passphrase=self.seed.passphrase is not None,
|
|
derivation_path=derivation_path,
|
|
xpub=xpub_base58,
|
|
).display()
|
|
|
|
if selected_menu_num == 0:
|
|
return Destination(
|
|
SeedExportXpubQRDisplayView,
|
|
dict(seed_num=self.seed_num,
|
|
coordinator=self.coordinator,
|
|
derivation_path=derivation_path,
|
|
)
|
|
)
|
|
|
|
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
|
|
|
|
class SeedExportXpubQRDisplayView(View):
|
|
def __init__(self, seed_num: int, coordinator: str, derivation_path: str):
|
|
super().__init__()
|
|
self.seed = self.controller.get_seed(seed_num)
|
|
|
|
qr_density = self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
|
|
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
|
|
qr_type = QRType.XPUB__SPECTER
|
|
|
|
elif coordinator == SettingsConstants.COORDINATOR__BLUE_WALLET:
|
|
qr_type = QRType.XPUB
|
|
|
|
elif coordinator == SettingsConstants.COORDINATOR__KEEPER:
|
|
qr_type = QRType.XPUB
|
|
|
|
elif coordinator == SettingsConstants.COORDINATOR__NUNCHUK:
|
|
qr_type = QRType.XPUB__UR
|
|
|
|
# As of 2022-03-02 Nunchuk doesn't seem to support animated QRs for Xpub import
|
|
qr_density = SettingsConstants.DENSITY__HIGH
|
|
|
|
else:
|
|
qr_type = QRType.XPUB__UR
|
|
|
|
self.qr_encoder = EncodeQR(
|
|
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
|
|
)
|
|
|
|
|
|
def run(self):
|
|
QRDisplayScreen(qr_encoder=self.qr_encoder).display()
|
|
|
|
return Destination(MainMenuView)
|
|
|
|
|
|
|
|
"""****************************************************************************
|
|
View Seed Words flow
|
|
****************************************************************************"""
|
|
class SeedWordsWarningView(View):
|
|
def __init__(self, seed_num: int, bip85_data: dict = None):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.bip85_data = bip85_data
|
|
|
|
|
|
def run(self):
|
|
destination = Destination(
|
|
SeedWordsView,
|
|
view_args=dict(
|
|
seed_num=self.seed_num,
|
|
page_index=0,
|
|
bip85_data=self.bip85_data
|
|
),
|
|
skip_current_view=True, # Prevent going BACK to WarningViews
|
|
)
|
|
if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED:
|
|
# Forward straight to showing the words
|
|
return destination
|
|
|
|
selected_menu_num = DireWarningScreen(
|
|
text="""You must keep your seed words private & away from all online devices.""",
|
|
).display()
|
|
|
|
if selected_menu_num == 0:
|
|
# User clicked "I Understand"
|
|
return destination
|
|
|
|
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
|
|
|
|
class SeedWordsView(View):
|
|
def __init__(self, seed_num: int, bip85_data: dict = None, page_index: int = 0):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
if self.seed_num is None:
|
|
self.seed = self.controller.storage.get_pending_seed()
|
|
else:
|
|
self.seed = self.controller.get_seed(self.seed_num)
|
|
self.bip85_data = bip85_data
|
|
self.page_index = page_index
|
|
|
|
|
|
def run(self):
|
|
NEXT = "Next"
|
|
DONE = "Done"
|
|
|
|
# Slice the mnemonic to our current 4-word section
|
|
words_per_page = 4 # TODO: eventually make this configurable for bigger screens?
|
|
|
|
if self.bip85_data is not None:
|
|
mnemonic = self.seed.get_bip85_child_mnemonic(self.bip85_data["child_index"], self.bip85_data["num_words"]).split()
|
|
title = f"""Child #{self.bip85_data["child_index"]}"""
|
|
else:
|
|
mnemonic = self.seed.mnemonic_display_list
|
|
title = "Seed Words"
|
|
words = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page]
|
|
|
|
button_data = []
|
|
num_pages = int(len(mnemonic)/words_per_page)
|
|
if self.page_index < num_pages - 1 or self.seed_num is None:
|
|
button_data.append(NEXT)
|
|
else:
|
|
button_data.append(DONE)
|
|
|
|
selected_menu_num = seed_screens.SeedWordsScreen(
|
|
title=f"{title}: {self.page_index+1}/{num_pages}",
|
|
words=words,
|
|
page_index=self.page_index,
|
|
num_pages=num_pages,
|
|
button_data=button_data,
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
if button_data[selected_menu_num] == NEXT:
|
|
if self.seed_num is None and self.page_index == num_pages - 1:
|
|
return Destination(
|
|
SeedWordsBackupTestPromptView,
|
|
view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data),
|
|
)
|
|
else:
|
|
return Destination(
|
|
SeedWordsView,
|
|
view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1, bip85_data=self.bip85_data)
|
|
)
|
|
|
|
elif button_data[selected_menu_num] == DONE:
|
|
# Must clear history to avoid BACK button returning to private info
|
|
return Destination(
|
|
SeedWordsBackupTestPromptView,
|
|
view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data),
|
|
)
|
|
|
|
|
|
|
|
"""****************************************************************************
|
|
BIP85 - Derive child mnemonic (seed) flow
|
|
****************************************************************************"""
|
|
class SeedBIP85ApplicationModeView(View):
|
|
"""
|
|
* Ask the user the application type as defined in the BIP0085 spec.
|
|
* Currently only Word mode of 12, 24 words (Application number: 39')
|
|
* Possible future additions are
|
|
* WIF (HDSEED)
|
|
* XPRV (BIP32)
|
|
"""
|
|
def __init__(self, seed_num: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.num_words = 0
|
|
self.bip85_app_num = 39 # TODO: Support other Application numbers
|
|
|
|
|
|
def run(self):
|
|
# TODO: Future enhancement to display WIF (HD-SEED) and XPRV (Bip32)?
|
|
WORDS_12 = "12 Words"
|
|
WORDS_24 = "24 Words"
|
|
|
|
button_data = [WORDS_12, WORDS_24]
|
|
|
|
selected_menu_num = ButtonListScreen(
|
|
title="BIP-85 Num Words",
|
|
button_data=button_data
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
if button_data[selected_menu_num] == WORDS_12:
|
|
self.num_words = 12
|
|
elif button_data[selected_menu_num] == WORDS_24:
|
|
self.num_words = 24
|
|
|
|
return Destination(
|
|
SeedBIP85SelectChildIndexView,
|
|
view_args=dict(seed_num=self.seed_num, num_words=self.num_words)
|
|
)
|
|
|
|
|
|
|
|
# View to retrieve the derived seed index
|
|
class SeedBIP85SelectChildIndexView(View):
|
|
def __init__(self, seed_num: int, num_words: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.num_words = num_words
|
|
|
|
|
|
def run(self):
|
|
# Change this later to use the generic Screen input keyboard
|
|
ret = seed_screens.SeedBIP85SelectChildIndexScreen().display()
|
|
|
|
if ret == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
return Destination(
|
|
SeedWordsWarningView,
|
|
view_args=dict(
|
|
seed_num=self.seed_num,
|
|
bip85_data=dict(child_index=int(ret), num_words=self.num_words),
|
|
)
|
|
)
|
|
|
|
|
|
|
|
"""****************************************************************************
|
|
Seed Words Backup Test
|
|
****************************************************************************"""
|
|
class SeedWordsBackupTestPromptView(View):
|
|
def __init__(self, seed_num: int, bip85_data: dict = None):
|
|
self.seed_num = seed_num
|
|
self.bip85_data = bip85_data
|
|
|
|
|
|
def run(self):
|
|
VERIFY = "Verify"
|
|
SKIP = "Skip"
|
|
button_data = [VERIFY, SKIP]
|
|
selected_menu_num = seed_screens.SeedWordsBackupTestPromptScreen(
|
|
button_data=button_data,
|
|
).display()
|
|
|
|
if button_data[selected_menu_num] == VERIFY:
|
|
return Destination(
|
|
SeedWordsBackupTestView,
|
|
view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data),
|
|
)
|
|
|
|
elif button_data[selected_menu_num] == SKIP:
|
|
if self.seed_num is not None:
|
|
return Destination(SeedOptionsView, view_args=dict(seed_num=self.seed_num))
|
|
else:
|
|
return Destination(SeedFinalizeView)
|
|
|
|
|
|
|
|
class SeedWordsBackupTestView(View):
|
|
def __init__(self, seed_num: int, bip85_data: dict = None, confirmed_list: List[bool] = None, cur_index: int = None):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
if self.seed_num is None:
|
|
self.seed = self.controller.storage.get_pending_seed()
|
|
else:
|
|
self.seed = self.controller.get_seed(self.seed_num)
|
|
self.bip85_data = bip85_data
|
|
|
|
if self.bip85_data is not None:
|
|
self.mnemonic_list = self.seed.get_bip85_child_mnemonic(self.bip85_data["child_index"], self.bip85_data["num_words"]).split()
|
|
else:
|
|
self.mnemonic_list = self.seed.mnemonic_display_list
|
|
|
|
self.confirmed_list = confirmed_list
|
|
if not self.confirmed_list:
|
|
self.confirmed_list = []
|
|
|
|
self.cur_index = cur_index
|
|
|
|
|
|
def run(self):
|
|
if self.cur_index is None:
|
|
self.cur_index = int(random.random() * len(self.mnemonic_list))
|
|
while self.cur_index in self.confirmed_list:
|
|
self.cur_index = int(random.random() * len(self.mnemonic_list))
|
|
|
|
real_word = self.mnemonic_list[self.cur_index]
|
|
fake_word1 = bip39.WORDLIST[int(random.random() * 2047)]
|
|
fake_word2 = bip39.WORDLIST[int(random.random() * 2047)]
|
|
fake_word3 = bip39.WORDLIST[int(random.random() * 2047)]
|
|
|
|
button_data = [real_word, fake_word1, fake_word2, fake_word3]
|
|
random.shuffle(button_data)
|
|
|
|
selected_menu_num = ButtonListScreen(
|
|
title=f"Verify Word #{self.cur_index + 1}",
|
|
show_back_button=False,
|
|
button_data=button_data,
|
|
is_bottom_list=True,
|
|
is_button_text_centered=True,
|
|
).display()
|
|
|
|
if button_data[selected_menu_num] == real_word:
|
|
self.confirmed_list.append(self.cur_index)
|
|
if len(self.confirmed_list) == len(self.mnemonic_list):
|
|
# Successfully confirmed the full mnemonic!
|
|
return Destination(
|
|
SeedWordsBackupTestSuccessView,
|
|
view_args=dict(seed_num=self.seed_num),
|
|
)
|
|
else:
|
|
# Continue testing the remaining words
|
|
return Destination(
|
|
SeedWordsBackupTestView,
|
|
view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, bip85_data=self.bip85_data),
|
|
)
|
|
|
|
else:
|
|
# Picked the WRONG WORD!
|
|
return Destination(
|
|
SeedWordsBackupTestMistakeView,
|
|
view_args=dict(
|
|
seed_num=self.seed_num,
|
|
bip85_data=self.bip85_data,
|
|
cur_index=self.cur_index,
|
|
wrong_word=button_data[selected_menu_num],
|
|
confirmed_list=self.confirmed_list,
|
|
)
|
|
)
|
|
|
|
|
|
|
|
class SeedWordsBackupTestMistakeView(View):
|
|
def __init__(self, seed_num: int, bip85_data: dict = None, cur_index: int = None, wrong_word: str = None, confirmed_list: List[bool] = None):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.bip85_data = bip85_data
|
|
self.cur_index = cur_index
|
|
self.wrong_word = wrong_word
|
|
self.confirmed_list = confirmed_list
|
|
|
|
|
|
def run(self):
|
|
REVIEW = "Review Seed Words"
|
|
RETRY = "Try Again"
|
|
button_data = [REVIEW, RETRY]
|
|
|
|
selected_menu_num = DireWarningScreen(
|
|
title="Verification Error",
|
|
show_back_button=False,
|
|
status_headline=f"Wrong Word!",
|
|
text=f"Word #{self.cur_index + 1} is not \"{self.wrong_word}\"!",
|
|
button_data=button_data,
|
|
).display()
|
|
|
|
if button_data[selected_menu_num] == REVIEW:
|
|
return Destination(
|
|
SeedWordsView,
|
|
view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data),
|
|
)
|
|
|
|
elif button_data[selected_menu_num] == RETRY:
|
|
return Destination(
|
|
SeedWordsBackupTestView,
|
|
view_args=dict(
|
|
seed_num=self.seed_num,
|
|
confirmed_list=self.confirmed_list,
|
|
cur_index=self.cur_index,
|
|
bip85_data=self.bip85_data,
|
|
)
|
|
)
|
|
|
|
|
|
|
|
class SeedWordsBackupTestSuccessView(View):
|
|
def __init__(self, seed_num: int):
|
|
self.seed_num = seed_num
|
|
|
|
def run(self):
|
|
LargeIconStatusScreen(
|
|
title="Backup Verified",
|
|
show_back_button=False,
|
|
status_headline="Success!",
|
|
text="All mnemonic backup words were successfully verified!",
|
|
button_data=["OK"]
|
|
).display()
|
|
|
|
if self.seed_num is not None:
|
|
return Destination(SeedOptionsView, view_args=dict(seed_num=self.seed_num), clear_history=True)
|
|
else:
|
|
return Destination(SeedFinalizeView)
|
|
|
|
|
|
|
|
"""****************************************************************************
|
|
Export as SeedQR
|
|
****************************************************************************"""
|
|
class SeedTranscribeSeedQRFormatView(View):
|
|
def __init__(self, seed_num: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
|
|
|
|
def run(self):
|
|
seed = self.controller.get_seed(self.seed_num)
|
|
if len(seed.mnemonic_list) == 12:
|
|
STANDARD = "Standard: 25x25"
|
|
COMPACT = "Compact: 21x21"
|
|
num_modules_standard = 25
|
|
num_modules_compact = 21
|
|
else:
|
|
STANDARD = "Standard: 29x29"
|
|
COMPACT = "Compact: 25x25"
|
|
num_modules_standard = 29
|
|
num_modules_compact = 25
|
|
|
|
if self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED:
|
|
# Only configured for standard SeedQR
|
|
return Destination(
|
|
SeedTranscribeSeedQRWarningView,
|
|
view_args={
|
|
"seed_num": self.seed_num,
|
|
"seedqr_format": QRType.SEED__SEEDQR,
|
|
"num_modules": num_modules_standard,
|
|
},
|
|
skip_current_view=True,
|
|
)
|
|
|
|
button_data = [STANDARD, COMPACT]
|
|
|
|
selected_menu_num = seed_screens.SeedTranscribeSeedQRFormatScreen(
|
|
title="SeedQR Format",
|
|
button_data=button_data,
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
if button_data[selected_menu_num] == STANDARD:
|
|
seedqr_format = QRType.SEED__SEEDQR
|
|
num_modules = num_modules_standard
|
|
else:
|
|
seedqr_format = QRType.SEED__COMPACTSEEDQR
|
|
num_modules = num_modules_compact
|
|
|
|
return Destination(
|
|
SeedTranscribeSeedQRWarningView,
|
|
view_args={
|
|
"seed_num": self.seed_num,
|
|
"seedqr_format": seedqr_format,
|
|
"num_modules": num_modules,
|
|
}
|
|
)
|
|
|
|
|
|
|
|
class SeedTranscribeSeedQRWarningView(View):
|
|
def __init__(self, seed_num: int, seedqr_format: str = QRType.SEED__SEEDQR, num_modules: int = 29):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.seedqr_format = seedqr_format
|
|
self.num_modules = num_modules
|
|
|
|
|
|
def run(self):
|
|
destination = Destination(
|
|
SeedTranscribeSeedQRWholeQRView,
|
|
view_args={
|
|
"seed_num": self.seed_num,
|
|
"seedqr_format": self.seedqr_format,
|
|
"num_modules": self.num_modules,
|
|
},
|
|
skip_current_view=True, # Prevent going BACK to WarningViews
|
|
)
|
|
|
|
if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED:
|
|
# Forward straight to transcribing the SeedQR
|
|
return destination
|
|
|
|
selected_menu_num = DireWarningScreen(
|
|
status_headline="SeedQR is your private key!",
|
|
text="""Never photograph it or scan it into an online device.""",
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
else:
|
|
# User clicked "I Understand"
|
|
return destination
|
|
|
|
|
|
|
|
class SeedTranscribeSeedQRWholeQRView(View):
|
|
def __init__(self, seed_num: int, seedqr_format: str, num_modules: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.seedqr_format = seedqr_format
|
|
self.num_modules = num_modules
|
|
self.seed = self.controller.get_seed(seed_num)
|
|
|
|
|
|
def run(self):
|
|
e = EncodeQR(
|
|
seed_phrase=self.seed.mnemonic_list,
|
|
qr_type=self.seedqr_format,
|
|
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
|
)
|
|
data = e.next_part()
|
|
|
|
ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen(
|
|
qr_data=data,
|
|
num_modules=self.num_modules,
|
|
).display()
|
|
|
|
if ret == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
else:
|
|
return Destination(
|
|
SeedTranscribeSeedQRZoomedInView,
|
|
view_args={
|
|
"seed_num": self.seed_num,
|
|
"seedqr_format": self.seedqr_format
|
|
}
|
|
)
|
|
|
|
|
|
|
|
class SeedTranscribeSeedQRZoomedInView(View):
|
|
def __init__(self, seed_num: int, seedqr_format: str):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.seedqr_format = seedqr_format
|
|
self.seed = self.controller.get_seed(seed_num)
|
|
|
|
|
|
def run(self):
|
|
e = EncodeQR(
|
|
seed_phrase=self.seed.mnemonic_list,
|
|
qr_type=self.seedqr_format,
|
|
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
|
)
|
|
data = e.next_part()
|
|
|
|
if len(self.seed.mnemonic_list) == 24:
|
|
if self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
|
num_modules = 25
|
|
else:
|
|
num_modules = 29
|
|
else:
|
|
if self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
|
num_modules = 21
|
|
else:
|
|
num_modules = 25
|
|
|
|
seed_screens.SeedTranscribeSeedQRZoomedInScreen(
|
|
qr_data=data,
|
|
num_modules=num_modules,
|
|
).display()
|
|
|
|
return Destination(SeedTranscribeSeedQRConfirmQRPromptView, view_args={"seed_num": self.seed_num})
|
|
|
|
|
|
|
|
class SeedTranscribeSeedQRConfirmQRPromptView(View):
|
|
def __init__(self, seed_num: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.seed = self.controller.get_seed(seed_num)
|
|
|
|
|
|
def run(self):
|
|
SCAN = ("Confirm SeedQR", FontAwesomeIconConstants.QRCODE)
|
|
DONE = "Done"
|
|
button_data = [SCAN, DONE]
|
|
|
|
selected_menu_option = seed_screens.SeedTranscribeSeedQRConfirmQRPromptScreen(
|
|
title="Confirm SeedQR?",
|
|
button_data=button_data,
|
|
).display()
|
|
|
|
if selected_menu_option == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
elif button_data[selected_menu_option] == SCAN:
|
|
return Destination(SeedTranscribeSeedQRConfirmScanView, view_args={"seed_num": self.seed_num})
|
|
|
|
elif button_data[selected_menu_option] == DONE:
|
|
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True)
|
|
|
|
|
|
|
|
class SeedTranscribeSeedQRConfirmScanView(View):
|
|
def __init__(self, seed_num: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
self.seed = self.controller.get_seed(seed_num)
|
|
|
|
def run(self):
|
|
from seedsigner.gui.screens.scan_screens import ScanScreen
|
|
|
|
# Run the live preview and QR code capture process
|
|
# TODO: Does this belong in its own BaseThread?
|
|
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
|
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
|
|
ScanScreen(decoder=self.decoder, instructions_text="Scan your SeedQR").display()
|
|
|
|
if self.decoder.is_complete:
|
|
if self.decoder.is_seed:
|
|
seed_mnemonic = self.decoder.get_seed_phrase()
|
|
# Found a valid mnemonic seed! But does it match?
|
|
if seed_mnemonic != self.seed.mnemonic_list:
|
|
DireWarningScreen(
|
|
title="Confirm SeedQR",
|
|
status_headline="Error!",
|
|
text="Your transcribed SeedQR does not match your original seed!",
|
|
show_back_button=False,
|
|
button_data=["Review SeedQR"],
|
|
).display()
|
|
|
|
return Destination(BackStackView, skip_current_view=True)
|
|
|
|
else:
|
|
LargeIconStatusScreen(
|
|
title="Confirm SeedQR",
|
|
status_headline="Success!",
|
|
text="Your transcribed SeedQR successfully scanned and yielded the same seed.",
|
|
show_back_button=False,
|
|
button_data=["OK"],
|
|
).display()
|
|
|
|
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num})
|
|
|
|
else:
|
|
# Will this case ever happen? Will trigger if a different kind of QR code is scanned
|
|
DireWarningScreen(
|
|
title="Confirm SeedQR",
|
|
status_headline="Error!",
|
|
text="Your transcribed SeedQR could not be read!",
|
|
show_back_button=False,
|
|
button_data=["Review SeedQR"],
|
|
).display()
|
|
|
|
return Destination(BackStackView, skip_current_view=True)
|
|
|
|
|
|
|
|
"""****************************************************************************
|
|
Address verification
|
|
****************************************************************************"""
|
|
class AddressVerificationStartView(View):
|
|
def __init__(self, address: str, script_type: str, network: str):
|
|
super().__init__()
|
|
self.controller.unverified_address = dict(
|
|
address=address,
|
|
script_type=script_type,
|
|
network=network
|
|
)
|
|
|
|
|
|
def run(self):
|
|
if self.controller.unverified_address["script_type"] == SettingsConstants.NESTED_SEGWIT:
|
|
# No way to differentiate single sig from multisig
|
|
return Destination(AddressVerificationSigTypeView, skip_current_view=True)
|
|
|
|
if self.controller.unverified_address["script_type"] == SettingsConstants.NATIVE_SEGWIT:
|
|
if len(self.controller.unverified_address["address"]) >= 62:
|
|
# Mainnet/testnet are 62, regtest is 64
|
|
sig_type = SettingsConstants.MULTISIG
|
|
if self.controller.multisig_wallet_descriptor:
|
|
# Can jump straight to the brute-force verification View
|
|
destination = Destination(SeedAddressVerificationView, skip_current_view=True)
|
|
else:
|
|
self.controller.resume_main_flow = Controller.FLOW__VERIFY_MULTISIG_ADDR
|
|
destination = Destination(LoadMultisigWalletDescriptorView, skip_current_view=True)
|
|
|
|
else:
|
|
sig_type = SettingsConstants.SINGLE_SIG
|
|
destination = Destination(SeedSingleSigAddressVerificationSelectSeedView, skip_current_view=True)
|
|
|
|
elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT:
|
|
# TODO: add Taproot support
|
|
return Destination(NotYetImplementedView)
|
|
|
|
elif self.controller.unverified_address["script_type"] == SettingsConstants.LEGACY_P2PKH:
|
|
# TODO: detect single sig vs multisig or have to prompt?
|
|
return Destination(NotYetImplementedView)
|
|
|
|
derivation_path = embit_utils.get_standard_derivation_path(
|
|
network=self.controller.unverified_address["network"],
|
|
wallet_type=sig_type,
|
|
script_type=self.controller.unverified_address["script_type"]
|
|
)
|
|
|
|
self.controller.unverified_address["sig_type"] = sig_type
|
|
self.controller.unverified_address["derivation_path"] = derivation_path
|
|
|
|
return destination
|
|
|
|
|
|
|
|
class AddressVerificationSigTypeView(View):
|
|
def run(self):
|
|
sig_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES)
|
|
SINGLE_SIG = sig_type_settings_entry.get_selection_option_display_name_by_value(SettingsConstants.SINGLE_SIG)
|
|
MULTISIG = sig_type_settings_entry.get_selection_option_display_name_by_value(SettingsConstants.MULTISIG)
|
|
|
|
button_data = [SINGLE_SIG, MULTISIG]
|
|
selected_menu_num = seed_screens.AddressVerificationSigTypeScreen(
|
|
title="Verify Address",
|
|
text="Sig type can't be auto-detected from this address. Please specify:",
|
|
button_data=button_data,
|
|
is_bottom_list=True,
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
self.controller.unverified_address = None
|
|
return Destination(BackStackView)
|
|
|
|
elif button_data[selected_menu_num] == SINGLE_SIG:
|
|
sig_type = SettingsConstants.SINGLE_SIG
|
|
destination = Destination(SeedSingleSigAddressVerificationSelectSeedView)
|
|
|
|
elif button_data[selected_menu_num] == MULTISIG:
|
|
sig_type = SettingsConstants.MULTISIG
|
|
if self.controller.multisig_wallet_descriptor:
|
|
destination = Destination(SeedAddressVerificationView)
|
|
else:
|
|
self.controller.resume_main_flow = Controller.FLOW__VERIFY_MULTISIG_ADDR
|
|
destination = Destination(LoadMultisigWalletDescriptorView)
|
|
|
|
self.controller.unverified_address["sig_type"] = sig_type
|
|
derivation_path = embit_utils.get_standard_derivation_path(
|
|
network=self.controller.unverified_address["network"],
|
|
wallet_type=sig_type,
|
|
script_type=self.controller.unverified_address["script_type"]
|
|
)
|
|
self.controller.unverified_address["derivation_path"] = derivation_path
|
|
|
|
return destination
|
|
|
|
|
|
|
|
class SeedSingleSigAddressVerificationSelectSeedView(View):
|
|
def run(self):
|
|
seeds = self.controller.storage.seeds
|
|
|
|
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
|
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
|
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
|
button_data = []
|
|
|
|
text = "Load the seed to verify"
|
|
|
|
for seed in seeds:
|
|
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
|
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
|
text = "Select seed to verify"
|
|
|
|
button_data.append(SCAN_SEED)
|
|
button_data.append(TYPE_12WORD)
|
|
button_data.append(TYPE_24WORD)
|
|
|
|
selected_menu_num = seed_screens.SeedSingleSigAddressVerificationSelectSeedScreen(
|
|
title="Verify Address",
|
|
text=text,
|
|
is_button_text_centered=False,
|
|
button_data=button_data
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
return Destination(BackStackView)
|
|
|
|
if len(seeds) > 0 and selected_menu_num < len(seeds):
|
|
# User selected one of the n seeds
|
|
return Destination(
|
|
SeedAddressVerificationView,
|
|
view_args=dict(
|
|
seed_num=selected_menu_num,
|
|
)
|
|
)
|
|
|
|
self.controller.resume_main_flow = Controller.FLOW__VERIFY_SINGLESIG_ADDR
|
|
|
|
if button_data[selected_menu_num] == SCAN_SEED:
|
|
from seedsigner.views.scan_views import ScanView
|
|
return Destination(ScanView)
|
|
|
|
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
|
|
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
|
if button_data[selected_menu_num] == TYPE_12WORD:
|
|
self.controller.storage.init_pending_mnemonic(num_words=12)
|
|
else:
|
|
self.controller.storage.init_pending_mnemonic(num_words=24)
|
|
return Destination(SeedMnemonicEntryView)
|
|
|
|
|
|
|
|
class SeedAddressVerificationView(View):
|
|
"""
|
|
Creates a worker thread to brute-force calculate addresses. Writes its
|
|
iteration status to a shared `ThreadsafeCounter`.
|
|
|
|
The `ThreadsafeCounter` is sent to the display Screen which is monitored in
|
|
its own `ProgressThread` to show the current iteration onscreen.
|
|
|
|
Performs single sig verification on `seed_num` if specified, otherwise assumes
|
|
multisig.
|
|
"""
|
|
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
|
|
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)
|
|
else:
|
|
self.seed = None
|
|
self.address = self.controller.unverified_address["address"]
|
|
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"]
|
|
self.network = self.controller.unverified_address["network"]
|
|
|
|
if self.script_type == SettingsConstants.TAPROOT:
|
|
# TODO: Taproot addr verification
|
|
return Destination(NotYetImplementedView)
|
|
|
|
# TODO: This should be in `Seed` or `PSBT` utility class
|
|
embit_network = SettingsConstants.map_network_to_embit(self.network)
|
|
|
|
# The ThreadsafeCounter will be shared by the brute-force thread to keep track of
|
|
# its current addr index number and the Screen to display its progress and
|
|
# respond to UI requests to jump the index ahead.
|
|
self.threadsafe_counter = ThreadsafeCounter()
|
|
|
|
# Shared coordination var so the display thread can detect success
|
|
self.verified_index = ThreadsafeCounter(initial_value=None)
|
|
self.verified_index_is_change = ThreadsafeCounter(initial_value=None)
|
|
|
|
# Create the brute-force calculation thread that will run in the background
|
|
self.addr_verification_thread = self.BruteForceAddressVerificationThread(
|
|
address=self.address,
|
|
seed=self.seed,
|
|
descriptor=self.controller.multisig_wallet_descriptor,
|
|
script_type=self.script_type,
|
|
embit_network=embit_network,
|
|
derivation_path=self.derivation_path,
|
|
threadsafe_counter=self.threadsafe_counter,
|
|
verified_index=self.verified_index,
|
|
verified_index_is_change=self.verified_index_is_change,
|
|
)
|
|
|
|
|
|
def run(self):
|
|
# Start brute-force calculations from the zero-th index
|
|
self.addr_verification_thread.start()
|
|
|
|
SKIP_10 = "Skip 10"
|
|
CANCEL = "Cancel"
|
|
button_data = [SKIP_10, CANCEL]
|
|
|
|
script_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES)
|
|
script_type_display = script_type_settings_entry.get_selection_option_display_name_by_value(self.script_type)
|
|
|
|
sig_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES)
|
|
sig_type_display = sig_type_settings_entry.get_selection_option_display_name_by_value(self.sig_type)
|
|
|
|
network_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK)
|
|
network_display = network_settings_entry.get_selection_option_display_name_by_value(self.network)
|
|
mainnet = network_settings_entry.get_selection_option_display_name_by_value(SettingsConstants.MAINNET)
|
|
|
|
# Display the Screen to show the brute-forcing progress.
|
|
# Using a loop here to handle the SKIP_10 button presses to increment the counter
|
|
# and resume displaying the screen. User won't even notice that the Screen is
|
|
# being re-constructed.
|
|
while True:
|
|
selected_menu_num = seed_screens.SeedAddressVerificationScreen(
|
|
address=self.address,
|
|
derivation_path=self.derivation_path,
|
|
script_type=script_type_display,
|
|
sig_type=sig_type_display,
|
|
network=network_display,
|
|
is_mainnet=network_display == mainnet,
|
|
threadsafe_counter=self.threadsafe_counter,
|
|
verified_index=self.verified_index,
|
|
button_data=button_data,
|
|
).display()
|
|
|
|
if self.verified_index.cur_count is not None:
|
|
break
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
break
|
|
|
|
if button_data[selected_menu_num] == SKIP_10:
|
|
self.threadsafe_counter.increment(10)
|
|
|
|
elif button_data[selected_menu_num] == CANCEL:
|
|
break
|
|
|
|
if self.verified_index.cur_count is not None:
|
|
# Successfully verified the addr; update the data
|
|
self.controller.unverified_address["verified_index"] = self.verified_index.cur_count
|
|
self.controller.unverified_address["verified_index_is_change"] = self.verified_index_is_change.cur_count == 1
|
|
return Destination(AddressVerificationSuccessView, view_args=dict(seed_num=self.seed_num))
|
|
|
|
else:
|
|
# Halt the thread if the user gave up (will already be stopped if it verified the
|
|
# target addr).
|
|
self.addr_verification_thread.stop()
|
|
while self.addr_verification_thread.is_alive():
|
|
time.sleep(0.01)
|
|
|
|
return Destination(MainMenuView)
|
|
|
|
|
|
|
|
class BruteForceAddressVerificationThread(BaseThread):
|
|
def __init__(self, address: str, seed: Seed, descriptor: Descriptor, script_type: str, embit_network: str, derivation_path: str, threadsafe_counter: ThreadsafeCounter, verified_index: ThreadsafeCounter, verified_index_is_change: ThreadsafeCounter):
|
|
"""
|
|
Either seed or descriptor will be None
|
|
"""
|
|
super().__init__()
|
|
self.address = address
|
|
self.seed = seed
|
|
self.descriptor = descriptor
|
|
self.script_type = script_type
|
|
self.embit_network = embit_network
|
|
self.derivation_path = derivation_path
|
|
self.threadsafe_counter = threadsafe_counter
|
|
self.verified_index = verified_index
|
|
self.verified_index_is_change = verified_index_is_change
|
|
|
|
if self.seed:
|
|
self.xpub = self.seed.get_xpub(wallet_path=self.derivation_path, network=Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK))
|
|
|
|
|
|
def run(self):
|
|
while self.keep_running:
|
|
if self.threadsafe_counter.cur_count % 10 == 0:
|
|
print(f"Incremented to {self.threadsafe_counter.cur_count}")
|
|
|
|
i = self.threadsafe_counter.cur_count
|
|
|
|
if self.descriptor:
|
|
receive_address = embit_utils.get_multisig_address(descriptor=self.descriptor, index=i, is_change=False, embit_network=self.embit_network)
|
|
change_address = embit_utils.get_multisig_address(descriptor=self.descriptor, index=i, is_change=True, embit_network=self.embit_network)
|
|
|
|
else:
|
|
receive_address = embit_utils.get_single_sig_address(xpub=self.xpub, script_type=self.script_type, index=i, is_change=False, embit_network=self.embit_network)
|
|
change_address = embit_utils.get_single_sig_address(xpub=self.xpub, script_type=self.script_type, index=i, is_change=True, embit_network=self.embit_network)
|
|
|
|
if self.address == receive_address:
|
|
self.verified_index.set_value(i)
|
|
self.verified_index_is_change.set_value(0)
|
|
self.keep_running = False
|
|
break
|
|
|
|
elif self.address == change_address:
|
|
self.verified_index.set_value(i)
|
|
self.verified_index_is_change.set_value(1)
|
|
self.keep_running = False
|
|
break
|
|
|
|
# Increment our index counter
|
|
self.threadsafe_counter.increment()
|
|
|
|
|
|
|
|
class AddressVerificationSuccessView(View):
|
|
def __init__(self, seed_num: int):
|
|
super().__init__()
|
|
self.seed_num = seed_num
|
|
if self.seed_num is not None:
|
|
self.seed = self.controller.get_seed(seed_num)
|
|
|
|
|
|
def run(self):
|
|
address = self.controller.unverified_address["address"]
|
|
sig_type = self.controller.unverified_address["sig_type"]
|
|
verified_index = self.controller.unverified_address["verified_index"]
|
|
verified_index_is_change = self.controller.unverified_address["verified_index_is_change"]
|
|
|
|
if sig_type == SettingsConstants.MULTISIG:
|
|
source = "multisig"
|
|
else:
|
|
source = f"seed {self.seed.get_fingerprint()}"
|
|
|
|
LargeIconStatusScreen(
|
|
status_headline="Address Verified",
|
|
text=f"""{address[:7]} = {source}'s {"change" if verified_index_is_change else "receive"} address #{verified_index}."""
|
|
).display()
|
|
|
|
return Destination(MainMenuView)
|
|
|
|
|
|
|
|
class LoadMultisigWalletDescriptorView(View):
|
|
def run(self):
|
|
SCAN = ("Scan Descriptor", FontAwesomeIconConstants.QRCODE)
|
|
CANCEL = "Cancel"
|
|
button_data = [SCAN, CANCEL]
|
|
selected_menu_num = seed_screens.LoadMultisigWalletDescriptorScreen(
|
|
button_data=button_data,
|
|
show_back_button=False,
|
|
).display()
|
|
|
|
if button_data[selected_menu_num] == SCAN:
|
|
return Destination(ScanView)
|
|
|
|
elif button_data[selected_menu_num] == CANCEL:
|
|
if self.controller.resume_main_flow == Controller.FLOW__PSBT:
|
|
return Destination(BackStackView)
|
|
else:
|
|
return Destination(MainMenuView)
|
|
|
|
|
|
|
|
class MultisigWalletDescriptorView(View):
|
|
def run(self):
|
|
descriptor = self.controller.multisig_wallet_descriptor
|
|
|
|
fingerprints = []
|
|
for key in descriptor.keys:
|
|
fingerprint = hexlify(key.fingerprint).decode()
|
|
fingerprints.append(fingerprint)
|
|
|
|
policy = descriptor.brief_policy.split("multisig")[0].strip()
|
|
|
|
RETURN = "Return to PSBT"
|
|
VERIFY = "Verify Addr"
|
|
EXPLORER = "Address Explorer"
|
|
OK = "OK"
|
|
|
|
button_data = [OK]
|
|
if self.controller.resume_main_flow:
|
|
if self.controller.resume_main_flow == Controller.FLOW__PSBT:
|
|
button_data = [RETURN]
|
|
elif self.controller.resume_main_flow == Controller.FLOW__VERIFY_MULTISIG_ADDR and self.controller.unverified_address:
|
|
VERIFY += f""" {self.controller.unverified_address["address"][:7]}"""
|
|
button_data = [VERIFY]
|
|
elif self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
|
button_data = [EXPLORER]
|
|
|
|
selected_menu_num = seed_screens.MultisigWalletDescriptorScreen(
|
|
policy=policy,
|
|
fingerprints=fingerprints,
|
|
button_data=button_data,
|
|
).display()
|
|
|
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
self.controller.multisig_wallet_descriptor = None
|
|
return Destination(BackStackView)
|
|
|
|
elif button_data[selected_menu_num] == RETURN:
|
|
# Jump straight back to PSBT change verification
|
|
self.controller.resume_main_flow = None
|
|
return Destination(PSBTChangeDetailsView, view_args=dict(change_address_num=0))
|
|
|
|
elif button_data[selected_menu_num] == VERIFY:
|
|
self.controller.resume_main_flow = None
|
|
return Destination(SeedAddressVerificationView)
|
|
|
|
elif button_data[selected_menu_num] == EXPLORER:
|
|
from seedsigner.views.tools_views import ToolsAddressExplorerAddressTypeView
|
|
self.controller.resume_main_flow = None
|
|
return Destination(ToolsAddressExplorerAddressTypeView)
|
|
|
|
return Destination(MainMenuView)
|