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" ] target: [ "pi0", "pi2", "pi02w", "pi4" ]
steps: steps:
- name: checkout seedsigner-os - name: checkout seedsigner-os
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: "seedsigner/seedsigner-os" repository: "seedsigner/seedsigner-os"
# use the os-ref input parameter in case of workflow_dispatch or default to main in case of cron triggers # 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 fetch-depth: 0
- name: checkout source - name: checkout source
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch) # ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch)
path: "seedsigner-os/opt/rootfs-overlay/opt" path: "seedsigner-os/opt/rootfs-overlay/opt"
@@ -78,7 +78,7 @@ jobs:
ls -la src ls -la src
- name: restore build cache - 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, # Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour,
# while consuming ~850 MB storage space). # while consuming ~850 MB storage space).
with: with:
@@ -89,10 +89,25 @@ jobs:
restore-keys: | restore-keys: |
build-cache-${{ matrix.target }}- build-cache-${{ matrix.target }}-
- name: Create build container
run: |
cd seedsigner-os
docker build -t seedsigner-os-build .
- name: build - name: build
run: | run: |
cd seedsigner-os/opt mkdir -p \
./build.sh --${{ matrix.target }} --skip-repo --no-clean ~/.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) - name: list image (before rename)
run: | run: |
@@ -113,9 +128,9 @@ jobs:
ls -la seedsigner-os/images ls -la seedsigner-os/images
- name: upload images - name: upload images
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: seedsigner_os_images name: seedsigner_os_images-${{ matrix.target }}
path: "seedsigner-os/images/*.img" path: "seedsigner-os/images/*.img"
if-no-files-found: error if-no-files-found: error
# maximum 90 days retention # maximum 90 days retention
@@ -127,14 +142,13 @@ jobs:
needs: build needs: build
steps: steps:
- name: download images - name: download images
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: seedsigner_os_images
path: images path: images
- name: list images - name: list images
run: | run: |
ls -la images ls -lRa images
- name: get seedsigner latest commit hash - name: get seedsigner latest commit hash
id: get-seedsigner-hash id: get-seedsigner-hash
@@ -145,12 +159,14 @@ jobs:
- name: write sha256sum - name: write sha256sum
run: | run: |
cd images cd images
# each downloaded image is in its own subfolder
find . -name "*.img" -exec mv {} . \;
sha256sum *.img > seedsigner_os.${{ env.source_hash }}.sha256 sha256sum *.img > seedsigner_os.${{ env.source_hash }}.sha256
- name: upload checksums - name: upload checksums
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: seedsigner_os_images name: seedsigner_os_images_sha256
path: "images/*.sha256" path: "images/*.sha256"
if-no-files-found: error if-no-files-found: error
# maximum 90 days retention # maximum 90 days retention
+4 -4
View File
@@ -26,12 +26,12 @@ jobs:
python-version: ["3.10", "3.12"] python-version: ["3.10", "3.12"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
# Needs to also pull the seedsigner-translations repo # Needs to also pull the seedsigner-translations repo
submodules: recursive submodules: recursive
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
@@ -63,9 +63,9 @@ jobs:
- name: Coverage report - name: Coverage report
run: coverage report run: coverage report
- name: Archive CI Artifacts - name: Archive CI Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: ci-artifacts name: ci-artifacts-${{ matrix.python-version }}
path: artifacts/** path: artifacts/**
retention-days: 10 retention-days: 10
# Upload also when tests fail. The workflow result (red/green) will # 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"] [submodule "src/seedsigner/resources/seedsigner-translations"]
path = src/seedsigner/resources/seedsigner-translations path = src/seedsigner/resources/seedsigner-translations
url = https://github.com/SeedSigner/seedsigner-translations.git url = https://github.com/SeedSigner/seedsigner-translations.git
branch = 0.8.5-rc1 branch = dev
[submodule "seedsigner-screenshots"] [submodule "seedsigner-screenshots"]
path = seedsigner-screenshots path = seedsigner-screenshots
url = https://github.com/SeedSigner/seedsigner-screenshots.git url = https://github.com/SeedSigner/seedsigner-screenshots.git
+66 -71
View File
@@ -1,14 +1,14 @@
# Translations template for seedsigner. # Translations template for seedsigner.
# Copyright (C) 2024 ORGANIZATION # Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the seedsigner project. # This file is distributed under the same license as the seedsigner project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024. # FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" 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" "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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -36,16 +36,6 @@ msgstr ""
msgid "sats" msgid "sats"
msgstr "" 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 #: src/seedsigner/gui/toast.py
msgid "" msgid ""
"You can remove\n" "You can remove\n"
@@ -121,9 +111,8 @@ msgid "OP_RETURN"
msgstr "" msgstr ""
#. Label for a change output in the PSBT Overview flow diagram #. 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/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" msgid "change"
msgstr "" msgstr ""
@@ -204,15 +193,19 @@ msgstr ""
msgid "Darker" msgid "Darker"
msgstr "" 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!" msgid "Success!"
msgstr "" 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" msgid "OK"
msgstr "" msgstr ""
#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py #: src/seedsigner/gui/screens/screen.py
msgid "Caution" msgid "Caution"
msgstr "" msgstr ""
@@ -228,6 +221,11 @@ msgstr ""
msgid "Classified Info!" msgid "Classified Info!"
msgstr "" 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 #: src/seedsigner/gui/screens/screen.py
msgid "Restarting" msgid "Restarting"
msgstr "" msgstr ""
@@ -391,6 +389,25 @@ msgstr ""
msgid "Checking address {}" msgid "Checking address {}"
msgstr "" 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 #: src/seedsigner/gui/screens/seed_screens.py
msgid "Multisig Verification" msgid "Multisig Verification"
msgstr "" msgstr ""
@@ -494,7 +511,7 @@ msgid "Home"
msgstr "" msgstr ""
#: src/seedsigner/gui/screens/tools_screens.py #: src/seedsigner/gui/screens/tools_screens.py
msgid "click joystick" msgid "click a button"
msgstr "" msgstr ""
#. A prompt to the user to either accept or reshoot the image #. A prompt to the user to either accept or reshoot the image
@@ -517,11 +534,12 @@ msgstr ""
msgid "Build Final Word" msgid "Build Final Word"
msgstr "" 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 #: src/seedsigner/gui/screens/tools_screens.py
msgid "" msgid ""
"The {}th word is built from {} more entropy bits plus auto-calculated " "The {mnemonic_length}th word is built from {num_bits} more entropy bits "
"checksum." "plus auto-calculated checksum."
msgstr "" msgstr ""
#. current coin-flip number vs total flips (e.g. flip 3 of 4) #. current coin-flip number vs total flips (e.g. flip 3 of 4)
@@ -744,6 +762,10 @@ msgstr ""
msgid "BIP-85 child seeds" msgid "BIP-85 child seeds"
msgstr "" msgstr ""
#: src/seedsigner/models/settings_definition.py
msgid "Electrum seeds"
msgstr ""
#: src/seedsigner/models/settings_definition.py #: src/seedsigner/models/settings_definition.py
msgid "Native Segwit only" msgid "Native Segwit only"
msgstr "" msgstr ""
@@ -874,13 +896,7 @@ msgstr ""
#. Variable is either "change" or "self-transfer". #. Variable is either "change" or "self-transfer".
#: src/seedsigner/views/psbt_views.py #: src/seedsigner/views/psbt_views.py
msgid "" msgid "PSBT's {} address could not be verified from wallet descriptor."
"PSBT's {} address could not be verified with your multisig wallet "
"descriptor."
msgstr ""
#: src/seedsigner/views/psbt_views.py
msgid "Suspicious PSBT"
msgstr "" msgstr ""
#. Variable is either "change" or "self-transfer". #. Variable is either "change" or "self-transfer".
@@ -888,6 +904,10 @@ msgstr ""
msgid "PSBT's {} address could not be generated from your seed." msgid "PSBT's {} address could not be generated from your seed."
msgstr "" msgstr ""
#: src/seedsigner/views/psbt_views.py
msgid "Suspicious PSBT"
msgstr ""
#: src/seedsigner/views/psbt_views.py #: src/seedsigner/views/psbt_views.py
msgid "Address Verification Failed" msgid "Address Verification Failed"
msgstr "" msgstr ""
@@ -928,23 +948,6 @@ msgstr ""
msgid "Wrong QR Type" msgid "Wrong QR Type"
msgstr "" 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 #: src/seedsigner/views/scan_views.py src/seedsigner/views/seed_views.py
msgid "Scan PSBT" msgid "Scan PSBT"
msgstr "" msgstr ""
@@ -977,6 +980,19 @@ msgstr ""
msgid "Expected an address QR" msgid "Expected an address QR"
msgstr "" 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 #. This is on the opening splash screen, displayed above the HRF logo
#: src/seedsigner/views/screensaver.py #: src/seedsigner/views/screensaver.py
msgid "With support from:" msgid "With support from:"
@@ -1262,30 +1278,6 @@ msgstr ""
msgid "Can't validate a single sig addr without specifying a seed" msgid "Can't validate a single sig addr without specifying a seed"
msgstr "" 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 #: src/seedsigner/views/seed_views.py
msgid "Scan Descriptor" msgid "Scan Descriptor"
msgstr "" msgstr ""
@@ -1456,10 +1448,13 @@ msgstr ""
msgid "Back to Main Menu" msgid "Back to Main Menu"
msgstr "" msgstr ""
#. The network setting (mainnet/testnet/regtest) doesn't match the provided
#. derivation path
#: src/seedsigner/views/view.py #: src/seedsigner/views/view.py
msgid "Network Mismatch" msgid "Network Mismatch"
msgstr "" msgstr ""
#. Button option to alter a setting
#: src/seedsigner/views/view.py #: src/seedsigner/views/view.py
msgid "Change Setting" msgid "Change Setting"
msgstr "" msgstr ""
+1 -1
View File
@@ -13,7 +13,7 @@ description = "Build an offline, airgapped Bitcoin signing device for less than
name = "seedsigner" name = "seedsigner"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "0.8.5-rc1" version = "0.8.5"
[project.urls] [project.urls]
"Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues" "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. 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 # Declare class member vars with type hints to enable richer IDE support throughout
# the code. # the code.
+26 -29
View File
@@ -27,17 +27,18 @@ class GUIConstants:
COMPONENT_PADDING = 8 COMPONENT_PADDING = 8
LIST_ITEM_PADDING = 4 LIST_ITEM_PADDING = 4
BACKGROUND_COLOR = "black" BACKGROUND_COLOR = "#000000"
INACTIVE_COLOR = "#414141"
ACCENT_COLOR = "#FF9F0A" # Active Color
WARNING_COLOR = "#FFD60A" WARNING_COLOR = "#FFD60A"
DIRE_WARNING_COLOR = "#FF453A" DIRE_WARNING_COLOR = "#FF5700"
ERROR_COLOR = "#FF1B0A"
SUCCESS_COLOR = "#30D158" SUCCESS_COLOR = "#30D158"
INFO_COLOR = "#0084FF" INFO_COLOR = "#409CFF"
ACCENT_COLOR = "#FF9F0A"
BITCOIN_ORANGE = "#FF9416" BITCOIN_ORANGE = "#FF9416"
TESTNET_COLOR = "#00F100" TESTNET_COLOR = "#00F100"
REGTEST_COLOR = "#00CAF1" REGTEST_COLOR = "#00CAF1"
GREEN_INDICATOR_COLOR = "#00FF00" GREEN_INDICATOR_COLOR = "#00FF00"
INACTIVE_COLOR = "#414141"
ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900" ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900"
ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons" ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons"
@@ -215,27 +216,34 @@ class SeedSignerIconConstants:
RESTART = "\ue911" RESTART = "\ue911"
# Messaging icons # Messaging icons
ERROR = "\ue912" INFO = "\ue912"
SUCCESS = "\ue913" SUCCESS = "\ue913"
WARNING = "\ue914" WARNING = "\ue914"
ERROR = "\ue915"
# Informational icons # Informational icons
ADDRESS = "\ue915" ADDRESS = "\ue916"
CHANGE = "\ue916" CHANGE = "\ue917"
DERIVATION = "\ue917" DERIVATION = "\ue918"
FEE = "\ue918" FEE = "\ue919"
FINGERPRINT = "\ue919" FINGERPRINT = "\ue91a"
PASSPHRASE = "\ue91a" PASSPHRASE = "\ue91b"
# Misc icons # Misc icons
BITCOIN = "\ue91b" BITCOIN = "\ue91c"
BITCOIN_ALT = "\ue91c" BITCOIN_ALT = "\ue91d"
BRIGHTNESS = "\ue91d" BRIGHTNESS = "\ue91e"
MICROSD = "\ue91e" MICROSD = "\ue91f"
QRCODE = "\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 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: def load_image(image_name: str) -> Image.Image:
image_url = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "img", image_name) image_url = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "img", image_name)
image = Image.open(image_url).convert("RGB") 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 typing import Tuple
from gettext import gettext as _ 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 from seedsigner.hardware.buttons import HardwareButtonsConstants
@@ -30,58 +30,55 @@ class Keyboard:
ENTER_RIGHT = "enter_right" ENTER_RIGHT = "enter_right"
REGULAR_KEY_FONT = "regular" 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 = { KEY_BACKSPACE = {
"code": "DEL", "code": "DEL",
"letter": del_label, "letter": SeedSignerIconConstants.DELETE,
"font": COMPACT_KEY_FONT, "font": ICON_KEY_FONT,
"size": 2, "size": 2,
} }
# TRANSLATOR_NOTE: The abbreviated label for the special key <space> on a standard keyboard.
space_label = _("space")
KEY_SPACE = { KEY_SPACE = {
"code": "SPACE", "code": "SPACE",
"letter": space_label, "letter": SeedSignerIconConstants.SPACE,
"font": COMPACT_KEY_FONT, "font": ICON_KEY_FONT,
"size": 1, "size": 1,
} }
KEY_SPACE_2 = { KEY_SPACE_2 = {
"code": "SPACE", "code": "SPACE",
"letter": space_label, "letter": SeedSignerIconConstants.SPACE,
"font": COMPACT_KEY_FONT, "font": ICON_KEY_FONT,
"size": 2, "size": 2,
} }
KEY_SPACE_3 = { KEY_SPACE_3 = {
"code": "SPACE", "code": "SPACE",
"letter": space_label, "letter": SeedSignerIconConstants.SPACE,
"font": COMPACT_KEY_FONT, "font": ICON_KEY_FONT,
"size": 3, "size": 3,
} }
KEY_SPACE_4 = { KEY_SPACE_4 = {
"code": "SPACE", "code": "SPACE",
"letter": space_label, "letter": SeedSignerIconConstants.SPACE,
"font": COMPACT_KEY_FONT, "font": ICON_KEY_FONT,
"size": 4, "size": 4,
} }
KEY_SPACE_5 = { KEY_SPACE_5 = {
"code": "SPACE", "code": "SPACE",
"letter": space_label, "letter": SeedSignerIconConstants.SPACE,
"font": COMPACT_KEY_FONT, "font": ICON_KEY_FONT,
"size": 5, "size": 5,
} }
KEY_CURSOR_LEFT = { KEY_CURSOR_LEFT = {
"code": "CURSOR_LEFT", "code": "CURSOR_LEFT",
"letter": "<", "letter": SeedSignerIconConstants.CHEVRON_LEFT,
"font": REGULAR_KEY_FONT, "font": ICON_KEY_FONT,
"size": 1, "size": 1,
} }
KEY_CURSOR_RIGHT = { KEY_CURSOR_RIGHT = {
"code": "CURSOR_RIGHT", "code": "CURSOR_RIGHT",
"letter": ">", "letter": SeedSignerIconConstants.CHEVRON_RIGHT,
"font": REGULAR_KEY_FONT, "font": ICON_KEY_FONT,
"size": 1, "size": 1,
} }
KEY_PREVIOUS_PAGE = { KEY_PREVIOUS_PAGE = {
@@ -123,9 +120,11 @@ class Keyboard:
def render_key(self): def render_key(self):
font = self.keyboard.font font = self.keyboard.font
text_height = self.keyboard.text_height
if self.is_additional_key: if self.is_additional_key:
if Keyboard.ADDITIONAL_KEYS[self.code]["font"] == Keyboard.COMPACT_KEY_FONT: if Keyboard.ADDITIONAL_KEYS[self.code]["font"] == Keyboard.ICON_KEY_FONT:
font = self.keyboard.additonal_key_compact_font font = self.keyboard.icon_key_font
text_height = self.keyboard.icon_key_height
outline_color = "#333" outline_color = "#333"
if not self.is_active: if not self.is_active:
@@ -159,15 +158,12 @@ class Keyboard:
radius=4 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.keyboard.draw.text(
( (
self.screen_x + int(self.keyboard.key_width * self.size / 2), 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.screen_y + self.keyboard.key_height - int((self.keyboard.key_height - text_height)/2)
), ),
_(self.letter), self.letter,
fill=font_color, fill=font_color,
font=font, font=font,
anchor="ms" anchor="ms"
@@ -217,8 +213,15 @@ class Keyboard:
# Set up the rendering and state params # Set up the rendering and state params
self.active_keys = list(self.charset) 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.x_start = rect[0]
self.y_start = rect[1] self.y_start = rect[1]
self.x_gap = 2 self.x_gap = 2
+1 -1
View File
@@ -768,7 +768,7 @@ class PSBTFinalizeScreen(ButtonListScreen):
super().__post_init__() super().__post_init__()
icon = Icon( icon = Icon(
icon_name=FontAwesomeIconConstants.PAPER_PLANE, icon_name=SeedSignerIconConstants.SIGN,
icon_color=GUIConstants.INFO_COLOR, icon_color=GUIConstants.INFO_COLOR,
icon_size=GUIConstants.ICON_LARGE_BUTTON_SIZE, icon_size=GUIConstants.ICON_LARGE_BUTTON_SIZE,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING
+44 -27
View File
@@ -1,3 +1,4 @@
import logging
import time import time
from dataclasses import dataclass, field 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.settings import SettingsConstants
from seedsigner.models.threads import BaseThread, ThreadsafeCounter 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 # Must be huge numbers to avoid conflicting with the selected_button returned by the
# screens with buttons. # screens with buttons.
@@ -71,6 +74,13 @@ class BaseScreen(BaseComponent):
for t in self.get_threads(): for t in self.get_threads():
t.stop() 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): def clear_screen(self):
# Clear the whole canvas # Clear the whole canvas
@@ -226,11 +236,7 @@ class BaseTopNavScreen(BaseScreen):
time.sleep(0.1) time.sleep(0.1)
continue continue
user_input = self.hw_inputs.wait_for( user_input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS)
HardwareButtonsConstants.ALL_KEYS,
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
)
with self.renderer.lock: with self.renderer.lock:
if not self.top_nav.is_selected and user_input in [ if not self.top_nav.is_selected and user_input in [
@@ -303,7 +309,7 @@ class ButtonListScreen(BaseTopNavScreen):
if len(self.button_data) == 1: if len(self.button_data) == 1:
button_list_height = button_height button_list_height = button_height
else: 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: if self.is_bottom_list:
button_list_y = self.canvas_height - (button_list_height + GUIConstants.EDGE_PADDING) 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 active_button_label = button_option.active_button_label
else: else:
raise Exception("Refactor needed!") raise Exception("Refactor to ButtonOption approach needed!")
button_kwargs = dict( button_kwargs = dict(
text=_(button_label), # Wrap here for just-in-time translations text=_(button_label), # Wrap here for just-in-time translations
@@ -447,6 +453,7 @@ class ButtonListScreen(BaseTopNavScreen):
while True: while True:
ret = self._run_callback() ret = self._run_callback()
if ret is not None: if ret is not None:
logging.info("Exiting ButtonListScreen due to _run_callback")
return ret return ret
user_input = self.hw_inputs.wait_for( user_input = self.hw_inputs.wait_for(
@@ -455,9 +462,7 @@ class ButtonListScreen(BaseTopNavScreen):
HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_DOWN,
HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_LEFT,
HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY_RIGHT,
] + HardwareButtonsConstants.KEYS__ANYCLICK, ] + HardwareButtonsConstants.KEYS__ANYCLICK
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
) )
with self.renderer.lock: with self.renderer.lock:
@@ -641,9 +646,7 @@ class LargeButtonScreen(BaseTopNavScreen):
HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_DOWN,
HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_LEFT,
HardwareButtonsConstants.KEY_RIGHT HardwareButtonsConstants.KEY_RIGHT
] + HardwareButtonsConstants.KEYS__ANYCLICK, ] + HardwareButtonsConstants.KEYS__ANYCLICK
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
) )
with self.renderer.lock: with self.renderer.lock:
@@ -858,9 +861,7 @@ class QRDisplayScreen(BaseScreen):
HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_DOWN,
HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_LEFT,
HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY_RIGHT,
] + HardwareButtonsConstants.KEYS__ANYCLICK, ] + HardwareButtonsConstants.KEYS__ANYCLICK
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
) )
if user_input == HardwareButtonsConstants.KEY_DOWN: if user_input == HardwareButtonsConstants.KEY_DOWN:
# Reduce QR code background brightness # Reduce QR code background brightness
@@ -923,13 +924,14 @@ class LargeIconStatusScreen(ButtonListScreen):
self.components.append(self.warning_headline_textarea) self.components.append(self.warning_headline_textarea)
next_y = next_y + self.warning_headline_textarea.height next_y = next_y + self.warning_headline_textarea.height
self.components.append(TextArea( if self.text:
height=self.buttons[0].screen_y - next_y, self.components.append(TextArea(
text=_(self.text), height=self.buttons[0].screen_y - next_y,
width=self.canvas_width, text=_(self.text),
edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges width=self.canvas_width,
screen_y=next_y, edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges
)) screen_y=next_y,
))
@@ -1011,9 +1013,12 @@ class WarningEdgesMixin:
@dataclass @dataclass
class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen):
"""
Exclamation point icon + yellow WARNING color
"""
title: str = _mft("Caution") title: str = _mft("Caution")
status_icon_name: str = SeedSignerIconConstants.WARNING 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 status_headline: str = _mft("Privacy Leak!") # The colored text under the alert icon
button_data: list = field(default_factory=lambda: [ButtonOption("I Understand")]) button_data: list = field(default_factory=lambda: [ButtonOption("I Understand")])
@@ -1021,11 +1026,25 @@ class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen):
@dataclass @dataclass
class DireWarningScreen(WarningScreen): class DireWarningScreen(WarningScreen):
"""
Exclamation point icon + orange DIRE_WARNING color
"""
status_headline: str = _mft("Classified Info!") # The colored text under the alert icon status_headline: str = _mft("Classified Info!") # The colored text under the alert icon
status_color: str = GUIConstants.DIRE_WARNING_COLOR 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 @dataclass
class ResetScreen(BaseTopNavScreen): class ResetScreen(BaseTopNavScreen):
def __post_init__(self): def __post_init__(self):
@@ -1191,9 +1210,7 @@ class KeyboardScreen(BaseTopNavScreen):
# Start the interactive update loop # Start the interactive update loop
while True: while True:
input = self.hw_inputs.wait_for( input = self.hw_inputs.wait_for(
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3], HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
) )
with self.renderer.lock: with self.renderer.lock:
+67 -30
View File
@@ -1,26 +1,26 @@
import math
import logging import logging
import math
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from gettext import gettext as _ from gettext import gettext as _
from PIL import Image, ImageDraw, ImageFilter
from typing import List from typing import List
from PIL import Image, ImageDraw, ImageFilter from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants
from seedsigner.gui.renderer import Renderer
from seedsigner.helpers.qr import QR 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 seedsigner.models.threads import BaseThread, ThreadsafeCounter
from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption, KeyboardScreen, WarningEdgesMixin from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption, KeyboardScreen, LargeIconStatusScreen, 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
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass @dataclass
class SeedMnemonicEntryScreen(BaseTopNavScreen): class SeedMnemonicEntryScreen(BaseTopNavScreen):
initial_letters: list = None initial_letters: list = None
@@ -243,11 +243,7 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
def _run(self): def _run(self):
while True: while True:
input = self.hw_inputs.wait_for( input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS)
HardwareButtonsConstants.ALL_KEYS,
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY2]
)
with self.renderer.lock: with self.renderer.lock:
if self.is_input_in_top_nav: if self.is_input_in_top_nav:
@@ -885,11 +881,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
# Start the interactive update loop # Start the interactive update loop
while True: while True:
input = self.hw_inputs.wait_for( input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS)
HardwareButtonsConstants.ALL_KEYS,
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY1, HardwareButtonsConstants.KEY2, HardwareButtonsConstants.KEY3]
)
keyboard_swap = False keyboard_swap = False
@@ -1057,7 +1049,6 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
self.hw_button2.render() self.hw_button2.render()
self.renderer.show_image() self.renderer.show_image()
@@ -1467,13 +1458,13 @@ class SeedAddressVerificationScreen(ButtonListScreen):
def _run_callback(self): def _run_callback(self):
# Exit the screen on success via a non-None value # Exit the screen on success via a non-None value.
logger.info(f"verified_index: {self.verified_index.cur_count}") # see: ButtonListScreen._run()
if self.verified_index.cur_count is not None: if self.verified_index.cur_count is not None:
logger.info("Screen callback returning success!") # Note that the ProgressThread will have already exited on its own.
self.threads[-1].stop()
while self.threads[-1].is_alive(): # Return a success value (anything other than None) to end the
time.sleep(0.01) # ButtonListScreen._run() loop.
return 1 return 1
@@ -1490,10 +1481,13 @@ class SeedAddressVerificationScreen(ButtonListScreen):
while self.keep_running: while self.keep_running:
if self.verified_index.cur_count is not None: if self.verified_index.cur_count is not None:
# This thread will detect the success state while its parent Screen # 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 # blocks in its `wait_for`. Have to trigger a hw_input override event
# the Screen._run out of the `wait_for` state. The Screen will then # to break the Screen._run out of the `wait_for` state. The Screen
# call its `_run_callback` and detect the success state and exit. # will then call its `_run_callback` and detect the success state and
HardwareButtons.get_instance().trigger_override(force_release=True) # exit.
HardwareButtons.get_instance().trigger_override()
# Exit the loop and thereby end this thread
return return
textarea = TextArea( 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 @dataclass
class LoadMultisigWalletDescriptorScreen(ButtonListScreen): class LoadMultisigWalletDescriptorScreen(ButtonListScreen):
def __post_init__(self): def __post_init__(self):
@@ -6,7 +6,7 @@ from PIL.ImageOps import autocontrast
from typing import List from typing import List
from seedsigner.helpers.l10n import mark_for_translation as _mft 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.scan_screens import ScanScreen
from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption
from seedsigner.hardware.buttons import HardwareButtonsConstants from seedsigner.hardware.buttons import HardwareButtonsConstants
@@ -82,7 +82,7 @@ class IOTestScreen(BaseTopNavScreen):
self.components.append(self.joystick_click_button) self.components.append(self.joystick_click_button)
self.joystick_up_button = IconButton( self.joystick_up_button = IconButton(
icon_name=FontAwesomeIconConstants.ANGLE_UP, icon_name=SeedSignerIconConstants.CHEVRON_UP,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
width=input_button_width, width=input_button_width,
height=input_button_height, height=input_button_height,
@@ -93,7 +93,7 @@ class IOTestScreen(BaseTopNavScreen):
self.components.append(self.joystick_up_button) self.components.append(self.joystick_up_button)
self.joystick_down_button = IconButton( self.joystick_down_button = IconButton(
icon_name=FontAwesomeIconConstants.ANGLE_DOWN, icon_name=SeedSignerIconConstants.CHEVRON_DOWN,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
width=input_button_width, width=input_button_width,
height=input_button_height, height=input_button_height,
@@ -104,9 +104,8 @@ class IOTestScreen(BaseTopNavScreen):
self.components.append(self.joystick_down_button) self.components.append(self.joystick_down_button)
self.joystick_left_button = IconButton( self.joystick_left_button = IconButton(
text=FontAwesomeIconConstants.ANGLE_LEFT, icon_name=SeedSignerIconConstants.CHEVRON_LEFT,
font_name=GUIConstants.ICON_FONT_NAME__FONT_AWESOME, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
font_size=GUIConstants.ICON_INLINE_FONT_SIZE,
width=input_button_width, width=input_button_width,
height=input_button_height, height=input_button_height,
screen_x=dpad_center_x - input_button_width - GUIConstants.COMPONENT_PADDING, 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.components.append(self.joystick_left_button)
self.joystick_right_button = IconButton( self.joystick_right_button = IconButton(
icon_name=FontAwesomeIconConstants.ANGLE_RIGHT, icon_name=SeedSignerIconConstants.CHEVRON_RIGHT,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
width=input_button_width, width=input_button_width,
height=input_button_height, height=input_button_height,
@@ -183,7 +182,7 @@ class IOTestScreen(BaseTopNavScreen):
screen_y=int((self.canvas_height - msg_height)/ 2), screen_y=int((self.canvas_height - msg_height)/ 2),
) )
while True: 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: if input == HardwareButtonsConstants.KEY1:
# Note that there are three distinct screen updates that happen at # 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) time.sleep(0.01)
continue continue
# Check for joystick click to take final entropy image # Check for ANYCLICK to take final entropy image
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_PRESS): 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 # Have to manually update last input time since we're not in a wait_for loop
self.hw_inputs.update_last_input_time() self.hw_inputs.update_last_input_time()
self.camera.stop_video_stream_mode() self.camera.stop_video_stream_mode()
@@ -77,7 +77,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
int(self.renderer.canvas_width/2), int(self.renderer.canvas_width/2),
self.renderer.canvas_height - GUIConstants.EDGE_PADDING 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, fill=GUIConstants.BODY_FONT_COLOR,
font=instructions_font, font=instructions_font,
stroke_width=4, stroke_width=4,
@@ -123,7 +123,8 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen):
) )
self.renderer.show_image() 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: if input == HardwareButtonsConstants.KEY_LEFT:
return RET_CODE__BACK_BUTTON return RET_CODE__BACK_BUTTON
@@ -140,7 +141,7 @@ class ToolsDiceEntropyEntryScreen(KeyboardScreen):
self.rows = 3 self.rows = 3
self.cols = 3 self.cols = 3
self.keyboard_font_name = GUIConstants.ICON_FONT_NAME__FONT_AWESOME 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([ self.keys_charset = "".join([
FontAwesomeIconConstants.DICE_ONE, FontAwesomeIconConstants.DICE_ONE,
FontAwesomeIconConstants.DICE_TWO, FontAwesomeIconConstants.DICE_TWO,
@@ -183,8 +184,8 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
super().__post_init__() super().__post_init__()
self.components.append(TextArea( self.components.append(TextArea(
# TRANSLATOR_NOTE: Number of BIP-39 seed words, and the entropy -- in bits, contained within. # TRANSLATOR_NOTE: Final word calc. `mnemonic_length` = 12 or 24. `num_bits` = 7 or 3 (bits of entropy in final word).
text=_("The {}th word is built from {} more entropy bits plus auto-calculated checksum.").format(self.mnemonic_length, self.num_entropy_bits), 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), screen_y=self.top_nav.height + int(GUIConstants.COMPONENT_PADDING/2),
)) ))
@@ -454,7 +455,7 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
self.components.append(IconTextLine( self.components.append(IconTextLine(
# TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor # TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor
label_text=_("Wallet 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, is_text_centered=True,
screen_x=GUIConstants.EDGE_PADDING, screen_x=GUIConstants.EDGE_PADDING,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
+49 -68
View File
@@ -33,6 +33,7 @@ class HardwareButtons(Singleton):
KEY2_PIN = 12 KEY2_PIN = 12
KEY3_PIN = 8 KEY3_PIN = 8
@classmethod @classmethod
def get_instance(cls): def get_instance(cls):
# This is the only way to access the one and only instance # 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.GPIO = GPIO
cls._instance.override_ind = False 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 # 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 = None # Track which direction or button was last pressed
cls._instance.cur_input_started = None # Track when that input began cls._instance.cur_input_started = None # Track when that input began
@@ -65,7 +64,6 @@ class HardwareButtons(Singleton):
return cls._instance return cls._instance
@classmethod @classmethod
def get_instance_no_hardware(cls): def get_instance_no_hardware(cls):
# This is the only way to access the one and only instance # This is the only way to access the one and only instance
@@ -73,17 +71,23 @@ class HardwareButtons(Singleton):
cls._instance = cls.__new__(cls) 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 # TODO: Refactor to keep control in the Controller and not here
from seedsigner.controller import Controller from seedsigner.controller import Controller
controller = Controller.get_instance() controller = Controller.get_instance()
if not release_keys:
release_keys = keys
self.override_ind = False self.override_ind = False
while True: 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) cur_time = int(time.time() * 1000)
if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.is_screensaver_running: 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. # Start the screensaver. Will block execution until input detected.
@@ -99,48 +103,42 @@ class HardwareButtons(Singleton):
# Resume from a fresh loop # Resume from a fresh loop
continue continue
# Check each candidate key to see if it was pressed
for key in keys: 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): if self.GPIO.input(key) == GPIO.LOW:
# when check release is False or the release lock is released (True) if self.cur_input != key:
if self.GPIO.input(key) == GPIO.LOW or self.override_ind: self.cur_input = key
HardwareButtonsConstants.release_lock = False self.cur_input_started = int(time.time() * 1000) # in milliseconds
if self.override_ind: self.last_input_time = self.cur_input_started
self.override_ind = False return key
return HardwareButtonsConstants.OVERRIDE
if self.cur_input != key: else:
self.cur_input = key # Still pressing the same input
self.cur_input_started = int(time.time() * 1000) # in milliseconds if cur_time - self.last_input_time > self.next_repeat_threshold:
self.last_input_time = self.cur_input_started # Too much time has elapsed to consider this the same
# continuous input. Treat as a new separate press.
self.cur_input_started = cur_time
self.last_input_time = cur_time
return key
elif cur_time - self.cur_input_started > self.first_repeat_threshold:
# We're good to relay this immediately as continuous
# input.
self.last_input_time = cur_time
return key return key
else: else:
# Still pressing the same input # We're not yet at the first repeat threshold; triggering
if cur_time - self.last_input_time > self.next_repeat_threshold: # a key now would be too soon and yields a bad user
# Too much time has elapsed to consider this the same # experience when only a single click was intended but
# continuous input. Treat as a new separate press. # a second input is processed because of race condition
self.cur_input_started = cur_time # against human response time to release the button.
self.last_input_time = cur_time # So there has to be a delay before we allow the first
return key # continuous repeat to register. So we'll ignore this
# round's input and **won't update any of our
elif cur_time - self.cur_input_started > self.first_repeat_threshold: # timekeeping vars**. But once we cross the threshold,
# We're good to relay this immediately as continuous # we let the repeats fly.
# input. pass
self.last_input_time = cur_time
return key
else:
# We're not yet at the first repeat threshold; triggering
# a key now would be too soon and yields a bad user
# experience when only a single click was intended but
# a second input is processed because of race condition
# against human response time to release the button.
# So there has to be a delay before we allow the first
# continuous repeat to register. So we'll ignore this
# round's input and **won't update any of our
# timekeeping vars**. But once we cross the threshold,
# we let the repeats fly.
pass
time.sleep(0.01) # wait 10 ms to give CPU chance to do other things time.sleep(0.01) # wait 10 ms to give CPU chance to do other things
@@ -149,29 +147,13 @@ class HardwareButtons(Singleton):
self.last_input_time = int(time.time() * 1000) self.last_input_time = int(time.time() * 1000)
def add_events(self, keys=[]): def trigger_override(self) -> bool:
for key in keys: """ Set the override flag to break out of the current `wait_for` loop """
GPIO.add_event_detect(key, self.GPIO.RISING, callback=HardwareButtons.rising_callback) self.override_ind = True
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:
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: 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: if key:
keys = [key] keys = [key]
for key in keys: for key in keys:
@@ -181,15 +163,16 @@ class HardwareButtons(Singleton):
else: else:
return False return False
def has_any_input(self) -> bool: def has_any_input(self) -> bool:
""" Returns True if any of the keys are pressed """
for key in HardwareButtonsConstants.ALL_KEYS: for key in HardwareButtonsConstants.ALL_KEYS:
if self.GPIO.input(key) == GPIO.LOW: if self.GPIO.input(key) == GPIO.LOW:
return True return True
return False return False
# class used as short hand for static button/channel lookup values # 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: class HardwareButtonsConstants:
if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO
KEY_UP = 31 KEY_UP = 31
@@ -227,5 +210,3 @@ class HardwareButtonsConstants:
KEYS__LEFT_RIGHT_UP_DOWN = [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN] KEYS__LEFT_RIGHT_UP_DOWN = [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN]
KEYS__ANYCLICK = [KEY_PRESS, KEY1, KEY2, KEY3] KEYS__ANYCLICK = [KEY_PRESS, KEY1, KEY2, KEY3]
release_lock = True # released when True, locked when False
+65 -47
View File
@@ -414,6 +414,14 @@ class DecodeQR:
pass pass
# Is it byte data? # 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 # 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR
if len(s) == 32 or len(s) == 16: if len(s) == 32 or len(s) == 16:
try: try:
@@ -499,8 +507,7 @@ class DecodeQR:
def is_bitcoin_address(s): def is_bitcoin_address(s):
if re.search(r'^bitcoin\:.*', s, re.IGNORECASE): if re.search(r'^bitcoin\:.*', s, re.IGNORECASE):
return True return True
elif re.search(r'^((bc1|tb1|bcr|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s): elif re.search(r'^((bc1|tb1|bcr|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s, re.IGNORECASE):
# TODO: Handle regtest bcrt?
return True return True
else: else:
return False return False
@@ -932,61 +939,72 @@ class BitcoinAddressQrDecoder(BaseSingleFrameQrDecoder):
def add(self, segment, qr_type=QRType.BITCOIN_ADDRESS): 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: Input may be prefixed with "bitcoin:" but will be ignored.
self.address = r.group(1)
if re.search(r'^((bc1q|tb1q|bcrt1q|bc1p|tb1p|bcrt1p|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,64})$', self.address) != None:
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)
if r == "1":
# Legacy P2PKH. mainnet
self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
elif r == "m" or r == "n": RegEx searches for a recognizable bitcoin address.
self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET) * The `^` ensures that the specified address prefixes can only match at
the beginning of the address.
elif r == "3": Result will yield the following match groups:
# Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); mainnet * group 1: complete address
# TODO: Would be more correct to use a P2SH constant * group 2: address prefix
self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET) """
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
# 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 addr_prefix == "1":
# Legacy P2PKH. mainnet
self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
elif r == "2": elif addr_prefix in ["m", "n"]:
# Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); testnet / regtest self.address_type = (SettingsConstants.LEGACY_P2PKH, SettingsConstants.TESTNET)
self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET)
elif r == "bc1q": elif addr_prefix == "3":
# Native Segwit (single sig or multisig), mainnet # Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); mainnet
self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET) # TODO: Would be more correct to use a P2SH constant
self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET)
elif r == "tb1q": elif addr_prefix == "2":
# Native Segwit (single sig or multisig), testnet # Nested segwit single sig (p2sh-p2wpkh), nested segwit multisig (p2sh-p2wsh), or legacy multisig (p2sh); testnet / regtest
self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET) self.address_type = (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET)
elif r == "bcrt1q": elif addr_prefix == "bc1q":
# Native Segwit (single sig or multisig), regtest # Native Segwit (single sig or multisig), mainnet
self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.REGTEST) self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET)
elif r == "bc1p": elif addr_prefix == "tb1q":
# Native Segwit (single sig or multisig), mainnet # Native Segwit (single sig or multisig), testnet
self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.MAINNET) self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET)
elif r == "tb1p": elif addr_prefix == "bcrt1q":
# Native Segwit (single sig or multisig), testnet # Native Segwit (single sig or multisig), regtest
self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.TESTNET) self.address_type = (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.REGTEST)
elif r == "bcrt1p": elif addr_prefix == "bc1p":
# Native Segwit (single sig or multisig), regtest self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.MAINNET)
self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.REGTEST)
return DecodeQRStatus.COMPLETE
elif addr_prefix == "tb1p":
self.address_type = (SettingsConstants.TAPROOT, SettingsConstants.TESTNET)
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 return DecodeQRStatus.INVALID
+1 -1
View File
@@ -504,7 +504,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS, attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
abbreviated_name="electrum", abbreviated_name="electrum",
display_name="Electrum seeds", display_name=_mft("Electrum seeds"),
help_text=_mft("Native Segwit only"), help_text=_mft("Native Segwit only"),
visibility=SettingsConstants.VISIBILITY__ADVANCED, visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED), default_value=SettingsConstants.OPTION__DISABLED),
Binary file not shown.
+2 -4
View File
@@ -460,16 +460,14 @@ class PSBTAddressVerificationFailedView(View):
def run(self): def run(self):
if self.is_multisig: if self.is_multisig:
title = _("Caution")
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer". # 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: else:
title = _("Suspicious PSBT")
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer". # 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")) text = _("PSBT's {} address could not be generated from your seed.").format(_("change") if self.is_change else _("self-transfer"))
DireWarningScreen( DireWarningScreen(
title=title, title=_("Suspicious PSBT"),
status_headline=_("Address Verification Failed"), status_headline=_("Address Verification Failed"),
text=text, text=text,
button_data=[ButtonOption("Discard PSBT")], 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.helpers.l10n import mark_for_translation as _mft
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, View, Destination from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, View, Destination
from seedsigner.gui.screens.screen import ButtonOption
logger = logging.getLogger(__name__) 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 # For now, don't even try to re-do the attempted operation, just reset and
# start everything over. # start everything over.
self.controller.resume_main_flow = None self.controller.resume_main_flow = None
return Destination(ErrorView, view_args=dict( return Destination(ScanInvalidQRTypeView)
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(MainMenuView) return Destination(MainMenuView)
@@ -212,3 +207,23 @@ class ScanAddressView(ScanView):
@property @property
def is_valid_qr_type(self): def is_valid_qr_type(self):
return self.decoder.is_address 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)
+65 -87
View File
@@ -279,8 +279,9 @@ class SeedMnemonicInvalidView(View):
def run(self): def run(self):
button_data = [self.EDIT, self.DISCARD] button_data = [self.EDIT, self.DISCARD]
selected_menu_num = self.run_screen( selected_menu_num = self.run_screen(
WarningScreen, DireWarningScreen,
title=_("Invalid Mnemonic!"), title=_("Invalid Mnemonic!"),
status_icon_name=SeedSignerIconConstants.ERROR,
status_headline=None, status_headline=None,
text=_("Checksum failure; not a valid seed phrase."), text=_("Checksum failure; not a valid seed phrase."),
show_back_button=False, show_back_button=False,
@@ -1206,6 +1207,7 @@ class SeedBIP85InvalidChildIndexView(View):
DireWarningScreen( DireWarningScreen(
title=_("BIP-85 Index Error"), title=_("BIP-85 Index Error"),
show_back_button=False, show_back_button=False,
status_icon_name=SeedSignerIconConstants.ERROR,
status_headline=_("Invalid Child Index"), status_headline=_("Invalid Child Index"),
text=_("BIP-85 Child Index must be between 0 and 2^31-1."), text=_("BIP-85 Child Index must be between 0 and 2^31-1."),
button_data=[ButtonOption("Try Again")] button_data=[ButtonOption("Try Again")]
@@ -1366,6 +1368,7 @@ class SeedWordsBackupTestMistakeView(View):
selected_menu_num = DireWarningScreen( selected_menu_num = DireWarningScreen(
title=_("Verification Error"), title=_("Verification Error"),
show_back_button=False, show_back_button=False,
status_icon_name=SeedSignerIconConstants.ERROR,
status_headline=status_headline, status_headline=status_headline,
button_data=button_data, button_data=button_data,
text=text, 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) 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: elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT:
# TODO: add Taproot support sig_type = SettingsConstants.SINGLE_SIG
return Destination(NotYetImplementedView) destination = Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), skip_current_view=True)
derivation_path = embit_utils.get_standard_derivation_path( derivation_path = embit_utils.get_standard_derivation_path(
network=self.controller.unverified_address["network"], network=self.controller.unverified_address["network"],
@@ -1816,10 +1819,6 @@ class SeedAddressVerificationView(View):
self.sig_type = self.controller.unverified_address["sig_type"] self.sig_type = self.controller.unverified_address["sig_type"]
self.network = self.controller.unverified_address["network"] 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 # TODO: This should be in `Seed` or `PSBT` utility class
embit_network = SettingsConstants.map_network_to_embit(self.network) embit_network = SettingsConstants.map_network_to_embit(self.network)
@@ -1848,66 +1847,69 @@ class SeedAddressVerificationView(View):
def run(self): def run(self):
# Start brute-force calculations from the zero-th index # Start brute-force calculations from the zero-th index
self.addr_verification_thread.start() try:
self.addr_verification_thread.start()
button_data = [self.SKIP_10, self.CANCEL] button_data = [self.SKIP_10, self.CANCEL]
script_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) script_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES)
script_type_display = script_type_settings_entry.get_selection_option_display_name_by_value(self.script_type) script_type_display = script_type_settings_entry.get_selection_option_display_name_by_value(self.script_type)
sig_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES) sig_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES)
sig_type_display = sig_type_settings_entry.get_selection_option_display_name_by_value(self.sig_type) sig_type_display = sig_type_settings_entry.get_selection_option_display_name_by_value(self.sig_type)
network_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK) network_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK)
network_display = network_settings_entry.get_selection_option_display_name_by_value(self.network) network_display = network_settings_entry.get_selection_option_display_name_by_value(self.network)
mainnet = network_settings_entry.get_selection_option_display_name_by_value(SettingsConstants.MAINNET) mainnet = network_settings_entry.get_selection_option_display_name_by_value(SettingsConstants.MAINNET)
# Display the Screen to show the brute-forcing progress. # Display the Screen to show the brute-forcing progress.
# Using a loop here to handle the SKIP_10 button presses to increment the counter # Using a loop here to handle the SKIP_10 button presses to increment the counter
# and resume displaying the screen. User won't even notice that the Screen is # and resume displaying the screen. User won't even notice that the Screen is
# being re-constructed. # being re-constructed.
while True: while True:
selected_menu_num = self.run_screen( selected_menu_num = self.run_screen(
seed_screens.SeedAddressVerificationScreen, seed_screens.SeedAddressVerificationScreen,
address=self.address, address=self.address,
derivation_path=self.derivation_path, derivation_path=self.derivation_path,
script_type=script_type_display, script_type=script_type_display,
sig_type=sig_type_display, sig_type=sig_type_display,
network=network_display, network=network_display,
is_mainnet=network_display == mainnet, is_mainnet=network_display == mainnet,
threadsafe_counter=self.threadsafe_counter, threadsafe_counter=self.threadsafe_counter,
verified_index=self.verified_index, verified_index=self.verified_index,
button_data=button_data, button_data=button_data,
) )
if self.verified_index.cur_count is not None:
break
if selected_menu_num == RET_CODE__BACK_BUTTON:
break
if selected_menu_num is None:
# Only happens in the test suite; the screen isn't actually executed so
# it returns before the brute force thread has completed.
time.sleep(0.1)
continue
if button_data[selected_menu_num] == self.SKIP_10:
self.threadsafe_counter.increment(10)
elif button_data[selected_menu_num] == self.CANCEL:
break
if self.verified_index.cur_count is not None: if self.verified_index.cur_count is not None:
break # Successfully verified the addr; update the data
self.controller.unverified_address["verified_index"] = self.verified_index.cur_count
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))
if selected_menu_num == RET_CODE__BACK_BUTTON: finally:
break
if selected_menu_num is None:
# Only happens in the test suite; the screen isn't actually executed so
# it returns before the brute force thread has completed.
time.sleep(0.1)
continue
if button_data[selected_menu_num] == self.SKIP_10:
self.threadsafe_counter.increment(10)
elif button_data[selected_menu_num] == self.CANCEL:
break
if self.verified_index.cur_count is not None:
# Successfully verified the addr; update the data
self.controller.unverified_address["verified_index"] = self.verified_index.cur_count
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:
# Halt the thread if the user gave up (will already be stopped if it verified the # Halt the thread if the user gave up (will already be stopped if it verified the
# target addr). # target addr).
self.addr_verification_thread.stop() self.addr_verification_thread.stop()
# Block until the thread has stopped
while self.addr_verification_thread.is_alive(): while self.addr_verification_thread.is_alive():
time.sleep(0.01) time.sleep(0.01)
@@ -1933,7 +1935,7 @@ class SeedAddressVerificationView(View):
if self.seed: if self.seed:
self.xpub = self.seed.get_xpub(wallet_path=self.derivation_path, network=Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK)) self.xpub = self.seed.get_xpub(wallet_path=self.derivation_path, network=Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK))
def run(self): def run(self):
from seedsigner.helpers import embit_utils from seedsigner.helpers import embit_utils
@@ -1965,7 +1967,7 @@ class SeedAddressVerificationView(View):
# Increment our index counter # Increment our index counter
self.threadsafe_counter.increment() self.threadsafe_counter.increment()
class SeedAddressVerificationSuccessView(View): class SeedAddressVerificationSuccessView(View):
@@ -1977,36 +1979,12 @@ class SeedAddressVerificationSuccessView(View):
def run(self): def run(self):
from seedsigner.gui.screens.screen import LargeIconStatusScreen self.run_screen(
address = self.controller.unverified_address["address"] seed_screens.SeedAddressVerificationSuccessScreen,
sig_type = self.controller.unverified_address["sig_type"] address = self.controller.unverified_address["address"],
verified_index = self.controller.unverified_address["verified_index"] verified_index = self.controller.unverified_address["verified_index"],
verified_index_is_change = self.controller.unverified_address["verified_index_is_change"] 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
) )
LargeIconStatusScreen(
status_headline=_("Address Verified"),
text=text,
show_back_button=False,
).display()
return Destination(MainMenuView) 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.helpers.l10n import mark_for_translation as _mft
from seedsigner.gui.components import SeedSignerIconConstants from seedsigner.gui.components import SeedSignerIconConstants
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON 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 import Settings, SettingsConstants
from seedsigner.models.settings_definition import SettingsDefinition from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.threads import BaseThread from seedsigner.models.threads import BaseThread
@@ -245,6 +245,7 @@ class PowerOptionsView(View):
class RestartView(View): class RestartView(View):
def run(self): def run(self):
from seedsigner.gui.screens.screen import ResetScreen
thread = RestartView.DoResetThread() thread = RestartView.DoResetThread()
thread.start() thread.start()
self.run_screen(ResetScreen) self.run_screen(ResetScreen)
@@ -270,6 +271,7 @@ class RestartView(View):
class PowerOffView(View): class PowerOffView(View):
def run(self): def run(self):
from seedsigner.gui.screens.screen import PowerOffNotRequiredScreen
self.run_screen(PowerOffNotRequiredScreen) self.run_screen(PowerOffNotRequiredScreen)
return Destination(BackStackView) return Destination(BackStackView)
@@ -300,6 +302,7 @@ class NotYetImplementedView(View):
class ErrorView(View): class ErrorView(View):
title: str = _mft("Error") title: str = _mft("Error")
show_back_button: bool = True show_back_button: bool = True
status_icon_name: str = SeedSignerIconConstants.ERROR
status_headline: str = None status_headline: str = None
text: str = None text: str = None
button_text: str = None button_text: str = None
@@ -307,8 +310,9 @@ class ErrorView(View):
def run(self): def run(self):
self.run_screen( self.run_screen(
WarningScreen, ErrorScreen,
title=self.title, title=self.title,
status_icon_name=self.status_icon_name,
status_headline=self.status_headline, status_headline=self.status_headline,
text=self.text, text=self.text,
button_data=[ButtonOption(self.button_text)], button_data=[ButtonOption(self.button_text)],
@@ -324,9 +328,14 @@ class NetworkMismatchErrorView(ErrorView):
def __post_init__(self): def __post_init__(self):
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
self.title: str = _("Network Mismatch")
self.show_back_button: bool = False # TRANSLATOR_NOTE: The network setting (mainnet/testnet/regtest) doesn't match the provided derivation path
self.button_text: str = _("Change Setting") 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) self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True)
super().__post_init__() super().__post_init__()
@@ -342,10 +351,9 @@ class NetworkMismatchErrorView(ErrorView):
class UnhandledExceptionView(View): class UnhandledExceptionView(View):
error: list[str] error: list[str]
def run(self): def run(self):
self.run_screen( self.run_screen(
DireWarningScreen, ErrorScreen,
title=_("System Error"), title=_("System Error"),
status_headline=self.error[0], status_headline=self.error[0],
text=self.error[1] + "\n" + self.error[2], 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.controller import Controller
from seedsigner.gui.renderer import Renderer from seedsigner.gui.renderer import Renderer
from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen 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.hardware.microsd import MicroSD
from seedsigner.helpers import embit_utils from seedsigner.helpers import embit_utils
from seedsigner.models.decode_qr import DecodeQR 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 import Settings
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView, 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.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 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(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(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")),
ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
ScreenshotConfig(ErrorView, dict( ScreenshotConfig(scan_views.ScanInvalidQRTypeView)
title="Error",
status_headline="Unknown QR Type",
text="QRCode is invalid or is a data format not yet supported.",
button_text="Back",
)),
] ]
} }
+113 -61
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"] assert d.get_seed_phrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"]
def test_bitcoin_address(): # Test data for bitcoin address decoding. All generated from test key: ["abandon"] * 11 + ["about"]
bad1 = "loremipsum" legacy_address_mainnet = "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA"
bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae" legacy_address_testnet = "mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV"
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"
d = DecodeQR()
d.add_data(bad1)
assert d.qr_type == QRType.INVALID
d = DecodeQR()
d.add_data(legacy_address1)
assert d.get_address() == legacy_address1
assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
d = DecodeQR()
d.add_data(legacy_address2)
assert d.get_address() == legacy_address2
assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
d = DecodeQR()
d.add_data(main_bech32_address)
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 nested_segwit_address_mainnet = "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf"
assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) nested_segwit_address_testnet = "2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2"
d = DecodeQR() native_segwit_address_mainnet = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
d.add_data(main_bech32_address2) native_segwit_address_testnet = "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl"
native_segwit_address_regtest = "bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk"
assert d.get_address() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET) taproot_address_mainnet = "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
taproot_address_testnet = "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj"
taproot_address_regtest = "bcrt1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqjeprhg"
def test_bitcoin_address():
"""
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(data)
assert d.get_address() == address
assert d.get_address_type() == (expected_script_type, expected_network)
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()
status = d.add_data(bad_input)
assert status == DecodeQRStatus.INVALID
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(data)
assert d.get_address_type() == (expected_script_type, expected_network)
if not is_case_sensitive:
assert d.get_address() == addr_variation.lower()
else:
assert d.get_address() == addr_variation
# 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)
# 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.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.SIGN_MESSAGE), 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(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), FlowStep(MainMenuView),
]) ])
+20 -11
View File
@@ -257,16 +257,25 @@ class TestToolsFlows(FlowTest):
settings = controller.settings settings = controller.settings
settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST) settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST)
def load_address_into_decoder(view: scan_views.ScanView): addrs = [
# Native segwit regtest receive addr @ index 6 # Native segwit regtest receive addr @ index 6
view.decoder.add_data("bcrt1q4e9q5taxnsvc6m0uxv6h75mkzvnkxeqk6l90u2") "bcrt1q4e9q5taxnsvc6m0uxv6h75mkzvnkxeqk6l90u2",
self.run_sequence([ # Taproot regtest change addr @ index 48
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), "bcrt1pj5v8ean2hc5lh2djsgfx4j9uc0n67942ngv6q9r49qv88ex5mrwsn3u4f7",
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.VERIFY_ADDRESS), ]
FlowStep(scan_views.ScanAddressView, before_run=load_address_into_decoder), # simulate read address QR
FlowStep(seed_views.AddressVerificationStartView, is_redirect=True), for test_addr in addrs:
FlowStep(seed_views.SeedSelectSeedView, screen_return_value=0), def load_address_into_decoder(view: scan_views.ScanView):
FlowStep(seed_views.SeedAddressVerificationView), # Native segwit regtest receive addr @ index 6
FlowStep(seed_views.SeedAddressVerificationSuccessView), view.decoder.add_data(test_addr)
])
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.VERIFY_ADDRESS),
FlowStep(scan_views.ScanAddressView, before_run=load_address_into_decoder), # simulate read address QR
FlowStep(seed_views.AddressVerificationStartView, is_redirect=True),
FlowStep(seed_views.SeedSelectSeedView, screen_return_value=0),
FlowStep(seed_views.SeedAddressVerificationView),
FlowStep(seed_views.SeedAddressVerificationSuccessView),
])
+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.decode_qr import DecodeQR, DecodeQRStatus
from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder
from seedsigner.models.qr_type import QRType 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 # 12-word seed, multiple null bytes in a row
entropy = os.urandom(10) + b'\x00\x00' + os.urandom(4) entropy = os.urandom(10) + b'\x00\x00' + os.urandom(4)
run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR) 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(" ") seed_phrase = input("\nEnter 12- or 24-word test seed phrase: ").strip().split(" ")
if format == COMPACT: 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: 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 = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3)
qr.add_data(encoder.next_part()) qr.add_data(encoder.next_part())