Files
seedsigner/src/seedsigner/controller.py
T
kdmukai b8d33dd4ec Interim commit
Initial implementation of new `view_function` Controller loop and `back_stack`.
2021-09-27 22:43:43 -05:00

1095 lines
45 KiB
Python

# External Dependencies
import time
from multiprocessing import Process, Queue
from subprocess import call
import os, sys
from embit import bip32
from embit.networks import NETWORKS
from binascii import hexlify
from threading import Thread
# Internal file class dependencies
from .views import (View, MenuView, SeedToolsView, SigningToolsView,
SettingsToolsView, IOTestView, OpeningSplashView, ScreensaverView)
from .gui import Renderer
from .helpers import Buttons, B, Path
from .models import (EncodeQRDensity, QRType, Seed, SeedStorage, Settings, ConfigurableSingleton, DecodeQR, DecodeQRStatus, EncodeQR, PSBTParser)
class Controller(ConfigurableSingleton):
"""
The Controller is a globally available singleton that maintains SeedSigner state.
It only makes sense to ever have a single Controller instance so it is
implemented here as a singleton. One departure from the typical singleton pattern
is the addition of a `configure_instance()` call to pass run-time settings into
the Controller.
Any code that needs to interact with the one and only Controller can just run:
```
from seedsigner.controller import Controller
controller = Controller.get_instance()
```
Note: In many/most cases you'll need to do the Controller import within a method
rather than at the top in order avoid circular imports.
"""
VERSION = "0.5.0"
@classmethod
def configure_instance(cls, config=None):
super().configure_instance(config)
# Instantiate the one and only Controller instance
controller = cls.__new__(cls)
cls._instance = controller
# Input Buttons
controller.buttons = Buttons.get_instance()
# models
controller.storage = SeedStorage()
Settings.configure_instance(config)
controller.settings = Settings.get_instance()
# settings
controller.DEBUG = controller.settings.debug
controller.color = controller.settings.text_color
# Configure the Renderer
Renderer.configure_instance({"text_color": controller.color})
# TODO: Refactor so that we don't need the Renderer here
controller.renderer = Renderer.get_instance()
# Views
controller.menu_view = MenuView()
controller.seed_tools_view = SeedToolsView()
controller.io_test_view = IOTestView()
controller.signing_tools_view = SigningToolsView(controller.storage)
controller.settings_tools_view = SettingsToolsView()
controller.screensaver = ScreensaverView(controller.buttons)
controller.back_stack = []
# Other behavior constants
controller.screensaver_activation_ms = 120 * 1000
@property
def camera(self):
from .camera import Camera
return Camera.get_instance()
def pop_back_stack(self):
if len(self.back_stack) > 0:
return self.back_stack.pop()
return (None, {})
def start(self) -> None:
opening_splash = OpeningSplashView()
# opening_splash.start()
""" Functions can be stored as variables in python!
This loop receives a view function to execute and stores it in the
`view_function` var along with any input arguments in the `function_args`
dict.
The `view_function` is executed w/its `function_args`. Each view returns either
a new view function to execute next or None.
Example:
def my_view(some_arg, other_arg):
print(other_arg)
def other_view():
return (my_view, {"some_arg": 1, "other_arg": "hello"})
When `other_view` is run, we capture its return values:
(view_function, function_args) = other_view()
And then we can execute that view function:
view_function(**function_args)
"""
try:
view_function = None
function_args = {}
while True:
if not view_function:
# None is a special case; render the Home screen
view_function = self.menu_view.display_main_menu
function_args = {}
# Home always wipes the back_stack
self.back_stack = []
print(f"Executing {view_function}")
# Each view returns the next view function to display plus any
# function_args in an argument dict.
(view_function, function_args) = view_function(**function_args)
print(f"view_function: {view_function}")
if view_function == self.pop_back_stack:
# "Back" arrow was clicked; load the previous view
(view_function, function_args) = self.pop_back_stack()
else:
self.back_stack.append((view_function, function_args))
finally:
# Clear the screen when exiting
self.renderer.display_blank_screen()
from seedsigner.gui.templates import BaseScreen, ButtonListScreen, FontTesterScreen, BottomButtonScreen
from seedsigner.gui.components import Fonts
# tests = [
# ("OpenSans-SemiBold", 16),
# ("OpenSans-SemiBold", 18),
# ("OpenSans-SemiBold", 20),
# ]
# screens = []
# screens.append(
# BottomButtonScreen(
# title="Supersampling: off",
# button_data=[{"text":"Proceed"}],
# is_button_text_centered=False,
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
# is_body_text_centered=True,
# body_font_name="OpenSans-Regular",
# body_font_size=17,
# body_font_color="white",
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
# supersampling_factor=1
# )
# )
# screens.append(
# BottomButtonScreen(
# title="Supersampling: 2",
# button_data=[{"text":"Proceed"}],
# is_button_text_centered=False,
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
# is_body_text_centered=True,
# body_font_name="OpenSans-Regular",
# body_font_size=17,
# body_font_color="white",
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
# supersampling_factor=2
# )
# )
# screens.append(
# BottomButtonScreen(
# title="Regular 16px w/SS",
# button_data=[{"text":"Proceed"}],
# is_button_text_centered=False,
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
# is_body_text_centered=True,
# body_font_name="OpenSans-Regular",
# body_font_size=16,
# body_font_color="white",
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
# supersampling_factor=2
# )
# )
# screens.append(
# BottomButtonScreen(
# title="SemiBold 16px, no SS",
# button_data=[{"text":"Proceed"}],
# is_button_text_centered=False,
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
# is_body_text_centered=True,
# body_font_name="OpenSans-SemiBold",
# body_font_size=16,
# body_font_color="white",
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
# supersampling_factor=1
# )
# )
# screens.append(
# BottomButtonScreen(
# title="SemiBold 17px, no SS",
# button_data=[{"text":"Proceed"}],
# is_button_text_centered=False,
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
# is_body_text_centered=True,
# body_font_name="OpenSans-SemiBold",
# body_font_size=17,
# body_font_color="white",
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
# supersampling_factor=1
# )
# )
# screens.append(
# ButtonListScreen(
# title="Color: Orange",
# button_data=[
# {"text": "cfd0883d"},
# {"text": "72f9a6bf"},
# ],
# is_button_text_centered=False,
# is_bottom_list=False,
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
# button_selected_color="orange"
# )
# )
# screens.append(
# ButtonListScreen(
# title="Color: #f7941d",
# button_data=[
# {"text": "cfd0883d"},
# {"text": "72f9a6bf"},
# ],
# is_button_text_centered=False,
# is_bottom_list=False,
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
# button_selected_color="#f7941d"
# )
# )
# index = 0
# while True:
# # test = tests[index]
# # screen = FontTesterScreen(
# # title="In-Memory Seeds",
# # button_data=[
# # {"text": "cfd0883d"},
# # {"text": "72f9a6bf"},
# # {"text": f"{test[0].split('.')[0]} {test[1]}"},
# # ],
# # is_button_text_centered=False,
# # is_bottom_list=False,
# # )
# screens[index].render()
# while True:
# if index == 0:
# input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_UP, B.KEY_DOWN])
# elif index < len(screens) - 1:
# input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN])
# else:
# input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN])
# if input == B.KEY_RIGHT:
# index += 1
# break
# elif input == B.KEY_LEFT:
# index -= 1
# break
# else:
# screens[index].update_from_input(input)
# time.sleep(180)
if self.DEBUG:
# Let Exceptions halt execution
try:
self.show_main_menu()
finally:
# Clear the screen when exiting
self.renderer.display_blank_screen()
else:
# Handle Unexpected crashes by restarting up to 3 times
crash_cnt = 0
while True:
try:
self.show_main_menu()
except Exception as error:
if crash_cnt >= 3:
break
else:
print('Caught this error: ' + repr(error)) # debug
self.renderer.draw_modal(["Crashed ..."], "", "restarting")
time.sleep(5)
crash_cnt += 1
self.renderer.draw_modal(["Crashed ..."], "", "requires hard restart")
def start_screensaver(self):
self.screensaver.start()
### Menu
### Menu View handles navigation within the menu
### Sub Menu's like Seed Tools, Signing Tools, Settings are all in the Menu View
def show_main_menu(self, sub_menu = 0):
ret_val = sub_menu
while True:
ret_val = self.menu_view.display_main_menu(ret_val)
if ret_val == Path.MAIN_MENU:
ret_val = Path.MAIN_MENU
elif ret_val == Path.GEN_LAST_WORD:
ret_val = self.show_generate_last_word_tool()
elif ret_val == Path.DICE_GEN_SEED:
ret_val = self.show_create_seed_with_dice_tool()
elif ret_val == Path.IMAGE_GEN_SEED:
ret_val = self.show_create_seed_with_image_tool()
elif ret_val == Path.SAVE_SEED:
ret_val = self.show_store_a_seed_tool()
elif ret_val == Path.PASSPHRASE_SEED:
ret_val = self.show_add_remove_passphrase_tool()
elif ret_val == Path.GEN_XPUB:
ret_val = self.show_generate_xpub()
elif ret_val == Path.SIGN_TRANSACTION:
ret_val = self.show_sign_transaction()
elif ret_val == Path.IO_TEST_TOOL:
ret_val = self.show_io_test_tool()
elif ret_val == Path.VERSION_INFO:
ret_val = self.show_version_info()
elif ret_val == Path.CURRENT_NETWORK:
ret_val = self.show_current_network_tool()
elif ret_val == Path.WALLET:
ret_val = self.show_wallet_tool()
elif ret_val == Path.QR_DENSITY_SETTING:
ret_val = self.show_qr_density_tool()
elif ret_val == Path.PERSISTENT_SETTINGS:
ret_val = self.show_persistent_settings_tool()
elif ret_val == Path.CAMERA_ROTATION:
ret_val = self.show_camera_rotation_tool()
elif ret_val == Path.DONATE:
ret_val = self.show_donate_tool()
elif ret_val == Path.RESET:
ret_val = self.show_reset_tool()
elif ret_val == Path.POWER_OFF:
ret_val = self.show_power_off()
raise Exception("Unhandled case")
### Power Off
def show_power_off(self):
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Power Off?")
if r == 1: #Yes
self.menu_view.display_power_off_screen()
call("sudo shutdown --poweroff now", shell=True)
time.sleep(10)
else: # No
return Path.MAIN_MENU
###
### Seed Tools Controller Naviation/Launcher
###
### Generate Last Word 12 / 24 Menu
def show_generate_last_word_tool(self) -> int:
seed = Seed(wordlist=self.settings.wordlist)
ret_val = 0
while True:
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_12_24_word_menu("... [ Return to Seed Tools ]")
if ret_val == Path.SEED_WORD_12:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(11)
elif ret_val == Path.SEED_WORD_24:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(23)
else:
return Path.SEED_TOOLS_SUB_MENU
if len(seed.mnemonic_list) > 0:
seed.mnemonic = self.seed_tools_view.display_last_word(seed.mnemonic_display_list)
break
# display seed phrase
while True:
ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic, show_qr_option=True)
if ret_val == True:
break
else:
# no-op; can't back out of the seed phrase view
pass
# Ask to save seed
if self.storage.slot_avaliable():
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
self.storage.add_seed(seed, slot_num)
self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
### Create a Seed w/ Dice Screen
def show_create_seed_with_dice_tool(self) -> int:
seed = Seed(wordlist=self.settings.wordlist)
ret_val = True
while True:
seed.mnemonic = self.seed_tools_view.display_generate_seed_from_dice()
if seed:
break
else:
return Path.SEED_TOOLS_SUB_MENU
# display seed phrase (24 words)
while True:
ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=True)
if ret_val == True:
break
else:
# no-op; can't back out of the seed phrase view
pass
# Ask to save seed
if self.storage.slot_avaliable():
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
self.storage.add_seed(seed, slot_num)
self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
### Create a Seed w/ Image
def show_create_seed_with_image_tool(self) -> int:
seed = Seed(wordlist=self.settings.wordlist)
ret_val = True
while True:
(reshoot, seed.mnemonic) = self.seed_tools_view.seed_phrase_from_camera_image()
if reshoot:
# Relaunch into another image capture cycle
continue
if seed:
break
else:
return Path.SEED_TOOLS_SUB_MENU
# display seed phrase (24 words)
while True:
ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=True)
if ret_val == True:
break
else:
# Start over
return self.show_create_seed_with_image_tool()
# Ask to save seed
if self.storage.slot_avaliable():
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
self.storage.add_seed(seed, slot_num)
self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
### Store a seed (temp) Menu
def show_store_a_seed_tool(self):
seed = Seed(wordlist=self.settings.wordlist)
ret_val = 0
display_saved_seed = False
ret_val = self.menu_view.display_saved_seed_menu(self.storage, 1, "... [ Return to Seed Tools ]")
if ret_val == 0:
return Path.SEED_TOOLS_SUB_MENU
slot_num = ret_val
if self.storage.check_slot(slot_num) == True:
display_saved_seed = True
# show seed phrase
# display seed phrase (24 words)
while True:
r = self.seed_tools_view.display_seed_phrase(self.storage.get_seed_phrase(abs(slot_num)), self.storage.get_passphrase(abs(slot_num)), show_qr_option=True)
if r == True:
break
else:
# no-op; can't back out of the seed phrase view
pass
return Path.SEED_TOOLS_SUB_MENU
else:
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Seed Tools ]")
if ret_val == Path.SEED_WORD_12:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
elif ret_val == Path.SEED_WORD_24:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
elif ret_val == Path.SEED_WORD_QR:
seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
else:
return Path.SEED_TOOLS_SUB_MENU
if not seed:
# seed is not valid
return Path.SEED_TOOLS_SUB_MENU
if ret_val == Path.SEED_WORD_QR and seed:
show_qr_option = False
else:
show_qr_option = True
if not seed:
# Seed is not valid, Exit if not valid with message
self.renderer.draw_modal(["Seed Invalid", "check seed phrase", "and try again", ""], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.SEED_TOOLS_SUB_MENU
else:
self.renderer.draw_modal(["Valid Seed!"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
while display_saved_seed == False:
r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=show_qr_option )
if r == True:
break
else:
# no-op; can't back out of the seed phrase view
pass
if seed:
self.storage.add_seed(seed, slot_num)
self.renderer.draw_modal(["", "Saved to Slot #" + str(slot_num)], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.SEED_TOOLS_SUB_MENU
### Add a PassPhrase Menu
def show_add_remove_passphrase_tool(self):
ret_val = 0
r = 0
if self.storage.num_of_saved_seeds() == 0:
self.renderer.draw_modal(["Store a seed phrase", "prior to adding", "a passphrase"], "Error", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
return Path.SEED_TOOLS_SUB_MENU
if self.storage.num_of_saved_seeds() > 0:
if self.storage.num_of_saved_seeds() == 1:
ret_val = self.storage.get_first_seed_slot()
else:
ret_val = self.menu_view.display_saved_seed_menu(self.storage, 3, None)
if ret_val == 0:
return Path.SEED_TOOLS_SUB_MENU
# continue after top level if to capture and store passphrase
slot_num = ret_val
if self.storage.check_slot_passphrase(slot_num) == True:
# only display menu to remove/update if there is a passphrase to remove
r = self.renderer.display_generic_selection_menu(["... [ Return to Seed Tools ]", "Change Passphrase", "Remove Passphrase"], "Passphrase Action")
if r == 3:
# Remove Passphrase Workflow
self.storage.delete_passphrase(slot_num)
self.renderer.draw_modal(["Passphrase Deleted", "from Slot #" + str(slot_num)], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
return Path.SEED_TOOLS_SUB_MENU
elif r == 1:
return Path.SEED_TOOLS_SUB_MENU
# continue if updating passphrase
# display a tool to pick letters/numbers to make a passphrase
passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry(existing_passphrase=self.storage.get_passphrase(slot_num))
if len(passphrase) == 0:
return Path.SEED_TOOLS_SUB_MENU
self.storage.save_passphrase(passphrase, slot_num)
if r in (0,1):
self.renderer.draw_modal(["Passphrase Added", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue")
elif r == 2:
self.renderer.draw_modal(["Passphrase Updated", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
return Path.SEED_TOOLS_SUB_MENU
###
### Signing Tools Navigation/Launcher
###
### Generate XPUB
def show_generate_xpub(self):
seed = Seed(wordlist=self.settings.wordlist)
# If there is a saved seed, ask to use saved seed
if self.storage.num_of_saved_seeds() > 0:
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Use Saved Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
if slot_num not in (1,2,3):
return Path.SEED_TOOLS_SUB_MENU
seed = self.storage.get_seed(slot_num)
if not seed:
# no valid seed, gather seed phrase
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Sign Tools ]")
if ret_val == Path.SEED_WORD_12:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
elif ret_val == Path.SEED_WORD_24:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
elif ret_val == Path.SEED_WORD_QR:
seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
else:
return Path.SEED_TOOLS_SUB_MENU
if not seed:
return Path.SEED_TOOLS_SUB_MENU
# check if seed phrase is valid
if not seed:
self.renderer.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
if r == 1:
# display a tool to pick letters/numbers to make a passphrase
seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
if len(seed.passphrase) == 0:
self.renderer.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
if input == B.KEY_LEFT:
return Path.MAIN_MENU
else:
self.renderer.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
# display seed phrase
while True:
r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
if r == True:
break
else:
# Cancel
return Path.SEED_TOOLS_SUB_MENU
# choose single sig or multisig wallet type
wallet_type = "multisig"
script_type = "native segwit"
derivation = self.settings.custom_derivation
r = self.renderer.display_generic_selection_menu(["Single Sig", "Multisig"], "Wallet Type?")
if r == 1:
wallet_type = "single sig"
elif r == 2:
wallet_type = "multisig"
# choose derivation standard
r = self.renderer.display_generic_selection_menu(["Native Segwit", "Nested Segwit", "Custom"], "Derivation Path?")
if r == 1:
script_type = "native segwit"
elif r == 2:
script_type = "nested segwit"
elif r == 3:
script_type = "custom"
# calculated derivation or get custom from keyboard entry
if script_type == "custom":
derivation = self.settings_tools_view.draw_derivation_keyboard_entry(existing_derivation=self.settings.custom_derivation)
self.settings.custom_derivation = derivation # save for next time
else:
derivation = Settings.calc_derivation(self.settings.network, wallet_type, script_type)
if derivation == "" or derivation == None:
self.renderer.draw_modal(["Invalid Derivation", "try again"], "", "Right to Continue")
return Path.SEED_TOOLS_SUB_MENU
if self.settings.software == "Prompt":
lines = ["Specter Desktop", "Blue Wallet", "Sparrow"]
r = self.renderer.display_generic_selection_menu(lines, "Which Wallet?")
qr_xpub_type = Settings.getXPubType(lines[r-1])
else:
qr_xpub_type = self.settings.qr_xpub_type
self.signing_tools_view.draw_modal(["Loading xPub Info ..."])
version = bip32.detect_version(derivation, default="xpub", network=NETWORKS[self.settings.network])
root = bip32.HDKey.from_seed(seed.seed, version=NETWORKS[self.settings.network]["xprv"])
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
xprv = root.derive(derivation)
xpub = xprv.to_public()
print(derivation)
xpub_base58 = xpub.to_string(version=version)
self.signing_tools_view.display_xpub_info(fingerprint, derivation, xpub_base58)
self.buttons.wait_for([B.KEY_RIGHT])
self.signing_tools_view.draw_modal(["Generating xPub QR ..."])
e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=derivation, network=self.settings.network, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist)
while e.totalParts() > 1:
image = e.nextPartImage(240,240,2)
self.renderer.show_image(image)
time.sleep(0.1)
if self.buttons.check_for_low(B.KEY_RIGHT):
break
if e.totalParts() == 1:
image = e.nextPartImage(240,240,1)
self.renderer.show_image(image)
self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
### Sign Transactions
def show_sign_transaction(self):
seed = Seed(wordlist=self.settings.wordlist)
used_saved_seed = False
# reusable qr scan function
def scan_qr(scan_text="Scan QR"):
# Scan QR using Camera
self.renderer.draw_modal(["Initializing Camera"])
self.camera.start_video_stream_mode(resolution=(480, 480), framerate=12, format="rgb")
decoder = DecodeQR(wordlist=self.settings.wordlist)
def live_preview(camera, decoder, scan_text):
while True:
frame = self.camera.read_video_stream(as_image=True)
if frame is not None:
if decoder.getPercentComplete() > 0 and decoder.isPSBT():
scan_text = str(decoder.getPercentComplete()) + "% Complete"
self.renderer.show_image_with_text(frame.resize((240,240)), scan_text, font=Fonts.get_font("Assistant-Medium", 22), text_color=View.color, text_background=(0,0,0,225))
time.sleep(0.1) # turn this up or down to tune performance while decoding psbt
if camera._video_stream is None:
break
# putting live preview in it's own thread to improve psbt decoding performance
t = Thread(target=live_preview, args=(self.camera, decoder, scan_text,))
t.start()
while True:
frame = self.camera.read_video_stream()
if frame is not None:
status = decoder.addImage(frame)
if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID):
self.camera.stop_video_stream_mode()
break
if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
self.camera.stop_video_stream_mode()
break
time.sleep(0.2) # time to let live preview thread complete to avoid race condition on display
return decoder
# first QR scan
decoder = scan_qr()
if decoder.isComplete() and decoder.isPSBT():
# first QR is PSBT
self.renderer.draw_modal(["Validating PSBT"])
psbt = decoder.getPSBT()
self.renderer.draw_modal(["PSBT Valid!", "Enter", "seed phrase", "to sign this tx"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
elif decoder.isComplete() and decoder.isSeed():
# first QR is Seed
self.renderer.draw_modal(["Validating Seed"])
seed.mnemonic = decoder.getSeedPhrase()
if not seed:
# seed is not valid, Exit if not valid with message
self.renderer.draw_modal(["Seed Invalid", "check seed phrase", "and try again", ""], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
else:
self.renderer.draw_modal(["Valid Seed!"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
if r == 1:
# display a tool to pick letters/numbers to make a passphrase
seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
if len(seed.passphrase) == 0:
self.renderer.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
if input == B.KEY_LEFT:
return Path.MAIN_MENU
else:
self.renderer.draw_modal(["Optional passphrase", "added to seed words"], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
# Ask to save seed
if self.storage.slot_avaliable():
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
self.storage.add_seed(seed, slot_num)
self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
# display seed phrase
while True:
r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
if r == True:
break
else:
# Cancel
return Path.MAIN_MENU
# second QR scan need PSBT now
decoder = scan_qr("Scan PSBT QR")
if decoder.isComplete() and decoder.isPSBT():
# second QR must be a PSBT
self.renderer.draw_modal(["Validating PSBT"])
psbt = decoder.getPSBT()
elif ( decoder.isComplete() and not decoder.isPSBT() ) or decoder.isInvalid():
self.renderer.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
else:
return Path.MAIN_MENU
elif ( decoder.isComplete() and not decoder.isPSBT() ) or decoder.isInvalid():
self.renderer.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
else:
return Path.MAIN_MENU
if not seed:
# No valid seed yet, If there is a saved seed, ask to use saved seed
if self.storage.num_of_saved_seeds() > 0:
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Use Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
if slot_num == 0:
return Path.MAIN_MENU
seed = self.storage.get_seed(slot_num)
used_saved_seed = True
if not seed:
# no valid seed yet, gather seed phrase
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Main Menu ]")
if ret_val == Path.SEED_WORD_12:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
elif ret_val == Path.SEED_WORD_24:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
elif ret_val == Path.SEED_WORD_QR:
seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
else:
return Path.MAIN_MENU
if not seed:
return Path.MAIN_MENU
# check if seed phrase is valid
self.renderer.draw_modal(["Validating Seed ..."])
if not seed:
self.renderer.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
if len(seed.passphrase) == 0:
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
if r == 1:
# display a tool to pick letters/numbers to make a passphrase
seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
if len(seed.passphrase) == 0:
self.renderer.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
if input == B.KEY_LEFT:
return Path.MAIN_MENU
else:
self.renderer.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
# display seed phrase
while True:
r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
if r == True:
break
else:
# Cancel
return Path.MAIN_MENU
# Ask to save seed
if self.storage.slot_avaliable() and used_saved_seed == False:
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
self.storage.add_seed(seed, slot_num)
self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
# show transaction information before sign
self.renderer.draw_modal(["Parsing PSBT"])
p = PSBTParser(psbt,seed,self.settings.network)
self.signing_tools_view.display_transaction_information(p)
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False)
if input == B.KEY_LEFT:
return Path.MAIN_MENU
# Sign PSBT
self.renderer.draw_modal(["PSBT Signing ..."])
sig_cnt = PSBTParser.sigCount(psbt)
psbt.sign_with(p.root)
trimmed_psbt = PSBTParser.trim(psbt)
if sig_cnt == PSBTParser.sigCount(trimmed_psbt):
self.renderer.draw_modal(["Signing failed", "left to exit", "or right to continue", "to display PSBT QR"], "", "")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False)
if input == B.KEY_LEFT:
return Path.MAIN_MENU
# Display Animated QR Code
self.renderer.draw_modal(["Generating PSBT QR ..."])
e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist)
while True:
image = e.nextPartImage(240,240,1)
self.renderer.show_image(image)
time.sleep(0.05)
if self.buttons.check_for_low(B.KEY_RIGHT):
break
# Return to Main Menu
return Path.MAIN_MENU
###
### Settings Tools Navigation/Launcher
###
#### Show IO Test
def show_io_test_tool(self):
ret_val = True
ret_val = self.io_test_view.display_io_test_screen()
if ret_val == True:
return Path.SETTINGS_SUB_MENU
### Show Current Network
def show_current_network_tool(self):
r = self.settings_tools_view.display_current_network()
if r is not None:
self.settings.network = r
return Path.SETTINGS_SUB_MENU
### Show Wallet Selection Tool
def show_wallet_tool(self):
r = self.settings_tools_view.display_wallet_selection()
if r is not None:
self.settings.software = r
return Path.SETTINGS_SUB_MENU
### Show QR Density Tool
def show_qr_density_tool(self):
r = self.settings_tools_view.display_qr_density_selection()
if r in (EncodeQRDensity.LOW, EncodeQRDensity.MEDIUM, EncodeQRDensity.HIGH):
self.settings.qr_density = r
return Path.SETTINGS_SUB_MENU
### Show Version Info
def show_version_info(self):
self.settings_tools_view.display_version_info()
input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT])
if input == B.KEY_LEFT:
return Path.SETTINGS_SUB_MENU
elif input == B.KEY_RIGHT:
return Path.SETTINGS_SUB_MENU
### Show Persistent Settings Screen
def show_persistent_settings_tool(self):
r = self.settings_tools_view.display_persistent_settings()
if r is not None:
if r == True:
self.renderer.draw_modal(["Persistent settings", "keeps settings saved", "across reboot.", "Seeds are never saved"], "Warning", "Right to Continue")
input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT])
if input == B.KEY_RIGHT:
self.settings.persistent = r
else:
self.renderer.draw_modal(["This will restore", "the default", "settings.", ""], "Warning", "Right to Continue")
input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT])
if input == B.KEY_RIGHT:
self.settings.persistent = r
return Path.SETTINGS_SUB_MENU
def show_camera_rotation_tool(self):
r = self.settings_tools_view.display_camera_rotation()
if r is not None:
self.settings.camera_rotation = r
return Path.SETTINGS_SUB_MENU ### Show Donate Screen and QR
def show_donate_tool(self):
self.settings_tools_view.display_donate_info_screen()
input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT])
if input == B.KEY_LEFT:
return Path.SETTINGS_SUB_MENU
elif input == B.KEY_RIGHT:
self.settings_tools_view.display_donate_qr()
time.sleep(1)
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
def show_reset_tool(self):
self.renderer.draw_modal(["This will restore", "default settings and", "restart the app", ""], "Warning", "Right to Continue")
input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT])
if input == B.KEY_RIGHT:
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Reset SeedSigner?")
if r == 1: #Yes
self.renderer.display_blank_screen()
self.settings.restoreDefault()
time.sleep(0.1) # give time to write to disk
return_code = os.system("sudo systemctl is-active --quiet seedsigner.service")
if return_code == 0:
# systemd service is running
call("sudo systemctl restart seedsigner.service", shell=True)
time.sleep(2)
else:
# systemd service is not running, restart script internally
os.execv(sys.executable, ['python3'] + sys.argv)
else: # No
return Path.MAIN_MENU
return Path.MAIN_MENU