mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
An output counted as change when its policy matched the inputs' and its rebuilt scriptPubKey matched what the output committed to. Neither step established that the key involved was ours. The policy comparison included cosigners resolved from the coordinator's own global xpubs, so one misannotated fingerprint made an output stop matching and skip verification altogether, and multisig never checked our key against the committed script at all. Outputs now compare on script shape alone, and every candidate proves ownership: single sig by rebuilding from the claimed derivation path, multisig by finding this seed's key in the committed script. Where the psbt's account of an output contradicts what the output commits to, the parse refuses rather than quietly reclassifying. A claim set too malformed to answer that question, one populating both derivation path maps or claiming more keys than its script uses, is refused as well. change_data now carries the verified derivation path in place of the coordinator's claimed fingerprints and paths, so the views no longer re-derive trust from strings the parse has already settled. Taproot mismatches stay exempt. A script tree tweaks the internal key, so honest taproot change fails the rebuild too, and embit leaves PSBT_OUT_TAP_TREE unparsed, which is what would tell the two apart.
613 lines
40 KiB
Python
613 lines
40 KiB
Python
import embit
|
|
import os
|
|
import pathlib
|
|
import pytest
|
|
import random
|
|
import sys
|
|
import time
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from PIL import ImageFont
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
|
|
from embit import compact
|
|
from embit.psbt import PSBT, OutputScope
|
|
from embit.script import Script
|
|
|
|
from seedsigner.helpers.version import Version, VersionUtils
|
|
|
|
# Prevent importing modules w/Raspi hardware dependencies.
|
|
# These must precede any SeedSigner imports.
|
|
sys.modules['seedsigner.hardware.displays.st7789_mpy'] = MagicMock()
|
|
sys.modules['seedsigner.hardware.displays.ili9341'] = MagicMock()
|
|
sys.modules['seedsigner.views.screensaver.ScreensaverScreen'] = MagicMock()
|
|
sys.modules['RPi'] = MagicMock()
|
|
sys.modules['RPi.GPIO'] = MagicMock()
|
|
sys.modules['seedsigner.hardware.camera.Camera'] = MagicMock()
|
|
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
|
|
|
from seedsigner.controller import Controller
|
|
from seedsigner.gui.renderer import Renderer
|
|
from seedsigner.gui.screens.screen import BaseScreen
|
|
from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen
|
|
from seedsigner.gui.toast import RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
|
|
from seedsigner.gui.toast import DefaultToast, InfoToast, SuccessToast, WarningToast, ErrorToast, DireWarningToast
|
|
from seedsigner.hardware.microsd import MicroSD
|
|
from seedsigner.helpers import embit_utils
|
|
from seedsigner.models.decode_qr import DecodeQR
|
|
from seedsigner.models.encode_qr import BaseQrEncoder
|
|
from seedsigner.models.psbt_parser import OPCODES, PSBTParser
|
|
from seedsigner.models.qr_type import QRType
|
|
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, RemoveMicroSDWarningView, NotYetImplementedView, UnhandledExceptionView,
|
|
psbt_views, seed_views, settings_views, tools_views, scan_views)
|
|
from seedsigner.views.screensaver import OpeningSplashView
|
|
from seedsigner.views.view import CameraConnectionErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView
|
|
|
|
from .utils import ScreenshotComplete, ScreenshotConfig, ScreenshotRenderer
|
|
|
|
import warnings; warnings.warn = lambda *args, **kwargs: None
|
|
|
|
# Dynamically generate a pytest test run for each locale
|
|
@pytest.mark.parametrize("locale", [x for x, y in SettingsConstants.get_detected_languages()])
|
|
def test_generate_all(locale, target_locale):
|
|
"""
|
|
`target_locale` is a fixture created in conftest.py via the `--locale` command line arg.
|
|
|
|
Optionally skips all other locales.
|
|
"""
|
|
if target_locale and locale != target_locale:
|
|
pytest.skip(f"Skipping {locale}")
|
|
|
|
if not ImageFont.core.HAVE_RAQM:
|
|
# We can't generate pixel-perfect screenshots that match what gets rendered on
|
|
# the device if we don't have libraqm.
|
|
pytest.fail("libraqm is not installed.")
|
|
|
|
generate_screenshots(locale)
|
|
|
|
|
|
|
|
"""**************************************************************************************
|
|
Set up global test data that will be re-used across a variety of screenshots and for
|
|
all locales.
|
|
**************************************************************************************"""
|
|
# Single sig ("abandon" test wallet) tx; 1mil sat input, 1 external output, 1 self-transfer, 1 change output, 400 sat fee
|
|
BASE64_SINGLE_SIG_PSBT = """cHNidP8BAJACAAAAAT8SmJzLhTMNgtn9QOmBmet0nnqqIJpsgpgBN5JWNJCxAQAAAAD9////A5CfBwAAAAAAFgAULzSqHPAKU7BVopGgOn1F8KaYi1KQ0AMAAAAAABYAFGQh2ztS8DzX4kGVKUKQhFPrlNNIkNADAAAAAAAWABRvoBZQCjxqc367Jg4t3KeLqSNFWGYAAABPAQQ1h88DDvSxr4AAAAA8jCA37kwWIdoNNI21EWNwmmItDSg43ebYQZxR9jAcYgO4jg++P2RjN+2TvAwPO4Q/z30lieXsiEdU5kAgJ6iQtBBzxdoKVAAAgAEAAIAAAACAAAEAcQIAAAABF84F9MpvLC1H3Cyews1xoNZ4ch3uJMu8jonehCIqmScAAAAAAP3///8CM6/2KQEAAAAWABQQumvlzzcWsGXBNIOliqXTvr9YxEBCDwAAAAAAFgAU0MSj7wnpl7bpnjl+UY/j5BoRjKFNAAAAAQEfQEIPAAAAAAAWABTQxKPvCemXtumeOX5Rj+PkGhGMoQEDBAEAAAAiBgLnqyU3tdSelwMJquBunknzbOHJ/rvUTsjg0cygtPnDGRhzxdoKVAAAgAEAAIAAAACAAAAAAAAAAAAAIgIDXUnszVTQCZ5DZ2J3x6bUYl1hHaiKXfSb+VF6d5Gnd6UYc8XaClQAAIABAACAAAAAgAEAAAAAAAAAAAAiAgPu7SBaaQIv7UpioCRX82mbGcBr90v4AazG2a6EvBap4RhzxdoKVAAAgAEAAIAAAACAAAAAAAEAAAAA"""
|
|
|
|
BASE64_MULTISIG_PSBT = """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"""
|
|
mnemonic_12b = ["abandon"] * 11 + ["about"]
|
|
seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
|
|
|
def add_op_return_to_psbt(psbt: PSBT, raw_payload_data: bytes):
|
|
data = (compact.to_bytes(OPCODES.OP_RETURN) +
|
|
compact.to_bytes(OPCODES.OP_PUSHDATA1) +
|
|
compact.to_bytes(len(raw_payload_data)) +
|
|
raw_payload_data)
|
|
script = Script(data)
|
|
output = OutputScope()
|
|
output.script_pubkey = script
|
|
output.value = 0
|
|
psbt.outputs.append(output)
|
|
return psbt.to_string()
|
|
|
|
# Prep a PSBT with a human-readable OP_RETURN
|
|
raw_payload_data = "Chancellor on the brink of third bailout for banks".encode()
|
|
psbt = PSBT.from_base64(BASE64_MULTISIG_PSBT)
|
|
|
|
# Simplify the output side
|
|
output = psbt.outputs[-1]
|
|
psbt.outputs.clear()
|
|
psbt.outputs.append(output)
|
|
assert len(psbt.outputs) == 1
|
|
BASE64_PSBT_WITH_OP_RETURN_TEXT = add_op_return_to_psbt(psbt, raw_payload_data)
|
|
|
|
# Prep a PSBT with a (repeatably) random 80-byte OP_RETURN
|
|
random.seed(6102)
|
|
BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES = add_op_return_to_psbt(PSBT.from_base64(BASE64_MULTISIG_PSBT), random.randbytes(80))
|
|
|
|
mnemonic_12 = "forum undo fragile fade shy sign arrest garment culture tube off merit".split()
|
|
mnemonic_24 = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split()
|
|
seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
|
seed_24 = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
|
seed_24_w_passphrase = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
|
|
|
MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx"""
|
|
|
|
# Grab the most recent release version info for the "release build" splash screenshots.
|
|
(latest_release_version_name, latest_release_version_timestamp) = VersionUtils._fetch_latest_seedsigner_release_tag()
|
|
if not latest_release_version_name or not latest_release_version_timestamp:
|
|
# The fetch can fail (offline, or the unauthenticated GitHub API rate-limit that
|
|
# shared CI runners regularly hit). Substitute a placeholder so the "release build"
|
|
# screenshots still render, rather than feeding None into the Version mock (which
|
|
# would otherwise crash both the splash screen and the Version settings screen on
|
|
# None slicing / len()).
|
|
print("Could not fetch latest release version from GitHub; using a placeholder")
|
|
latest_release_version_name = latest_release_version_name or "version_not_available"
|
|
latest_release_version_timestamp = latest_release_version_timestamp or datetime(1970, 1, 1)
|
|
|
|
|
|
# Wrap QRDisplayScreen's `render_brightness_tip` in a simple View + Screen so we
|
|
# can call it outside of its child thread and generate a screenshot.
|
|
class SeedExportXpubQR_ScreenBrightnessView(seed_views.SeedExportXpubQRDisplayView):
|
|
@dataclass
|
|
class QRDisplayScreenBrightnessTipScreen(BaseScreen):
|
|
qr_encoder: BaseQrEncoder = None
|
|
|
|
def _render(self):
|
|
from seedsigner.gui.screens.screen import QRDisplayScreen
|
|
image = self.qr_encoder.part_to_image(self.qr_encoder.cur_part(), 240, 240, border=2, background_color="white")
|
|
QRDisplayScreen.QRDisplayThread.render_brightness_tip(None, image)
|
|
self.renderer.show_image(image)
|
|
|
|
def run(self):
|
|
self.run_screen(
|
|
SeedExportXpubQR_ScreenBrightnessView.QRDisplayScreenBrightnessTipScreen,
|
|
qr_encoder=self.qr_encoder, # initialized by SeedExportXpubQRDisplayView
|
|
)
|
|
|
|
|
|
|
|
def generate_screenshots(locale):
|
|
"""
|
|
The `Renderer` class is mocked so that calls in the normal code are ignored
|
|
(necessary to avoid having it trying to wire up hardware dependencies).
|
|
|
|
When the `Renderer` instance is needed, we patch in our own test-only
|
|
`ScreenshotRenderer`.
|
|
"""
|
|
# Prep the ScreenshotRenderer that will be patched over the normal Renderer
|
|
screenshot_root = os.path.join(os.getcwd(), "seedsigner-screenshots")
|
|
ScreenshotRenderer.configure_instance()
|
|
screenshot_renderer: ScreenshotRenderer = ScreenshotRenderer.get_instance()
|
|
|
|
# Replace the core `Singleton` calls so that only our ScreenshotRenderer is used.
|
|
Renderer.configure_instance = Mock()
|
|
Renderer.get_instance = Mock(return_value=screenshot_renderer)
|
|
|
|
|
|
def setup_screenshots(locale: str) -> dict[str, list[ScreenshotConfig]]:
|
|
""" Set up some test data that we'll need in the `Controller` for certain Views """
|
|
# Must reset the Controller so each locale gets a fresh start
|
|
Controller.reset_instance()
|
|
controller = Controller.get_instance()
|
|
|
|
controller.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [attr for attr, name in SettingsConstants.ALL_SIG_TYPES])
|
|
controller.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [attr for attr, name in SettingsConstants.ALL_SCRIPT_TYPES])
|
|
|
|
controller.storage.seeds.append(seed_12)
|
|
controller.storage.seeds.append(seed_12b)
|
|
controller.storage.seeds.append(seed_24)
|
|
controller.storage.set_pending_seed(seed_24_w_passphrase)
|
|
|
|
# Pending mnemonic for ToolsCalcFinalWordShowFinalWordView
|
|
controller.storage.init_pending_mnemonic(num_words=12)
|
|
for i, word in enumerate(mnemonic_12[:11]):
|
|
controller.storage.update_pending_mnemonic(word=word, index=i)
|
|
controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet)
|
|
|
|
# Load a PSBT into memory
|
|
decoder = DecodeQR()
|
|
decoder.add_data(BASE64_MULTISIG_PSBT)
|
|
controller.psbt = decoder.get_psbt()
|
|
controller.psbt_seed = seed_12b
|
|
|
|
# Message signing data
|
|
derivation_path = "m/84h/0h/0h/0/0"
|
|
controller.sign_message_data = {
|
|
"seed": seed_12,
|
|
"derivation_path": derivation_path,
|
|
"message": "I attest that I control this bitcoin address blah blah blah",
|
|
"addr_format": embit_utils.parse_derivation_path(derivation_path)
|
|
}
|
|
|
|
# so we get a choice for transcribe seed qr format
|
|
controller.settings.set_value(
|
|
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
|
value=SettingsConstants.OPTION__ENABLED
|
|
)
|
|
|
|
# Automatically populate all Settings options Views
|
|
settings_views_list = []
|
|
def add_settings_entries(visibility = SettingsConstants.VISIBILITY__GENERAL):
|
|
for settings_entry in SettingsDefinition.settings_entries:
|
|
if settings_entry.visibility != visibility:
|
|
continue
|
|
|
|
if settings_entry.attr_name == SettingsConstants.SETTING__LOCALE:
|
|
# Locale selection has its own dedicated View
|
|
settings_views_list.append(ScreenshotConfig(settings_views.LocaleSelectionView))
|
|
else:
|
|
# Generic SettingsEntry selection View
|
|
settings_views_list.append(ScreenshotConfig(settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), screenshot_name=f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
|
|
|
# Add the top level "General" settings menu and entries
|
|
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView))
|
|
|
|
# Scroll the general settings to the bottom
|
|
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView, dict(selected_button_option=settings_views.SettingsMenuView.VERSION), screenshot_name="SettingsMenuView_2"))
|
|
add_settings_entries(SettingsConstants.VISIBILITY__GENERAL)
|
|
|
|
# Add the "Advanced" menu...
|
|
settings_views_list.append(
|
|
ScreenshotConfig(
|
|
settings_views.SettingsMenuView,
|
|
dict(
|
|
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
|
),
|
|
screenshot_name="SettingsMenuView__Advanced"
|
|
)
|
|
)
|
|
|
|
# ...and Advanced entries
|
|
add_settings_entries(SettingsConstants.VISIBILITY__ADVANCED)
|
|
|
|
# Render the nested "Advanced" -> "Hardware" submenu
|
|
settings_views_list.append(
|
|
ScreenshotConfig(
|
|
settings_views.SettingsMenuView,
|
|
dict(visibility=SettingsConstants.VISIBILITY__HARDWARE),
|
|
screenshot_name="SettingsMenuView__Hardware"
|
|
)
|
|
)
|
|
add_settings_entries(SettingsConstants.VISIBILITY__HARDWARE)
|
|
|
|
settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
|
settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
|
|
|
# Set up mocks to provide whatever temporary data/state a particular screenshot
|
|
# might need.
|
|
@contextmanager
|
|
def mock_load_psbt(base64_psbt: str, seed: Seed = seed_12b):
|
|
"""
|
|
Reusable utility for other context managers to mock loading a PSBT into the
|
|
Controller.
|
|
"""
|
|
decoder = DecodeQR()
|
|
decoder.add_data(base64_psbt)
|
|
with patch.object(controller, 'psbt', decoder.get_psbt()):
|
|
with patch.object(controller, 'psbt_seed', seed):
|
|
with patch.object(controller, 'psbt_parser', PSBTParser(p=controller.psbt, seed=seed)):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_single_sig_psbt_loaded():
|
|
with mock_load_psbt(BASE64_SINGLE_SIG_PSBT):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_multisig_psbt_loaded():
|
|
with mock_load_psbt(BASE64_MULTISIG_PSBT):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_multisig_wallet_descriptor_loaded():
|
|
with patch.object(controller, 'multisig_wallet_descriptor', embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR)):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_multisig_psbt_and_descriptor_loaded():
|
|
with mock_multisig_psbt_loaded():
|
|
with mock_multisig_wallet_descriptor_loaded():
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_address_verification_data_loaded():
|
|
fake_addr_verification_data = dict(
|
|
# These are all totally fake data
|
|
address="bc1q6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek",
|
|
network=SettingsConstants.MAINNET,
|
|
sig_type=SettingsConstants.SINGLE_SIG,
|
|
script_type=SettingsConstants.NATIVE_SEGWIT,
|
|
derivation_path = "m/84h/0h/0h",
|
|
verified_index=5,
|
|
verified_index_is_change=False
|
|
)
|
|
with patch.object(controller, 'unverified_address', fake_addr_verification_data):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_controller_psbt_seed_empty():
|
|
# Have to ensure this is cleared out in order to get the seed selection screen
|
|
with patch.object(controller, 'psbt_seed', None):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_psbt_with_op_return_loaded():
|
|
with mock_load_psbt(BASE64_PSBT_WITH_OP_RETURN_TEXT):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_psbt_with_op_return_raw_bytes_loaded():
|
|
with mock_load_psbt(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_version_to_most_recent_release():
|
|
# Patch the Version get_* calls to the most recent release
|
|
with patch.multiple(Version,
|
|
get_version_name=Mock(return_value=latest_release_version_name),
|
|
get_version_fork=Mock(return_value="SeedSigner"),
|
|
get_version_timestamp=Mock(return_value=latest_release_version_timestamp),
|
|
get_short_commit_hash=Mock(return_value="abcd1234") # dummy value should be ignored
|
|
):
|
|
# Also have to mock us into SeedSigner OS
|
|
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
|
|
yield
|
|
|
|
|
|
screenshot_sections = {
|
|
"Main Menu Views": [
|
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True), mock_context_manager=mock_version_to_most_recent_release),
|
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos", mock_context_manager=mock_version_to_most_recent_release),
|
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True), screenshot_name="OpeningSplashView_current_git_state"),
|
|
ScreenshotConfig(MainMenuView),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_RemoveSDCardToast', toast_thread=RemoveSDCardToastManagerThread(activation_delay=0, duration=0)),
|
|
ScreenshotConfig(RemoveMicroSDWarningView),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_DefaultToast', toast_thread=DefaultToast("This is a default text toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_InfoToast', toast_thread=InfoToast("This is an info toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SuccessToast', toast_thread=SuccessToast("This is a success toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_WarningToast', toast_thread=WarningToast("This is a warning toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_DireWarningToast', toast_thread=DireWarningToast("This is a dire warning toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_ErrorToast', toast_thread=ErrorToast("This is an error toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(PowerOptionsView),
|
|
ScreenshotConfig(RestartView),
|
|
ScreenshotConfig(PowerOffView),
|
|
],
|
|
"Seed Views": [
|
|
ScreenshotConfig(seed_views.SeedsMenuView),
|
|
ScreenshotConfig(seed_views.LoadSeedView),
|
|
ScreenshotConfig(seed_views.SeedMnemonicEntryView),
|
|
ScreenshotConfig(seed_views.SeedMnemonicInvalidView),
|
|
ScreenshotConfig(seed_views.SeedFinalizeView),
|
|
ScreenshotConfig(seed_views.SeedAddPassphraseView, screenshot_name="SeedAddPassphraseView_lowercase"),
|
|
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__UPPERCASE_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_uppercase"),
|
|
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__DIGITS_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_digits"),
|
|
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_1_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_symbols_1"),
|
|
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_2_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_symbols_2"),
|
|
ScreenshotConfig(seed_views.SeedAddPassphraseExitDialogView),
|
|
ScreenshotConfig(seed_views.SeedReviewPassphraseView),
|
|
|
|
ScreenshotConfig(seed_views.SeedOptionsView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedBackupView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedExportXpubSigTypeView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedExportXpubScriptTypeView, dict(seed=seed_12, sig_type="msig")),
|
|
ScreenshotConfig(seed_views.SeedExportXpubCustomDerivationView, dict(seed=seed_12, sig_type="ss", script_type="")),
|
|
ScreenshotConfig(seed_views.SeedExportXpubQRFormatView, dict(seed=seed_12, sig_type="ss", script_type="nat")),
|
|
ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed=seed_12, sig_type="msig", script_type="nes", xpub_qr_format="urca", custom_derivation="")),
|
|
ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed=seed_12, sig_type="ss", script_type="nat", xpub_qr_format="urca", custom_derivation="")),
|
|
ScreenshotConfig(SeedExportXpubQR_ScreenBrightnessView, dict(seed=seed_12, xpub_qr_format="urca", derivation_path="m/84'/0'/0'")),
|
|
|
|
ScreenshotConfig(seed_views.SeedWordsWarningView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedWordsView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedWordsView, dict(seed=seed_12, page_index=2), screenshot_name="SeedWordsView_2"),
|
|
ScreenshotConfig(seed_views.SeedBIP85SelectNumWordsView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedBIP85SelectChildIndexView, dict(seed=seed_12, num_words=24)),
|
|
ScreenshotConfig(seed_views.SeedBIP85InvalidChildIndexView, dict(seed=seed_12, num_words=12)),
|
|
ScreenshotConfig(seed_views.SeedWordsBackupTestPromptView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedWordsBackupTestView, dict(seed=seed_12, rand_seed=6102)),
|
|
ScreenshotConfig(seed_views.SeedWordsBackupTestMistakeView, dict(seed=seed_12, cur_index=7, wrong_word="satoshi")),
|
|
ScreenshotConfig(seed_views.SeedWordsBackupTestSuccessView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRFormatView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWarningView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed=seed_12, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Compact"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed=seed_12, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Standard"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed=seed_24, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Compact"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed=seed_24, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Standard"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed=seed_12, seedqr_format=QRType.SEED__COMPACTSEEDQR, initial_zone_x=1, initial_zone_y=1), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Compact"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed=seed_12, seedqr_format=QRType.SEED__SEEDQR, initial_zone_x=2, initial_zone_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"),
|
|
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmWrongSeedView),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmInvalidQRView),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmSuccessView, dict(seed=seed_12)),
|
|
|
|
# Screenshot can't render live preview screens
|
|
# ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed=seed_12)),
|
|
|
|
ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), screenshot_name="SeedSelectSeedView_address_verification"),
|
|
ScreenshotConfig(seed_views.AddressVerificationSigTypeView),
|
|
ScreenshotConfig(seed_views.SeedAddressVerificationView, dict(seed=seed_12), mock_context_manager=mock_address_verification_data_loaded),
|
|
ScreenshotConfig(seed_views.SeedAddressVerificationSuccessView, mock_context_manager=mock_address_verification_data_loaded),
|
|
|
|
ScreenshotConfig(seed_views.LoadMultisigWalletDescriptorView),
|
|
ScreenshotConfig(seed_views.MultisigWalletDescriptorView, mock_context_manager=mock_multisig_wallet_descriptor_loaded),
|
|
ScreenshotConfig(seed_views.SeedDiscardView, dict(seed=seed_12)),
|
|
|
|
ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__SIGN_MESSAGE), screenshot_name="SeedSelectSeedView_sign_message"),
|
|
ScreenshotConfig(seed_views.SeedSignMessageConfirmMessageView),
|
|
ScreenshotConfig(seed_views.SeedSignMessageConfirmAddressView),
|
|
|
|
ScreenshotConfig(seed_views.SeedElectrumMnemonicStartView),
|
|
],
|
|
"PSBT Views": [
|
|
ScreenshotConfig(psbt_views.PSBTSelectSeedView, mock_context_manager=mock_controller_psbt_seed_empty),
|
|
ScreenshotConfig(psbt_views.PSBTOverviewView, mock_context_manager=mock_multisig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTUnsupportedScriptTypeWarningView),
|
|
ScreenshotConfig(psbt_views.PSBTNoChangeWarningView),
|
|
ScreenshotConfig(psbt_views.PSBTMathView, mock_context_manager=mock_multisig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTAddressDetailsView, dict(address_num=0), mock_context_manager=mock_multisig_psbt_loaded),
|
|
|
|
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_single_sig_change_verified", mock_context_manager=mock_single_sig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=1), screenshot_name="PSBTChangeDetailsView_single_sig_self_transfer_verified", mock_context_manager=mock_single_sig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_unverified", mock_context_manager=mock_multisig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_verified", mock_context_manager=mock_multisig_psbt_and_descriptor_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTOverviewView, screenshot_name="PSBTOverviewView_op_return", mock_context_manager=mock_psbt_with_op_return_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_text", mock_context_manager=mock_psbt_with_op_return_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_raw_hex_data", mock_context_manager=mock_psbt_with_op_return_raw_bytes_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTSurplusDerivationPathsView),
|
|
ScreenshotConfig(psbt_views.PSBTMixedDerivationPathTypesView),
|
|
ScreenshotConfig(psbt_views.PSBTOutputOwnershipContradictionView),
|
|
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True), screenshot_name="PSBTAddressVerificationFailedView_multisig_change"),
|
|
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False), screenshot_name="PSBTAddressVerificationFailedView_multisig_selftransfer"),
|
|
ScreenshotConfig(psbt_views.PSBTOutputOwnershipClaimFailedView),
|
|
ScreenshotConfig(psbt_views.PSBTInputOwnershipClaimFailedView),
|
|
ScreenshotConfig(psbt_views.PSBTSeedCannotSignView),
|
|
ScreenshotConfig(psbt_views.PSBTFinalizeView, mock_context_manager=mock_multisig_psbt_loaded),
|
|
#ScreenshotConfig(PSBTSignedQRDisplayViewScreenshotConfig),
|
|
ScreenshotConfig(psbt_views.PSBTSigningErrorView, mock_context_manager=mock_multisig_psbt_loaded),
|
|
],
|
|
"Tools Views": [
|
|
ScreenshotConfig(tools_views.ToolsMenuView),
|
|
#ScreenshotConfig(ToolsImageEntropyLivePreviewView),
|
|
#ScreenshotConfig(ToolsImageEntropyFinalImageView),
|
|
ScreenshotConfig(tools_views.ToolsImageEntropyMnemonicLengthView),
|
|
ScreenshotConfig(tools_views.ToolsDiceEntropyMnemonicLengthView),
|
|
ScreenshotConfig(tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)),
|
|
ScreenshotConfig(tools_views.ToolsCalcFinalWordNumWordsView),
|
|
ScreenshotConfig(tools_views.ToolsCalcFinalWordFinalizePromptView),
|
|
ScreenshotConfig(tools_views.ToolsCalcFinalWordCoinFlipsView),
|
|
ScreenshotConfig(tools_views.ToolsCalcFinalWordShowFinalWordView, screenshot_name="ToolsCalcFinalWordShowFinalWordView_pick_word"),
|
|
ScreenshotConfig(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), screenshot_name="ToolsCalcFinalWordShowFinalWordView_coin_flips"),
|
|
ScreenshotConfig(tools_views.ToolsCalcFinalWordDoneView),
|
|
ScreenshotConfig(tools_views.ToolsAddressExplorerSelectSourceView),
|
|
ScreenshotConfig(tools_views.ToolsAddressExplorerAddressTypeView, mock_context_manager=mock_multisig_wallet_descriptor_loaded),
|
|
ScreenshotConfig(tools_views.ToolsAddressExplorerAddressListView),
|
|
# ScreenshotConfig(tools_views.ToolsAddressExplorerAddressView),
|
|
],
|
|
"Settings Views": settings_views_list + [
|
|
ScreenshotConfig(settings_views.IOTestView),
|
|
ScreenshotConfig(settings_views.DonateView),
|
|
ScreenshotConfig(settings_views.VersionView, mock_context_manager=mock_version_to_most_recent_release),
|
|
ScreenshotConfig(settings_views.VersionView, screenshot_name="VersionView_current_git_state"),
|
|
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
|
|
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
|
|
ScreenshotConfig(settings_views.SettingsSelectionRequiredWarningView, dict(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES)),
|
|
],
|
|
"Misc Error Views": [
|
|
ScreenshotConfig(NotYetImplementedView),
|
|
ScreenshotConfig(UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])),
|
|
ScreenshotConfig(CameraConnectionErrorView),
|
|
ScreenshotConfig(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")),
|
|
ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
|
|
ScreenshotConfig(scan_views.ScanInvalidQRTypeView)
|
|
]
|
|
}
|
|
|
|
return screenshot_sections
|
|
|
|
|
|
def screencap_view(screenshot_config: ScreenshotConfig):
|
|
# Block until we have exclusive access to the screenshot renderer. Without this
|
|
# we were occasionally running into confusing race conditions where the next
|
|
# screenshot would begin rendering over the previous one. Claiming the lock
|
|
# guarantees that the previous screenshot has been fully rendered and saved.
|
|
with screenshot_renderer.lock:
|
|
screenshot_renderer.set_screenshot_filename(f"{screenshot_config.screenshot_name}.png")
|
|
|
|
controller = Controller.get_instance()
|
|
toast_thread = screenshot_config.toast_thread
|
|
try:
|
|
print(f"Running {screenshot_config.screenshot_name}")
|
|
try:
|
|
cur_count = screenshot_renderer.render_count
|
|
|
|
# Activate the (optional) context manager for this screenshot to activate
|
|
# any specialized mocks.
|
|
with screenshot_config.mock_context_manager():
|
|
# Set up and run the target View
|
|
screenshot_config.View_cls(**screenshot_config.view_kwargs).run()
|
|
|
|
if screenshot_renderer.render_count == cur_count:
|
|
# The View didn't actually render anything
|
|
raise Exception(f"{screenshot_config.screenshot_name} did not render a screenshot. Verify that its `run_screen()` is reachable by the screenshot generator.")
|
|
|
|
except ScreenshotComplete:
|
|
# The target View has run and its Screen has rendered what it needs to
|
|
if toast_thread is not None:
|
|
# Now run the Toast so it can render on top of the current image buffer
|
|
controller.activate_toast(toast_thread)
|
|
while controller.toast_notification_thread.is_alive():
|
|
# Give the Toast a moment to complete its work
|
|
time.sleep(0.01)
|
|
|
|
print(f"Completed {screenshot_config.screenshot_name}")
|
|
|
|
except Exception as e:
|
|
# Something else went wrong
|
|
from traceback import print_exc
|
|
print_exc()
|
|
raise e
|
|
finally:
|
|
if toast_thread and toast_thread.is_alive():
|
|
toast_thread.stop()
|
|
toast_thread.join()
|
|
|
|
|
|
# Parse the main `l10n/messages.pot` for overall stats
|
|
messages_source_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "l10n", "messages.pot")
|
|
with open(messages_source_path, 'r') as messages_source_file:
|
|
num_source_messages = messages_source_file.read().count("msgid \"") - 1
|
|
|
|
locale_tuple_list = [locale_tuple for locale_tuple in SettingsConstants.get_detected_languages() if locale_tuple[0] == locale]
|
|
if not locale_tuple_list:
|
|
raise Exception(f"Invalid locale: {locale}")
|
|
|
|
locale, display_name = locale_tuple_list[0]
|
|
|
|
Settings.get_instance().set_value(SettingsConstants.SETTING__LOCALE, value=locale)
|
|
|
|
locale_readme = f"""# SeedSigner Screenshots: {display_name}\n"""
|
|
|
|
# Report the translation progress
|
|
if locale != SettingsConstants.LOCALE__ENGLISH:
|
|
try:
|
|
translated_messages_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "src", "seedsigner", "resources", "seedsigner-translations", "l10n", locale, "LC_MESSAGES", "messages.po")
|
|
with open(translated_messages_path, 'r') as translation_file:
|
|
locale_translations = translation_file.read()
|
|
num_locale_translations = locale_translations.count("msgid \"") - locale_translations.count("""msgstr ""\n\n""") - 1
|
|
|
|
if locale != "en":
|
|
locale_readme += f"## Translation progress: {num_locale_translations / num_source_messages:.1%}\n\n"
|
|
locale_readme += "---\n\n"
|
|
except Exception as e:
|
|
from traceback import print_exc
|
|
print_exc()
|
|
|
|
for section_name, screenshot_list in setup_screenshots(locale).items():
|
|
subdir = section_name.lower().replace(" ", "_")
|
|
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale, subdir))
|
|
locale_readme += "\n\n---\n\n"
|
|
locale_readme += f"## {section_name}\n\n"
|
|
locale_readme += """<table style="border: 0;">"""
|
|
locale_readme += f"""<tr><td align="center">"""
|
|
for screenshot_config in screenshot_list:
|
|
screencap_view(screenshot_config)
|
|
locale_readme += """ <table align="left" style="border: 1px solid gray;">"""
|
|
locale_readme += f"""<tr><td align="center">{screenshot_config.screenshot_name}<br/><br/><img src="{subdir}/{screenshot_config.screenshot_name}.png"></td></tr>"""
|
|
locale_readme += """</table>\n"""
|
|
|
|
locale_readme += "</td></tr></table>"
|
|
|
|
with open(os.path.join(screenshot_root, locale, "README.md"), 'w') as readme_file:
|
|
readme_file.write(locale_readme)
|
|
|
|
print(f"Done with locale: {locale}.")
|
|
|
|
# Write the main README; ensure it writes all locales, not just the one that may
|
|
# have been specified for this run.
|
|
with open(os.path.join("tests", "screenshot_generator", "template.md"), 'r') as readme_template:
|
|
main_readme = readme_template.read()
|
|
|
|
for locale, display_name in SettingsConstants.get_detected_languages():
|
|
main_readme += f"* [{display_name}]({locale}/README.md)\n"
|
|
|
|
with open(os.path.join(screenshot_root, "README.md"), 'w') as readme_file:
|
|
readme_file.write(main_readme)
|
|
|
|
print(f"Screenshots rendered: {screenshot_renderer.render_count}")
|