Merge branch 'dev' into Version-bump-in-verification-instructions

This commit is contained in:
Nick Klockenga
2025-01-31 09:21:28 -05:00
committed by GitHub
29 changed files with 673 additions and 530 deletions
+28 -12
View File
@@ -31,7 +31,7 @@ jobs:
target: [ "pi0", "pi2", "pi02w", "pi4" ]
steps:
- name: checkout seedsigner-os
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: "seedsigner/seedsigner-os"
# use the os-ref input parameter in case of workflow_dispatch or default to main in case of cron triggers
@@ -42,7 +42,7 @@ jobs:
fetch-depth: 0
- name: checkout source
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch)
path: "seedsigner-os/opt/rootfs-overlay/opt"
@@ -78,7 +78,7 @@ jobs:
ls -la src
- name: restore build cache
uses: actions/cache@v3
uses: actions/cache@v4
# Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour,
# while consuming ~850 MB storage space).
with:
@@ -89,10 +89,25 @@ jobs:
restore-keys: |
build-cache-${{ matrix.target }}-
- name: Create build container
run: |
cd seedsigner-os
docker build -t seedsigner-os-build .
- name: build
run: |
cd seedsigner-os/opt
./build.sh --${{ matrix.target }} --skip-repo --no-clean
mkdir -p \
~/.buildroot-ccache \
seedsigner-os/buildroot_dl
docker run \
--rm \
-v "$(pwd)/seedsigner-os/opt:/opt" \
-v "$(pwd)/seedsigner-os/images:/images" \
-v "$(pwd)/seedsigner-os/buildroot_dl:/buildroot_dl" \
-v "${HOME}/.buildroot-ccache:/root/.buildroot-ccache" \
seedsigner-os-build \
--${{ matrix.target }} --skip-repo --no-clean
sudo chown -R $USER:$USER seedsigner-os/images seedsigner-os/buildroot_dl ~/.buildroot-ccache/
- name: list image (before rename)
run: |
@@ -113,9 +128,9 @@ jobs:
ls -la seedsigner-os/images
- name: upload images
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: seedsigner_os_images
name: seedsigner_os_images-${{ matrix.target }}
path: "seedsigner-os/images/*.img"
if-no-files-found: error
# maximum 90 days retention
@@ -127,14 +142,13 @@ jobs:
needs: build
steps:
- name: download images
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: seedsigner_os_images
path: images
- name: list images
run: |
ls -la images
ls -lRa images
- name: get seedsigner latest commit hash
id: get-seedsigner-hash
@@ -145,12 +159,14 @@ jobs:
- name: write sha256sum
run: |
cd images
# each downloaded image is in its own subfolder
find . -name "*.img" -exec mv {} . \;
sha256sum *.img > seedsigner_os.${{ env.source_hash }}.sha256
- name: upload checksums
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: seedsigner_os_images
name: seedsigner_os_images_sha256
path: "images/*.sha256"
if-no-files-found: error
# maximum 90 days retention
+4 -4
View File
@@ -26,12 +26,12 @@ jobs:
python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
# Needs to also pull the seedsigner-translations repo
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -63,9 +63,9 @@ jobs:
- name: Coverage report
run: coverage report
- name: Archive CI Artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ci-artifacts
name: ci-artifacts-${{ matrix.python-version }}
path: artifacts/**
retention-days: 10
# Upload also when tests fail. The workflow result (red/green) will
+1 -1
View File
@@ -1,7 +1,7 @@
[submodule "src/seedsigner/resources/seedsigner-translations"]
path = src/seedsigner/resources/seedsigner-translations
url = https://github.com/SeedSigner/seedsigner-translations.git
branch = 0.8.5-rc1
branch = dev
[submodule "seedsigner-screenshots"]
path = seedsigner-screenshots
url = https://github.com/SeedSigner/seedsigner-screenshots.git
+66 -71
View File
@@ -1,14 +1,14 @@
# Translations template for seedsigner.
# Copyright (C) 2024 ORGANIZATION
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the seedsigner project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: seedsigner 0.8.5\n"
"Project-Id-Version: seedsigner 0.8.5-rc1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-11-03 14:15-0600\n"
"POT-Creation-Date: 2025-01-28 13:28-0600\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -36,16 +36,6 @@ msgstr ""
msgid "sats"
msgstr ""
#. The abbreviated label for the special key <del> on a standard keyboard.
#: src/seedsigner/gui/keyboard.py
msgid "del"
msgstr ""
#. The abbreviated label for the special key <space> on a standard keyboard.
#: src/seedsigner/gui/keyboard.py
msgid "space"
msgstr ""
#: src/seedsigner/gui/toast.py
msgid ""
"You can remove\n"
@@ -121,9 +111,8 @@ msgid "OP_RETURN"
msgstr ""
#. Label for a change output in the PSBT Overview flow diagram
#. Used in a sentence describing the address type (change or receive)
#: src/seedsigner/gui/screens/psbt_screens.py
#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py
#: src/seedsigner/views/psbt_views.py
msgid "change"
msgstr ""
@@ -204,15 +193,19 @@ msgstr ""
msgid "Darker"
msgstr ""
#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py
#: src/seedsigner/gui/screens/screen.py
#: src/seedsigner/gui/screens/seed_screens.py
#: src/seedsigner/views/seed_views.py
msgid "Success!"
msgstr ""
#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py
#: src/seedsigner/gui/screens/screen.py
#: src/seedsigner/gui/screens/seed_screens.py
#: src/seedsigner/views/seed_views.py
msgid "OK"
msgstr ""
#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py
#: src/seedsigner/gui/screens/screen.py
msgid "Caution"
msgstr ""
@@ -228,6 +221,11 @@ msgstr ""
msgid "Classified Info!"
msgstr ""
#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/scan_views.py
#: src/seedsigner/views/view.py
msgid "Error"
msgstr ""
#: src/seedsigner/gui/screens/screen.py
msgid "Restarting"
msgstr ""
@@ -391,6 +389,25 @@ msgstr ""
msgid "Checking address {}"
msgstr ""
#: src/seedsigner/gui/screens/seed_screens.py
msgid "Address Verified"
msgstr ""
#. Describes the address type (change or receive)
#: src/seedsigner/gui/screens/seed_screens.py
msgid "change address"
msgstr ""
#. Describes the address type (change or receive)
#: src/seedsigner/gui/screens/seed_screens.py
msgid "receive address"
msgstr ""
#. Describes the address index (e.g. "index 7")
#: src/seedsigner/gui/screens/seed_screens.py
msgid "index {}"
msgstr ""
#: src/seedsigner/gui/screens/seed_screens.py
msgid "Multisig Verification"
msgstr ""
@@ -494,7 +511,7 @@ msgid "Home"
msgstr ""
#: src/seedsigner/gui/screens/tools_screens.py
msgid "click joystick"
msgid "click a button"
msgstr ""
#. A prompt to the user to either accept or reshoot the image
@@ -517,11 +534,12 @@ msgstr ""
msgid "Build Final Word"
msgstr ""
#. Number of BIP-39 seed words, and the entropy -- in bits, contained within.
#. Final word calc. `mnemonic_length` = 12 or 24. `num_bits` = 7 or 3 (bits of
#. entropy in final word).
#: src/seedsigner/gui/screens/tools_screens.py
msgid ""
"The {}th word is built from {} more entropy bits plus auto-calculated "
"checksum."
"The {mnemonic_length}th word is built from {num_bits} more entropy bits "
"plus auto-calculated checksum."
msgstr ""
#. current coin-flip number vs total flips (e.g. flip 3 of 4)
@@ -744,6 +762,10 @@ msgstr ""
msgid "BIP-85 child seeds"
msgstr ""
#: src/seedsigner/models/settings_definition.py
msgid "Electrum seeds"
msgstr ""
#: src/seedsigner/models/settings_definition.py
msgid "Native Segwit only"
msgstr ""
@@ -874,13 +896,7 @@ msgstr ""
#. Variable is either "change" or "self-transfer".
#: src/seedsigner/views/psbt_views.py
msgid ""
"PSBT's {} address could not be verified with your multisig wallet "
"descriptor."
msgstr ""
#: src/seedsigner/views/psbt_views.py
msgid "Suspicious PSBT"
msgid "PSBT's {} address could not be verified from wallet descriptor."
msgstr ""
#. Variable is either "change" or "self-transfer".
@@ -888,6 +904,10 @@ msgstr ""
msgid "PSBT's {} address could not be generated from your seed."
msgstr ""
#: src/seedsigner/views/psbt_views.py
msgid "Suspicious PSBT"
msgstr ""
#: src/seedsigner/views/psbt_views.py
msgid "Address Verification Failed"
msgstr ""
@@ -928,23 +948,6 @@ msgstr ""
msgid "Wrong QR Type"
msgstr ""
#: src/seedsigner/views/scan_views.py src/seedsigner/views/view.py
msgid "Error"
msgstr ""
#: src/seedsigner/views/scan_views.py
msgid "Unknown QR Type"
msgstr ""
#: src/seedsigner/views/scan_views.py
msgid "QRCode is invalid or is a data format not yet supported."
msgstr ""
#: src/seedsigner/views/scan_views.py src/seedsigner/views/seed_views.py
#: src/seedsigner/views/view.py
msgid "Done"
msgstr ""
#: src/seedsigner/views/scan_views.py src/seedsigner/views/seed_views.py
msgid "Scan PSBT"
msgstr ""
@@ -977,6 +980,19 @@ msgstr ""
msgid "Expected an address QR"
msgstr ""
#: src/seedsigner/views/scan_views.py
msgid "Unknown QR Type"
msgstr ""
#: src/seedsigner/views/scan_views.py
msgid "QRCode is invalid or is a data format not yet supported."
msgstr ""
#: src/seedsigner/views/scan_views.py src/seedsigner/views/seed_views.py
#: src/seedsigner/views/view.py
msgid "Done"
msgstr ""
#. This is on the opening splash screen, displayed above the HRF logo
#: src/seedsigner/views/screensaver.py
msgid "With support from:"
@@ -1262,30 +1278,6 @@ msgstr ""
msgid "Can't validate a single sig addr without specifying a seed"
msgstr ""
#: src/seedsigner/views/seed_views.py
msgid "multisig"
msgstr ""
#. Inserts the seed fingerprint
#: src/seedsigner/views/seed_views.py
msgid "seed {}"
msgstr ""
#. Used in a sentence describing the address type (change or receive)
#: src/seedsigner/views/seed_views.py
msgid "receive"
msgstr ""
#. Address verification success message (e.g. "bc1qabc = seed 12345678's
#. receive address #0.")
#: src/seedsigner/views/seed_views.py
msgid "{} = {}'s {} address #{}."
msgstr ""
#: src/seedsigner/views/seed_views.py
msgid "Address Verified"
msgstr ""
#: src/seedsigner/views/seed_views.py
msgid "Scan Descriptor"
msgstr ""
@@ -1456,10 +1448,13 @@ msgstr ""
msgid "Back to Main Menu"
msgstr ""
#. The network setting (mainnet/testnet/regtest) doesn't match the provided
#. derivation path
#: src/seedsigner/views/view.py
msgid "Network Mismatch"
msgstr ""
#. Button option to alter a setting
#: src/seedsigner/views/view.py
msgid "Change Setting"
msgstr ""
+1 -1
View File
@@ -13,7 +13,7 @@ description = "Build an offline, airgapped Bitcoin signing device for less than
name = "seedsigner"
readme = "README.md"
requires-python = ">=3.10"
version = "0.8.5-rc1"
version = "0.8.5"
[project.urls]
"Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues"
+1 -1
View File
@@ -99,7 +99,7 @@ class Controller(Singleton):
rather than at the top in order avoid circular imports.
"""
VERSION = "0.8.5-rc1"
VERSION = "0.8.5"
# Declare class member vars with type hints to enable richer IDE support throughout
# the code.
+26 -29
View File
@@ -27,17 +27,18 @@ class GUIConstants:
COMPONENT_PADDING = 8
LIST_ITEM_PADDING = 4
BACKGROUND_COLOR = "black"
BACKGROUND_COLOR = "#000000"
INACTIVE_COLOR = "#414141"
ACCENT_COLOR = "#FF9F0A" # Active Color
WARNING_COLOR = "#FFD60A"
DIRE_WARNING_COLOR = "#FF453A"
DIRE_WARNING_COLOR = "#FF5700"
ERROR_COLOR = "#FF1B0A"
SUCCESS_COLOR = "#30D158"
INFO_COLOR = "#0084FF"
ACCENT_COLOR = "#FF9F0A"
INFO_COLOR = "#409CFF"
BITCOIN_ORANGE = "#FF9416"
TESTNET_COLOR = "#00F100"
REGTEST_COLOR = "#00CAF1"
GREEN_INDICATOR_COLOR = "#00FF00"
INACTIVE_COLOR = "#414141"
ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900"
ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons"
@@ -215,27 +216,34 @@ class SeedSignerIconConstants:
RESTART = "\ue911"
# Messaging icons
ERROR = "\ue912"
INFO = "\ue912"
SUCCESS = "\ue913"
WARNING = "\ue914"
ERROR = "\ue915"
# Informational icons
ADDRESS = "\ue915"
CHANGE = "\ue916"
DERIVATION = "\ue917"
FEE = "\ue918"
FINGERPRINT = "\ue919"
PASSPHRASE = "\ue91a"
ADDRESS = "\ue916"
CHANGE = "\ue917"
DERIVATION = "\ue918"
FEE = "\ue919"
FINGERPRINT = "\ue91a"
PASSPHRASE = "\ue91b"
# Misc icons
BITCOIN = "\ue91b"
BITCOIN_ALT = "\ue91c"
BRIGHTNESS = "\ue91d"
MICROSD = "\ue91e"
QRCODE = "\ue91f"
BITCOIN = "\ue91c"
BITCOIN_ALT = "\ue91d"
BRIGHTNESS = "\ue91e"
MICROSD = "\ue91f"
QRCODE = "\ue920"
SIGN = "\ue921"
# Input icons
DELETE = "\ue922"
SPACE = "\ue923"
# Must be updated whenever new icons are added. See usage in `Icon` class below.
MIN_VALUE = SCAN
MAX_VALUE = QRCODE
MAX_VALUE = SPACE
@@ -273,17 +281,6 @@ def calc_text_centering(font: ImageFont,
def load_icon(icon_name: str, load_selected_variant: bool = False):
icon_url = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "icons", icon_name)
icon = Image.open(icon_url + ".png").convert("RGB")
if not load_selected_variant:
return icon
else:
icon_selected = Image.open(icon_url + "_selected.png").convert("RGB")
return (icon, icon_selected)
def load_image(image_name: str) -> Image.Image:
image_url = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "img", image_name)
image = Image.open(image_url).convert("RGB")
+32 -29
View File
@@ -3,7 +3,7 @@ from PIL import Image, ImageDraw, ImageFont
from typing import Tuple
from gettext import gettext as _
from seedsigner.gui.components import Fonts, GUIConstants
from seedsigner.gui.components import Fonts, GUIConstants, SeedSignerIconConstants
from seedsigner.hardware.buttons import HardwareButtonsConstants
@@ -30,58 +30,55 @@ class Keyboard:
ENTER_RIGHT = "enter_right"
REGULAR_KEY_FONT = "regular"
COMPACT_KEY_FONT = "compact"
ICON_KEY_FONT = GUIConstants.ICON_FONT_NAME__SEEDSIGNER
# TRANSLATOR_NOTE: The abbreviated label for the special key <del> on a standard keyboard.
del_label = _("del")
KEY_BACKSPACE = {
"code": "DEL",
"letter": del_label,
"font": COMPACT_KEY_FONT,
"letter": SeedSignerIconConstants.DELETE,
"font": ICON_KEY_FONT,
"size": 2,
}
# TRANSLATOR_NOTE: The abbreviated label for the special key <space> on a standard keyboard.
space_label = _("space")
KEY_SPACE = {
"code": "SPACE",
"letter": space_label,
"font": COMPACT_KEY_FONT,
"letter": SeedSignerIconConstants.SPACE,
"font": ICON_KEY_FONT,
"size": 1,
}
KEY_SPACE_2 = {
"code": "SPACE",
"letter": space_label,
"font": COMPACT_KEY_FONT,
"letter": SeedSignerIconConstants.SPACE,
"font": ICON_KEY_FONT,
"size": 2,
}
KEY_SPACE_3 = {
"code": "SPACE",
"letter": space_label,
"font": COMPACT_KEY_FONT,
"letter": SeedSignerIconConstants.SPACE,
"font": ICON_KEY_FONT,
"size": 3,
}
KEY_SPACE_4 = {
"code": "SPACE",
"letter": space_label,
"font": COMPACT_KEY_FONT,
"letter": SeedSignerIconConstants.SPACE,
"font": ICON_KEY_FONT,
"size": 4,
}
KEY_SPACE_5 = {
"code": "SPACE",
"letter": space_label,
"font": COMPACT_KEY_FONT,
"letter": SeedSignerIconConstants.SPACE,
"font": ICON_KEY_FONT,
"size": 5,
}
KEY_CURSOR_LEFT = {
"code": "CURSOR_LEFT",
"letter": "<",
"font": REGULAR_KEY_FONT,
"letter": SeedSignerIconConstants.CHEVRON_LEFT,
"font": ICON_KEY_FONT,
"size": 1,
}
KEY_CURSOR_RIGHT = {
"code": "CURSOR_RIGHT",
"letter": ">",
"font": REGULAR_KEY_FONT,
"letter": SeedSignerIconConstants.CHEVRON_RIGHT,
"font": ICON_KEY_FONT,
"size": 1,
}
KEY_PREVIOUS_PAGE = {
@@ -123,9 +120,11 @@ class Keyboard:
def render_key(self):
font = self.keyboard.font
text_height = self.keyboard.text_height
if self.is_additional_key:
if Keyboard.ADDITIONAL_KEYS[self.code]["font"] == Keyboard.COMPACT_KEY_FONT:
font = self.keyboard.additonal_key_compact_font
if Keyboard.ADDITIONAL_KEYS[self.code]["font"] == Keyboard.ICON_KEY_FONT:
font = self.keyboard.icon_key_font
text_height = self.keyboard.icon_key_height
outline_color = "#333"
if not self.is_active:
@@ -159,15 +158,12 @@ class Keyboard:
radius=4
)
# Fixed-width fonts will all have same height, ignoring below baseline (e.g. "Q" or "q")
(left, top, right, bottom) = font.getbbox("X", anchor="ls")
text_height = -1 * top
self.keyboard.draw.text(
(
self.screen_x + int(self.keyboard.key_width * self.size / 2),
self.screen_y + self.keyboard.key_height - int((self.keyboard.key_height - text_height)/2)
),
_(self.letter),
self.letter,
fill=font_color,
font=font,
anchor="ms"
@@ -217,8 +213,15 @@ class Keyboard:
# Set up the rendering and state params
self.active_keys = list(self.charset)
self.icon_key_font = Fonts.get_font(GUIConstants.ICON_FONT_NAME__SEEDSIGNER, 26)
# Fixed-width fonts will all have same height, ignoring below baseline (e.g. "Q" or "q")
(left, top, right, bottom) = self.font.getbbox("X", anchor="ls")
self.text_height = -1 * top
(left, top, right, bottom) = self.icon_key_font.getbbox(SeedSignerIconConstants.DELETE + SeedSignerIconConstants.SPACE, anchor="ls")
self.icon_key_height = -1 * top
self.additonal_key_compact_font = Fonts.get_font("RobotoCondensed-Bold", 18)
self.x_start = rect[0]
self.y_start = rect[1]
self.x_gap = 2
+1 -1
View File
@@ -768,7 +768,7 @@ class PSBTFinalizeScreen(ButtonListScreen):
super().__post_init__()
icon = Icon(
icon_name=FontAwesomeIconConstants.PAPER_PLANE,
icon_name=SeedSignerIconConstants.SIGN,
icon_color=GUIConstants.INFO_COLOR,
icon_size=GUIConstants.ICON_LARGE_BUTTON_SIZE,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING
+37 -20
View File
@@ -1,3 +1,4 @@
import logging
import time
from dataclasses import dataclass, field
@@ -15,6 +16,8 @@ from seedsigner.models.encode_qr import BaseQrEncoder
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
logger = logging.getLogger(__name__)
# Must be huge numbers to avoid conflicting with the selected_button returned by the
# screens with buttons.
@@ -71,6 +74,13 @@ class BaseScreen(BaseComponent):
for t in self.get_threads():
t.stop()
for t in self.get_threads():
# Wait for each thread to stop; equivalent to `join()` but gracefully
# handles threads that were never run (necessary for screenshot generator
# compatibility, perhaps other edge cases).
while t.is_alive():
time.sleep(0.01)
def clear_screen(self):
# Clear the whole canvas
@@ -226,11 +236,7 @@ class BaseTopNavScreen(BaseScreen):
time.sleep(0.1)
continue
user_input = self.hw_inputs.wait_for(
HardwareButtonsConstants.ALL_KEYS,
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
)
user_input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS)
with self.renderer.lock:
if not self.top_nav.is_selected and user_input in [
@@ -303,7 +309,7 @@ class ButtonListScreen(BaseTopNavScreen):
if len(self.button_data) == 1:
button_list_height = button_height
else:
button_list_height = (len(self.button_data) * button_height) + (GUIConstants.COMPONENT_PADDING * (len(self.button_data) - 1))
button_list_height = (len(self.button_data) * button_height) + (GUIConstants.LIST_ITEM_PADDING * (len(self.button_data) - 1))
if self.is_bottom_list:
button_list_y = self.canvas_height - (button_list_height + GUIConstants.EDGE_PADDING)
@@ -332,7 +338,7 @@ class ButtonListScreen(BaseTopNavScreen):
active_button_label = button_option.active_button_label
else:
raise Exception("Refactor needed!")
raise Exception("Refactor to ButtonOption approach needed!")
button_kwargs = dict(
text=_(button_label), # Wrap here for just-in-time translations
@@ -447,6 +453,7 @@ class ButtonListScreen(BaseTopNavScreen):
while True:
ret = self._run_callback()
if ret is not None:
logging.info("Exiting ButtonListScreen due to _run_callback")
return ret
user_input = self.hw_inputs.wait_for(
@@ -455,9 +462,7 @@ class ButtonListScreen(BaseTopNavScreen):
HardwareButtonsConstants.KEY_DOWN,
HardwareButtonsConstants.KEY_LEFT,
HardwareButtonsConstants.KEY_RIGHT,
] + HardwareButtonsConstants.KEYS__ANYCLICK,
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
] + HardwareButtonsConstants.KEYS__ANYCLICK
)
with self.renderer.lock:
@@ -641,9 +646,7 @@ class LargeButtonScreen(BaseTopNavScreen):
HardwareButtonsConstants.KEY_DOWN,
HardwareButtonsConstants.KEY_LEFT,
HardwareButtonsConstants.KEY_RIGHT
] + HardwareButtonsConstants.KEYS__ANYCLICK,
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
] + HardwareButtonsConstants.KEYS__ANYCLICK
)
with self.renderer.lock:
@@ -858,9 +861,7 @@ class QRDisplayScreen(BaseScreen):
HardwareButtonsConstants.KEY_DOWN,
HardwareButtonsConstants.KEY_LEFT,
HardwareButtonsConstants.KEY_RIGHT,
] + HardwareButtonsConstants.KEYS__ANYCLICK,
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
] + HardwareButtonsConstants.KEYS__ANYCLICK
)
if user_input == HardwareButtonsConstants.KEY_DOWN:
# Reduce QR code background brightness
@@ -923,6 +924,7 @@ class LargeIconStatusScreen(ButtonListScreen):
self.components.append(self.warning_headline_textarea)
next_y = next_y + self.warning_headline_textarea.height
if self.text:
self.components.append(TextArea(
height=self.buttons[0].screen_y - next_y,
text=_(self.text),
@@ -1011,9 +1013,12 @@ class WarningEdgesMixin:
@dataclass
class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen):
"""
Exclamation point icon + yellow WARNING color
"""
title: str = _mft("Caution")
status_icon_name: str = SeedSignerIconConstants.WARNING
status_color: str = "yellow"
status_color: str = GUIConstants.WARNING_COLOR
status_headline: str = _mft("Privacy Leak!") # The colored text under the alert icon
button_data: list = field(default_factory=lambda: [ButtonOption("I Understand")])
@@ -1021,11 +1026,25 @@ class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen):
@dataclass
class DireWarningScreen(WarningScreen):
"""
Exclamation point icon + orange DIRE_WARNING color
"""
status_headline: str = _mft("Classified Info!") # The colored text under the alert icon
status_color: str = GUIConstants.DIRE_WARNING_COLOR
@dataclass
class ErrorScreen(WarningScreen):
"""
X icon + red ERROR color
"""
title: str = _mft("Error")
status_icon_name: str = SeedSignerIconConstants.ERROR
status_color: str = GUIConstants.ERROR_COLOR
@dataclass
class ResetScreen(BaseTopNavScreen):
def __post_init__(self):
@@ -1191,9 +1210,7 @@ class KeyboardScreen(BaseTopNavScreen):
# Start the interactive update loop
while True:
input = self.hw_inputs.wait_for(
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3],
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
)
with self.renderer.lock:
+67 -30
View File
@@ -1,26 +1,26 @@
import math
import logging
import math
import time
from dataclasses import dataclass
from gettext import gettext as _
from PIL import Image, ImageDraw, ImageFilter
from typing import List
from PIL import Image, ImageDraw, ImageFilter
from seedsigner.gui.renderer import Renderer
from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants
from seedsigner.helpers.qr import QR
from seedsigner.gui.components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton,
IconTextLine, SeedSignerIconConstants, TextArea, GUIConstants, reflow_text_into_pages)
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.gui.renderer import Renderer
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption, KeyboardScreen, WarningEdgesMixin
from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton,
IconTextLine, SeedSignerIconConstants, TextArea, GUIConstants, reflow_text_into_pages)
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants
from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption, KeyboardScreen, LargeIconStatusScreen, WarningEdgesMixin
logger = logging.getLogger(__name__)
@dataclass
class SeedMnemonicEntryScreen(BaseTopNavScreen):
initial_letters: list = None
@@ -243,11 +243,7 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
def _run(self):
while True:
input = self.hw_inputs.wait_for(
HardwareButtonsConstants.ALL_KEYS,
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY2]
)
input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS)
with self.renderer.lock:
if self.is_input_in_top_nav:
@@ -885,11 +881,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
# Start the interactive update loop
while True:
input = self.hw_inputs.wait_for(
HardwareButtonsConstants.ALL_KEYS,
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY1, HardwareButtonsConstants.KEY2, HardwareButtonsConstants.KEY3]
)
input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS)
keyboard_swap = False
@@ -1060,7 +1052,6 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
@dataclass
class SeedReviewPassphraseScreen(ButtonListScreen):
fingerprint_without: str = None
@@ -1467,13 +1458,13 @@ class SeedAddressVerificationScreen(ButtonListScreen):
def _run_callback(self):
# Exit the screen on success via a non-None value
logger.info(f"verified_index: {self.verified_index.cur_count}")
# Exit the screen on success via a non-None value.
# see: ButtonListScreen._run()
if self.verified_index.cur_count is not None:
logger.info("Screen callback returning success!")
self.threads[-1].stop()
while self.threads[-1].is_alive():
time.sleep(0.01)
# Note that the ProgressThread will have already exited on its own.
# Return a success value (anything other than None) to end the
# ButtonListScreen._run() loop.
return 1
@@ -1490,10 +1481,13 @@ class SeedAddressVerificationScreen(ButtonListScreen):
while self.keep_running:
if self.verified_index.cur_count is not None:
# This thread will detect the success state while its parent Screen
# holds in its `wait_for`. Have to trigger a hw_input event to break
# the Screen._run out of the `wait_for` state. The Screen will then
# call its `_run_callback` and detect the success state and exit.
HardwareButtons.get_instance().trigger_override(force_release=True)
# blocks in its `wait_for`. Have to trigger a hw_input override event
# to break the Screen._run out of the `wait_for` state. The Screen
# will then call its `_run_callback` and detect the success state and
# exit.
HardwareButtons.get_instance().trigger_override()
# Exit the loop and thereby end this thread
return
textarea = TextArea(
@@ -1512,6 +1506,49 @@ class SeedAddressVerificationScreen(ButtonListScreen):
@dataclass
class SeedAddressVerificationSuccessScreen(LargeIconStatusScreen):
address: str = None
verified_index: int = None
verified_index_is_change: bool = None
def __post_init__(self):
# Customize defaults
self.title = _("Success!")
self.status_headline = _("Address Verified")
self.button_data = [ButtonOption("OK")]
self.is_bottom_list = True
self.show_back_button = False
super().__post_init__()
if self.verified_index_is_change:
# TRANSLATOR_NOTE: Describes the address type (change or receive)
address_type = _("change address")
else:
# TRANSLATOR_NOTE: Describes the address type (change or receive)
address_type = _("receive address")
self.components.append(FormattedAddress(
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
address=self.address,
max_lines=1, # Use abbreviated format w/ellipsis
))
self.components.append(TextArea(
text=address_type,
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
))
# TRANSLATOR_NOTE: Describes the address index (e.g. "index 7")
index_str = _("index {}").format(self.verified_index)
self.components.append(TextArea(
text=index_str,
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
))
@dataclass
class LoadMultisigWalletDescriptorScreen(ButtonListScreen):
def __post_init__(self):
@@ -6,7 +6,7 @@ from PIL.ImageOps import autocontrast
from typing import List
from seedsigner.helpers.l10n import mark_for_translation as _mft
from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, Fonts, GUIConstants, Icon, IconButton, IconTextLine, TextArea
from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, Fonts, GUIConstants, Icon, IconButton, IconTextLine, SeedSignerIconConstants, TextArea
from seedsigner.gui.screens.scan_screens import ScanScreen
from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption
from seedsigner.hardware.buttons import HardwareButtonsConstants
@@ -82,7 +82,7 @@ class IOTestScreen(BaseTopNavScreen):
self.components.append(self.joystick_click_button)
self.joystick_up_button = IconButton(
icon_name=FontAwesomeIconConstants.ANGLE_UP,
icon_name=SeedSignerIconConstants.CHEVRON_UP,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
width=input_button_width,
height=input_button_height,
@@ -93,7 +93,7 @@ class IOTestScreen(BaseTopNavScreen):
self.components.append(self.joystick_up_button)
self.joystick_down_button = IconButton(
icon_name=FontAwesomeIconConstants.ANGLE_DOWN,
icon_name=SeedSignerIconConstants.CHEVRON_DOWN,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
width=input_button_width,
height=input_button_height,
@@ -104,9 +104,8 @@ class IOTestScreen(BaseTopNavScreen):
self.components.append(self.joystick_down_button)
self.joystick_left_button = IconButton(
text=FontAwesomeIconConstants.ANGLE_LEFT,
font_name=GUIConstants.ICON_FONT_NAME__FONT_AWESOME,
font_size=GUIConstants.ICON_INLINE_FONT_SIZE,
icon_name=SeedSignerIconConstants.CHEVRON_LEFT,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
width=input_button_width,
height=input_button_height,
screen_x=dpad_center_x - input_button_width - GUIConstants.COMPONENT_PADDING,
@@ -116,7 +115,7 @@ class IOTestScreen(BaseTopNavScreen):
self.components.append(self.joystick_left_button)
self.joystick_right_button = IconButton(
icon_name=FontAwesomeIconConstants.ANGLE_RIGHT,
icon_name=SeedSignerIconConstants.CHEVRON_RIGHT,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
width=input_button_width,
height=input_button_height,
@@ -183,7 +182,7 @@ class IOTestScreen(BaseTopNavScreen):
screen_y=int((self.canvas_height - msg_height)/ 2),
)
while True:
input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS, check_release=False)
input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS)
if input == HardwareButtonsConstants.KEY1:
# Note that there are three distinct screen updates that happen at
+9 -8
View File
@@ -43,8 +43,8 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
time.sleep(0.01)
continue
# Check for joystick click to take final entropy image
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_PRESS):
# Check for ANYCLICK to take final entropy image
if self.hw_inputs.check_for_low(keys=HardwareButtonsConstants.KEYS__ANYCLICK):
# 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()
@@ -77,7 +77,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
int(self.renderer.canvas_width/2),
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
),
text="< " + _("back") + " | " + _("click joystick"), # TODO: Render with UI elements instead of text
text="< " + _("back") + " | " + _("click a button"), # TODO: Render with UI elements instead of text
fill=GUIConstants.BODY_FONT_COLOR,
font=instructions_font,
stroke_width=4,
@@ -123,7 +123,8 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen):
)
self.renderer.show_image()
input = self.hw_inputs.wait_for([HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT])
# LEFT = reshoot, RIGHT / ANYCLICK = accept
input = self.hw_inputs.wait_for([HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] + HardwareButtonsConstants.KEYS__ANYCLICK)
if input == HardwareButtonsConstants.KEY_LEFT:
return RET_CODE__BACK_BUTTON
@@ -140,7 +141,7 @@ class ToolsDiceEntropyEntryScreen(KeyboardScreen):
self.rows = 3
self.cols = 3
self.keyboard_font_name = GUIConstants.ICON_FONT_NAME__FONT_AWESOME
self.keyboard_font_size = None # Force auto-scaling to Key height
self.keyboard_font_size = 36
self.keys_charset = "".join([
FontAwesomeIconConstants.DICE_ONE,
FontAwesomeIconConstants.DICE_TWO,
@@ -183,8 +184,8 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
super().__post_init__()
self.components.append(TextArea(
# TRANSLATOR_NOTE: Number of BIP-39 seed words, and the entropy -- in bits, contained within.
text=_("The {}th word is built from {} more entropy bits plus auto-calculated checksum.").format(self.mnemonic_length, self.num_entropy_bits),
# TRANSLATOR_NOTE: Final word calc. `mnemonic_length` = 12 or 24. `num_bits` = 7 or 3 (bits of entropy in final word).
text=_("The {mnemonic_length}th word is built from {num_bits} more entropy bits plus auto-calculated checksum.").format(mnemonic_length=self.mnemonic_length, num_bits=self.num_entropy_bits),
screen_y=self.top_nav.height + int(GUIConstants.COMPONENT_PADDING/2),
))
@@ -454,7 +455,7 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
self.components.append(IconTextLine(
# TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor
label_text=_("Wallet descriptor"),
value_text=self.wallet_descriptor_display_name,
value_text=self.wallet_descriptor_display_name, # TODO: English text from embit (e.g. "1 / 2 multisig"); make l10 friendly
is_text_centered=True,
screen_x=GUIConstants.EDGE_PADDING,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
+19 -38
View File
@@ -33,6 +33,7 @@ class HardwareButtons(Singleton):
KEY2_PIN = 12
KEY3_PIN = 8
@classmethod
def get_instance(cls):
# This is the only way to access the one and only instance
@@ -53,8 +54,6 @@ class HardwareButtons(Singleton):
cls._instance.GPIO = GPIO
cls._instance.override_ind = False
cls._instance.add_events([HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY1, HardwareButtonsConstants.KEY2, HardwareButtonsConstants.KEY3])
# Track state over time so we can apply input delays/ignores as needed
cls._instance.cur_input = None # Track which direction or button was last pressed
cls._instance.cur_input_started = None # Track when that input began
@@ -65,7 +64,6 @@ class HardwareButtons(Singleton):
return cls._instance
@classmethod
def get_instance_no_hardware(cls):
# This is the only way to access the one and only instance
@@ -73,17 +71,23 @@ class HardwareButtons(Singleton):
cls._instance = cls.__new__(cls)
def wait_for(self, keys=[]) -> int:
"""
Block execution until one of the target keys is pressed.
def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int:
Optionally override the wait by calling `trigger_override()`.
"""
# TODO: Refactor to keep control in the Controller and not here
from seedsigner.controller import Controller
controller = Controller.get_instance()
if not release_keys:
release_keys = keys
self.override_ind = False
while True:
if self.override_ind:
# Break out of the wait_for without waiting for user input
self.override_ind = False
return HardwareButtonsConstants.OVERRIDE
cur_time = int(time.time() * 1000)
if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.is_screensaver_running:
# Start the screensaver. Will block execution until input detected.
@@ -99,15 +103,9 @@ class HardwareButtons(Singleton):
# Resume from a fresh loop
continue
# Check each candidate key to see if it was pressed
for key in keys:
if not check_release or ((check_release and key in release_keys and HardwareButtonsConstants.release_lock) or check_release and key not in release_keys):
# when check release is False or the release lock is released (True)
if self.GPIO.input(key) == GPIO.LOW or self.override_ind:
HardwareButtonsConstants.release_lock = False
if self.override_ind:
self.override_ind = False
return HardwareButtonsConstants.OVERRIDE
if self.GPIO.input(key) == GPIO.LOW:
if self.cur_input != key:
self.cur_input = key
self.cur_input_started = int(time.time() * 1000) # in milliseconds
@@ -149,29 +147,13 @@ class HardwareButtons(Singleton):
self.last_input_time = int(time.time() * 1000)
def add_events(self, keys=[]):
for key in keys:
GPIO.add_event_detect(key, self.GPIO.RISING, callback=HardwareButtons.rising_callback)
def rising_callback(channel):
HardwareButtonsConstants.release_lock = True
def trigger_override(self, force_release = False) -> bool:
if force_release:
HardwareButtonsConstants.release_lock = True
if not self.override_ind:
def trigger_override(self) -> bool:
""" Set the override flag to break out of the current `wait_for` loop """
self.override_ind = True
return True
return False
def force_release(self) -> bool:
HardwareButtonsConstants.release_lock = True
return True
def check_for_low(self, key: int = None, keys: List[int] = None) -> bool:
""" Returns True if one of the target keys/key is pressed """
if key:
keys = [key]
for key in keys:
@@ -181,15 +163,16 @@ class HardwareButtons(Singleton):
else:
return False
def has_any_input(self) -> bool:
""" Returns True if any of the keys are pressed """
for key in HardwareButtonsConstants.ALL_KEYS:
if self.GPIO.input(key) == GPIO.LOW:
return True
return False
# class used as short hand for static button/channel lookup values
# TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design
# patterns to have a static constants class plus a settable global value.
class HardwareButtonsConstants:
if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO
KEY_UP = 31
@@ -227,5 +210,3 @@ class HardwareButtonsConstants:
KEYS__LEFT_RIGHT_UP_DOWN = [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN]
KEYS__ANYCLICK = [KEY_PRESS, KEY1, KEY2, KEY3]
release_lock = True # released when True, locked when False
+41 -23
View File
@@ -414,6 +414,14 @@ class DecodeQR:
pass
# Is it byte data?
if not isinstance(s, bytes):
try:
# TODO: remove this check & conversion once above cast to str is removed
s = s.encode()
except UnicodeError:
# Couldn't convert back to bytes; shouldn't happen
raise Exception("Conversion to bytes failed")
# 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR
if len(s) == 32 or len(s) == 16:
try:
@@ -499,8 +507,7 @@ class DecodeQR:
def is_bitcoin_address(s):
if re.search(r'^bitcoin\:.*', s, re.IGNORECASE):
return True
elif re.search(r'^((bc1|tb1|bcr|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s):
# TODO: Handle regtest bcrt?
elif re.search(r'^((bc1|tb1|bcr|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s, re.IGNORECASE):
return True
else:
return False
@@ -932,61 +939,72 @@ class BitcoinAddressQrDecoder(BaseSingleFrameQrDecoder):
def add(self, segment, qr_type=QRType.BITCOIN_ADDRESS):
r = re.search(r'((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})', segment)
if r != None:
self.address = r.group(1)
"""
Input may be prefixed with "bitcoin:" but will be ignored.
if re.search(r'^((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})$', self.address) != None:
RegEx searches for a recognizable bitcoin address.
* The `^` ensures that the specified address prefixes can only match at
the beginning of the address.
Result will yield the following match groups:
* group 1: complete address
* group 2: address prefix
"""
address_match = re.search(r'^((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})', segment.split(":")[-1], re.IGNORECASE)
if address_match != None:
self.address = address_match.group(1)
self.complete = True
self.collected_segments = 1
# get address type
r = re.search(r'^((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})$', self.address)
if r != None:
r = r.group(2)
# Have to handle wallets that uppercase bech32 addresses.
# Note that it's safe to lowercase the prefix for ALL addr formats.
addr_prefix = address_match.group(2).lower()
if r == "1":
if addr_prefix == "1":
# Legacy P2PKH. mainnet
self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
elif r == "m" or r == "n":
elif addr_prefix in ["m", "n"]:
self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET)
elif r == "3":
elif addr_prefix == "3":
# Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); mainnet
# TODO: Would be more correct to use a P2SH constant
self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET)
elif r == "2":
elif addr_prefix == "2":
# Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); testnet / regtest
self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET)
elif r == "bc1q":
elif addr_prefix == "bc1q":
# Native Segwit (single sig or multisig), mainnet
self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET)
elif r == "tb1q":
elif addr_prefix == "tb1q":
# Native Segwit (single sig or multisig), testnet
self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET)
elif r == "bcrt1q":
elif addr_prefix == "bcrt1q":
# Native Segwit (single sig or multisig), regtest
self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.REGTEST)
elif r == "bc1p":
# Native Segwit (single sig or multisig), mainnet
elif addr_prefix == "bc1p":
self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.MAINNET)
elif r == "tb1p":
# Native Segwit (single sig or multisig), testnet
elif addr_prefix == "tb1p":
self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.TESTNET)
elif r == "bcrt1p":
# Native Segwit (single sig or multisig), regtest
elif addr_prefix == "bcrt1p":
self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.REGTEST)
# Note: there is no final "else" here because the regex won't return any other matches.
# If the addr type is case-insensitive, ensure we return it lowercase
if self.address_type[0] in [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TAPROOT]:
self.address = self.address.lower()
return DecodeQRStatus.COMPLETE
logger.debug(f"Invalid address: {segment}")
return DecodeQRStatus.INVALID
+1 -1
View File
@@ -504,7 +504,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
abbreviated_name="electrum",
display_name="Electrum seeds",
display_name=_mft("Electrum seeds"),
help_text=_mft("Native Segwit only"),
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED),
Binary file not shown.
+2 -4
View File
@@ -460,16 +460,14 @@ class PSBTAddressVerificationFailedView(View):
def run(self):
if self.is_multisig:
title = _("Caution")
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer".
text = _("PSBT's {} address could not be verified with your multisig wallet descriptor.").format(_("change") if self.is_change else _("self-transfer"))
text = _("PSBT's {} address could not be verified from wallet descriptor.").format(_("change") if self.is_change else _("self-transfer"))
else:
title = _("Suspicious PSBT")
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer".
text = _("PSBT's {} address could not be generated from your seed.").format(_("change") if self.is_change else _("self-transfer"))
DireWarningScreen(
title=title,
title=_("Suspicious PSBT"),
status_headline=_("Address Verification Failed"),
text=text,
button_data=[ButtonOption("Discard PSBT")],
+22 -7
View File
@@ -5,6 +5,7 @@ from gettext import gettext as _
from seedsigner.helpers.l10n import mark_for_translation as _mft
from seedsigner.models.settings import SettingsConstants
from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, View, Destination
from seedsigner.gui.screens.screen import ButtonOption
logger = logging.getLogger(__name__)
@@ -163,13 +164,7 @@ class ScanView(View):
# 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=_("Done"),
next_destination=Destination(MainMenuView, clear_history=True),
))
return Destination(ScanInvalidQRTypeView)
return Destination(MainMenuView)
@@ -212,3 +207,23 @@ class ScanAddressView(ScanView):
@property
def is_valid_qr_type(self):
return self.decoder.is_address
class ScanInvalidQRTypeView(View):
def run(self):
from seedsigner.gui.screens import WarningScreen
# TODO: This screen says "Error" but is intentionally using the WarningScreen in
# order to avoid the perception that something is broken on our end. This should
# either change to use the red ErrorScreen or the "Error" title should be
# changed to something softer.
self.run_screen(
WarningScreen,
title=_("Error"),
status_headline=_("Unknown QR Type"),
text=_("QRCode is invalid or is a data format not yet supported."),
button_data=[ButtonOption("Done")],
)
return Destination(MainMenuView, clear_history=True)
+15 -37
View File
@@ -279,8 +279,9 @@ class SeedMnemonicInvalidView(View):
def run(self):
button_data = [self.EDIT, self.DISCARD]
selected_menu_num = self.run_screen(
WarningScreen,
DireWarningScreen,
title=_("Invalid Mnemonic!"),
status_icon_name=SeedSignerIconConstants.ERROR,
status_headline=None,
text=_("Checksum failure; not a valid seed phrase."),
show_back_button=False,
@@ -1206,6 +1207,7 @@ class SeedBIP85InvalidChildIndexView(View):
DireWarningScreen(
title=_("BIP-85 Index Error"),
show_back_button=False,
status_icon_name=SeedSignerIconConstants.ERROR,
status_headline=_("Invalid Child Index"),
text=_("BIP-85 Child Index must be between 0 and 2^31-1."),
button_data=[ButtonOption("Try Again")]
@@ -1366,6 +1368,7 @@ class SeedWordsBackupTestMistakeView(View):
selected_menu_num = DireWarningScreen(
title=_("Verification Error"),
show_back_button=False,
status_icon_name=SeedSignerIconConstants.ERROR,
status_headline=status_headline,
button_data=button_data,
text=text,
@@ -1722,8 +1725,8 @@ class AddressVerificationStartView(View):
destination = Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), skip_current_view=True)
elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT:
# TODO: add Taproot support
return Destination(NotYetImplementedView)
sig_type = SettingsConstants.SINGLE_SIG
destination = Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), skip_current_view=True)
derivation_path = embit_utils.get_standard_derivation_path(
network=self.controller.unverified_address["network"],
@@ -1816,10 +1819,6 @@ class SeedAddressVerificationView(View):
self.sig_type = self.controller.unverified_address["sig_type"]
self.network = self.controller.unverified_address["network"]
if self.script_type == SettingsConstants.TAPROOT:
# TODO: Taproot addr verification
return Destination(NotYetImplementedView)
# TODO: This should be in `Seed` or `PSBT` utility class
embit_network = SettingsConstants.map_network_to_embit(self.network)
@@ -1848,6 +1847,7 @@ class SeedAddressVerificationView(View):
def run(self):
# Start brute-force calculations from the zero-th index
try:
self.addr_verification_thread.start()
button_data = [self.SKIP_10, self.CANCEL]
@@ -1904,10 +1904,12 @@ class SeedAddressVerificationView(View):
self.controller.unverified_address["verified_index_is_change"] = self.verified_index_is_change.cur_count == 1
return Destination(SeedAddressVerificationSuccessView, view_args=dict(seed_num=self.seed_num))
else:
finally:
# Halt the thread if the user gave up (will already be stopped if it verified the
# target addr).
self.addr_verification_thread.stop()
# Block until the thread has stopped
while self.addr_verification_thread.is_alive():
time.sleep(0.01)
@@ -1977,36 +1979,12 @@ class SeedAddressVerificationSuccessView(View):
def run(self):
from seedsigner.gui.screens.screen import LargeIconStatusScreen
address = self.controller.unverified_address["address"]
sig_type = self.controller.unverified_address["sig_type"]
verified_index = self.controller.unverified_address["verified_index"]
verified_index_is_change = self.controller.unverified_address["verified_index_is_change"]
if sig_type == SettingsConstants.MULTISIG:
source = _("multisig")
else:
# TRANSLATOR_NOTE: Inserts the seed fingerprint
source = _("seed {}").format(self.seed.get_fingerprint())
# TRANSLATOR_NOTE: Used in a sentence describing the address type (change or receive)
change_text = _("change")
# TRANSLATOR_NOTE: Used in a sentence describing the address type (change or receive)
receive_text = _("receive")
# TRANSLATOR_NOTE: Address verification success message (e.g. "bc1qabc = seed 12345678's receive address #0.")
text = _("{} = {}'s {} address #{}.").format(
address[:7],
source,
change_text if verified_index_is_change else receive_text,
verified_index
self.run_screen(
seed_screens.SeedAddressVerificationSuccessScreen,
address = self.controller.unverified_address["address"],
verified_index = self.controller.unverified_address["verified_index"],
verified_index_is_change = self.controller.unverified_address["verified_index_is_change"],
)
LargeIconStatusScreen(
status_headline=_("Address Verified"),
text=text,
show_back_button=False,
).display()
return Destination(MainMenuView)
+15 -7
View File
@@ -5,7 +5,7 @@ from typing import Type
from seedsigner.helpers.l10n import mark_for_translation as _mft
from seedsigner.gui.components import SeedSignerIconConstants
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON
from seedsigner.gui.screens.screen import BaseScreen, ButtonOption, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
from seedsigner.gui.screens.screen import BaseScreen, ButtonOption, LargeButtonScreen, WarningScreen, ErrorScreen
from seedsigner.models.settings import Settings, SettingsConstants
from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.threads import BaseThread
@@ -245,6 +245,7 @@ class PowerOptionsView(View):
class RestartView(View):
def run(self):
from seedsigner.gui.screens.screen import ResetScreen
thread = RestartView.DoResetThread()
thread.start()
self.run_screen(ResetScreen)
@@ -270,6 +271,7 @@ class RestartView(View):
class PowerOffView(View):
def run(self):
from seedsigner.gui.screens.screen import PowerOffNotRequiredScreen
self.run_screen(PowerOffNotRequiredScreen)
return Destination(BackStackView)
@@ -300,6 +302,7 @@ class NotYetImplementedView(View):
class ErrorView(View):
title: str = _mft("Error")
show_back_button: bool = True
status_icon_name: str = SeedSignerIconConstants.ERROR
status_headline: str = None
text: str = None
button_text: str = None
@@ -307,8 +310,9 @@ class ErrorView(View):
def run(self):
self.run_screen(
WarningScreen,
ErrorScreen,
title=self.title,
status_icon_name=self.status_icon_name,
status_headline=self.status_headline,
text=self.text,
button_data=[ButtonOption(self.button_text)],
@@ -324,9 +328,14 @@ class NetworkMismatchErrorView(ErrorView):
def __post_init__(self):
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
self.title: str = _("Network Mismatch")
self.show_back_button: bool = False
self.button_text: str = _("Change Setting")
# TRANSLATOR_NOTE: The network setting (mainnet/testnet/regtest) doesn't match the provided derivation path
self.title = _("Network Mismatch")
self.status_icon_name = SeedSignerIconConstants.WARNING
self.show_back_button = False
# TRANSLATOR_NOTE: Button option to alter a setting
self.button_text = _("Change Setting")
self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True)
super().__post_init__()
@@ -342,10 +351,9 @@ class NetworkMismatchErrorView(ErrorView):
class UnhandledExceptionView(View):
error: list[str]
def run(self):
self.run_screen(
DireWarningScreen,
ErrorScreen,
title=_("System Error"),
status_headline=self.error[0],
text=self.error[1] + "\n" + self.error[2],
+4 -9
View File
@@ -26,7 +26,7 @@ patch('PIL.ImageFont.core.HAVE_RAQM', False).start()
from seedsigner.controller import Controller
from seedsigner.gui.renderer import Renderer
from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen
from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
from seedsigner.gui.toast import RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
from seedsigner.hardware.microsd import MicroSD
from seedsigner.helpers import embit_utils
from seedsigner.models.decode_qr import DecodeQR
@@ -36,9 +36,9 @@ from seedsigner.models.seed import Seed
from seedsigner.models.settings import Settings
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)
psbt_views, seed_views, settings_views, tools_views, scan_views)
from seedsigner.views.screensaver import OpeningSplashView
from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View
from seedsigner.views.view import NetworkMismatchErrorView, OptionDisabledView, PowerOffView
from .utils import ScreenshotComplete, ScreenshotConfig, ScreenshotRenderer
@@ -359,12 +359,7 @@ def generate_screenshots(locale):
ScreenshotConfig(UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])),
ScreenshotConfig(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")),
ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
ScreenshotConfig(ErrorView, dict(
title="Error",
status_headline="Unknown QR Type",
text="QRCode is invalid or is a data format not yet supported.",
button_text="Back",
)),
ScreenshotConfig(scan_views.ScanInvalidQRTypeView)
]
}
+104 -52
View File
@@ -281,69 +281,121 @@ def test_short_4_letter_mnemonic_qr():
assert d.get_seed_phrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"]
# Test data for bitcoin address decoding. All generated from test key: ["abandon"] * 11 + ["about"]
legacy_address_mainnet = "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA"
legacy_address_testnet = "mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV"
nested_segwit_address_mainnet = "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf"
nested_segwit_address_testnet = "2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2"
native_segwit_address_mainnet = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
native_segwit_address_testnet = "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl"
native_segwit_address_regtest = "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk"
taproot_address_mainnet = "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
taproot_address_testnet = "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj"
taproot_address_regtest = "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg"
def test_bitcoin_address():
bad1 = "loremipsum"
bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae"
bad3 = "121802020768124106400009195602431595117715840445"
legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY"
legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe"
main_bech32_address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
test_bech32_address = "tb1qkurj377gtlmu0j5flcykcsh2xagexh9h3jk06a"
main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX"
test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm"
main_bech32_address2 = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=12000"
main_bech32_address3 = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?junk"
"""
Decoder should parse various types of valid bitcoin addresses with or without the
"bitcoin:" prefix and optional query params.
"""
def decode(address, expected_script_type, expected_network=SettingsConstants.MAINNET):
for data in [address, "bitcoin:" + address, "bitcoin:" + address + "?amount=12000"]:
d = DecodeQR()
d.add_data(bad1)
d.add_data(data)
assert d.get_address() == address
assert d.get_address_type() == (expected_script_type, expected_network)
assert d.qr_type == QRType.INVALID
decode(legacy_address_mainnet, SettingsConstants.LEGACY_P2PKH)
decode(legacy_address_testnet, SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET)
decode(nested_segwit_address_mainnet, SettingsConstants.NESTED_SEGWIT)
decode(nested_segwit_address_testnet, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET)
decode(native_segwit_address_mainnet, SettingsConstants.NATIVE_SEGWIT)
decode(native_segwit_address_testnet, SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET)
decode(native_segwit_address_regtest, SettingsConstants.NATIVE_SEGWIT, SettingsConstants.REGTEST)
decode(taproot_address_mainnet, SettingsConstants.TAPROOT)
decode(taproot_address_testnet, SettingsConstants.TAPROOT, SettingsConstants.TESTNET)
decode(taproot_address_regtest, SettingsConstants.TAPROOT, SettingsConstants.REGTEST)
def test_invalid_bitcoin_address():
"""
Decoder should fail to parse invalid address data.
* Test incorrect "bitcoin:" prefix.
* Test invalid addresses.
* Test valid addresses w/additional prefixes (to ensure regexp is not finding
mid-string matches) which make the data invalid.
"""
bad_inputs = [
# wrong separator char
"bitcoin=bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
# Unrecognized addr prefix
"bitcoin:bcfakehrp1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
# valid addr w/garbage addr prefix
"abcbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
# valid "bitcoin:" prefix w/garbage addr prefix
"bitcoin:abcbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
# typo in "bitcoin:" prefix
"bitcon:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
]
for bad_input in bad_inputs:
d = DecodeQR()
d.add_data(legacy_address1)
status = d.add_data(bad_input)
assert status == DecodeQRStatus.INVALID
assert d.get_address() == legacy_address1
assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
def test_bitcoin_address_ignores_case_where_allowed():
"""
Decoder should ignore case in QR data prefix and in the address itself (for the
address types where case is ignored).
"""
def decode(address, expected_script_type, is_case_sensitive, expected_network=SettingsConstants.MAINNET):
addr_variations = [address]
if not is_case_sensitive:
# Test as-is and all uppercase
addr_variations.append(address.upper())
for addr_variation in addr_variations:
# First add prefix capitalizations
variations_1 = ["bitcoin:" + addr_variation, "BITCOIN:" + addr_variation]
# Now add query params
variations_2 = [v + "?amount=12000" for v in variations_1]
variations_3 = [v + "?AMOUNT=12000" for v in variations_1]
for data in [addr_variation] + variations_1 + variations_2 + variations_3:
d = DecodeQR()
d.add_data(legacy_address2)
d.add_data(data)
assert d.get_address_type() == (expected_script_type, expected_network)
assert d.get_address() == legacy_address2
assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
if not is_case_sensitive:
assert d.get_address() == addr_variation.lower()
else:
assert d.get_address() == addr_variation
d = DecodeQR()
d.add_data(main_bech32_address)
# Case sensitive address types
decode(legacy_address_mainnet, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True)
decode(legacy_address_testnet, SettingsConstants.LEGACY_P2PKH, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET)
decode(nested_segwit_address_mainnet, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True)
decode(nested_segwit_address_testnet, SettingsConstants.NESTED_SEGWIT, is_case_sensitive=True, expected_network=SettingsConstants.TESTNET)
assert d.get_address() == main_bech32_address
assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET)
d = DecodeQR()
d.add_data(test_bech32_address)
assert d.get_address() == test_bech32_address
assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET)
d = DecodeQR()
d.add_data(main_nested_segwit_address)
assert d.get_address() == main_nested_segwit_address
assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET)
d = DecodeQR()
d.add_data(test_nested_segwit_address)
assert d.get_address() == test_nested_segwit_address
assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET)
d = DecodeQR()
d.add_data(main_bech32_address2)
assert d.get_address() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET)
# Case insensitive address types
decode(native_segwit_address_mainnet, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False)
decode(native_segwit_address_testnet, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False, expected_network=SettingsConstants.TESTNET)
decode(native_segwit_address_regtest, SettingsConstants.NATIVE_SEGWIT, is_case_sensitive=False, expected_network=SettingsConstants.REGTEST)
decode(taproot_address_mainnet, SettingsConstants.TAPROOT, is_case_sensitive=False)
decode(taproot_address_testnet, SettingsConstants.TAPROOT, is_case_sensitive=False, expected_network=SettingsConstants.TESTNET)
decode(taproot_address_regtest, SettingsConstants.TAPROOT, is_case_sensitive=False, expected_network=SettingsConstants.REGTEST)
+1 -1
View File
@@ -646,7 +646,7 @@ class TestMessageSigningFlows(FlowTest):
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
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(scan_views.ScanInvalidQRTypeView),
FlowStep(MainMenuView),
])
+10 -1
View File
@@ -257,9 +257,18 @@ class TestToolsFlows(FlowTest):
settings = controller.settings
settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST)
addrs = [
# Native segwit regtest receive addr @ index 6
"bcrt1q4e9q5taxnsvc6m0uxv6h75mkzvnkxeqk6l90u2",
# Taproot regtest change addr @ index 48
"bcrt1pj5v8ean2hc5lh2djsgfx4j9uc0n67942ngv6q9r49qv88ex5mrwsn3u4f7",
]
for test_addr in addrs:
def load_address_into_decoder(view: scan_views.ScanView):
# Native segwit regtest receive addr @ index 6
view.decoder.add_data("bcrt1q4e9q5taxnsvc6m0uxv6h75mkzvnkxeqk6l90u2")
view.decoder.add_data(test_addr)
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
+25 -1
View File
@@ -4,7 +4,6 @@ from seedsigner.helpers.qr import QR
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder
from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import Seed
@@ -104,3 +103,28 @@ def test_compact_seedqr_handles_null_bytes():
# 12-word seed, multiple null bytes in a row
entropy = os.urandom(10) + b'\x00\x00' + os.urandom(4)
run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR)
def test_compact_seedqr_bytes_interpretable_as_str():
"""
Should successfully decode a Compact SeedQR whose bytes can be interpreted as a valid
string. Most Compact SeedQR byte data will raise a UnicodeDecodeError when attempting to
interpret it as a string, but edge cases are possible.
see: Issue #656
"""
# Randomly generated to pass the str.decode() step; 12- and 24-word entropy.
entropy_bytes_tests = [
b'\x00' * 16, # abandon * 11 + about
b'\x12\x15\\1j`3\x0bkL}f\x00ZYK',
b'tv\x1bZjmqN@t\x13\x1aK\\v)',
b'|9\x05\x1aHF9j\xda\xb6v\x05\x08#\x12=',
b"iHK`4\x1a5\xd3\xaf\xd3\xb47htJ.}<\xea\xbf\x88Xh\x01.?R2^\xc2\xb1'",
b'|Z\x11\x1dt\xdd\x97~t&f &G$H|^[\xd3\x9d<q]z\x14.\x11`!\xd1\x91',
b'0\xd4\xb3\\,\xcd\x8d7c/Rp\x0e\xc2\xbb\xe4\x99\xa3=j5,\xcc\x9a[>\x19Z{\ng^',
]
for entropy_bytes in entropy_bytes_tests:
entropy_bytes.decode() # should not raise an exception
mnemonic_length = 12 if len(entropy_bytes) == 16 else 24
run_encode_decode_test(entropy_bytes, mnemonic_length=mnemonic_length, qr_type=QRType.SEED__COMPACTSEEDQR)
+2 -2
View File
@@ -31,9 +31,9 @@ if __name__ == "__main__":
seed_phrase = input("\nEnter 12- or 24-word test seed phrase: ").strip().split(" ")
if format == COMPACT:
encoder = CompactSeedQrEncoder(seed_phrase=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
encoder = CompactSeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
else:
encoder = SeedQrEncoder(seed_phrase=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
encoder = SeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3)
qr.add_data(encoder.next_part())