mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge branch 'SeedSigner:dev' into Swap-Photo-in-main-Readme
This commit is contained in:
@@ -82,27 +82,27 @@ Notes:
|
||||
## A Special Note On Minimizing Trust
|
||||
As is the nature of pre-packaged software downloads, downloading and using the prepared SeedSigner release images means implicitly placing trust in the individual preparing those images; in our project the release images are prepared and signed by the eponymous creator of the project, SeedSigner "the person". That individual is additionally the only person in possession of the PGP keys that are used to sign the release images.
|
||||
|
||||
However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made availabile. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md).
|
||||
However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made available. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md).
|
||||
|
||||
## Downloading the Software
|
||||
|
||||
|
||||
Download the current Version (0.6.0) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board.
|
||||
Download the current Version (0.7.0) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board.
|
||||
| Board | Download Image Link/Name |
|
||||
| --------------------- | --------------------------------- |
|
||||
|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.6.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi0.img) |
|
||||
|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.6.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi0.img) |
|
||||
|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.6.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi02w.img) |
|
||||
|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.6.0.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi2.img) |
|
||||
|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.6.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi02w.img) |
|
||||
|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.6.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi4.img) |
|
||||
|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.6.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi4.img) |
|
||||
|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.7.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi0.img) |
|
||||
|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.7.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi0.img) |
|
||||
|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.7.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi02w.img) |
|
||||
|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.7.0.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi2.img) |
|
||||
|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.7.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi02w.img) |
|
||||
|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.7.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi4.img) |
|
||||
|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.7.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner_os.0.7.0.pi4.img) |
|
||||
|
||||
Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original(un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware.
|
||||
|
||||
**also download** these 2 signature verification files to the same folder
|
||||
[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner.0.6.0.sha256)
|
||||
[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner.0.6.0.sha256.sig)
|
||||
[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.sha256.txt)
|
||||
[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.7.0/seedsigner.0.7.0.sha256.txt.sig)
|
||||
|
||||
Users familiar with older versions of the SeedSigner software might be surprised with how fast their software downloads now are, because since version 0.6.0 the software image files are now 100x smaller! Each image file is now under 42 Megabytes so your downloads and verifications will be very quick now (and might even seem *too* quick)!
|
||||
|
||||
@@ -131,7 +131,7 @@ The result should confirm that 1 key was *either* imported or updated. *Ignore*
|
||||
|
||||
Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier. The `*`'s in this command will auto-fill the version from your current folder, so it should be copied and pasted as-is.)
|
||||
```
|
||||
gpg --verify seedsigner.0.6.*.sha256.sig
|
||||
gpg --verify seedsigner.0.7.*.sha256.txt.sig
|
||||
```
|
||||
|
||||
When the verify command completes successfully, it should display output like this:
|
||||
@@ -201,21 +201,21 @@ Now that you have confirmed that you do have the real SeedSigner Project's Publi
|
||||
|
||||
**On Linux or OSX:** Run this command
|
||||
```
|
||||
shasum -a 256 --ignore-missing --check seedsigner.0.6.*.sha256
|
||||
shasum -a 256 --ignore-missing --check seedsigner.0.7.*.sha256.txt
|
||||
```
|
||||
|
||||
**On Windows (inside Powershell):** Run this command
|
||||
```
|
||||
CertUtil -hashfile seedsigner_os.0.6.0.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256
|
||||
CertUtil -hashfile seedsigner_os.0.7.0.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256
|
||||
```
|
||||
On Windows, you must then manually compare the resulting file hash value to the corresponding hash value shown inside the .SHA256 cleartext file.
|
||||
<BR>
|
||||
|
||||
Wait up to 30 seconds for the command to complete, and it should display:
|
||||
```
|
||||
seedsigner_os.0.6.x.[Your_Pi_Model_For_Example:pi02w].img: OK
|
||||
seedsigner_os.0.7.x.[Your_Pi_Model_For_Example:pi02w].img: OK
|
||||
```
|
||||
**If you receive the "OK" message** for your **seedsigner_os.0.6.x.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete!
|
||||
**If you receive the "OK" message** for your **seedsigner_os.0.7.x.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete!
|
||||
**All of your downloaded files have now been confirmed as both authentic and unaltered!** You can proceed to create/write your MicroSD card😄😄 !!
|
||||
|
||||
If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above.
|
||||
|
||||
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
||||
|
||||
setuptools.setup(
|
||||
name="seedsigner",
|
||||
version="0.6.0",
|
||||
version="0.7.0",
|
||||
author="SeedSigner",
|
||||
author_email="author@example.com",
|
||||
description="Build an offline, airgapped Bitcoin signing device for less than $50!",
|
||||
|
||||
@@ -130,6 +130,7 @@ class Controller(Singleton):
|
||||
|
||||
back_stack: BackStack = None
|
||||
screensaver: 'ScreensaverScreen' = None
|
||||
toast_notification_thread: 'BaseToastOverlayManagerThread' = None
|
||||
|
||||
|
||||
@classmethod
|
||||
@@ -181,7 +182,7 @@ class Controller(Singleton):
|
||||
controller.back_stack = BackStack()
|
||||
|
||||
# Other behavior constants
|
||||
controller.screensaver_activation_ms = 120 * 1000
|
||||
controller.screensaver_activation_ms = 2 * 60 * 1000 # two minutes
|
||||
|
||||
background_import_thread = BackgroundImportThread()
|
||||
background_import_thread.start()
|
||||
@@ -240,8 +241,9 @@ class Controller(Singleton):
|
||||
* initial_destination: The first View to run. If None, the MainMenuView is
|
||||
used. Only used by the test suite.
|
||||
"""
|
||||
from .views import MainMenuView, BackStackView
|
||||
from .views.screensaver import OpeningSplashScreen
|
||||
from seedsigner.views import MainMenuView, BackStackView
|
||||
from seedsigner.views.screensaver import OpeningSplashScreen
|
||||
from seedsigner.gui.toast import RemoveSDCardToastManagerThread
|
||||
|
||||
OpeningSplashScreen().start()
|
||||
|
||||
@@ -275,6 +277,10 @@ class Controller(Singleton):
|
||||
next_destination = initial_destination
|
||||
else:
|
||||
next_destination = Destination(MainMenuView)
|
||||
|
||||
# Set up our one-time toast notification tip to remove the SD card
|
||||
self.activate_toast(RemoveSDCardToastManagerThread())
|
||||
|
||||
while True:
|
||||
# Destination(None) is a special case; render the Home screen
|
||||
if next_destination.View_cls is None:
|
||||
@@ -354,6 +360,9 @@ class Controller(Singleton):
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
if self.is_screensaver_running:
|
||||
self.screensaver.stop()
|
||||
|
||||
if self.toast_notification_thread and self.toast_notification_thread.is_alive():
|
||||
self.toast_notification_thread.stop()
|
||||
|
||||
# Clear the screen when exiting
|
||||
print("Clearing screen, exiting")
|
||||
@@ -366,12 +375,43 @@ class Controller(Singleton):
|
||||
|
||||
|
||||
def start_screensaver(self):
|
||||
# If a toast is running, tell it to give up the Renderer.lock; it will then
|
||||
# block until the screensaver is done, at which point the toast can re-acquire
|
||||
# the Renderer.lock and resume where it left off.
|
||||
if self.toast_notification_thread and self.toast_notification_thread.is_alive():
|
||||
print(f"Controller: settings toggle_render_lock for {self.toast_notification_thread.__class__.__name__}")
|
||||
self.toast_notification_thread.toggle_renderer_lock()
|
||||
|
||||
print("Controller: Starting screensaver")
|
||||
if not self.screensaver:
|
||||
# Do a lazy/late import and instantiation to reduce Controller initial startup time
|
||||
from seedsigner.views.screensaver import ScreensaverScreen
|
||||
from seedsigner.hardware.buttons import HardwareButtons
|
||||
self.screensaver = ScreensaverScreen(HardwareButtons.get_instance())
|
||||
|
||||
# Start the screensaver, but it will block until it can acquire the Renderer.lock.
|
||||
self.screensaver.start()
|
||||
print("Controller: Screensaver started")
|
||||
|
||||
|
||||
def activate_toast(self, toast_manager_thread: 'BaseToastOverlayManagerThread'):
|
||||
"""
|
||||
Ensures that the Controller has explicit control over which processes get to
|
||||
claim the Renderer.lock and which need to (potentially) release it.
|
||||
"""
|
||||
if self.is_screensaver_running:
|
||||
# New toast notifications break out of the Screensaver
|
||||
print("Controller: stopping screensaver")
|
||||
self.screensaver.stop()
|
||||
|
||||
if self.toast_notification_thread and self.toast_notification_thread.is_alive():
|
||||
# Can only run one toast at a time
|
||||
print(f"Controller: stopping {self.toast_notification_thread.__class__.__name__}")
|
||||
self.toast_notification_thread.stop()
|
||||
|
||||
self.toast_notification_thread = toast_manager_thread
|
||||
print(f"Controller: starting {self.toast_notification_thread.__class__.__name__}")
|
||||
self.toast_notification_thread.start()
|
||||
|
||||
|
||||
def handle_exception(self, e) -> Destination:
|
||||
|
||||
@@ -35,6 +35,7 @@ class GUIConstants:
|
||||
ICON_FONT_SIZE = 22
|
||||
ICON_INLINE_FONT_SIZE = 24
|
||||
ICON_LARGE_BUTTON_SIZE = 48
|
||||
ICON_TOAST_FONT_SIZE = 30
|
||||
ICON_PRIMARY_SCREEN_SIZE = 50
|
||||
|
||||
TOP_NAV_TITLE_FONT_NAME = "OpenSans-SemiBold"
|
||||
@@ -138,7 +139,6 @@ class SeedSignerIconConstants:
|
||||
BRIGHTNESS = "\ue91d"
|
||||
MICROSD = "\ue91e"
|
||||
QRCODE = "\ue91f"
|
||||
SDCARD = "\ue920"
|
||||
|
||||
MIN_VALUE = SCAN
|
||||
MAX_VALUE = QRCODE
|
||||
@@ -287,6 +287,7 @@ class TextArea(BaseComponent):
|
||||
supersampling_factor: int = 1
|
||||
auto_line_break: bool = True
|
||||
allow_text_overflow: bool = False
|
||||
height_ignores_below_baseline: bool = False # If True, characters that render below the baseline (e.g. "pqgy") will not affect the final height calculation
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -316,11 +317,9 @@ class TextArea(BaseComponent):
|
||||
font = Fonts.get_font(self.font_name, self.font_size)
|
||||
|
||||
# Note: from the baseline anchor, `top` is a negative number while `bottom`
|
||||
# conveys the pixels used below the baseline (e.g. in "py").
|
||||
# For consistency, ensure we have a full-height character above baseline.
|
||||
# Also include some "below baseline" chars.
|
||||
measurement_chars = "Agjpqy"
|
||||
(left, top, right, bottom) = font.getbbox(self.text + measurement_chars, anchor="ls")
|
||||
# conveys the height of the pixels that rendered below the baseline, if any
|
||||
# (e.g. "py" in "python").
|
||||
(left, top, right, bottom) = font.getbbox(self.text, anchor="ls")
|
||||
self.text_height_above_baseline = -1 * top
|
||||
self.text_height_below_baseline = bottom
|
||||
|
||||
@@ -333,12 +332,14 @@ class TextArea(BaseComponent):
|
||||
|
||||
# Calculate the actual height
|
||||
if len(self.text_lines) == 1:
|
||||
total_text_height = self.text_height_above_baseline + self.text_height_below_baseline
|
||||
total_text_height = self.text_height_above_baseline
|
||||
if not self.height_ignores_below_baseline:
|
||||
total_text_height += self.text_height_below_baseline
|
||||
else:
|
||||
# Multiply for the number of lines plus the spacer
|
||||
total_text_height = self.text_height_above_baseline * len(self.text_lines) + self.line_spacing * (len(self.text_lines) - 1)
|
||||
|
||||
if re.findall(f"[gjpqy]", self.text_lines[-1]["text"]):
|
||||
if not self.height_ignores_below_baseline and re.findall(f"[gjpqy]", self.text_lines[-1]["text"]):
|
||||
# Last line has at least one char that dips below baseline
|
||||
total_text_height += self.text_height_below_baseline
|
||||
|
||||
@@ -356,40 +357,37 @@ class TextArea(BaseComponent):
|
||||
|
||||
else:
|
||||
# Vertically center the text's starting point
|
||||
if len(self.text_lines) == 1:
|
||||
# For consistency when used in TopNav and elsewhere, ignore the
|
||||
# text's pixels below the baseline.
|
||||
# In other words: "Home" and "Something" will get the same text_y,
|
||||
# even though the "g" dips below baseline.
|
||||
self.text_y += int(self.height - (total_text_height - self.text_height_below_baseline))/2
|
||||
else:
|
||||
# Vertically center for the full height.
|
||||
self.text_y += int(self.height - (total_text_height))/2
|
||||
self.text_y += int(self.height - total_text_height)/2
|
||||
|
||||
|
||||
def render(self):
|
||||
# Render to a temp img scaled up by self.supersampling_factor, then resize down
|
||||
# with bicubic resampling.
|
||||
# TODO: Store resulting super-sampled image as a member var in __post_init__ and
|
||||
# just re-paste it here.
|
||||
# Add a `resample_padding` above and below when supersampling to avoid edge
|
||||
# effects (resized text that's right up against the top/bottom gets slightly
|
||||
# dimmer at the edge otherwise).
|
||||
if self.font_size < 20 and (not self.supersampling_factor or self.supersampling_factor == 1):
|
||||
self.supersampling_factor = 2
|
||||
|
||||
actual_text_height = self.height
|
||||
if self.height_ignores_below_baseline:
|
||||
# Even though we're ignoring the pixels below the baseline for spacing
|
||||
# purposes, we have to make sure we don't crop those pixels out during the
|
||||
# supersampling operations here.
|
||||
actual_text_height += self.text_height_below_baseline
|
||||
|
||||
resample_padding = 10 if self.supersampling_factor > 1.0 else 0
|
||||
img = Image.new(
|
||||
"RGBA",
|
||||
(
|
||||
self.width * self.supersampling_factor,
|
||||
(self.height + 2*resample_padding) * self.supersampling_factor
|
||||
(actual_text_height + 2*resample_padding) * self.supersampling_factor
|
||||
),
|
||||
self.background_color
|
||||
)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# draw.line((0, resample_padding * self.supersampling_factor, self.width * self.supersampling_factor, resample_padding * self.supersampling_factor), fill="blue", width=1)
|
||||
# draw.line((0, (resample_padding + self.height) * self.supersampling_factor, self.width * self.supersampling_factor, (resample_padding + self.height) * self.supersampling_factor), fill="red", width=1)
|
||||
cur_y = (self.text_y + resample_padding) * self.supersampling_factor
|
||||
|
||||
supersampled_font = Fonts.get_font(self.font_name, int(self.supersampling_factor * self.font_size))
|
||||
|
||||
if self.is_text_centered:
|
||||
@@ -409,13 +407,20 @@ class TextArea(BaseComponent):
|
||||
text_x = self.min_text_x + int(line["text_width"]/2)
|
||||
|
||||
draw.text((text_x * self.supersampling_factor, cur_y), line["text"], fill=self.font_color, font=supersampled_font, anchor=anchor)
|
||||
|
||||
# Debugging: show the exact vertical extents of each line of text
|
||||
# draw.line((0, cur_y - self.text_height_above_baseline * self.supersampling_factor, self.width * self.supersampling_factor, cur_y - self.text_height_above_baseline * self.supersampling_factor), fill="red", width=int(self.supersampling_factor))
|
||||
# draw.line((0, cur_y, self.width * self.supersampling_factor, cur_y), fill="red", width=int(self.supersampling_factor))
|
||||
|
||||
cur_y += (self.text_height_above_baseline + self.line_spacing) * self.supersampling_factor
|
||||
|
||||
# Crop off the top_padding and resize the result down to onscreen size
|
||||
if self.supersampling_factor > 1.0:
|
||||
resized = img.resize((self.width, self.height + 2*resample_padding), Image.LANCZOS)
|
||||
resized = img.resize((self.width, actual_text_height + 2*resample_padding), Image.LANCZOS)
|
||||
sharpened = resized.filter(ImageFilter.SHARPEN)
|
||||
img = sharpened.crop((0, resample_padding, self.width, self.height + 2*resample_padding))
|
||||
|
||||
# Crop args are actually (left, top, WIDTH, HEIGHT)
|
||||
img = sharpened.crop((0, resample_padding, self.width, actual_text_height + resample_padding))
|
||||
self.canvas.paste(img, (self.screen_x, self.screen_y))
|
||||
|
||||
|
||||
@@ -567,76 +572,6 @@ class IconTextLine(BaseComponent):
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToastOverlay(BaseComponent):
|
||||
icon_name: str = None
|
||||
color: str = None
|
||||
label_text: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
self.icon = Icon(
|
||||
image_draw=self.image_draw,
|
||||
canvas=self.canvas,
|
||||
screen_x=20,
|
||||
screen_y=190,
|
||||
icon_name=self.icon_name,
|
||||
icon_size=30,
|
||||
icon_color=self.color
|
||||
)
|
||||
|
||||
self.label = TextArea(
|
||||
image_draw=self.image_draw,
|
||||
canvas=self.canvas,
|
||||
text=self.label_text,
|
||||
font_size=19,
|
||||
font_color=self.color,
|
||||
edge_padding=0,
|
||||
is_text_centered=False,
|
||||
auto_line_break=False,
|
||||
width=160,
|
||||
height=20,
|
||||
screen_x=55,
|
||||
screen_y=195,
|
||||
allow_text_overflow=False
|
||||
)
|
||||
|
||||
def render(self):
|
||||
import time
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.hardware.buttons import HardwareButtons
|
||||
|
||||
self.controller: Controller = Controller.get_instance()
|
||||
self.current_screen = self.renderer.canvas.copy()
|
||||
buttons = HardwareButtons.get_instance()
|
||||
|
||||
# Special case when screensaver is running
|
||||
if self.controller.is_screensaver_running:
|
||||
buttons.override_ind = True
|
||||
|
||||
self.image_draw.rounded_rectangle(
|
||||
( GUIConstants.EDGE_PADDING + 2, self.canvas_height - 60, self.canvas_width - GUIConstants.EDGE_PADDING - 2, self.canvas_width - GUIConstants.EDGE_PADDING - 2),
|
||||
fill=GUIConstants.BACKGROUND_COLOR,
|
||||
radius=8,
|
||||
outline=self.color,
|
||||
width=2,
|
||||
)
|
||||
|
||||
self.icon.render()
|
||||
self.label.render()
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
t_end = time.time() + 3
|
||||
while time.time() < t_end:
|
||||
if buttons.has_any_input():
|
||||
break
|
||||
|
||||
self.renderer.show_image(self.current_screen)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class FormattedAddress(BaseComponent):
|
||||
"""
|
||||
@@ -1283,7 +1218,7 @@ class TopNav(BaseComponent):
|
||||
icon_name=SeedSignerIconConstants.BACK,
|
||||
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=GUIConstants.EDGE_PADDING,
|
||||
screen_y=GUIConstants.EDGE_PADDING - 1, # Text can't perfectly vertically center relative to the button; shifting it down 1px looks better.
|
||||
width=GUIConstants.TOP_NAV_BUTTON_SIZE,
|
||||
height=GUIConstants.TOP_NAV_BUTTON_SIZE,
|
||||
)
|
||||
@@ -1327,6 +1262,7 @@ class TopNav(BaseComponent):
|
||||
is_text_centered=True,
|
||||
font_name=self.font_name,
|
||||
font_size=self.font_size,
|
||||
height_ignores_below_baseline=True, # Consistently vertically center text, ignoring chars that render below baseline (e.g. "pqyj")
|
||||
)
|
||||
|
||||
|
||||
@@ -1354,15 +1290,6 @@ class TopNav(BaseComponent):
|
||||
self.right_button.is_selected = self.is_selected
|
||||
self.right_button.render()
|
||||
|
||||
# self.image_draw.text(
|
||||
# (self.text_x, self.text_y),
|
||||
# self.text,
|
||||
# font=self.font,
|
||||
# fill=self.font_color,
|
||||
# stroke_width=1,
|
||||
# stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
# )
|
||||
|
||||
|
||||
|
||||
def linear_interp(a, b, t):
|
||||
|
||||
@@ -174,36 +174,3 @@ class ScanScreen(BaseScreen):
|
||||
self.camera.stop_video_stream_mode()
|
||||
break
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingsUpdatedScreen(ButtonListScreen):
|
||||
config_name: str = None
|
||||
title: str = "Settings QR"
|
||||
is_bottom_list: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.button_data = ["Home"]
|
||||
self.show_back_button = False
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
start_y = self.top_nav.height + 20
|
||||
if self.config_name:
|
||||
self.config_name_textarea = TextArea(
|
||||
text=f'"{self.config_name}"',
|
||||
is_text_centered=True,
|
||||
auto_line_break=True,
|
||||
screen_y=start_y
|
||||
)
|
||||
self.components.append(self.config_name_textarea)
|
||||
start_y = self.config_name_textarea.screen_y + 50
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="Settings imported successfully!",
|
||||
is_text_centered=True,
|
||||
auto_line_break=True,
|
||||
screen_y=start_y
|
||||
))
|
||||
|
||||
|
||||
@@ -3,17 +3,15 @@ import time
|
||||
from dataclasses import dataclass
|
||||
from PIL import Image, ImageDraw, ImageColor
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
from seedsigner.gui.components import (GUIConstants,
|
||||
BaseComponent, Button, Icon, IconButton, LargeIconButton,
|
||||
SeedSignerIconConstants, TopNav, TextArea, load_image)
|
||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
from ..components import (FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton,
|
||||
LargeIconButton, SeedSignerIconConstants, TopNav, TextArea, load_image, ToastOverlay,
|
||||
Fonts)
|
||||
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
|
||||
|
||||
# Must be huge numbers to avoid conflicting with the selected_button returned by the
|
||||
@@ -828,6 +826,7 @@ class LargeIconStatusScreen(ButtonListScreen):
|
||||
status_color: str = GUIConstants.SUCCESS_COLOR
|
||||
status_headline: str = "Success!" # The colored text under the large icon
|
||||
text: str = "" # The body text of the screen
|
||||
text_edge_padding: int = GUIConstants.EDGE_PADDING
|
||||
button_data: list = None
|
||||
allow_text_overflow: bool = False
|
||||
|
||||
@@ -863,7 +862,7 @@ class LargeIconStatusScreen(ButtonListScreen):
|
||||
height=self.buttons[0].screen_y - next_y,
|
||||
text=self.text,
|
||||
width=self.canvas_width,
|
||||
edge_padding=GUIConstants.EDGE_PADDING, # Don't render all the way up to the far left/right edges
|
||||
edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges
|
||||
screen_y=next_y,
|
||||
allow_text_overflow=self.allow_text_overflow,
|
||||
))
|
||||
@@ -937,6 +936,7 @@ class WarningEdgesThread(BaseThread):
|
||||
@dataclass
|
||||
class WarningEdgesMixin:
|
||||
status_color: str = GUIConstants.WARNING_COLOR
|
||||
text_edge_padding: int = 2 * GUIConstants.EDGE_PADDING
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
@@ -1222,37 +1222,11 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
"""
|
||||
return False
|
||||
|
||||
class MicroSDToastScreen(BaseScreen):
|
||||
"""
|
||||
This screen is an overlay with special behavior with the ToastOverlay component. The ToastOverlay component overides all button
|
||||
input and captures the existing screen content and stashes it to be restored once X second passes or any button is pressed. The
|
||||
display method on this screen will not complete until after the ToastOverlay render method is complete it's takeover of the screen.
|
||||
"""
|
||||
def __init__(self, action):
|
||||
self.action = action
|
||||
self.toast = None
|
||||
super().__init__()
|
||||
|
||||
def _run(self):
|
||||
return
|
||||
|
||||
def _render(self):
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
|
||||
if self.action == MicroSD.ACTION__REMOVED:
|
||||
|
||||
self.toast = ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.MICROSD,
|
||||
color=GUIConstants.NOTIFICATION_COLOR,
|
||||
label_text="MicroSD removed"
|
||||
)
|
||||
|
||||
elif self.action == MicroSD.ACTION__INSERTED:
|
||||
|
||||
self.toast = ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.MICROSD,
|
||||
color=GUIConstants.NOTIFICATION_COLOR,
|
||||
label_text="MicroSD inserted"
|
||||
)
|
||||
|
||||
self.toast.render()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MainMenuScreen(LargeButtonScreen):
|
||||
# Override LargeButtonScreen defaults
|
||||
title_font_size: int = 26
|
||||
show_back_button: bool = False
|
||||
show_power_button: bool = True
|
||||
|
||||
@@ -296,3 +296,36 @@ class DonateScreen(BaseTopNavScreen):
|
||||
supersampling_factor=1,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||
))
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingsQRConfirmationScreen(ButtonListScreen):
|
||||
config_name: str = None
|
||||
title: str = "Settings QR"
|
||||
status_message: str = "Settings updated..."
|
||||
is_bottom_list: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.button_data = ["Home"]
|
||||
self.show_back_button = False
|
||||
super().__post_init__()
|
||||
|
||||
start_y = self.top_nav.height + 20
|
||||
if self.config_name:
|
||||
self.config_name_textarea = TextArea(
|
||||
text=f'"{self.config_name}"',
|
||||
is_text_centered=True,
|
||||
auto_line_break=True,
|
||||
screen_y=start_y
|
||||
)
|
||||
self.components.append(self.config_name_textarea)
|
||||
start_y = self.config_name_textarea.screen_y + 50
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=self.status_message,
|
||||
is_text_centered=True,
|
||||
auto_line_break=True,
|
||||
screen_y=start_y
|
||||
))
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from PIL.Image import Image
|
||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, FormattedAddress, GUIConstants, IconTextLine, SeedSignerIconConstants, TextArea
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerIconConstants, TextArea
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, ButtonListScreen, KeyboardScreen
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.helpers.qr import QR
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
super().__post_init__()
|
||||
|
||||
self.camera = Camera.get_instance()
|
||||
self.camera.start_video_stream_mode(resolution=(240, 240), framerate=24, format="rgb")
|
||||
self.camera.start_video_stream_mode(resolution=(self.canvas_width, self.canvas_height), framerate=24, format="rgb")
|
||||
|
||||
|
||||
def _run(self):
|
||||
@@ -32,27 +32,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
|
||||
while True:
|
||||
frame = self.camera.read_video_stream(as_image=True)
|
||||
if frame is not None:
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text="< back | click joystick",
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
if len(preview_images) < max_entropy_frames:
|
||||
preview_images.append(frame)
|
||||
|
||||
# Check for BACK button press
|
||||
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT):
|
||||
# Have to manually update last input time since we're not in a wait_for loop
|
||||
self.hw_inputs.update_last_input_time()
|
||||
@@ -60,7 +40,15 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
self.camera.stop_video_stream_mode()
|
||||
return RET_CODE__BACK_BUTTON
|
||||
|
||||
elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_PRESS):
|
||||
frame = self.camera.read_video_stream(as_image=True)
|
||||
|
||||
if frame is None:
|
||||
# Camera probably isn't ready yet
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
# Check for joystick click to take final entropy image
|
||||
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_PRESS):
|
||||
# Have to manually update last input time since we're not in a wait_for loop
|
||||
self.hw_inputs.update_last_input_time()
|
||||
self.camera.stop_video_stream_mode()
|
||||
@@ -83,6 +71,29 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
|
||||
return preview_images
|
||||
|
||||
# If we're still here, it's just another preview frame loop
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text="< back | click joystick",
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
if len(preview_images) == max_entropy_frames:
|
||||
# Keep a moving window of the last n preview frames; pop the oldest
|
||||
# before we add the currest frame.
|
||||
preview_images.pop(0)
|
||||
preview_images.append(frame)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -240,12 +251,13 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=f"""Your input: \"{selection_text}\"""",
|
||||
screen_y=self.top_nav.height,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING - 2, # Nudge to last line doesn't get too close to "Next" button
|
||||
height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline
|
||||
))
|
||||
|
||||
# ...and that entropy's associated 11 bits
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||
self.components.append(TextArea(
|
||||
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||
first_bits_line = TextArea(
|
||||
text=keeper_selected_bits,
|
||||
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||
font_size=bit_font_size,
|
||||
@@ -253,10 +265,13 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
screen_x=bit_display_x,
|
||||
screen_y=screen_y,
|
||||
is_text_centered=False,
|
||||
))
|
||||
)
|
||||
self.components.append(first_bits_line)
|
||||
|
||||
# Render the least significant bits that will be replaced by the checksum in a
|
||||
# de-emphasized font color.
|
||||
if "_" in discard_selected_bits:
|
||||
screen_y += int(first_bits_line.height/2) # center the underscores vertically like hypens
|
||||
self.components.append(TextArea(
|
||||
text=discard_selected_bits,
|
||||
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||
@@ -272,7 +287,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
self.components.append(TextArea(
|
||||
text="Checksum",
|
||||
edge_padding=0,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
screen_y=first_bits_line.screen_y + first_bits_line.height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
|
||||
# ...and its actual bits. Prepend spacers to keep vertical alignment
|
||||
@@ -288,7 +303,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
font_size=bit_font_size,
|
||||
edge_padding=0,
|
||||
screen_x=bit_display_x,
|
||||
screen_y=screen_y,
|
||||
screen_y=screen_y + int(first_bits_line.height/2), # center the underscores vertically like hypens
|
||||
is_text_centered=False,
|
||||
))
|
||||
|
||||
@@ -308,6 +323,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
self.components.append(TextArea(
|
||||
text=f"""Final Word: \"{self.actual_final_word}\"""",
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline
|
||||
))
|
||||
|
||||
# Once again show the bits that came from the user's entropy...
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from seedsigner.gui.components import BaseComponent, GUIConstants, Icon, SeedSignerIconConstants, TextArea
|
||||
from seedsigner.models.threads import BaseThread
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToastOverlay(BaseComponent):
|
||||
icon_name: str = None
|
||||
color: str = GUIConstants.NOTIFICATION_COLOR
|
||||
label_text: str = None
|
||||
height: int = GUIConstants.ICON_TOAST_FONT_SIZE + 2*GUIConstants.EDGE_PADDING
|
||||
font_size: int = 19
|
||||
outline_thickness: int = 2 # pixels
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
self.icon = Icon(
|
||||
image_draw=self.image_draw,
|
||||
canvas=self.canvas,
|
||||
screen_x=self.outline_thickness + 2*GUIConstants.EDGE_PADDING, # Push the icon further from the left edge than strictly necessary
|
||||
icon_name=self.icon_name,
|
||||
icon_size=GUIConstants.ICON_TOAST_FONT_SIZE,
|
||||
icon_color=self.color
|
||||
)
|
||||
self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2)
|
||||
|
||||
self.label = TextArea(
|
||||
image_draw=self.image_draw,
|
||||
canvas=self.canvas,
|
||||
text=self.label_text,
|
||||
font_size=self.font_size,
|
||||
font_color=self.color,
|
||||
edge_padding=0,
|
||||
is_text_centered=False,
|
||||
auto_line_break=True,
|
||||
width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness,
|
||||
screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING,
|
||||
allow_text_overflow=False,
|
||||
)
|
||||
|
||||
# Vertically center the message within the toast (for single- or multi-line
|
||||
# messages).
|
||||
self.label.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - 2*self.outline_thickness - self.label.height)/2)
|
||||
|
||||
|
||||
def render(self):
|
||||
# Render the toast's solid background
|
||||
self.image_draw.rounded_rectangle(
|
||||
(0, self.canvas_height - self.height, self.canvas_width, self.canvas_height),
|
||||
fill=GUIConstants.BACKGROUND_COLOR,
|
||||
radius=8,
|
||||
outline=self.color,
|
||||
width=self.outline_thickness,
|
||||
)
|
||||
|
||||
# Draw the toast visual elements
|
||||
self.icon.render()
|
||||
self.label.render()
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
|
||||
class BaseToastOverlayManagerThread(BaseThread):
|
||||
"""
|
||||
The toast notification popup consists of a gui component (`ToastOverlay`) and this
|
||||
manager thread that the Controller will use to coordinate handing off resources
|
||||
between competing toasts, the screensaver, and the current underlying Screen.
|
||||
|
||||
Controller should set BaseThread.keep_running = False to terminate the toast when it
|
||||
needs to be removed or replaced.
|
||||
|
||||
Controller should set toggle_renderer_lock = True to make the toast temporarily
|
||||
release the Renderer.lock so another process (e.g. screensaver) can use it. The toast
|
||||
thread will immediately try to reacquire the lock, but will have to block and wait
|
||||
until it's available again. Note that this thread will be unresponsive while it
|
||||
waits to reacquire the lock!
|
||||
|
||||
Note: any process can call lock.release() but it simplifies the logic to try to keep
|
||||
each process aware of whether it is currently holding the lock or not (i.e. it's
|
||||
better for the "owner" thread to release the lock itself).
|
||||
"""
|
||||
def __init__(self,
|
||||
activation_delay: int = 0, # seconds before toast is displayed
|
||||
duration: int = 3, # seconds toast is displayed
|
||||
):
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.buttons import HardwareButtons
|
||||
super().__init__()
|
||||
self.activation_delay: int = activation_delay
|
||||
self.duration: int = duration
|
||||
self._toggle_renderer_lock: bool = False
|
||||
|
||||
self.renderer = Renderer.get_instance()
|
||||
self.controller = Controller.get_instance()
|
||||
self.hw_inputs = HardwareButtons.get_instance()
|
||||
|
||||
# Special case when screensaver is running
|
||||
self.hw_inputs.override_ind = True
|
||||
|
||||
self.toast = self.instantiate_toast()
|
||||
|
||||
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
raise Exception("Must be implemented by subclass")
|
||||
|
||||
|
||||
def should_keep_running(self) -> bool:
|
||||
""" Placeholder for custom exit conditions """
|
||||
return True
|
||||
|
||||
|
||||
def toggle_renderer_lock(self):
|
||||
self._toggle_renderer_lock = True
|
||||
|
||||
|
||||
def run(self):
|
||||
print(f"{self.__class__.__name__}: started")
|
||||
start = time.time()
|
||||
while time.time() - start < self.activation_delay:
|
||||
if self.hw_inputs.has_any_input():
|
||||
# User has pressed a button, cancel the toast
|
||||
print(f"{self.__class__.__name__}: Canceling toast due to user input")
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
try:
|
||||
# Hold onto the Renderer lock so we're guaranteed to restore the original
|
||||
# screen before any other listener can get a screen write in.
|
||||
print(f"{self.__class__.__name__}: Acquiring lock")
|
||||
self.renderer.lock.acquire()
|
||||
print(f"{self.__class__.__name__}: Lock acquired")
|
||||
|
||||
has_rendered = False
|
||||
previous_screen_state = None
|
||||
while self.keep_running and self.should_keep_running():
|
||||
if self.hw_inputs.has_any_input():
|
||||
# User has pressed a button, hide the toast
|
||||
print(f"{self.__class__.__name__}: Exiting due to user input")
|
||||
break
|
||||
|
||||
if self._toggle_renderer_lock:
|
||||
# Controller has notified us that another process needs the lock
|
||||
print(f"{self.__class__.__name__}: Releasing lock")
|
||||
self._toggle_renderer_lock = False
|
||||
self.renderer.lock.release()
|
||||
|
||||
# pause to avoid race conditions reacquiring the lock
|
||||
while not self.renderer.lock.locked():
|
||||
# Wait for a different process to grab the lock
|
||||
time.sleep(0.1)
|
||||
|
||||
# Block while waiting to reaquire the lock
|
||||
print(f"{self.__class__.__name__}: Blocking to re-acquire lock")
|
||||
self.renderer.lock.acquire()
|
||||
print(f"{self.__class__.__name__}: Lock re-acquired")
|
||||
|
||||
if not has_rendered:
|
||||
previous_screen_state = self.renderer.canvas.copy()
|
||||
print(f"{self.__class__.__name__}: Showing toast")
|
||||
self.toast.render()
|
||||
has_rendered = True
|
||||
|
||||
if time.time() - start > self.activation_delay + self.duration and has_rendered:
|
||||
print(f"{self.__class__.__name__}: Hiding toast")
|
||||
break
|
||||
|
||||
# Free up cpu resources for main thread
|
||||
time.sleep(0.1)
|
||||
|
||||
finally:
|
||||
print(f"{self.__class__.__name__}: exiting")
|
||||
if has_rendered and self.renderer.lock.locked():
|
||||
# As far as we know, we currently hold the Renderer.lock
|
||||
self.renderer.show_image(previous_screen_state)
|
||||
print(f"{self.__class__.__name__}: restored previous screen state")
|
||||
|
||||
# We're done, release the lock
|
||||
self.renderer.lock.release()
|
||||
|
||||
|
||||
|
||||
class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread):
|
||||
def __init__(self, activation_delay=3):
|
||||
# Note: activation_delay is configurable so the screenshot generator can get the
|
||||
# toast to immediately render.
|
||||
super().__init__(
|
||||
activation_delay=activation_delay, # seconds
|
||||
duration=1e6, # seconds ("forever")
|
||||
)
|
||||
|
||||
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.MICROSD,
|
||||
label_text="You can remove\nthe SD card now",
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING,
|
||||
)
|
||||
|
||||
|
||||
def should_keep_running(self) -> bool:
|
||||
""" Custom exit condition: keep running until the SD card is removed """
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
return MicroSD.get_instance().is_inserted
|
||||
|
||||
|
||||
|
||||
class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread):
|
||||
def __init__(self, action: str, *args, **kwargs):
|
||||
# Note: we could just directly detect the MicroSD status here, but passing it in
|
||||
# via `action` lets us simulate the state we want in the screenshot generator.
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
if action not in [MicroSD.ACTION__INSERTED, MicroSD.ACTION__REMOVED]:
|
||||
raise Exception(f"Invalid MicroSD action: {action}")
|
||||
self.message = "SD card removed" if action == MicroSD.ACTION__REMOVED else "SD card inserted"
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
print("instantiating toast!")
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.MICROSD,
|
||||
label_text=self.message,
|
||||
)
|
||||
@@ -1,55 +1,71 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
from seedsigner.models.singleton import Singleton
|
||||
from seedsigner.models.threads import BaseThread
|
||||
from seedsigner.models.settings import Settings
|
||||
#from seedsigner.views.view import MicroSDToastView
|
||||
from seedsigner.gui.screens.screen import MicroSDToastScreen
|
||||
|
||||
|
||||
|
||||
class MicroSD(Singleton, BaseThread):
|
||||
|
||||
ACTION__INSERTED = "add"
|
||||
ACTION__REMOVED = "remove"
|
||||
MOUNT_POINT = "/mnt/microsd"
|
||||
FIFO_PATH = "/tmp/mdev_fifo"
|
||||
FIFO_MODE = 0o600
|
||||
ACTION__INSERTED = "add"
|
||||
ACTION__REMOVED = "remove"
|
||||
|
||||
settings_handler = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
# This is the only way to access the one and only instance
|
||||
if cls._instance is None:
|
||||
# Instantiate the one and only instance
|
||||
microsd = cls.__new__(cls)
|
||||
cls._instance = microsd
|
||||
|
||||
# explicitly call BaseThread __init__ since multiple class inheritance
|
||||
BaseThread.__init__(microsd)
|
||||
|
||||
return cls._instance
|
||||
|
||||
def start_detection(self):
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
import os
|
||||
|
||||
FIFO_PATH = "/tmp/mdev_fifo"
|
||||
FIFO_MODE = 0o600
|
||||
action = ""
|
||||
|
||||
# explicitly only microsd add/remove detection in seedsigner-os
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
|
||||
if os.path.exists(FIFO_PATH):
|
||||
os.remove(FIFO_PATH)
|
||||
|
||||
os.mkfifo(FIFO_PATH, FIFO_MODE)
|
||||
|
||||
while self.keep_running:
|
||||
with open(FIFO_PATH) as fifo:
|
||||
action = fifo.read()
|
||||
print(f"fifo message: {action}")
|
||||
|
||||
Settings.microsd_handler(action=action)
|
||||
|
||||
toastscreen = MicroSDToastScreen(action=action)
|
||||
toastscreen.display()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
# This is the only way to access the one and only instance
|
||||
if cls._instance is None:
|
||||
# Instantiate the one and only instance
|
||||
microsd = cls.__new__(cls)
|
||||
cls._instance = microsd
|
||||
|
||||
# explicitly call BaseThread __init__ since multiple class inheritance
|
||||
BaseThread.__init__(microsd)
|
||||
|
||||
return cls._instance
|
||||
|
||||
|
||||
@property
|
||||
def is_inserted(self):
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
return os.path.exists(MicroSD.MOUNT_POINT)
|
||||
else:
|
||||
# Always True for Raspi OS
|
||||
return True
|
||||
|
||||
|
||||
def start_detection(self):
|
||||
self.start()
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.toast import SDCardStateChangeToastManagerThread
|
||||
action = ""
|
||||
|
||||
# explicitly only microsd add/remove detection in seedsigner-os
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
|
||||
# at start-up, get current status and inform Settings
|
||||
Settings.handle_microsd_state_change(
|
||||
action=MicroSD.ACTION__INSERTED if self.is_inserted else MicroSD.ACTION__REMOVED
|
||||
)
|
||||
|
||||
if os.path.exists(self.FIFO_PATH):
|
||||
os.remove(self.FIFO_PATH)
|
||||
|
||||
os.mkfifo(self.FIFO_PATH, self.FIFO_MODE)
|
||||
|
||||
while self.keep_running:
|
||||
with open(self.FIFO_PATH) as fifo:
|
||||
action = fifo.read()
|
||||
print(f"fifo message: {action}")
|
||||
|
||||
Settings.handle_microsd_state_change(action=action)
|
||||
Controller.get_instance().activate_toast(SDCardStateChangeToastManagerThread(action=action))
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
@@ -146,8 +146,24 @@ def parse_derivation_path(derivation_path: str) -> dict:
|
||||
if not details["script_type"]:
|
||||
details["script_type"] = SettingsConstants.CUSTOM_DERIVATION
|
||||
details["network"] = lookups["networks"].get(sections[2])
|
||||
details["is_change"] = sections[-2] == "1"
|
||||
details["index"] = int(sections[-1])
|
||||
|
||||
# Check if there's a standard change path
|
||||
if sections[-2] in ["0", "1"]:
|
||||
details["is_change"] = sections[-2] == "1"
|
||||
else:
|
||||
details["is_change"] = None
|
||||
|
||||
# Check if there's a standard address index
|
||||
if sections[-1].isdigit():
|
||||
details["index"] = int(sections[-1])
|
||||
else:
|
||||
details["index"] = None
|
||||
|
||||
if details["is_change"] is not None and details["index"] is not None:
|
||||
# standard change and addr index; safe to truncate to the wallet level
|
||||
details["wallet_derivation_path"] = "/".join(sections[:-2])
|
||||
else:
|
||||
details["wallet_derivation_path"] = None
|
||||
|
||||
details["clean_match"] = True
|
||||
for k, v in details.items():
|
||||
|
||||
@@ -126,7 +126,7 @@ class Bytewords:
|
||||
elif style == Bytewords_Style_minimal:
|
||||
return encode_minimal(bytes)
|
||||
else:
|
||||
assert(False)
|
||||
assert False
|
||||
|
||||
@staticmethod
|
||||
def decode(style, str):
|
||||
@@ -137,4 +137,4 @@ class Bytewords:
|
||||
elif style == Bytewords_Style_minimal:
|
||||
return decode(str, 0, 2)
|
||||
else:
|
||||
assert(False)
|
||||
assert False
|
||||
|
||||
@@ -255,7 +255,7 @@ class FountainDecoder:
|
||||
elif self.is_failure():
|
||||
return 'Exception: {}'.format(self.result)
|
||||
else:
|
||||
assert(False)
|
||||
assert False
|
||||
|
||||
def print_part(self, p):
|
||||
print('part indexes: {}'.format(self.indexes_to_string(p.indexes)))
|
||||
|
||||
@@ -84,7 +84,7 @@ class Part:
|
||||
|
||||
class FountainEncoder:
|
||||
def __init__(self, message, max_fragment_len, first_seq_num = 0, min_fragment_len = 10):
|
||||
assert(len(message) <= MAX_UINT32)
|
||||
assert len(message) <= MAX_UINT32
|
||||
self.message_len = len(message)
|
||||
self.checksum = crc32_int(message)
|
||||
self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len)
|
||||
@@ -93,9 +93,9 @@ class FountainEncoder:
|
||||
|
||||
@staticmethod
|
||||
def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len):
|
||||
assert(message_len > 0)
|
||||
assert(min_fragment_len > 0)
|
||||
assert(max_fragment_len >= min_fragment_len)
|
||||
assert message_len > 0
|
||||
assert min_fragment_len > 0
|
||||
assert max_fragment_len >= min_fragment_len
|
||||
max_fragment_count = message_len // min_fragment_len
|
||||
fragment_len = None
|
||||
|
||||
@@ -104,7 +104,7 @@ class FountainEncoder:
|
||||
if fragment_len <= max_fragment_len:
|
||||
break
|
||||
|
||||
assert(fragment_len != None)
|
||||
assert fragment_len != None
|
||||
return fragment_len
|
||||
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ class RandomSampler:
|
||||
|
||||
def __init__(self, probs):
|
||||
for p in probs:
|
||||
assert(p > 0)
|
||||
assert p > 0
|
||||
|
||||
# Normalize given probabilities
|
||||
total = sum(probs)
|
||||
assert(total > 0)
|
||||
assert total > 0
|
||||
|
||||
n = len(probs)
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ def join_bytes(list_of_ba):
|
||||
|
||||
def xor_into(target, source):
|
||||
count = len(target)
|
||||
assert(count == len(source)) # Must be the same length
|
||||
assert count == len(source) # Must be the same length
|
||||
for i in range(count):
|
||||
target[i] ^= source[i]
|
||||
|
||||
|
||||
@@ -63,6 +63,11 @@ class SeedStorage:
|
||||
|
||||
|
||||
def update_pending_mnemonic(self, word: str, index: int):
|
||||
"""
|
||||
Replaces the nth word in the pending mnemonic.
|
||||
|
||||
* may specify a negative `index` (e.g. -1 is the last word).
|
||||
"""
|
||||
if index >= len(self._pending_mnemonic):
|
||||
raise Exception(f"index {index} is too high")
|
||||
self._pending_mnemonic[index] = word
|
||||
|
||||
@@ -87,6 +87,12 @@ class Settings(Singleton):
|
||||
values = value
|
||||
for v in values:
|
||||
if v not in [opt[0] for opt in settings_entry.selection_options]:
|
||||
if settings_entry.attr_name == SettingsConstants.SETTING__PERSISTENT_SETTINGS and v == SettingsConstants.OPTION__ENABLED:
|
||||
# Special case: trying to enable Persistent Settings when
|
||||
# DISABLED is the only option allowed (because the SD card is not
|
||||
# inserted. Explicitly set to DISABLED.
|
||||
value = SettingsConstants.OPTION__DISABLED
|
||||
break
|
||||
raise InvalidSettingsQRData(f"""{abbreviated_name} = '{v}' is not valid""")
|
||||
|
||||
updated_settings[settings_entry.attr_name] = value
|
||||
@@ -213,7 +219,6 @@ class Settings(Singleton):
|
||||
return display_names
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Intentionally keeping the properties very limited to avoid an expectation of
|
||||
boilerplate property code for every SettingsEntry.
|
||||
@@ -227,28 +232,39 @@ class Settings(Singleton):
|
||||
return self._data[SettingsConstants.SETTING__DEBUG] == SettingsConstants.OPTION__ENABLED
|
||||
|
||||
|
||||
def microsd_handler(action):
|
||||
def handle_microsd_state_change(action: str):
|
||||
"""
|
||||
Enables/Disables the Persistent Settings option based on the MicroSD card state.
|
||||
"""
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
|
||||
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
|
||||
if action == MicroSD.ACTION__INSERTED:
|
||||
# restore persistent settings back to defaults
|
||||
# SD card was just inserted.
|
||||
# Restore persistent settings back to defaults
|
||||
entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS)
|
||||
entry.selection_options = SettingsConstants.OPTIONS__ENABLED_DISABLED
|
||||
entry.help_text = "Store Settings on SD card."
|
||||
|
||||
entry.help_text = SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT
|
||||
|
||||
# TODO: Perhaps prompt the user if the current settings (not including persistent
|
||||
# settings) should overwrite the settings on disk, if they differ:
|
||||
# - Overwrite settings on the SD?
|
||||
# - Load settings from SD?
|
||||
# if Settings file exists (meaning persistent settings was previously enabled), write out current settings to disk
|
||||
if os.path.exists(Settings.SETTINGS_FILENAME):
|
||||
# enable persistent settings first, then save
|
||||
Settings.get_instance()._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] = SettingsConstants.OPTION__ENABLED
|
||||
Settings.get_instance().save()
|
||||
|
||||
|
||||
elif action == MicroSD.ACTION__REMOVED:
|
||||
# set persistent settings to disabled value directly
|
||||
# SD card was just removed.
|
||||
# Set persistent settings to disabled value directly
|
||||
Settings.get_instance()._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] = SettingsConstants.OPTION__DISABLED
|
||||
|
||||
|
||||
# set persistent settings to only have disabled as an option, adding additional help text that microSD is removed
|
||||
entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS)
|
||||
entry.selection_options = SettingsConstants.OPTIONS__ONLY_DISABLED
|
||||
entry.help_text = "MicroSD card is removed"
|
||||
entry.help_text = SettingsConstants.PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT
|
||||
|
||||
else:
|
||||
raise Exception(f"Invalid MicroSD action: {action}")
|
||||
|
||||
@@ -101,6 +101,8 @@ class SettingsConstants:
|
||||
if network == SettingsConstants.REGTEST:
|
||||
return "regtest"
|
||||
|
||||
PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT = "Store Settings on SD card"
|
||||
PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT = "Insert SD card to enable"
|
||||
|
||||
SINGLE_SIG = "ss"
|
||||
MULTISIG = "ms"
|
||||
@@ -354,7 +356,7 @@ class SettingsDefinition:
|
||||
attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS,
|
||||
abbreviated_name="persistent",
|
||||
display_name="Persistent settings",
|
||||
help_text="Store Settings on SD card.",
|
||||
help_text=SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__WALLET,
|
||||
|
||||
@@ -513,6 +513,8 @@ class PSBTSignedQRDisplayView(View):
|
||||
|
||||
|
||||
class PSBTSigningErrorView(View):
|
||||
SELECT_DIFF_SEED = "Select Diff Seed"
|
||||
|
||||
def run(self):
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
if not psbt_parser:
|
||||
@@ -520,15 +522,18 @@ class PSBTSigningErrorView(View):
|
||||
return Destination(MainMenuView)
|
||||
|
||||
# Just a WarningScreen here; only use DireWarningScreen for true security risks.
|
||||
selected_menu_num = WarningScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="PSBT Error",
|
||||
status_icon_name=SeedSignerIconConstants.WARNING,
|
||||
status_headline="Signing Failed",
|
||||
text="Signing with this seed did not add a valid signature.",
|
||||
button_data=["Select Diff Seed"],
|
||||
).display()
|
||||
button_data=[self.SELECT_DIFF_SEED]
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
# clear seed selected for psbt signing since it did not add a valid signature
|
||||
self.controller.psbt_seed = None
|
||||
return Destination(PSBTSelectSeedView, clear_history=True)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
|
||||
@@ -136,30 +136,30 @@ class ScanView(View):
|
||||
)
|
||||
|
||||
elif self.decoder.is_sign_message:
|
||||
if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__ENABLED:
|
||||
from seedsigner.views.seed_views import SeedSignMessageStartView
|
||||
qr_data = self.decoder.get_qr_data()
|
||||
from seedsigner.views.seed_views import SeedSignMessageStartView
|
||||
qr_data = self.decoder.get_qr_data()
|
||||
|
||||
return Destination(
|
||||
SeedSignMessageStartView,
|
||||
view_args=dict(
|
||||
derivation_path=qr_data["derivation_path"],
|
||||
message=qr_data["message"],
|
||||
)
|
||||
return Destination(
|
||||
SeedSignMessageStartView,
|
||||
view_args=dict(
|
||||
derivation_path=qr_data["derivation_path"],
|
||||
message=qr_data["message"],
|
||||
)
|
||||
else:
|
||||
return Destination(OptionDisabledView, view_args=dict(error_msg="Message signing is currently disabled in Settings"))
|
||||
)
|
||||
|
||||
else:
|
||||
return Destination(NotYetImplementedView)
|
||||
|
||||
elif self.decoder.is_invalid:
|
||||
# For now, don't even try to re-do the attempted operation, just reset and
|
||||
# start everything over.
|
||||
self.controller.resume_main_flow = None
|
||||
return Destination(ErrorView, view_args=dict(
|
||||
title="Error",
|
||||
status_headline="Unknown QR Type",
|
||||
text="QRCode is invalid or is a data format not yet supported.",
|
||||
button_text="Back",
|
||||
next_destination=Destination(BackStackView, skip_current_view=True),
|
||||
button_text="Done",
|
||||
next_destination=Destination(MainMenuView, clear_history=True),
|
||||
))
|
||||
|
||||
return Destination(MainMenuView)
|
||||
|
||||
@@ -89,7 +89,6 @@ class OpeningSplashScreen(LogoScreen):
|
||||
|
||||
|
||||
|
||||
|
||||
class ScreensaverScreen(LogoScreen):
|
||||
def __init__(self, buttons):
|
||||
super().__init__()
|
||||
@@ -141,9 +140,9 @@ class ScreensaverScreen(LogoScreen):
|
||||
# never gives up the lock until it returns.
|
||||
with self.renderer.lock:
|
||||
try:
|
||||
while True:
|
||||
while self._is_running:
|
||||
if self.buttons.has_any_input() or self.buttons.override_ind:
|
||||
return self.stop()
|
||||
break
|
||||
|
||||
# Must crop the image to the exact display size
|
||||
crop = self.image.crop((
|
||||
@@ -176,20 +175,23 @@ class ScreensaverScreen(LogoScreen):
|
||||
self.increment_y = self.rand_increment()
|
||||
if self.increment_y > 0.0:
|
||||
self.increment_y *= -1.0
|
||||
|
||||
except KeyboardInterrupt as e:
|
||||
# Exit triggered; close gracefully
|
||||
print("Shutting down Screensaver")
|
||||
self.stop()
|
||||
|
||||
# Have to let the interrupt bubble up to exit the main app
|
||||
raise e
|
||||
|
||||
finally:
|
||||
self._is_running = False
|
||||
|
||||
# Restore the original screen
|
||||
self.renderer.show_image(self.last_screen)
|
||||
|
||||
|
||||
|
||||
def stop(self):
|
||||
# Restore the original screen
|
||||
self.renderer.show_image(self.last_screen)
|
||||
|
||||
self._is_running = False
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from seedsigner.models.seed import InvalidSeedException, Seed
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
from seedsigner.models.settings_definition import SettingsDefinition
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
from seedsigner.views.view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView
|
||||
from seedsigner.views.view import NotYetImplementedView, OptionDisabledView, View, Destination, BackStackView, MainMenuView
|
||||
|
||||
|
||||
|
||||
@@ -1921,23 +1921,41 @@ class SeedSignMessageStartView(View):
|
||||
self.derivation_path = derivation_path
|
||||
self.message = message
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__DISABLED:
|
||||
self.set_redirect(Destination(OptionDisabledView, view_args=dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)))
|
||||
return
|
||||
|
||||
# calculate the actual receive address
|
||||
addr_format = embit_utils.parse_derivation_path(derivation_path)
|
||||
if not addr_format["clean_match"]:
|
||||
raise NotYetImplementedView("Signing messages for custom derivation paths not supported")
|
||||
|
||||
# Note: addr_format["network"] can be MAINNET or [TESTNET, REGTEST]
|
||||
if self.settings.get_value(SettingsConstants.SETTING__NETWORK) not in addr_format["network"]:
|
||||
from seedsigner.views.view import NetworkMismatchErrorView
|
||||
self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}")))
|
||||
|
||||
# cleanup. Note: We could leave this in place so the user can resume the
|
||||
# flow, but for now we avoid complications and keep things simple.
|
||||
self.controller.resume_main_flow = None
|
||||
return
|
||||
|
||||
data = self.controller.sign_message_data
|
||||
if not data:
|
||||
data = {}
|
||||
self.controller.sign_message_data = data
|
||||
data["derivation_path"] = derivation_path
|
||||
data["message"] = message
|
||||
data["addr_format"] = addr_format
|
||||
|
||||
# May be None
|
||||
self.seed_num = data.get("seed_num")
|
||||
|
||||
|
||||
def run(self):
|
||||
if self.seed_num is not None:
|
||||
# We already know which seed we're signing with
|
||||
return Destination(SeedSignMessageConfirmMessageView, skip_current_view=True)
|
||||
self.set_redirect(Destination(SeedSignMessageConfirmMessageView, skip_current_view=True))
|
||||
else:
|
||||
return Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__SIGN_MESSAGE), skip_current_view=True)
|
||||
self.set_redirect(Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__SIGN_MESSAGE), skip_current_view=True))
|
||||
|
||||
|
||||
|
||||
@@ -1979,16 +1997,19 @@ class SeedSignMessageConfirmAddressView(View):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
data = self.controller.sign_message_data
|
||||
self.seed_num = data.get("seed_num")
|
||||
seed_num = data.get("seed_num")
|
||||
self.derivation_path = data.get("derivation_path")
|
||||
|
||||
if self.seed_num is None or not self.derivation_path:
|
||||
if seed_num is None or not self.derivation_path:
|
||||
raise Exception("Routing error: sign_message_data hasn't been set")
|
||||
|
||||
seed = self.controller.storage.seeds[seed_num]
|
||||
addr_format = data.get("addr_format")
|
||||
|
||||
# calculate the actual receive address
|
||||
seed = self.controller.storage.seeds[self.seed_num]
|
||||
seed = self.controller.storage.seeds[seed_num]
|
||||
addr_format = embit_utils.parse_derivation_path(self.derivation_path)
|
||||
if not addr_format["clean_match"]:
|
||||
if not addr_format["clean_match"] or addr_format["script_type"] == SettingsConstants.CUSTOM_DERIVATION:
|
||||
raise Exception("Signing messages for custom derivation paths not supported")
|
||||
|
||||
if addr_format["network"] != SettingsConstants.MAINNET:
|
||||
@@ -2005,7 +2026,7 @@ class SeedSignMessageConfirmAddressView(View):
|
||||
self.controller.sign_message_data = None
|
||||
return
|
||||
|
||||
xpub = seed.get_xpub(wallet_path=self.derivation_path, network=addr_format["network"])
|
||||
xpub = seed.get_xpub(wallet_path=addr_format["wallet_derivation_path"], network=addr_format["network"])
|
||||
embit_network = embit_utils.get_embit_network_name(addr_format["network"])
|
||||
self.address = embit_utils.get_single_sig_address(xpub=xpub, script_type=addr_format["script_type"], index=addr_format["index"], is_change=addr_format["is_change"], embit_network=embit_network)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from seedsigner.gui.components import SeedSignerIconConstants
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
|
||||
from .view import View, Destination, MainMenuView
|
||||
|
||||
@@ -195,14 +196,21 @@ class SettingsIngestSettingsQRView(View):
|
||||
# May raise an Exception which will bubble up to the Controller to display to the
|
||||
# user.
|
||||
self.config_name, settings_update_dict = Settings.parse_settingsqr(data)
|
||||
|
||||
self.settings.update(settings_update_dict)
|
||||
|
||||
|
||||
if MicroSD.get_instance().is_inserted and self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED:
|
||||
self.status_message = "Persistent Settings enabled. Settings saved to SD card."
|
||||
else:
|
||||
self.status_message = "Settings updated in temporary memory"
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.scan_screens import SettingsUpdatedScreen
|
||||
from seedsigner.gui.screens.settings_screens import SettingsQRConfirmationScreen
|
||||
self.run_screen(
|
||||
SettingsUpdatedScreen,
|
||||
config_name=self.config_name
|
||||
SettingsQRConfirmationScreen,
|
||||
config_name=self.config_name,
|
||||
status_message=self.status_message,
|
||||
)
|
||||
|
||||
# Only one exit point
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import os
|
||||
import time
|
||||
@@ -223,9 +224,7 @@ class ToolsDiceEntropyEntryView(View):
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
print(f"Dice rolls: {ret}")
|
||||
dice_seed_phrase = mnemonic_generation.generate_mnemonic_from_dice(ret)
|
||||
print(f"""Mnemonic: "{dice_seed_phrase}" """)
|
||||
|
||||
# Add the mnemonic as an in-memory Seed
|
||||
seed = Seed(dice_seed_phrase, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
@@ -313,8 +312,7 @@ class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
|
||||
class ToolsCalcFinalWordCoinFlipsView(View):
|
||||
def run(self):
|
||||
mnemonic = self.controller.storage.pending_mnemonic
|
||||
mnemonic_length = len(mnemonic)
|
||||
mnemonic_length = len(self.controller.storage.pending_mnemonic)
|
||||
|
||||
if mnemonic_length == 12:
|
||||
total_flips = 7
|
||||
@@ -329,68 +327,74 @@ class ToolsCalcFinalWordCoinFlipsView(View):
|
||||
return Destination(BackStackView)
|
||||
|
||||
else:
|
||||
print(ret_val)
|
||||
binary_string = ret_val + "0" * (11 - total_flips)
|
||||
wordlist_index = int(binary_string, 2)
|
||||
wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
word = wordlist[wordlist_index]
|
||||
self.controller.storage.update_pending_mnemonic(word, mnemonic_length - 1)
|
||||
|
||||
return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips=ret_val))
|
||||
|
||||
|
||||
|
||||
class ToolsCalcFinalWordShowFinalWordView(View):
|
||||
def __init__(self, coin_flips=None):
|
||||
def __init__(self, coin_flips: str = None):
|
||||
super().__init__()
|
||||
self.coin_flips = coin_flips
|
||||
|
||||
|
||||
def run(self):
|
||||
# Construct the actual final word. The user's selected_final_word
|
||||
# contributes:
|
||||
# * 3 bits to a 24-word seed (plus 8-bit checksum)
|
||||
# * 7 bits to a 12-word seed (plus 4-bit checksum)
|
||||
from seedsigner.helpers import mnemonic_generation
|
||||
|
||||
mnemonic = self.controller.storage.pending_mnemonic
|
||||
mnemonic_length = len(mnemonic)
|
||||
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
wordlist = Seed.get_wordlist(wordlist_language_code)
|
||||
|
||||
# Prep the user's selected word / coin flips and the actual final word for
|
||||
# the display.
|
||||
if coin_flips:
|
||||
self.selected_final_word = None
|
||||
self.selected_final_bits = coin_flips
|
||||
else:
|
||||
# Convert the user's final word selection into its binary index equivalent
|
||||
self.selected_final_word = self.controller.storage.pending_mnemonic[-1]
|
||||
self.selected_final_bits = format(wordlist.index(self.selected_final_word), '011b')
|
||||
|
||||
if coin_flips:
|
||||
# fill the last bits (what will eventually be the checksum) with zeros
|
||||
binary_string = coin_flips + "0" * (11 - len(coin_flips))
|
||||
|
||||
# retrieve the matching word for the resulting index
|
||||
wordlist_index = int(binary_string, 2)
|
||||
wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
word = wordlist[wordlist_index]
|
||||
|
||||
# update the pending mnemonic with our new "final" (pre-checksum) word
|
||||
self.controller.storage.update_pending_mnemonic(word, -1)
|
||||
|
||||
# Now calculate the REAL final word (has a proper checksum)
|
||||
final_mnemonic = mnemonic_generation.calculate_checksum(
|
||||
mnemonic=self.controller.storage.pending_mnemonic,
|
||||
wordlist_language_code=wordlist_language_code,
|
||||
)
|
||||
self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], mnemonic_length - 1)
|
||||
|
||||
# Prep the user's selected word (if there was one) and the actual final word for
|
||||
# the display.
|
||||
if self.coin_flips:
|
||||
selected_final_word = None
|
||||
selected_final_bits = self.coin_flips
|
||||
else:
|
||||
# Convert the user's final word selection into its binary index equivalent
|
||||
selected_final_word = mnemonic[-1]
|
||||
selected_final_bits = format(wordlist.index(selected_final_word), '011b')
|
||||
# Update our pending mnemonic with the real final word
|
||||
self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], -1)
|
||||
|
||||
mnemonic = self.controller.storage.pending_mnemonic
|
||||
mnemonic_length = len(mnemonic)
|
||||
|
||||
# And grab the actual final word's checksum bits
|
||||
actual_final_word = self.controller.storage.pending_mnemonic[-1]
|
||||
if mnemonic_length == 12:
|
||||
checksum_bits = format(wordlist.index(actual_final_word), '011b')[-4:]
|
||||
else:
|
||||
checksum_bits = format(wordlist.index(actual_final_word), '011b')[-8:]
|
||||
self.actual_final_word = self.controller.storage.pending_mnemonic[-1]
|
||||
num_checksum_bits = 4 if mnemonic_length == 12 else 8
|
||||
self.checksum_bits = format(wordlist.index(self.actual_final_word), '011b')[-num_checksum_bits:]
|
||||
|
||||
|
||||
def run(self):
|
||||
NEXT = "Next"
|
||||
button_data = [NEXT]
|
||||
selected_menu_num = ToolsCalcFinalWordScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ToolsCalcFinalWordScreen,
|
||||
title="Final Word Calc",
|
||||
button_data=button_data,
|
||||
selected_final_word=selected_final_word,
|
||||
selected_final_bits=selected_final_bits,
|
||||
checksum_bits=checksum_bits,
|
||||
actual_final_word=actual_final_word,
|
||||
).display()
|
||||
selected_final_word=self.selected_final_word,
|
||||
selected_final_bits=self.selected_final_bits,
|
||||
checksum_bits=self.checksum_bits,
|
||||
actual_final_word=self.actual_final_word,
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
@@ -5,6 +5,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconCo
|
||||
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
from seedsigner.models.settings_definition import SettingsDefinition
|
||||
from seedsigner.models.threads import BaseThread
|
||||
|
||||
|
||||
@@ -185,34 +186,33 @@ class MainMenuView(View):
|
||||
TOOLS = ("Tools", SeedSignerIconConstants.TOOLS)
|
||||
SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS)
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import MainMenuScreen
|
||||
button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS]
|
||||
selected_menu_num = self.run_screen(
|
||||
LargeButtonScreen,
|
||||
MainMenuScreen,
|
||||
title="Home",
|
||||
title_font_size=26,
|
||||
button_data=button_data,
|
||||
show_back_button=False,
|
||||
show_power_button=True,
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__POWER_BUTTON:
|
||||
return Destination(PowerOptionsView)
|
||||
|
||||
if button_data[selected_menu_num] == self.SCAN:
|
||||
from .scan_views import ScanView
|
||||
from seedsigner.views.scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.SEEDS:
|
||||
from .seed_views import SeedsMenuView
|
||||
from seedsigner.views.seed_views import SeedsMenuView
|
||||
return Destination(SeedsMenuView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TOOLS:
|
||||
from .tools_views import ToolsMenuView
|
||||
from seedsigner.views.tools_views import ToolsMenuView
|
||||
return Destination(ToolsMenuView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.SETTINGS:
|
||||
from .settings_views import SettingsMenuView
|
||||
from seedsigner.views.settings_views import SettingsMenuView
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
|
||||
@@ -332,7 +332,7 @@ class ErrorView(View):
|
||||
class NetworkMismatchErrorView(ErrorView):
|
||||
title: str = "Network Mismatch"
|
||||
show_back_button: bool = False
|
||||
button_text: str = "Change Settings"
|
||||
button_text: str = "Change Setting"
|
||||
next_destination: Destination = None
|
||||
|
||||
|
||||
@@ -369,17 +369,53 @@ class UnhandledExceptionView(View):
|
||||
|
||||
@dataclass
|
||||
class OptionDisabledView(View):
|
||||
error_msg: str
|
||||
UPDATE_SETTING = "Update Setting"
|
||||
DONE = "Done"
|
||||
settings_attr: str
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.settings_entry = SettingsDefinition.get_settings_entry(self.settings_attr)
|
||||
self.error_msg = f"\"{self.settings_entry.display_name}\" is currently disabled in Settings."
|
||||
|
||||
|
||||
def run(self):
|
||||
WarningScreen(
|
||||
button_data = [self.UPDATE_SETTING, self.DONE]
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="Option Disabled",
|
||||
status_headline=None,
|
||||
text=self.error_msg,
|
||||
button_data=["OK"],
|
||||
button_data=button_data,
|
||||
show_back_button=False,
|
||||
allow_text_overflow=True, # Fit what we can, let the rest go off the edges
|
||||
).display()
|
||||
|
||||
return Destination(MainMenuView, clear_history=True)
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == self.UPDATE_SETTING:
|
||||
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
|
||||
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_attr), clear_history=True)
|
||||
else:
|
||||
return Destination(MainMenuView, clear_history=True)
|
||||
|
||||
|
||||
|
||||
class RemoveMicroSDWarningView(View):
|
||||
"""
|
||||
Warning to remove the microsd
|
||||
"""
|
||||
def __init__(self, next_view: View):
|
||||
super().__init__()
|
||||
self.next_view = next_view
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Security Tip",
|
||||
status_icon_name=FontAwesomeIconConstants.SDCARD,
|
||||
status_headline="",
|
||||
text="For maximum security,\nremove the MicroSD card\nbefore continuing.",
|
||||
show_back_button=False,
|
||||
button_data=["Continue"],
|
||||
)
|
||||
|
||||
return Destination(self.next_view, clear_history=True)
|
||||
|
||||
+23
-3
@@ -1,26 +1,38 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from mock import MagicMock, patch
|
||||
from mock import MagicMock, Mock, patch
|
||||
from typing import Callable
|
||||
|
||||
# Prevent importing modules w/Raspi hardware dependencies.
|
||||
# These must precede any SeedSigner imports.
|
||||
sys.modules['seedsigner.gui.renderer'] = MagicMock()
|
||||
sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.gui.toast'] = MagicMock()
|
||||
sys.modules['seedsigner.views.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.buttons'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
|
||||
from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.views.view import Destination, MainMenuView, View
|
||||
|
||||
|
||||
|
||||
|
||||
class BaseTest:
|
||||
|
||||
class MockMicroSD(Mock):
|
||||
"""
|
||||
A test suite-friendly replacement for `MicroSD` that gives a test explicit
|
||||
control over the reported state of the SD card.
|
||||
"""
|
||||
# Tests are free to directly manipulate this attribute as needed (it's reset to
|
||||
# True before each test in `BaseTest.setup_method()`).
|
||||
is_inserted: bool = True
|
||||
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
# Ensure there are no on-disk artifacts after running tests.
|
||||
@@ -29,6 +41,13 @@ class BaseTest:
|
||||
# Mock out the loading screen so it can't spawn. View classes must import locally!
|
||||
patch('seedsigner.gui.screens.screen.LoadingScreenThread').start()
|
||||
|
||||
# Instantiate the mocked MicroSD; hold on to the instance so tests can manipulate
|
||||
# it later.
|
||||
cls.mock_microsd = BaseTest.MockMicroSD()
|
||||
|
||||
# And mock it over `MicroSD`'s instance
|
||||
MicroSD.get_instance = Mock(return_value=cls.mock_microsd)
|
||||
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
@@ -61,11 +80,12 @@ class BaseTest:
|
||||
|
||||
|
||||
def setup_method(self):
|
||||
""" Guarantee a clean/default Controller and Settings state for each test case """
|
||||
""" Guarantee a clean/default Controller, Settings, & MicroSD state for each test case """
|
||||
BaseTest.reset_controller()
|
||||
BaseTest.reset_settings()
|
||||
self.controller = Controller.get_instance()
|
||||
self.settings = Settings.get_instance()
|
||||
self.mock_microsd.is_inserted = True
|
||||
|
||||
|
||||
def teardown_method(self):
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import embit
|
||||
import os
|
||||
import sys
|
||||
from mock import Mock, MagicMock
|
||||
import time
|
||||
from mock import Mock, patch, MagicMock
|
||||
from seedsigner.helpers import embit_utils
|
||||
|
||||
from seedsigner.models.settings import Settings
|
||||
|
||||
|
||||
# Prevent importing modules w/Raspi hardware dependencies.
|
||||
# These must precede any SeedSigner imports.
|
||||
sys.modules['seedsigner.hardware.ST7789'] = MagicMock()
|
||||
sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.views.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.buttons'] = MagicMock()
|
||||
sys.modules['RPi'] = MagicMock()
|
||||
sys.modules['RPi.GPIO'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
|
||||
from seedsigner.hardware.buttons import HardwareButtons
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView,
|
||||
psbt_views, seed_views, settings_views, tools_views)
|
||||
from seedsigner.views.view import NetworkMismatchErrorView, PowerOffView, View
|
||||
from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View
|
||||
|
||||
from .utils import ScreenshotComplete, ScreenshotRenderer
|
||||
|
||||
@@ -61,6 +71,12 @@ def test_generate_screenshots(target_locale):
|
||||
controller.storage.set_pending_seed(seed_24)
|
||||
UnhandledExceptionViewFood = ["IndexError", "line 1, in some_buggy_code.py", "list index out of range"]
|
||||
|
||||
# Pending mnemonic for ToolsCalcFinalWordShowFinalWordView
|
||||
controller.storage.init_pending_mnemonic(num_words=12)
|
||||
for i, word in enumerate(mnemonic_12[:11]):
|
||||
controller.storage.update_pending_mnemonic(word=word, index=i)
|
||||
controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet)
|
||||
|
||||
# Load a PSBT into memory
|
||||
BASE64_PSBT_1 = """cHNidP8BAP06AQIAAAAC5l4E3oEjI+H0im8t/K2nLmF5iJFdKEiuQs8ESveWJKcAAAAAAP3///8iBZMRhYIq4s/LmnTmKBi79M8ITirmsbO++63evK4utwAAAAAA/f///wZYQuoDAAAAACIAIAW5jm3UnC5fyjKCUZ8LTzjENtb/ioRTaBMXeSXsB3n+bK2fCgAAAAAWABReJY7akT1+d+jx475yBRWORdBd7VxbUgUAAAAAFgAU4wj9I/jB3GjNQudNZAca+7g9R16iWtYOAAAAABYAFIotPApLZlfscg8f3ppKqO3qA5nv7BnMFAAAAAAiACAs6SGc8qv4FwuNl0G0SpMZG8ODUEk5RXiWUcuzzw5iaRSfAhMAAAAAIgAgW0f5QxQIgVCGQqKzsvfkXZjUxdFop5sfez6Pt8mUbmZ1AgAAAAEAkgIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BQIRAgEB/////wJAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQErQL5AJQAAAAAiACCET6KNi75K8K4a2BYS4ZT+N4s8WwOBKOmOohRYkGHV0QEFR1EhArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEIQNDxn7PjUzvsP6KYw4s7dmoZE0qO1K6MaM+2ScRZ7hyxFKuIgYCsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEHCK94akwAACAAQAAgAAAAIACAACAAAAAAAMAAAAAAQCSAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8FAhACAQH/////AkC+QCUAAAAAIgAghE+ijYu+SvCuGtgWEuGU/jeLPFsDgSjpjqIUWJBh1dEAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QAAAAABAStAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAQVHUSECsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQhA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEUq4iBgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDQ8Z+z41M77D+imMOLO3ZqGRNKjtSujGjPtknEWe4csQcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAABAUdRIQJ5XLCBS0hdo4NANq4lNhimzhyHj7dvObmPAwNj8L2xASEC9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI1SriICAnlcsIFLSF2jg0A2riU2GKbOHIePt285uY8DA2PwvbEBHCK94akwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAAAAAAEBR1EhAgpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrIQOwLg0emiQbbxafIh69Xjtpj4eclsMhKq1y/7vYDdE7LVKuIgICCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ouscc8XaCjAAAIABAACAAAAAgAIAAIAAAAAABQAAACICA7AuDR6aJBtvFp8iHr1eO2mPh5yWwyEqrXL/u9gN0TstHCK94akwAACAAQAAgAAAAIACAACAAAAAAAUAAAAAAQFHUSECk50GLh/YhZaLJkDq/dugU3H/WvE6rTgQuY6N57pI4ykhA/H8MdLVP9SA/Hg8l3hvibSaC1bCBzwz7kTW+rsEZ8uFUq4iAgKTnQYuH9iFlosmQOr926BTcf9a8TqtOBC5jo3nukjjKRxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAGAAAAIgID8fwx0tU/1ID8eDyXeG+JtJoLVsIHPDPuRNb6uwRny4UcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAABgAAAAA="""
|
||||
decoder = DecodeQR()
|
||||
@@ -73,10 +89,12 @@ def test_generate_screenshots(target_locale):
|
||||
controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR)
|
||||
|
||||
# Message signing data
|
||||
derivation_path = "m/84h/0h/0h/0/0"
|
||||
controller.sign_message_data = {
|
||||
"seed_num": 0,
|
||||
"derivation_path": "m/84h/0h/0h/0/0",
|
||||
"derivation_path": derivation_path,
|
||||
"message": "I attest that I control this bitcoin address blah blah blah",
|
||||
"addr_format": embit_utils.parse_derivation_path(derivation_path)
|
||||
}
|
||||
|
||||
# Automatically populate all Settings options Views
|
||||
@@ -92,22 +110,20 @@ def test_generate_screenshots(target_locale):
|
||||
continue
|
||||
|
||||
settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
||||
settings_views_list.append(settings_views.IOTestView)
|
||||
settings_views_list.append(settings_views.DonateView)
|
||||
|
||||
|
||||
settingsqr_data_persistent = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
settingsqr_data_not_persistent = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
|
||||
screenshot_sections = {
|
||||
"Main Menu Views": [
|
||||
MainMenuView,
|
||||
(MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_removed', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED)),
|
||||
(MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_inserted', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED)),
|
||||
(MainMenuView, {}, 'MainMenuView_RemoveSDCardToast', RemoveSDCardToastManagerThread(activation_delay=0)),
|
||||
PowerOptionsView,
|
||||
RestartView,
|
||||
PowerOffView,
|
||||
NotYetImplementedView,
|
||||
(UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)),
|
||||
(settings_views.SettingsIngestSettingsQRView, dict(data="settings::v1 name=factory_reset")),
|
||||
NetworkMismatchErrorView,
|
||||
|
||||
|
||||
],
|
||||
"Seed Views": [
|
||||
seed_views.SeedsMenuView,
|
||||
@@ -191,31 +207,61 @@ def test_generate_screenshots(target_locale):
|
||||
tools_views.ToolsCalcFinalWordNumWordsView,
|
||||
tools_views.ToolsCalcFinalWordFinalizePromptView,
|
||||
tools_views.ToolsCalcFinalWordCoinFlipsView,
|
||||
#(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips=3)),
|
||||
(tools_views.ToolsCalcFinalWordShowFinalWordView, {}, "ToolsCalcFinalWordShowFinalWordView_pick_word"),
|
||||
(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), "ToolsCalcFinalWordShowFinalWordView_coin_flips"),
|
||||
#tools_views.ToolsCalcFinalWordDoneView,
|
||||
tools_views.ToolsAddressExplorerSelectSourceView,
|
||||
tools_views.ToolsAddressExplorerAddressTypeView,
|
||||
tools_views.ToolsAddressExplorerAddressListView,
|
||||
#tools_views.ToolsAddressExplorerAddressView,
|
||||
],
|
||||
"Settings Views": settings_views_list,
|
||||
"Settings Views": settings_views_list + [
|
||||
settings_views.IOTestView,
|
||||
settings_views.DonateView,
|
||||
(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), "SettingsIngestSettingsQRView_persistent"),
|
||||
(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), "SettingsIngestSettingsQRView_not_persistent"),
|
||||
],
|
||||
"Misc Error Views": [
|
||||
NotYetImplementedView,
|
||||
(UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)),
|
||||
NetworkMismatchErrorView,
|
||||
(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
|
||||
(ErrorView, dict(
|
||||
title="Error",
|
||||
status_headline="Unknown QR Type",
|
||||
text="QRCode is invalid or is a data format not yet supported.",
|
||||
button_text="Back",
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
readme = f"""# SeedSigner Screenshots\n"""
|
||||
|
||||
def screencap_view(view_cls: View, view_name: str, view_args: dict={}):
|
||||
def screencap_view(view_cls: View, view_name: str, view_args: dict={}, toast_thread: BaseToastOverlayManagerThread = None):
|
||||
screenshot_renderer.set_screenshot_filename(f"{view_name}.png")
|
||||
try:
|
||||
print(f"Running {view_name}")
|
||||
view_cls(**view_args).run()
|
||||
try:
|
||||
view_cls(**view_args).run()
|
||||
except ScreenshotComplete:
|
||||
if toast_thread is not None:
|
||||
controller.activate_toast(toast_thread)
|
||||
while controller.toast_notification_thread.is_alive():
|
||||
time.sleep(0.1)
|
||||
raise ScreenshotComplete()
|
||||
except ScreenshotComplete:
|
||||
# Slightly hacky way to exit ScreenshotRenderer as expected
|
||||
pass
|
||||
print(f"Completed {view_name}")
|
||||
except Exception as e:
|
||||
# Something else went wrong
|
||||
print(repr(e))
|
||||
from traceback import print_exc
|
||||
print_exc()
|
||||
raise e
|
||||
finally:
|
||||
if toast_thread:
|
||||
toast_thread.stop()
|
||||
|
||||
|
||||
for section_name, screenshot_list in screenshot_sections.items():
|
||||
subdir = section_name.lower().replace(" ", "_")
|
||||
@@ -231,12 +277,15 @@ def test_generate_screenshots(target_locale):
|
||||
view_name = view_cls.__name__
|
||||
elif len(screenshot) == 3:
|
||||
view_cls, view_args, view_name = screenshot
|
||||
elif len(screenshot) == 4:
|
||||
view_cls, view_args, view_name, toast_thread = screenshot
|
||||
else:
|
||||
view_cls = screenshot
|
||||
view_args = {}
|
||||
view_name = view_cls.__name__
|
||||
toast_thread = None
|
||||
|
||||
screencap_view(view_cls, view_name, view_args)
|
||||
screencap_view(view_cls, view_name, view_args, toast_thread=toast_thread)
|
||||
readme += """ <table align="left" style="border: 1px solid gray;">"""
|
||||
readme += f"""<tr><td align="center">{view_name}<br/><br/><img src="{subdir}/{view_name}.png"></td></tr>"""
|
||||
readme += """</table>\n"""
|
||||
|
||||
+26
-16
@@ -331,12 +331,12 @@ def test_parse_derivation_path():
|
||||
derivation_path = "m/84'/0'/0'/0/0"
|
||||
|
||||
result = embit_utils.parse_derivation_path(derivation_path)
|
||||
assert(result["script_type"] == SC.NATIVE_SEGWIT)
|
||||
assert(result["network"] == SC.MAINNET)
|
||||
assert result["script_type"] == SC.NATIVE_SEGWIT
|
||||
assert result["network"] == SC.MAINNET
|
||||
|
||||
result = embit_utils.parse_derivation_path(derivation_path.replace("'", "h"))
|
||||
assert(result["script_type"] == SC.NATIVE_SEGWIT)
|
||||
assert(result["network"] == SC.MAINNET)
|
||||
assert result["script_type"] == SC.NATIVE_SEGWIT
|
||||
assert result["network"] == SC.MAINNET
|
||||
|
||||
# Now exhaustively test supported permutations
|
||||
vectors_args = {
|
||||
@@ -361,7 +361,7 @@ def test_parse_derivation_path():
|
||||
(SC.TESTNET, SC.TAPROOT, True): "m/86'/1'/0'/1/5",
|
||||
(SC.REGTEST, SC.TAPROOT, True): "m/86'/1'/0'/1/5",
|
||||
|
||||
# Try a typical custom derivation path (Unchained Capital)
|
||||
# Try a typical custom derivation path (Unchained vault keys)
|
||||
(SC.MAINNET, SC.CUSTOM_DERIVATION, False): "m/45'/0'/0'/0/5",
|
||||
(SC.TESTNET, SC.CUSTOM_DERIVATION, False): "m/45'/1'/0'/0/5",
|
||||
(SC.REGTEST, SC.CUSTOM_DERIVATION, False): "m/45'/1'/0'/0/5",
|
||||
@@ -369,24 +369,34 @@ def test_parse_derivation_path():
|
||||
(SC.TESTNET, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5",
|
||||
(SC.REGTEST, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5",
|
||||
|
||||
# CRAZY custom derivation path
|
||||
(None, SC.CUSTOM_DERIVATION, False): "m/879345978543'/908327034508534983495'/9085098430894380959043'/0/5",
|
||||
# CRAZY custom derivation paths
|
||||
(None, SC.CUSTOM_DERIVATION, False, 5): "m/123'/9083270/9083270/9083270/9083270/0/5",
|
||||
|
||||
# non-standard change and/or index
|
||||
(None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78/5",
|
||||
(None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78'/5",
|
||||
(None, SC.CUSTOM_DERIVATION, None, None): "m/9'/78'/5'",
|
||||
(None, SC.CUSTOM_DERIVATION, False, None): "m/9'/0/5'",
|
||||
}
|
||||
|
||||
for expected_result, derivation_path in vectors_args.items():
|
||||
actual_result = embit_utils.parse_derivation_path(derivation_path)
|
||||
|
||||
if expected_result[0] == SC.MAINNET:
|
||||
assert(actual_result["network"] == expected_result[0])
|
||||
assert(actual_result["clean_match"] is True)
|
||||
assert actual_result["network"] == expected_result[0]
|
||||
assert actual_result["clean_match"] is True
|
||||
elif expected_result[0] is None:
|
||||
assert(actual_result["network"] is None)
|
||||
assert(actual_result["clean_match"] is False)
|
||||
assert actual_result["network"] is None
|
||||
assert actual_result["clean_match"] is False
|
||||
else:
|
||||
# Testnet and regtest are returned as a list since the parser can't tell which is intended
|
||||
assert(expected_result[0] in actual_result["network"])
|
||||
assert(actual_result["clean_match"] is True)
|
||||
assert expected_result[0] in actual_result["network"]
|
||||
assert actual_result["clean_match"] is True
|
||||
|
||||
assert(actual_result["script_type"] == expected_result[1])
|
||||
assert(actual_result["is_change"] == expected_result[2])
|
||||
assert(actual_result["index"] == int(derivation_path.split("/")[-1]))
|
||||
assert actual_result["script_type"] == expected_result[1]
|
||||
assert actual_result["is_change"] == expected_result[2]
|
||||
|
||||
if len(expected_result) == 4:
|
||||
assert actual_result["index"] == expected_result[3]
|
||||
else:
|
||||
assert actual_result["index"] == int(derivation_path.split("/")[-1])
|
||||
|
||||
@@ -54,3 +54,40 @@ class TestPSBTFlows(FlowTest):
|
||||
FlowStep(psbt_views.PSBTSignedQRDisplayView),
|
||||
FlowStep(MainMenuView)
|
||||
])
|
||||
|
||||
def test_scan_multisig_psbt_seed_already_signed_flow(self):
|
||||
|
||||
def load_psbt_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("cHNidP8BAIkCAAAAAc9dCSh2RcRPfHaT5bNVBpbg0jAekRLqOK+bpN/QA0jeAAAAAAD9////AtAHAAAAAAAAIlEg24shYsV3IRCzlgmMKjAsR4Ad9tX896z7zDAi5q0TU9H3CgAAAAAAACIAIByGQg/VP2aRID62ty40E64HYZeRRsKRGLt8J/76R6stQ04FAE8BBDWHzwSLLGdzgAAAAq3q6nR20JnHR+vKrBQdWxN9C7xU8zNX942mVF7AQpl2ArrdLwVlkGxaatQJ4wwkvypNBKbwOq9hXGLNlKi7rZWAFDUxzXUwAACAAQAAgAAAAIACAACATwEENYfPBHOCZmWAAAACmH6KTXIny0vueRgQFBq4M6oMuG8f1QM0I/RzKQ03bCgCHrF0fyUtV0+FD2N34u/woqb8MAt/o+7Ed58RddhY8zYUCUjSaDAAAIABAACAAAAAgAIAAIAAAQEriBMAAAAAAAAiACBY4WsjDgJXLj3VW222jU1tkIIhT26ce/2efH73BWGGBiICAqyfkrdUO662QBrdvJcSOZMFxniD7M1awm9U0Kb5XCm5RzBEAiAPkQTY84YjFFkpD6MI2cc5rJySqws5fsTQA/8XEZFpbAIgTNVykbEH4Z7bqyzhhy6lty0K8rtCUDCaHNv+47NNIWgBAQMEAQAAAAEFR1IhApL4XO+VE1pPYn5wnRFyJQKVSc9TX2dO6KIBH6jwvgPaIQKsn5K3VDuutkAa3byXEjmTBcZ4g+zNWsJvVNCm+VwpuVKuIgYCkvhc75UTWk9ifnCdEXIlApVJz1NfZ07oogEfqPC+A9ocNTHNdTAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGAqyfkrdUO662QBrdvJcSOZMFxniD7M1awm9U0Kb5XCm5HAlI0mgwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAAEBR1IhApYXaczuYbBM/A+EH639Ir2yIB4PxL46dK/I1V1O9aHgIQLa02HCI/+EP+9gGpxHskjYWFN5hZzXY7RRvwV4UF42ylKuIgIClhdpzO5hsEz8D4Qfrf0ivbIgHg/Evjp0r8jVXU71oeAcNTHNdTAAAIABAACAAAAAgAIAAIABAAAAAAAAACICAtrTYcIj/4Q/72AanEeySNhYU3mFnNdjtFG/BXhQXjbKHAlI0mgwAACAAQAAgAAAAIACAACAAQAAAAAAAAAA")
|
||||
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("073318950739065415961602009907670428187212261116")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored
|
||||
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(psbt_views.PSBTOverviewView),
|
||||
FlowStep(psbt_views.PSBTMathView),
|
||||
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0),
|
||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
|
||||
FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED),
|
||||
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="abc"),
|
||||
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(psbt_views.PSBTOverviewView),
|
||||
FlowStep(psbt_views.PSBTMathView),
|
||||
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0),
|
||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
|
||||
FlowStep(psbt_views.PSBTSignedQRDisplayView),
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
|
||||
|
||||
+84
-15
@@ -1,3 +1,4 @@
|
||||
from typing import Callable
|
||||
import pytest
|
||||
|
||||
# Must import test base before the Controller
|
||||
@@ -5,10 +6,14 @@ from base import BaseTest, FlowTest, FlowStep
|
||||
from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.views.view import MainMenuView, View, NetworkMismatchErrorView
|
||||
from seedsigner.views import seed_views, scan_views, settings_views
|
||||
from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView
|
||||
from seedsigner.views import seed_views, scan_views, settings_views, tools_views
|
||||
|
||||
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("0000" * 11 + "0003")
|
||||
|
||||
|
||||
|
||||
@@ -19,9 +24,6 @@ class TestSeedFlows(FlowTest):
|
||||
Selecting "Scan" from the MainMenuView and scanning a SeedQR should enter the
|
||||
Finalize Seed flow and end at the SeedOptionsView.
|
||||
"""
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("0000" * 11 + "0003")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
|
||||
@@ -36,6 +38,7 @@ class TestSeedFlows(FlowTest):
|
||||
the SeedOptionsView.
|
||||
"""
|
||||
def test_with_mnemonic(mnemonic):
|
||||
Settings.HOSTNAME = "not seedsigner-os"
|
||||
sequence = [
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
|
||||
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
|
||||
@@ -418,19 +421,85 @@ class TestMessageSigningFlows(FlowTest):
|
||||
# Ensure message signing is enabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
# Ensure we're configured for mainnet
|
||||
def expect_network_mismatch_error(load_message: Callable):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_message), # simulate read message QR; ret val is ignored
|
||||
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
|
||||
FlowStep(NetworkMismatchErrorView),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView),
|
||||
])
|
||||
|
||||
# MAINNET settings vs TESTNET derivation path with the message
|
||||
self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.MAINNET)
|
||||
expect_network_mismatch_error(self.load_testnet_message_into_decoder)
|
||||
|
||||
# TESTNET settings vs MAINNET derivation path with the message
|
||||
self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.TESTNET)
|
||||
expect_network_mismatch_error(self.load_short_message_into_decoder)
|
||||
|
||||
# REGTEST settings vs MAINNET derivation path with the message
|
||||
self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST)
|
||||
expect_network_mismatch_error(self.load_short_message_into_decoder)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_sign_message_option_disabled(self):
|
||||
"""
|
||||
Should redirect to OptionDisabledView if a `signmessage` QR is scanned with
|
||||
message signing disabled.
|
||||
|
||||
Should offer the option to route directly to enable that settings or return to
|
||||
MainMenuView.
|
||||
"""
|
||||
# Ensure message signing is disabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__DISABLED)
|
||||
|
||||
sequence = [
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=self.load_short_message_into_decoder), # simulate read message QR; ret val is ignored
|
||||
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
|
||||
]
|
||||
|
||||
# First test routing to update the setting
|
||||
self.run_sequence(
|
||||
sequence + [
|
||||
FlowStep(OptionDisabledView, button_data_selection=OptionDisabledView.UPDATE_SETTING, is_redirect=True),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView),
|
||||
]
|
||||
)
|
||||
|
||||
# Now test exiting to Main Menu
|
||||
self.run_sequence(
|
||||
sequence + [
|
||||
FlowStep(OptionDisabledView, button_data_selection=OptionDisabledView.DONE, is_redirect=True),
|
||||
FlowStep(MainMenuView),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_sign_message_invalid_qr_flow(self):
|
||||
"""
|
||||
Should clear `Controller.resume_main_flow` and redirect to ErrorView if an
|
||||
invalid signmessage QR is scanned.
|
||||
|
||||
The error view should then forward to MainMenuView.
|
||||
"""
|
||||
# Ensure message signing is enabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
def load_invalid_signmessage_qr(view: scan_views.ScanView):
|
||||
view.decoder.add_data("this text will not make sense to the decoder")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=self.load_testnet_message_into_decoder), # simulate read message QR; ret val is ignored
|
||||
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=self.inject_mesage_as_paged_message, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedSignMessageConfirmAddressView, is_redirect=True),
|
||||
FlowStep(NetworkMismatchErrorView),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView),
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.SIGN_MESSAGE),
|
||||
FlowStep(scan_views.ScanView, before_run=load_invalid_signmessage_qr), # simulate read message QR; ret val is ignored
|
||||
FlowStep(ErrorView),
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
|
||||
assert self.controller.resume_main_flow is None
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
from mock import PropertyMock, patch
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
@@ -6,13 +9,13 @@ from base import FlowTest, FlowStep
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsDefinition, SettingsConstants
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import settings_views
|
||||
from seedsigner.views import scan_views, settings_views
|
||||
|
||||
|
||||
|
||||
class TestSettingsFlows(FlowTest):
|
||||
|
||||
def test_persistent_settings(self):
|
||||
""" Basic flow from MainMenuView to enable/disable persistent settings """
|
||||
# Which option are we testing?
|
||||
@@ -67,3 +70,69 @@ class TestSettingsFlows(FlowTest):
|
||||
FlowStep(settings_views.DonateView),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_settingsqr(self):
|
||||
"""
|
||||
Scanning a SettingsQR should present the success screen and then return to
|
||||
MainMenuView.
|
||||
"""
|
||||
def load_persistent_settingsqr_into_decoder(view: scan_views.ScanView):
|
||||
settingsqr_data_persistent: str = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
view.decoder.add_data(settingsqr_data_persistent)
|
||||
|
||||
def load_not_persistent_settingsqr_into_decoder(view: scan_views.ScanView):
|
||||
settingsqr_data_not_persistent: str = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
view.decoder.add_data(settingsqr_data_not_persistent)
|
||||
|
||||
def _run_test(initial_setting_state: str, load_settingsqr_into_decoder: Callable, expected_setting_state: str):
|
||||
self.settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, initial_setting_state)
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_settingsqr_into_decoder), # simulate read message QR; ret val is ignored
|
||||
FlowStep(settings_views.SettingsIngestSettingsQRView), # ret val is ignored
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
|
||||
assert self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == expected_setting_state
|
||||
|
||||
|
||||
# First load a SettingsQR that enables persistent settings
|
||||
self.mock_microsd.is_inserted = True
|
||||
assert MicroSD.get_instance().is_inserted is True
|
||||
|
||||
_run_test(
|
||||
initial_setting_state=SettingsConstants.OPTION__DISABLED,
|
||||
load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder,
|
||||
expected_setting_state=SettingsConstants.OPTION__ENABLED
|
||||
)
|
||||
|
||||
# Then one that disables it
|
||||
_run_test(
|
||||
initial_setting_state=SettingsConstants.OPTION__ENABLED,
|
||||
load_settingsqr_into_decoder=load_not_persistent_settingsqr_into_decoder,
|
||||
expected_setting_state=SettingsConstants.OPTION__DISABLED
|
||||
)
|
||||
|
||||
# Now try to enable persistent settings when the SD card is not inserted
|
||||
self.mock_microsd.is_inserted = False
|
||||
assert MicroSD.get_instance().is_inserted is False
|
||||
|
||||
# Have to jump through some hoops to completely simulate the SD card being
|
||||
# removed; we need Settings to restrict Persistent Settings to only allow
|
||||
# DISABLED.
|
||||
with patch('seedsigner.models.settings.Settings.HOSTNAME', new_callable=PropertyMock) as mock_hostname:
|
||||
# Must identify itself as SeedSigner OS to trigger the SD card removal logic
|
||||
mock_hostname.return_value = Settings.SEEDSIGNER_OS
|
||||
Settings.handle_microsd_state_change(MicroSD.ACTION__REMOVED)
|
||||
|
||||
selection_options = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS).selection_options
|
||||
assert len(selection_options) == 1
|
||||
assert selection_options[0][0] == SettingsConstants.OPTION__DISABLED
|
||||
assert self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
|
||||
_run_test(
|
||||
initial_setting_state=SettingsConstants.OPTION__DISABLED,
|
||||
load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder,
|
||||
expected_setting_state=SettingsConstants.OPTION__DISABLED
|
||||
)
|
||||
|
||||
@@ -65,6 +65,7 @@ class TestViewFlows(FlowTest):
|
||||
"""
|
||||
Basic flow from any arbitrary View to the UnhandledExceptionView
|
||||
"""
|
||||
Settings.HOSTNAME = "NOT seedsigner-os"
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD),
|
||||
|
||||
+12
-11
@@ -8,6 +8,7 @@ from seedsigner.models.settings_definition import SettingsConstants
|
||||
class TestSettings(BaseTest):
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
super().setup_class()
|
||||
cls.settings = Settings.get_instance()
|
||||
|
||||
|
||||
@@ -37,17 +38,17 @@ class TestSettings(BaseTest):
|
||||
|
||||
# Now parse the settingsqr_data
|
||||
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||
assert(config_name == settings_name)
|
||||
assert config_name == settings_name
|
||||
self.settings.update(new_settings=settings_update_dict)
|
||||
|
||||
# Now verify that the settings were updated correctly
|
||||
assert(self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED)
|
||||
assert(self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED)
|
||||
assert self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED
|
||||
assert self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
|
||||
|
||||
coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)
|
||||
assert(SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators)
|
||||
assert(SettingsConstants.COORDINATOR__SPARROW in coordinators)
|
||||
assert(SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators)
|
||||
assert SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators
|
||||
assert SettingsConstants.COORDINATOR__SPARROW in coordinators
|
||||
assert SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators
|
||||
|
||||
|
||||
def test_settingsqr_version(self):
|
||||
@@ -61,7 +62,7 @@ class TestSettings(BaseTest):
|
||||
settingsqr_data = "settings::v2 name=Foo"
|
||||
with pytest.raises(InvalidSettingsQRData) as e:
|
||||
Settings.parse_settingsqr(settingsqr_data)
|
||||
assert("Unsupported SettingsQR version" in str(e.value))
|
||||
assert "Unsupported SettingsQR version" in str(e.value)
|
||||
|
||||
# Should also fail if version omitted
|
||||
settingsqr_data = "settings name=Foo"
|
||||
@@ -79,8 +80,8 @@ class TestSettings(BaseTest):
|
||||
settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D"
|
||||
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||
|
||||
assert("favorite_food" not in settings_update_dict)
|
||||
assert("xpub_export" in settings_update_dict)
|
||||
assert "favorite_food" not in settings_update_dict
|
||||
assert "xpub_export" in settings_update_dict
|
||||
|
||||
# Accepts update with no Exceptions
|
||||
self.settings.update(new_settings=settings_update_dict)
|
||||
@@ -91,7 +92,7 @@ class TestSettings(BaseTest):
|
||||
settingsqr_data = "settings::v1 name=Foo xpub_export=Yep"
|
||||
with pytest.raises(InvalidSettingsQRData) as e:
|
||||
Settings.parse_settingsqr(settingsqr_data)
|
||||
assert("xpub_export" in str(e.value))
|
||||
assert "xpub_export" in str(e.value)
|
||||
|
||||
|
||||
def test_settingsqr_parses_line_break_separators(self):
|
||||
@@ -99,7 +100,7 @@ class TestSettings(BaseTest):
|
||||
settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n"
|
||||
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||
|
||||
assert(len(settings_update_dict.keys()) == 3)
|
||||
assert len(settings_update_dict.keys()) == 3
|
||||
|
||||
# Accepts update with no Exceptions
|
||||
self.settings.update(new_settings=settings_update_dict)
|
||||
|
||||
@@ -14,11 +14,11 @@ class TestSettingsQRDecoder:
|
||||
# Now parse the settings_qr_str
|
||||
decoder = DecodeQR()
|
||||
status = decoder.add_data(settings_qr_str)
|
||||
assert(decoder.is_settings)
|
||||
assert(status == DecodeQRStatus.COMPLETE)
|
||||
assert decoder.is_settings
|
||||
assert status == DecodeQRStatus.COMPLETE
|
||||
|
||||
data = decoder.get_settings_data()
|
||||
assert(data == settings_qr_str)
|
||||
assert data == settings_qr_str
|
||||
|
||||
|
||||
def test_settingsqr_version(self):
|
||||
@@ -26,4 +26,4 @@ class TestSettingsQRDecoder:
|
||||
settings_qr_str = "name=Foo"
|
||||
decoder = DecodeQR()
|
||||
status = decoder.add_data(settings_qr_str)
|
||||
assert(status == DecodeQRStatus.INVALID)
|
||||
assert status == DecodeQRStatus.INVALID
|
||||
|
||||
Reference in New Issue
Block a user