diff --git a/requirements-emulator.txt b/requirements-emulator.txt index 53f68d55..11b40e38 100644 --- a/requirements-emulator.txt +++ b/requirements-emulator.txt @@ -5,5 +5,5 @@ Pillow==8.2.0 qrcode==7.3.1 six==1.16.0 spidev==3.5 -opencv-python==4.5.5 +opencv-python~=4.5.5 urtypes @ git+https://github.com/jreesun/urtypes.git@e0d0db277ec2339650343eaf7b220fffb9233241 diff --git a/src/seedsigner/camera.py b/src/seedsigner/camera.py index 82780bdc..8fe08d99 100644 --- a/src/seedsigner/camera.py +++ b/src/seedsigner/camera.py @@ -1,7 +1,9 @@ +import os import io import numpy -from picamera import PiCamera +if not os.getenv("NOTAPI", False): + from picamera import PiCamera from PIL import Image from seedsigner.helpers import PiVideoStream, Singleton @@ -78,7 +80,7 @@ class Camera(Singleton): # "Rewind" the stream to the beginning so we can read its content # stream.seek(0) # return Image.open(stream) - return Image.fromarray(self._video_stream.frame.astype('uint8'), 'RGB').rotate(90) + return Image.fromarray(self._video_stream.frame.astype('uint8'), 'BGR') def stop_single_frame_mode(self): diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index dc21082c..4fc6df66 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -11,6 +11,8 @@ from embit.descriptor import Descriptor from binascii import hexlify from threading import Thread from queue import Queue +from datetime import datetime +from pathlib import Path as Pathlib # Internal file class dependencies from .views import (View, MenuView, SeedToolsView,SigningToolsView, @@ -98,8 +100,8 @@ class Controller(Singleton): # Views controller.menu_view = MenuView(controller.disp, controller.q) controller.seed_tools_view = SeedToolsView(controller.disp, controller.q) - # controller.io_test_view = IOTestView() - # controller.signing_tools_view = SigningToolsView(controller.storage) + controller.io_test_view = IOTestView(controller.disp, controller.q) + controller.signing_tools_view = SigningToolsView(controller.storage, controller.disp, controller.q) controller.settings_tools_view = SettingsToolsView(controller.disp, controller.q) controller.screensaver = ScreensaverView(controller.buttons, controller.disp, controller.q) @@ -108,9 +110,9 @@ class Controller(Singleton): @property def camera(self): - if not os.getenv("NOTAPI", False): - from .camera import Camera - return Camera.get_instance() + # if not os.getenv("NOTAPI", False): + from .camera import Camera + return Camera.get_instance() def start(self) -> None: @@ -196,6 +198,9 @@ class Controller(Singleton): ret_val = self.show_reset_tool() elif ret_val == Path.POWER_OFF: ret_val = self.show_power_off() + elif ret_val == Path.PSBT_SIGN_SUB_MENU: + ret_val = self.sign_psbt() + raise Exception("Unhandled case") @@ -555,6 +560,14 @@ class Controller(Singleton): xpub = xprv.to_public() xpub_base58 = xpub.to_string(version=version) + with open(f"/home/skorn/Desktop/xpub-{fingerprint}.txt", "w") as f: + f.write(xpub_base58) + f.write("\n") + f.write(derivation) + f.write("\n") + f.write(fingerprint) + f.write("\n") + self.signing_tools_view.display_xpub_info(fingerprint, derivation, xpub_base58) self.buttons.wait_for([B.KEY_RIGHT]) @@ -592,11 +605,146 @@ class Controller(Singleton): return Path.MAIN_MENU ### Sign Transactions + def sign_psbt(self): + seed = Seed(wordlist=self.settings.wordlist) + used_saved_seed = False + + base_path = Pathlib("/home/skorn/Desktop/psbt/") + psbt_names = [p.name for p in list(base_path.rglob("*.psbt")) ] + + psbt_names.append("... [ Return to Main ]") + + r = self.menu_view.display_generic_selection_menu(psbt_names, "Select PSBT?") + print(f"got selected: {r}") + exit_index = len(psbt_names) + if r == exit_index: + return Path.MAIN_MENU + else: + psbt_filename = psbt_names[r-1] + with open(base_path.joinpath(psbt_filename),'r') as f: + psbtdata = f.read() + + decoder = DecodeQR(wordlist=self.settings.wordlist) + decoder.qr_type = QRType.PSBTBASE64 + decoder.add_data(psbtdata) + + self.menu_view.draw_modal(["Validating PSBT"]) + psbt = decoder.getPSBT() + + # show transaction information before sign + self.menu_view.draw_modal(["Parsing PSBT"]) + p = PSBTParser(psbt,seed,self.settings.network) + self.signing_tools_view.display_transaction_information(p) + + input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False) + if input == B.KEY_LEFT: + return Path.MAIN_MENU + + # validate single sig using seed + + # No valid seed yet, If there is a saved seed, ask to use saved seed + if self.storage.num_of_saved_seeds() > 0: + r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Saved Seed?") + if r == 1: #Yes + slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None) + if slot_num == 0: + return Path.MAIN_MENU + seed = self.storage.get_seed(slot_num) + used_saved_seed = True + + if not seed: + # no valid seed yet, gather seed phrase + # display menu to select 12 or 24 word seed for last word + ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Cancel ]") + if ret_val == Path.SEED_WORD_12: + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12) + elif ret_val == Path.SEED_WORD_24: + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24) + elif ret_val == Path.SEED_WORD_QR: + seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr() + else: + return Path.MAIN_MENU + + if not seed: + return Path.MAIN_MENU + + # check if seed phrase is valid + self.menu_view.draw_modal(["Validating Seed ..."]) + if not seed: + 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 + + if len(seed.passphrase) == 0: + r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?") + if r == 1: + # display a tool to pick letters/numbers to make a passphrase + seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry() + if len(seed.passphrase) == 0: + self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue") + input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT]) + if input == B.KEY_LEFT: + return Path.MAIN_MENU + else: + self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue") + self.buttons.wait_for([B.KEY_RIGHT]) + + # display seed phrase + while True: + r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue") + if r == True: + break + else: + # Cancel + return Path.MAIN_MENU + + # Ask to save seed + if self.storage.slot_avaliable() and used_saved_seed == False: + r = self.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.add_seed(seed, 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]) + + + self.menu_view.draw_modal(["Parsing PSBT"]) + p = PSBTParser(psbt, seed, self.settings.network) + self.signing_tools_view.display_transaction_information(p) + + + # Sign PSBT + self.menu_view.draw_modal(["PSBT Signing ..."]) + sig_cnt = PSBTParser.sigCount(psbt) + + + psbt.sign_with(p.root) + trimmed_psbt = PSBTParser.trim(psbt) + + if sig_cnt == PSBTParser.sigCount(trimmed_psbt): + self.menu_view.draw_modal(["Signing failed", "left to exit", "or right to continue", "to display PSBT QR"], "", "") + input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False) + if input == B.KEY_LEFT: + return Path.MAIN_MENU + + # TODO: handle multisig case. Increment filename smarter + signed_filename = f"{psbt_filename}.signed" + + with open(base_path.joinpath(signed_filename), "wb") as f: + f.write(trimmed_psbt.serialize()) + + self.menu_view.draw_modal(["PSBT Signed", f"Saved to Filename: {signed_filename}"], "", "Right to Continue") + input = self.buttons.wait_for([B.KEY_RIGHT]) + def show_sign_transaction(self): seed = Seed(wordlist=self.settings.wordlist) used_saved_seed = False + decoder = DecodeQR(wordlist=self.settings.wordlist) + decoder.qr_type = QRType.PSBTBASE64 + # reusable qr scan function def scan_qr(scan_text="Scan QR"): # Scan QR using Camera @@ -1002,6 +1150,11 @@ class Controller(Singleton): if input == B.KEY_LEFT: return Path.MAIN_MENU + + with open(f"/home/skorn/Desktop/txn-{datetime.utcnow().replace(microsecond=0).isoformat()}-signed.psbt", "wb") as f: + f.write(trimmed_psbt.serialize()) + + # Display Animated QR Code self.menu_view.draw_modal(["Generating PSBT QR ..."]) e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) diff --git a/src/seedsigner/helpers/path.py b/src/seedsigner/helpers/path.py index b333518e..9fd4b88f 100644 --- a/src/seedsigner/helpers/path.py +++ b/src/seedsigner/helpers/path.py @@ -7,11 +7,13 @@ class Path: SEED_TOOLS_SUB_MENU = 10 SIGNING_TOOLS_SUB_MENU = 11 SETTINGS_SUB_MENU = 12 + PSBT_SIGN_SUB_MENU = 13 SEED_WORD_12 = 20 SEED_WORD_24 = 21 POWER_OFF = 22 SEED_WORD_QR = 23 + # Tools / Functions Routes GEN_LAST_WORD = 40 DICE_GEN_SEED = 41 diff --git a/src/seedsigner/helpers/pivideostream.py b/src/seedsigner/helpers/pivideostream.py index 7afa5332..4178389f 100644 --- a/src/seedsigner/helpers/pivideostream.py +++ b/src/seedsigner/helpers/pivideostream.py @@ -7,6 +7,7 @@ from threading import Thread import time import cv2 import numpy as np +from time import sleep # Modified from: https://github.com/jrosebr1/imutils class PiVideoStream: @@ -45,7 +46,10 @@ class PiVideoStream: # for f in self.stream: while True: ret, frame = self.cap.read() + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) self.frame = frame + + sleep(0.25) # grab the frame from the stream and clear the stream in # preparation for the next frame # self.frame = f.array diff --git a/src/seedsigner/views/io_test_view.py b/src/seedsigner/views/io_test_view.py index ca3a6f3a..18458218 100644 --- a/src/seedsigner/views/io_test_view.py +++ b/src/seedsigner/views/io_test_view.py @@ -10,8 +10,8 @@ from seedsigner.helpers import B class IOTestView(View): - def __init__(self) -> None: - View.__init__(self) + def __init__(self, disp, q) -> None: + View.__init__(self, disp, q) self.redraw = False self.redraw_complete = False self.qr_text = "Scan ANY QR Code" diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index cf86c71e..55c1faf2 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -25,7 +25,7 @@ class MenuView(View): def display_main_menu(self, sub_menu = None) -> int: ret_val = 0 input = 0 - lines = ["Seed Tools", "Scan QR", "Settings", "Power Off"] + lines = ["Seed Tools", "Scan QR", "Sign PSBT", "Settings", "Power Off"] if sub_menu == Path.SEED_TOOLS_SUB_MENU: return self.display_seed_tools_menu() @@ -53,8 +53,10 @@ class MenuView(View): elif self.selected_menu_num == 2: ret_val = Path.SIGN_TRANSACTION elif self.selected_menu_num == 3: - ret_val = self.display_settings_menu() + ret_val = Path.PSBT_SIGN_SUB_MENU elif self.selected_menu_num == 4: + ret_val = self.display_settings_menu() + elif self.selected_menu_num == 5: ret_val = Path.POWER_OFF if ret_val != Path.MAIN_MENU: # When no main menu, return to controller diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index d9e12733..befa7d5b 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -50,6 +50,18 @@ class SettingsToolsView(View): else: return None + + def display_saved_psbts(self) -> str: + r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Low", "Medium", "High"], "Which QR Density?") + if r == 2: + return EncodeQRDensity.LOW + elif r == 3: + return EncodeQRDensity.MEDIUM + elif r == 4: + return EncodeQRDensity.HIGH + else: + return None + ### Display QR Density Selection def display_qr_density_selection(self) -> str: diff --git a/src/seedsigner/views/signing_tools_view.py b/src/seedsigner/views/signing_tools_view.py index 854aa8fe..383a2e92 100644 --- a/src/seedsigner/views/signing_tools_view.py +++ b/src/seedsigner/views/signing_tools_view.py @@ -8,8 +8,8 @@ import time class SigningToolsView(View): - def __init__(self, seed_storage) -> None: - View.__init__(self) + def __init__(self, seed_storage, disp, q) -> None: + View.__init__(self, disp, q) self.seed_storage = seed_storage ### diff --git a/src/settings.ini b/src/settings.ini index 6d22247e..dec6af40 100644 --- a/src/settings.ini +++ b/src/settings.ini @@ -6,7 +6,7 @@ persistent_settings = False [display] text_color = ORANGE qr_background_color = 555555 -camera_rotation = 0 +camera_rotation = -90 [wallet] network = main