refactoring of seedsigner to a move MVC application model with a few additional minor enhancements

This commit is contained in:
Nick Klockenga
2021-05-06 14:31:06 -04:00
parent 4fc0bcd597
commit d950e21578
17 changed files with 2541 additions and 3199 deletions
+1
View File
@@ -0,0 +1 @@
.DS_Store
+417
View File
@@ -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
-1
View File
@@ -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."""
+82
View File
@@ -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
+80
View File
@@ -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
+29
View File
@@ -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
+331
View File
@@ -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
+49
View File
@@ -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
+10 -42
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+141
View File
@@ -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
+318
View File
@@ -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
+72
View File
@@ -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()
+105
View File
@@ -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
View File
@@ -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()