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