mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
refactoring of seedsigner to a move MVC application model with a few additional minor enhancements
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
# External Dependencies
|
||||
import time
|
||||
from multiprocessing import Process, Queue
|
||||
from subprocess import call
|
||||
|
||||
# Internal file class dependencies
|
||||
from view import View
|
||||
from menu_view import MenuView
|
||||
from seed_tools_view import SeedToolsView
|
||||
from signing_tools_view import SigningToolsView
|
||||
from settings_tools_view import SettingsToolsView
|
||||
from io_test_view import IOTestView
|
||||
from buttons import Buttons, B
|
||||
from camera_process import CameraProcess
|
||||
from path import Path
|
||||
from seed_storage import SeedStorage
|
||||
from psbt_signer import PSBTSigner
|
||||
|
||||
class Controller:
|
||||
|
||||
VERSION = "0.4.0b3"
|
||||
|
||||
def __init__(self) -> None:
|
||||
controller = self
|
||||
|
||||
# Input Buttons
|
||||
self.buttons = Buttons()
|
||||
|
||||
# models
|
||||
self.storage = SeedStorage("main", "m/48h/0h/0h/2h")
|
||||
|
||||
# Views
|
||||
self.menu_view = MenuView(controller)
|
||||
self.seed_tools_view = SeedToolsView(controller)
|
||||
self.io_test_view = IOTestView(controller)
|
||||
self.signing_tools_view = SigningToolsView(controller, self.storage)
|
||||
self.settings_tools_view = SettingsToolsView(controller)
|
||||
|
||||
# Then start seperate background camera process with two queues for communication
|
||||
# CameraProcess handles connecting to camera hardware and passing back barcode data via from camera queue
|
||||
self.from_camera_queue = Queue()
|
||||
self.to_camera_queue = Queue()
|
||||
p = Process(target=CameraProcess.start, args=(self.from_camera_queue, self.to_camera_queue))
|
||||
p.start()
|
||||
|
||||
return
|
||||
|
||||
def start(self, debug = False) -> None:
|
||||
|
||||
if debug == False:
|
||||
crash_cnt = 0
|
||||
|
||||
# Handle Unexpected crashes by restarting up to 3 times
|
||||
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.menu_view.draw_modal(["Crashed ..."], "", "restarting")
|
||||
time.sleep(5)
|
||||
|
||||
crash_cnt += 1
|
||||
|
||||
self.menu_view.draw_modal(["Crashed ..."], "", "requires hard restart")
|
||||
else:
|
||||
self.show_main_menu()
|
||||
|
||||
### 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.SAVE_SEED:
|
||||
ret_val = self.show_store_a_seed_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.DONATE:
|
||||
ret_val = self.show_donate_tool()
|
||||
elif ret_val == Path.POWER_OFF:
|
||||
ret_val = self.show_power_off()
|
||||
|
||||
print("exit show_main_menu")
|
||||
return # should never return/exit
|
||||
|
||||
### Power Off
|
||||
|
||||
def show_power_off(self):
|
||||
|
||||
r = self.menu_view.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_phrase = []
|
||||
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_phrase = self.seed_tools_view.display_gather_words_screen(11)
|
||||
elif ret_val == Path.SEED_WORD_24:
|
||||
seed_phrase = self.seed_tools_view.display_gather_words_screen(23)
|
||||
else:
|
||||
return Path.SEED_TOOLS_SUB_MENU
|
||||
|
||||
if len(seed_phrase) > 0:
|
||||
completed_seed_phrase = self.seed_tools_view.display_last_word(seed_phrase)
|
||||
break
|
||||
|
||||
# Ask to save seed
|
||||
if self.storage.slot_avaliable():
|
||||
r = self.menu_view.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.save_seed_phrase(completed_seed_phrase, slot_num)
|
||||
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
|
||||
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_phrase = []
|
||||
ret_val = True
|
||||
|
||||
while True:
|
||||
seed_phrase = self.seed_tools_view.display_generate_seed_from_dice()
|
||||
if len(seed_phrase) > 0:
|
||||
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_phrase, "Right to Continue")
|
||||
if ret_val == True:
|
||||
break
|
||||
|
||||
# Ask to save seed
|
||||
if self.storage.slot_avaliable():
|
||||
r = self.menu_view.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.save_seed_phrase(seed_phrase, slot_num)
|
||||
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
|
||||
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_phrase = []
|
||||
ret_val = 0
|
||||
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:
|
||||
# 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)), "Right to Continue")
|
||||
if r == True:
|
||||
break
|
||||
return Path.MAIN_MENU
|
||||
else:
|
||||
# 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_phrase = self.seed_tools_view.display_gather_words_screen(12)
|
||||
elif ret_val == Path.SEED_WORD_24:
|
||||
seed_phrase = self.seed_tools_view.display_gather_words_screen(24)
|
||||
else:
|
||||
return Path.SEED_TOOLS_SUB_MENU
|
||||
|
||||
if len(seed_phrase) == 0:
|
||||
return Path.SEED_TOOLS_SUB_MENU
|
||||
|
||||
self.menu_view.draw_modal(["Validating ..."])
|
||||
is_valid = self.storage.check_if_seed_valid(seed_phrase)
|
||||
if is_valid:
|
||||
self.storage.save_seed_phrase(seed_phrase, slot_num)
|
||||
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
else:
|
||||
self.menu_view.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
|
||||
|
||||
|
||||
###
|
||||
### Signing Tools Navigation/Launcher
|
||||
###
|
||||
|
||||
### Generate XPUB
|
||||
|
||||
def show_generate_xpub(self):
|
||||
seed_phrase = []
|
||||
|
||||
# If there is a saved seed, ask to use saved seed
|
||||
if self.storage.num_of_saved_seeds() > 0:
|
||||
r = self.menu_view.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 not in (1,2,3):
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
seed_phrase = self.storage.get_seed_phrase(slot_num)
|
||||
|
||||
if len(seed_phrase) == 0:
|
||||
# gather seed phrase
|
||||
# display menu to select 12 or 24 word seed for last word
|
||||
ret_val = self.menu_view.display_12_24_word_menu("... [ Return to Sign Tools ]")
|
||||
if ret_val == Path.SEED_WORD_12:
|
||||
seed_phrase = self.seed_tools_view.display_gather_words_screen(12)
|
||||
elif ret_val == Path.SEED_WORD_24:
|
||||
seed_phrase = self.seed_tools_view.display_gather_words_screen(24)
|
||||
else:
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
|
||||
if len(seed_phrase) == 0:
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
|
||||
# check if seed phrase is valid
|
||||
self.menu_view.draw_modal(["Validating ..."])
|
||||
is_valid = self.storage.check_if_seed_valid(seed_phrase)
|
||||
if is_valid == False:
|
||||
self.menu_view.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
|
||||
|
||||
# display seed phrase
|
||||
while True:
|
||||
r = self.seed_tools_view.display_seed_phrase(seed_phrase, "Right to See QR")
|
||||
if r == True:
|
||||
break
|
||||
|
||||
self.signing_tools_view.display_xpub_qr(seed_phrase)
|
||||
time.sleep(1)
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
return Path.MAIN_MENU
|
||||
|
||||
### Sign Transactions
|
||||
|
||||
def show_sign_transaction(self):
|
||||
seed_phrase = []
|
||||
|
||||
# If there is a saved seed, ask to use saved seed
|
||||
if self.storage.num_of_saved_seeds() > 0:
|
||||
r = self.menu_view.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.SIGNING_TOOLS_SUB_MENU
|
||||
seed_phrase = self.storage.get_seed_phrase(slot_num)
|
||||
|
||||
if len(seed_phrase) == 0:
|
||||
# gather seed phrase
|
||||
# display menu to select 12 or 24 word seed for last word
|
||||
ret_val = self.menu_view.display_12_24_word_menu("... [ Return to Sign Tools ]")
|
||||
if ret_val == Path.SEED_WORD_12:
|
||||
seed_phrase = self.seed_tools_view.display_gather_words_screen(12)
|
||||
elif ret_val == Path.SEED_WORD_24:
|
||||
seed_phrase = self.seed_tools_view.display_gather_words_screen(24)
|
||||
else:
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
|
||||
if len(seed_phrase) == 0:
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
|
||||
# check if seed phrase is valid
|
||||
self.menu_view.draw_modal(["Validating ..."])
|
||||
is_valid = self.storage.check_if_seed_valid(seed_phrase)
|
||||
if is_valid == False:
|
||||
self.menu_view.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
|
||||
|
||||
# display seed phrase
|
||||
while True:
|
||||
r = self.seed_tools_view.display_seed_phrase(seed_phrase, "Right to Scan QR")
|
||||
if r == True:
|
||||
break
|
||||
else:
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
|
||||
# Scan PSBT Animated QR using Camera
|
||||
self.menu_view.draw_modal(["Loading..."])
|
||||
signer = PSBTSigner(seed_phrase, self)
|
||||
raw_pbst = signer.scan_animated_psbt_qr()
|
||||
print("raw_pbst: " + raw_pbst)
|
||||
if raw_pbst == "nodata":
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
if raw_pbst == "invalid":
|
||||
self.menu_view.draw_modal(["QR Format Unexpected"], "", "RIGHT to EXIT")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
self.menu_view.draw_modal(["Parsing PSBT ..."])
|
||||
signer.parse_psbt(raw_pbst)
|
||||
|
||||
# show transaction information before sign
|
||||
self.signing_tools_view.display_transaction_information(signer)
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False)
|
||||
if input == B.KEY_LEFT:
|
||||
return Path.SIGNING_TOOLS_SUB_MENU
|
||||
|
||||
# Sign PSBT
|
||||
self.menu_view.draw_modal(["PSBT Signing ..."])
|
||||
(status, signed_pbst) = signer.sign_transaction()
|
||||
|
||||
# Display Animated QR Code
|
||||
self.signing_tools_view.display_signed_psbt_animated_qr(signed_pbst)
|
||||
|
||||
# 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.MAIN_MENU
|
||||
|
||||
### Show Current Network
|
||||
|
||||
def show_current_network_tool(self):
|
||||
r = self.settings_tools_view.display_current_network()
|
||||
if r == "main":
|
||||
self.storage.set_network("main")
|
||||
self.storage.set_hardened_derivation("m/48h/0h/0h/2h")
|
||||
elif r == "test":
|
||||
self.storage.set_network("test")
|
||||
self.storage.set_hardened_derivation("m/48h/1h/0h/2h")
|
||||
|
||||
return Path.SETTINGS_SUB_MENU
|
||||
|
||||
### Show Wallet Selection Tool
|
||||
|
||||
def show_wallet_tool(self):
|
||||
r = self.settings_tools_view.display_wallet_selection()
|
||||
if r == "Specter Desktop":
|
||||
pass
|
||||
|
||||
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.MAIN_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
|
||||
|
||||
@@ -3,7 +3,6 @@ import RPi.GPIO as GPIO
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
|
||||
class ST7789(object):
|
||||
"""class for ST7789 240*240 1.3inch OLED displays."""
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
class Buttons:
|
||||
|
||||
KEY_UP_PIN = 6
|
||||
KEY_DOWN_PIN = 19
|
||||
KEY_LEFT_PIN = 5
|
||||
KEY_RIGHT_PIN = 26
|
||||
KEY_PRESS_PIN = 13
|
||||
|
||||
KEY1_PIN = 21
|
||||
KEY2_PIN = 20
|
||||
KEY3_PIN = 16
|
||||
|
||||
def __init__(self) -> None:
|
||||
#init GPIO
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(Buttons.KEY_UP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||
GPIO.setup(Buttons.KEY_DOWN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||
GPIO.setup(Buttons.KEY_LEFT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||
GPIO.setup(Buttons.KEY_RIGHT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||
GPIO.setup(Buttons.KEY_PRESS_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||
GPIO.setup(Buttons.KEY1_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||
GPIO.setup(Buttons.KEY2_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||
GPIO.setup(Buttons.KEY3_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||
|
||||
self.GPIO = GPIO
|
||||
self.override_ind = False
|
||||
|
||||
self.add_events([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS, B.KEY_LEFT, B.KEY_RIGHT, B.KEY1, B.KEY2, B.KEY3])
|
||||
|
||||
return
|
||||
|
||||
def wait_for(self, keys = [], check_release = True) -> int:
|
||||
while True:
|
||||
for i in keys:
|
||||
if check_release == False or (check_release == True and B.release_lock == True):
|
||||
# when check release is False or the release lock is released (True)
|
||||
if self.GPIO.input(i) == GPIO.LOW or self.override_ind == True:
|
||||
B.release_lock = False
|
||||
if self.override_ind == True:
|
||||
self.override_ind = False
|
||||
return B.OVERRIDE
|
||||
return i
|
||||
time.sleep(0.01) # wait 10 ms to give CPU chance to do other things
|
||||
|
||||
def add_events(self, keys = []):
|
||||
for i in keys:
|
||||
GPIO.add_event_detect(i, self.GPIO.RISING, callback=Buttons.rising_callback)
|
||||
|
||||
def rising_callback(channel):
|
||||
B.release_lock = True
|
||||
return
|
||||
|
||||
def trigger_override(self) -> bool:
|
||||
if self.override_ind == False:
|
||||
self.override_ind = True
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_for_low(self, key) -> bool:
|
||||
if self.GPIO.input(key) == self.GPIO.LOW:
|
||||
return True
|
||||
return False
|
||||
|
||||
# class used as short hand for static button/channel lookup values
|
||||
|
||||
class B:
|
||||
|
||||
KEY_UP = 6
|
||||
KEY_DOWN = 19
|
||||
KEY_LEFT = 5
|
||||
KEY_RIGHT = 26
|
||||
KEY_PRESS = 13
|
||||
KEY1 = 21
|
||||
KEY2 = 20
|
||||
KEY3 = 16
|
||||
OVERRIDE = 1000
|
||||
|
||||
release_lock = True # released when True, locked when False
|
||||
@@ -0,0 +1,80 @@
|
||||
# External Dependencies
|
||||
from multiprocessing import Queue
|
||||
from threading import Timer
|
||||
import time
|
||||
|
||||
class CameraProcess:
|
||||
|
||||
def start(out_queue, in_queue):
|
||||
|
||||
print("CameraProcess start")
|
||||
|
||||
from imutils.video import VideoStream
|
||||
from pyzbar import pyzbar
|
||||
|
||||
print("CameraProcess finish import")
|
||||
|
||||
running = True
|
||||
|
||||
while running == True:
|
||||
|
||||
camera_on = False
|
||||
|
||||
msg = []
|
||||
msg = in_queue.get()
|
||||
|
||||
if msg[0] == "start":
|
||||
print("start camera!!")
|
||||
vs = VideoStream(usePiCamera=True,resolution=(512, 384),framerate=12).start() # For Pi Camera
|
||||
time.sleep(2.0)
|
||||
|
||||
msg[0] = ""
|
||||
camera_on = True
|
||||
|
||||
while camera_on:
|
||||
frame = vs.read()
|
||||
barcodes = pyzbar.decode(frame)
|
||||
for barcode in barcodes:
|
||||
data = barcode.data.decode("utf-8")
|
||||
out_queue.put([data])
|
||||
break
|
||||
if len(barcodes) == 0:
|
||||
out_queue.put(["nodata"])
|
||||
|
||||
try:
|
||||
msg = in_queue.get(False)
|
||||
except:
|
||||
pass
|
||||
|
||||
if msg[0] == "stop":
|
||||
vs.stop()
|
||||
camera_on = False
|
||||
|
||||
elif msg[0] == "stop":
|
||||
print("stop camera!!")
|
||||
|
||||
class CameraPoll(object):
|
||||
|
||||
def __init__(self, interval, function, *args, **kwargs):
|
||||
self._timer = None
|
||||
self.interval = interval
|
||||
self.function = function
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.is_running = False
|
||||
self.start()
|
||||
|
||||
def _run(self):
|
||||
self.is_running = False
|
||||
self.start()
|
||||
self.function(*self.args, **self.kwargs)
|
||||
|
||||
def start(self):
|
||||
if not self.is_running:
|
||||
self._timer = Timer(self.interval, self._run)
|
||||
self._timer.start()
|
||||
self.is_running = True
|
||||
|
||||
def stop(self):
|
||||
self._timer.cancel()
|
||||
self.is_running = False
|
||||
@@ -0,0 +1,29 @@
|
||||
# Static Consents used globally to route to different parts of the app making it easier to read
|
||||
|
||||
class Path:
|
||||
|
||||
# Menu Location Routes
|
||||
MAIN_MENU = 0
|
||||
SEED_TOOLS_SUB_MENU = 10
|
||||
SIGNING_TOOLS_SUB_MENU = 11
|
||||
SETTINGS_SUB_MENU = 12
|
||||
SEED_WORD_12 = 20
|
||||
SEED_WORD_24 = 21
|
||||
POWER_OFF = 22
|
||||
|
||||
# Tools / Functions Routes
|
||||
GEN_LAST_WORD = 40
|
||||
DICE_GEN_SEED = 41
|
||||
SAVE_SEED = 42
|
||||
GEN_XPUB = 50
|
||||
SIGN_TRANSACTION = 51
|
||||
IO_TEST_TOOL = 60
|
||||
CURRENT_NETWORK = 61
|
||||
VERSION_INFO = 62
|
||||
DONATE = 63
|
||||
WALLET = 64
|
||||
|
||||
# Seed Slots
|
||||
SEED_SLOT_1 = 80
|
||||
SEED_SLOT_2 = 81
|
||||
SEED_SLOT_3 = 82
|
||||
@@ -0,0 +1,331 @@
|
||||
from buttons import Buttons, B
|
||||
from camera_process import CameraProcess
|
||||
from seed_storage import SeedStorage
|
||||
from camera_process import CameraPoll
|
||||
from view import View
|
||||
|
||||
# External Dependencies
|
||||
from embit.bip39 import mnemonic_to_bytes
|
||||
from embit.bip39 import mnemonic_from_bytes
|
||||
from embit import bip39
|
||||
from embit import script
|
||||
from embit import bip32
|
||||
from embit import psbt
|
||||
from embit.networks import NETWORKS
|
||||
from embit import ec
|
||||
import time
|
||||
import threading
|
||||
from threading import Timer
|
||||
import re
|
||||
from io import BytesIO
|
||||
from binascii import unhexlify, hexlify, a2b_base64, b2a_base64
|
||||
import textwrap
|
||||
|
||||
class PSBTSigner():
|
||||
|
||||
def __init__(self, seed_phrase, controller) -> None:
|
||||
|
||||
# check if seed is valid
|
||||
try:
|
||||
seed = bip39.mnemonic_to_seed((" ".join(seed_phrase)).strip())
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
self.controller = controller
|
||||
self.seed_storage = controller.storage
|
||||
self.buttons = controller.buttons
|
||||
|
||||
self.seed = seed
|
||||
self.root = bip32.HDKey.from_seed(self.seed, version=NETWORKS[self.seed_storage.get_network()]["xprv"])
|
||||
self.fingerprint = self.root.child(0).fingerprint
|
||||
self.bip48_xprv = self.root.derive(self.seed_storage.get_hardened_derivation())
|
||||
self.bip48_xpub = self.bip48_xprv.to_public()
|
||||
self.seed_phrase = seed_phrase
|
||||
|
||||
self.tx = None
|
||||
self.inp_amount = None
|
||||
self.fee = None
|
||||
self.spend = None
|
||||
self.destinationaddress = None
|
||||
self.raw_psbt = None
|
||||
|
||||
self.camera_loop_timer = None
|
||||
self.camera_data = None
|
||||
self.is_camera_data = False
|
||||
|
||||
self.qr_total_frames = 0
|
||||
self.qr_cur_frame_count = 0
|
||||
self.qr_data = []
|
||||
self.frame_display = []
|
||||
self.percentage_complete = 0
|
||||
|
||||
self.scan_started_ind = 0
|
||||
|
||||
return
|
||||
|
||||
def parse_psbt(self, raw_psbt) -> bool:
|
||||
# Parse pbst transaction
|
||||
#try:
|
||||
base64_psbt = a2b_base64(raw_psbt)
|
||||
tx = psbt.PSBT.parse(base64_psbt)
|
||||
#except:
|
||||
# return False
|
||||
|
||||
self.tx = tx
|
||||
(self.inp_amount, policy) = PSBTSigner.input_amount(self.tx)
|
||||
(self.change, self.fee, self.spend, self.destinationaddress) = PSBTSigner.change_fee_spend_amounts(self.tx, self.inp_amount, policy, self.seed_storage.get_network())
|
||||
self.raw_psbt = raw_psbt
|
||||
|
||||
return True
|
||||
|
||||
def sign_transaction(self) -> (bool, str):
|
||||
|
||||
# try:
|
||||
# sign the transaction
|
||||
self.tx.sign_with(self.root)
|
||||
|
||||
#added section to trim psbt
|
||||
trimmed_psbt = psbt.PSBT(self.tx.tx)
|
||||
sigsEnd = 0
|
||||
for i, inp in enumerate(self.tx.inputs):
|
||||
sigsEnd += len(list(inp.partial_sigs.keys()))
|
||||
trimmed_psbt.inputs[i].partial_sigs = inp.partial_sigs
|
||||
|
||||
raw_trimmed_signed_psbt = trimmed_psbt.serialize()
|
||||
# except:
|
||||
# return (False, self.tx)
|
||||
|
||||
# convert to base64
|
||||
b64_psbt = b2a_base64(raw_trimmed_signed_psbt)
|
||||
# somehow b2a ends with \n...
|
||||
if b64_psbt[-1:] == b"\n":
|
||||
b64_psbt = b64_psbt[:-1]
|
||||
|
||||
return (True, b64_psbt.decode('utf-8'))
|
||||
|
||||
def scan_animated_psbt_qr(self) -> str:
|
||||
self.controller.menu_view.draw_modal(["Initializing Camera"])
|
||||
# initialize camera
|
||||
self.controller.to_camera_queue.put(["start"])
|
||||
# First get blocking, this way it's clear when the camera is ready for the end user
|
||||
self.controller.from_camera_queue.get()
|
||||
self.camera_loop_timer = CameraPoll(0.05, self.process_camera_data)
|
||||
|
||||
input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT])
|
||||
if input in (B.KEY_LEFT, B.KEY_RIGHT):
|
||||
self.camera_loop_timer.stop()
|
||||
self.controller.to_camera_queue.put(["stop"])
|
||||
return "nodata"
|
||||
elif input == B.OVERRIDE:
|
||||
self.camera_loop_timer.stop()
|
||||
self.controller.to_camera_queue.put(["stop"])
|
||||
if self.qr_data[0] == "invalid":
|
||||
return "invalid"
|
||||
return "".join(self.qr_data)
|
||||
|
||||
def process_camera_data(self):
|
||||
try:
|
||||
data = self.controller.from_camera_queue.get(False)
|
||||
except:
|
||||
data = ["nodata"]
|
||||
|
||||
if data[0] != "nodata":
|
||||
if self.qr_total_frames == 0:
|
||||
self.qr_total_frames = self.get_totals_frames(data[0])
|
||||
if self.qr_total_frames == -1:
|
||||
self.qr_data = ["invalid"]
|
||||
self.buttons.trigger_override() # something went wrong, invalid QR
|
||||
return
|
||||
self.qr_data = ["empty"] * self.qr_total_frames
|
||||
self.frame_display = ["-"] * self.qr_total_frames
|
||||
current_frame = self.get_current_frame(data[0])
|
||||
if self.qr_data[current_frame - 1] == "empty":
|
||||
self.qr_data[current_frame - 1] = self.parse_qr_data(data[0])
|
||||
self.qr_cur_frame_count += 1
|
||||
self.frame_display[current_frame - 1] = "*"
|
||||
if self.qr_total_frames > 0:
|
||||
self.percentage_complete = int((self.qr_cur_frame_count / self.qr_total_frames) * 100)
|
||||
if "empty" not in self.qr_data:
|
||||
# pulled all animated qr frames
|
||||
self.buttons.trigger_override()
|
||||
|
||||
|
||||
#self.frame_display = [f.replace('x', '*') for f in self.frame_display]
|
||||
#self.frame_display[current_frame - 1] = "x"
|
||||
|
||||
# display percentage / frames complete
|
||||
print(self.frame_display)
|
||||
|
||||
if "empty" in self.qr_data:
|
||||
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill="ORANGE", font=View.IMPACT22)
|
||||
lines = textwrap.wrap("".join(self.frame_display), width=11)
|
||||
yheight = 60
|
||||
for line in lines:
|
||||
tw, th = View.draw.textsize(line, font=View.COURIERNEW30)
|
||||
View.draw.text(((240 - tw) / 2, yheight), line, fill="ORANGE", font=View.COURIERNEW30)
|
||||
yheight += 30
|
||||
tw, th = View.draw.textsize("Right to Exit", font=View.IMPACT18)
|
||||
View.draw.text(((240 - tw) / 2, 215), "Right to Exit", fill="ORANGE", font=View.IMPACT18)
|
||||
View.DispShowImage()
|
||||
|
||||
elif self.scan_started_ind == 0:
|
||||
self.scan_started_ind = 1
|
||||
self.controller.menu_view.draw_modal(["Scan Animated QR"], "", "Right to Exit")
|
||||
|
||||
def get_totals_frames(self, camera_data) -> int:
|
||||
if re.search("^p(\d+)of(\d+) ", camera_data, re.IGNORECASE) != None:
|
||||
return int(re.search("^p(\d+)of(\d+) ", camera_data, re.IGNORECASE).group(2))
|
||||
else:
|
||||
return -1
|
||||
|
||||
def get_current_frame(self, camera_data) -> int:
|
||||
if re.search("^p(\d+)of(\d+) ", camera_data, re.IGNORECASE) != None:
|
||||
return int(re.search("^p(\d+)of(\d+) ", camera_data, re.IGNORECASE).group(1))
|
||||
else:
|
||||
return -1
|
||||
|
||||
def parse_qr_data(self, camera_data) -> str:
|
||||
return camera_data.split(" ")[-1].strip()
|
||||
|
||||
###
|
||||
### Internal PSBTSigner Transactions
|
||||
###
|
||||
|
||||
def input_amount(tx) -> (float, str):
|
||||
# Check inputs of the transaction and check that they use the same script type
|
||||
# For multisig parsed policy will look like this:
|
||||
# { script_type: p2wsh, cosigners: [xpubs strings], m: 2, n: 3}
|
||||
policy = None
|
||||
inp_amount = 0.0
|
||||
for inp in tx.inputs:
|
||||
inp_amount += inp.witness_utxo.value
|
||||
# get policy of the input
|
||||
inp_policy = PSBTSigner.get_policy(inp, inp.witness_utxo.script_pubkey, tx.xpubs)
|
||||
# if policy is None - assign current
|
||||
if policy is None:
|
||||
policy = inp_policy
|
||||
# otherwise check that everything in the policy is the same
|
||||
else:
|
||||
# check policy is the same
|
||||
if policy != inp_policy:
|
||||
raise RuntimeError("Mixed inputs in the transaction")
|
||||
|
||||
return (inp_amount, policy)
|
||||
|
||||
def change_fee_spend_amounts(tx, inp_amount, policy, currentnetwork) -> (float, float, float):
|
||||
spend = 0
|
||||
change = 0
|
||||
for i, out in enumerate(tx.outputs):
|
||||
out_policy = PSBTSigner.get_policy(out, tx.tx.vout[i].script_pubkey, tx.xpubs)
|
||||
is_change = False
|
||||
# if policy is the same - probably change
|
||||
if out_policy == policy:
|
||||
# double-check that it's change
|
||||
# we already checked in get_cosigners and parse_multisig
|
||||
# that pubkeys are generated from cosigners,
|
||||
# and witness script is corresponding multisig
|
||||
# so we only need to check that scriptpubkey is generated from
|
||||
# witness script
|
||||
|
||||
# empty script by default
|
||||
sc = script.Script(b"")
|
||||
# multisig, we know witness script
|
||||
if policy["type"] == "p2wsh":
|
||||
sc = script.p2wsh(out.witness_script)
|
||||
elif policy["type"] == "p2sh-p2wsh":
|
||||
sc = script.p2sh(script.p2wsh(out.witness_script))
|
||||
# single-sig
|
||||
elif "pkh" in policy["type"]:
|
||||
if len(out.bip32_derivations.values()) > 0:
|
||||
der = list(out.bip32_derivations.values())[0].derivation
|
||||
my_pubkey = root.derive(der)
|
||||
if policy["type"] == "p2wpkh":
|
||||
sc = script.p2wpkh(my_pubkey)
|
||||
elif policy["type"] == "p2sh-p2wpkh":
|
||||
sc = script.p2sh(script.p2wpkh(my_pubkey))
|
||||
if sc.data == tx.tx.vout[i].script_pubkey.data:
|
||||
is_change = True
|
||||
if is_change:
|
||||
change += tx.tx.vout[i].value
|
||||
print("Change %d sats" % tx.tx.vout[i].value)
|
||||
else:
|
||||
spend += tx.tx.vout[i].value
|
||||
print("Spending %d sats to %s" % (tx.tx.vout[i].value, tx.tx.vout[i].script_pubkey.address(NETWORKS[currentnetwork])))
|
||||
destinationaddress = tx.tx.vout[i].script_pubkey.address(NETWORKS[currentnetwork])
|
||||
|
||||
fee = inp_amount - change - spend
|
||||
|
||||
return (change, fee, spend, destinationaddress)
|
||||
|
||||
def parse_multisig(sc):
|
||||
"""Takes a script and extracts m,n and pubkeys from it"""
|
||||
# OP_m <len:pubkey> ... <len:pubkey> OP_n OP_CHECKMULTISIG
|
||||
# check min size
|
||||
if len(sc.data) < 37 or sc.data[-1] != 0xae:
|
||||
raise ValueError("Not a multisig script")
|
||||
m = sc.data[0] - 0x50
|
||||
if m < 1 or m > 16:
|
||||
raise ValueError("Invalid multisig script")
|
||||
n = sc.data[-2] - 0x50
|
||||
if n < m or n > 16:
|
||||
raise ValueError("Invalid multisig script")
|
||||
s = BytesIO(sc.data)
|
||||
# drop first byte
|
||||
s.read(1)
|
||||
# read pubkeys
|
||||
pubkeys = []
|
||||
for i in range(n):
|
||||
char = s.read(1)
|
||||
if char != b"\x21":
|
||||
raise ValueError("Invlid pubkey")
|
||||
pubkeys.append(ec.PublicKey.parse(s.read(33)))
|
||||
# check that nothing left
|
||||
if s.read() != sc.data[-2:]:
|
||||
raise ValueError("Invalid multisig script")
|
||||
return m, n, pubkeys
|
||||
|
||||
def get_cosigners(pubkeys, derivations, xpubs):
|
||||
"""Returns xpubs used to derive pubkeys using global xpub field from psbt"""
|
||||
cosigners = []
|
||||
for i, pubkey in enumerate(pubkeys):
|
||||
if pubkey not in derivations:
|
||||
raise ValueError("Missing derivation")
|
||||
der = derivations[pubkey]
|
||||
for xpub in xpubs:
|
||||
origin_der = xpubs[xpub]
|
||||
# check fingerprint
|
||||
if origin_der.fingerprint == der.fingerprint:
|
||||
# check derivation - last two indexes give pub from xpub
|
||||
if origin_der.derivation == der.derivation[:-2]:
|
||||
# check that it derives to pubkey actually
|
||||
if xpub.derive(der.derivation[-2:]).key == pubkey:
|
||||
# append strings so they can be sorted and compared
|
||||
cosigners.append(xpub.to_base58())
|
||||
break
|
||||
if len(cosigners) != len(pubkeys):
|
||||
raise RuntimeError("Can't get all cosigners")
|
||||
return sorted(cosigners)
|
||||
|
||||
def get_policy(scope, scriptpubkey, xpubs):
|
||||
"""Parse scope and get policy"""
|
||||
# we don't know the policy yet, let's parse it
|
||||
script_type = scriptpubkey.script_type()
|
||||
# p2sh can be either legacy multisig, or nested segwit multisig
|
||||
# or nested segwit singlesig
|
||||
if script_type == "p2sh":
|
||||
if scope.witness_script is not None:
|
||||
script_type = "p2sh-p2wsh"
|
||||
elif scope.redeem_script is not None and scope.redeem_script.script_type() == "p2wpkh":
|
||||
script_type = "p2sh-p2wpkh"
|
||||
policy = { "type": script_type }
|
||||
# expected multisig
|
||||
if "p2wsh" in script_type and scope.witness_script is not None:
|
||||
m, n, pubkeys = PSBTSigner.parse_multisig(scope.witness_script)
|
||||
# check pubkeys are derived from cosigners
|
||||
cosigners = PSBTSigner.get_cosigners(pubkeys, scope.bip32_derivations, xpubs)
|
||||
policy.update({
|
||||
"m": m, "n": n, "cosigners": cosigners
|
||||
})
|
||||
return policy
|
||||
@@ -0,0 +1,49 @@
|
||||
import qrcode
|
||||
|
||||
class QR:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.qrsize = 120 # Default
|
||||
|
||||
def qrimage(self, data, qrsize = 0):
|
||||
if qrsize != 0:
|
||||
self.qrsize = qrsize
|
||||
|
||||
qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=1, border=2 )
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
return(qr.make_image(fill_color="black", back_color="white").resize((240,240)).convert('RGB'))
|
||||
|
||||
def makeqrcodes(self, data, format, qrsize = 0, callback = None) -> []:
|
||||
if qrsize != 0:
|
||||
self.qrsize = qrsize
|
||||
|
||||
cnt = 0
|
||||
qr_cnt = 0
|
||||
start = 0
|
||||
stop = self.qrsize
|
||||
data_parts = []
|
||||
images = []
|
||||
|
||||
if format == "Specter Desktop":
|
||||
|
||||
qr_cnt = (len(data) // self.qrsize) + 1
|
||||
|
||||
if qr_cnt == 1:
|
||||
return [self.qrimage(data)]
|
||||
else:
|
||||
while cnt < qr_cnt:
|
||||
|
||||
data_parts.append("p" + str(cnt+1) + "of" + str(qr_cnt) + " " + data[start:stop])
|
||||
images.append(self.qrimage(data_parts[-1], qrsize))
|
||||
print(data_parts[-1])
|
||||
start = start + self.qrsize
|
||||
stop = stop + self.qrsize
|
||||
if stop > len(data):
|
||||
stop = len(data)
|
||||
cnt += 1
|
||||
|
||||
if callback != None:
|
||||
callback((cnt * 100.0) / qr_cnt)
|
||||
|
||||
return images
|
||||
@@ -1,44 +1,12 @@
|
||||
import resources
|
||||
import sys
|
||||
sys.path.append('./views')
|
||||
sys.path.append('./models')
|
||||
sys.path.append('./helpers')
|
||||
|
||||
runstate = [0, "", ["", "", ""]]
|
||||
seedsignerrunning = True
|
||||
from controller import Controller
|
||||
import time
|
||||
|
||||
while seedsignerrunning == True:
|
||||
if runstate[0] == 0:
|
||||
resources.main_menu(runstate)
|
||||
if runstate[0] == 1:
|
||||
resources.seedgen_menu(runstate)
|
||||
if runstate[0] == 2:
|
||||
resources.signing_menu(runstate)
|
||||
if runstate[0] == 3:
|
||||
resources.settings_menu(runstate)
|
||||
if runstate[0] == 50:
|
||||
resources.iotestscreen(runstate)
|
||||
if runstate[0] == 51:
|
||||
resources.get_pre_calc_words(runstate)
|
||||
if runstate[0] == 52:
|
||||
resources.get_final_word(runstate)
|
||||
if runstate[0] == 53:
|
||||
resources.getseedfromdice(runstate)
|
||||
if runstate[0] == 54:
|
||||
resources.make_xpub(runstate)
|
||||
if runstate[0] == 55:
|
||||
resources.sign_psbt(runstate)
|
||||
if runstate[0] == 56:
|
||||
resources.donate(runstate)
|
||||
if runstate[0] == 57:
|
||||
resources.version_info(runstate)
|
||||
if runstate[0] == 58:
|
||||
resources.powering_down_notifier(runstate)
|
||||
if runstate[0] == 59:
|
||||
resources.gather_23_words(runstate)
|
||||
if runstate[0] == 60:
|
||||
resources.gather_11_words(runstate)
|
||||
if runstate[0] == 61:
|
||||
resources.network_menu(runstate)
|
||||
if runstate[0] == 62:
|
||||
resources.seedselect_menu(runstate)
|
||||
if runstate[0] == 63:
|
||||
resources.tempsaveseed(runstate)
|
||||
print("Back at main and runstate is:")
|
||||
print(runstate)
|
||||
DEBUG = True
|
||||
|
||||
controller = Controller()
|
||||
controller.start(DEBUG)
|
||||
File diff suppressed because one or more lines are too long
-3156
File diff suppressed because one or more lines are too long
@@ -0,0 +1,141 @@
|
||||
# External Dependencies
|
||||
import threading
|
||||
from threading import Timer
|
||||
|
||||
# Internal file class dependencies
|
||||
from view import View
|
||||
from buttons import B
|
||||
from camera_process import CameraPoll
|
||||
|
||||
class IOTestView(View):
|
||||
|
||||
def __init__(self, controller) -> None:
|
||||
View.__init__(self, controller)
|
||||
self.camera_loop_timer = None
|
||||
self.redraw = False
|
||||
self.redraw_complete = False
|
||||
self.qr_text = "Scan ANY QR Code"
|
||||
|
||||
def display_io_test_screen(self):
|
||||
|
||||
# display loading screen
|
||||
self.draw_modal(["Initializing I/O Test"])
|
||||
print("Initializing I/O Test")
|
||||
self.qr_text = "Scan ANY QR Code"
|
||||
|
||||
# initialize camera
|
||||
self.controller.to_camera_queue.put(["start"])
|
||||
|
||||
# First get blocking, this way it's clear when the camera is ready for the end user
|
||||
self.controller.from_camera_queue.get()
|
||||
|
||||
self.camera_loop_timer = CameraPoll(0.05, self.get_camera_data) # it auto-starts, no need of rt.start()
|
||||
|
||||
while True:
|
||||
|
||||
self.draw_io_screen()
|
||||
|
||||
input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS, B.KEY_RIGHT, B.KEY_LEFT, B.KEY1, B.KEY2, B.KEY3], False)
|
||||
if input == B.KEY_UP:
|
||||
ret_val = self.up_button()
|
||||
elif input == B.KEY_DOWN:
|
||||
ret_val = self.down_button()
|
||||
elif input == B.KEY_RIGHT:
|
||||
ret_val = self.right_button()
|
||||
elif input == B.KEY_LEFT:
|
||||
ret_val = self.left_button()
|
||||
elif input == B.KEY_PRESS:
|
||||
ret_val = self.press_button()
|
||||
elif input == B.KEY1:
|
||||
ret_val = self.a_button()
|
||||
elif input == B.KEY2:
|
||||
ret_val = self.b_button()
|
||||
elif input == B.KEY3:
|
||||
ret_val = self.c_button()
|
||||
return True
|
||||
|
||||
def get_camera_data(self):
|
||||
try:
|
||||
data = self.controller.from_camera_queue.get(False)
|
||||
if data[0] != "nodata":
|
||||
self.draw_scan_detected()
|
||||
except:
|
||||
data = ["nodata"]
|
||||
|
||||
if self.redraw == True and self.redraw_complete == True:
|
||||
self.draw_io_screen()
|
||||
|
||||
def draw_io_screen(self):
|
||||
self.redraw_complete = False
|
||||
self.redraw = False
|
||||
self.draw.rectangle((0,0,View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
self.draw.text((45, 5), "Input/Output Check:", fill="ORANGE", font=View.IMPACT18)
|
||||
self.draw.polygon([(61, 89), (80, 46), (99, 89)], outline="ORANGE", fill=0)
|
||||
self.draw.polygon([(51, 100), (8, 119), (51, 138)], outline="ORANGE", fill=0)
|
||||
self.draw.polygon([(109, 100), (152, 119), (109, 138)], outline="ORANGE", fill=0)
|
||||
self.draw.polygon([(61, 151), (80, 193), (99, 151)], outline="ORANGE", fill=0)
|
||||
self.draw.ellipse([(61, 99), (99, 141)], outline="ORANGE", fill=0)
|
||||
self.draw.ellipse([(198, 40), (238, 80)], outline="ORANGE", fill=0)
|
||||
self.draw.ellipse([(198, 95), (238, 135)], outline="ORANGE", fill=0)
|
||||
self.draw.text((200, 160), "EXIT", fill="ORANGE", font=View.IMPACT18)
|
||||
self.draw.rectangle((30, 205, 210, 235), outline="ORANGE", fill="BLACK")
|
||||
tw, th = self.draw.textsize(self.qr_text, font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill="ORANGE", font=View.IMPACT22)
|
||||
View.DispShowImage()
|
||||
self.redraw_complete = True
|
||||
|
||||
def a_button(self):
|
||||
if self.redraw == False and self.redraw_complete == True:
|
||||
self.draw.ellipse([(198, 40), (238, 80)], outline="ORANGE", fill="ORANGE")
|
||||
View.DispShowImage()
|
||||
self.redraw = True
|
||||
|
||||
def b_button(self):
|
||||
if self.redraw == False and self.redraw_complete == True:
|
||||
self.draw.ellipse([(198, 95), (238, 135)], outline="ORANGE", fill="ORANGE")
|
||||
View.DispShowImage()
|
||||
self.redraw = True
|
||||
|
||||
def c_button(self):
|
||||
self.camera_loop_timer.stop()
|
||||
self.controller.to_camera_queue.put(["stop"])
|
||||
|
||||
def up_button(self):
|
||||
if self.redraw == False and self.redraw_complete == True:
|
||||
self.draw.polygon([(61, 89), (80, 46), (99, 89)], outline="ORANGE", fill="ORANGE")
|
||||
View.DispShowImage()
|
||||
self.redraw = True
|
||||
|
||||
def down_button(self):
|
||||
if self.redraw == False and self.redraw_complete == True:
|
||||
self.draw.polygon([(61, 151), (80, 193), (99, 151)], outline="ORANGE", fill="ORANGE")
|
||||
View.DispShowImage()
|
||||
self.redraw = True
|
||||
|
||||
def left_button(self):
|
||||
if self.redraw == False and self.redraw_complete == True:
|
||||
self.draw.polygon([(51, 100), (8, 119), (51, 138)], outline="ORANGE", fill="ORANGE")
|
||||
View.DispShowImage()
|
||||
self.redraw = True
|
||||
|
||||
def right_button(self):
|
||||
if self.redraw == False and self.redraw_complete == True:
|
||||
self.draw.polygon([(109, 100), (152, 119), (109, 138)], outline="ORANGE", fill="ORANGE")
|
||||
View.DispShowImage()
|
||||
self.redraw = True
|
||||
|
||||
def press_button(self):
|
||||
if self.redraw == False and self.redraw_complete == True:
|
||||
self.draw.ellipse([(61, 99), (99, 141)], outline="ORANGE", fill="ORANGE")
|
||||
View.DispShowImage()
|
||||
self.redraw = True
|
||||
|
||||
def draw_scan_detected(self):
|
||||
self.qr_text = "QR Scanned"
|
||||
if self.redraw == False and self.redraw_complete == True:
|
||||
self.draw.rectangle((30, 205, 210, 235), outline="ORANGE", fill="ORANGE")
|
||||
tw, th = self.draw.textsize(self.qr_text, font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill="BLACK", font=View.IMPACT22)
|
||||
View.DispShowImage()
|
||||
self.redraw = True
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
# Internal file class dependencies
|
||||
from view import View
|
||||
from buttons import B
|
||||
from path import Path
|
||||
from seed_storage import SeedStorage
|
||||
|
||||
# External Dependencies
|
||||
import time
|
||||
import re
|
||||
|
||||
class MenuView(View):
|
||||
|
||||
def __init__(self, controller) -> None:
|
||||
View.__init__(self, controller)
|
||||
|
||||
self.menu_lines = []
|
||||
self.selected_menu_num = 1
|
||||
|
||||
###
|
||||
### Main Navigation
|
||||
###
|
||||
|
||||
### Main Menu
|
||||
|
||||
def display_main_menu(self, sub_menu = None) -> int:
|
||||
ret_val = 0
|
||||
input = 0
|
||||
lines = ["Seed Tools", "Signing Tools", "Settings", "Power OFF Device"]
|
||||
|
||||
if sub_menu == Path.SEED_TOOLS_SUB_MENU:
|
||||
return self.display_seed_tools_menu()
|
||||
elif sub_menu == Path.SIGNING_TOOLS_SUB_MENU:
|
||||
return self.display_signing_tools_menu()
|
||||
elif sub_menu == Path.SETTINGS_SUB_MENU:
|
||||
return self.display_settings_menu()
|
||||
else:
|
||||
self.draw_menu(lines, 1)
|
||||
|
||||
# Wait for Button Input (specifically menu selection/press)
|
||||
while True:
|
||||
if ret_val == 0:
|
||||
input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS])
|
||||
else:
|
||||
return ret_val
|
||||
if input == B.KEY_UP:
|
||||
self.menu_up()
|
||||
elif input == B.KEY_DOWN:
|
||||
self.menu_down()
|
||||
elif input == B.KEY_PRESS:
|
||||
if self.selected_menu_num == 1:
|
||||
ret_val = self.display_seed_tools_menu()
|
||||
elif self.selected_menu_num == 2:
|
||||
ret_val = self.display_signing_tools_menu()
|
||||
elif self.selected_menu_num == 3:
|
||||
ret_val = self.display_settings_menu()
|
||||
elif self.selected_menu_num == 4:
|
||||
ret_val = Path.POWER_OFF
|
||||
|
||||
if ret_val != Path.MAIN_MENU: # When no main menu, return to controller
|
||||
return ret_val
|
||||
else:
|
||||
self.draw_menu(lines)
|
||||
|
||||
### Seed Tools Menu
|
||||
|
||||
def display_seed_tools_menu(self) -> int:
|
||||
lines = ["... [ Return to Main ]", "Generate Word 12 / 24", "Create a Seed w/ Dice", "Store a Seed (temp)"]
|
||||
self.draw_menu(lines, 1)
|
||||
input = 0
|
||||
|
||||
# Wait for Button Input (specifically menu selection/press)
|
||||
while True:
|
||||
input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS])
|
||||
if input == B.KEY_UP:
|
||||
self.menu_up()
|
||||
elif input == B.KEY_DOWN:
|
||||
self.menu_down()
|
||||
elif input == B.KEY_PRESS:
|
||||
if self.selected_menu_num == 1:
|
||||
return Path.MAIN_MENU
|
||||
elif self.selected_menu_num == 2:
|
||||
return Path.GEN_LAST_WORD
|
||||
elif self.selected_menu_num == 3:
|
||||
return Path.DICE_GEN_SEED
|
||||
elif self.selected_menu_num == 4:
|
||||
return Path.SAVE_SEED
|
||||
|
||||
### Signing Tools Menu
|
||||
|
||||
def display_signing_tools_menu(self) -> None:
|
||||
lines = ["... [ Return to Main ]", "Generate XPUB", "Sign a Transaction"]
|
||||
self.draw_menu(lines, 1)
|
||||
input = 0
|
||||
|
||||
# Wait for Button Input (specifically menu selection/press)
|
||||
while True:
|
||||
input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS])
|
||||
if input == B.KEY_UP:
|
||||
self.menu_up()
|
||||
elif input == B.KEY_DOWN:
|
||||
self.menu_down()
|
||||
elif input == B.KEY_PRESS:
|
||||
if self.selected_menu_num == 1:
|
||||
return Path.MAIN_MENU
|
||||
elif self.selected_menu_num == 2:
|
||||
return Path.GEN_XPUB
|
||||
elif self.selected_menu_num == 3:
|
||||
return Path.SIGN_TRANSACTION
|
||||
|
||||
### Settings Menu
|
||||
|
||||
def display_settings_menu(self) -> int:
|
||||
lines = ["... [ Return to Main ]", "Input / Output Tests", "Current Network: <network>", "Wallet: <wallet>", "Version Info", "Donate to SeedSigner"]
|
||||
input = 0
|
||||
|
||||
lines[2] = lines[2].replace("<network>", self.controller.storage.get_network())
|
||||
lines[3] = lines[3].replace("<wallet>", "Specter Desktop")
|
||||
|
||||
# Draw Menu
|
||||
self.draw_menu(lines, 1)
|
||||
|
||||
# Wait for Button Input (specifically menu selection/press)
|
||||
while True:
|
||||
input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS])
|
||||
if input == B.KEY_UP:
|
||||
self.menu_up()
|
||||
elif input == B.KEY_DOWN:
|
||||
self.menu_down()
|
||||
elif input == B.KEY_PRESS:
|
||||
if self.selected_menu_num == 1:
|
||||
return Path.MAIN_MENU
|
||||
elif self.selected_menu_num == 2:
|
||||
return Path.IO_TEST_TOOL
|
||||
elif self.selected_menu_num == 3:
|
||||
return Path.CURRENT_NETWORK
|
||||
elif self.selected_menu_num == 4:
|
||||
return Path.WALLET
|
||||
elif self.selected_menu_num == 5:
|
||||
return Path.VERSION_INFO
|
||||
elif self.selected_menu_num == 6:
|
||||
return Path.DONATE
|
||||
|
||||
### Generic Single Menu Selection (returns 1,2,3,4,5,6 ...)
|
||||
|
||||
def display_generic_selection_menu(self, lines = [], title = None, bottom = None) -> int:
|
||||
self.draw_menu(lines, 1, title, bottom)
|
||||
|
||||
while True:
|
||||
input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS])
|
||||
if input == B.KEY_UP:
|
||||
self.menu_up(title, bottom)
|
||||
elif input == B.KEY_DOWN:
|
||||
self.menu_down(title, bottom)
|
||||
elif input == B.KEY_PRESS:
|
||||
return self.selected_menu_num
|
||||
|
||||
### Generic Word 12 or 24 seed phrase menu
|
||||
|
||||
def display_12_24_word_menu(self, return_txt = "... [ Return to ... ]") -> int:
|
||||
lines = [return_txt, "Use a 12 word seed", "Use a 24 word seed"]
|
||||
self.draw_menu(lines)
|
||||
|
||||
# Wait for Button Input (specifically menu selection/press)
|
||||
while True:
|
||||
input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS])
|
||||
if input == B.KEY_UP:
|
||||
self.menu_up()
|
||||
elif input == B.KEY_DOWN:
|
||||
self.menu_down()
|
||||
elif input == B.KEY_PRESS:
|
||||
if self.selected_menu_num == 1:
|
||||
return -1
|
||||
elif self.selected_menu_num == 2:
|
||||
return Path.SEED_WORD_12
|
||||
elif self.selected_menu_num == 3:
|
||||
return Path.SEED_WORD_24
|
||||
|
||||
### Select a Seed Slot to Save a Seed Menu
|
||||
|
||||
def display_saved_seed_menu(self, storage, type = 1, return_sel_txt = "... [ Return to Seed Tools ]") -> int:
|
||||
lines = []
|
||||
if return_sel_txt != None:
|
||||
lines.append(return_sel_txt)
|
||||
|
||||
if type == 1:
|
||||
# Show all slots used and free
|
||||
lines.extend(["Use Seed Slot #1", "Use Seed Slot #2", "Use Seed Slot #3"])
|
||||
if storage.check_slot_1():
|
||||
lines[1] = "Display Seed Slot #1" # replace
|
||||
if storage.check_slot_2():
|
||||
lines[2] = "Display Seed Slot #2" # replace
|
||||
if storage.check_slot_3():
|
||||
lines[3] = "Display Seed Slot #3" # replace
|
||||
elif type == 2:
|
||||
# Show only free slots
|
||||
if storage.check_slot_1() == False:
|
||||
lines.append("Use Seed Slot #1")
|
||||
if storage.check_slot_2() == False:
|
||||
lines.append("Use Seed Slot #2")
|
||||
if storage.check_slot_3() == False:
|
||||
lines.append("Use Seed Slot #3")
|
||||
if storage.num_of_free_slots() == 0:
|
||||
return 0
|
||||
elif type == 3:
|
||||
# Show only used slots
|
||||
if storage.check_slot_1():
|
||||
lines.append("Use Seed Slot #1")
|
||||
if storage.check_slot_2():
|
||||
lines.append("Use Seed Slot #2")
|
||||
if storage.check_slot_3():
|
||||
lines.append("Use Seed Slot #3")
|
||||
else:
|
||||
return 0
|
||||
|
||||
self.draw_menu(lines)
|
||||
|
||||
# Wait for Button Input (specifically menu selection/press)
|
||||
while True:
|
||||
input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS])
|
||||
if input == B.KEY_UP:
|
||||
self.menu_up()
|
||||
elif input == B.KEY_DOWN:
|
||||
self.menu_down()
|
||||
elif input == B.KEY_PRESS:
|
||||
if lines[self.selected_menu_num-1] == return_sel_txt:
|
||||
return 0
|
||||
else:
|
||||
return int(re.search("#(\d+)", lines[self.selected_menu_num-1], re.IGNORECASE).group(1))
|
||||
|
||||
###
|
||||
### Generic Reusable Menu Methods/Functions
|
||||
###
|
||||
|
||||
### Generic Draw Menu Method
|
||||
|
||||
def draw_menu(self, lines, selected_menu_num = 1, title = None, bottom = None) -> None:
|
||||
if title == None:
|
||||
t = "SeedSigner v" + self.controller.VERSION
|
||||
else:
|
||||
t = title
|
||||
|
||||
if bottom == None:
|
||||
b = "Press Control Stick to Select"
|
||||
else:
|
||||
b = bottom
|
||||
|
||||
if lines != self.menu_lines or selected_menu_num != self.selected_menu_num:
|
||||
#Menu has changed, redraw
|
||||
|
||||
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
tw, th = View.draw.textsize(t, font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 2), t, fill="ORANGE", font=View.IMPACT22)
|
||||
|
||||
num_of_lines = len(lines)
|
||||
|
||||
if selected_menu_num <= 4:
|
||||
if num_of_lines >= 1:
|
||||
self.draw_menu_text(15, 45 , lines[0], (True if selected_menu_num == 1 else False))
|
||||
if num_of_lines >= 2:
|
||||
self.draw_menu_text(15, 80 , lines[1], (True if selected_menu_num == 2 else False))
|
||||
if num_of_lines >= 3:
|
||||
self.draw_menu_text(15, 115, lines[2], (True if selected_menu_num == 3 else False))
|
||||
if num_of_lines >= 4:
|
||||
self.draw_menu_text(15, 150, lines[3], (True if selected_menu_num == 4 else False))
|
||||
elif selected_menu_num >= 5 and selected_menu_num <= 8:
|
||||
if num_of_lines >= 5:
|
||||
self.draw_menu_text(15, 45 , lines[4], (True if selected_menu_num == 5 else False))
|
||||
if num_of_lines >= 6:
|
||||
self.draw_menu_text(15, 80 , lines[5], (True if selected_menu_num == 6 else False))
|
||||
if num_of_lines >= 7:
|
||||
self.draw_menu_text(15, 115, lines[6], (True if selected_menu_num == 7 else False))
|
||||
if num_of_lines >= 8:
|
||||
self.draw_menu_text(15, 150, lines[7], (True if selected_menu_num == 8 else False))
|
||||
elif selected_menu_num >= 9 and selected_menu_num <= 12:
|
||||
if num_of_lines >= 9:
|
||||
self.draw_menu_text(15, 45 , lines[8], (True if selected_menu_num == 9 else False))
|
||||
if num_of_lines >= 10:
|
||||
self.draw_menu_text(15, 80 , lines[9], (True if selected_menu_num == 10 else False))
|
||||
if num_of_lines >= 11:
|
||||
self.draw_menu_text(15, 115, lines[10], (True if selected_menu_num == 11 else False))
|
||||
if num_of_lines >= 12:
|
||||
self.draw_menu_text(15, 150, lines[11], (True if selected_menu_num == 12 else False))
|
||||
|
||||
tw, th = View.draw.textsize(b, font=View.IMPACT18)
|
||||
View.draw.text(((240 - tw) / 2, 210), b, fill="ORANGE", font=View.IMPACT18)
|
||||
View.DispShowImage()
|
||||
|
||||
# saved update menu lines and selection
|
||||
self.menu_lines = lines
|
||||
self.selected_menu_num = selected_menu_num
|
||||
|
||||
return
|
||||
|
||||
### Generic Menu Navigation
|
||||
|
||||
def menu_up(self, title = None, bottom = None):
|
||||
if self.selected_menu_num <= 1:
|
||||
self.draw_menu(self.menu_lines, len(self.menu_lines), title, bottom)
|
||||
else:
|
||||
self.draw_menu(self.menu_lines, self.selected_menu_num - 1, title, bottom)
|
||||
|
||||
def menu_down(self, title = None, bottom = None):
|
||||
if self.selected_menu_num >= len(self.menu_lines):
|
||||
self.draw_menu(self.menu_lines, 1, title, bottom)
|
||||
else:
|
||||
self.draw_menu(self.menu_lines, self.selected_menu_num + 1, title, bottom)
|
||||
|
||||
### Internal View Method to Display a Line in a Menu Screen
|
||||
|
||||
def draw_menu_text(self, x, y, line, selected) -> None:
|
||||
|
||||
if selected == True:
|
||||
View.draw.rectangle((5, y-5, 235, y+30), outline=0, fill="ORANGE")
|
||||
View.draw.text((x, y) , line, fill="BLACK", font=View.IMPACT20)
|
||||
else:
|
||||
View.draw.text((x, y) , line, fill="ORANGE", font=View.IMPACT20)
|
||||
|
||||
return
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,72 @@
|
||||
# SeedSigner file class dependencies
|
||||
from view import View
|
||||
from qr import QR
|
||||
from buttons import B
|
||||
|
||||
class SettingsToolsView(View):
|
||||
|
||||
def __init__(self, controller) -> None:
|
||||
View.__init__(self, controller)
|
||||
|
||||
self.qr = QR()
|
||||
self.donate_image = None
|
||||
|
||||
### Donate Menu Item
|
||||
|
||||
def display_donate_info_screen(self):
|
||||
self.draw_modal(["You can support", "SeedSigner by donating", "any amount of BTC", "Thank You!!!"], "", "(Press right for a QR code)")
|
||||
return True
|
||||
|
||||
def display_donate_qr(self):
|
||||
self.draw_modal(["Loading..."])
|
||||
self.donate_image = self.qr.qrimage("bc1q8u3dyltlg6pu56fe7m58aqz9cwrfld0s03zlrjl0wvm9x4nfa60q2l0g97")
|
||||
View.DispShowImage(self.donate_image)
|
||||
return True
|
||||
|
||||
### Display Network Selection
|
||||
|
||||
def display_current_network(self) -> str:
|
||||
r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Mainnet", "Testnet"], "Which Network?")
|
||||
if r == 2:
|
||||
return "main"
|
||||
elif r == 3:
|
||||
return "test"
|
||||
else:
|
||||
return "cancel"
|
||||
|
||||
### Display Wallet Selection
|
||||
|
||||
def display_wallet_selection(self) ->str:
|
||||
r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Specter Desktop"], "Which Wallet?")
|
||||
if r == 2:
|
||||
return "Specter Desktop"
|
||||
else:
|
||||
return "cancel"
|
||||
|
||||
###
|
||||
### Version Info
|
||||
###
|
||||
|
||||
def display_version_info(self):
|
||||
|
||||
line1 = "SeedSigner"
|
||||
line2 = "Version v" + self.controller.VERSION
|
||||
line3 = "built for use with"
|
||||
line4 = "Specter-desktop"
|
||||
line5 = "v1.1.0 or higher"
|
||||
line6 = "(Joystick RIGHT to EXIT)"
|
||||
|
||||
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
tw, th = View.draw.textsize(line1, font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 20), line1, fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(line2, font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 55), line2, fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(line3, font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 90), line3, fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(line4, font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 125), line4, fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(line5, font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 160), line5, fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(line6, font=View.IMPACT18)
|
||||
View.draw.text(((240 - tw) / 2, 210), line6, fill="ORANGE", font=View.IMPACT18)
|
||||
View.DispShowImage()
|
||||
@@ -0,0 +1,105 @@
|
||||
# Internal file class dependencies
|
||||
from view import View
|
||||
from qr import QR
|
||||
from buttons import Buttons, B
|
||||
|
||||
# External Dependencies
|
||||
from embit.bip39 import mnemonic_to_bytes
|
||||
from embit.bip39 import mnemonic_from_bytes
|
||||
from embit import script
|
||||
from embit import bip32
|
||||
from embit import bip39
|
||||
from embit.networks import NETWORKS
|
||||
from embit import psbt
|
||||
from embit import ec
|
||||
from binascii import unhexlify, hexlify, a2b_base64, b2a_base64
|
||||
import time
|
||||
|
||||
class SigningToolsView(View):
|
||||
|
||||
def __init__(self, controller, seed_storage) -> None:
|
||||
View.__init__(self, controller)
|
||||
self.seed_storage = seed_storage
|
||||
self.qr = QR()
|
||||
|
||||
###
|
||||
### XPub
|
||||
###
|
||||
|
||||
def display_xpub_qr(self, seed_phrase):
|
||||
self.draw_modal(["Generating QR ..."])
|
||||
|
||||
seed = bip39.mnemonic_to_seed(' '.join(seed_phrase).strip())
|
||||
|
||||
root = bip32.HDKey.from_seed(seed, version=NETWORKS[self.seed_storage.get_network()]["xprv"])
|
||||
fingerprint = root.child(0).fingerprint
|
||||
|
||||
# derive account according to bip84
|
||||
bip48_xprv = root.derive(self.seed_storage.get_hardened_derivation())
|
||||
|
||||
# corresponding master public key:
|
||||
bip48_xpub = bip48_xprv.to_public()
|
||||
|
||||
xpubstring = "[%s%s]%s" % (
|
||||
hexlify(fingerprint).decode('utf-8'),
|
||||
self.seed_storage.get_hardened_derivation()[1:],
|
||||
bip48_xpub.to_base58(NETWORKS[self.seed_storage.get_network()]["Zpub"]))
|
||||
|
||||
print(xpubstring)
|
||||
|
||||
xpub_image = self.qr.qrimage(xpubstring, 120)
|
||||
|
||||
View.DispShowImage(xpub_image)
|
||||
|
||||
###
|
||||
### Sign Transaction
|
||||
###
|
||||
|
||||
def display_signed_psbt_animated_qr(self, psbt) -> None:
|
||||
self.draw_modal(["Generating QR ..."])
|
||||
|
||||
print(psbt)
|
||||
images = self.qr.makeqrcodes(psbt, "Specter Desktop", 60, SigningToolsView.qr_gen_status)
|
||||
|
||||
cnt = 0
|
||||
while True:
|
||||
View.DispShowImage(images[cnt])
|
||||
cnt += 1
|
||||
if cnt >= len(images):
|
||||
cnt = 0
|
||||
time.sleep(0.2)
|
||||
if self.buttons.check_for_low(B.KEY_RIGHT):
|
||||
return
|
||||
|
||||
def display_transaction_information(self, signer) -> None:
|
||||
self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
tw, th = self.draw.textsize("Confirm last 13 chars", font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 5), "Confirm last 13 chars", fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = self.draw.textsize("of the receiving address:", font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 30), "of the receiving address:", fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = self.draw.textsize(signer.destinationaddress[-13:], font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 55), signer.destinationaddress[-13:], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = self.draw.textsize("Amount Sending:", font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 90), "Amount Sending:", fill="ORANGE", font=View.IMPACT22)
|
||||
if signer.spend == 0:
|
||||
tw, th = self.draw.textsize("Self-Transfer (not parsed)", font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 115), "Self-Transfer (not parsed)", fill="ORANGE", font=View.IMPACT22)
|
||||
else:
|
||||
tw, th = self.draw.textsize(str(signer.spend) + " satoshis", font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 115), str(signer.spend) + " satoshis", fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = self.draw.textsize("Plus a fee of:", font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 150), "Plus a fee of:", fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = self.draw.textsize(str(int(signer.fee)) + " satoshis", font=View.IMPACT22)
|
||||
self.draw.text(((240 - tw) / 2, 175), str(int(signer.fee)) + " satoshis", fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.IMPACT18)
|
||||
self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill="ORANGE", font=View.IMPACT18)
|
||||
View.DispShowImage()
|
||||
|
||||
def qr_gen_status(percentage):
|
||||
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
tw, th = View.draw.textsize("QR Generation", font=View.IMPACT25)
|
||||
View.draw.text(((240 - tw) / 2, 90), "QR Generation", fill="ORANGE", font=View.IMPACT25)
|
||||
tw, th = View.draw.textsize(str(round(percentage)) + "% Complete", font=View.IMPACT25)
|
||||
View.draw.text(((240 - tw) / 2, 125), str(round(percentage)) + "% Complete", fill="ORANGE", font=View.IMPACT25)
|
||||
View.DispShowImage()
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
# External Dependencies
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
from PIL import ImageFont
|
||||
import ST7789
|
||||
import spidev as SPI
|
||||
from multiprocessing import Queue
|
||||
|
||||
### Generic View Class to Instatiate Display
|
||||
### Static Class variables are used for display
|
||||
### Designed to be inherited for other view classes, but not required
|
||||
|
||||
class View:
|
||||
|
||||
WIDTH = 240
|
||||
HEIGHT = 240
|
||||
|
||||
# Define necessart fonts
|
||||
IMPACT16 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 16)
|
||||
IMPACT18 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 18)
|
||||
IMPACT20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 20)
|
||||
IMPACT21 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 21)
|
||||
IMPACT22 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 22)
|
||||
IMPACT23 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 23)
|
||||
IMPACT25 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 25)
|
||||
IMPACT26 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 26)
|
||||
IMPACT35 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 35)
|
||||
IMPACT50 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 50)
|
||||
COURIERNEW38 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 38)
|
||||
COURIERNEW30 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 30)
|
||||
|
||||
RST = 27
|
||||
DC = 25
|
||||
BL = 24
|
||||
|
||||
controller = None
|
||||
buttons = None
|
||||
canvas_width = 0
|
||||
canvas_height = 0
|
||||
canvas = None
|
||||
draw = None
|
||||
bus = 0
|
||||
device = 0
|
||||
disp = None
|
||||
|
||||
def __init__(self, controller) -> None:
|
||||
|
||||
# Global Singleton
|
||||
View.controller = controller
|
||||
View.buttons = View.controller.buttons
|
||||
|
||||
View.canvas_width = View.WIDTH
|
||||
View.canvas_height = View.HEIGHT
|
||||
View.canvas = Image.new('RGB', (View.canvas_width, View.canvas_height))
|
||||
View.draw = ImageDraw.Draw(View.canvas)
|
||||
|
||||
# 240x240 display with hardware SPI:
|
||||
View.bus = 0
|
||||
View.device = 0
|
||||
View.disp = ST7789.ST7789(SPI.SpiDev(View.bus, View.device),View.RST, View.DC, View.BL)
|
||||
View.disp.Init()
|
||||
|
||||
self.queue = Queue()
|
||||
|
||||
def DispShowImage(image = None):
|
||||
if image == None:
|
||||
image = View.canvas
|
||||
View.disp.ShowImage(image, 0, 0)
|
||||
|
||||
def draw_modal(self, lines = [], title = "", bottom = "") -> None:
|
||||
|
||||
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
|
||||
if len(title) > 0:
|
||||
tw, th = View.draw.textsize(title, font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 2), title, fill="ORANGE", font=View.IMPACT22)
|
||||
|
||||
if len(bottom) > 0:
|
||||
tw, th = View.draw.textsize(bottom, font=View.IMPACT18)
|
||||
View.draw.text(((240 - tw) / 2, 210), bottom, fill="ORANGE", font=View.IMPACT18)
|
||||
|
||||
if len(lines) == 1:
|
||||
tw, th = View.draw.textsize(lines[0], font=View.IMPACT26)
|
||||
View.draw.text(((240 - tw) / 2, 90), lines[0], fill="ORANGE", font=View.IMPACT26)
|
||||
elif len(lines) == 2:
|
||||
tw, th = View.draw.textsize(lines[0], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 90), lines[0], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 125), lines[1], fill="ORANGE", font=View.IMPACT22)
|
||||
elif len(lines) == 3:
|
||||
tw, th = View.draw.textsize(lines[0], font=View.IMPACT26)
|
||||
View.draw.text(((240 - tw) / 2, 55), lines[0], fill="ORANGE", font=View.IMPACT26)
|
||||
tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 90), lines[1], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[2], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 125), lines[2], fill="ORANGE", font=View.IMPACT22)
|
||||
elif len(lines) == 4:
|
||||
tw, th = View.draw.textsize(lines[0], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 55), lines[0], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 90), lines[1], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[2], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 125), lines[2], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[3], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 160), lines[3], fill="ORANGE", font=View.IMPACT22)
|
||||
|
||||
View.DispShowImage()
|
||||
|
||||
return
|
||||
|
||||
def draw_prompt_yes_no(self, lines = [], title = "", bottom = "") -> None:
|
||||
|
||||
self.draw_prompt_custom("", "Yes ", "No ", lines, title, bottom)
|
||||
return
|
||||
|
||||
def draw_prompt_custom(self, a_txt, b_txt, c_txt, lines = [], title = "", bottom = "") -> None:
|
||||
|
||||
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
|
||||
if len(title) > 0:
|
||||
tw, th = View.draw.textsize(title, font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 2), title, fill="ORANGE", font=View.IMPACT22)
|
||||
|
||||
if len(bottom) > 0:
|
||||
tw, th = View.draw.textsize(bottom, font=View.IMPACT18)
|
||||
View.draw.text(((240 - tw) / 2, 210), bottom, fill="ORANGE", font=View.IMPACT18)
|
||||
|
||||
if len(lines) == 1:
|
||||
tw, th = View.draw.textsize(lines[0], font=View.IMPACT26)
|
||||
View.draw.text(((240 - tw) / 2, 90), lines[0], fill="ORANGE", font=View.IMPACT26)
|
||||
elif len(lines) == 2:
|
||||
tw, th = View.draw.textsize(lines[0], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 90), lines[0], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 125), lines[1], fill="ORANGE", font=View.IMPACT22)
|
||||
elif len(lines) == 3:
|
||||
tw, th = View.draw.textsize(lines[0], font=View.IMPACT26)
|
||||
View.draw.text(((240 - tw) / 2, 20), lines[0], fill="ORANGE", font=View.IMPACT26)
|
||||
tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 90), lines[1], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[2], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 125), lines[2], fill="ORANGE", font=View.IMPACT22)
|
||||
elif len(lines) == 4:
|
||||
tw, th = View.draw.textsize(lines[0], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 20), lines[0], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 90), lines[1], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[2], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 125), lines[2], fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(lines[3], font=View.IMPACT22)
|
||||
View.draw.text(((240 - tw) / 2, 160), lines[3], fill="ORANGE", font=View.IMPACT22)
|
||||
|
||||
a_x_offset = 240 - View.IMPACT25.getsize(a_txt)[0]
|
||||
View.draw.text((a_x_offset, 39 + 0), a_txt, fill="ORANGE", font=View.IMPACT25)
|
||||
|
||||
b_x_offset = 240 - View.IMPACT25.getsize(b_txt)[0]
|
||||
View.draw.text((b_x_offset , 39 + 60), b_txt, fill="ORANGE", font=View.IMPACT25)
|
||||
|
||||
c_x_offset = 240 - View.IMPACT25.getsize(c_txt)[0]
|
||||
View.draw.text((c_x_offset , 39 + 120), c_txt, fill="ORANGE", font=View.IMPACT25)
|
||||
|
||||
View.DispShowImage()
|
||||
|
||||
return
|
||||
|
||||
###
|
||||
### Power Off Screen
|
||||
###
|
||||
|
||||
def display_power_off_screen(self):
|
||||
|
||||
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
|
||||
|
||||
line1 = "Powering Down..."
|
||||
line2 = "Please wait about"
|
||||
line3 = "30 seconds before"
|
||||
line4 = "disconnecting power."
|
||||
|
||||
tw, th = View.draw.textsize(line1, font=View.IMPACT22)
|
||||
View.draw.text(((240-tw)/2, 45), line1, fill="ORANGE", font=View.IMPACT22)
|
||||
tw, th = View.draw.textsize(line2, font=View.IMPACT20)
|
||||
View.draw.text(((240-tw)/2, 100), line2, fill="ORANGE", font=View.IMPACT20)
|
||||
tw, th = View.draw.textsize(line3, font=View.IMPACT20)
|
||||
View.draw.text(((240-tw)/2, 130), line3, fill="ORANGE", font=View.IMPACT20)
|
||||
tw, th = View.draw.textsize(line4, font=View.IMPACT20)
|
||||
View.draw.text(((240-tw)/2, 160), line4, fill="ORANGE", font=View.IMPACT20)
|
||||
View.DispShowImage()
|
||||
Reference in New Issue
Block a user