Merge pull request #161 from kornpow/0_4_6_emulator

More Emulator
This commit is contained in:
SeedSigner
2022-03-12 10:23:40 -06:00
committed by GitHub
12 changed files with 261 additions and 43 deletions
+12
View File
@@ -1,10 +1,22 @@
# Emulator Setup
## Create/Enter virtualenv
```
mkvirtualenv seedsigner-gui
```
## Pip installs
```
pip install -r requirements.txt
```
## Install Tkinter
```
sudo apt install python3-tk
```
## Run the emulator
```
cd seedsigner/src
NOTAPI=true python3 main.py
```
+9
View File
@@ -0,0 +1,9 @@
embit==0.4.10
numpy==1.21.1
Pillow==8.2.0
-e git+https://github.com/kdmukai/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar
qrcode==7.3.1
six==1.16.0
spidev==3.5
opencv-python~=4.5.5
urtypes @ git+https://github.com/jreesun/urtypes.git@e0d0db277ec2339650343eaf7b220fffb9233241
+2 -3
View File
@@ -1,12 +1,11 @@
embit==0.4.10
numpy==1.21.1
# need to not install this on x86...
#picamera==1.13
picamera==1.13
Pillow==8.2.0
-e git+https://github.com/kdmukai/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar
qrcode==7.3.1
#RPi.GPIO==0.7.1
RPi.GPIO==0.7.1
six==1.16.0
spidev==3.5
urtypes @ git+https://github.com/jreesun/urtypes.git@e0d0db277ec2339650343eaf7b220fffb9233241
#-e src/vendor/urtypes
+23 -17
View File
@@ -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
@@ -49,32 +51,36 @@ class Camera(Singleton):
def start_single_frame_mode(self, resolution=(720, 480)):
if self._video_stream is not None:
self.stop_video_stream_mode()
if self._picamera is not None:
self._picamera.close()
pass
# self._video_stream.should_stop = True
# if self._video_stream is not None:
# self.stop_video_stream_mode()
# if self._picamera is not None:
# self._picamera.close()
self._picamera = PiCamera(resolution=resolution, framerate=24)
self._picamera.start_preview()
# self._picamera = PiCamera(resolution=resolution, framerate=24)
# self._picamera.start_preview()
def capture_frame(self):
if self._picamera is None:
raise Exception("Must call start_single_frame_mode first.")
# if self._picamera is None:
# raise Exception("Must call start_single_frame_mode first.")
# Set auto-exposure values
self._picamera.shutter_speed = self._picamera.exposure_speed
self._picamera.exposure_mode = 'off'
g = self._picamera.awb_gains
self._picamera.awb_mode = 'off'
self._picamera.awb_gains = g
# self._picamera.shutter_speed = self._picamera.exposure_speed
# self._picamera.exposure_mode = 'off'
# g = self._picamera.awb_gains
# self._picamera.awb_mode = 'off'
# self._picamera.awb_gains = g
stream = io.BytesIO()
self._picamera.capture(stream, format='jpeg')
self._video_stream
# self._picamera.capture(stream, format='jpeg')
# "Rewind" the stream to the beginning so we can read its content
stream.seek(0)
return Image.open(stream).rotate(90 + self._camera_rotation)
# stream.seek(0)
# return Image.open(stream)
return Image.fromarray(self._video_stream.frame.astype('uint8'), 'BGR')
def stop_single_frame_mode(self):
+160 -5
View File
@@ -5,11 +5,14 @@ from multiprocessing import Process, Queue
from subprocess import call
import os, sys
from embit import bip32, script, ec
import traceback
from embit.networks import NETWORKS
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,
@@ -97,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)
@@ -107,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:
@@ -135,6 +138,7 @@ class Controller(Singleton):
break
else:
print('Caught this error: ' + repr(error)) # debug
print(traceback.format_exc())
self.menu_view.draw_modal(["Crashed ..."], "", "restarting")
time.sleep(5)
@@ -194,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")
@@ -553,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])
@@ -590,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
@@ -1000,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)
+2
View File
@@ -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
+32 -11
View File
@@ -5,18 +5,27 @@ if not os.getenv("NOTAPI", False):
from picamera import PiCamera
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:
def __init__(self, resolution=(320, 240), framerate=32, format="bgr", **kwargs):
# initialize the camera
self.camera = PiCamera(resolution=resolution, framerate=framerate, **kwargs)
# self.camera = PiCamera(resolution=resolution, framerate=framerate, **kwargs)
# initialize the stream
self.rawCapture = PiRGBArray(self.camera, size=resolution)
self.stream = self.camera.capture_continuous(self.rawCapture,
format=format, use_video_port=True)
# self.rawCapture = PiRGBArray(self.camera, size=resolution)
# self.stream = self.camera.capture_continuous(self.rawCapture,
# format=format, use_video_port=True)
self.cap = cv2.VideoCapture(0)
self.cap.set(3, 320)
self.cap.set(4, 240)
# img1 = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# initialize the frame and the variable used to indicate
# if the thread should be stopped
@@ -34,23 +43,35 @@ class PiVideoStream:
def update(self):
# keep looping infinitely until the thread is stopped
for f in self.stream:
# 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
self.rawCapture.truncate(0)
# self.frame = f.array
# self.rawCapture.truncate(0)
# if the thread indicator variable is set, stop the thread
# and resource camera resources
if self.should_stop:
print("PiVideoStream: closing everything")
self.stream.close()
self.rawCapture.close()
self.camera.close()
# self.stream.close()
# self.rawCapture.close()
# self.camera.close()
self.cap.release()
self.should_stop = False
self.is_stopped = True
return
def get_frame(self):
ret, frame = self.cap.read()
self.frame = frame
return frame
def read(self):
# return the frame most recently read
return self.frame
+2 -2
View File
@@ -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"
+4 -2
View File
@@ -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
@@ -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:
+2 -2
View File
@@ -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
###
+1 -1
View File
@@ -6,7 +6,7 @@ persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = 555555
camera_rotation = 0
camera_rotation = -90
[wallet]
network = main